pax_global_header00006660000000000000000000000064152552110620014511gustar00rootroot0000000000000052 comment=bdafd2e82192bd34ae71dca62f62ae41add4fd3b python-pot-0.9.7+dfsg/000077500000000000000000000000001525521106200145665ustar00rootroot00000000000000python-pot-0.9.7+dfsg/.circleci/000077500000000000000000000000001525521106200164215ustar00rootroot00000000000000python-pot-0.9.7+dfsg/.circleci/artifact_path000066400000000000000000000000351525521106200211530ustar00rootroot000000000000000/docs/build/html/index.html python-pot-0.9.7+dfsg/.circleci/config.yml000066400000000000000000000143641525521106200204210ustar00rootroot00000000000000# Tagging a commit with [circle front] will build the front page and perform test-doc. # Tagging a commit with [circle full] will build everything. version: 2 jobs: build_docs: docker: - image: cimg/python:3.12 resource_class: large steps: - checkout - run: name: Update submodules command: | git submodule update --init --recursive - run: name: Set BASH_ENV command: | echo "set -e" >> $BASH_ENV echo "export DISPLAY=:99" >> $BASH_ENV echo "export OPENBLAS_NUM_THREADS=4" >> $BASH_ENV echo "BASH_ENV:" cat $BASH_ENV - run: name: Merge with upstream command: | echo $(git log -1 --pretty=%B) | tee gitlog.txt echo ${CI_PULL_REQUEST//*pull\//} | tee merge.txt if [[ $(cat merge.txt) != "" ]]; then echo "Merging $(cat merge.txt)"; git remote add upstream https://github.com/PythonOT/POT.git; git pull --ff-only upstream "refs/pull/$(cat merge.txt)/merge"; git fetch upstream master; git submodule update --init --recursive fi # Load our data - restore_cache: keys: - data-cache-0 - pip-cache - run: name: Install ffmpeg command: | sudo apt update sudo apt install ffmpeg - run: name: Get Python running command: | python -m pip install --user --upgrade --progress-bar off pip python -m pip install --user -e . python -m pip install --user --upgrade --progress-bar off -r docs/requirements.txt python -m pip install --user --upgrade --progress-bar off ipython sphinx-gallery memory_profiler # python -m pip install --user --upgrade --progress-bar off ipython "https://api.github.com/repos/sphinx-gallery/sphinx-gallery/zipball/master" memory_profiler - save_cache: key: pip-cache paths: - ~/.cache/pip # Look at what we have and fail early if there is some library conflict - run: name: Check installation command: | which python python -c "import ot" - run: name: Correct link in release file command: | sed -i -r 's/PR #([[:digit:]]*)/\[PR #\1\]\(https:\/\/github.com\/PythonOT\/POT\/pull\/\1\)/' RELEASES.md sed -i -r 's/Issue #([[:digit:]]*)/\[Issue #\1\]\(https:\/\/github.com\/PythonOT\/POT\/issues\/\1\)/' RELEASES.md # Build docs - run: name: make html command: | cd docs; make html; no_output_timeout: 30m # Save the outputs - store_artifacts: path: docs/build/html/ destination: dev - persist_to_workspace: root: docs/build paths: - html deploy_master: docker: - image: circleci/python:3.6-jessie steps: - attach_workspace: at: /tmp/build - run: name: Fetch docs command: | set -e mkdir -p ~/.ssh echo -e "Host *\nStrictHostKeyChecking no" > ~/.ssh/config chmod og= ~/.ssh/config if [ ! -d ~/PythonOT.github.io ]; then git clone git@github.com:/PythonOT/PythonOT.github.io.git ~/PythonOT.github.io --depth=1 fi - run: name: Deploy docs command: | set -e; if [ "${CIRCLE_BRANCH}" == "master" ]; then git config --global user.email "circle@PythonOT.com"; git config --global user.name "Circle CI"; cd ~/PythonOT.github.io; git checkout master git remote -v git fetch origin git reset --hard origin/master git clean -xdf echo "Deploying dev docs for ${CIRCLE_BRANCH}."; cd master cp -a /tmp/build/html/* .; touch .nojekyll; git add -A; git commit -m "CircleCI update of dev docs (${CIRCLE_BUILD_NUM})."; git push origin master; else echo "No deployment (build: ${CIRCLE_BRANCH})."; fi deploy_tag: docker: - image: circleci/python:3.6-jessie steps: - attach_workspace: at: /tmp/build - run: name: Fetch docs command: | set -e mkdir -p ~/.ssh echo -e "Host *\nStrictHostKeyChecking no" > ~/.ssh/config chmod og= ~/.ssh/config if [ ! -d ~/PythonOT.github.io ]; then git clone git@github.com:/PythonOT/PythonOT.github.io.git ~/PythonOT.github.io --depth=1 fi - run: name: Deploy docs command: | set -e; git config --global user.email "circle@PythonOT.com"; git config --global user.name "Circle CI"; cd ~/PythonOT.github.io; git checkout master git remote -v git fetch origin git reset --hard origin/master git clean -xdf echo "Deploying dev docs for ${CIRCLE_BRANCH}."; cp -a /tmp/build/html/* .; touch .nojekyll; git add -A; git commit -m "CircleCI update of dev docs (${CIRCLE_BUILD_NUM})."; git push origin master; workflows: version: 2 default: jobs: - build_docs: filters: tags: only: /[0-9]+(\.[0-9]+)*$/ - deploy_master: requires: - build_docs filters: branches: only: - master - deploy_tag: requires: - build_docs filters: branches: ignore: /.*/ tags: only: /[0-9]+(\.[0-9]+)*$/python-pot-0.9.7+dfsg/.gitattributes000066400000000000000000000001321525521106200174550ustar00rootroot00000000000000 ot/lp/*.cpp linguist-vendored ot/lp/*.h linguist-vendored *.ipynb linguist-documentation python-pot-0.9.7+dfsg/.github/000077500000000000000000000000001525521106200161265ustar00rootroot00000000000000python-pot-0.9.7+dfsg/.github/CODE_OF_CONDUCT.md000066400000000000000000000062041525521106200207270ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ python-pot-0.9.7+dfsg/.github/CONTRIBUTING.md000066400000000000000000000214211525521106200203570ustar00rootroot00000000000000# Contributing to POT First off, thank you for considering contributing to POT. ## How to contribute The preferred workflow for contributing to POT is to fork the [main repository](https://github.com/rflamary/POT) on GitHub, clone, and develop on a branch. Steps: 1. Fork the [project repository](https://github.com/rflamary/POT) by clicking on the 'Fork' button near the top right of the page. This creates a copy of the code under your GitHub user account. For more details on how to fork a repository see [this guide](https://help.github.com/articles/fork-a-repo/). 2. Clone your fork of the POT repo from your GitHub account to your local disk: ```bash $ git clone git@github.com:YourLogin/POT.git $ cd POT ``` 3. Install a recent version of Python. Using an isolated environment such as venv or conda allows you to install a specific version of POT. For instance, for creating a conda environment with python 3.12 and for activating it: ```bash $ conda create -n dev-pot-env python=3.12 $ conda activate dev-pot-env ``` 4. Install pre-commit hooks to ensure that your code is properly formatted: ```bash $ pip install pre-commit $ pre-commit install ``` This will install the pre-commit hooks that will run on every commit. If the hooks fail, the commit will be aborted. 5. Create a `feature` branch to hold your development changes: ```bash $ git checkout -b my-feature ``` Always use a `feature` branch. It's good practice to never work on the `master` branch! 6. Install a compiler with OpenMP support for your platform (see details on the [scikit-learn contributing guide](https://scikit-learn.org/stable/developers/advanced_installation.html#platform-specific-instructions)). For instance, with macOS, Apple clang does not support OpenMP. One can install the LLVM OpenMP library from homebrew: ```bash $ brew install libomp ``` and set environment variables: ```bash $ export CC=/usr/local/opt/llvm/bin/clang $ export CXX=/usr/local/opt/llvm/bin/clang++ ``` 7. Build the projet with pip: ```bash pip install -e . ``` If you want to install all dependencies, you can use ```bash pip install -e .[all] ``` 8. Develop the feature on your feature branch. Add changed files using `git add` and then `git commit` files: ```bash $ git add modified_files $ git commit ``` to record your changes in Git, then push the changes to your GitHub account with: ```bash $ git push -u origin my-feature ``` 9. Follow [these instructions](https://help.github.com/articles/creating-a-pull-request-from-a-fork) to create a pull request from your fork. This will send an email to the committers. (If any of the above seems like magic to you, please look up the [Git documentation](https://git-scm.com/documentation) on the web, or ask a friend or another contributor for help.) ## Pull Request Checklist We recommended that your contribution complies with the following rules before you submit a pull request: * Follow the PEP8 Guidelines which should be handles automatically by pre-commit. * If your pull request addresses an issue, please use the pull request title to describe the issue and mention the issue number in the pull request description. This will make sure a link back to the original issue is created. * All public methods should have informative docstrings with sample usage presented as doctests when appropriate. * Please prefix the title of your pull request with `[MRG]` (Ready for Merge), if the contribution is complete and ready for a detailed review. Two core developers will review your code and change the prefix of the pull request to `[MRG + 1]` and `[MRG + 2]` on approval, making it eligible for merging. An incomplete contribution -- where you expect to do more work before receiving a full review -- should be prefixed `[WIP]` (to indicate a work in progress) and changed to `[MRG]` when it matures. WIPs may be useful to: indicate you are working on something to avoid duplicated work, request broad review of functionality or API, or seek collaborators. WIPs often benefit from the inclusion of a [task list](https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments) in the PR description. * When adding additional functionality, provide at least one example script in the `examples/` folder. Have a look at other examples for reference. Examples should demonstrate why the new functionality is useful in practice and, if possible, compare it to other methods available in POT. * Documentation and high-coverage tests are necessary for enhancements to be accepted. Bug-fixes or new features should be provided with [non-regression tests](https://en.wikipedia.org/wiki/Non-regression_testing). These tests verify the correct behavior of the fix or feature. In this manner, further modifications on the code base are granted to be consistent with the desired behavior. For the Bug-fixes case, at the time of the PR, this tests should fail for the code base in master and pass for the PR code. * At least one paragraph of narrative documentation with links to references in the literature (with PDF links when possible) and the example. You can also check for common programming errors with the following tools: * All lint checks pass. You can run the following command to check: ```bash $ pre-commit run --all-files ``` This will run the pre-commit checks on all files in the repository. * All tests pass. You can run the following command to check: ```bash $ pytest --durations=20 -v test/ --doctest-modules ``` Bonus points for contributions that include a performance analysis with a benchmark script and profiling output (please report on the mailing list or on the GitHub issue). ## Filing bugs We use Github issues to track all bugs and feature requests; feel free to open an issue if you have found a bug or wish to see a feature implemented. It is recommended to check that your issue complies with the following rules before submitting: * Verify that your issue is not being currently addressed by other [issues](https://github.com/rflamary/POT/issues?q=) or [pull requests](https://github.com/rflamary/POT/pulls?q=). * Please ensure all code snippets and error messages are formatted in appropriate code blocks. See [Creating and highlighting code blocks](https://help.github.com/articles/creating-and-highlighting-code-blocks). * Please include your operating system type and version number, as well as your Python, POT, numpy, and scipy versions. This information can be found by running the following code snippet: ```python import platform; print(platform.platform()) import sys; print("Python", sys.version) import numpy; print("NumPy", numpy.__version__) import scipy; print("SciPy", scipy.__version__) import ot; print("POT", ot.__version__) ``` * Please be specific about what estimators and/or functions are involved and the shape of the data, as appropriate; please include a [reproducible](http://stackoverflow.com/help/mcve) code snippet or link to a [gist](https://gist.github.com). If an exception is raised, please provide the traceback. ## New contributor tips A great way to start contributing to POT is to pick an item from the list of [Easy issues](https://github.com/rflamary/POT/issues?labels=Easy) in the issue tracker. Resolving these issues allow you to start contributing to the project without much prior knowledge. Your assistance in this area will be greatly appreciated by the more experienced developers as it helps free up their time to concentrate on other issues. ## Documentation We are glad to accept any sort of documentation: function docstrings, reStructuredText documents (like this one), tutorials, etc. reStructuredText documents live in the source code repository under the doc/ directory. You can edit the documentation using any text editor and then generate the HTML output by typing `make html` from the `docs/` directory. Alternatively, `make` can be used to quickly generate the documentation without the example gallery with `make html-noplot`. The resulting HTML files will be placed in `docs/build/html/` and are viewable in a web browser. For building the documentation, you will need [sphinx](http://sphinx.pocoo.org/), [matplotlib](http://matplotlib.org/), and [pillow](http://pillow.readthedocs.io/en/latest/). When you are writing documentation, it is important to keep a good compromise between mathematical and algorithmic details, and give intuition to the reader on what the algorithm does. It is best to always start with a small paragraph with a hand-waving explanation of what the method does to the data and a figure (coming from an example) illustrating it. This Contribution guide is strongly inspired by the one of the [scikit-learn](https://github.com/scikit-learn/scikit-learn) team. python-pot-0.9.7+dfsg/.github/ISSUE_TEMPLATE/000077500000000000000000000000001525521106200203115ustar00rootroot00000000000000python-pot-0.9.7+dfsg/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000025631525521106200230110ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve POT title: '' labels: bug, help wanted assignees: '' --- ## Describe the bug ### To Reproduce Steps to reproduce the behavior: 1. ... 2. #### Screenshots #### Code sample ### Expected behavior ### Environment (please complete the following information): - OS (e.g. MacOS, Windows, Linux): - Python version: - How was POT installed (source, `pip`, `conda`): - Build command you used (if compiling from source): - Only for GPU related bugs: - CUDA version: - GPU models and configuration: - Any other relevant information: Output of the following code snippet: ```python import platform; print(platform.platform()) import sys; print("Python", sys.version) import numpy; print("NumPy", numpy.__version__) import scipy; print("SciPy", scipy.__version__) import ot; print("POT", ot.__version__) ``` ### Additional context python-pot-0.9.7+dfsg/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000013531525521106200240400ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement, feature request assignees: '' --- ## 🚀 Feature ### Motivation ### Pitch ### Alternatives ### Additional context python-pot-0.9.7+dfsg/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000024021525521106200217250ustar00rootroot00000000000000## Types of changes ## Motivation and context / Related issue ## How has this been tested (if it applies) ## PR checklist - [ ] I have read the [**CONTRIBUTING**](https://pythonot.github.io/contributing.html) document. - [ ] The documentation is up-to-date with the changes I made (check build artifacts). - [ ] All tests passed, and additional code has been **covered with new tests**. - [ ] I have added the PR and Issue fix to the [**RELEASES.md**](RELEASES.md) file. python-pot-0.9.7+dfsg/.github/labeler.yml000066400000000000000000000034511525521106200202620ustar00rootroot00000000000000 Documentation: - changed-files: - any-glob-to-any-file: docs/** Examples: - changed-files: - any-glob-to-any-file: examples/** Tests: - changed-files: - any-glob-to-any-file: test/** CI: - changed-files: - any-glob-to-any-file: .github/workflows/** - any-glob-to-any-file: .circleci/** ot.bregman: - changed-files: - any-glob-to-any-file: ot/bregman/** ot.gromov: - changed-files: - any-glob-to-any-file: ot/gromov/** ot.unbalanced: - changed-files: - any-glob-to-any-file: ot/unbalanced/** ot.lp: - changed-files: - any-glob-to-any-file: ot/lp/** ot.bsp: - changed-files: - any-glob-to-any-file: ot/bsp/** ot.utils: - changed-files: - any-glob-to-any-file: ot/utils.py ot.backend: - changed-files: - any-glob-to-any-file: ot/backend.py ot.coot: - changed-files: - any-glob-to-any-file: ot/coot.py ot.optim: - changed-files: - any-glob-to-any-file: ot/optim.py ot.plot: - changed-files: - any-glob-to-any-file: ot/plot.py ot.factored: - changed-files: - any-glob-to-any-file: ot/factored.py ot.gaussian: - changed-files: - any-glob-to-any-file: ot/gaussian.py ot.gmm: - changed-files: - any-glob-to-any-file: ot/gmm.py ot.lowrank: - changed-files: - any-glob-to-any-file: ot/lowrank.py ot.sliced: - changed-files: - any-glob-to-any-file: ot/sliced/** ot.solvers: - changed-files: - any-glob-to-any-file: ot/solvers/** ot.batch: - changed-files: - any-glob-to-any-file: ot/batch/** ot.partial: - changed-files: - any-glob-to-any-file: ot/partial/** ot.smooth: - changed-files: - any-glob-to-any-file: ot/smooth.py ot.weak: - changed-files: - any-glob-to-any-file: ot/weak.py ot.dr: - changed-files: - any-glob-to-any-file: ot/dr.py ot.gnn: - changed-files: - any-glob-to-any-file: ot/gnn/** ot.sgot: - changed-files: - any-glob-to-any-file: ot/sgot.pypython-pot-0.9.7+dfsg/.github/requirements_doctests.txt000066400000000000000000000002671525521106200233270ustar00rootroot00000000000000numpy>=1.20 scipy>=1.6 matplotlib autograd pymanopt cvxopt scikit-learn torch<2.12 jax jaxlib tensorflow; python_version < '3.14' pytest torch_geometric cvxpy geomloss>=0.3.1 pykeops python-pot-0.9.7+dfsg/.github/requirements_no_backend.txt000066400000000000000000000001251525521106200235530ustar00rootroot00000000000000numpy>=1.20 scipy>=1.6 matplotlib autograd pymanopt cvxopt scikit-learn pytest cvxpy python-pot-0.9.7+dfsg/.github/requirements_strict.txt000066400000000000000000000000371525521106200230020ustar00rootroot00000000000000numpy scipy>=1.3 cython pytest python-pot-0.9.7+dfsg/.github/requirements_test_windows.txt000066400000000000000000000001171525521106200242220ustar00rootroot00000000000000numpy scipy>=1.3 cython matplotlib autograd pymanopt cvxopt scikit-learn pytestpython-pot-0.9.7+dfsg/.github/workflows/000077500000000000000000000000001525521106200201635ustar00rootroot00000000000000python-pot-0.9.7+dfsg/.github/workflows/build_doc.yml000066400000000000000000000024671525521106200226430ustar00rootroot00000000000000name: Build doc on: workflow_dispatch: pull_request: push: branches: - 'master' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: submodules: true # Standard drop-in approach that should work for most people. - name: Free Disk Space (Ubuntu) uses: insightsengineering/disk-space-reclaimer@v1 with: android: true dotnet: true - name: Set up Python 3.10 uses: actions/setup-python@v5 with: python-version: "3.10" cache: 'pip' - name: Get Python running run: | python -m pip install --user --upgrade --progress-bar off pip python -m pip install --user --upgrade --progress-bar off -r docs/requirements.txt python -m pip install --user --upgrade --progress-bar off ipython sphinx-gallery memory_profiler python -m pip install -v --user -e . # Look at what we have and fail early if there is some library conflict - name: Check installation run: | which python python -c "import ot" # Build docs - name: Generate HTML docs uses: rickstaa/sphinx-action@master with: docs-folder: "docs/" - uses: actions/upload-artifact@v7 with: name: Documentation path: docs/build/html/ python-pot-0.9.7+dfsg/.github/workflows/build_tests.yml000066400000000000000000000223011525521106200232250ustar00rootroot00000000000000name: Tests on: workflow_dispatch: pull_request: branches: - 'master' push: branches: - 'master' create: branches: - 'master' tags: - '**' env: PY_VERSION: "3.13" jobs: Lint: runs-on: ubuntu-latest strategy: fail-fast: false defaults: run: shell: bash -l {0} steps: - name: Checking Out Repository uses: actions/checkout@v7 with: submodules: true # Install Python & Packages - uses: actions/setup-python@v6 with: python-version: ${{ env.PY_VERSION }} - run: which python - name: Lint with pre-commit run: | pip install pre-commit pre-commit install --install-hooks pre-commit run --all-files build_from_source: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: submodules: true - name: Set up Python uses: actions/setup-python@v6 with: python-version: ${{ env.PY_VERSION }} - name: Build from source run: | python -m pip install --upgrade pip setuptools wheel python -m pip install cython numpy python setup.py sdist bdist_wheel pip install dist/*.tar.gz doctests: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, 'no ci')" steps: - name: Free Disk Space (Ubuntu) uses: insightsengineering/disk-space-reclaimer@v1 with: android: true dotnet: true - uses: actions/checkout@v7 with: submodules: true - name: Set up Python uses: actions/setup-python@v6 with: python-version: ${{ env.PY_VERSION }} cache: 'pip' - name: Install dependencies run: | python -m pip install --upgrade pip setuptools pip install -r .github/requirements_doctests.txt pip install pytest pytest-cov - name: Install POT run: | pip install -e . - name: Run tests run: | python -m pytest -v ot/ test/conftest.py --doctest-modules --color=yes --cov=./ --cov-report=xml linux-minimal: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, 'no ci')" steps: - uses: actions/checkout@v7 with: submodules: true - name: Set up Python uses: actions/setup-python@v6 with: python-version: ${{ env.PY_VERSION }} cache: 'pip' - name: Install dependencies run: | python -m pip install --upgrade pip setuptools pip install pytest pytest-cov - name: Install POT run: | pip install -e . - name: Run tests run: | python -m pytest --durations=20 -v test/ ot/ --color=yes --cov=./ --cov-report=xml linux: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, 'no ci')" strategy: max-parallel: 4 matrix: python-version: ["3.11", "3.12", "3.13", "3.14"] steps: # - name: Free Disk Space (Ubuntu) # uses: insightsengineering/disk-space-reclaimer@v1 # with: # android: true # dotnet: true - uses: actions/checkout@v7 with: submodules: true - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} cache: 'pip' - name: Install POT run: | pip install -e . - name: Install dependencies run: | python -m pip install --upgrade pip setuptools pip install -r .github/requirements_no_backend.txt pip install pytest pytest-cov - name: test POT import run: | python -c "import ot; print(ot.__version__)" - name: Run tests run: | python -m pytest --durations=20 -v test/ --color=yes --cov=./ --cov-report=xml - name: Upload coverage reports to Codecov with GitHub Action uses: codecov/codecov-action@v4 linux-torch: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, 'no ci')" steps: - uses: actions/checkout@v7 with: submodules: true - name: Set up Python ${{ env.PY_VERSION }} uses: actions/setup-python@v6 with: python-version: ${{ env.PY_VERSION }} cache: 'pip' - name: Install POT run: | pip install -e . - name: Install dependencies run: | python -m pip install --upgrade pip setuptools pip install torch torch_geometric geomloss pykeops scikit-learn pip install pytest pytest-cov - name: Run tests run: | python -m pytest --durations=20 -v test/ --color=yes --cov=./ --cov-report=xml - name: Upload coverage reports to Codecov with GitHub Action uses: codecov/codecov-action@v4 linux-jax: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, 'no ci')" steps: - uses: actions/checkout@v7 with: submodules: true - name: Set up Python ${{ env.PY_VERSION }} uses: actions/setup-python@v6 with: python-version: ${{ env.PY_VERSION }} cache: 'pip' - name: Install POT run: | pip install -e . - name: Install dependencies run: | python -m pip install --upgrade pip setuptools pip install jax jaxlib pip install pytest pytest-cov - name: Run tests run: | python -m pytest --durations=20 -v test/ --color=yes --cov=./ --cov-report=xml - name: Upload coverage reports to Codecov with GitHub Action uses: codecov/codecov-action@v4 linux-tf: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, 'no ci')" steps: - uses: actions/checkout@v7 with: submodules: true - name: Set up Python ${{ env.PY_VERSION }} uses: actions/setup-python@v6 with: python-version: ${{ env.PY_VERSION }} cache: 'pip' - name: Install POT run: | pip install -e . - name: Install dependencies run: | python -m pip install --upgrade pip setuptools pip install tensorflow pip install pytest pytest-cov - name: Run tests run: | python -m pytest --durations=20 -v test/ --color=yes --cov=./ --cov-report=xml - name: Upload coverage reports to Codecov with GitHub Action uses: codecov/codecov-action@v4 # linux-minimal-deps-ft: # runs-on: ubuntu-latest # if: "!contains(github.event.head_commit.message, 'no ci')" # steps: # - uses: actions/checkout@v4 # with: # submodules: true # - name: Set up Python # uses: actions/setup-python@v5 # with: # python-version: "3.14t" # cache: 'pip' # - name: Install dependencies # run: | # python -m pip install --upgrade pip setuptools # pip install pytest pytest-cov # - name: Install POT # run: | # pip install -e . # - name: Run tests # run: | # python -m pytest --durations=20 -v test/ ot/ --color=yes --cov=./ --cov-report=xml macos: runs-on: macos-latest if: "!contains(github.event.head_commit.message, 'no ci')" steps: - uses: actions/checkout@v7 with: submodules: true - name: Set up Python ${{ env.PY_VERSION }} uses: actions/setup-python@v6 with: python-version: ${{ env.PY_VERSION }} cache: 'pip' - name: Install POT run: | pip install -e . - name: Install dependencies run: | python -m pip install --upgrade pip setuptools pip install -r .github/requirements_no_backend.txt pip install pytest - name: Run tests run: | python -m pytest --durations=20 -v test/ ot/ --color=yes windows: runs-on: windows-latest if: "!contains(github.event.head_commit.message, 'no ci')" steps: - uses: actions/checkout@v7 with: submodules: true - name: Set up Python ${{ env.PY_VERSION }} uses: actions/setup-python@v6 with: python-version: ${{ env.PY_VERSION }} cache: 'pip' - name: RC.exe run: | function Invoke-VSDevEnvironment { $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" $installationPath = & $vswhere -prerelease -legacy -latest -property installationPath $Command = Join-Path $installationPath "Common7\Tools\vsdevcmd.bat" & "${env:COMSPEC}" /s /c "`"$Command`" -no_logo && set" | Foreach-Object { if ($_ -match '^([^=]+)=(.*)') { [System.Environment]::SetEnvironmentVariable($matches[1], $matches[2]) } } } Invoke-VSDevEnvironment Get-Command rc.exe | Format-Table -AutoSize - name: Update pip run: | python -m pip install --upgrade pip setuptools python -m pip install cython - name: Install POT run: | python -m pip install -e . - name: Install dependencies run: | python -m pip install -r .github/requirements_test_windows.txt python -m pip3 install torch torchvision torchaudio python -m pip install pytest - name: Run tests run: | python -m pytest --durations=20 -v test/ ot/ --color=yes python-pot-0.9.7+dfsg/.github/workflows/build_wheels.yml000066400000000000000000000020201525521106200233460ustar00rootroot00000000000000name: Build wheels on: workflow_dispatch: release: pull_request: jobs: build_wheels: name: ${{ matrix.os }} runs-on: ${{ matrix.os }} if: "contains(github.event.head_commit.message, 'build wheels')" strategy: matrix: os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest] steps: - uses: actions/checkout@v7 with: submodules: true - name: Set up Python 3.11 uses: actions/setup-python@v6 with: python-version: "3.11" - name: Install dependencies run: | python -m pip install --upgrade pip setuptools - name: Install cibuildwheel run: | python -m pip install cibuildwheel==3.1.4 - name: Build wheels env: CIBW_SKIP: "cp38* cp*musl* *i686 *win32" # remove pypy on mac and win (wrong version) run: | python -m cibuildwheel --output-dir wheelhouse - uses: actions/upload-artifact@v4 with: name: wheels-${{ strategy.job-index }} path: ./wheelhouse python-pot-0.9.7+dfsg/.github/workflows/build_wheels_weekly.yml000066400000000000000000000020621525521106200247340ustar00rootroot00000000000000name: Build all wheels on: workflow_dispatch: schedule: - cron: '30 0 * * 1' push: branches: - "master" jobs: build_wheels: name: ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest] steps: - uses: actions/checkout@v4 with: submodules: true - name: Set up Python 3.11 uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies run: | python -m pip install --upgrade pip setuptools - name: Install cibuildwheel run: | python -m pip install cibuildwheel==3.1.4 - name: Build wheels env: CIBW_SKIP: "cp*musl* cp38* *i686 *win32" # remove pypy on mac and win (wrong version) CIBW_ARCHS_MACOS: x86_64 universal2 arm64 run: | python -m cibuildwheel --output-dir wheelhouse - uses: actions/upload-artifact@v4 with: name: wheels-${{ strategy.job-index }} path: ./wheelhouse python-pot-0.9.7+dfsg/.github/workflows/check_release.yml000066400000000000000000000010071525521106200234610ustar00rootroot00000000000000name: "Check updated RELEASES.md file" on: pull_request: branches: - 'master' jobs: check_release_file: permissions: contents: read pull-requests: write runs-on: ubuntu-latest steps: - name: Verify Changed files uses: tj-actions/changed-files@v45 id: changed-release-file with: files: 'RELEASES.md' - name: Check if RELEASES.md file is changed if: steps.changed-release-file.outputs.any_changed == 'false' run: exit 1 python-pot-0.9.7+dfsg/.github/workflows/circleci-redirector.yml000066400000000000000000000010121525521106200246150ustar00rootroot00000000000000name: circleci-redirector on: [status] jobs: circleci_artifacts_redirector_job: runs-on: ubuntu-latest if: "${{ github.event.context == 'ci/circleci: build_docs' }}" name: Run CircleCI artifacts redirector steps: - name: GitHub Action step uses: larsoner/circleci-artifacts-redirector-action@master with: api-token: ${{ secrets.CIRCLE_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }} artifact-path: 0/dev/index.html circleci-jobs: build_docs python-pot-0.9.7+dfsg/.github/workflows/labeler.yml000066400000000000000000000004031525521106200223110ustar00rootroot00000000000000name: "Pull Request Labeler" on: pull_request_target: branches: - 'master' jobs: labeler: permissions: contents: read pull-requests: write issues: write runs-on: ubuntu-latest steps: - uses: actions/labeler@v5 python-pot-0.9.7+dfsg/.gitignore000066400000000000000000000027711525521106200165650ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class .spyproject # sphinx-gallery temp files docs/source/auto_examples/*.pickle docs/source/auto_examples/*.md5 docs/auto_examples/ docs/modules/ # C extensions *.so # Cython output ot/lp/emd_wrap.cpp ot/partial/partial_cython.cpp ot/bsp/bsp_wrap.cpp # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # env pythonenv3.8/ # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # xml *.xml # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ .venv/ # Spyder project settings .spyderproject # Rope project settings .ropeproject # Mac stuff .DS_Store # coverage output folder cov_html/ docs/source/modules/generated/* docs/source/_build/* # local debug folder debug # vscode parameters .vscode # pytest cahche .pytest_cache docs/source/ python-pot-0.9.7+dfsg/.gitmodules000066400000000000000000000001471525521106200167450ustar00rootroot00000000000000[submodule "deps/eigen"] path = deps/eigen url = https://gitlab.com/libeigen/eigen.git branch = 3.4 python-pot-0.9.7+dfsg/.mailmap000066400000000000000000000005111525521106200162040ustar00rootroot00000000000000Nicolas Courty Nicolas Courty Nicolas Courty ncourty Nicolas Courty Nicolas Courty Léo Gautheron Leo gautheron python-pot-0.9.7+dfsg/.pre-commit-config.yaml000066400000000000000000000025041525521106200210500ustar00rootroot00000000000000repos: # Ruff skada - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.5.2 hooks: - id: ruff name: ruff lint args: ["--fix"] files: ^ot/ - id: ruff name: ruff lint preview args: ["--fix", "--preview", "--select=NPY201"] files: ^ot/ - id: ruff name: ruff lint doc, tutorials, tests and examples # D103: missing docstring in public function # D400: docstring first line must end with period args: ["--ignore=D103,D400", "--fix"] files: ^docs/|^examples/^test/ - id: ruff-format files: ^ot/|^docs/|^examples/| # Codespell - repo: https://github.com/codespell-project/codespell rev: v2.2.6 hooks: - id: codespell additional_dependencies: - tomli files: ^ot/|^docs/|^examples/ types_or: [python, bib, rst, inc] args: [ "--ignore-words", "ignore-words.txt", ] # yamllint - repo: https://github.com/adrienverge/yamllint.git rev: v1.35.1 hooks: - id: yamllint # args: [--strict] # # rstcheck # - repo: https://github.com/rstcheck/rstcheck.git # rev: v6.2.0 # hooks: # - id: rstcheck # additional_dependencies: # - tomli # files: ^docs/source/.*\.(rst|inc)$ python-pot-0.9.7+dfsg/.yamllint.yml000066400000000000000000000002471525521106200172230ustar00rootroot00000000000000extends: default ignore: | .circleci/config.yml codecov.yml .github/labeler.yml rules: line-length: disable document-start: disable indentation: disable python-pot-0.9.7+dfsg/CITATION.cff000066400000000000000000000061041525521106200164610ustar00rootroot00000000000000# CFF file for POT contributors cff-version: 1.2.0 title: POT Python Optimal Transport message: >- If you use this software, please cite it using the metadata from this file. type: software authors: - given-names: Rémi family-names: Flamary affiliation: École Polytechnique orcid: 'https://orcid.org/0000-0002-4212-6627' - given-names: Cédric family-names: Vincent-Cuaz affiliation: EPFL - given-names: Nicolas family-names: Courty affiliation: Université Bretagne Sud - given-names: Alexandre family-names: Gramfort affiliation: INRIA - given-names: Oleksii family-names: Kachaiev affiliation: Università degli Studi di Genova - given-names: Huy family-names: Quang Tran affiliation: Université Bretagne Sud - given-names: Laurène family-names: David affiliation: Institut Polytechnique de Paris - given-names: Clément family-names: Bonet affiliation: ENSAE - CREST - given-names: Nathan family-names: Cassereau affiliation: IDRIS-CNRS - given-names: Théo family-names: Gnassounou affiliation: INRIA - given-names: Eloi family-names: Tanguy affiliation: Université Paris-Cité - given-names: Julie family-names: Delon affiliation: Université Paris-Cité - given-names: Antoine family-names: Collas affiliation: INRIA - given-names: Sonia family-names: Mazelet affiliation: Ecole Polytechnique - given-names: Laetitia family-names: Chapel affiliation: Institut Agro Rennes-Angers, IRISA - given-names: Tanguy family-names: Kerdoncuff affiliation: Université de Lyon - given-names: Xizheng family-names: Yu affiliation: Brown University - given-names: Matthew family-names: Feickert affiliation: University of Wisconsin-Madison - given-names: Paul family-names: Krzakala affiliation: Telecom Paris - given-names: Tianlin family-names: Liu affiliation: University of Basel - given-names: Eduardo family-names: Fernandes Montesuma affiliation: Université Paris-Saclay & CEA-List orcid: 'https://orcid.org/0000-0003-3850-4602' - given-names: Nathan family-names: Neike affiliation: Hi! PARIS - given-names: Baptiste family-names: Genest affiliation: CNRS, LIRIS - given-names: David family-names: Coeurjolly affiliation: CNRS, LIRIS - given-names: Thibaut family-names: Germain affiliation: Ecole Polytechnique - given-names: Sienna family-names: O'Shea affiliation: Ecole Polytechnique - given-names: Marco family-names: Corneli affiliation: Université Côte d'Azur - given-names: Ferdinand family-names: Genans affiliation: Sorbonne Université, LPSM, CNRS identifiers: - type: url value: 'https://github.com/PythonOT/POT' description: Code - type: doi value: 10.5281/zenodo.17161062 repository-code: 'https://github.com/PythonOT/POT' url: 'https://pythonot.github.io/' keywords: - optimal transport - python - sinkhorn - wasserstein - gromov-wasserstein license: MIT version: 0.9.7 date-released: 2026-07-10 python-pot-0.9.7+dfsg/CONTRIBUTORS.md000066400000000000000000000110051525521106200170420ustar00rootroot00000000000000 ## Creators and Maintainers This toolbox has been created by [Rémi Flamary](https://remi.flamary.com/) and [Nicolas Courty](http://people.irisa.fr/Nicolas.Courty/). It is currently maintained by : * [Rémi Flamary](https://remi.flamary.com/) * [Cédric Vincent-Cuaz](https://cedricvincentcuaz.github.io/) ## Contributors The contributors to this library are: * [Rémi Flamary](http://remi.flamary.com/) (EMD wrapper, Pytorch backend, DA classes, conditional gradients, WDA, weak OT, linear OT mapping, documentation) * [Nicolas Courty](http://people.irisa.fr/Nicolas.Courty/) (Original sinkhorn, Wasserstein barycenters and convolutional barycenters, 1D wasserstein) * [Alexandre Gramfort](http://alexandre.gramfort.net/) (CI, documentation) * [Laetitia Chapel](http://people.irisa.fr/Laetitia.Chapel/) (Partial OT, Unbalanced OT non-regularized) * [Michael Perrot](http://perso.univ-st-etienne.fr/pem82055/) (Mapping estimation) * [Léo Gautheron](https://github.com/aje) (Initial GPU implementation) * [Nathalie Gayraud](https://www.linkedin.com/in/nathalie-t-h-gayraud/?ppe=1) (DA classes) * [Stanislas Chambon](https://slasnista.github.io/) (DA classes) * [Antoine Rolet](https://arolet.github.io/) (EMD solver debug) * Erwan Vautier (Gromov-Wasserstein) * [Kilian Fatras](https://kilianfatras.github.io/) (Stochastic solvers, empirical sinkhorn) * [Alain Rakotomamonjy](https://sites.google.com/site/alainrakotomamonjy/home) (Greenkhorn) * [Vayer Titouan](https://tvayer.github.io/) (Gromov-Wasserstein, Fused-Gromov-Wasserstein) * [Hicham Janati](https://hichamjanati.github.io/) (Unbalanced OT, Debiased barycenters) * [Romain Tavenard](https://rtavenar.github.io/) (1D Wasserstein) * [Mokhtar Z. Alaya](http://mzalaya.github.io/) (Screenkhorn) * [Ievgen Redko](https://ievred.github.io/) (Laplacian DA, JCPOT) * [Adrien Corenflos](https://adriencorenflos.github.io/) (Sliced Wasserstein Distance) * [Tanguy Kerdoncuff](https://hv0nnus.github.io/) (Sampled Gromov Wasserstein) * [Minhui Huang](https://mhhuang95.github.io) (Projection Robust Wasserstein Distance) * [Nathan Cassereau](https://github.com/ncassereau-idris) (Backends) * [Cédric Vincent-Cuaz](https://github.com/cedricvincentcuaz) (Graph Dictionary Learning, FGW, semi-relaxed FGW, quantized FGW, partial FGW) * [Eloi Tanguy](https://github.com/eloitanguy) (Generalized Wasserstein Barycenters, GMMOT, Barycenters for General Transport Costs) * [Camille Le Coz](https://www.linkedin.com/in/camille-le-coz-8593b91a1/) (EMD2 debug) * [Eduardo Fernandes Montesuma](https://eddardd.github.io/my-personal-blog/) (Free support sinkhorn barycenter) * [Theo Gnassounou](https://github.com/tgnassou) (OT between Gaussian distributions) * [Clément Bonet](https://clbonet.github.io) (Wasserstein on circle, Spherical Sliced-Wasserstein) * [Ronak Mehta](https://ronakrm.github.io) (Efficient Discrete Multi Marginal Optimal Transport Regularization) * [Xizheng Yu](https://github.com/x12hengyu) (Efficient Discrete Multi Marginal Optimal Transport Regularization) * [Sonia Mazelet](https://github.com/SoniaMaz8) (Template based GNN layers) * [Laurène David](https://github.com/laudavid) (Low rank sinkhorn, Low rank Gromov-Wasserstein samples) * [Julie Delon](https://judelo.github.io/) (GMM OT) * [Samuel Boïté](https://samuelbx.github.io/) (GMM OT) * [Nathan Neike](https://github.com/nathanneike) (Sparse EMD solver) * [Thibaut Germain](https://thibaut-germain.github.io) (SGOT) * Sienna O'Shea (SGOT) ## Acknowledgments This toolbox benefit a lot from open source research and we would like to thank the following persons for providing some code (in various languages): * [Gabriel Peyré](http://gpeyre.github.io/) (Wasserstein Barycenters in Matlab) * [Mathieu Blondel](https://mblondel.org/) (original implementation smooth OT) * [Nicolas Bonneel](http://liris.cnrs.fr/~nbonneel/) (C++ code for EMD) * [Marco Cuturi](http://marcocuturi.net/) (Sinkhorn Knopp in Matlab/Cuda) POT has benefited from the financing or manpower from the following partners: ANRCNRS3IAHi!PARISpython-pot-0.9.7+dfsg/LICENSE000066400000000000000000000020661525521106200155770ustar00rootroot00000000000000MIT License Copyright (c) 2016-2023 POT contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-pot-0.9.7+dfsg/MANIFEST.in000066400000000000000000000010621525521106200163230ustar00rootroot00000000000000include README.md include RELEASES.md include LICENSE include requirements_all.txt include ot/lp/core.h include ot/lp/EMD.h include ot/lp/EMD_wrapper.cpp include ot/lp/emd_wrap.pyx include ot/lp/full_bipartitegraph.h include ot/lp/full_bipartitegraph_omp.h include ot/lp/network_simplex_simple.h include ot/lp/network_simplex_simple_omp.h include ot/lp/sparse_bipartitegraph.h include ot/partial/partial_cython.pyx include ot/bsp/BSP-OT_header_only.h include ot/bsp/bsp_wrapper.cpp include ot/bsp/bsp_wrapper.h include ot/bsp/bsp_wrap.pyx graft deps/eigen/Eigen python-pot-0.9.7+dfsg/Makefile000066400000000000000000000050301525521106200162240ustar00rootroot00000000000000 PYTHON=python3 branch := $(shell git symbolic-ref --short -q HEAD) help : @echo "The following make targets are available:" @echo " help - print this message" @echo " build - build python package" @echo " install - install python package (local user)" @echo " sinstall - install python package (system with sudo)" @echo " remove - remove the package (local user)" @echo " sremove - remove the package (system with sudo)" @echo " clean - remove any temporary files" @echo " notebook - launch ipython notebook" build : $(PYTHON) setup.py build buildext : $(PYTHON) setup.py build_ext --inplace install : $(PYTHON) setup.py install --user sinstall : sudo $(PYTHON) setup.py install remove : $(PYTHON) setup.py install --user --record files.txt tr '\n' '\0' < files.txt | xargs -0 rm -f -- rm files.txt sremove : $(PYTHON) setup.py install --record files.txt tr '\n' '\0' < files.txt | sudo xargs -0 rm -f -- rm files.txt clean : FORCE $(PYTHON) setup.py clean pep8 : flake8 examples/ ot/ test/ --count --max-line-length=127 --statistics --show-source test : FORCE pep8 $(PYTHON) -m pytest --durations=20 -v test/ --doctest-modules --ignore ot/gpu/ pytest : FORCE $(PYTHON) -m pytest --durations=20 -v test/ --doctest-modules --ignore ot/gpu/ release : twine upload dist/* release_test : twine upload --repository-url https://test.pypi.org/legacy/ dist/* rdoc : pandoc --from=markdown --to=rst --output=docs/source/readme.rst README.md sed -i 's,https://pythonot.github.io/auto_examples/,auto_examples/,g' docs/source/readme.rst pandoc --from=markdown --to=rst --output=docs/source/releases.rst RELEASES.md sed -i 's,https://pot.readthedocs.io/en/latest/,,g' docs/source/releases.rst sed -i 's,https://github.com/rflamary/POT/blob/master/notebooks/,auto_examples/,g' docs/source/releases.rst sed -i 's,.ipynb,.html,g' docs/source/releases.rst sed -i 's,https://pythonot.github.io/auto_examples/,auto_examples/,g' docs/source/releases.rst notebook : ipython notebook --matplotlib=inline --notebook-dir=notebooks/ bench : @git stash >/dev/null 2>&1 @echo 'Branch master' @git checkout master >/dev/null 2>&1 python3 $(script) @echo 'Branch $(branch)' @git checkout $(branch) >/dev/null 2>&1 python3 $(script) @git stash apply >/dev/null 2>&1 autopep8 : autopep8 -ir test ot examples --jobs -1 aautopep8 : autopep8 -air test ot examples --jobs -1 wheels : cibuildwheel --platform linux --output-dir dist dist : wheels $(PYTHON) setup.py sdist pydocstyle : pydocstyle ot FORCE : python-pot-0.9.7+dfsg/README.md000066400000000000000000001166211525521106200160540ustar00rootroot00000000000000# POT: Python Optimal Transport [![PyPI version](https://badge.fury.io/py/POT.svg)](https://badge.fury.io/py/POT) [![Anaconda Cloud](https://anaconda.org/conda-forge/pot/badges/version.svg)](https://anaconda.org/conda-forge/pot) [![Build Status](https://github.com/PythonOT/POT/actions/workflows/build_tests.yml/badge.svg)](https://github.com/PythonOT/POT/actions) [![Codecov Status](https://codecov.io/gh/PythonOT/POT/branch/master/graph/badge.svg)](https://codecov.io/gh/PythonOT/POT) [![Downloads](https://static.pepy.tech/badge/pot)](https://pepy.tech/project/pot) [![Anaconda downloads](https://anaconda.org/conda-forge/pot/badges/downloads.svg)](https://anaconda.org/conda-forge/pot) [![License](https://anaconda.org/conda-forge/pot/badges/license.svg)](https://github.com/PythonOT/POT/blob/master/LICENSE) This open source Python library provides several solvers for optimization problems related to Optimal Transport for signal, image processing and machine learning. Website and documentation: Source Code (MIT): POT has the following main features: * A large set of differentiable solvers for optimal transport problems, including: * Exact linear OT, entropic and quadratic regularized OT, * Gromov-Wasserstein (GW) distances, Fused GW distances and variants of quadratic OT, * Unbalanced and partial OT for different divergences, * OT barycenters (Wasserstein and GW) for fixed and free support, * Fast OT solvers in 1D, on the circle and between Gaussian Mixture Models (GMMs), * Many ML related solvers, such as domain adaptation, optimal transport mapping estimation, subspace learning, Graph Neural Networks (GNNs) layers. * Several backends for easy use with Pytorch, Jax, Tensorflow, Numpy and Cupy arrays. ### Implemented Features POT provides the following generic OT solvers: * [OT Network Simplex solver](https://pythonot.github.io/auto_examples/plot_OT_1D.html) for the linear program/ Earth Movers Distance \[1] . * [Conditional gradient](https://pythonot.github.io/auto_examples/plot_optim_OTreg.html) \[6] and [Generalized conditional gradient](https://pythonot.github.io/auto_examples/plot_optim_OTreg.html) for regularized OT \[7]. * Entropic regularization OT solver with [Sinkhorn Knopp Algorithm](https://pythonot.github.io/auto_examples/plot_OT_1D.html) \[2] , stabilized version \[9] \[10] \[34], lazy CPU/GPU solver from geomloss \[60] \[61], greedy Sinkhorn \[22] and Screening Sinkhorn \[26]. * Bregman projections for [Wasserstein barycenter](https://pythonot.github.io/auto_examples/barycenters/plot_barycenter_lp_vs_entropic.html) \[3], [convolutional barycenter](https://pythonot.github.io/auto_examples/barycenters/plot_convolutional_barycenter.html) \[21] and unmixing \[4]. * Sinkhorn divergence \[23] and entropic regularization OT from empirical data. * Debiased Sinkhorn barycenters [Sinkhorn divergence barycenter](https://pythonot.github.io/auto_examples/barycenters/plot_debiased_barycenter.html) \[37] * Smooth optimal transport solvers (dual and semi-dual) for KL and squared L2 regularizations \[17]. * Weak OT solver between empirical distributions \[39] * Non regularized [Wasserstein barycenters \[16\] ](https://pythonot.github.io/auto_examples/barycenters/plot_barycenter_lp_vs_entropic.html) with LP solver (only small scale). * [Gromov-Wasserstein distances](https://pythonot.github.io/auto_examples/gromov/plot_gromov.html) and [GW barycenters](https://pythonot.github.io/auto_examples/gromov/plot_gromov_barycenter.html) (exact \[13] and regularized \[12,51]), differentiable using gradients from Graph Dictionary Learning \[38] * [Fused-Gromov-Wasserstein distances solver](https://pythonot.github.io/auto_examples/gromov/plot_fgw.html#sphx-glr-auto-examples-plot-fgw-py) and [FGW barycenters](https://pythonot.github.io/auto_examples/gromov/plot_barycenter_fgw.html) (exact \[24] and regularized \[12,51]). * [Stochastic solver](https://pythonot.github.io/auto_examples/others/plot_stochastic.html) and [differentiable losses](https://pythonot.github.io/auto_examples/backends/plot_stoch_continuous_ot_pytorch.html) for Large-scale Optimal Transport (semi-dual problem [18] and dual problem [19]) * [Sampled solver of Gromov Wasserstein](https://pythonot.github.io/auto_examples/gromov/plot_gromov.html) for large-scale problem with any loss functions [33] * Non regularized [free support Wasserstein barycenters](https://pythonot.github.io/auto_examples/barycenters/plot_free_support_barycenter.html) [20]. * [One dimensional Unbalanced OT](https://pythonot.github.io/auto_examples/unbalanced-partial/plot_UOT_1D.html) with KL relaxation [73] and [barycenter](https://pythonot.github.io/auto_examples/unbalanced-partial/plot_UOT_barycenter_1D.html) [10, 25]. Also [exact unbalanced OT](https://pythonot.github.io/auto_examples/unbalanced-partial/plot_unbalanced_ot.html) with KL and quadratic regularization and the [regularization path of UOT](https://pythonot.github.io/auto_examples/unbalanced-partial/plot_regpath.html) [41] * [Partial Wasserstein and Gromov-Wasserstein](https://pythonot.github.io/auto_examples/unbalanced-partial/plot_partial_wass_and_gromov.html) and [Partial Fused Gromov-Wasserstein](https://pythonot.github.io/auto_examples/gromov/plot_partial_fgw.html) (exact [29] and entropic [3] formulations). * [Sliced Wasserstein](https://pythonot.github.io/auto_examples/sliced-wasserstein/plot_variance.html) [31, 32] and Max-sliced Wasserstein [35] that can be used for gradient flows [36]. * [Sliced Unbalanced OT and Unbalanced Sliced OT](https://pythonot.github.io/auto_examples/unbalanced-partial/plot_UOT.html) [82] * [Wasserstein distance on the circle](https://pythonot.github.io/auto_examples/sliced-wasserstein/plot_compute_wasserstein_circle.html) \[44, 45] and [Spherical Sliced Wasserstein](https://pythonot.github.io/auto_examples/sliced-wasserstein/plot_variance_ssw.html) \[46] * [Graph Dictionary Learning solvers](https://pythonot.github.io/auto_examples/gromov/plot_gromov_wasserstein_dictionary_learning.html) \[38]. * [Semi-relaxed (Fused) Gromov-Wasserstein divergences](https://pythonot.github.io/auto_examples/gromov/plot_semirelaxed_fgw.html) with corresponding [barycenter solvers](https://pythonot.github.io/auto_examples/gromov/plot_semirelaxed_gromov_wasserstein_barycenter.hmtl) (exact and regularized \[48]). * [Quantized (Fused) Gromov-Wasserstein distances](https://pythonot.github.io/auto_examples/gromov/plot_quantized_gromov_wasserstein.html) \[68]. * [Efficient Discrete Multi Marginal Optimal Transport Regularization](https://pythonot.github.io/auto_examples/others/plot_demd_gradient_minimize.html) \[50]. * [Several backends](https://pythonot.github.io/quickstart.html#solving-ot-with-multiple-backends) for easy use of POT with [Pytorch](https://pytorch.org/)/[jax](https://github.com/google/jax)/[Numpy](https://numpy.org/)/[Cupy](https://cupy.dev/)/[Tensorflow](https://www.tensorflow.org/) arrays. * [Smooth Strongly Convex Nearest Brenier Potentials](https://pythonot.github.io/auto_examples/others/plot_SSNB.html#sphx-glr-auto-examples-others-plot-ssnb-py) [58], with an extension to bounding potentials using [59]. * [Gaussian Mixture Model OT](https://pythonot.github.io/auto_examples/gaussian_gmm/plot_GMMOT_plan.html#sphx-glr-auto-examples-others-plot-gmmot-plan-py) [69]. * [Co-Optimal Transport](https://pythonot.github.io/auto_examples/others/plot_COOT.html) [49] and [unbalanced Co-Optimal Transport](https://pythonot.github.io/auto_examples/others/plot_learning_weights_with_COOT.html) [71]. * Fused unbalanced Gromov-Wasserstein [70]. * [Optimal Transport Barycenters for Generic Costs](https://pythonot.github.io/auto_examples/barycenters/plot_free_support_barycenter_generic_cost.html) [77] * [Barycenters between Gaussian Mixture Models](https://pythonot.github.io/auto_examples/barycenters/plot_gmm_barycenter.html) [69, 77] * [Fast and accurate transport bijections using BSP-OT](https://pythonot.github.io/auto_examples/plot_bsp_ot.html) [84] * [Sliced Transport Plans](https://pythonot.github.io/auto_examples/sliced-wasserstein/plot_sliced_plans.html) [85, 86, 87] POT provides the following Machine Learning related solvers: * [Optimal transport for domain adaptation](https://pythonot.github.io/auto_examples/domain-adaptation/plot_otda_classes.html) with [group lasso regularization](https://pythonot.github.io/auto_examples/domain-adaptation/plot_otda_classes.html), [Laplacian regularization](https://pythonot.github.io/auto_examples/domain-adaptation/plot_otda_laplacian.html) \[5] \[30] and [semi supervised setting](https://pythonot.github.io/auto_examples/domain-adaptation/plot_otda_semi_supervised.html). * [Linear OT mapping](https://pythonot.github.io/auto_examples/domain-adaptation/plot_otda_linear_mapping.html) \[14] and [Joint OT mapping estimation](https://pythonot.github.io/auto_examples/domain-adaptation/plot_otda_mapping.html) \[8]. * [Wasserstein Discriminant Analysis](https://pythonot.github.io/auto_examples/others/plot_WDA.html) \[11] (requires autograd + pymanopt). * [JCPOT algorithm for multi-source domain adaptation with target shift](https://pythonot.github.io/auto_examples/domain-adaptation/plot_otda_jcpot.html) \[27]. * [Graph Neural Network OT layers TFGW](https://pythonot.github.io/auto_examples/gromov/plot_gnn_TFGW.html) \[52] and TW (OT-GNN) \[53] Some other examples are available in the [documentation](https://pythonot.github.io/auto_examples/index.html). #### Using and citing the toolbox If you use this toolbox in your research and find it useful, please cite POT using the following references from the current version and from our [JMLR paper](https://jmlr.org/papers/v22/20-451.html): ``` Flamary R., Vincent-Cuaz C., Courty N., Gramfort A., Kachaiev O., Quang Tran H., David L., Bonet C., Cassereau N., Gnassounou T., Tanguy E., Delon J., Collas A., Mazelet S., Chapel L., Kerdoncuff T., Yu X., Feickert M., Krzakala P., Liu T., Fernandes Montesuma E., Neike N., Genest B., Coeurjolly D., Germain T., O'Shea S., Corneli M., Genans F. (2026). POT Python Optimal Transport (version 0.9.7). DOI: 10.5281/zenodo.17161062 URL: https://github.com/PythonOT/POT Rémi Flamary, Nicolas Courty, Alexandre Gramfort, Mokhtar Z. Alaya, Aurélie Boisbunon, Stanislas Chambon, Laetitia Chapel, Adrien Corenflos, Kilian Fatras, Nemo Fournier, Léo Gautheron, Nathalie T.H. Gayraud, Hicham Janati, Alain Rakotomamonjy, Ievgen Redko, Antoine Rolet, Antony Schutz, Vivien Seguy, Danica J. Sutherland, Romain Tavenard, Alexander Tong, Titouan Vayer, POT Python Optimal Transport library, Journal of Machine Learning Research, 22(78):1−8, 2021. URL: https://pythonot.github.io/ ``` In Bibtex format: ```bibtex @software{flamary2026pot, author = {Flamary, Rémi and Vincent-Cuaz, Cédric and Courty, Nicolas and Gramfort, Alexandre and Kachaiev, Oleksii and Quang Tran, Huy and David, Laurène and Bonet, Clément and Cassereau, Nathan and Gnassounou, Théo and Tanguy, Eloi and Delon, Julie and Collas, Antoine and Mazelet, Sonia and Chapel, Laetitia and Kerdoncuff, Tanguy and Yu, Xizheng and Feickert, Matthew and Krzakala, Paul and Liu, Tianlin and Fernandes Montesuma, Eduardo and Neike, Nathan and Genest, Baptiste and Coeurjolly, David and Germain, Thibaut and O'Shea, Sienna and Corneli, Marco and Genans, Ferdinand}, doi = {10.5281/zenodo.17161062}, month = {7}, title = {POT Python Optimal Transport}, version = {0.9.7}, url = {https://github.com/PythonOT/POT}, year = {2026} } @article{flamary2021pot, author = {R{\'e}mi Flamary and Nicolas Courty and Alexandre Gramfort and Mokhtar Z. Alaya and Aur{\'e}lie Boisbunon and Stanislas Chambon and Laetitia Chapel and Adrien Corenflos and Kilian Fatras and Nemo Fournier and L{\'e}o Gautheron and Nathalie T.H. Gayraud and Hicham Janati and Alain Rakotomamonjy and Ievgen Redko and Antoine Rolet and Antony Schutz and Vivien Seguy and Danica J. Sutherland and Romain Tavenard and Alexander Tong and Titouan Vayer}, title = {POT: Python Optimal Transport}, journal = {Journal of Machine Learning Research}, year = {2021}, volume = {22}, number = {78}, pages = {1-8}, url = {http://jmlr.org/papers/v22/20-451.html} } ``` ## Installation The library has been tested on Linux, MacOSX and Windows. It requires a C++ compiler for building/installing the EMD solver and relies on the following Python modules: * Numpy (>=1.16) * Scipy (>=1.0) * Cython (>=0.23) (build only, not necessary when installing from pip or conda) #### Pip installation You can install the toolbox through PyPI with: ```console pip install POT ``` or get the very latest version by running: ```console pip install -U git+https://github.com/PythonOT/POT.git # with --user for user install (no root) ``` Optional dependencies may be installed with ```console pip install POT[all] ``` Note that this installs `cvxopt`, which is licensed under GPL 3.0. Alternatively, if you cannot use GPL-licensed software, the specific optional dependencies may be installed individually, or per-submodule. The available optional installations are `backend-jax, backend-tf, backend-torch, cvxopt, dr, gnn, all`. #### Anaconda installation with conda-forge If you use the Anaconda python distribution, POT is available in [conda-forge](https://conda-forge.org). To install it and the required dependencies: ```console conda install -c conda-forge pot ``` #### Post installation check After a correct installation, you should be able to import the module without errors: ```python import ot ``` Note that for easier access the module is named `ot` instead of `pot`. ### Dependencies Some sub-modules require additional dependencies which are discussed below * **ot.dr** (Wasserstein dimensionality reduction) depends on autograd and pymanopt that can be installed with: ```shell pip install pymanopt autograd ``` ## Examples ### Short examples * Import the toolbox ```python import ot ``` * Compute Wasserstein distances ```python # a,b are 1D histograms (sum to 1 and positive) # M is the ground cost matrix # With the unified API : Wd = ot.solve(M, a, b).value # exact linear program Wd_reg = ot.solve(M, a, b, reg=reg).value # entropic regularized OT # With the old API : Wd = ot.emd2(a, b, M) # exact linear program Wd_reg = ot.sinkhorn2(a, b, M, reg) # entropic regularized OT # if b is a matrix compute all distances to a and return a vector ``` * Compute OT matrix ```python # a,b are 1D histograms (sum to 1 and positive) # M is the ground cost matrix # With the unified API : T = ot.solve(M, a, b).plan # exact linear program T_reg = ot.solve(M, a, b, reg=reg).plan # entropic regularized OT # With the old API : T = ot.emd(a, b, M) # exact linear program T_reg = ot.sinkhorn(a, b, M, reg) # entropic regularized OT ``` * Compute OT on empirical distributions ```python # X and Y are two 2D arrays of shape (n_samples, n_features) # with squared euclidean metric T = ot.solve_sample(X, Y).plan # exact linear program T_reg = ot.solve_sample(X, Y, reg=reg).plan # entropic regularized OT Wass_2 = ot.solve_sample(X, Y).value # Squared Wasserstein_2 Wass_1 = ot.solve_sample(X, Y, metric='euclidean').value # Wasserstein 1 ``` * Compute Wasserstein barycenter ```python # A is a n*d matrix containing d 1D histograms # M is the ground cost matrix ba = ot.barycenter(A, M, reg) # reg is regularization parameter ``` ### Examples and Notebooks The examples folder contain several examples and use case for the library. The full documentation with examples and output is available on . ## Acknowledgements This toolbox has been created by [Rémi Flamary](https://remi.flamary.com/) and [Nicolas Courty](http://people.irisa.fr/Nicolas.Courty/). It is currently maintained by : * [Rémi Flamary](https://remi.flamary.com/) * [Cédric Vincent-Cuaz](https://cedricvincentcuaz.github.io/) The POT contributors to this library are listed [here](CONTRIBUTORS.md). POT has benefited from the financing or manpower from the following partners: ANRCNRS3IAHi!PARIS ## Contributions and code of conduct Every contribution is welcome and should respect the [contribution guidelines](https://pythonot.github.io/master/contributing.html). Each member of the project is expected to follow the [code of conduct](https://pythonot.github.io/master/code_of_conduct.html). ## Support You can ask questions and join the development discussion: * On the POT [slack channel](https://pot-toolbox.slack.com) * On the POT [gitter channel](https://gitter.im/PythonOT/community) * On the POT [mailing list](https://mail.python.org/mm3/mailman3/lists/pot.python.org/) You can also post bug reports and feature requests in Github issues. Make sure to read our [guidelines](.github/CONTRIBUTING.md) first. ## References \[1] Bonneel, N., Van De Panne, M., Paris, S., & Heidrich, W. (2011, December). [Displacement interpolation using Lagrangian mass transport](https://people.csail.mit.edu/sparis/publi/2011/sigasia/Bonneel_11_Displacement_Interpolation.pdf). In ACM Transactions on Graphics (TOG) (Vol. 30, No. 6, p. 158). ACM. \[2] Cuturi, M. (2013). [Sinkhorn distances: Lightspeed computation of optimal transport](https://arxiv.org/pdf/1306.0895.pdf). In Advances in Neural Information Processing Systems (pp. 2292-2300). \[3] Benamou, J. D., Carlier, G., Cuturi, M., Nenna, L., & Peyré, G. (2015). [Iterative Bregman projections for regularized transportation problems](https://arxiv.org/pdf/1412.5154.pdf). SIAM Journal on Scientific Computing, 37(2), A1111-A1138. \[4] S. Nakhostin, N. Courty, R. Flamary, D. Tuia, T. Corpetti, [Supervised planetary unmixing with optimal transport](https://hal.archives-ouvertes.fr/hal-01377236/document), Workshop on Hyperspectral Image and Signal Processing : Evolution in Remote Sensing (WHISPERS), 2016. \[5] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, [Optimal Transport for Domain Adaptation](https://arxiv.org/pdf/1507.00504.pdf), in IEEE Transactions on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 \[6] Ferradans, S., Papadakis, N., Peyré, G., & Aujol, J. F. (2014). [Regularized discrete optimal transport](https://arxiv.org/pdf/1307.5551.pdf). SIAM Journal on Imaging Sciences, 7(3), 1853-1882. \[7] Rakotomamonjy, A., Flamary, R., & Courty, N. (2015). [Generalized conditional gradient: analysis of convergence and applications](https://arxiv.org/pdf/1510.06567.pdf). arXiv preprint arXiv:1510.06567. \[8] M. Perrot, N. Courty, R. Flamary, A. Habrard (2016), [Mapping estimation for discrete optimal transport](http://remi.flamary.com/biblio/perrot2016mapping.pdf), Neural Information Processing Systems (NIPS). \[9] Schmitzer, B. (2016). [Stabilized Sparse Scaling Algorithms for Entropy Regularized Transport Problems](https://arxiv.org/pdf/1610.06519.pdf). arXiv preprint arXiv:1610.06519. \[10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). [Scaling algorithms for unbalanced transport problems](https://arxiv.org/pdf/1607.05816.pdf). arXiv preprint arXiv:1607.05816. \[11] Flamary, R., Cuturi, M., Courty, N., & Rakotomamonjy, A. (2016). [Wasserstein Discriminant Analysis](https://arxiv.org/pdf/1608.08063.pdf). arXiv preprint arXiv:1608.08063. \[12] Gabriel Peyré, Marco Cuturi, and Justin Solomon (2016), [Gromov-Wasserstein averaging of kernel and distance matrices](http://proceedings.mlr.press/v48/peyre16.html) International Conference on Machine Learning (ICML). \[13] Mémoli, Facundo (2011). [Gromov–Wasserstein distances and the metric approach to object matching](https://media.adelaide.edu.au/acvt/Publications/2011/2011-Gromov%E2%80%93Wasserstein%20Distances%20and%20the%20Metric%20Approach%20to%20Object%20Matching.pdf). Foundations of computational mathematics 11.4 : 417-487. \[14] Knott, M. and Smith, C. S. (1984).[On the optimal mapping of distributions](https://link.springer.com/article/10.1007/BF00934745), Journal of Optimization Theory and Applications Vol 43. \[15] Peyré, G., & Cuturi, M. (2018). [Computational Optimal Transport](https://arxiv.org/pdf/1803.00567.pdf) . \[16] Agueh, M., & Carlier, G. (2011). [Barycenters in the Wasserstein space](https://hal.archives-ouvertes.fr/hal-00637399/document). SIAM Journal on Mathematical Analysis, 43(2), 904-924. \[17] Blondel, M., Seguy, V., & Rolet, A. (2018). [Smooth and Sparse Optimal Transport](https://arxiv.org/abs/1710.06276). Proceedings of the Twenty-First International Conference on Artificial Intelligence and Statistics (AISTATS). \[18] Genevay, A., Cuturi, M., Peyré, G. & Bach, F. (2016) [Stochastic Optimization for Large-scale Optimal Transport](https://arxiv.org/abs/1605.08527). Advances in Neural Information Processing Systems (2016). \[19] Seguy, V., Bhushan Damodaran, B., Flamary, R., Courty, N., Rolet, A.& Blondel, M. [Large-scale Optimal Transport and Mapping Estimation](https://arxiv.org/pdf/1711.02283.pdf). International Conference on Learning Representation (2018) \[20] Cuturi, M. and Doucet, A. (2014) [Fast Computation of Wasserstein Barycenters](http://proceedings.mlr.press/v32/cuturi14.html). International Conference in Machine Learning \[21] Solomon, J., De Goes, F., Peyré, G., Cuturi, M., Butscher, A., Nguyen, A. & Guibas, L. (2015). [Convolutional wasserstein distances: Efficient optimal transportation on geometric domains](https://dl.acm.org/citation.cfm?id=2766963). ACM Transactions on Graphics (TOG), 34(4), 66. \[22] J. Altschuler, J.Weed, P. Rigollet, (2017) [Near-linear time approximation algorithms for optimal transport via Sinkhorn iteration](https://papers.nips.cc/paper/6792-near-linear-time-approximation-algorithms-for-optimal-transport-via-sinkhorn-iteration.pdf), Advances in Neural Information Processing Systems (NIPS) 31 \[23] Aude, G., Peyré, G., Cuturi, M., [Learning Generative Models with Sinkhorn Divergences](https://arxiv.org/abs/1706.00292), Proceedings of the Twenty-First International Conference on Artificial Intelligence and Statistics, (AISTATS) 21, 2018 \[24] Vayer, T., Chapel, L., Flamary, R., Tavenard, R. and Courty, N. (2019). [Optimal Transport for structured data with application on graphs](http://proceedings.mlr.press/v97/titouan19a.html) Proceedings of the 36th International Conference on Machine Learning (ICML). \[25] Frogner C., Zhang C., Mobahi H., Araya-Polo M., Poggio T. (2015). [Learning with a Wasserstein Loss](http://cbcl.mit.edu/wasserstein/) Advances in Neural Information Processing Systems (NIPS). \[26] Alaya M. Z., Bérar M., Gasso G., Rakotomamonjy A. (2019). [Screening Sinkhorn Algorithm for Regularized Optimal Transport](https://papers.nips.cc/paper/9386-screening-sinkhorn-algorithm-for-regularized-optimal-transport), Advances in Neural Information Processing Systems 33 (NeurIPS). \[27] Redko I., Courty N., Flamary R., Tuia D. (2019). [Optimal Transport for Multi-source Domain Adaptation under Target Shift](http://proceedings.mlr.press/v89/redko19a.html), Proceedings of the Twenty-Second International Conference on Artificial Intelligence and Statistics (AISTATS) 22, 2019. \[28] Caffarelli, L. A., McCann, R. J. (2010). [Free boundaries in optimal transport and Monge-Ampere obstacle problems](http://www.math.toronto.edu/~mccann/papers/annals2010.pdf), Annals of mathematics, 673-730. \[29] Chapel, L., Alaya, M., Gasso, G. (2020). [Partial Optimal Transport with Applications on Positive-Unlabeled Learning](https://arxiv.org/abs/2002.08276), Advances in Neural Information Processing Systems (NeurIPS), 2020. \[30] Flamary R., Courty N., Tuia D., Rakotomamonjy A. (2014). [Optimal transport with Laplacian regularization: Applications to domain adaptation and shape matching](https://remi.flamary.com/biblio/flamary2014optlaplace.pdf), NIPS Workshop on Optimal Transport and Machine Learning OTML, 2014. \[31] Bonneel, Nicolas, et al. [Sliced and radon wasserstein barycenters of measures](https://perso.liris.cnrs.fr/nicolas.bonneel/WassersteinSliced-JMIV.pdf), Journal of Mathematical Imaging and Vision 51.1 (2015): 22-45 \[32] Huang, M., Ma S., Lai, L. (2021). [A Riemannian Block Coordinate Descent Method for Computing the Projection Robust Wasserstein Distance](http://proceedings.mlr.press/v139/huang21e.html), Proceedings of the 38th International Conference on Machine Learning (ICML). \[33] Kerdoncuff T., Emonet R., Marc S. [Sampled Gromov Wasserstein](https://hal.archives-ouvertes.fr/hal-03232509/document), Machine Learning Journal (MJL), 2021 \[34] Feydy, J., Séjourné, T., Vialard, F. X., Amari, S. I., Trouvé, A., & Peyré, G. (2019, April). [Interpolating between optimal transport and MMD using Sinkhorn divergences](http://proceedings.mlr.press/v89/feydy19a/feydy19a.pdf). In The 22nd International Conference on Artificial Intelligence and Statistics (pp. 2681-2690). PMLR. \[35] Deshpande, I., Hu, Y. T., Sun, R., Pyrros, A., Siddiqui, N., Koyejo, S., ... & Schwing, A. G. (2019). [Max-sliced wasserstein distance and its use for gans](https://openaccess.thecvf.com/content_CVPR_2019/papers/Deshpande_Max-Sliced_Wasserstein_Distance_and_Its_Use_for_GANs_CVPR_2019_paper.pdf). In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (pp. 10648-10656). \[36] Liutkus, A., Simsekli, U., Majewski, S., Durmus, A., & Stöter, F. R. (2019, May). [Sliced-Wasserstein flows: Nonparametric generative modeling via optimal transport and diffusions](http://proceedings.mlr.press/v97/liutkus19a/liutkus19a.pdf). In International Conference on Machine Learning (pp. 4104-4113). PMLR. \[37] Janati, H., Cuturi, M., Gramfort, A. [Debiased sinkhorn barycenters](http://proceedings.mlr.press/v119/janati20a/janati20a.pdf) Proceedings of the 37th International Conference on Machine Learning, PMLR 119:4692-4701, 2020 \[38] C. Vincent-Cuaz, T. Vayer, R. Flamary, M. Corneli, N. Courty, [Online Graph Dictionary Learning](https://arxiv.org/pdf/2102.06555.pdf), International Conference on Machine Learning (ICML), 2021. \[39] Gozlan, N., Roberto, C., Samson, P. M., & Tetali, P. (2017). [Kantorovich duality for general transport costs and applications](https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.712.1825\&rep=rep1\&type=pdf). Journal of Functional Analysis, 273(11), 3327-3405. \[40] Forrow, A., Hütter, J. C., Nitzan, M., Rigollet, P., Schiebinger, G., & Weed, J. (2019, April). [Statistical optimal transport via factored couplings](http://proceedings.mlr.press/v89/forrow19a/forrow19a.pdf). In The 22nd International Conference on Artificial Intelligence and Statistics (pp. 2454-2465). PMLR. \[41] Chapel\*, L., Flamary\*, R., Wu, H., Févotte, C., Gasso, G. (2021). [Unbalanced Optimal Transport through Non-negative Penalized Linear Regression](https://proceedings.neurips.cc/paper/2021/file/c3c617a9b80b3ae1ebd868b0017cc349-Paper.pdf) Advances in Neural Information Processing Systems (NeurIPS), 2020. (Two first co-authors) \[42] Delon, J., Gozlan, N., and Saint-Dizier, A. [Generalized Wasserstein barycenters between probability measures living on different subspaces](https://arxiv.org/pdf/2105.09755). arXiv preprint arXiv:2105.09755, 2021. \[43] �lvarez-Esteban, Pedro C., et al. [A fixed-point approach to barycenters in Wasserstein space.](https://arxiv.org/pdf/1511.05355.pdf) Journal of Mathematical Analysis and Applications 441.2 (2016): 744-762. \[44] Delon, Julie, Julien Salomon, and Andrei Sobolevski. [Fast transport optimization for Monge costs on the circle.](https://arxiv.org/abs/0902.3527) SIAM Journal on Applied Mathematics 70.7 (2010): 2239-2258. \[45] Hundrieser, Shayan, Marcel Klatt, and Axel Munk. [The statistics of circular optimal transport.](https://arxiv.org/abs/2103.15426) Directional Statistics for Innovative Applications: A Bicentennial Tribute to Florence Nightingale. Singapore: Springer Nature Singapore, 2022. 57-82. \[46] Bonet, C., Berg, P., Courty, N., Septier, F., Drumetz, L., & Pham, M. T. (2023). [Spherical Sliced-Wasserstein](https://openreview.net/forum?id=jXQ0ipgMdU). International Conference on Learning Representations. \[47] Chowdhury, S., & Mémoli, F. (2019). [The gromov–wasserstein distance between networks and stable network invariants](https://academic.oup.com/imaiai/article/8/4/757/5627736). Information and Inference: A Journal of the IMA, 8(4), 757-787. \[48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty (2022). [Semi-relaxed Gromov-Wasserstein divergence and applications on graphs](https://openreview.net/pdf?id=RShaMexjc-x). International Conference on Learning Representations (ICLR), 2022. \[49] Redko, I., Vayer, T., Flamary, R., and Courty, N. (2020). [CO-Optimal Transport](https://proceedings.neurips.cc/paper/2020/file/cc384c68ad503482fb24e6d1e3b512ae-Paper.pdf). Advances in Neural Information Processing Systems, 33. \[50] Liu, T., Puigcerver, J., & Blondel, M. (2023). [Sparsity-constrained optimal transport](https://openreview.net/forum?id=yHY9NbQJ5BP). Proceedings of the Eleventh International Conference on Learning Representations (ICLR). \[51] Xu, H., Luo, D., Zha, H., & Carin, L. (2019). [Gromov-wasserstein learning for graph matching and node embedding](http://proceedings.mlr.press/v97/xu19b.html). In International Conference on Machine Learning (ICML), 2019. \[52] Collas, A., Vayer, T., Flamary, F., & Breloy, A. (2023). [Entropic Wasserstein Component Analysis](https://arxiv.org/abs/2303.05119). ArXiv. \[53] C. Vincent-Cuaz, R. Flamary, M. Corneli, T. Vayer, N. Courty (2022). [Template based graph neural network with optimal transport distances](https://papers.nips.cc/paper_files/paper/2022/file/4d3525bc60ba1adc72336c0392d3d902-Paper-Conference.pdf). Advances in Neural Information Processing Systems, 35. \[54] Bécigneul, G., Ganea, O. E., Chen, B., Barzilay, R., & Jaakkola, T. S. (2020). [Optimal transport graph neural networks](https://arxiv.org/pdf/2006.04804). \[55] Ronak Mehta, Jeffery Kline, Vishnu Suresh Lokhande, Glenn Fung, & Vikas Singh (2023). [Efficient Discrete Multi Marginal Optimal Transport Regularization](https://openreview.net/forum?id=R98ZfMt-jE). In The Eleventh International Conference on Learning Representations (ICLR). \[56] Jeffery Kline. [Properties of the d-dimensional earth mover’s problem](https://www.sciencedirect.com/science/article/pii/S0166218X19301441). Discrete Applied Mathematics, 265: 128–141, 2019. \[57] Delon, J., Desolneux, A., & Salmona, A. (2022). [Gromov–Wasserstein distances between Gaussian distributions](https://hal.science/hal-03197398v2/file/main.pdf). Journal of Applied Probability, 59(4), 1178-1198. \[58] Paty F-P., d’Aspremont 1., & Cuturi M. (2020). [Regularity as regularization:Smooth and strongly convex brenier potentials in optimal transport.](http://proceedings.mlr.press/v108/paty20a/paty20a.pdf) In International Conference on Artificial Intelligence and Statistics, pages 1222–1232. PMLR, 2020. \[59] Taylor A. B. (2017). [Convex interpolation and performance estimation of first-order methods for convex optimization.](https://dial.uclouvain.be/pr/boreal/object/boreal%3A182881/datastream/PDF_01/view) PhD thesis, Catholic University of Louvain, Louvain-la-Neuve, Belgium, 2017. \[60] Feydy, J., Roussillon, P., Trouvé, A., & Gori, P. (2019). [Fast and scalable optimal transport for brain tractograms](https://arxiv.org/pdf/2107.02010.pdf). In Medical Image Computing and Computer Assisted Intervention–MICCAI 2019: 22nd International Conference, Shenzhen, China, October 13–17, 2019, Proceedings, Part III 22 (pp. 636-644). Springer International Publishing. \[61] Charlier, B., Feydy, J., Glaunes, J. A., Collin, F. D., & Durif, G. (2021). [Kernel operations on the gpu, with autodiff, without memory overflows](https://www.jmlr.org/papers/volume22/20-275/20-275.pdf). The Journal of Machine Learning Research, 22(1), 3457-3462. \[62] H. Van Assel, C. Vincent-Cuaz, T. Vayer, R. Flamary, N. Courty (2023). [Interpolating between Clustering and Dimensionality Reduction with Gromov-Wasserstein](https://arxiv.org/pdf/2310.03398.pdf). NeurIPS 2023 Workshop Optimal Transport and Machine Learning. \[63] Li, J., Tang, J., Kong, L., Liu, H., Li, J., So, A. M. C., & Blanchet, J. (2022). [A Convergent Single-Loop Algorithm for Relaxation of Gromov-Wasserstein in Graph Data](https://openreview.net/pdf?id=0jxPyVWmiiF). In The Eleventh International Conference on Learning Representations. \[64] Ma, X., Chu, X., Wang, Y., Lin, Y., Zhao, J., Ma, L., & Zhu, W. (2023). [Fused Gromov-Wasserstein Graph Mixup for Graph-level Classifications](https://openreview.net/pdf?id=uqkUguNu40). In Thirty-seventh Conference on Neural Information Processing Systems. \[65] Scetbon, M., Cuturi, M., & Peyré, G. (2021). [Low-Rank Sinkhorn Factorization](https://arxiv.org/pdf/2103.04737.pdf). \[66] Pooladian, Aram-Alexandre, and Jonathan Niles-Weed. [Entropic estimation of optimal transport maps](https://arxiv.org/pdf/2109.12004.pdf). arXiv preprint arXiv:2109.12004 (2021). \[67] Scetbon, M., Peyré, G. & Cuturi, M. (2022). [Linear-Time Gromov-Wasserstein Distances using Low Rank Couplings and Costs](https://proceedings.mlr.press/v162/scetbon22b/scetbon22b.pdf). In International Conference on Machine Learning (ICML), 2022. \[68] Chowdhury, S., Miller, D., & Needham, T. (2021). [Quantized gromov-wasserstein](https://link.springer.com/chapter/10.1007/978-3-030-86523-8_49). ECML PKDD 2021. Springer International Publishing. \[69] Delon, J., & Desolneux, A. (2020). [A Wasserstein-type distance in the space of Gaussian mixture models](https://epubs.siam.org/doi/abs/10.1137/19M1301047). SIAM Journal on Imaging Sciences, 13(2), 936-970. \[70] A. Thual, H. Tran, T. Zemskova, N. Courty, R. Flamary, S. Dehaene & B. Thirion (2022). [Aligning individual brains with Fused Unbalanced Gromov-Wasserstein.](https://proceedings.neurips.cc/paper_files/paper/2022/file/8906cac4ca58dcaf17e97a0486ad57ca-Paper-Conference.pdf). Neural Information Processing Systems (NeurIPS). \[71] H. Tran, H. Janati, N. Courty, R. Flamary, I. Redko, P. Demetci & R. Singh (2023). [Unbalanced Co-Optimal Transport](https://dl.acm.org/doi/10.1609/aaai.v37i8.26193). AAAI Conference on Artificial Intelligence. \[72] Thibault Séjourné, François-Xavier Vialard, and Gabriel Peyré (2021). [The Unbalanced Gromov Wasserstein Distance: Conic Formulation and Relaxation](https://proceedings.neurips.cc/paper/2021/file/4990974d150d0de5e6e15a1454fe6b0f-Paper.pdf). Neural Information Processing Systems (NeurIPS). \[73] Séjourné, T., Vialard, F. X., & Peyré, G. (2022). [Faster Unbalanced Optimal Transport: Translation Invariant Sinkhorn and 1-D Frank-Wolfe](https://proceedings.mlr.press/v151/sejourne22a.html). In International Conference on Artificial Intelligence and Statistics (pp. 4995-5021). PMLR. \[74] Chewi, S., Maunu, T., Rigollet, P., & Stromme, A. J. (2020). [Gradient descent algorithms for Bures-Wasserstein barycenters](https://proceedings.mlr.press/v125/chewi20a.html). In Conference on Learning Theory (pp. 1276-1304). PMLR. \[75] Altschuler, J., Chewi, S., Gerber, P. R., & Stromme, A. (2021). [Averaging on the Bures-Wasserstein manifold: dimension-free convergence of gradient descent](https://papers.neurips.cc/paper_files/paper/2021/hash/b9acb4ae6121c941324b2b1d3fac5c30-Abstract.html). Advances in Neural Information Processing Systems, 34, 22132-22145. \[76] Chapel, L., Tavenard, R. (2025). [One for all and all for one: Efficient computation of partial Wasserstein distances on the line](https://iclr.cc/virtual/2025/poster/28547). In International Conference on Learning Representations. \[77] Tanguy, Eloi and Delon, Julie and Gozlan, Nathaël (2024). [Computing Barycentres of Measures for Generic Transport Costs](https://arxiv.org/abs/2501.04016). arXiv preprint 2501.04016 (2024) \[78] Martin, R. D., Medri, I., Bai, Y., Liu, X., Yan, K., Rohde, G. K., & Kolouri, S. (2024). [LCOT: Linear Circular Optimal Transport](https://openreview.net/forum?id=49z97Y9lMq). International Conference on Learning Representations. \[79] Liu, X., Bai, Y., Martín, R. D., Shi, K., Shahbazi, A., Landman, B. A., Chang, C., & Kolouri, S. (2025). [Linear Spherical Sliced Optimal Transport: A Fast Metric for Comparing Spherical Data](https://openreview.net/forum?id=fgUFZAxywx). International Conference on Learning Representations. \[80] Altschuler, J., Bach, F., Rudi, A., Niles-Weed, J., [Massively scalable Sinkhorn distances via the Nyström method](https://proceedings.neurips.cc/paper_files/paper/2019/file/f55cadb97eaff2ba1980e001b0bd9842-Paper.pdf), Advances in Neural Information Processing Systems, 2019. \[81] Xu, H., Luo, D., & Carin, L. (2019). [Scalable Gromov-Wasserstein learning for graph partitioning and matching](https://proceedings.neurips.cc/paper/2019/hash/6e62a992c676f611616097dbea8ea030-Abstract.html). Neural Information Processing Systems (NeurIPS). \[82] Bonet, C., Nadjahi, K., Séjourné, T., Fatras, K., & Courty, N. (2024). [Slicing Unbalanced Optimal Transport](https://openreview.net/forum?id=AjJTg5M0r8). Transactions on Machine Learning Research. \[83] Germain, T., Flamary, R., Kostic, V. R., & Lounici, K. (2025). [A Spectral-Grassmann Wasserstein Metric for Operator Representations of Dynamical Systems](https://arxiv.org/abs/2509.24920). \[84] Genest, B., Bonneel, N., Nivoliers, V., & Coeurjolly, D. (2025). [BSP-OT: Sparse transport plans between discrete measures in loglinear time.](https://dl.acm.org/doi/10.1145/3763281) ACM Transactions on Graphics (TOG), 44(6), 1-15. \[85] Mahey, G., Chapel, L., Gasso, G., Bonet, C., & Courty, N. (2023). [Fast Optimal Transport through Sliced Generalized Wasserstein Geodesics](https://proceedings.neurips.cc/paper_files/paper/2023/hash/6f1346bac8b02f76a631400e2799b24b-Abstract-Conference.html). Advances in Neural Information Processing Systems, 36, 35350–35385. \[86] Tanguy, E., Chapel, L., Delon, J. (2025). [Sliced Transport Plans](https://arxiv.org/abs/2508.01243) arXiv preprint 2506.03661. \[87] Liu, X., Diaz Martin, R., Bai Y., Shahbazi A., Thorpe M., Aldroubi A., Kolouri, S. (2024). [Expected Sliced Transport Plans](https://openreview.net/forum?id=P7O1Vt1BdU). International Conference on Learning Representations. \[88] Bouveyron, C. & Corneli, M. (2026). [Scaling optimal transport to high-dimensional Gaussian distributions with application to domain adaptation](https://hal.science/hal-04930868v4/file/Article-OT-HDGauss-v4.pdf). Statistics and Computing 36.2 (2026): 88. \[89] Tipping, M.E. & Bishop, C.M. (1999). [Probabilistic principal component analysis](https://www.cs.columbia.edu/~blei/seminar/2020-representation/readings/TippingBishop1999.pdf). Journal of the Royal Statistical Society Series B: Statistical Methodology 61.3 (1999): 611-622. \[90] Genans, F., Godichon-Baggioni, A., Vialard, F. X., & Wintenberger, O. (2025). [Decreasing Entropic Regularization Averaged Gradient for Semi-Discrete Optimal Transport](https://proceedings.neurips.cc/paper_files/paper/2025/file/d7efa12e98f5e0dd8b4f48cd60b4e3aa-Paper-Conference.pdf). Advances in Neural Information Processing Systems, 38, 146913-146949. \[91] Fatras, K., Zine, Y., Majewski, S., Flamary, R., Gribonval, R., & Courty, N. (2021). [Minibatch optimal transport distances; analysis and applications](https://arxiv.org/pdf/2101.01792). arXiv preprint arXiv:2101.01792. \[92] Xie, Y., Wang, X., Wang, R., & Zha, H. (2020, August). [A fast proximal point method for computing exact wasserstein distance.](https://proceedings.mlr.press/v115/xie20b/xie20b.pdf) In Uncertainty in artificial intelligence (pp. 433-453). PMLR. python-pot-0.9.7+dfsg/RELEASES.md000066400000000000000000001667221525521106200163310ustar00rootroot00000000000000# Releases ## 0.9.7 This release contains several bug fixes and performance improvements, as well as updates to the documentation and examples and tests. We provide below a summary of the main new features and closed issues. **New exact OT solvers.** This new release adds many new variants and updates for the exact OT solver. First a new lazy exact OT solver that re-computes distances on-the-fly from coordinates, reducing memory usage from O(n×m) to O(n+m) ( available in [`ot.solve_sample`](https://pythonot.github.io/all.html#ot.solve_sample) with `lazy=True)`). Another major feature is the addition of a warmstart feature for the EMD solver, allowing users to provide initial potentials to speed up convergence (with `init_potentials` in [`ot.solve`](https://pythonot.github.io/all.html#ot.solve) and [`ot.solve_sample`](https://pythonot.github.io/all.html#ot.solve_sample)). Finally the release also include a sparse solver and [`ot.solve`](https://pythonot.github.io/all.html#ot.solve) now accept sparse cost matrices and provides an OT plan whose support is included in the support of the cost matrix. All implementations are backend-agnostic and preserve gradient computation for automatic differentiation (but are solved on CPU). The computational time for different solvers (on a laptop CPU) are shown below: | n| 100| 500| 1000| 5000| |--------------------------|:------------------------:|:------------------------:|:------------------------:|:------------------------:| | Solve | 7.259e-04| 1.501e-02| 1.055e-01| 2.343e+00| | Solve Warm Start | 6.970e-04| 5.413e-03| 2.291e-02| 6.036e-01| | Solve Lazy | 1.280e-03| 3.098e-02| 1.421e-01| 3.377e+00| | Solve Sparse 10% | 3.453e-04| 6.716e-04| 1.535e-03| 3.534e-02| **BSPOT solver.** A new solver relying on [Binary Space Partitioning (BSP)](https://pythonot.github.io/master/auto_examples/plot_bsp_ot.html) has been added to compute sparse transport plans between discrete measures in loglinear time which allows for very large problems to be solved. The release also includes a new [`ot.unbalanced.uot_1d`](https://pythonot.github.io/master/gen_modules/ot.unbalanced.html#ot.unbalanced.uot_1d) solver with a Frank-Wolfe solver for unbalanced optimal transport in 1D. **Sliced OT pans** Finally we now have a sliced OT plan solver that can be used to compute sliced transport plans ([min-pivot sliced](https://pythonot.github.io/master/gen_modules/ot.sliced.html#ot.sliced.min_sliced_transport_plan) and [expected sliced](https://pythonot.github.io/master/gen_modules/ot.sliced.html#ot.sliced.expected_sliced_plan)) between two measures. **OT between dynamical systems.** A novel [Spectral-Grassmann Wasserstein metric for operator representations of dynamical systems](https://pythonot.github.io/master/auto_examples/others/plot_sgot.html) has been implemented in `ot.sgot`. **Unified API for barycenter solvers in `ot.solve_bary_sample`.** A new free support solver for barycenter solvers has been added in [`ot.solve_bary_sample`](https://pythonot.github.io/master/all.html#ot.solve_bary_sample). You can see [examples of how to use it here](https://pythonot.github.io/master/auto_examples/barycenters/plot_solve_barycenter_variants.html). **OT between high dimensional Gaussian distributions.** New methods to compute the linear transport map and the related 2-Wasserstein distance between high-dimensional (HD) Gaussian distributions have been added in [`ot.gaussian.bures_wasserstein_mapping_hd`](https://pythonot.github.io/master/gen_modules/ot.gaussian.html#id56) and [`ot.gaussian.bures_wasserstein_distance_hd`](https://pythonot.github.io/master/gen_modules/ot.gaussian.html#ot.gaussian.bures_wasserstein_distance_hd), respectively with empirical versions that estimate the distance from sample. **Batched solver for exact OT.** The batch implementations have also been improved and you can now solve exact OT problems in parallel on CPU or GPU using the new proximal point solver in functions [`ot.solve_batch`](https://pythonot.github.io/master/gen_modules/ot.batch.html#ot.batch.solve_batch) and [`ot.solve_sample_batch`](https://pythonot.github.io/master/gen_modules/ot.batch.html#ot.batch.solve_sample_batch) when `reg=0` or no provided. A new batch [loss for Fused unbalanced Gromov-Wasserstein](https://pythonot.github.io/master/gen_modules/ot.batch.html#ot.batch.loss_quadratic_batch) is also now available in `ot.batch`. **New methods in unified API [`ot.solve_sample`](https://pythonot.github.io/all.html#ot.solve_sample).** The unified API function [`ot.solve_sample`](https://pythonot.github.io/all.html#ot.solve_sample) has also been updated to allows solving of specific problems such as BSP-OT, distance between high-dimensional (HD) Gaussian distributions and sliced and max-sliced distances. **Data normalization for sliced and [`ot.solve_sample`](https://pythonot.github.io/all.html#ot.solve_sample) solvers.** The release also includes tools for data normalization and scaling which can not be used in sliced Wasserstein distance computations. A simple normalization class [`ot.utils.DataScaler`](https://pythonot.github.io/master/gen_modules/ot.utils.html#id46) has been added and supports all backends for `'standard'`, `'minmax'`, and `'l2'` methods. Finally an optional `scaler` parameter has been added to [`ot.sliced_wasserstein_distance`](https://pythonot.github.io/master/all.html#ot.sliced_wasserstein_distance), [`ot.max_sliced_wasserstein_distance`](https://pythonot.github.io/master/all.html#ot.max_sliced_wasserstein_distance) and [`ot.solve_sample`](https://pythonot.github.io/all.html#ot.solve_sample). #### New features - Refactor lazy EMD network simplex storage to avoid dense per-arc cost, endpoint, flow, and state storage where possible, and return sparse lazy transport plans instead of materializing dense plans internally (PR #813) - Removed OpenMP implementation in Simplex since no time gains are observed #820 - Add sliced transport plans (min-pivot sliced and expected sliced) solvers (PR #767) - Add lazy EMD solver with on-the-fly distance computation from coordinates (PR #788) - Add Warmstart feature to the EMD solver for existing potentials (PR #793) - Add Warmstart potentials feature to the EMD solver for lazy and sparse solver (PR #795) - Faster init and result retrieval for EMD solver (PR #796) - Migrate backend from deprecated `scipy.sparse.coo_matrix` to modern `scipy.sparse.coo_array` (PR #782) - Geomloss function now handles both scalar and slice indices for i and j (PR #785) - Add support for sparse cost matrices in EMD solver (PR #778, Issue #397) - Add "BSP-OT: Sparse transport plans between discrete measures in loglinear time" (PR #768) - Added UOT1D with Frank-Wolfe in `ot.unbalanced.uot_1d` (PR #765) - Add Sliced UOT and Unbalanced Sliced OT in `ot/unbalanced/_sliced.py` (PR #765) - Add `ot.utils.DataScaler` class for backend-aware joint normalization of input distributions, with sklearn-compatible `fit`/`transform`/`fit_transform` API and support for `'standard'`, `'minmax'`, and `'l2'` methods (PR #808) - Add `ot.utils.apply_scaler` helper that dispatches preprocessing to a scaler object, a callable, or a no-op (PR #808) - Add optional `scaler` parameter to `sliced_wasserstein_distance` and `max_sliced_wasserstein_distance` (PR #808) - Add SGD based semi-discrete OT solver in `ot.semidiscrete` and a gallery example. (PR #812) - Add a numerically stable log-domain solver for entropic partial Wasserstein, selectable via the new `method` parameter of `entropic_partial_wasserstein` (`method='sinkhorn_log'`) or directly through `entropic_partial_wasserstein_logscale` (Issue #723) - Add cost functions between linear operators following [A Spectral-Grassmann Wasserstein metric for operator representations of dynamical systems](https://arxiv.org/pdf/2509.24920), implemented in `ot.sgot` (PR #792, PR #830) - Add batch FUGW loss to `ot.batch` and fix issues in some default parameters in the batch module (PR #775) - Wrapper for barycenter solvers with free support `ot.solvers.bary_free_support` (PR #730) - Build wheels on ubuntu ARM to avoid QEMU emulation (PR #818) - Add new methods to compute the linear transport map and the related 2-Wasserstein distance betweeen high-dimensional (HD) Gaussian distributions as described in [88], implemented in `ot.gaussian.bures_wasserstein_mapping_hd` and `ot.gaussian.bures_wasserstein_distance_hd`, respectively. Two additional methods estimate the same quantities from the source and destination observed data and are implemented in `ot.gaussian.empirical_bures_wasserstein_mapping_hd` and `ot.gaussian.empirical_bures_wasserstein_distance_hd`, respectively (PR #814) - Update the geomloss wrapper to the new version and API (PR #826) - Fix docstrings for `lowrank_gromov_wasserstein_samples` and `lowrank_sinkhorn` (PR #823) - Reorganize all tests per backend (PR #828) - Implemented batch proximal point solver for OT problems `ot.batch.proximal_bregman_log_plan_batch` function and updated wrapper functions `ot.batch.solve_batch` and `ot.batch.solve_sample_batch` (PR #832) - Implement debiased OT solvers in `ot.solve_sample`. #### Closed issues - Fix label-aware cost correction in `ot.da` transport classes: the large cost was applied to unlabeled pairs instead of labeled pairs with different labels, so `ys`/`yt` had no effect when all labels were known (PR #833, Issue #664) - Mitigate NaN regime of `entropic_partial_wasserstein` at small `reg` via a new log-domain solver, reachable with `entropic_partial_wasserstein(..., method='sinkhorn_log')` (Issue #723; the default `method='sinkhorn'` path is unchanged — callers opt into the log-domain variant) - Fix NumPy 2.x compatibility in Brenier potential bounds (PR #788) - Fix MSVC Windows build by removing **restrict** keyword (PR #788) - Fix O(n³) performance bottleneck in sparse bipartite graph arc iteration (PR #785) - Fix deprecated JAX function in `ot.backend.JaxBackend` (PR #771, Issue #770) - Add test for build from source (PR #772, Issue #764) - Fix device for batch Ot solver in `ot.batch` (PR #784, Issue #783) - Fix openmp flags on macOS (PR #789) - Clean documentation (PR #787) - Fix code coverage (PR #791) - Fix test of the version of jax in `ot.backend` (PR #794) - Reverting the openmp fix on macOS (PR #789) for macOS (PR #797) - Align documentation build dependencies and doc extras (PR #801) - Debug Debug linux test core dump (PR #815) - Fix entropic regularization in `gcg`(PR #817, Issue #758) - Fix documentation build on master with submodules (PR #818) - Fix failing test for unbalanced solver with generic regularization (PR #824) - Fix reference number error introduced in PR #767 (PR #819) - Fix docstrings for `lowrank_gromov_wasserstein_samples` and `lowrank_sinkhorn` (PR #823) - Update sgot cost function and example (PR #830) ## 0.9.6.post1 *September 2025* #### Closed issues - Fix missing cython file in MANIFEST.in (PR #763) ## 0.9.6 *September 2025* This new release contains several new features and bug fixes. Among the new features we have a new submodule `ot.batch` that contains batch parallel solvers for several OT problems including [Sinkhorn, Gromov-Wasserstein and Fused Gromov-Wasserstein](https://pythonot.github.io/master/auto_examples/backends/plot_ot_batch.html). This new submodule can be used to solve many independent OT problems in parallel on CPU or GPU with shared source and target support sizes. We also implemented a new Nystrom kernel approximation for the Sinkhorn solver that can be used to speed up the computation of the Sinkhorn divergence on large datasets. We also added new 1D solvers for [Linear circular OT](https://pythonot.github.io/master/auto_examples/sliced-wasserstein/plot_compute_wasserstein_circle.html) and new solvers for free support [OT barycenters with generic cost functions](https://pythonot.github.io/master/auto_examples/barycenters/plot_free_support_barycenter_generic_cost.html) and for [barycenters between Gaussian Mixture Models (GMMs)](https://pythonot.github.io/master/auto_examples/barycenters/plot_gmm_barycenter.html). We also implemented two solvers for partial Fused Gromov-Wasserstein problems based on [conditional gradient](https://pythonot.github.io/master/gen_modules/ot.gromov.html#ot.gromov.partial_fused_gromov_wasserstein) and [projected gradient](https://pythonot.github.io/master/gen_modules/ot.gromov.html#ot.gromov.entropic_partial_fused_gromov_wasserstein) descents. Finally we have updated the documentation to reflect the new generic API and reorganized the [examples gallery](https://pythonot.github.io/auto_examples/index.html). #### New features - Implement CG solvers for partial FGW (PR #687) - Added feature `grad=last_step` for `ot.solvers.solve` (PR #693) - Automatic PR labeling and release file update check (PR #704) - Reorganize sub-module `ot/lp/__init__.py` into separate files (PR #714) - Implement fixed-point solver for OT barycenters with generic cost functions (generalizes `ot.lp.free_support_barycenter`), with example. (PR #715) - Implement fixed-point solver for barycenters between GMMs (PR #715), with example. - Fix warning raise when import the library (PR #716) - Implement projected gradient descent solvers for entropic partial FGW (PR #702) - Fix documentation in the module `ot.gaussian` (PR #718) - Refactored `ot.bregman._convolutional` to improve readability (PR #709) - Added `ot.gaussian.bures_barycenter_gradient_descent` (PR #680) - Added `ot.gaussian.bures_wasserstein_distance` (PR #680) - `ot.gaussian.bures_wasserstein_distance` can be batched (PR #680) - Backend implementation of `ot.dist` for (PR #701) - Updated documentation Quickstart guide and User guide with new API (PR #726) - Fix jax version for auto-grad (PR #732) - Add Nystrom kernel approximation for Sinkhorn (PR #742) - Added `ot.solver_1d.linear_circular_ot` and `ot.sliced.linear_sliced_wasserstein_sphere` (PR #736) - Implement 1d solver for partial optimal transport (PR #741) - Fix reg_div function compatibility with numpy in `ot.unbalanced.lbfgsb_unbalanced` via new function `ot.utils.fun_to_numpy` (PR #731) - Added to each example in the examples gallery the information about the release version in which it was introduced (PR #743) - Removed release information from quickstart guide (PR #744) - Implement batch parallel solvers in ot.batch (PR #745) - Update REAMDE with new API and reorganize examples (PR #754) - Speedup and update tests and wheels (PR #759) #### Closed issues - Fixed `ot.mapping` solvers which depended on deprecated `cvxpy` `ECOS` solver (PR #692, Issue #668) - Fixed numerical errors in `ot.gmm` (PR #690, Issue #689) - Add version number to the documentation (PR #696) - Update doc for default regularization in `ot.unbalanced` sinkhorn solvers (Issue #691, PR #700) - Clean documentation for `gromov`, `lp` and `unbalanced` folders (PR #710) - Clean references in documentation (PR #722) - Clean documentation for `ot.gromov.gromov_wasserstein` (PR #737) - Debug wheels building (PR #739) - Fix doc for projection sparse simplex (PR #734, PR #746) - Changed the default behavior of `ot.lp.solver_1d.wasserstein_circle` (Issue #738) - Avoid raising unnecessary warnings in `ot.lp.solver_1d.binary_search_circle` (Issue #738) - Avoid deprecation warning in `ot.lp.solver_1d.wasserstein_1d` (Issue #760, PR #761) ## 0.9.5 *November 2024* This new release contains several new features, starting with a novel [Gaussian Mixture Model Optimal Transport (GMM-OT)](https://pythonot.github.io/master/gen_modules/ot.gmm.html#examples-using-ot-gmm-gmm-ot-apply-map) solver to compare GMM while enforcing the transport plan to remain a GMM, that benefits from a closed-form solution making it practical for high-dimensional matching problems. We also extended our general unbalanced OT solvers to support any non-negative reference measure in the regularization terms, before adding the novel [translation invariant UOT](https://pythonot.github.io/master/auto_examples/unbalanced-partial/plot_conv_sinkhorn_ti.html) solver showcasing a higher convergence speed. We also implemented several new solvers and enhanced existing ones to perform OT across spaces. These include a [semi-relaxed FGW barycenter](https://pythonot.github.io/master/auto_examples/gromov/plot_semirelaxed_gromov_wasserstein_barycenter.html) solver, coupled with new initialization heuristics for the inner divergence computation, to perform graph partitioning or dictionary learning. Followed by novel [unbalanced FGW and Co-optimal transport](https://pythonot.github.io/master/auto_examples/others/plot_outlier_detection_with_COOT_and_unbalanced_COOT.html) solvers to promote robustness to outliers in such matching problems. And we finally updated the implementation of partial GW now supporting asymmetric structures and the KL divergence, while leveraging a new generic conditional gradient solver for partial transport problems enabling significant speed improvements. These latest updates required some modifications to the line search functions of our generic conditional gradient solver, paving the way for future improvements to other GW-based solvers. Last but not least, we implemented a pre-commit scheme to automatically correct common programming mistakes likely to be made by our future contributors. This release also contains few bug fixes, concerning the support of any metric in `ot.emd_1d` / `ot.emd2_1d`, and the support of any weights in `ot.gaussian`. #### Breaking change - Custom functions provided as parameter `line_search` to `ot.optim.generic_conditional_gradient` must now have the signature `line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs)`, adding as input `df_G` the gradient of the regularizer evaluated at the transport plan `G`. This change aims at improving speed of solvers having quadratic polynomial functions as regularizer such as the Gromov-Wassertein loss (PR #663). #### New features - New linter based on pre-commit using ruff, codespell and yamllint (PR #681) - Added feature `mass=True` for `nx.kl_div` (PR #654) - Implemented Gaussian Mixture Model OT `ot.gmm` (PR #649) - Added feature `semirelaxed_fgw_barycenters` and generic FGW-related barycenter updates `update_barycenter_structure` and `update_barycenter_feature` (PR #659) - Added initialization heuristics for sr(F)GW problems via `semirelaxed_init_plan`, integrated in all sr(F)GW solvers (PR #659) - Improved `ot.plot.plot1D_mat` (PR #649) - Added `nx.det` (PR #649) - `nx.sqrtm` is now broadcastable (takes ..., d, d) inputs (PR #649) - Restructured `ot.unbalanced` module (PR #658) - Added `ot.unbalanced.lbfgsb_unbalanced2` and add flexible reference measure `c` in all unbalanced solvers (PR #658) - Implemented Fused unbalanced Gromov-Wasserstein and unbalanced Co-Optimal Transport (PR #677) - Notes before depreciating partial Gromov-Wasserstein function in `ot.partial` moved to ot.gromov (PR #663) - Create `ot.gromov._partial` add new features `loss_fun = "kl_loss"` and `symmetry=False` to all solvers while increasing speed + updating adequatly `ot.solvers` (PR #663) - Added `ot.unbalanced.sinkhorn_unbalanced_translation_invariant` (PR #676) #### Closed issues - Fixed `ot.gaussian` ignoring weights when computing means (PR #649, Issue #648) - Fixed `ot.emd_1d` and `ot.emd2_1d` incorrectly allowing any metric (PR #670, Issue #669) ## 0.9.4 *June 2024* This new release contains several new features and bug fixes. Among the new features we have novel [Quantized FGW](https://pythonot.github.io/auto_examples/gromov/plot_quantized_gromov_wasserstein.html) solvers that can be used to speed up the computation of the FGW loss on large datasets or to promote a structure on the pairwise matrices. We also updated the continuous entropic mapping to provide efficient out-of-sample continuous mapping thanks to entropic regularization. We also have a new general unbalanced solvers for `ot.solve` and BFGS solver and illustrative example. Finally we have a new solver for the [Low Rank Gromov-Wasserstein](https://pythonot.github.io/auto_examples/others/plot_lowrank_GW.html) that can be used to compute the GW distance between two large scale datasets with a low rank approximation. From a maintenance point of view, we now have a new option to install optional dependencies with `pip install POT[all]` and the specific backends or submodules' dependencies may also be installed individually. The pip options are: `backend-jax, backend-tf, backend-torch, cvxopt, dr, gnn, plot, all`. We also provide with this release support for NumPy 2.0 (the wheels should now be compatible with NumPy 2.0 and below). We also fixed several issues such as gradient sign errors for FGW solvers, empty weights for `ot.emd2`, and line-search in partial GW. We also split the `test/test_gromov.py` into `test/gromov/` to make the tests more manageable. #### New features + NumPy 2.0 support is added (PR #629) + New quantized FGW solvers `ot.gromov.quantized_fused_gromov_wasserstein`, `ot.gromov.quantized_fused_gromov_wasserstein_samples` and `ot.gromov.quantized_fused_gromov_wasserstein_partitioned` (PR #603) + `ot.gromov._gw.solve_gromov_linesearch` now has an argument to specify if the matrices are symmetric in which case the computation can be done faster (PR #607). + Continuous entropic mapping (PR #613) + New general unbalanced solvers for `ot.solve` and BFGS solver and illustrative example (PR #620) + Add gradient computation with envelope theorem to sinkhorn solver of `ot.solve` with `grad='envelope'` (PR #605). + Added support for [Low rank Gromov-Wasserstein](https://proceedings.mlr.press/v162/scetbon22b/scetbon22b.pdf) with `ot.gromov.lowrank_gromov_wasserstein_samples` (PR #614) + Optional dependencies may now be installed with `pip install POT[all]` The specific backends or submodules' dependencies may also be installed individually. The pip options are: `backend-jax, backend-tf, backend-torch, cvxopt, dr, gnn, all`. The installation of the `cupy` backend should be done with conda. #### Closed issues - Fix gpu compatibility of sr(F)GW solvers when `G0 is not None`(PR #596) - Fix doc and example for lowrank sinkhorn (PR #601) - Fix issue with empty weights for `ot.emd2` (PR #606, Issue #534) - Fix a sign error regarding the gradient of `ot.gromov._gw.fused_gromov_wasserstein2` and `ot.gromov._gw.gromov_wasserstein2` for the kl loss (PR #610) - Fix same sign error for sr(F)GW conditional gradient solvers (PR #611) - Split `test/test_gromov.py` into `test/gromov/` (PR #619) - Fix (F)GW barycenter functions to support computing barycenter on 1 input + deprecate structures as lists (PR #628) - Fix line-search in partial GW and change default init to the interior of partial transport plans (PR #602) - Fix `ot.da.sinkhorn_lpl1_mm` compatibility with JAX (PR #592) - Fiw linesearch import error on Scipy 1.14 (PR #642, Issue #641) - Upgrade supported JAX versions from jax<=0.4.24 to jax<=0.4.30 (PR #643) ## 0.9.3 *January 2024* #### Closed issues - Fixed an issue with cost correction for mismatched labels in `ot.da.BaseTransport` fit methods. This fix addresses the original issue introduced PR #587 (PR #593) ## 0.9.2 *December 2023* This new release contains several new features and bug fixes. Among the new features we have a new solver for estimation of nearest Brenier potentials (SSNB) that can be used for OT mapping estimation (on small problems), new Bregman Alternated Projected Gradient solvers for GW and FGW, and new solvers for Bures-Wasserstein barycenters. We also provide a first solver for Low Rank Sinkhorn that will be ussed to provide low rak OT extensions in the next releases. Finally we have a new exact line-search for (F)GW solvers with KL loss that can be used to improve the convergence of the solvers. We also have a new `LazyTensor` class that can be used to model OT plans and low rank tensors in large scale OT. This class is used to return the plan for the new wrapper for `geomloss` Sinkhorn solver on empirical samples that can lead to x10/x100 speedups on CPU or GPU and have a lazy implementation that allows solving very large problems of a few millions samples. We also have a new API for solving OT problems from empirical samples with `ot.solve_sample` Finally we have a new API for Gromov-Wasserstein solvers with `ot.solve_gromov` function that centralizes most of the (F)GW methods with unified notation. Some example of how to use the new API below: ```python # Generate random data xs, xt = np.random.randn(100, 2), np.random.randn(50, 2) # Solve OT problem with empirical samples sol = ot.solve_sample(xs, xt) # Exact OT betwen smaples with uniform weights sol = ot.solve_sample(xs, xt, wa, wb) # Exact OT with weights given by user sol = ot.solve_sample(xs, xt, reg= 1, metric='euclidean') # sinkhorn with euclidean metric sol = ot.solve_sample(xs, xt, reg= 1, method='geomloss') # faster sinkhorn solver on CPU/GPU sol = ot.solve_sample(x,x2, method='factored', rank=10) # compute factored OT sol = ot.solve_sample(x,x2, method='lowrank', rank=10) # compute lowrank sinkhorn OT value_bw = ot.solve_sample(xs, xt, method='gaussian').value # Bures-Wasserstein distance # Solve GW problem Cs, Ct = ot.dist(xs, xs), ot.dist(xt, xt) # compute cost matrices sol = ot.solve_gromov(Cs,Ct) # Exact GW between samples with uniform weights # Solve FGW problem M = ot.dist(xs, xt) # compute cost matrix # Exact FGW between samples with uniform weights sol = ot.solve_gromov(Cs, Ct, M, loss='KL', alpha=0.7) # FGW with KL data fitting # recover solutions objects P = sol.plan # OT plan u, v = sol.potentials # dual variables value = sol.value # OT value # for GW and FGW value_linear = sol.value_linear # linear part of the loss value_quad = sol.value_quad # quadratic part of the loss ``` Users are encouraged to use the new API (it is much simpler) but it might still be subjects to small changes before the release of POT 1.0 . We also fixed a number of issues, the most pressing being a problem of GPU memory allocation when pytorch is installed that will not happen now thanks to Lazy initialization of the backends. We now also have the possibility to deactivate some backends using environment which prevents POT from importing them and can lead to large import speedup. #### New features + Added support for [Nearest Brenier Potentials (SSNB)](http://proceedings.mlr.press/v108/paty20a/paty20a.pdf) (PR #526) + minor fix (PR #535) + Tweaked `get_backend` to ignore `None` inputs (PR #525) + Callbacks for generalized conditional gradient in `ot.da.sinkhorn_l1l2_gl` are now vectorized to improve performance (PR #507) + The `linspace` method of the backends now has the `type_as` argument to convert to the same dtype and device. (PR #533) + The `convolutional_barycenter2d` and `convolutional_barycenter2d_debiased` functions now work with different devices.. (PR #533) + New API for Gromov-Wasserstein solvers with `ot.solve_gromov` function (PR #536) + New LP solvers from scipy used by default for LP barycenter (PR #537) + Update wheels to Python 3.12 and remove old i686 arch that do not have scipy wheels (PR #543) + Upgraded unbalanced OT solvers for more flexibility (PR #539) + Add LazyTensor for modeling plans and low rank tensor in large scale OT (PR #544) + Add exact line-search for `gromov_wasserstein` and `fused_gromov_wasserstein` with KL loss (PR #556) + Add KL loss to all semi-relaxed (Fused) Gromov-Wasserstein solvers (PR #559) + Further upgraded unbalanced OT solvers for more flexibility and future use (PR #551) + New API function `ot.solve_sample` for solving OT problems from empirical samples (PR #563) + Wrapper for `geomloss`` solver on empirical samples (PR #571) + Add `stop_criterion` feature to (un)regularized (f)gw barycenter solvers (PR #578) + Add `fixed_structure` and `fixed_features` to entropic fgw barycenter solver (PR #578) + Add new BAPG solvers with KL projections for GW and FGW (PR #581) + Add Bures-Wasserstein barycenter in `ot.gaussian` and example (PR #582, PR #584) + Domain adaptation method `SinkhornL1l2Transport` now supports JAX backend (PR #587) + Added support for [Low-Rank Sinkhorn Factorization](https://arxiv.org/pdf/2103.04737.pdf) (PR #568) #### Closed issues - Fix line search evaluating cost outside of the interpolation range (Issue #502, PR #504) - Lazily instantiate backends to avoid unnecessary GPU memory pre-allocations on package import (Issue #516, PR #520) - Handle documentation and warnings when integers are provided to (f)gw solvers based on cg (Issue #530, PR #559) - Correct independence of `fgw_barycenters` to `init_C` and `init_X` (Issue #547, PR #566) - Avoid precision change when computing norm using PyTorch backend (Discussion #570, PR #572) - Create `ot/bregman/`repository (Issue #567, PR #569) - Fix matrix feature shape in `entropic_fused_gromov_barycenters`(Issue #574, PR #573) - Fix (fused) gromov-wasserstein barycenter solvers to support `kl_loss`(PR #576) ## 0.9.1 *August 2023* This new release contains several new features and bug fixes. New features include a new submodule `ot.gnn` that contains two new Graph neural network layers (compatible with [Pytorch Geometric](https://pytorch-geometric.readthedocs.io/)) for template-based pooling of graphs with an example on [graph classification](https://pythonot.github.io/master/auto_examples/gromov/plot_gnn_TFGW.html). Related to this, we also now provide FGW and semi relaxed FGW solvers for which the resulting loss is differentiable w.r.t. the parameter `alpha`. Other contributions on the (F)GW front include a new solver for the Proximal Point algorithm [that can be used to solve entropic GW problems](https://pythonot.github.io/master/auto_examples/gromov/plot_fgw_solvers.html) (using the parameter `solver="PPA"`), new solvers for entropic FGW barycenters, novels Sinkhorn-based solvers for entropic semi-relaxed (F)GW, the possibility to provide a warm-start to the solvers, and optional marginal weights of the samples (uniform weights ar used by default). Finally we added in the submodule `ot.gaussian` and `ot.da` new loss and mapping estimators for the Gaussian Gromov-Wasserstein that can be used as a fast alternative to GW and estimates linear mappings between unregistered spaces that can potentially have different size (See the update [linear mapping example](https://pythonot.github.io/master/auto_examples/domain-adaptation/plot_otda_linear_mapping.html) for an illustration). We also provide a new solver for the [Entropic Wasserstein Component Analysis](https://pythonot.github.io/master/auto_examples/others/plot_EWCA.html) that is a generalization of the celebrated PCA taking into account the local neighborhood of the samples. We also now have a new solver in `ot.smooth` for the [sparsity-constrained OT (last plot)](https://pythonot.github.io/master/auto_examples/plot_OT_1D_smooth.html) that can be used to find regularized OT plans with sparsity constraints. Finally we have a first multi-marginal solver for regular 1D distributions with a Monge loss (see [here](https://pythonot.github.io/master/auto_examples/others/plot_dmmot.html)). The documentation and testings have also been updated. We now have nearly 95% code coverage with the tests. The documentation has been updated and some examples have been streamlined to build more quickly and avoid timeout problems with CircleCI. We also added an optional CI on GPU for the master branch and approved PRs that can be used when a GPU runner is online. Many other bugs and issues have been fixed and we want to thank all the contributors, old and new, who made this release possible. More details below. #### New features - Added Bures Wasserstein distance in `ot.gaussian` (PR ##428) - Added Generalized Wasserstein Barycenter solver + example (PR #372), fixed graphical details on the example (PR #376) - Added Free Support Sinkhorn Barycenter + example (PR #387) - New API for OT solver using function `ot.solve` (PR #388) - Backend version of `ot.partial` and `ot.smooth` (PR #388) - Added argument for warmstart of dual vectors in Sinkhorn-based methods in `ot.bregman` (PR #437) #### Closed issues - Fixed an issue with the documentation gallery sections (PR #395) - Fixed an issue where sinkhorn divergence did not have a gradients (Issue #393, PR #394) - Fixed an issue where we could not ask TorchBackend to place a random tensor on GPU (Issue #371, PR #373) - Fixed an issue where Sinkhorn solver assumed a symmetric cost matrix (Issue #374, PR #375) - Fixed an issue where hitting iteration limits would be reported to stderr by std::cerr regardless of Python's stderr stream status (PR #377) - Fixed an issue where the metric argument in ot.dist did not allow a callable parameter (Issue #378, PR #379) - Fixed an issue where the max number of iterations in ot.emd was not allowed to go beyond 2^31 (PR #380) - Fixed an issue where pointers would overflow in the EMD solver, returning an incomplete transport plan above a certain size (slightly above 46k, its square being roughly 2^31) (PR #381) - Error raised when mass mismatch in emd2 (PR #386) - Fixed an issue where a pytorch example would throw an error if executed on a GPU (Issue #389, PR #391) - Added a work-around for scipy's bug, where you cannot compute the Hamming distance with a "None" weight attribute. (Issue #400, PR #402) - Fixed an issue where the doc could not be built due to some changes in matplotlib's API (Issue #403, PR #402) - Replaced Numpy C Compiler with Setuptools C Compiler due to deprecation issues (Issue #408, PR #409) - Fixed weak optimal transport docstring (Issue #404, PR #410) - Fixed error with parameter `log=True`for `SinkhornLpl1Transport` (Issue #412, PR #413) - Fixed an issue about `warn` parameter in `sinkhorn2` (PR #417) - Fix an issue where the parameter `stopThr` in `empirical_sinkhorn_divergence` was rendered useless by subcalls that explicitly specified `stopThr=1e-9` (Issue #421, PR #422). - Fixed a bug breaking an example where we would try to make an array of arrays of different shapes (Issue #424, PR #425) ## 0.8.2 This releases introduces several new notable features. The less important but most exiting one being that we now have a logo for the toolbox (color and dark background) : ![](https://pythonot.github.io/master/_images/logo.svg)![](https://pythonot.github.io/master/_static/logo_dark.svg) This logo is generated using with matplotlib and using the solution of an OT problem provided by POT (with `ot.emd`). Generating the logo can be done with a simple python script also provided in the [documentation gallery](https://pythonot.github.io/auto_examples/others/plot_logo.html#sphx-glr-auto-examples-others-plot-logo-py). New OT solvers include [Weak OT](https://pythonot.github.io/gen_modules/ot.weak.html#ot.weak.weak_optimal_transport) and [OT with factored coupling](https://pythonot.github.io/gen_modules/ot.factored.html#ot.factored.factored_optimal_transport) that can be used on large datasets. The [Majorization Minimization](https://pythonot.github.io/gen_modules/ot.unbalanced.html?highlight=mm_#ot.unbalanced.mm_unbalanced) solvers for non-regularized Unbalanced OT are now also available. We also now provide an implementation of [GW and FGW unmixing](https://pythonot.github.io/gen_modules/ot.gromov.html#ot.gromov.gromov_wasserstein_linear_unmixing) and [dictionary learning](https://pythonot.github.io/gen_modules/ot.gromov.html#ot.gromov.gromov_wasserstein_dictionary_learning). It is now possible to use autodiff to solve entropic an quadratic regularized OT in the dual for full or stochastic optimization thanks to the new functions to compute the dual loss for [entropic](https://pythonot.github.io/gen_modules/ot.stochastic.html#ot.stochastic.loss_dual_entropic) and [quadratic](https://pythonot.github.io/gen_modules/ot.stochastic.html#ot.stochastic.loss_dual_quadratic) regularized OT and reconstruct the [OT plan](https://pythonot.github.io/gen_modules/ot.stochastic.html#ot.stochastic.plan_dual_entropic) on part or all of the data. They can be used for instance to solve OT problems with stochastic gradient or for estimating the [dual potentials as neural networks](https://pythonot.github.io/auto_examples/backends/plot_stoch_continuous_ot_pytorch.html#sphx-glr-auto-examples-backends-plot-stoch-continuous-ot-pytorch-py). On the backend front, we now have backend compatible functions and classes in the domain adaptation [`ot.da`](https://pythonot.github.io/gen_modules/ot.da.html#module-ot.da) and unbalanced OT [`ot.unbalanced`](https://pythonot.github.io/gen_modules/ot.unbalanced.html) modules. This means that the DA classes can be used on tensors from all compatible backends. The [free support Wasserstein barycenter](https://pythonot.github.io/gen_modules/ot.lp.html?highlight=free%20support#ot.lp.free_support_barycenter) solver is now also backend compatible. Finally we have worked on the documentation to provide an update of existing examples in the gallery and and several new examples including [GW dictionary learning](https://pythonot.github.io/auto_examples/gromov/plot_gromov_wasserstein_dictionary_learning.html#sphx-glr-auto-examples-gromov-plot-gromov-wasserstein-dictionary-learning-py) [weak Optimal Transport](https://pythonot.github.io/auto_examples/others/plot_WeakOT_VS_OT.html#sphx-glr-auto-examples-others-plot-weakot-vs-ot-py), [NN based dual potentials estimation](https://pythonot.github.io/auto_examples/backends/plot_stoch_continuous_ot_pytorch.html#sphx-glr-auto-examples-backends-plot-stoch-continuous-ot-pytorch-py) and [Factored coupling OT](https://pythonot.github.io/auto_examples/others/plot_factored_coupling.html#sphx-glr-auto-examples-others-plot-factored-coupling-py). . #### New features - Remove deprecated `ot.gpu` submodule (PR #361) - Update examples in the gallery (PR #359) - Add stochastic loss and OT plan computation for regularized OT and backend examples(PR #360) - Implementation of factored OT with emd and sinkhorn (PR #358) - A brand new logo for POT (PR #357) - Better list of related examples in quick start guide with `minigallery` (PR #334) - Add optional log-domain Sinkhorn implementation in WDA to support smaller values of the regularization parameter (PR #336) - Backend implementation for `ot.lp.free_support_barycenter` (PR #340) - Add weak OT solver + example (PR #341) - Add backend support for Domain Adaptation and Unbalanced solvers (PR #343) - Add (F)GW linear dictionary learning solvers + example (PR #319) - Add links to related PR and Issues in the doc release page (PR #350) - Add new minimization-maximization algorithms for solving exact Unbalanced OT + example (PR #362) #### Closed issues - Fix mass gradient of `ot.emd2` and `ot.gromov_wasserstein2` so that they are centered (Issue #364, PR #363) - Fix bug in instantiating an `autograd` function `ValFunction` (Issue #337, PR #338) - Fix POT ABI compatibility with old and new numpy (Issue #346, PR #349) - Warning when feeding integer cost matrix to EMD solver resulting in an integer transport plan (Issue #345, PR #343) - Fix bug where gromov_wasserstein2 does not perform backpropagation with CUDA tensors (Issue #351, PR #352) ## 0.8.1.0 *December 2021* This is a bug fix release that will remove the `benchmarks` module form the installation and correct the documentation generation. #### Closed issues - Bug in documentation generation (tag VS master push, PR #332) - Remove installation of the benchmarks in global namespace (Issue #331, PR #333) ## 0.8.1 *December 2021* This release fixes several bugs and introduces two new backends: Cupy and Tensorflow. Note that the tensorflow backend will work only when tensorflow has enabled the Numpy behavior (for transpose that is not by default in tensorflow). We also introduce a simple benchmark on CPU GPU for the sinkhorn solver that will be provided in the [backend](https://pythonot.github.io/gen_modules/ot.backend.html) documentation. This release also brings a few changes in dependencies and compatibility. First we removed tests for Python 3.6 that will not be updated in the future. Also note that POT now depends on Numpy (>= 1.20) because a recent change in ABI is making the wheels non-compatible with older numpy versions. If you really need an older numpy POT will work with no problems but you will need to build it from source. As always we want to that the contributors who helped make POT better (and bug free). #### New features - New benchmark for sinkhorn solver on CPU/GPU and between backends (PR #316) - New tensorflow backend (PR #316) - New Cupy backend (PR #315) - Documentation always up-to-date with README, RELEASES, CONTRIBUTING and CODE_OF_CONDUCT files (PR #316, PR #322). #### Closed issues - Fix bug in older Numpy ABI (<1.20) (Issue #308, PR #326) - Fix bug in `ot.dist` function when non euclidean distance (Issue #305, PR #306) - Fix gradient scaling for functions using `nx.set_gradients` (Issue #309, PR #310) - Fix bug in generalized Conditional gradient solver and SinkhornL1L2 (Issue #311, PR #313) - Fix log error in `gromov_barycenters` (Issue #317, PR #3018) ## 0.8.0 *November 2021* This new stable release introduces several important features. First we now have an OpenMP compatible exact ot solver in `ot.emd`. The OpenMP version is used when the parameter `numThreads` is greater than one and can lead to nice speedups on multi-core machines. Second we have introduced a backend mechanism that allows to use standard POT function seamlessly on Numpy, Pytorch and Jax arrays. Other backends are coming but right now POT can be used seamlessly for training neural networks in Pytorch. Notably we propose the first differentiable computation of the exact OT loss with `ot.emd2` (can be differentiated w.r.t. both cost matrix and sample weights), but also for the classical Sinkhorn loss with `ot.sinkhorn2`, the Wasserstein distance in 1D with `ot.wasserstein_1d`, sliced Wasserstein with `ot.sliced_wasserstein_distance` and Gromov-Wasserstein with `ot.gromov_wasserstein2`. Examples of how this new feature can be used are now available in the documentation where the Pytorch backend is used to estimate a [minimal Wasserstein estimator](https://PythonOT.github.io/auto_examples/backends/plot_unmix_optim_torch.html), a [Generative Network (GAN)](https://PythonOT.github.io/auto_examples/backends/plot_wass2_gan_torch.html), for a [sliced Wasserstein gradient flow](https://PythonOT.github.io/auto_examples/backends/plot_sliced_wass_grad_flow_pytorch.html) and [optimizing the Gromov-Wassersein distance](https://PythonOT.github.io/auto_examples/backends/plot_optim_gromov_pytorch.html). Note that the Jax backend is still in early development and quite slow at the moment, we strongly recommend for Jax users to use the [OTT toolbox](https://github.com/google-research/ott) when possible. As a result of this new feature, the old `ot.gpu` submodule is now deprecated since GPU implementations can be done using GPU arrays on the torch backends. Other novel features include implementation for [Sampled Gromov Wasserstein and Pointwise Gromov Wasserstein](https://PythonOT.github.io/auto_examples/gromov/plot_gromov.html#compute-gw-with-a-scalable-stochastic-method-with-any-loss-function), Sinkhorn in log space with `method='sinkhorn_log'`, [Projection Robust Wasserstein](https://PythonOT.github.io/gen_modules/ot.dr.html?highlight=robust#ot.dr.projection_robust_wasserstein), ans [deviased Sinkorn barycenters](https://PythonOT.github.ioauto_examples/barycenters/plot_debiased_barycenter.html). This release will also simplify the installation process. We have now a `pyproject.toml` that defines the build dependency and POT should now build even when cython is not installed yet. Also we now provide pe-compiled wheels for linux `aarch64` that is used on Raspberry PI and android phones and for MacOS on ARM processors. Finally POT was accepted for publication in the Journal of Machine Learning Research (JMLR) open source software track and we ask the POT users to cite [this paper](https://www.jmlr.org/papers/v22/20-451.html) from now on. The documentation has been improved in particular by adding a "Why OT?" section to the quick start guide and several new examples illustrating the new features. The documentation now has two version : the stable version [https://pythonot.github.io/](https://pythonot.github.io/) corresponding to the last release and the master version [https://pythonot.github.io/master](https://pythonot.github.io/master) that corresponds to the current master branch on GitHub. As usual, we want to thank all the POT contributors (now 37 people have contributed to the toolbox). But for this release we thank in particular Nathan Cassereau and Kamel Guerda from the AI support team at [IDRIS](http://www.idris.fr/) for their support to the development of the backend and OpenMP implementations. #### New features - OpenMP support for exact OT solvers (PR #260) - Backend for running POT in numpy/torch + exact solver (PR #249) - Backend implementation of most functions in `ot.bregman` (PR #280) - Backend implementation of most functions in `ot.optim` (PR #282) - Backend implementation of most functions in `ot.gromov` (PR #294, PR #302) - Test for arrays of different type and device (CPU/GPU) (PR #304, #303) - Implementation of Sinkhorn in log space with `method='sinkhorn_log'` (PR #290) - Implementation of regularization path for L2 Unbalanced OT (PR #274) - Implementation of Projection Robust Wasserstein (PR #267) - Implementation of Debiased Sinkhorn Barycenters (PR #291) - Implementation of Sampled Gromov Wasserstein and Pointwise Gromov Wasserstein (PR #275) - Add `pyproject.toml` and build POT without installing cython first (PR #293) - Lazy implementation in log space for sinkhorn on samples (PR #259) - Documentation cleanup (PR #298) - Two up-to-date documentations [for stable release](https://PythonOT.github.io/) and for [master branch](https://pythonot.github.io/master/). - Building wheels on ARM for Raspberry PI and smartphones (PR #238) - Update build wheels to new version and new pythons (PR #236, #253) - Implementation of sliced Wasserstein distance (Issue #202, PR #203) - Add minimal build to CI and perform pep8 test separately (PR #210) - Speedup of tests and return run time (PR #262) - Add "Why OT" discussion to the documentation (PR #220) - New introductory example to discrete OT in the documentation (PR #191) - Add templates for Issues/PR on Github (PR#181) #### Closed issues - Debug Memory leak in GAN example (#254) - DEbug GPU bug (Issue #284, #287, PR #288) - set_gradients method for JAX backend (PR #278) - Quicker GAN example for CircleCI build (PR #258) - Better formatting in Readme (PR #234) - Debug CI tests (PR #240, #241, #242) - Bug in Partial OT solver dummy points (PR #215) - Bug when Armijo linesearch (Issue #184, #198, #281, PR #189, #199, #286) - Bug Barycenter Sinkhorn (Issue 134, PR #195) - Infeasible solution in exact OT (Issues #126,#93, PR #217) - Doc for SUpport Barycenters (Issue #200, PR #201) - Fix labels transport in BaseTransport (Issue #207, PR #208) - Bug in `emd_1d`, non respected bounds (Issue #169, PR #170) - Removed Python 2.7 support and update codecov file (PR #178) - Add normalization for WDA and test it (PR #172, #296) - Cleanup code for new version of `flake8` (PR #176) - Fixed requirements in `setup.py` (PR #174) - Removed specific MacOS flags (PR #175) ## 0.7.0 *May 2020* This is the new stable release for POT. We made a lot of changes in the documentation and added several new features such as Partial OT, Unbalanced and Multi Sources OT Domain Adaptation and several bug fixes. One important change is that we have created the GitHub organization [PythonOT](https://github.com/PythonOT) that now owns the main POT repository [https://github.com/PythonOT/POT](https://github.com/PythonOT/POT) and the repository for the new documentation is now hosted at [https://PythonOT.github.io/](https://PythonOT.github.io/). This is the first release where the Python 2.7 tests have been removed. Most of the toolbox should still work but we do not offer support for Python 2.7 and will close related Issues. A lot of changes have been done to the documentation that is now hosted on [https://PythonOT.github.io/](https://PythonOT.github.io/) instead of readthedocs. It was a hard choice but readthedocs did not allow us to run sphinx-gallery to update our beautiful examples and it was a huge amount of work to maintain. The documentation is now automatically compiled and updated on merge. We also removed the notebooks from the repository for space reason and also because they are all available in the [example gallery](https://pythonot.github.io/auto_examples/index.html). Note that now the output of the documentation build for each commit in the PR is available to check that the doc builds correctly before merging which was not possible with readthedocs. The CI framework has also been changed with a move from Travis to Github Action which allows to get faster tests on Windows, MacOS and Linux. We also now report our coverage on [Codecov.io](https://codecov.io/gh/PythonOT/POT) and we have a reasonable 92% coverage. We also now generate wheels for a number of OS and Python versions at each merge in the master branch. They are available as outputs of this [action](https://github.com/PythonOT/POT/actions?query=workflow%3A%22Build+dist+and+wheels%22). This will allow simpler multi-platform releases from now on. In terms of new features we now have [OTDA Classes for unbalanced OT](https://pythonot.github.io/gen_modules/ot.da.html#ot.da.UnbalancedSinkhornTransport), a new Domain adaptation class form [multi domain problems (JCPOT)](https://pythonot.github.io/auto_examples/domain-adaptation/plot_otda_jcpot.html#sphx-glr-auto-examples-domain-adaptation-plot-otda-jcpot-py), and several solvers to solve the [Partial Optimal Transport](https://pythonot.github.io/auto_examples/unbalanced-partial/plot_partial_wass_and_gromov.html#sphx-glr-auto-examples-unbalanced-partial-plot-partial-wass-and-gromov-py) problems. This release is also the moment to thank all the POT contributors (old and new) for helping making POT such a nice toolbox. A lot of changes (also in the API) are coming for the next versions. #### Features - New documentation on [https://PythonOT.github.io/](https://PythonOT.github.io/) (PR #160, PR #143, PR #144) - Documentation build on CircleCI with sphinx-gallery (PR #145,PR #146, #155) - Run sphinx gallery in CI (PR #146) - Remove notebooks from repo because available in doc (PR #156) - Build wheels in CI (#157) - Move from travis to GitHub Action for Windows, MacOS and Linux (PR #148, PR #150) - Partial Optimal Transport (PR#141 and PR #142) - Laplace regularized OTDA (PR #140) - Multi source DA with target shift (PR #137) - Screenkhorn algorithm (PR #121) #### Closed issues - Add JMLR paper to the readme and Mathieu Blondel to the Acknoledgments (PR #231, #232) - Bug in Unbalanced OT example (Issue #127) - Clean Cython output when calling setup.py clean (Issue #122) - Various Macosx compilation problems (Issue #113, Issue #118, PR#130) - EMD dimension mismatch (Issue #114, Fixed in PR #116) - 2D barycenter bug for non square images (Issue #124, fixed in PR #132) - Bad value in EMD 1D (Issue #138, fixed in PR #139) - Log bugs for Gromov-Wassertein solver (Issue #107, fixed in PR #108) - Weight issues in barycenter function (PR #106) ## 0.6.0 *July 2019* This is the first official stable release of POT and this means a jump to 0.6! The library has been used in the wild for a while now and we have reached a state where a lot of fundamental OT solvers are available and tested. It has been quite stable in the last months but kept the beta flag in its Pypi classifiers until now. Note that this release will be the last one supporting officially Python 2.7 (See https://python3statement.org/ for more reasons). For next release we will keep the travis tests for Python 2 but will make them non necessary for merge in 2020. The features are never complete in a toolbox designed for solving mathematical problems and research but with the new contributions we now implement algorithms and solvers from 24 scientific papers (listed in the README.md file). New features include a direct implementation of the [empirical Sinkhorn divergence](https://pot.readthedocs.io/en/latest/all.html#ot.bregman.empirical_sinkhorn_divergence), a new efficient (Cython implementation) solver for [EMD in 1D](https://pot.readthedocs.io/en/latest/all.html#ot.lp.emd_1d) and corresponding [Wasserstein 1D](https://pot.readthedocs.io/en/latest/all.html#ot.lp.wasserstein_1d). We now also have implementations for [Unbalanced OT](https://github.com/rflamary/POT/blob/master/notebooks/plot_UOT_1D.ipynb) and a solver for [Unbalanced OT barycenters](https://github.com/rflamary/POT/blob/master/notebooks/plot_UOT_barycenter_1D.ipynb). A new variant of Gromov-Wasserstein divergence called [Fused Gromov-Wasserstein](https://pot.readthedocs.io/en/latest/all.html?highlight=fused_#ot.gromov.fused_gromov_wasserstein) has been also contributed with exemples of use on [structured data](https://github.com/rflamary/POT/blob/master/notebooks/plot_fgw.ipynb) and computing [barycenters of labeld graphs](https://github.com/rflamary/POT/blob/master/notebooks/plot_barycenter_fgw.ipynb). A lot of work has been done on the documentation with several new examples corresponding to the new features and a lot of corrections for the docstrings. But the most visible change is a new [quick start guide](https://pot.readthedocs.io/en/latest/quickstart.html) for POT that gives several pointers about which function or classes allow to solve which specific OT problem. When possible a link is provided to relevant examples. We will also provide with this release some pre-compiled Python wheels for Linux 64bit on github and pip. This will simplify the install process that before required a C compiler and numpy/cython already installed. Finally we would like to acknowledge and thank the numerous contributors of POT that has helped in the past build the foundation and are still contributing to bring new features and solvers to the library. #### Features * Add compiled manylinux 64bits wheels to pip releases (PR #91) * Add quick start guide (PR #88) * Make doctest work on travis (PR #90) * Update documentation (PR #79, PR #84) * Solver for EMD in 1D (PR #89) * Solvers for regularized unbalanced OT (PR #87, PR#99) * Solver for Fused Gromov-Wasserstein (PR #86) * Add empirical Sinkhorn and empirical Sinkhorn divergences (PR #80) #### Closed issues - Issue #59 fail when using "pip install POT" (new details in doc+ hopefully wheels) - Issue #85 Cannot run gpu modules - Issue #75 Greenkhorn do not return log (solved in PR #76) - Issue #82 Gromov-Wasserstein fails when the cost matrices are slightly different - Issue #72 Macosx build problem ## 0.5.0 *Sep 2018* POT is 2 years old! This release brings numerous new features to the toolbox as listed below but also several bug correction. Among the new features, we can highlight a [non-regularized Gromov-Wasserstein solver](https://github.com/rflamary/POT/blob/master/notebooks/plot_gromov.ipynb), a new [greedy variant of sinkhorn](https://pot.readthedocs.io/en/latest/all.html#ot.bregman.greenkhorn), [non-regularized](https://pot.readthedocs.io/en/latest/all.html#ot.lp.barycenter), [convolutional (2D)](https://github.com/rflamary/POT/blob/master/notebooks/plot_convolutional_barycenter.ipynb) and [free support](https://github.com/rflamary/POT/blob/master/notebooks/plot_free_support_barycenter.ipynb) Wasserstein barycenters and [smooth](https://github.com/rflamary/POT/blob/prV0.5/notebooks/plot_OT_1D_smooth.ipynb) and [stochastic](https://pot.readthedocs.io/en/latest/all.html#ot.stochastic.sgd_entropic_regularization) implementation of entropic OT. POT 0.5 also comes with a rewriting of ot.gpu using the cupy framework instead of the unmaintained cudamat. Note that while we tried to keed changes to the minimum, the OTDA classes were deprecated. If you are happy with the cudamat implementation, we recommend you stay with stable release 0.4 for now. The code quality has also improved with 92% code coverage in tests that is now printed to the log in the Travis builds. The documentation has also been greatly improved with new modules and examples/notebooks. This new release is so full of new stuff and corrections thanks to the old and new POT contributors (you can see the list in the [readme](https://github.com/rflamary/POT/blob/master/README.md)). #### Features * Add non regularized Gromov-Wasserstein solver (PR #41) * Linear OT mapping between empirical distributions and 90\% test coverage (PR #42) * Add log parameter in class EMDTransport and SinkhornLpL1Transport (PR #44) * Add Markdown format for Pipy (PR #45) * Test for Python 3.5 and 3.6 on Travis (PR #46) * Non regularized Wasserstein barycenter with scipy linear solver and/or cvxopt (PR #47) * Rename dataset functions to be more sklearn compliant (PR #49) * Smooth and sparse Optimal transport implementation with entropic and quadratic regularization (PR #50) * Stochastic OT in the dual and semi-dual (PR #52 and PR #62) * Free support barycenters (PR #56) * Speed-up Sinkhorn function (PR #57 and PR #58) * Add convolutional Wassersein barycenters for 2D images (PR #64) * Add Greedy Sinkhorn variant (Greenkhorn) (PR #66) * Big ot.gpu update with cupy implementation (instead of un-maintained cudamat) (PR #67) #### Deprecation Deprecated OTDA Classes were removed from ot.da and ot.gpu for version 0.5 (PR #48 and PR #67). The deprecation message has been for a year here since 0.4 and it is time to pull the plug. #### Closed issues * Issue #35 : remove import plot from ot/__init__.py (See PR #41) * Issue #43 : Unusable parameter log for EMDTransport (See PR #44) * Issue #55 : UnicodeDecodeError: 'ascii' while installing with pip ## 0.4 *15 Sep 2017* This release contains a lot of contribution from new contributors. #### Features * Automatic notebooks and doc update (PR #27) * Add gromov Wasserstein solver and Gromov Barycenters (PR #23) * emd and emd2 can now return dual variables and have max_iter (PR #29 and PR #25) * New domain adaptation classes compatible with scikit-learn (PR #22) * Proper tests with pytest on travis (PR #19) * PEP 8 tests (PR #13) #### Closed issues * emd convergence problem du to fixed max iterations (#24) * Semi supervised DA error (#26) ## 0.3.1 *11 Jul 2017* * Correct bug in emd on windows ## 0.3 *7 Jul 2017* * emd* and sinkhorn* are now performed in parallel for multiple target distributions * emd and sinkhorn are for OT matrix computation * emd2 and sinkhorn2 are for OT loss computation * new notebooks for emd computation and Wasserstein Discriminant Analysis * relocate notebooks * update documentation * clean_zeros(a,b,M) for removimg zeros in sparse distributions * GPU implementations for sinkhorn and group lasso regularization ## V0.2 *7 Apr 2017* * New dimensionality reduction method (WDA) * Efficient method emd2 returns only tarnsport (in paralell if several histograms given) ## 0.1.11 *5 Jan 2017* * Add sphinx gallery for better documentation * Small efficiency tweak in sinkhorn * Add simple tic() toc() functions for timing ## 0.1.10 *7 Nov 2016* * numerical stabilization for sinkhorn (log domain and epsilon scaling) ## 0.1.9 *4 Nov 2016* * Update classes and examples for domain adaptation * Joint OT matrix and mapping estimation ## 0.1.7 *31 Oct 2016* * Original Domain adaptation classes ## 0.1.3 * pipy works ## First pre-release *28 Oct 2016* It provides the following solvers: * OT solver for the linear program/ Earth Movers Distance. * Entropic regularization OT solver with Sinkhorn Knopp Algorithm. * Bregman projections for Wasserstein barycenter [3] and unmixing. * Optimal transport for domain adaptation with group lasso regularization * Conditional gradient and Generalized conditional gradient for regularized OT. Some demonstrations (both in Python and Jupyter Notebook format) are available in the examples folder. python-pot-0.9.7+dfsg/benchmarks/000077500000000000000000000000001525521106200167035ustar00rootroot00000000000000python-pot-0.9.7+dfsg/benchmarks/__init__.py000066400000000000000000000001711525521106200210130ustar00rootroot00000000000000from . import benchmark from . import sinkhorn_knopp from . import emd __all__ = ["benchmark", "sinkhorn_knopp", "emd"] python-pot-0.9.7+dfsg/benchmarks/benchmark.py000066400000000000000000000074551525521106200212220ustar00rootroot00000000000000# /usr/bin/env python3 # -*- coding: utf-8 -*- from ot.backend import get_backend_list, jax, tf import gc def setup_backends(): if jax: from jax.config import config config.update("jax_enable_x64", True) if tf: from tensorflow.python.ops.numpy_ops import np_config np_config.enable_numpy_behavior() def exec_bench(setup, tested_function, param_list, n_runs, warmup_runs): backend_list = get_backend_list() for i, nx in enumerate(backend_list): if nx.__name__ == "tf" and i < len(backend_list) - 1: # Tensorflow should be the last one to be benchmarked because # as far as I'm aware, there is no way to force it to release # GPU memory. Hence, if any other backend is benchmarked after # Tensorflow and requires the usage of a GPU, it will not have the # full memory available and you may have a GPU Out Of Memory error # even though your GPU can technically hold your tensors in memory. backend_list.pop(i) backend_list.append(nx) break inputs = [setup(param) for param in param_list] results = dict() for nx in backend_list: for i in range(len(param_list)): print(nx, param_list[i]) args = inputs[i] results_nx = nx._bench( tested_function, *args, n_runs=n_runs, warmup_runs=warmup_runs ) gc.collect() results_nx_with_param_in_key = dict() for key in results_nx: new_key = (param_list[i], *key) results_nx_with_param_in_key[new_key] = results_nx[key] results.update(results_nx_with_param_in_key) return results def convert_to_html_table(results, param_name, main_title=None, comments=None): string = "\n" keys = list(results.keys()) params, names, devices, bitsizes = zip(*keys) devices_names = sorted(list(set(zip(devices, names)))) params = sorted(list(set(params))) bitsizes = sorted(list(set(bitsizes))) length = len(devices_names) + 1 cpus_cols = list(devices).count("CPU") / len(bitsizes) / len(params) gpus_cols = list(devices).count("GPU") / len(bitsizes) / len(params) assert cpus_cols + gpus_cols == len(devices_names) if main_title is not None: string += ( f'\n' ) for i, bitsize in enumerate(bitsizes): if i != 0: string += f'\n' # make bitsize header text = f"{bitsize} bits" if comments is not None: text += " - " if isinstance(comments, (tuple, list)) and len(comments) == len(bitsizes): text += str(comments[i]) else: text += str(comments) string += f'' string += f'\n' # make device header string += f'' string += f'' string += f'\n' # make param_name / backend header string += f'' for device, name in devices_names: string += f'' string += "\n" # make results rows for param in params: string += f'' for device, name in devices_names: key = (param, name, device, bitsize) string += f'' string += "\n" string += "
{str(main_title)}
 
Bitsize{text}
DeviceCPUGPU
{param_name}{name}
{param}{results[key]:.4f}
" return string python-pot-0.9.7+dfsg/benchmarks/emd.py000066400000000000000000000015361525521106200200270ustar00rootroot00000000000000# /usr/bin/env python3 # -*- coding: utf-8 -*- import numpy as np import ot from .benchmark import setup_backends, exec_bench, convert_to_html_table def setup(n_samples): rng = np.random.RandomState(789465132) x = rng.randn(n_samples, 2) y = rng.randn(n_samples, 2) a = ot.utils.unif(n_samples) M = ot.dist(x, y) return a, M if __name__ == "__main__": n_runs = 100 warmup_runs = 10 param_list = [50, 100, 500, 1000, 2000, 5000] setup_backends() results = exec_bench( setup=setup, tested_function=lambda a, M: ot.emd(a, a, M), param_list=param_list, n_runs=n_runs, warmup_runs=warmup_runs, ) print( convert_to_html_table( results, param_name="Sample size", main_title=f"EMD - Averaged on {n_runs} runs", ) ) python-pot-0.9.7+dfsg/benchmarks/sinkhorn_knopp.py000066400000000000000000000017011525521106200223160ustar00rootroot00000000000000# /usr/bin/env python3 # -*- coding: utf-8 -*- import numpy as np import ot from .benchmark import setup_backends, exec_bench, convert_to_html_table def setup(n_samples): rng = np.random.RandomState(123456789) a = rng.rand(n_samples // 4, 100) b = rng.rand(n_samples, 100) wa = ot.unif(n_samples // 4) wb = ot.unif(n_samples) M = ot.dist(a.copy(), b.copy()) return wa, wb, M if __name__ == "__main__": n_runs = 100 warmup_runs = 10 param_list = [50, 100, 500, 1000, 2000, 5000] setup_backends() results = exec_bench( setup=setup, tested_function=lambda *args: ot.bregman.sinkhorn(*args, reg=1, stopThr=1e-7), param_list=param_list, n_runs=n_runs, warmup_runs=warmup_runs, ) print( convert_to_html_table( results, param_name="Sample size", main_title=f"Sinkhorn Knopp - Averaged on {n_runs} runs", ) ) python-pot-0.9.7+dfsg/codecov.yml000066400000000000000000000023321525521106200167330ustar00rootroot00000000000000# Docs ref: https://docs.codecov.io/docs/codecovyml-reference # Validation check: $ curl --data-binary @codecov.yml https://codecov.io/validate codecov: token: 057953e4-d263-41c0-913c-5d45c0371df9 bot: "codecov-io" strict_yaml_branch: "yaml-config" require_ci_to_pass: yes notify: wait_for_ci: yes coverage: precision: 2 round: down range: "70...100" status: project: default: base: auto # target to compare against target: auto # target "X%" coverage to hit on project threshold: 1% # allow this much decrease from base if_ci_failed: error patch: default: base: auto # target to compare against target: 50% # target "X%" coverage to hit on patch # threshold: 50% # allow this much decrease on patch changes: false parsers: gcov: branch_detection: conditional: yes loop: yes method: no macro: no # https://docs.codecov.io/docs/ignoring-paths ignore: - "ot/helpers/openmp_helpers.py" # https://docs.codecov.io/docs/pull-request-comments comment: layout: header, diff, sunburst, uncovered behavior: default require_changes: true # if true: only post the comment if coverage changes python-pot-0.9.7+dfsg/deps/000077500000000000000000000000001525521106200155215ustar00rootroot00000000000000python-pot-0.9.7+dfsg/deps/eigen/000077500000000000000000000000001525521106200166105ustar00rootroot00000000000000python-pot-0.9.7+dfsg/docs/000077500000000000000000000000001525521106200155165ustar00rootroot00000000000000python-pot-0.9.7+dfsg/docs/Makefile000066400000000000000000000173031525521106200171620ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" .PHONY: clean clean: rm -rf $(BUILDDIR)/* rm -rf source/gen_modules/* rm -rf source/auto_examples/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." html-noplot: $(SPHINXBUILD) -D plot_gallery=0 -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/POT.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/POT.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/POT" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/POT" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." python-pot-0.9.7+dfsg/docs/make.bat000066400000000000000000000161241525521106200171270ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source set I18NSPHINXOPTS=%SPHINXOPTS% source if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 1>NUL 2>NUL if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\POT.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\POT.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end python-pot-0.9.7+dfsg/docs/nb_build000077500000000000000000000006361525521106200172270ustar00rootroot00000000000000#!/bin/bash # remove comment sed -i "s/#'sphinx\_gallery/'sphinx\_gallery/" source/conf.py sed -i "s/sys.modules.update/#sys.modules.update/" source/conf.py make html # put comment again sed -i "s/'sphinx\_gallery/#'sphinx\_gallery/" source/conf.py sed -i "s/#sys.modules.update/sys.modules.update/" source/conf.py #rsync --out-format="%n" --update source/auto_examples/*.ipynb ../notebooks2 ./nb_run_conv python-pot-0.9.7+dfsg/docs/nb_run_conv000077500000000000000000000040771525521106200177640ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ Convert sphinx gallery notebook from empty to image filled Created on Fri Sep 1 16:43:45 2017 @author: rflamary """ import json import glob import hashlib import subprocess import os cache_file = "cache_nbrun" path_doc = "source/auto_examples/" path_nb = "../notebooks/" def load_json(fname): try: f = open(fname) nb = json.load(f) f.close() except (OSError, IOError): nb = {} return nb def save_json(fname, nb): f = open(fname, "w") f.write(json.dumps(nb)) f.close() def md5(fname): hash_md5 = hashlib.md5() with open(fname, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): hash_md5.update(chunk) return hash_md5.hexdigest() def to_update(fname, cache): if fname in cache: if md5(path_doc + fname) == cache[fname]: res = False else: res = True else: res = True return res def update(fname, cache): # jupyter nbconvert --to notebook --execute mynotebook.ipynb --output target subprocess.check_call(["cp", path_doc + fname, path_nb]) print( " ".join( [ "jupyter", "nbconvert", "--to", "notebook", "--ExecutePreprocessor.timeout=600", "--execute", path_nb + fname, "--inplace", ] ) ) subprocess.check_call( [ "jupyter", "nbconvert", "--to", "notebook", "--ExecutePreprocessor.timeout=600", "--execute", path_nb + fname, "--inplace", ] ) cache[fname] = md5(path_doc + fname) cache = load_json(cache_file) lst_file = glob.glob(path_doc + "*.ipynb") lst_file = [os.path.basename(name) for name in lst_file] for fname in lst_file: if to_update(fname, cache): print("Updating file: {}".format(fname)) update(fname, cache) save_json(cache_file, cache) python-pot-0.9.7+dfsg/docs/requirements.txt000066400000000000000000000003631525521106200210040ustar00rootroot00000000000000numpy>=1.20 scipy>=1.6 matplotlib autograd pymanopt cvxopt scikit-learn torch<2.12 pytest torch_geometric cvxpy geomloss pykeops sphinx sphinx-rtd-theme sphinx-gallery sphinxcontrib-jquery numpydoc myst-parser pillow networkx memory_profiler python-pot-0.9.7+dfsg/docs/requirements_rtd.txt000066400000000000000000000002221525521106200216470ustar00rootroot00000000000000sphinx_gallery numpydoc memory_profiler pillow networkx myst-parser numpy scipy>=1.0 cython matplotlib autograd pymanopt cvxopt scikit-learn cvxpypython-pot-0.9.7+dfsg/docs/rtd/000077500000000000000000000000001525521106200163075ustar00rootroot00000000000000python-pot-0.9.7+dfsg/docs/rtd/conf.py000066400000000000000000000002131525521106200176020ustar00rootroot00000000000000from recommonmark.parser import CommonMarkParser source_parsers = {".md": CommonMarkParser} source_suffix = [".md"] master_doc = "index" python-pot-0.9.7+dfsg/docs/rtd/index.md000066400000000000000000000003101525521106200177320ustar00rootroot00000000000000 # POT: Python Optimal Transport The documentation has been moved to : [https://PythonOT.github.io](https://PythonOT.github.io)python-pot-0.9.7+dfsg/docs/source/000077500000000000000000000000001525521106200170165ustar00rootroot00000000000000python-pot-0.9.7+dfsg/docs/source/_static/000077500000000000000000000000001525521106200204445ustar00rootroot00000000000000python-pot-0.9.7+dfsg/docs/source/_static/images/000077500000000000000000000000001525521106200217115ustar00rootroot00000000000000python-pot-0.9.7+dfsg/docs/source/_static/images/bak.png000066400000000000000000011230351525521106200231610ustar00rootroot00000000000000‰PNG  IHDR·M|G•sBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î< IDATxœì½y¸dÉUØù‹»ßÜßZûÚUÕkõÞ­ #6±ca3Æc f†ÅÆöŒ3ØöáÁØŒÀ€%c@m@ˆE€’ZÝ’ZRw«÷ÚºÖWÛ[sÏ»Fø�È›™ï½|¯^u—z‘îïûò{ùî½7òfä‰'Î99_.¼xxqä5ýª¶(''''çµÈ-À+@œî¼F™ïþGÿõ«_ÔÖåä\ëÕnÀ— oþÁ® �:pxxP7¨ ÜÝoǯô�™7¨î�ü2ðàŸ~u›““““óªao¾8L%  4�yà4pxXº�÷¾øð<ðN`/ð~ ¶I™»�_\àqàßÜÀöäää¼F©oþ>ZyU@|päõGÀg�Fÿ¼žÞxƒÛò�#õo»Áu¿ÞȰ]Ï¿ÊmÉÉÉÉy5°€œC˳Àoÿø¿�üvÿx&/¯Üà6üy¿Þ·Œ{øÊ ®/ e¶þ#ZÁÍÉÉù2ãtNص|°_þócÎm¦Üþ#t*²7Œ)g/ôËýÒ˜ó�öÏýþšãß>r¿÷¢s7ÐJpÄP¹øïl¬Ü~Ú¥@÷¯9wdäß2rü×úÇÞ=¦¾%Wnsrr^}>ÌP~½ëÕùÀ_Œ9ž"šhëëZþ)×ö¹ýÿúל¼5ç~�aËÌÐà04jܺæz=&æÊmNÎëœL¹•hKgöj±z™ÿ}¬¶‚Žã+Ñ~¤kÉØâ˜s)·ßƒVlß¼Á½¾a¤Ü=cÎgÚBš1ªÜ®mëßcµpÜL¹ýÝþ¹/lоcýó¿=r,³Büð˜ëËÀá êÊÉÉÉy¥ø$Cùu7¨Î7 7Z˨<g8¹–r[b8V�³2ãHýô�ÝÎp̧P#:~"'熑”½z¬³ý×�èe ïGû½f>©kyxƒº3Á4N�Œã[��¶z~jƒk¾¦ÿ7E/-­åäÈ=�2ô±ÝŒßÜbû¾ºÿ÷É ÎŸDÏþGïVÿï¡—¿.­9×"'''çÕ¥<ò¾wƒê|lƒã£�´/e³…·¢\ЫukyfäýÐ1!Iÿ�Ó~yM™™ ('È•ÛW“ˆÍ§î@/ñ¿=³ÿF6öEÍ0щ·×.mÆÛÐA Ÿbce´ÿ*hë?òþFϧÐJ?èÍ-Æmj‘Ž\›ñ»h°[€çÐKf`¼rž“““ójÐy_Üðª×·�¼Ÿs~eäýLÿïÉþë0Ú¥í­h÷nÜöò999¯2[Í–Úé>[â9Éú×ûÑÑ©'fHгsÅj¡™1ê–ðVo÷ûÝ›´åá‘ë®õú¶‘r›¹%¬e#·„ƒ×qï•5e‘¡¯nöú|¿]9999¯6™ÜSèl07‚: ø!´õôEô–½£îoGÆ”»–[ÂÿËÖeñ¿)÷ÚMnôüYà'Z‚srr^Ç\�r ZqÍ„Áè¬ùÛѨ‹Nár�a*¬ÿ‡­)·-ô>æY¶�†ÒµdÊíé-¶{´�/W¹½i¤ŽŸºÎûƒÎòð« ƒFÜrrrr^MþC™ôs7 ¾] c>‚ÎszŒø»#÷z)ÊmL–ùß·Ékך²3ÀÏ0L–½N®ûSæä伦¸^åös …À[GŽŸëûWcÊlU¹ýÙþ±£ha¦€ßÙ ÂxË赸ÊíìH¿p�÷ÅFþ3 6  ËÉÉÉy%¸—¡|çÇz-¶±zó…ßè×õ)Ö¯ö�¿åö§FÊ¿Ô :ú�FêúØK¬+'g,[Ù;:çÕeÔ¿ôbÿïvô¶‹°q€ÕVøwý¿Ï gä ·ýýÖ1מêÿ­²:â•`ž¡¢~-y3b´ñW¢SáàyyMËÉÉÉyY<�Ž«�*ñ–M®LJѩ3¾ªÿ7KÏx#93ò~ûK¬C¢Wß ü‡þ±·±:^"'çe‘+·¯mîbœu’¡KÀ¨ry£¶Úý×è¼… �ý«kÎgÂW “o¿Ò<Òÿû5l}‹È_c¼?×ô€[Ï(‘“““óÅâÇÑ“oÁõåß6ÑÁÇñȱ,�kÜÊÝËå³#ï·j؋λ;ŽÑœé¹ïmÎ #Wn_»l~«ÿ^?1rî"zö :›ÂZ®'[BF€Þ–Q¡}¥Öú£þÃTZ?ìx ÷x9üzÿï$Zø¯]nÇh³µ†žzùMËÉÉÉyY| ½±Œ¾ mlØ GÑ“ýQåörÿï¸|ä/— ÓŒý0ã3׬¥ŠÞr}\v¦éþß6«S5æää¼NȪ+hÖ¾­|½½ÿzz3…ÿˆöÉUèý÷�©ïC }£~­˜þ4z¹çlÿ\“õ˜¿ÍÐÏiœ‚úŸú£~ÍšsßÈ0+ÃU´~W¿íß�VÀ×î˜ö#÷Û?æ~ã>Ó#œÿ«}Ó~mE~úýÜš6ý¼¿‡¡Â_þÿ~O‘[nsrr^;¼‹a›¿@»)¬•Ḡ-ï³qbt»ÛË0sλÑnXïBÇXœàåùÜ‚ÎÒÓé_7�VÊïE+ºw åñ?¹þhÿÚÿ„Þ®=cŠá–ëùn‘99¯Cþz)|tG²ÍR¨\E;Øÿ$°sƒ:¢óµŽ–;‰^þy`ä>g�Cç¿}�!;wø¾‘:=ëÏÎ/öËŒZ‚ߨo[²æÞ]à�ѾS ó5~bÍýæÑ³þ¿1RŸ ü9Ú—kôùœGï_>µæÚeL7úšC+­£»®ýÚʭЖ�•þûx?g†ÈÉÉÉyµ˜B+¥Y¦œ%´Üü+àq†r,K§õ;¬6TlCçõ•� :-؆²ûZm-þ4ÃÀâ, ù1àWÆ´ñ 蔊�_?;rí†F� ÿžDntºÆÜÈ�sCÙÊÒnÎ+ƒ�V¯g×,�>¨¡•» _„vmDm‰.£•Ñ“ w¢y%ØÃPñ¿ÀæKZSýëKhkö ´PÍÉÉÉy-³ m¥�B(²�çÐ��üj´ÅvÚÀð9´Ò Zn›ý÷1Z~ÖØxó‰V¢Çqm™�ê·å8Z±NÇ\{=^Í Ç®+hÞëÍÀ““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““““ózD<ôÇñÁöÊÓ»by¥]·a¤=–Ï>OE4.1¹k?ÖMo¦A*¥ˆS‰çô·§–Ðî%”ŠíNBÉ·ÀxU?×KâþZ›Š‚0y¦Ub!²˜u#î(¶0M“ZYB÷〠Y>…5u„^½ƒ³ã;1MýLÒ4¥Ñh ”Úòý…8Žƒã8†A£Ñ@J‰aë®›œœD)ÅJ£‰J“UçËå2¦iR¯×Ç–ß)%¶mS­Véõzt:�ueÇ¡\.“¦)õz}]©Ó^÷åÈþáClÛ3à _x�Ûn;J·ÓåîûîãÔåSø±Ã…¹ó$iÊ•K—8|óÍø�ùKWñ=Ÿ çÏ3=3C¹Z¢ÕlñàW¼‰çŸz–S'O‘¦ ·=Ê©ŽS®–1LaìÓzÎúïbfP#õôµ5¹‹^z ã/–Vâ“8­±§­Þ ©¿Œ³�ü”ÜÍ^ÿfŠÅ"gÏs¾}ÓÃIHíö¦m´‚I¿�{¹�½“7!„`yyyÓß“”’b±ˆïû4›M¢(ôßZ­†iš4 ’$Ù°¼ïû”J¥Á±^¯G·Û¥T*aZAc[I«µþ¹LOOÿ­M?Ü|äáÇß_ŸŸsoº÷-Ø…I®,7pºW8ûÔ§˜ÜµÇ/Qݾ—+Æ6¢Xbš Œ¾°í†1¦i`[‚v7¥R°_JS^uÊvÌ!¿MÕJi+‡ãmŸVls¨Ôc·Ó´ž(…Ðû42�I»—°K»é¬t0·'žç ê‹¢ˆf³¹åû�Êðjµ 0�åBˆU×MLL ¥d¹^)ç²~Z­VBÐjµHÓtUùÍHÓÇq¨T*A@·Û]U6;Ç1�FcUY…†ÐuX6Žc!×Þà¨4¥àû†A¯×B žíèñQ\×]5nDQDÇ[‡@ÿMÓÄó<Ò4¥×ëáû>¦i’¦)—¢3¤2ÅS%�&‘½²ºa°^¢+ÛT™f¯qÕj•ÅÆ§;'(ÙEv»©V«†AÇ\lœá¼ñv¹Ðó¸½ÔÆ1Ð*Þô¾€’1²·ˆQ˜¡Yw)îþfâ8ü¸•Rt»]‚ ¸¦’+„Àu]Â0DJ‰ã8‹E¢(¢Ûí®¬¥R Ïóèõz´ÛíuD)…ã8 â8¦Ûí^×sPJQ*•°m›N§CEƒ{Ôj5,Ë¢Õj†áêrÂD¡…Yµ\¢\*Ž­?Ia©­(¸`™‚•¶DÁTI`h‡ŠZAPï*z\¨ú‚v è„0U´CEÅ$)Ä©¢è V:Š^¤A©?¶U|}¼è ¢DáXúÿ²W¦TB³§˜( :¡¢ÑU¸¶ â –Z ©Õ‚ àVºŠ‰‚`³ßäã—Æ´ îÆs}æ›—¸ÍãØ‚ÝæÍÔŠ“ƒgºÐ¾Ì‚œC*…K�Øì©� Q1–°q…�À P ¢LDˆRK8ÔÓR_ó{±‡' ô¬eý}¥^4Eè�Wn­¸Lj„(3ZwNÅ6–°H­Õ€¯Ê̈½ì(íàâÊeÕ"«…‹XÂ"¶›H3\W'€ÀÀ jÄÞ2ž*q›ÿFJÅQÑh4®) 3¡Z*•PJÑét°m›r¹¾ï“¦)®ëâyq¯QªÊ0ɾø4Mñ\·?ãZˇ�9v)åÑ)A õlB*a[UðäÙ”ßx8æm·YüêGcZŠ�|!áÐv“÷2b±­8vQòñîÞgqêjÊc/¦Ü¶Ëä…K’ßýLÂܲ~†ý|Â}LÞû§3ÁgN¥|þLʥţ'R8¨ûê %ïýÓ�·Ýjñ—Ï$|þ´ä‰³ºm}6Á0´b+üØï¼á�EÉÛø9.5ØïߊëzÌ7/sV=C R¯E*VÒ+$QJÅ™À², N 3qXN®ÐêôD“ˆ.‘ HTL¤z²MW¶ˆT@G6 d‡PuéÉj‹v7©Úuº¿†€ÔAZã|#.€ X?1ó£bwµµxRîâ�wS¥º½çºÇhÇzÞU”HQv€0%$6"v˜`HFû¯—T‘"Aš;Œ›˜­ìX¾¶² "„@)5˜P–Ëe<ÏCA»ÝÞ²PÃp`m²,‹$‘DIŠk[¤I´¡’ôÞ÷¾÷=[ºÁÞùCïþÉ.žíÏì¡h%:À!rªLlß‹0-ZÒ!Œ%†„QŠï™ƒß`¦¦1�½¯gÅV#PBàÏH©Z1Œ|ÿŽÙÃuPËȨ�°}T" w«"qÇ1QaY–eáº.Žã ¥ÜP»®Kǃ¾&úJbض=˜4™¦I¡P MSZí6æªö >ÅY<*Ë·ªàf²¸P(àû>Žã`Û6qÓétVÕ“Éál¼qlÇy}Zð_.ÍV“Ýîa ~‘¹æ‹Ìq#õ‰�)Rj‘(Œ([zìÖ(¤5º,áD(3!×V®K¹D2FYÃI‰e Ì JbwÖ]/ÏðHŒ`Ý97­Š˜Ãþ쪇�{™.o§Ýmq&x–åd¬ièÏ¢Œ„Änc*3.à¤a( ¡ Tä‘8M ªÂþâ­Ø¶�Rj`€Ûª.‘ýK¥–e‘¦éX£ÜFå³ßO¦SQ‚”Š‚go*Ó €^ i‘ÅÐÄðÊ( Á²½�îÄ:1 X ¼kA/H@’H’T½nÛQŽwJœè–¤‰‰ª²‹#šÈ¨Ž0mTÜÅpJÈDâVÐëõ_X»ÝÌN2³V«á8κAÚ0Œ�EAÔëõÁX¹\F¡-c”޵åÃ0¤^¯cƒz-²NÕjµèt: ŠEm‰]+Põ—¾õ/~¶"höàM‡-lS[r¥ÒVÖGO¤ÜºÓàÔÝaƒX«>g$‡·|×m¾þ.½2ð¡ÏÇ|ìùt°xï~“;v|Å“ýÓÇ/Iz,f¾1ìü۪͞â­7'aŸ:™pç^“ç/é뢺¸¶ž°Å‰¶$?r<á-7›|îÅ�');ý8ŽC³Ó -–ðÍU»Êt²‡è„EÕš& Í•Öº½.†a0]žåÖÂT<1åø‰ÁË¡§Ú(1²tŠDY!^<¹îZ��À@Žq90•C¼Æ¢1%wq x¶cs¥>Ç ñg¨«¥U ©ÓF¹,å`5¬ †�q£ 줌´ºÚòP؃‚$I(—˃IâVÈ„j†…×4Íë*Ÿ$ÉÀÒ% p,Ã0®k‰u«HÍPÒ # §@"a1²˜—%âPà8&®cêɉ>DK,Shyü% „p9ty¢Uål¯°ZoT¡^K:¤!ÂòQÒÀ*XQ3™Úh4¨×ëã@¥R¡Z­bY«WMÓDJ¹á`Ùívi4X–50t»]ÝØ-t«L–[–õ’dq³Ù¤Óéàº.Àz÷a0*‡•R(u½ _:lóö`6/2Ç "zib “Šœ¥˜NcH‡Ëê4gš/ dñdyŠ#Å{)8låßÐv¹²Œa¬þÞS£Œ3]oe÷Ò*‘1Þ•+M ;ÆZ^kÅö>J^…Ëõ9^H>C]\%�=s½:²ZDÞ2�½ˆP&FPAu*˜aåj‹èvëžë ú`±X¤R© äêµÈäQ&3¥”w�ëqÛ4 ƒ8IIR‰ëX!¨T*«\�Füº•’¨$BØÃ S¥ßT*¢0Ų |¨Øêå0MA»�P,~iÌp)ôp Åo8“RÂF™TÐ×D �ðK‹4 _d6¸ÖëulÛ¦X,bÛöÀOªÛíÌòŽã¬ZîG§Ó!MÓ�PM’…@lEªöËgþ*¾ïÓét¶d=ÈùLà'IB©T¢Ûí‡�…6 •ðÎû,Þ÷×´8¼Ýà¾&ç%WÚuàÇLîÞgPïH¦Ë‚c—óMÅ“gµÂõ¶Û,Î-H.×Ç ðý3o¿Ãâ™ Ãó¶ ï¸Ëæ?üeÈÑ=&�P×wûn“�>›pd‡Á}ûM9‘0]ÌV Þxȶà“ÇSŽl7xäDÂ;î¶07øØÏ<ý—/ÏÑ º´šmß|„ógŸÆvl’8aéâ6:íw~õ̧sJî¤RªR,¹Ùº‡óâ ½%í“emÞ/¶ŠVVkGáÔêbuËX–K"†ÖH7©’šã-*¶(ÛeÂ@÷Ū类pÓ09Õ|šeã6…qÆ,]‡�Dvl°e�¤];$ô/ ì3ïÀs}¤”Çb±H­V£Ûí’¦é5­°B …J)�Žã011±ÎÕf3ŠÅ"q"I¥Âw,@/“MNNÒívéõz×% ¯…JLo½¯ºRÐ lÛ ÛKV¹~)©ð}sð¬Móõ¯Üf¤JP²×|Ï«€ê,�c¢dŠ0,„·�0NÇ.û'IÂÊÊÊÀÅÀ¶mjµÚÀU ImÚDgƒy&73—…8•¤ýÉÏVh·Û˜¦9ðߪ,í³ V&&&&ú²8âK²t ¢€Kñ)öU}@´ãR¥$¢�%,•P”ÛXË4ÒÏr8¹‹j©F±Pd¿u˜bë ç{ç0lÕ_ázù¿õ ð-—6«ÖÄiau¦I½E 3EŠ“T¥‹ëZžðö;, ®6ßûU·í6ùó§bvMl¯ ª‹•Ž¢äÁÇ_HØ5!øæ{,ÞxÈâÇÚ�öÃÇ… çØ¶c–³gšÜÿàƒ¼xòGïº,/-áù>†�Ve(^”Ï3›ìdGeŽãp v3ã"K½eé"ÒÝ<k+XÊÁz¬Ÿ½wÄ ^XÀÄC™Òî'¼õ½@PHwðéÇ—¸¼¤gö{g'ø–7&,ÊŠ-€)L¤ÚÜÊmI?œ!,-‘D&)PQÓL§‡+}­mT)Èü 7 ÒÉj¶ú‘$ q…B�B¡0Öõ}Çq°,‹Nc�iVVVð<ß÷) _²�|.¯ %‘a³81öóô/Á4‡>ßJ)‚PÇ9hÜW&ÞÁ5$±Òq_LJfÊŒ®š$)¥0 aY@ dgÓ ­ÌÝ*Š¢�’›-ñgK¥[ñ‡Üýv IDAT-´U½Ùl¢”Âó|P’n§³¥ò£–ØlpÞÊý³`ãÌ8s]Ëdq»Ó%¾NYü¥Ì•ô4Ëâ&¦¶J†D¡‰Ñ÷/M @—ËXÊ% ‹œä)v&Ø^Ù�ã8l¯íÂ4LƒEÁ©·õÀÄ�ˆdÆz·&…²Ry¨ÐÀÂD™ñ†~¿E1Á‰S1Ï�¾Šìÿ@ž;µÈ;ßr+Ëþ±UŠ­), ª@WŒ<¶•O8$N�"½ØD¸‚ÎAǸ惼̯}Tq×w³ÀÀ,)óo®×ë%9 ÃuA“YYÃ0(‹$©$U ß±éu»t»Ýu†Ãl²Ç1âƒ'”’aa¹s½åU)…”Úzk¾¶I*‰"IÁ·hµcÊ¥WÆj»Ïë¡’�Ëi…X}ñ~Ä ûÝöªyšRJG]7í R²‰rî¦oÛ0 {-™`ͤÃ0«ÍÊdA1Y–Ëñ(ý—¨0(†á¦ƒó¸ ²U�;hËpÒoþµʾÔy칇)nóè.D$-ÉôÁ*çž›cÏ­;Yš[¡2QaþÒ“·éРì¢ã\e·q˜m¥Ýx®íN›³�ôâ˜È]ÌÞ¦èGt«dàž"00±IˆXka0¤M1™¡ëÎÊe”ƒ=´¼  ^ZCÄ.q, Ø@!r$ØÀ§ÆŸ�¹…;§j(¥¸´ÔààŽ)Ž>ÑSCáï« QÈÆBjcD”ßD¡(G;‰D�› ·2]™%MSVVVÖSJ &hÍüMÓ¤VÓí[M;ÚwG!Aƒ�®´Y‘Š7LT¸§èSrmzÝa@’‚R©„ã8ƒåïN§CµZ}ISüžTÝ´½â~a¬WP£H‹%©\åOÛí%8¶ z¥‚yM¡¸§´‚ˆZœH¶ÓLÝ`ÏZ�Ü^î0½&hQJÉd­ˆÑýk 0wBz�T¤%÷nYgß÷Wùj�N¨Öâûþ 8,ËÀ „‰k[ þ5eé8¶*‹7 æ]D �n‡$UÄIB¥T <’ýãˉ'祮®`FX!õ 6c…20‚*¸]v7±½´Ïõ²ø\ç$½8&pQ}Yì¨"$&†©�Î þÁV>$&Ò HUߟ¨� <_bƘ’X 2(i ìC˜( F·Š+ „fÓ–$FIŠ¥œæ>>üÉ©|Þ~ï-\XXá±cgÙ»½ÌÝoLé¥C£ˆ“TQHbk}ð•%}Dèƒ×#ºïÙQ•Ü6{t|àâŠÅ¢6¬1(¥¨T*¸®K·¯�®E)E¡P\“­/aðoÏ/òèr‹IÇâûömçÛ·MP°u&¨Á„¿ÿûÉŒJ)¢(ÂüŽþ—ïV2ŰܱË�n¢…©bU†„0Jñ<³Ÿ–F`m´F{)š)w–˜²zL[]åJóÚ¯“²•r{©½NÀ !(x"xV›P0¦þô‚tKË“‘aòl7ä|7 j›x¦‰eY¿‘�FSÊdJ¬êÏÜ3ŸÞÌb«ŽÚk%¤”ë–Ç<ÏÃó<’$Yå¾6p§T,b›I“$)žënPö¥ÎéøºFa9ˆ™M±ˆ5+é T-$p›˜Ó)=ŠØì Âuë"�¨IIM`Û:±dVHT�$20SCØq™“O6é-›T* ;Å6\Ãáü3SÕ†`JlÃE*í—l¦>—_ hÍ vVw“´l¤4ÀбRŸ$‘('±,R•`Æ%ŠL‰‰×ÞÃ矿ÂLµÌ|óWrï‘}=/ùã'bN/HLC𻟎9qE2[1è„ðG�ëX„?:áác '¯HΚ|â…„OO0–"üÉ“1ç—$‡¶™BÇ^üÚÇ"îÜkrvAòß>óÂÅ”ZAðÐgcž›“t#Ø=eðþOFì�2ð��Ÿï•ÎÚXq•Äm ŒkŒÏB»øÑ ‹ÖYZa�’šè?C—’U%‘=Â8A¤Ž,ñâs Z йÛìÞ;�ˆmLå¶aá\Dkff+ˆÔÂH\„%)D³4æW/¶¹|¦KÍ™¥·,˜.Í@là8�ó®éã[âÂ"–°Q‰…×™7†Ø£¹bsöRƒûïã-G±m¢ÂS§çèôbn9âÊá$)v\!µVÿž,é£"—Ä[AŠaw ‹]þ!Ê~™(ŠÖÇÙ¬amzæÊ˜YV•Rf5 :Ë2œ þÞ³çy¢ÑáÝ3ËR”ðþóóì-ùÜd ýÍ3?Þì÷“>lÛÆRiœÝaÝMÃ(Ŷ zÁj¯4U8¶‰!–e`_|�j8\è` °¼U%¹ËZf9²9×h&/_ɵ„¢hIöz=ÔFVTÙB˜–*3Lpf=ÀfƒñF�à‚üÓcx¡¥;ÝÍ%Ÿ_¸e7·”ü�EÀ÷ý�/_V�ßÏ7EÑ�«Ë®aR(>…an9è%KW–Eãf³/Ã0KpE7Žú»®KµRÆl·¯#¼ìK‘xH;@ŠkûÌJ‘ ¼&^0Cݿ³½“;™®l£è—8`ßÌ…æ‹,%W©O·�R²«ì˜žbþÜ<–ç²´Øæö£;¨¯,qåá:;w×(W].·Ùdé7¹8×ÀEölßÎå Wyìñ|Ë7<ÈÉÓ1=IwYXhrû‘ýx» ~!erZñì.1;[ej»«—È„^¹IÒ¾vª«qÁ+-U C¡ªR')“xõUA‘6.» ûp,g°|¼é3îL†aˆïûÔj5z½Þ ³‡”rÓ4xYßm4xžG­Vã'ž:�cüÆýG¸µ\àWN_â·ÎÏóMS%ôEÌ¥nÀ¤çâõs‰6›MÃDZ/Ãj* „=~õ+ŒR<פ&øæj«­çš$©N͸Véýbà›’ýnÓ´†‰í˜ì4¦íy·%ÎEe¤zyí(š) %3k0B HQ*D¥!B€ À‹®›-ñ¿˜(þÎÓgYŠCðç uYjñ ‡wRð\­ÈŒøñeVÒÑå×UŠ«îü7šL–Öjµkº¾¬%+?p5è�)™ïøfQæ£.p¶mS«”7uQxäxÂî)ƒ‚#¸\Wì¨ ¾þN߆>sürÊՆřyÉÛ�Z|øñ˜g.¤¼pIò=o±yäDÊå­¸¦Î,èßÿ]ûLZ ׆#;L~ï31I Ç/§Üµ×àÅ«’ó‹’;÷ê Þ8չџ<—òâUÉsSÒTgÏ©Ÿ:™²ÜVüà×:8Ì-IN\–|²�òÎû6ÿÍÒ!¯­Øž¡"´ëØa�¦»ÈÉÞØ•bº¼�‚W`¿u»i³ çèª:i’Rv ,…m¼Î6ž=ñ"õf“{ï;ÈÅÞeΜ]`ÇöÛ9uj DZؾ·€á÷h-�÷ܽ‡Sg.°x5À”>Ín“^±RïppïÎ,]âö;wpáÂ2�Nˆc·ñ�Õ= ¯¢ûþ±¹+Ü9ÍùùeºAĶiŸX­ŸÐË5Yuœ¤L›}‹öêçPL§é·êjÓjµV¹Ú˜¦9ð×ÝÌø—MÐ2׳ÇCÉ…^ÄÏßy�¥(æ`i†;«EÞwæ2ßRÙ@7•|ð™ãüÝ;oÁ€ÁoóÉ+óÝ>‹!Ãö 2w¥è'7Ö Î^ ó(aJš~‘�®úls"&haÚnÿa¸ŽÃö‚â¾ÂnöÔ][Ä1Áë¿löù÷–ÌØäà‘¶† 2B˜*é ¤^-–Jxý¼ŠÀª¨êXüóS—yºÙe±øÑC»xp¢Â��¸ÈJ·Çòòò@!-•JLNNbÛö@ÁÌ:ÙHc ¿4�ݯÓéÐh4p]w�´þzÒ'µÛmZ­¾ïS©T(—Ë«„ùµêˆ¢ˆ••”RØöë3×ñ�B¤Îuƒ)‘ÙM̨D(ºœLž`nå,qcYûj‡9àÞŠ²{`œ¾xžçÏž \,`¤.U¯ÆâU=!Ú·o†½»f˜;[çž÷pê…%\ÓãðÞ=„QÀ^8ÁÎÝl›­‘(I¹ê°²ÔeÛl�}{g¹tyW¹x¾Îs_¸B¥êrîÜåM‡-?“]ãqXQ…tƒMh6"5{˜X©KK,q2y‚‹õ³Ä±N+·wâ7Ùwá aÀä.›ò„M#\F ˆºƒRÉcv¦‚!3ÛJ쿹Æâ|‡Pô8xp–½{§yôÑcÌ”·±m[ ×6Q(‚ bçŽI¦§k„aŒP&Q˜2waI’Ç`X ^­Í¾]%mÞÿ?>ÃÇŸ:�aŽÞQ"HWË>…¶Î[I#.àöfAš¤þ05d†§ŠìónèÙ¤n«ý7ó§ÕzÀ0}ØõdYìÔã„Ï®´h')KQÌó­.Ë#ñIÏ,,SC®vŽ­4ù�§^à“¯°Ðér¹ÑÄ0¼ÊØ›ô‚”"Š$®3dRj§gЂױ_çuC(%H“hÕC†�ãúìõCî/ÎsÈ^Ä×– õs|âø§ùÜélj£elCO`”Rƒô\™‚jöÝž\Œ‰ŒÌ’Ž)z•ç.]áÑ —¨V«4%üÖÓÇøË³sH !á©f‡·ÏÔøºÙ n/ØWpx¶Õe9UƒYO–²&ÛÝ£V« ,T«—h¬™‰� ÆN§ó’Rvd�qvÙÊ&£uŒ[ þrÂIK°Atëf¤V¤‰!"Ψ§8Ý|~°ü>Y™æfï>|UfjªÌîÝÓ\ZXäì…KH+F†¡ûK¤�f=¶L”’œ»|…ZµBm²Ä¥¹h¯ÄtºIßa: lËa¥Þ&c|ßÁumöíŸF™]n½Ý¢VvhõBÚAÈdÍåÈ­±Zï+Ø5ê8q7˜A*…r[c�mÞž�{@š¦Ôjµ-§ÿÊ”�Ñ~7º™ÊVÙã:ÄR±¯à±Ów˜qµ5u§£ËÅ^Èí³ÓœYi(øÈ©3,õBÎ6Z<·°¼åû¬Å(T×SJ篕 ‚0]e`P ü‚EšªáN‘¯s�Ë•¤D¤Éê>nšEÏå°ßâ^wŽmæÖVp‚¨Ã“ç�óØ™çè„m.E6óaŒ0 º©¤•¤tSÉbш:R±Ü ™�§i« ÎÛI� ºqÊB!¥ä·>÷”ö©-•øÀ3Ç9¶T'î[WÏôB¾rºÂ¹÷ËQ‰v�V’r.ˆV¥Ë³M@€ÁÒÿ€¾Õv-™Ìm4t»]Êå2¥Réºeñ¨¥6“¶ÚŸ¯Õ÷?s*e©-‰ûáø¶ èÂ#ÇS¦JEWpêŠ$Œá{‹CÙ¸6Ì7%ÿõш?yRÿïzÐæÛØ8{ÌÁYƒïý*›Ã;†z–”ˆ·¾µDj×Ç TG�‘Kê56ÜlB ØÉaNÞŠ:#A¶ì»Q ÂFd;7�¦K’d°,v­ò�ÇŠ~æÜИ!àöŠÏ¯ß² •$zúÇš:aÄ[÷íæOŽŸæ?xA*ù×�~Ž›&'øîÛ�`£øÑÓ󬤒_»ç0?õü9þìê2±T|àè>î÷×�)ÙŽ�úsißÖÁª€0ûñ›ÚkƒÆÆE†�+“Éá^·‹LÜBaK²<“ÃåryÃmг�í¾•µÕÓcºc�k  Ú�þUA˜€eêrÝHQñô®�®­?G«Á®�a¢¿WË„^¤wˆl ߱³�žÂµ èwšô½K¤!tFË€0ÑJ°”`[ºÿ•…Yú¢Z­¶n Éqd9åÒ4%nëŒÂu¹=¯UÞ0 �óóóz¥ß9]×Å4M‚ • ! LÃ$î/Ÿ|o�_[–\Ýó¼Án%Ùç3EŠmJi`9ÅUûÂg~Ë™%>Û§|4â,Ÿ¸®K’$,—i ¥ïÅ“¶¶È Sì¢â×8_ŸC¸]�I$6ðE‰.-ª•"1A©ç¥è9Šž³ˆTH=lˆ6'ÂÇ9$ï¡Vž¤\*rßÍG9Ý~ŽukF—ìÑÂìL“ˆkºK$$•}"*!EB/”¸»zÔâIZÌáí´é…‚Ò¾У ¶—踗)ïE@¨íñ±�P.fP¶%;J3Œ� ¾fªÌ}EƸÃd›=d.B¾ï#»£ñ~ÁkMCæû>ôz½ Ó'ÁPË4EÐYY¡811È�~-Y¼¦Åá½§ÊëÛ1º£ß�K³Mðú í¨;L¦ØÂj…³Ø?žÕ•ý?9roËÖUtW·Ã2µEy-î54…IDä¶y©kËÊŒ±”À wÐu¯"I©‹«tƒGäýL”§(Kìµ¢Ú1WÔ’¾ÑÃìLc81‰ÓB¡p’*±(ª ³K ÚxÊ%VŽgG‚ÄÑ©6ÍÄ3!¥3Ìt—ÚøI•Ž3�m Œ^�þaL$Gv•ð¦ãkŒK ˆ tP›(¶&ûÌÛð=�Ef4€<Š¢AlO–o<Ël°ê^b˜¶4›ŽúÓŽn�¾QŒR¡PàÊ:QÊ¿?}™�žÍ/ß²‹jø?É{óËÒó¼ï÷-g½kíÕKM÷Ìô g†‡”H“±$K� Šƒ@ŠN"  �“ÀH� ‚ „8Šƒ û?+J)°ıµY²eJ ‡CÎ�r–ž­§÷êZï~ÖïûòǹçÔ­êªîjJÑ 0˜©sÏ=÷|ç=ï÷¼Ïû< A«Åªçó—>õ”üì§_$ÔŠ7wöù·^~ c-�Àø¾ãÈrK–›‡(µ½nš}ühÁboÌ–y#Yc7•äYr¬U$„@{+-�OF‡<>’�뀋-�@I>wq“å(Ä“²”GeŠ©’Iõð:vg†Ûƒ!¥µdåÑËæÖh\íàæ/ìvò_¿x•XJ~ñÖRky©ñŸ?µ†:%9.˜‡‡‡M¡V/,¥õQjw)%ý~ÿLwœíÈ“-¾¸²$™Í�Bœé´VŸk‘ÒQÓ9”RM¡»´´D·ÓF©ªàÔó å(ªv{µO}\ÍÃåy+++�|�ç)"â€F{´ŒSJ5ç¯�æEzmÉZÿÿn·KÇ"eGß`¢˜èöÄÝêßÕJ*Vºkì燉ëøÂæþÙ§øW_x–�Ðç¹ÞéÙ÷³¤DÍù´'éµ;™µî�5;`¯øv²Æ�¼Ç,/)³ã¦BH‚¸ËV[ð¹ö×Â!¡|¸(´Gàù\‰,×ú-$p±Ó¦øxJÒ»Ôåo~úYfeEùøá~›ŸfqJa[kS­�ÃÃÃc³Ñ|€íI¨/‹NgµSÙIŽùbÎ’„:£(Ï#KÒá5_ÏOzþ?1!PÓ…‰ÞA(ƒŸiOç"å½üuö†;M.~ºû"›<Ýü�å¬Ìã%Ôt•Ô&`…š2Óûøž‚²z×9e~ô,û¦ƒX°ÔV£ò6i¸S½TÆRÐ¥×ér0;¤uqë¥Hu<'‰ÓM\âù�þ-ºn•¥Ö*À™æ4õÐd3´8—§[\¿‹ â†�ËGuÐZãû>EaøËWøáÜAØ6 IDATå6ÿéSË\ÕG´K—¥ôÛí²ï{xÎñÙõZJÒõ4ÂÚ‡i “iI*ÒÔÐj특ä×öý3ÁO¹"·‰<…Dq²(7EÎýDqˬ’Ø£ëòE‚/%Ÿw¹“¤ä¥å¹å>…‹#Ôša–Ó|R©ÐÅ,S’ÉJ™H¯ñú~‡–’¬Æ8G[+�’×·÷¸Ø‰¹G�,F¢}þÂëïaŒå—^ºÌòƒ '5 µ =Ï#+ ZÒ@üç�Ŭ)“Éäæû>ÝnS–dÃ!Z çPqŒ™NQBP~«…Tê¡ãÏsVµÖ6øEQ0™%ÄaHMK¹FÞêÄ_ó�ëï_“ÜgÓ1Zd( :ZÛOœ—eÉÁÁÁCŠ‹×/„`2™4yyžóæðkŒeUüù¶…,"Ò`�Àµx!ø<(É{ßDIªV»£*B½aT”ASDûç¾'g†ñPeˆ ŽsTŸ/q¹ûLóÒÝmó�ùFˆ¬Eh» „.1DT(€7A(¥�'4‰AÒ¦å‡$þ.:ï „"õ*ÔZš¿ì�{͹WÜežï½Ìöð>·g7±Á«�¯?mb\îaUA,c¦z�³¢í–ùT÷‹ø¾ß¸è=*Nk�9çšéö,Ë�F§¢5-¡–þZl�ÕŽ9Ó´¨ZiYÊß»»Ë_\Ž›�Tµhød2y¨½¶¶¶ö¦%8WuÉ„˜Ó§ ØÉ´ {ä…Á÷Ž,¤¿Â–+ú€uoFäû(í=tLY0Kf|X®²kÚ˜9mÌ“ŽP|Ú¿KÜêPZ‡0%Nêc[Î::½.¾yÌ]L:EûšÒ¬“xŸ"Ës$ó�O­ƒ)+)K1ß„9çðƒ€ÿ{ÂÿüÑ6ÿÇK[ôÏÈË4—Å<«µ¦Õjaœ ôu•ÇÎ�®;+êœ^›�,ªíËãŽåaOJ¬sBàŸrüyh ÿ�oßÿc{ˆ Ï?XzVÄi…Þ.êä $¹Æ•Þóh­)MÉîðÙ70ÎRNZ�?! ¢g7¹Ÿß£Ã#¹:'È—(‚q¾A&Æ8U œÏ4ËЭ' Ú†¸,ÄDCêb½åz|"üþ$ír‘zv¬¸5¦²Óµ¶Úô,Ú8ÖF “iAéï«â † /á’?!Vå¹eδp|”uØ/2WýP[zÀ³Á!AÜÃ9‡) L™!tÜH¤9çX]jAú5)N´vã®1qO�»ÀÌóœß$Ïã§V;§&ÃÚ°¡ž:\ çžTº‡ÁaûI,ëÏ©t-™dŒayy¥³Ñ]VS«…ÖàþÂw-�Ã*…Ç áŸóü5íy^ãDRËŠ-ê;æy~*²±øý“Ù„v§×,~c ãÁ.NHÌ)ú›5¡>?ÀƒÁ}Þµ¯¢(²6B—8•³Å‹\ì^áÍ�ï2 n˜.ÒXëÈÕí"dTµhlPzÃGr›Î�ä)Æ'’’Dr‰Op±}µ‘¼º?¸ÃGé»Äfé4ýV -5µDîRвd7¿�2!C±‹+=¼Ð�1Å7ŒO* W ļ´x¦EqT÷}É^à™øSH)ywúM&nH:ñˆ‰Òãlõ›`(ƒ!a¾J¦8yúï ñ¹¦?Ãzï"ÖZžl «n×­2€ÃÃÃÇ*-,®�:)÷z=JëHsCä+‡¤iÚ '�_,JŸ½?Œâ6M ~ ±öxÛ·(,¥q„�b:-hLÆ9O�´<ŒX“´öÑþñn€sc F™ã~pÏT/³gõ.W¢/¬�yk-e:W3TèO¿ã#&¿�P¨.Â�+#�´{î<\ƒ4ã«©á_¹P©œl±žfØpü:ÀCÚqô�!Ï£$!þ×è�Ötªn·ûP.UåÌèŸxgœ–‹�1”eù'º¸}kïuög\<úƒ4ÒPeŒD=d‚PÏ lu®5îq7¶?à°Ò ;,«D^Øt­µEÁ8‘dv—ÄÎ*¯€ ÀM;”Á%E&PJ œO½O–Ý%.OÓ {ÜÞâ¦ø�b“ýéŒ ´´E�•‘áü' ¤mLx¶šËªÛâùþË  ö(ºÌɨ‹Ì8Ž›nm’$O¤VSçò¢(Â�YZà€P vww«nüü7<íüQ†á±"YýÌÒ’(ДÆãÚÖr4Oòañï‡ÈœâVÞæ~sEïsQ�ð‚åyÔ¤©}:Úñ¢“¤»Ü´ëì—ô=—C«Ûé¥)ªQ½@|ßÇ$·�Å=\™!tˆÓTç“xIzn¯z€?+j¥ƒZ›sq1-¶WOF-µ1™Œ)rÿ˜…d�XÏË«§‚kÔ h¦Õ) ‚Ò9”çáf³cZÈz¾ *Çc¬Ö��ðy‡ÖåËå›Nê8Ö¦‹m1ç³c ‡ø�O§Ûo^0u(i ¼’¤ð(̉vÍœ�\äYž²mo•»ŒKC¤6ä*¡ï6Øh_æþh›±w„#-JBã ^!¢££S䬃 ¿û}\ÌÄ�¸\%3J•P‰ºXîò.£ñ>ÏÙÏG1KÑ*ÓqÆÅÕ h¿Z³y™3ÎÆh©é·—XUkyÁá¬Ãm>b:/ Ñ.`"v(Ã]¶(§1^˜QÎ庚5w…§:×Á‡ã·Š](BâÀ£t%Êh<É©~nN*ŠœG“±^ûµ¶’¼(в²Ù•R>„Ø._S%�Òþ0,x‹Òb]5ÓpRAJA¨%ÆX‚ð��Žð¸È¬äí¤Ï=òŒw@Ï&è…ÙˆêE峤,5¥[8ëz‚ö;Xk‘RbËkͱ¡eß÷q“7‘Òa“ûH‚“-Ð+´Z~ã/ÿ¸5 µ¦ãþ¬ªøý½^ï!�ïZú묜&¤¤Ès³´±!]œ�x’\¸hÑ^K‡•eIÉIJç�¾ÿP†*;k)ÆcÜ|=gYö0ò'-!-ÂúÍ@Ö÷üY:£/Ñõ–˜ÉaÃ�u8ìüA‹¥ö q‰KÝUâVÄAzÈîtßiÝ–å&žç³ÖYAv%ñÈçvz‹iZT�Lq*ÃJCa# Œ¬ jÁŠ»ÈÓ­OV–À‡{Ü,>¤´¹*XŠ#R5d" …ÀÌ¥ô‚b « ÎÊÄ>—‚gzA<ß$�·¦©ß¥Óé´y�×�Ûóæòš¿»´´D^Œu´B�,MN;]ÔִͲ,�ŠÛ¼0ž"IKÚ­ãЯµø’Ò8â�aˆL-U�IqÆ ÊYQ8ÉûÅ»®Ï%¦¬™)ž’(ÏŸrÅÇíhŸÊ3C¬ΖäI†TkòÊylŽTðy€;|‡˜ô.aK�™‹û---�I°^ŒšÃ4 mQð»F1‡bNý…œ;&`_ï\êA‡óÊwÕ s8²²RM€c°Tƒ…ø>&Ëðϸ&-ÎÒÁ9ÜyÜ ÄÉó×[c ív›ñxÜ)u!räm?ÇXIVj¤²øbÂt”Òê®4¿­˜ó­õ ê!2k-w'7±‹4>6÷+ôR¤h|®x/Q¸’»Ù‡¸ G™O´Al âéü…#šÛ#è2U>’sú¸Ð.df‡•jB±†õÇ2A1Æø¼�¾Éº¿‰g=Z:f–%LÆ;Ü4×ÑeL;02'?¬Ä´/µ¶¸Ð¿@gÖçzùE°ÃLŒè¥›$Å!E¦þ ;·NXxĆû¬ð²>e"L ôHsÁMï]îú\rÏT`šsŽLÜ.Cñ�õHÃCörG™—Üž~Àº‰­å-<2ôÙ•R0ÁK{€ ¹x€|²ÅK¬v60¦äƒÑw™æ%"L‰².Y°Of4¾­”kغã0§8ýÕ±ÂE:­J¶–¿«Ÿƒ³h‡§E ÊÕô€óÖDpT8 /M5H+«9™')’kÓ•~¿TÜJYYè y|c˜f­ªèãò._öK^RwHJ¸cWx`ºOL –£²ÏÕâŠÚeÅŸáùááZ¤Tt<ƒ´0e�kMEÕÒu1\=ôžV8U …×!ZŸmxwI’4 m6›=r׳øàŸœ"¬‡]é "ıš¡.&ëó× ¶.rÏ#9ÏÍ'ò<ÇCØë‘&I5¸–ç¡ǾàK‰Í2Ò>µ,¥Äɘ¤H�ýV‘W�•ëDûOGìÚÛh¹lÕ3OÜ+ö2q+æúÞõf·îå=Šèkå,"p¬CKë ®ð0ÖâS”)àá¹ÂYå#~Î�EDá�@§ÕKÀzÈÙ±ê3µS”N)‚!ž¸DàWYKý%Úi ³œû¼�L{d¥©Äº•á¦9d°sÈSý+|ró%&ÉE·˜´Ñ˜x†ÊÛH+öý`�~¼ÌpzÈ�⻘ÒdžcÚÙ Óà%ªc(mYHL0<óÚ–¹@w®í:›Íš Vôz=Ò4=æÒwZÔ\Ûš0›Íð<�n·Û �KŒQáÆâ)‰À5ªçIÊp„¼ýA£“ãˆÂãy¶–K3ÓèŒÿQÇÕ(á²Ýårä³cRnem&.|ü�'bXz¼iVÙÐ!—ÅŒ–ª”jÚ˜5Ú PÚC)�s–²Èq¦¬@†yÔ­IõpfRqq…‚à%f“Y3l¢µnKÏK×ZDãÃ0<¦kûˆƒ*鈅¨‹Ù8ާ³0 Ï…(×y¸¦HL&“J>ižÓáÔã75Á<gÖRIš6œáGûµÅûp8lô}£(jt§�1M‘¬¤%öª¼¼¨ ãätša8¨•kfØ2Ÿj]‡ºD GR(ZínÓy|oö-f !m#Ã;/Óºn•g;?ÀÝÑ=öÅ ŒÈ‰Í )¬Êð] —Á!¾ð�¾¾ˆçõ 3#©ÔG\„³¢jןA—:¾ma(±j®²£ Ö9¼¬Cªº�r1[á–:kÜß¹O¿ßGJÉRÖGO|¶£ØI›"cTlÛ ƒÝžj?Ç•å§YK×ÙIï’cr™�š=‚|™¾ße=Ü"cFÉ€»ÙÌì«*©ÖH¶(8D(‹Î#„ÎÉæŽ�² 0*;S­Æ#`3ºÒ¬¹:�ÕÏQMÝšL&�júVMq¬%óNºž>jýU¶½žnˆŸDʱ.’…Uq›f•Íî,)O•÷ò<ÉtV šýQ‡'ÏøC”…~;bIælÎvø(k3´Ñc°¸ãထñø®¹È:WÜ�¶NѺ²Ž,ó#~—ÒUQiÒ H�X@m«‚Ӣ¸j…}PK,‘ç“9šL&ͰÒyЃ:êÄZ/ž¡x4‚u<©ÖçO’„V«ÕP êÝ×£¸hJ©cî;eY6(„VŠD)0¦¢"<"¤ø€�ÛÑ ÉÓ†Îꢺæü QéœÖVÂ'[Íz¡€:‰,·Î9‚0FˆÓéç �6øÊ …ÅÈn5P–ÜLß!3tÞ! ³ùç¶Ý[­kìNöÈÛÕ®˜ŠCHˆZ†2 pÑgqºÉTïa£„EŸ;á,.mˆ6E0Äžá¾Ô\›õ�&¤l¨ YDØhJ¢FesÉ~Š¥¥U>Ø{Ÿ¶é»>qÔâŠÿÁ(â>÷ÉõÔü|Ê2P7�¶Ù ^d%Zæ…Ö’$3 S²oî!½€Uo�v\É_í ïs‹·pVb‘‘±¤PLÐi¯|“–v±Aª¦Íïu2"×a+~þÔE–eMR|Üο¦ã,ʾ•eyné»:¡:W%ÔÑhØ õÚ}\q´¸Ž¿×p®RHð}IYZ´–Çþ_¼ æéïUÜèüÑÓ† jÂÃ�Zl Éz8âîlÊí¢Kꞌï[8É�¢ËvÙaÃKØ2CbeÑ^€-sü¨K5õUu@œ)@zÍ0Y�Ëb‚§%f:DúPËd&ƘɱNÏ"ed‘‡zž{´(>ßn·Ï¥ë¹ÆÆãq¥’Ón£ç´­Ç �-æáº@XD”Ã^�l:E[‹:G.¥Dƒô}úW®4ùý4·Þ dYÖt¿êX^^&‚cï‘@Býpqèdt�ñ|Çã1«««Í&[Ð p�¹�nk- ®¶!}ÊÌGÏõÄ4/ù_ÄH‡ðz^ ÄtŠ ÆJ6†9~ÙA¹Õ¹CŒ &•4×ü§’ pU´ÐÎÇøãǨ¹ÈÊø`.Ëš…6äþ 2ÖÍUú�Un ?bèߣ3l±Ü[! B®è*(o4…-€•93ÿ·ÆP”WÐRóTïÎ:в ´9(EÛo‘f w±ínŠPÈF­Æa)‚AºBÌAº2À…óÅëØ/ÐŽ:ÍÚh®ûm± N£ÚÔ Q)ë:æ<ò_‹›:W–xª'“JŸx1—?ð¨s:€.�«†br‹wBú+Í*�[ka¨>®íÅ ¥-@¢t•d6Ú’åpÂv2ãVÖaâžLOÒò€½b� /eK< öRUeSÌ­áꮲ映Àætw§6˜NÓc(l}CëÄú$èA-yåœãðð°á>Ib]Dê)B½À‡=ë{DQô�¶ì" RïÞòÙ uÎÄ@ÅÇ�»ûœÖnÖZ7¨í⢭‹Ý,ËhµZôz½*)›}–Gø)ŒéxåÅ´ÛÕù³dBa Æy´ÚÕ5 & ă£ß°¾h¶Ô xžÇ�ƒ�°Ñé–̹˜!<•õ°:e&›–þñ¿K JP6D¤mD8ªˆþ§†@ä1&8JJ Œ3dªBC5WýXmoòîîû$j—C1Æ .µŸÆ÷}.öŸÂ#f/¿Í‹CiëÍ¥‡h£›!+ÑÒW<¾É�Û{w˜Ú9…S nŽÆ*!‘NWCÂR†C‚líœ2�°¼\OGG–Ò'×c}ÿÛ¼5º»ø·5:t2áÖ¬š²sš&c��­c,¾¯(ËârQù,\žö /®ãï5²¤’^Ì2C+^¤…ÁtVÐnyéŽÿQÆŠ—!m>Ï‘UŽ <Í3=ÇFvÀÍYÀvÙybÚXéwó˜EÈsÞ.+ù˜(ªÚ™y:­.Ø9� ¡Ž~¥¦˜¢ä=p)®˜"âU,+LOðôá8e¤Þ蟇òâû~ƒ@�ÇãfXåQºžg^ë|£U«±xžÇÒÒiš6ßc1NËË9Ýó«««äy~b†¡’w¬‹‘“Çz6¡v»œÍ¦x$ø§tœ8ücŸ¡µB˜ VT2ŒÆòtŒÀÐk/WCL“#ïzÞLV£üê÷y† ß[âí�·q­ÙüoÂ*´§@U×aýqÞÅÑÃI‰ÕguÂbü1žég›ŒÂÓU$švv�Ä?hø«Õïfn !�\áS¬,­ñÁà}ö²m”WòŽ}…K‡Ïq¡}•0¹ØŠ@¶Ø+îp(¶+-ôydº¢�íewÙNnÉKÑ Zi†³7n2å�PÄH3C¼0ÇŠ�$u3‡£� Et9ëQ‡ŽSª¢hœˆ–[bµu!Ö_\Cõ�wýœ Â0l€¢ZC~ñøZþ«~ëNwýÖ›º¼¨Üä]� ‹€G�Ë—––Îì‚,vòÒ4Eýô¿û_|É÷+©ß_äÉTz·�_¡¶�ÿñÈΔN๢R|p‹—ºŸ¡ÌoÞ¹Îl–³ÚYÆM yŸKþÓ+yõÎ+äÁ€Ü?Dú%ïï¼ÏöøÞüów‡Ã³•’ƒ‘Õ5H€SÎh™U‚²G¡'9H>¥^d¹³zê»ÿ´5TY–5ÀYýt»]ä<žÅí®×ï¢FôâúCHŒu�¬Þi€ÞY¹|,«äñx\�Eñ0j[ñ¾¥q(%?–Â`j4ofk|#Ùd{j*îUƒˆBOó\Ïñ…î>[Þ}æ àÙQÐaF‘N0E†¶À™ª vµu&!(¾…`Š“ë¨ÖElzH–•�ä¡ÖÇ×r^µ¿rMº^Dy›N¢Pµ`²ïûsíÛjøí¼‘ç9 g%Š"–——è¾F¿Îm®¯£v:+Ê’¨×ù;[˶9–jÐAÉ`€¤¾«‘Œ:ñŸy|ƒÀ’ç�n§—˜>¥}x“°x®ìŠÕÏùt´C_ÎO8±®ÆD"Ç–9ÙlˆTº6ê×Vk�I÷ˆä-° ¶¨Ö:&“‹�s�«ž'¨õ;—æ._‹9ã¬{¹Øb]\CRq, ¹xn±²²Ò¼{·~…˜²d6Þ#KZ�%„¿Ì4(ŒlÎ!Ï # QÑ1f44ó%“é¥SºªMìÚ¨´O.'øé2éáy>û³˜s|’0_ÁÆ %~º‚HºÈ¤¥ÏÔßFŸsÃUè)žðˆËʼ@:™Ç¨¬Kâï2ÕGzå¢ÔüÚkÿ„•nŸßùÖïÓ³¸³»ÇÍÁMnNÞåÁî€b&ø?_ÿö†û¤ûwwùúîWø­·~“álÈ7ï¾N¿³„t!åTqÁ¿Êï¼ñ{¤yÊ{÷or‰¸ux‡¯Þý 7‡ïS˜ kÁa¸´|%<>|peîß`3�Ðw¶[QH{ IDATȦŽ�6"�±áTÉÄ¿O©fé:ÚÄM>î¸ÖÛN•Ú<ëþC•sëùˆ•••cô²óD–e"çëÏ÷}²¼¤4¶yÎÎ:ÿb.oµZMaíœk¤뮈ú å¿üRiÜ172kÆV\[kÝ‹Xxîû&fj�Kð„iЃªÝ¢X ]‘�7ç€�cøÇsÞ.mmñÂ6E6Ášù0’ÒM+¬F‡ìàUZ­8‡™~„ Ú8ÿ2ñ4Ö=½¬¿o]Ä-î.ê—i�ü,¢‹ÇÂÑË7ŠâÆ&öId“ê–A=”áû>a6& ãñÙmäÅïRŽö<ÂV‹ÂZlY>–ª ˜Ë‡•%YQÏQÜÚNõqz_d$i B·º”V“æ5çÈ ã‹åé}4?ˆ ƒöe›âäBÀA–�e…?ÀSŒ(ÙTÏ`°ÜŸÝé¼B*³ø3J5ƒÂG!"ž¢Ë%4…š”=Jýx}?+s<¡ÊÎ(¼¢‹Syp\V Éëï¾Å®}Žk—Ÿ"¾öηxg÷:‰óåë¿Ïýƒ²ÌðÛoý6Ÿ¼ü ~嫟õÖ%^ù諼ró+¬D«gùÄæ'øæÝoòÓ?ð3XSMó¾q÷Û|÷æ;,·×ùß^ÿ;tÛ1ûã÷v¸´ºŽÐ–­þ;üÈ _ä½ö;ìDÄ7¿ÃÁpÌ`2f£}‰Y°}t�Ò"…DAÙ§´†%¹Æ•v%x^O£ŸÕÍxhý, WQ5²5µ÷ùãb± µ&BÒ¼¾§Î�\Ô.w5U§æŠÕ\²_ø…_øž�Ûùßþk?çùÒS -‰4›+†È l8 @|áŒLÀƒ²Ea ‘ ¥h¨ RÚ¾dÃOi‰”‰Q§»º �áy_º†~fM�³T€˜çúÊÊG ~—¨ãò!®ØEz„ŸEDÏœ{ Áñûx‰­%¸êá™ÓŽ­;YÆ:Z­6ž§çÅóæâúüRÊÆ‰²V&8O^¼Ž,Ëð|?ŽŸ8›J릋ò¸kð•!öK”(ÈÒ'T•‹�"/ª!1'<±ñ<ïÞ»Áû{ï‘Ù“~èòç8Hyíö׉¼ˆP¶ÍFl�ï3HFüæ{¿Æ'.?Íw¾Ëýƒmž½ð%Zx¼öÁ·ø³/ÿ¿÷æ×¸°´Á—ßþ*Išß¾ñÏö_Ä…“cë…Lp*É*Ú„e�­èÝ¸× †axþÿyž7³µ~_üïõ&-Œ­$OSäÙc×áÉŽr»ÝÆ÷ýê³æ|wiíÈ@’´¥¥(ÝÇ^ØÖQ:ÁvóÚlƒ7ÇÆIŠ]@„�¬ÆŠÏöS>ïÒ“ÉcуU5eI§H�'#¼°[%j©*QðyH)1Å”È]G=ÊñmTk 3Ýa8ƒvw©i#=Éî½¶ÕZÓï÷ÊB=ÄuVÔòd:#M�[×�7¬µL&›äZÇÉ!‚G}¨åÁ`€ò}ü^�|®Åø¸�sŽ�”ò±¨íÑ1Ž@”tÄ^‰Ïˆéh� Ý]¡]²òaT뤉‡QƒÌäåB8²Ò±—&dá.VH�Ø�«m0Fç=‚b ! ¹œ!�OA† r‚¢Gé�!Ò’¹yNbá�0¦êÏ£] =>FSNd+˜BàT 6¼Ë|ýö×çC¶'wYn-ñ¹«?„±†ç6®±ÔîñÜæ5Ö:ëH!§SvÓmFé!ÃÉ!y‘ó½ñ«(¥ùñk?²"g{r——¯~’NÜâ׿ó\Û|šÒø2äw¾û{üø'„¤H½€}鋼uç:©™ñÏ¿øãÜÝ}€ n{•PCÒp—–ŠY’—Kéýý}Š¢ ßï7]„óD]8W90¹0bbìéý!sÌ�”¤EYÍø 3Oê�‹E¯–ðJ1�ÁýAÂä±A1çŽ:jIb>–!²GEá$·ò¯M׸7•dÙq©AOk.µ%?Ôñ´w€ÿ“‹zDKYü¨ƒ³Ì×€òê(yž‡™Ý#òÊÃ$û¨h�bü€ûÃвlÖÐyóðiHlm^Sž�;ÞËh4¤,˦#÷$ï�ú%<�Qaj ãI®c:�VóA€ßë‘ q®\ŒÖxs~ñ#Õyæ!…#˜‘Iá=ƒ²c&£@wV)D‡ÂÌyí@nF)Œ8ꈖÎ' «û•f„,)¬e'™�†{ 8�ÊÛ”VP˜£§xa‰™U›J§ „S¨<†öá$ÒyX/A1¾Ô¸ò|ï4‡Å†#bÑ! öpò8õHáágË„ªÅ8Ë.Þ¾CRTs"Ÿ¹ò)Úa‹««Wð”fµ½Âr{‰õνh‰ûãÛ¬ôz$EÆÔŽygô³.t/òÃÏþÿÚçÿunÜd=Þ$+2.õ.³ÞY'+Rr“£•Æ>_{ç›|áÙÏãIMVæl­^ÂS³|Æ¥Õ5–¢%2&”òaª—ÖK(ÂÚ~D¿UÉN§SF£Óé”v»MgNWyÜZ^DJÓ4e0�ò»wwøÊö³S&-ðÝýÁ±5æû>×÷Ìæ!O«¦6xÓšW?J3®ïì5ǪŸù÷¿ô%uÂz°Bk%IZIý1Õ¶M8ë³WÆXS¸ µˆHAË—¬û)žËHìéè�Âò‚ÿ€PVfàRbMQñ»|߇Á—‰»>®œUí¿Íd2æÍÀ‘§†@´èvºñXÏ5¯¶.�O˜95¤Âšª®¹+À�¿Ö.¬u…�ô¢¦ì£â$¢Æ1Ê÷ÉËœ;&¼¾¥sóVÂyí*#¿@ÉEÊxÊâlF’–ø~HUHpæàïÞÝáùð>_›¬„!›>FÄ•ÑÃt6ÆÉCŽ�IÁÄ€•·E„oc6[—¦#µ�U &ó±Q…î…ù Ex@©R�# ”óp…‡sÊ@Ћ 9§S¼¢_MÑ.¨?x¦…ÎzÞ˜ån—o|ðööY‰70ÖÐmÅ\YÛb–ÏXî,‘ÃdÄzw�Ù´Ä àæà#’rÆ¥þåêCƒŒíÑ6ÿÒK?Åg·~�Ãé!_ûèpmíyzþ}o™—.¾È›wÞæÙ Wy°·Ï›·ßA¢é]>ܹÁîpŸg6žf”Žy0x@¿Õ¥¿�)7Ž®XæéþóÇ�±E^y�„>n𲦳ԓ³ÿÙ?þ2Jk¾±½Ëg6ר™%´|ŸÒ9Æ¥á·>¼Å¬¯’Ï!Ã0ä¿ýý¯³Gô£_KfyA§Õb{2ÃW óO),Õ†¬pà)É^šjA…Ú¾zç¿òÆÛüسWIò‚ÿþþ»ï�sû|éç4€uu7MT²C ¨íi‘;Ån³W†h“É1×G…ê7Zzг†Ù)³¡(xÑ€jx¶9Ö˜y{E7¨m•‡=äà· {=lr€ð"„òÙŸ¦¼ ËäiAÛïÒiwfÎ�æ×<¾º3u.$X*„;¾†�ÏFœï­©”j¤çj.x=XV#Â罎¦»×j!<�¢,g\GéáæáÐ+Ñòø3.E…æ:“‘f9žÇ-,ð·>Úæßùö{üõ·ùýaÁó�6ë TП?à ¥ÛEË0±ìÊ»iñ²>ªŒQÚ²lâË�[é»8�á»áXQàŒªº*Ũ´Ñ°ui„£8CŠpq^çb!¨¤>ÓÖœþPýMdûØ4Ày)^lMf¼sû>µöYžßx�7¶_£E,w–è¶Zk¹´|�íÃ6ã-Z]�íá6I‘°Þ_¢µi}r›Ñó{\l_ĸ}p“ÃdÀ³רê?ŋ˟f)ZæÚòsäEA?\æÕ¯‘ä)ãCž¿x�WÞû:W×¶¸vñi¾öîk¬„«¬m¶ð*ÍßSò±Bs-|™8lÍÍ &Ç�ÐE^ú£j ¥TS»ŒÇcv§3^¹s�/n]â—ßz�gW—ùúý]–㈛£ 7ÇS>ŒXŠ+u_JBþ›ú®ô{xž¢(rniÅ1¯Ü}@¤5ûYÆ[û¤RŒò‚Npw:CkÅ+÷vˆ<�•v5�ñ÷¿{�/t‹?÷âó¼»wpÜ~*Ô6 çndð}éFÉ’-oÄå EûÁ±‡×9GVnM5·‹åж©F¼ìãGmÊ"CiSäÕ2VµìŒí&„³Hп@yøª3;à}·ÌNP½ƒ<²Î6¢-¢ðH°{}TÔ\«ÅÁ…ÇZÖI '8·µu]=aþ¸ó×˵ÕTEC½cz§³ÅÏl,$Ë’b6Ãçx+Â6ˆZ­F×ôq¡¤¥åçgn°P”’Âù´Ú}þ÷oòOw‡t´"³–Ì:þæ'/ðC­ˆv§_ñ7ÇwPޔ̖|8>¤ð‡¨´�ôJr="¤ÍKÑ9˜rÓ½�“QºAìcEI'ÝbÞ9–$%Šl¦ðãòÔ¤rÕ\,p=a³ Ò¢D ËL±Â …B¸ÆÅ¥Ë|g÷›LÅaèSÚ²ÙH8~±<ׯ-(2ÐJ!ÕÜî¸ðË¥7!,{lú$–ûËôZ•Æ¥ j1Íò)eYp˜ï’—%FäLõ>±ë!‹ ’�¶‚2´ã˜B¤´Åûö8G*'$TCw Í þXé¬�zßkŠB{NUY4ñX )ec”Ps¶þÆ+¯óïý©Ïð?¾òM~ô™§¦ã$a’\îuØO‘RðW/³”~Àÿþ­7YŽ">¿u‘£×w¸Ðiq˜eÌò’H+%¹;šÐþüóÏðËß½ÎÕ~—q–3Ésþòç^&Š"þö+¯1Ë2þÒË/ñw¿s�ÿèÏ|ñ{J˜‹ö»ÖVe­X“¦† øøiaç -«:á)D×cn”sÖ:RËÍ4bß´ÇkÞ[Á?ê`M‰� g«ù ô]k�KïÑ1ÿݹ@qðÞò'(†·y3¸ÊH=g·Ì†|šåx­¡‰<‰ø|-ƒUº�•:‘‡ë5Ç5ê<2�'óð"U¢.8žÔéÌÍç ꡟ2Ï)’äX.v€õ}¢ù»çËV¯Ò·>ÞCyCJgø`4Àz):ï’y‡•"Q\0/p±¿Å7î~¦xRS:ƒñ&è¢E.R¤>^˜w³-ÆþÝS) g1“Ó2³™@dúxE—Ip¯ióká±ážæ©Þs|°ÿ>#ï%9–Ú¨¢z÷z¦�6!So-+—TãªÏ0®D›¿ìRZCKôYõ–É\Îfƒ08zœsU§1ÏIÊ)Î:JW0p;¬ˆ  N8"­"— ±ì’»„Üdk˜]¦’Š)ëî Ï/¿Œ”²Ñ>>-Â0lTjN£ ÔúͳٌÙlÆnšñ?}í[üÐÅ rkyfy‰Až3NRî�Æüô‹Ïñ÷Þ|—Ø×ü›Ÿ~‘vòÚγ<çæ`ÄŸºÄ­ƒC®ïàœãR¯Ëõ½Öâˆ/\Úà×Þ½�¯?ûòKüÒ·ßOõz¼wpÈ_û‰­6S_ý+QÈË›ëüƒë‡7ËÒV®!’¤<&Ióý‰Õ¼—-³o2¶Š!Ë�­´kçÓ™ÕЬ§ûÜJCvÊ Ý¼â Ð~ˆ� /˜þˆÊB±^Þõd®Û¿·ŒÍˆ ƒ@0NÝæã…H¹Ëuv’›l%/²ÚÞ8—`1T‰¦F«Æãq…~žC›ó´˜Íf�¦âyÎ_KwÔœ-¨Ø8Ž›E]Ëõ��‚½$¥ø Ÿ[:8È2ÖÃà˜ÆhÜë‘g6Mñ¤äþ,e-�ééžú»HÉn’kM¬†â¡Â6³‚½¤äRKUÚ¢lÆ×†üê½=þ…õ%þêµK|4Mù�ß¼Á_ÿpŸøÅ—˜ÎÆH�`qÜMN¥›¤á¥(QΣÌ49!ôCÌ~LÛo1+B±„Ű[nÓË—ª TYÌ÷È'üda{V2]ü﹜�_f+j†(=Ì‚6¡u†Žês±}™ÛƒÛ$á$�»d>�\…¶-ŒJ+N°áƒan»(?£ðGÈ2¤(5m´ÏGÉ;<%ža)^ÃÓ(ðç…Ê*ë —ªF³„�3ØÉ�ÔE¶0ÆPEQ°WÜÃ9G?^–í‚£—òiÚ¤‹ÚÏ5 §�RòÎîÿÏõùÙO¿È�Áˆ²4LJËr»Å�]¹ÄÏùUzQÀZ…!ÿëw®c�ãÕ»÷ùÉO<˯¾ñ[ý.߸÷€�vÅ+]Š#þÅçŸáø½¯á+…ÆYÎwì²Eè¹ãáýñ„oÝ{À…N‹ßzï‘úÃAW+•šj˜·²<ÿCùØ?ô¨ic»eĪšñt0¢í+Ôœâ$¥`5V,…)ÉŒ÷“Ɇ£ý*'×neÖ9œP0_cU±(�Ó¯ ×.b&÷Qí ¸rÆ¡í2VÇŸ³±8`â<˜,³©®²ÚÞhhX�âÖ6»@³Þj º'uX:¯®g§I(-òÔ…nm‘eÁkö(ŒåóÖŽ©Ö|ùÎ6úÒja2Š"â~Ÿ4IpYÆ«öøç.mâ/\óÉk¨ÿýýᘛÃ1_¸¸Á(ŸñœïÏ¡¢¹mƒü³Û|v£O׫‘{GfK~éö™µ|q¹Ã':¿xk‡û_åüýê–ª:÷N1€"DXE>¨\ÆœD¦==Æ“š�àv™–S|Ó¥eV˜¸š^PøClÃë=NN|ü¨ÝÑßX�à²p.kØbÞ«ÓêOv’¾^g’OÙq7À§°gñlL‘CV3…}˜&PªF%„Å +þ2¾¸—¿ÇƒÑ{¬ù—Ø·ˆýV3#£µ&æˆú²ÁÅc÷í(j—ÄêùrÎqÁm‘å“|„¯|¤”äyþHíûzžVSœ”ð¬ã«Ëü䵫DZñ·^ÿ/o®3•’ ½.›íÇ,/Ñógîw>øˆ íoíîñS/<Ë/~ãî¹§yõÎ=bOó§·.r2å…Í ~í½�ð¤$- ÆZþ?êÞ<Ȳì®ïüœånoÏ}«¬¬½ª7µ´#!!4€ È€ñˆ™lÏb` ‡Àž〈 3f¼Ø²eƒ@2’P7ÝR«÷¥ªººkϪÜóíw9÷œùã¾÷*·ZÔj æ×QÑ]�yóÝ|ïÜßù�ßï»d¹¥ú¼ÿèa¤”¼pc•­6™1t’”šïí.n­ƒ (DÄ=ý­SHx=ဠ°i¦™Èú,ù-¾C6e! ijAÊV¼N;±D2Gy•]?'ÏRÂrk Ÿçyäñeµ‚Ї0­+xc'ÉÚ7¸ÍcЋËDÌEža½3Á´:ÌDéV‘{»Ä6<�Çq<úž»is"daM¹'†_ß«K{»×*6ìeÇ;¿C;ß¡Æh¿ßç_<ó"¥ˆ+ÍŸxè>šiJ¤ýÌðÅË×ùþSÇð¤ÀJI;I¸Òl3[¯Ñt‚9ßçÓ/œç»Oç˜lw{dÆàgi%ÓQ�wþÓK×èe†›�.ýÁ“´“¯ÑN3j�ÏF?AKÍg_»Á'ZâZ;f±–ëqJfO7»œm÷8Û.‹d4f1¦‰ò-[y›V?ÇÓ9IXXÇjRJfèûk3SY`Á_dÍ»Acœ°„A†É»è¬ R‹ƒâ^’êè3T†¾mâ÷¦0Ñæ¾¯O‹%¬´lØkéu›kòðîÝp«c&J D"$Ñ-zn�sÙQ³ÆŒw˜©`žÐ�È]ιí—i¥-ŽÖŽ3[šÝÿ²ƒNÑȹH©ÑŸ0 ©¸Säy¾ Ïx'ÂÁPaèT6Ôyoö*Š|ûÒ?xê(Î9mwÈ]Al¨Ö«TÊeÞsäußãå­&ï>Ú@ øÛïxÏ­®rqcƒéJ™‡g¦˜¯Vxus›…F}4žž­UY¬WùÔÙ (!xû—šMÕëøZqamƒŸxø>NŒÕùƒ³¯R îì�~/áxZ µ Ióo‰qÎ7¹¬˜2-rÈ´™õû„¾€•”L•%5¿C7ŽñµºåÉ­5T©¾ôCÒŸi½L© Îæ8£*s$Û7¹-áØ¿1;,m±NÛ®ÓÜ>´¿¸K|~/Ôà †5ë°ÛíŽ q†Åå­Nðí%À†äš�ºž·Ó׿á½ýa‘¼Óé¬Z­Eÿé¥óXk‰<Í‹ÛJr¥Ùæý‡8»^L ŽÔª\iµ©//_*ˆÏJqd¬Îùf›¾ƒ¹±:‘RŒ+É«Ûmæ«e¿~“c�ÇëUnöc¾péßsâ_¸|•““UnöOßXçáÙ ^XÝd¶RâüF‹3“ žXÞ ÔŠ÷.ÅUj-©- As�Ïr¿(ð2 øEœn£ç,pK;Õ9Kw)Udy‘÷¬µhbýÿ¥<>N³×ç±ë+ÌÔ&˜)—y- F� éA)òéôb•›­mB/¤T˜V‹Œû3Ú‘ n†Æ9Ã�ºÃeê…;•s£iBžçEww¨’s@£á ð}ŸR©4‚:HYàºkµî Ͼ¾Öš_ÿÂcüÝw½•$ŽIòœöÌ‹üµ“Çøò•ë´’”@+>rêðÒÀS’Ͻz™O<|?øÊE&Ë%¾çô þís/!œåã÷Ÿâÿ~êÞ2?ÍÍv—£cuÞ>3ÉãË«xZñ¶é /ãwž½À±F !`³ŸpßTƒõnÌÅí6G5žº±J3Iù{?º_æ7oöøýëëüƒû�2ékþl£Éo½z�ϼçAKGf¯c¬åJg›Ž¿2G»WPB’Û#Ç£<Ìlí—6_eݬ�…û› ‰ŸŒëMl®�Ú öþÜK.¤ q¸Âé¸WEI…S)ÖïrZ½/ô¹Ð|�Ž^=ðgä±� ïMK¥Uª¼#žO¾¼ïë¾¹±Ýäw_ùFÿÏ:Ë�Ÿüßä‡XëÜäZvž”>%3NÇ[+ŠfDAH–Ñ`\Ï2YžMÚ†ÐÃáìn„Ú!Ô¥R©Œ¸9CY½Ñ{·§ó¿óïÃëK¥J)úé¯JèÓîtÈLaà0D¸Hq«û^ü ‡’‚J¹ ƒi÷ðà¹w_>ߣ¬'OKâ$çˆÃßp””}C‰jÆ .%U¾Ú�åB' ÛO ’ ”Œ [€Ü$¸<Ã9F8S!iû5¢ �›Yþ×/}šßîI>¿ae ›(üÇÊú&7W¶ø‹—žáæj‹—^»Èµ•5Öã ~ÿÜïrný>sîO¨7ê£Ñ×P>h¨i8ò§Ä¼&žb›ÕÁ‡\|€�Nç–ž›RƒÍáÎ1ì’APtWs—£â:cöJkÖ“åƒ [@ …½GŒtžC$K»oçp áÓ×þˆ7Ÿç{—>‡}˜Ép’úòo³¯P¯Ô™a…¡K‹›ô\“žkÑb�{‰sé“<ß}Œ^|«+ÁH×3·¶�º>|øŒÄq<’JÂ#`€sÜùýÎáÒ¹Ñ`2`Ï#ËòÂ"ÚSôúq±Œ~qG %£¾œs()I3S¸AIA½Vc²^ÃS…„�pn”�¥sÅß¿�p/.¥ ›ozƒA Çt�áßÎõïuFã¹Þ$k]K–&;6=¹+Àµ¦IŒïûdYV˜ul?GX*Šßkë×¹'l´›üÇ«W&àÚõ È ùÇkk7è´R>ùIJµÕãñs_£¹¶¾Â?þÒoð¯_ü§ü_�þ6J+ÆÆÆFj;»¶Ãû‹érMœå²x�.Û mäI’P¯×‹†È×ÑêÒ1ãBH>óg_âs_ü2Ë7oÞõúa.o·Û®U¹°Ýâz’ñòV«ÈË�Ošɧäy´ÓŒ¾)4“gÊ%aÀt¹4Z¯‘çဒÐJŠ<{l¬ÁB­Ê—¯\GÁl¥ÄÕf›¾ÍøìÅ[÷¸T/ã)I¤óÕbOKóœHkÞ¿´@ "¤ŽøŽ/°ùüø×ÎòÝñÿèÂ5þçS‡8S‰Èó”Ø8–;1IP(è¸AžzȨO®í²'_u—ɲŒÙÒeQC›ò¾÷ÈaI‚uªù . Œ·î”‹ÅàmC*Ù è#|é“D+X™õæ‰Â«­U:zí6?§pn,›)Ô=XT—¨#…¤go£íŠæ/ÿ!ÓÑ ?~ê¿æã'~Œ£Õcü‡KŸd3Ý`¶¾À}•·Ó�ÓpœÁ’“c0"¥ëš¬ØËl›õQ±YÈ빸T*sáÒeZíÎ]sñ�h9Œ½®Œ{ÿ{g®NNó<ÇXW¸Bj…1†438vA· ˜’Û©£/È-l7[ôûñ¨¦9Hwx�úøÏ=òˆµ¾I)AœäDѽIh¼Þ¥åÛÆ:Ô=C3Ó÷Æ•¹9‚í_zåK¼iöAÊ~utꉢhŸ¶ì†¸F‡-:b“¾h¡ññE´KÏÍB|Oßq´»3vêÒA@«ÓåÙ^"3†CsûGÌ]oÚ‰k½>�]]æJ«Íw�<Æï¿xŽN’òŽCs£y~c‹™J‰PkkUšIÂXP Cžº±Ê+뛼}nšËÍ6WÛ]–êU®l·h%)''ƨzßóøìkW8»±Á›f'Øì'T}Ÿ‡¦ª<·²Å×שÆ¿G6{1G f«ãxZó¾©1žiuyl³Elÿð�#|×ôØ€•­Xí¶ñJ1q_Ø*Æë�úÛ8¹{ˆä°Ä¶Ç¸ž%T%’4)Rò€a“ÊqÒ�‰Ôw×Vôó*¹LG ÕY‰—Ô±A„#†R6I´Q E'Yë­ÓÌÖñ•Þå….�Fd!‘™`;_§fgHò”@†»,#w†ÎËLê9¤„›ù%ö i•T…Ï^ü<ï˜~7mÓ¤�uxóä[¹Ú¹ÌL4Ëñú "¿Ä„7‡Îšv»ÏJX0£3S)Ü£†k}/xô‰¯²¾¹…’’JùöÆ;Ù»½^� v9•ÝiòðŠq<ÖIéKÅ|ä“e9�¯QúýÞ½éT‹[˜fœRûýþ�„¥×«sûñ¿óÈ/¤©õ¿èêI hÂ73&}Ûëfü)£x#[}«Y1eÚ¹FÛ˜@ä±�o™� “ÆX“D„”ä&!èþ)~m†¼u…m9A»×ä- ÷ñ¥«ç9{ýröÚŽÏ/¢„Ç+7_£TÙên¡•ÇVo­<ÖÚkLÕ¦èæ-ju�Mµ¨±ËDfçæØa‹¶Ø #¡%ÖˆE‡5´ðn9ÖIE½Z¹'u�Ñï)ÄH!çÆêë›XgY˜�Åóî¾ß'‹GêU¿ºÌj·Çw;‚¯5ÏÜXå»�¦ø¼evŠ?¿²Ìáz•ÓãcÔ|�jà1W)Q ŽNMðÌòM–*%Þ<3ÉWn¬qzj‚™RÈËk<<;Å\9b, XïǼ¸¾Î»&�jeÆ#�£õ�__GIÁ±±*ožg­—�åpfzg-å0äCS žÚî°œ¤üý‡ø™cóç0ƲÕO|K;NÉ�„°‹ÓÉhü¿3ŒHðM™‰êiÚ§™¶ ’ìöå©êx •V´AßõžæÂj�y19³^ZÇ„­QŽÍ…Afø ‡ü£h/à\óEJn ã ¾ôqÂa3�0�€\£•Ä:‹0�i œÆ©ƒ‰Æu;G#h°ž_§ëöû>Ÿ»üy>°ð!¶ÒMb3_^àÙ�§yïÌû'¼€‰`å4]ÛÀ2nEH™“•‡ñµOÇ£\¬µfmcƒ^>G)Š(•"”Rwäê„a8"I‹C囃®Ròå^Æo]Ûà�6»¤BrªbsG)ô‡ÍsÌm®ß÷ó„$·Ok´¾¥…}ÇHCáŒS.y˜Ü}KHd‡¢”äN™KYNB.öûÆu*:¹Çóý jiÆRºÍd`ð<ÿ4À ɳgRlž.Û/Q� r™Ë:¨ÚlnY ª8çxéúËh© u…Ђë[˼íÈÛ¸¾µL }¬³L”'85s’‡–îçÊÖU–Õ9ÖúWXˆO2;VlöC+ÃáýŒ¹9z¢M�&¶éˆm*Œ1㎈R�W4–(ðß7b»] ¿Ç /ž=Ç;Þúf&'ÆGÚ÷² œs|xi!%AE?ÿÁ÷Ðïõ˜b ‡cˆÁi냋s �z�·>D»Ó!M~òÍ÷ã p<ï?z˜xÐÁvÎñÖéqÞ{h Oæ@ÎÇN Špgø‰‡–pB"\2$Æi¬(‘¥1" ©jÅ?~à—»}~ta’�ÎŽ#` V Júô{‘*0NFÝ>¶ÅkÝ,Œ-1‘NЉ[dtpz÷ÔÉ/ÌÈ’š}r3ÃQ€³ßÖI½&Â*¼¤An�Šb' ÿ&^¿ÁD8A)ˆx­}égtÓŒ0›£§×)çÅ«è”nx貌vš,Sh;F6÷%Um#|哸ÆíŸY—é�ÇWå—¾íW8^;Åÿñâo²™lÒ·-^m¾Äbù8�27vˆZÜàrÿ,vyT(‡”9/ºìøÐº±Óíqsu�·=ü&N?ÆæÖ½~ŸzµºO’h(Q7ÄMB1æ­×ë¤iz vÌJÉ?º¶É?¿²NÝ×4SæêüæG¨ê‚I_¯ÕŠëûqÁâ¿ÍsЗM+A€�¦›@ÃöGp‰;á꿞°¶ ô¾)Ö|3CÇÊ16·xä S¾Ï«½ˆ­TˆÅÝá€u±�‡,ä½6áؘ” ¥4yc�Í' *Uœ‰AH„Žˆ ¼f‘Òl6©Gufë38õr…Ç^yœŸúàßâ?>ýG¼óø;Xoo0^c¼2ÎTm%¹MÛm‘¶cŽLœåàá¿ëL#œàº8_`hqQ~úXñÞòðÓã`-ï^:„çyœªUF„å�=4Êé‹õÚÈ!Í9LJ–& tœc±âq«[-øø™¹Íëœ��š#uer“ ¼‚í?åi91ϯ_¸ÎO/ä7 —F¹ÎZ;Æ3ü¸F,n¯äpÜp¨öÌTh³I3è@ŒÜ#wF&ûø2@›òÀ|¿ô*Gõk© µƒ$$‹Éé0¬î2FØ_dlr’ë�kèRLBL=�§Ýkcññ<ÂaýÞbäNã›*Õdžvp »#kàÉbï컃;·`'xaó9~õ�ÿ;�<ù‹TýëùÄvÆáÒI¢ ÄLmžz:Æ•îyVíÕ[2ï'òK#¾‚sn$=úä3ÏròØQæçf©UÊ|íÙç©W«,ÌÍìÊÅBˆ]ñ!§g_Œ¢h$)w‹#$øƒÍ.¿rþK倪Vüýç/r¾ÕãO/Ž i-¤Óía¬e¯>=0…¯¦ežïJœÙ’œ(9>EöØØÝnwWãOýàOÿÒ#�Ö’~ßøßܤHËýÕØÁç5m˜ I7WoXbHœbÍ”‰­"Üãt†�H!ñ¢ôïµ>CؘÂvo"£I�pö9VºMNÏŸ`¦>M?�™mÌ0Ѩ‘º„íf�“óÇPRQ/×ñµG%¬pyý '¦Nòâõ—¹ïÐiRúLêyªQ}„o:jXkQB““ÑÛ(43î(©èÓmªL �Yx„÷z½}þÎwb+¥XÛܤÛëqce…3'Ž�äþ§³§œ’ü¡‡NgCa~3иÝaŒÜOºM=ò&q£•  cð¤%Ô)·4·’“CŒ0ÇÅŸÜ ,^Ñ-(ÊÍmjÞãçh±[÷V){ër›úª;£Ã�|–Z¹A?o‘då|¬Ú_‘¢Ò2**ßÕÁÝ™T­J! Y-SÚUØŽ¾_84> =G𧬹×Èe‚¶Fõ‘A‚ŸWHJ7qÆ+,))cEVtQU†9*n`uO’dW.®ãL*þÞÙë|d~‚Ÿ:6ÇwL7XˆþååU>:?�Žû£ë­µT*e<­1Y¶oª¦”âç/Eœ( ~ãŠà½cðO® ~b&¥ßï�$ü¢(­õÃ?ûÈ#ž'IÓ­%{ Þè¤c\§xb÷Lc™ôSfBC'Woh c=VL™¾„¶Çpâç%¤ÒØÞEJÞ2BùØx Už!ÙºÆü‰2?3CµR­”˜›cj| ã¤Pœš;IùL7&¯7hTk”£€“sÇѾàôü „‚’«q¸rrD‚¹‡eIÓd�Xtc–)ScŠ*ãHdQˆ+…k(å1L¬·ÓJ¥øÔ–8N¨”Ëœ9y­ÔêP­VG�±{‰�ÂçišŽdJ‚ �ùv‚Óëõ[‰î ÝÜ¡Y†….£±ØÜ ”»§iNˆÍ-Úñ}Ÿ4M0q“#aDIBmÈr5ò‘7™A�Óe›N–ìÓHÜVä¤yLÅ5˜*Ïçl.Ñy´«ë+�H©Ð^ŽNëÈÜê öl�´*�H£ƒFúEȬ̩‰3œÛÎúæ&7WW¹ÿô ʃ�xˆ)JÝÝ“™»sq–e£\¼SblÔ‘Þ‘‡oGæô(Š£qZŒŒµº{�›Y‡FHIEtÚoâûAHHŒIñƒÒÈj^kG—®1X‘“ଵ3ú¢Ðžg¨© b×Äd…ÚÕ-ÕYi}²h+StVÇ7U”bTKgUnßö™S¦Ä±Ê}lô7Ø× 0™2d^pøY ‘îhB¬ÌˆE· œga6�‘}<á#œÂª›ø¤™`±¼„!e5¿ºgÂ&È󈘚†±(å©Õ§hg-Þè}¼mþAŒK‘N£œÇ*—h'-JT ¼�’_fŸ£.'‰‚Ò.Æa8ç¨U*LON`Œáâ•«ø¾Ç�•5.^¹ÊÒáEZ­6õzm¤�pÐ>>l8ì4–BÐ19¿qi•N7x¡ÕåR/&R’Ï­mó}“&Äþë…T+¤di68lÒ“ŸÜøÙ#ÐËá§áßÝ€OL'£õœ$ yž�j"õ‘ŸüÅG|O‚ß‹ÇÌ –“ãU•£ölüšœù ¥¬òÂiì î´­Ï SÁ·}B×'ªÔ1YŸJY£T‚ím`ÂdžrÅj6|‡^Æaq2Çb<à„¹�8,Z $p$Rœ´àÇõÃ…Px–Œ¬w~ µj�ºœD›€ŠG =@d~!|d 9Ll©;‹\A�{ÙØÚæô‰cT2a;;`;ÝF¾^§³¡”™R ÏóF]RcÌ.7©Ûéöïs˜ £R í—é'ksô]È.9>©q£â¢ÛiâiIUP^„$Åä9AX(O,oß ±–ÜHâ,/àø·=ÙÄ¢ClúTå8S¥9r“Òtkˆ,Âé�ÀO&ð„OêoaDŠÑ¤TøéÒz·ØÀy@”M`¥! 6n/*n=¼S€`%¿€ðS„Þ±džˆ¼è>g²‹Õ1B2¯ƒD‘Ë ã·Hº - Mš9êrŠzXc%½LÊnXF’.âœGš—89åÈs؈7ù÷}œjŒî7ÊëôU“.MÖ’e¤Ñ”�ƒF4Ž’êŽ¦$Cã‰RTâòµkäyÎìô4IšòµgŸgb|œõÍ-¦&&n{0Æpí e“Îvc¾°Ñæ}“5r îi]o±j>Ô¸…ïÝ·ö¢�( ÈMF>`z;`܇ØÂg×àÓé¨`b!‡�¯_ûµ_{}˜ÛŸ+ʤßã‹`=óYÏ|<á¥eçËJ,u�±$$NÑ·’{;nÞ[dN²fJlf>³ABUˆ»›h®Ð¶m›Ô&ã¢,ÓRf4Ò•B‘» ‹-ò° Gërøoi5išã,H%0.C[�cáC„AH/é±ÑÞÀŒ…ó<IÎMT¦©Û‚¼²Ÿ„$$¾ï‘î8lí�&T«û�Òœsœ<~Œ'Ÿ}žïüö÷RL†×ïu:»ÛDno ÷c Zë]¹Ø÷ý m¯üã®_mðZI’ '¤ÑO-8‹”·_Æ)Ï+^«Ûí¨Tyà`'ΠýRJ¶ã&­¬ËvÇ€UÄ6Áwå"oa�ˆ}M€žh‘$1Ñ,c^ÑÁ5¹Cä>(C�L`È0w2',¹î–ºi©€¦¡! ²12ÙÃ�;¶ƒw…J6Çb}‘kí×ȼV¡Â ‡j’’™Ä©êà„-öi t�`te«Š1l¢¥"÷zŒ¹Y&‚IŒHX·×wíÎùô“c¤fŠ08Ç|µNfãá8Xz†Aà ¢­‘)}:¬e×±)”u_û^q˜ê=Cò×ÌÔ$õZ��­mf¦'ÙÚjrþµ‹Øù;ð½}M†½ëgøù¾O•ø“ÕmÞÔ(ì ‹QÀsÛ]~b®Ayãz­5ÕJ7ÄóJÉ¿ß ˜ àƒðj¾¸Y·;sñΚHýØÿ+�¤YŽú%Õa´r�µÌÇ"©(Ãî—v”•a&H�BÐÎõšX‚˜€¥º.ÆüÍó|ò¹ßã+7n26þ&>ùòW¨‡ó¬øU´§ñeˆ›mª¥žôñeÁž÷„�'}¬Ëñ„Ï—ž‚K7¯qñæŽNÁSš†�fº:_œÞÏÿ¿B¨oYß›”’‰Ê4Jè}§÷ÂoÜíë‚í,’wЇ¯÷<�£K‡Y[ß`vzr#r'éìõXHî¼ÿ<ÏñµÐ#ÇÐÍ{([àe ²—ÙWä ܆7ÅD8‹4‚Íl�—Qž-÷„@µdR!É ~hȼNݹ™ãçfƒ#hØÅ}MäF�“a½Û~ƒ�.tUzj›’#¾+ÓŠÿ^Ï—wåxë:½‡)¯á{k$I˜l0L $h¯x³Ø##Cêâsµä4Ý:­d›qof„m÷¼‚Ku§æ•4Ð�6·\¿y“¾ï=œ}åU¶¶¶9²xˆ4ÍÈL6R9ð×<ÖdL•Bþ·³×x|£Í×¶:|nu›Ÿ]šâ½ÕðÀkG×C’f„�O)ŠÀ:>ÛôùÏëðÙuøÒ&D ~d"Þ·¦‡5‘úøÏ=òHœä„Á·^,Ü8É–ñ¸™„DÊí‘“@Cg̳¼“«7¤ÈÀÕã%�´ßäk—>ÅÌø<ïŸçj·ËK«WX˜~'z�?|ú�9{õ‘ªðoý·|Ûáwð¥gy}�ÓÓ÷ñ©¯~š—®žãÄÌ <­ùÊ+Oó×ßõ1®l\Å#à+çŸæêú ÆÊãüöŸÿœ…È9·~–ÅÚâ®ûžZ‚ ØWderîø~Æ´C2i9¬âÛ>G¯sûË¿àœóþ²tmc«XIz¹¢¬r¼=L -S^B¨,=«ÈÜ7~ŸžtŠ pxAoG¡-¥Ä÷"BY¨ª�&m"2#Áï“ê6Î�Ÿ�¡l€Tv¤àÐ5²ž Qm0NÐÍš$°}µ)“ç`Â-œ—$“…½ã]’ª—Õ9\;Ævo›-uåÀïÑ„dÖ€ww›f•TQy„ º8‘Sµ3·-n=½…§·Â"¤@jŸfÿÛ±Ì†× Ï'P~†”`25êLÉ%ªGØÞÞÞ…ÉÞIê»]BtÎQŠ"¦''™g«ÙäèáE®ß¸I¹\âü« |Ÿje¿ÎåΟQ.—Y,—8S ùãÕmþ—3‡ø‘é:æ.XF7H¨Brë0YʉFÈ;êðPÐç´h±¤“;N!^oqûÃ?ûË¿à{ÊûVvm÷†CÐÉ5ËI„APÕ†½ÛBU‚„@9ZÆû†¤ÃŽ„=k’<Ï�Í?% «\Z»Ì›ޱžZ.­ß VZd­»F-¬ó}÷}„ç¯?OäGQ¹FœIŒÉGE®¬SXWäûa›å‹DyÂp÷~çy¡_EÙ€H[êÔt„MJX'°^L®ûH"­ l¡j€“¶Š´͸E–æ�:˜›¡—wÈD²«P”ÖGf%L´Â"¨¬D®ïœ;…õ˜SÇñ=ŸÕô ™Üé|S+`wmZ„$±@E ¹JHsÃlp{Pq+,¾·†· +–bó¾×ÄÓ[䉇2„²#>…��€S¥·x!qÓjµHÓt®�»5ª•2‡‘™œù™)V76QJ¡µæÙ^fnfß»½º–…6øéЧ“梀_½oo y{Ûu, „(Ê`­µÀ£®,Uað³Þ [™ûWÂÇ[ÆãùN�gÛuâ°¶ã^Æ›*-®¶ðåÝÇ+…Ž¥¨À6¥½U"q‘æNðW_å‰mÅg_þ ÈBH²4CK�¯Ƽi”õ˜*ÍòêêEnn¯P B¤#ü—q‘ð�‡ÞÍ;x�µÖÆXúY„@ …’Š�>øIíâå+LŒ�sti%S“÷tíðõ‡&I’P«ÕF–¾w‹aqÐév¸±u•,+¬Z­F½^¿ãˆÙ“vÄÐUÒyÏ5é´¶RV&Il™~V¨jxÊàòxä²"¥¤VkV¦ +3ÁõýïOµÒ Z>v’PùÌV"…ã”û TE«c’p «D\!�}òØ'M$YnYInr­ý Qqªú»�;„Ã¥SE§4Ø‘ý>"‹öÝÏžwŸÈ6ý€m³rÛïÊR º�|ÌîðE„�†Þë‚áæ*9hDo;FoRZ¢è*¥èR¤ä™FyE¡o�C:|‡¢cQØHµ···GãÐ h4£õqP áF½Æ§O±¹µÍúæ&.^¢V­�šlß&¹3†¶¿Ijx@Ã�Î6øNÏ¢¤¤ÑhŒÌ ö†;NLÜ*´a`S-Ä.€7:r³¿ö—¸G<Õnp-‰öÊŽy¿Ï[ªÛ ã}¼‰{ _Z—LQ°µ.}ækU:Æñ»ç/ñä¥sTËG)y¿BÙ+ƒƒùÚ!«KÄ=C/Žù쳟çÁCg(‡e,9å0"Í{”‚�÷œy;§Ž,ârK§ß§—ö�BR�êh¡ù©÷ü4—¶.Ñ7wÆ¡sá“[©*w[J)n¬®ñÄSÏð¦î#3ãc ‚ øºrñð9ÊóœF£At‡ŽÙ0væá~¿Ï‹­ÇYo®�0¹õz}T0ô³¤p»Š5! dH³M·½…çyD• [&É$…ºK¥ä¨³ÕZƒJmr$#uÐ{T«Õ©D‡!ŸBâ1_ó˜V jÉãr ”Á†M¬š.AB�®Ü¤Í«âU¶ÚT«5N—߯¼;y‹«xi�Üf©ì`q#ãœÛ…—Ö˜­Í°Ú½I¬v‘YˆóûwÕ6èÉ-"ÏÛ#_V`Qï%<½Nµü4Qp —+ä¢_nRy{V¡TF’_RJò<§ÕjÑl6Gk ÑhŒ4ÄŠáú)…žÖ$IÂöv“kË7¨V+LMM�H˜{c¸þrëèõb~ ñ£ã++¸Dg¯ ÄèZ±ûs‘@4øÞíf‹Ìêõúm×Ô0Ä¿~1wþ_�âvo(áXb惘è©ãדˆIHÿë � ú<4aÁæØõÿDuLãLÆ¥6¬Å}ÎT—XK„º„¯}zI%5¡²¯1Užæ•õsôe“úXH'î!…@)‰“9iß•<2›pL>Ìõæ*�¨Áx0Nô“‘ ¶ú[Œ‡ãw¿avº~H¹ÞÖ§|RJ®.ßàʵå‚I›¥¼ï]ï¤\*t9ûýþAá·{ý¡®çÝ®÷<�z½Ž1†WšÏ±ÆÊ®ÁQïAjåÆèDŸ$ ½^ïÀN®––PÔžL– 2ë–j!èu»x¢�§-ý¼B•Fªpë�`Œ)äElŽ„#üÑ0’$¡oãD +RV;)¾µI;ïîÒ&„¿J\F†}ŽÉ‡™nÌ~òím.›—èˆ-tZ%S½}ã4?ž ¶n› uZåLõ­4Óm–{W y•ïº?«aqïÞ¬l•)#�*ÈN2gàPc�³Ý'ir°ÓÎAá¬* þk›ø(¿`´.¨“¯ß_$ íí;Cò!|`¨Õ¼3�b� » RJší6Ý^Ÿó¯^äcÿå÷¯=°B,‡Q­V‘Ê£—¤X“’gi�q`À‡˜ô½ú´NîžjxJҨװֲ¹ÝÂ:‡’P¸ v»Ý±ÄSSS¯«DÚ゙k¿ÙÉœã¥.“^zà6ÜÉ5çzÚæÞõqO—:œ“dYŠ·ñ{”&¦È»+äá,‰�ùôÒc„ ±å„0ã&)ÓÿD“¨t“O�@×£ ½¬Ç8Ý%¬2*—S²háCƒÃ2t{Üï¿A%Teƒ£­«º�]e=âèZßB.Ï_�IÈ] l ¥äé‹#FÃ!£á€8 9ØÛ]‘îòê"p}ÛFÝcU׳{¼ßÓù„OôO§-¸‡3´›JOöÎ{ëÅã$• �Â!ÅÙg"%„ª¦*K´Y’ÎdL¥Å™ÔäWŸRs„Š©Ê‚DÍ ¥!e‘SÕµü>aÙ¹‰{8“ᬣ ¦eEj{d2iÐnÉŽGRi A³ŽÅ!Ö8&ó)£á˜}u³©£ÓWGhª¿µÕ g`CŒ«ûÜÛyÀ''¿b=a(ö(˜Uc¤²§ù�$’TòbŠª>D©³ÿî°H.1·ŠÃà.Qò\±urku€�±Tb²:DEÍœw7xƒý¥¾øº-m7¼Æ3°B÷PŸct¥ãœkO£Á€A¿×X� Ñây“$ióŠÁ`€CPÕ†$Q²É!ºUÞM¸v³¦oŠÞZ3�-–+^3+m(«’d™Ó¬ê¿úþÁ?Øê‰~‰ø:ºlf™ä^$YÇaT²UTV²¸$É}9]p³/Ѻ&˜þK’Ѹà ób“¼uéõx ~–öØËé×»Ô¶¦g»J�à~ð vzûÍKéØìvµ·&mI K*]W²f�t&¥d¶XpïÎþý�Bžçܹu¥ÎüÜýù׉Û`È®ºþFþ#EëšOç0Gì¹Û ØmŸßEÕj’+0Nb¬"Pv%÷PÒŠš¢,0NÐëIÓlYÍ�0'RšZ[Â8ÃÔ9Ñò7éPˉU� kf¯,Ç=œI�$„¢²uŸHÄ8Yc0¸*ÆD !aÏÝa”îòxöˆ�§²ï³?¸ÁX�Q:¢ó{[Yg¸ 8oÚà$Yq“×wßàÉô1“à…ÉqŽfˆ2CÙ¥S„Ò(Å…ú¸Ö¦3@©3÷.e£æJC\�%#jJNìv•[SK¤eÁ�©‚¦j Ü ^ç w»5l¸(ººœ›0`qÜ0«½aC7œk0ãÖÚVª¨»Iò¿£-hcI¢ MBºc{]“±‘�[²ˆ€ Ž2/x?ÏWߦzØ8þ ÁR“ñlƒö›–ÜJšó×AÄÍ­âI3Õ!£ &è¶­�L¢ ‰cf‚�x\¼žÍÙëGTå”dñ¯‰F7@"ÃWž2·obÃÛ^Ǻ:ÆN—Ýà&qÕgÆñÊwñRø£ÞxÅgíj�WUu9žUø»‰õ$ÓÏ…RJ¾xô„‡DQȳ/xéÞÝva_¿þ­Ï¿áþ�1ôûý•�V·ñùì#NÅSúì°Ã™íº×yösŸÿ×MQ´mÈÜa'@ �¨)‹KÀpØtÕòù ýø!"Lq¢‡ º ™‡¿‹Wû|ÃéjرÝ"­GÔ®¢ zŒy¹ÿ6ApæbÓ^ë—!m ήüƺd�'êüÅø/NNxóµWyç­7ÎÉ»uÏßHn]�4vÙõûÝÞ³éc>uï Qò€�Ué_Åó;?ÿQœÓ‡D m#�Ô• Bå�®¦6ÑRZ§,KœmˆÚ*Â8£.ç„ju!¤Cº‚E^b\s ~b�¢˜(`uDX†‰ÄÔc9F (­Æ%‰ës/~�àxvÊiò ÓbÆt1c/=`7kô2…iˆ�¬2Œ³œU”�I˼±ûÚhÕQˆ)¡K‰È0¢FØ€2—¨Ðª] T°Ù_]Hƒ6c)kœ“8©74g(jÍar$<7_\ú¾Û÷U(Ä’ãêtD5ù”¯÷¿ÝX¨ÓºÕ\5Ž|‚iŒiIg>ñÌóüʱè�÷�€ šÝ|­ÁAª•Må¹g$Ö$ðz=œiª#_µ˜Í0n3&Îc1ËbÕ)íËš8üº’Û×zïH¹‘è¯hÈà,¬âq•à¤Ò¬$¹J8všÛq£T2·« 7aÅë#Ó0¼�þ ½Aã†åª 2L0á7)ÄKX'¶š‹º•Ýá>;ÜBê�B̸= ßh .“QB´‰Ù…d)W7-ξ“ðüè˜ãÓS^óö¯¨óE—õóYC�uÒYš¦gÝ3û>‰@rè­!_üfÓ;öùMâ&…“¦Ø äj±APÊb]@œ¤í³+*·Lö•¡ëŠP­v`›‚ƒEº’Å"Ç!W QE}l)M?ÌrÁ½d‡ii0ñ�ä%ùýtÀ“Oв`.Ž9Ê�©ôŒQ´Çaÿ6»î±é!�Ä–©ÌÀʆdF€³W{¼Þ§Q¹X¼ÇT>à K­-J�¬227¢¢D8I-rÂz€ ΛõY³(^CÊcS„¨qÎ mˆ¨c!ãdÌÄ‘»«af¦¤ÄÖ؆w"Ý<ƒèmvÒýöù×u½Õšîǰ/xØ€ßälƒïîj,A@¥-Z[Ò8 ªÊsE†•gÔÇBÀ`Ðo¥i}n3�N’Ù†ßB€µ”åÙf5Äðõ³#$ðÝñŒÝ°¦¶ð¢Žøå"a¦¿¹1%aŃd±’äúÐK£ˆ�ól)ó ßîŸrwQ•s£?&Û¿…ž~†ÊqÖaÒ¿‹ vÎû#_Ý�rY<ý1ÏÅgÜvo`ÑìpëÒßðú¼ ëŸCv!„# 4áÚûÎëˆÞp¯m¡øêß|6,ýÁÅì9Ixñs0VPÙˆ(œ#+ÕuÍ|q‚eÂãù%-ÃÌql' ô=n�ïñäô!Ï‹#nRSÇ'¤nÀ}ù6£l§m?™?FÙ€ÚhNôQÛjÍ‚>wÆ·)tÉÃâ#Nä#ÀÖCRÌu#4Wcl”#ë„È 9­Opñœ,Ž(ÄêÄX”÷¨ô`Â2Ì~‚� °V�$Ž×’w#ÅO‹?Csùbêtã+/”4² ñ-5ü6w†/aŒa±X�¦i‹§Þöòø¬vCãyžo=¥”ììì �%/5Yb�æôôt«kpK¨ƒÇ€ùq|zzJƒ³Ë£¯Í2––ÛqÁý8?‡ËtÀ©ù´H9ª#¤püÁø”Ý~B>}B¶ø'Ä»wÐ' ïcòcòä?'éߺÒpà¢ð2ˆ³Ec¥:ì¬à·=Þ[4¯ŒŸKæaXº»-[¤J)„<}ö çê"¡îµH’¤å6lÂ^>Áõ�°_¾ø9Oů¸é^!"!"»[Ü] œýhEúK/b‘â¢9J Š‚XÔ}„‰X؃`ÀB-^ŠCr:ý}’ès¦‹wþœT-`ºKiR†¼¶û�ª�ùL¿Å ˜*BÅÍo §ÚâÆ�}¾5þ[(¥ÚΗï¬^ÆÑYÿý~¿%,v7oþ7® �Õ-*�6–~Ú8LÎf³kqÓ4m mžÑÜ´Ø#ÎtþgóþZ’Û—³’×z9Æèv£«”bf>Íc—!ök°t4²^/§s’ ê ¹U|Z¤h'ùθ Šʧʰ÷!ƒÆf·‹üè³ôïƽ¥ìöÂG—t%¥l“­mÉ[^>+MÓó¤­+&Uh*YžÍmŒáäääZƒÉ_¿?ÿED‰M!DÃ$÷¬Ìy>ãyñˆ{[5h?Fß�f2óÄJZâ ¨+0ªaß—eI�5-™’õíquþ‚8°�s�©ìud?±FÉ ÕjôQ–%OQ¹n"L�’%·(«’ç?cQըдÎ8>Fî��ÛcíDZŠÚª„OФ”<Ÿ½à³ùûLƒ§í±u’ˆ ’†SD¨duqH\Ÿ¼ÒôÜ.¹<�(Çá(«[”õMŒÍˆÂ'ô’{ઇKfÜwïp8ÞçÇó?»¢b 0EŒJη̆ìóÎ胰�€ü{¼0Aؽ^¯MŠ­µ­Ð»µöŒ4&š‘´i‚õaU´±dI³IºˆtvYìîî¶xöétºJ:òL¢ÆÙ•n 4cøððð7&¹ý�ñŒ�°Fë,F ô2ÉýÕ"¦þšæáaPó ÉÙÝ@:sÀó*faoŒL3O<ýg Æg*œ)‘É‹£yÿ'™—„ÿ¦|«^ÁéééÖÕÐîFåék‹yx=ÁœÍf×JR}RáÌë’Î#–Òó(™/*Ò¬GÅK}Ü3­rm%y¶˜^ëj A ¥¦®”•A,çbc;àl†Ñ YÖãÑÑc”¬õ,'ò9®JÒ—(aŒ%s»¼p�ÉMÎ ý„'î#¦Õ ¶ô’óbÁûÇ?¡ŒWçaá1UÐ$³²êSÇç;NXœ4è`NRíQ/+•š1/ÞDÉ9YüŒ"ÔCnBÅ(“2Nv9ÖO©¹ æÖ„Èðü&mWÜæÁà5„X%€y蔷ýÊ}Ô·ó}ëÝ±÷?ú%?zïç()Ù�V hiš†µ6x§ÅÓ““•ã“$i¿…‹ÂC„ YŠ¢8Ïb9_�9ç6]~ÅøÆ '›¢}¾« ~w<åƒüœΗ‰Ü*~±èño'c¾(“•ª°sŽ$n´)ÅäDƒ1¶š"T„’b!1ñ«íß Ñ˜(ø6Öu´-Ųâ�ÕaÇŒF£ µ70ŸÏÛ‰q0"—;§óöï²¥DQQ, NNNÚvíu®ßË$M&¢(ºTtýžýÎÞŸ$ÉÖ÷A›ÀEÁÑÑQ›0¥iÊÎÎN{€Ú(¸‡”’², DÑVŒ„€,Ôí‡-¥¤?ܧpŠZb¬@[‰s‚Ê(fe̬ŠÐV"€(0ÄrNµxÎlzÚ~ˆJJ²´Çhx‡ÁpÈ“é3â¸à M¤ÄDS„t¤ù (2†ÅLQŠ £µ¨x(~Á”#úý>“ù„Çó/0áœ�¹±t�i"eÐþÿ‘l&?¬L"£ó‰o@ÄÝäU¤T-nv=üüzzzJ†mu¿;|µÞËzu!bóùœ““¦Ó?ûùÜØßçõW_i (‹f}õy@Y›¥é‚d±ìºÎf³ÖÔ§Ñ¡¿\ïÞ´êºnqÓɄÅb±õ8N’äëMncié³V-Xç·­ÛQÎŒ'¼”•çlU¿LVñÁ¢Ï�çNuØ0î–•T§dÑ"H±‹§¨þMêécÊì?á"]? ¶MR}K½Áåç&–ëNlÓé´y¡’AãäÕ‹*Âe…Êk‡v“äë&™ëç÷ ˦ë÷-èFðh“ÓîÄÚ�Ý¿¢u¯ZþÇÇÇíî±×ë±»»ÛZJú„£*¦k›¤ÚȶêVæS&“S¢(&îí“›ŒîÞËfYA¨—É¥"eˆÅ”ùäYƒ#[^Ÿ)÷öîp�½BUgøÎoL™§!Y0I>A&2)ÐÉ ¢eFd3î…oàœã³ù¯˜EOXŽÉ¢¦Îœ™$ ä sfŒ@_ÂÊ0ª ¨k“’¸>�ËÉÔûÔu„ÍNТ$ $eòŒG̪‰ÈÈ�Í¿W«Ö°¡Š�ûÉëíÆâ¢–¯O¼þ`0h“Y�ïónIëz·@‹•üä³Ïy÷›ß`<QVßû·ÿއOž"D3*ÝÀ´bÐT¿Ž��[¼WÇìîî¶ Hûþ�kÛo>)ñ×ß—Ÿm»:Êëq™9Å_g„Ò¢�@›«‰ ÖZú²à[ý)o¤H#]'Œ|Q&ü»É˜}J+ÛJ¼4G¤Ã~㨤ÈdD5=¢J›Ëˆ{>I�FW>çî»ÔZ)„îùµ®YÌ'8gÏ‰Ç áHÚ^T…jeöÇw“äõo`›ów u.ºÿ4MÛµg�À¼Íñ>#'¸(BG3¬u¡Ã&Sr7¡65;ê�PÄç~ËY ­³|È{ŒÒÝ6 ½Š´Õ-8ŒF£vMöÆ$9?°$I1�xëõ×H“„?ù³ðþ‡¡�át:G‹¶çh»ÐÝãOOO[ Ÿdû¼¦{�þûêÎå¾cà;—™8øßøZa Æ ^TáRÍàª|µ)[ö"ÍNh(�$7_=ßö’5Ö­AÎáŽþéh€£‡"pšE>@'ßä¢äÖ?Øu6ì&É_JâLʨˮ͆�‹§gÔf•MJC*#I{Muu�Ü„w¯[ ÇM×ß�üº-åMÌt?±¯ßÿºðx×;}=<›w“܈O®”�žÛÕFÆýÇéÎ�½Ç‹üÚmÆ ¤8Ãã†Ê"]#YãZ²…‚0ŒHÂ}»G¡ó�6¼g¬*ÉÊ[ì&�cÜÓü)utÂIžs#¸ƒˆs¬¬ê>Zæ8U˜ ‰Äª-ðÛ�&Ñ;h‘Š„J•æU9£oc¬¬‘�CF5”ãd„p’£ú8‰�~ó¤–Éíù*Å¡¼Ïí¥Íît:½r‘.äÇo‹Å…²5BnߺÉññ)7÷ùó¿ü+Â0äþ�;üÕ�~ÂÓç/8Ø? °f# ÍË÷­·Çœs-CØ·ì.RXØ$�×m3ûoáÿðÿÆa ••<Ì#r«è†p 1ç\c­›Ö( S­¾’µ.€E05JXnC´®�Gÿ'ñ`€ n ÃWM™ëûØø¥KË�¡<_U§Xo±®ÏIm1 ûÚ¦EÛ�HY!sв@ª33g*"e¨Œ"íï´­yc¤õë÷Ê>Á½llýþ�ö»ñmÜ‹ˆ;ÛÖX*‘Ÿ3àñae�r!}sÈ^�g“#òð9�¨{ ûÍß`e!IˆÆa2*/ÅÆhY"”¥r5¹ÞÅ…–ÂÜFë]”š£\‰Åâ‚ZæDfH*SJ·`VKHEs[‡ÈP³>LBÞèýQÐèØoƒíî®é^Y kÇ{™6nGܹyƒ¼,˜ÍçL¦3вY—?øè#Œ±Œ†CziÔ ×ÏÝUÉéJ�ú1äÇÃEzé]¨ÅEãØÃ$¿vÌmí$O«˜guD,-é–In" ·“Š,pLµúÊ’5x­_0JCÊÓÈä/�aŠÓ52J°Ü`!߯];Éó»�Ž×[O|ýÄâmháb<¬ŽPY"epHÒ B‰ºù]Òï°NRV¤¤i³Øêò”8Ðí¤°éú»ÉÅ6:xë×ß]<Ò4%Š¢¸”´³>±ƒ¢â±¶W]KwbõŒvÿïËÚ‚3Hyv×µ�(¬Ø»ÏÁ?|;ÍO¬‘2DÊ4äÁ ÔÁ9AÚ¶î„m‹<‰Sö£[ôÍÚÖ\�ä ÐN3R%xX„S5c{ƒ<{„•‘R–à¢E#Ö]§èèã‡s¿ï¨dÁóü;ÊúÚŒ©êCv‚N(kCæF AÙ�A4â”çTZS—!R9t% bsîœ)oô~‹88³½Žt‘Ÿ˜Ò4m™Ë“ÉäÒo0PŠÑ°qËyö∲(Ù�yòì9·oÞäãO>ag<$Ï—Î5]MG¿IJ’¤] /Ó†ìntëºn�ócé7!¹…f£63Ïê¦4PæÜâxþÀYÆAÍa¢1æ_QáF Ç»ÃÆ8§>ú+²ä¤1lXÏsNOOQJ1�H’dk6»¿þ3rY#£6î ¢JclóìÍïtQ£®kŽ��ÑKŸê¯æ•Yê8ž—½×.nÇËäôˆÙäxåÚƒ à`tƒ;âMß$¥¿ñ>l˜sZM°ÂÑ œ\IJÅbJg/賋Daœ>Gèº,ªú!ap„uM¾ä�¶Â�³’\LÉ“çëc b$°¤2˜Za�ÚxÎ[Á+dQCvô6»_fùÅHk½¶~3ô�wÞæoýîwù싇h­ùÕgŸ…ÿߟ}ŸÙ|Ñn8.ú}2=™L¨ë¥T ×¹¬jÕ½vµðXÎápx­ûÿëŒÒJ>Ì{|²ÃÓ*ÞŠ8æœ#¡âíÞŒo ç ƒ« ÅÝhÁ°Sbý#T¶‡™=B¦CÌü)“Ó9io°Ò"½*6µX‡ÃáJEþ¢öj÷øé´i‘{<ïúùãŽxÜIh”tô£[3Ÿ�’¦=†Ãë>ŸÏI–snhqIhúQÕÊú±é¹Ûp֯ߟk6›-Õ|Gñ:ÇŸžž’$ ;;;M%úcØ·“ýwÇ1ƒñ6Ø!ׯ6ÏY)u¡Œ”ŸÓ���[ÅŸ0{…éõv|©iæÛè3Lñœéé‹•Ä< CúÁ˜›å[¼ü6ÃsçvX ÕH�ÝŠo!‘D¤�Ó(Š¸Ú§NŽ)Ųš©#’ :÷;…s‹âµ†Ü›¿‰µ Ì,B£eŽ’:=â4ùŒyòŒçù=5BVGÒ«©J…“çÛ>cn÷^Z>ƒ9GGGí\zQ»~St«¦Zë+¡W>¬5¼óÖ¼õê«<|ô¥yQpëÆ!?üéϘ/ònØ…¬>Ÿ&w8::j‹>Öm™7…dz٬…_îííµ0É_«C™¯<®bŒ“ ½¶VÒTbƒv’ܨk±y›jÁœARž¼G?üö°å *; ú>½¾§ÁYdÔ£˜””ÉoS”Õ¥-ê‹b½ã+6뺬ÛïÙâZk¤°¤ASíÎMŠç+ßJ:i¨L°²¹)ª_ª–Š�´HÑ8uÕS‹n7À·d·ùº×ï%˪ªj!7שÞùócÚnÊ6Ç{Ðbê�öÂ(¥ÔÍ7'½+%1×+òIÚ£¨ÀMÐ1à±N ‚!q¼,ŠÔ B ÃlQQkÓÚmðÑ/É‹‚Ï>yÊÛwÞE¹�­:F¢4.O¹5ºMYVÌÔóÆEÒ Œ¨pAE­-20è«F½f«äÕËKIÆfVrÁ8üÂ9)}*ÕÀ Œ¬(°“ì(ÉT>i~ ÂºRWÞ°aœî­X•¯w6ÁvÖc4á¡”eYn­rM¡`4q÷ÎöööÐuͧŸÁ7Þx�ãÓ 7ùÞþ-ÆØsÊ ›~Ëëû¹¸k­~ÕXô÷ê=~­eÝpK ݧUŒ�*ƒÚbŽ …áF\1 -3£¨¶”»/xy' ®r¢é¿ ßÄÌ7Ò_Å ÞÁ† Ó»›dv-·�Œ1+ºœ_ÕéË ®�#8!‰d3XH¯­"ë7Â÷óù¼Ù�)yiqØ&Éíü½^s ZO2¯3±vL¿cü2Ngëxàmއ(¥Ú²O®|«9Š"‚(ųê×ñ½EwÄI�²rXÛ@´è‘fMò6ŸM¡ÄÒ{]ÓÊæTuÍÏÞÿ€÷Þÿƒ~[¼¼û:ÎZJ—·«‘.�¸9ºE]hNÕ#°Ši•#ã éBS‰±ëQ©‹±Pç.äåèô>Òà˜¾¨HèQv4¥²¨ºÇíþmfBÎg%R(�]9'É+ñ;ŒÓÝ3ǘµç·î¾é=vE�‰ôã`=Á¸h�‡ .Z*Þ|õeNNNPJ‘Ä1÷îÜæ_}ïÏ`™°öz½ eôüÆÊ[Bw%�€Kåjº¿‘¦)Æ´ÖüÑýÑodrë#·Š§ULa£P·Ú¢—†s UÍí´F ˜›«ñ¸x#›ssR.ŽZ F»xŠÊÑóg̃ßÃã�-êëÊ0*¥Úª­¯€^§Õ¿~~„ ®É*íbUSµ}¨LHo0^&§8kÉúCŒ )*K(Ï’bÿOë.†�y¸Ô6›,sε�¸>·ã"ˆÀEÇûïÔÃ?|âêÛÄžE1QÜŽ·Ñ7]/üdY� ê±(Mi)µ�H{M•|:�R›¥s¥€ÊcÛçøÓŸÿ‚'Ïž¡”âó/žðí×¾ÃÈb�&g¸Æ}¬6 Õ.{ñÓú„"«xÇ�¡UÄ�>·=èÂ¥®‚ûtça¿±¾èømc"pÑñžHÖU…hŸÉr-ñß±oK‡Ã+¡GÝëpîLm'ë PÉ.‹:EEƒvÝkÁ¼ŠXT®ÓUh*˜Š—îßãéóçüíßÿªªæO¾÷T3¾ý;âI�òÅéçAÈKñÛì¹;$‘¢²š^u³•gì±C¥÷·~ž pv=Ï,ô€Ð¥-ȧQ žOdÈ�¸€«íyEÀƒä-” ÚÍĦØÛéÂnºPÊõwxQN°>—{ãkIÔpoÂ@ñûßýFkþü/þA ƒ€?þU[U]�$9“’óÕ㣣£•›���s*7Ýð‰°çöTUõ×S¹]�béM>5™2ÄÜÅÖCàèÉš;iE?ph'¨œ`y?]poP§$ù÷ˆ{Øü92ÛÇ,^°�ßÅuôÛß×W6èxòUlÞ ïO¬’¶Â(¥]b¸:ÏÏÄdËÝëbvJTÊÈ3F0¢2в6(é�¡¤#Zâç̢ùêçIo^”|�YÙ=^)ÕV®sÿ]d—tæÃ ùÄo"�y�ú6;¿õßi6/CÂ(m£+Oâ8?þú½ŒW^ºÏ�ñwþà÷ˆÂ�¿üá�ØßÝ¥ÈkêÅýñKT¶À%³©f7Ýe7; «v85GØô‡%•=J1'71–¥¶Ã6çÅ«ç&á^ø)#9CD5Z®VQB©�uÆN²Ã#ý1ÆX†"àµäÛôâÁ•ÍÝç·^Zoe^t¼ë„_©ªŒÅG–äË´”œ#KSÞÿð—eÉ·ßy›—ïßk…ÃW䃖ŒZ_µ€fA¨ëºÃ~ïV/ºÑ…Wx¬ýoЉÃ6a—µã:Üšü  °ì†57“%S­Î}oöfìbŠùszúýƒ¦jÛ»�žþœsܺqˆ‚—ïÝe<ñøÙ3Ž�O8<8à½÷~ÉNpƒ»ƒ—˜Øç”ä¸ûâ!ÿåñ÷ÚùÒç>üº¾n@Ñ-bù–ßluóIêvtá×àPv�XXÅ‹: H¥!زEÖW5·“ŠýØ`,tƒÉ�…á[ãªñv>ú>Y¯DD;աɧPg¿{éÏw“Ì®äʦ™oŸ­·Ö'–ë8u5‹»]&hÍùË¥«‹’ã$*EUY ̬iËдCåÀUäE‰ "’´OQ ´¶KæérbU'V�Cõh÷úý@\·Åí¯µ¾tb¾êþ7=?Oé&??Ó~ØBˆ–‰¹â6µlÂ�ƒn¢ù͹F;ôþ�Ûh­ùøÓϼ8>æñÓ§!Øéï±Þ&“Nä#óšXDì w±Z1±ÏA%çôÜ.³zLm÷‰£Ç8Péu1·ÒÔzZlŸe?ú1©H(¢ç¸µïÌ ºô£�ô/Á+ÉힸýÁ«xlÖ¶›?޼.¦W©køã×�ÎÂ0D*EYi‚@HÁt:]Û|…<¸{‡4‰ÙßÝ%Ž£ÖéΫ\øÖ/l^ Ö�÷NgÛ&ù�ßWü¼lÞU¿ãa"EQlt:»*ºj:^Â=®‚ 9爣�0lºŸ¼÷>Ö6ïo8èóÙÃGHòÊè-©8­O8.Ÿ“ÈŒƒÑ!ea(˜‰„JΉõ/ê»j‚Tey¯¡žÉÍ A­÷1öŒàÕ ?%�ž“»ÙF„È Ù�÷(ÜœcûkTkÞózï;$a³‘ëjÚ_뛄ÑhÔv4/*2¬ß-8 ƒ¦0PkŒ=+2¬¿Ï( Ù�ÙÛÝ! „+Rž^mû›^]ñ‡u•?w7pÝ‚Ûßhr ÑáD‡<­cBiémY=p®±ó½kbMí$7£‚[Ã�bqLVþ)áð&?BF ÷(ƒwÑ6¸V’ÕÕEìî~»U›Me}8�]Ù6±Bà,ΔTUIœ4»ÿ¼c½~SýÊ'ÄêüáIgΔ i.Ɉ’ÅGH·ü›Í«�u L¿û^¯`nzvÝûߦz"„8÷þü~a[O$6ï#I’ É®Üȶ+œá’¼JöÒ=þýG''Ü»}›²ªyöâï}ð!ý^�ãç3Þ¾óMfÅœÏù9ªŒ¸5¼�­rq‚•šÄŒX؄ڎ ƒS&óï²(^! N.¬ä*µ ¨îâ–ŠQpÄHS‡³s„ˆ@8Œîª€ÇúW-Q�Ñ6¼‘}‡$JÛ…êºU0ÿ÷¾…_ÅÖ„hÆ‘'icÑÚ’%!ó ZŠþÙ÷²)ÅJ%¿;1úÖæeÚ�]É£uMF_IèV~á?ÎäÖGå$Ïê˜Ò*i.…øpÎ!\cÞs7«é–øbœ†”§¿$S¢’fþ°™‡Ã7p½ïP׿Z¤±n’ê%«üÿîâ7ÿ¥õe¥B¸†¬äôc!í p2¦6ª…”‹¢ ó0€’4¤³²@GÖàDLQ9$)Ïæk)ÜRö|’Û½¯Kz™.óeÇ_;É_ï7…BlOÚó°„ù|Þ’­=\ÌC..*’|¨áztÌ?z>ãÁ;ã!¡ uÚºnáèp�[7(ÊŠ÷?ú%ƒ~�²ªøÅGóæý79È8.NyÂ/ˆë>·Gw©‚ãà¢z‡S»‹’%‹âuæÅkÔz�8zŒ›6mFËB€c˜|ŠÒ-ªs„ÝFw÷€½t�Sû‚©9nˆ½K(æMõ27{ \Ak½µ-s÷™úM‚—Ò¼¬#²éx/»†aC´S‚@Š !YÍ#jçR_0ð_‰¿ªhâœkó‰îñþŸBˆsÝ¿ñäÖ‡góë�Pº†Í»ÅÚé“Ü›qÅ^¶¬Bý¿dã Wç©ÀÖ,æ Ñè­k ˆ‹v¯~’¸ª’u¾Å}Ö¸tbœ¥7’^uUPÕ†4ë“õšÉ%Ïs7¿T}BÊfҬʲ=^)‹²±õÊ ^×Õ^2±cÚj—¯|™…颉UkÍ?ýÇÿˆÿõùŸ™ÏfÌfSXâm¬mœÐ¦Ó³6N¾X —Ä‘ºªø«¿ü‹ÖªØ,Ïå'äM¤³m<®×á=È'ûÛ¶úÆÃÁR¸äþ�ÛDQH^<þ‚wïq0Þ'Xô˜†O1…ãÎøe¡™‹Ô¢Dé>ÓúEum†€¢Öû¡�%×p´¦G^¾‚¯Ü†Á1Iúe „%nMâ+ªw¸×¿ÏÂÌxf>Çš3]Åò%n÷pttô¥Í@úý~#³ìˆ\‡¼ ,7WZ”’(Ù,°Ûï«ι–I ´jWÅúñ>±õ-áî÷ÿ7™Ün·Õ¸<¼ñ“ª±¯ÜF–›9ké+Í(‹0¦FMþÉpˆ+§È¨‡«L&ÄwÚ$éº&¾…ê+—ža½M‹¶Û‘ºZ!§©Ü gÉš(°H*ª²À¢ e†¢(ÀÌWª¶çÏ �r(QSÆ)zý†�²ÔKØXSíõI®q’îí&éþÙUUµ•.éúñ.t]Ò^kë%ô®"ìúïÄϳ¾`Ñ…ýDQ„^ƒ ý¼vü7?ù„O‹ŠæÿϳS~>Ëù{£Œº®¶&°vCІxØëe¼òà>¿úìsܽKYU<~ú”·^�zæHê‹àeCúÉ�y5¥9Új¬2+^Á˜>Mç3¦Ö{Tõ¡:EÊUÈEYÝB›�å3t$ÉGDA…+STdVæbin„÷ÉâŒGåÇÌê*lò €ˆ7{ß% "òH �¨©ªcES=� ^Æ»¿ø‰U-'Öu6o×ÅÄãY¿ w}b•RòÍw¾ÅO~üCþÛÿî¿çùÓ§Ìçsþ¯öOøñ�Èx¼Ãÿïÿ?þÑÙÛÛçŸÿßÿŒÿð‡ÑDw�ºÛ9¿ó[¾ßï¯ÑCý]¨ì4rË©Å|l£ ðôlW2ŽBÓŠ2ãÊ;TÍkhv•<î¡Ùu•6Qå1²¬€û¼_FlT*t]¿«-¬¯Hmd ì°yÑ¡P‰]“d¹†íxk$®L#ÞD>ðŽw3pr5ô#4ÝÄ­Ô‰R�8ÉÊù…-.Gßqí—*¬x’$e3‘»m±¦ýºðìÍ9rãîšLÞð~W¤[Ã0¨×ë¥óª~î†ßÇ6 !Ê ³”’—ç™®V¸ÙóYêù¸ƒ,n,%ß»|�ó+Ž�4À?½4Ï;~ŸŸŸhñ…½dÀwVx´QaÚÐÖ%Žî6àÖDÑxgÏä$³·o36:‚m;ŒŽ´øÎÓ? ×ísxò(¶nã97WgÉÌ©§è‰A˜� Y#zå¹K–{Tœ‹$i]ï“ç6I:B?:L.­Á»Ï¨ØWÐ;A “k:^4ÁLu†$�¸ž½‹LM4#C Øgœ`¼²g]¶^JY–ò·a'ŸfV¨àƒ»•bT™{IAL³L�<+ºBÞ­O”�©ª¬Þ�J� T7’Ô†¯¿³RîcƉ8àì±›«�‹ÿ[èJ`n€›´Šß¿éLJ9ˆÞbŒàEÌÖ(yÔA³jäq�>‡�z ÁZ¿zÛ¶i4eG±Ý µ¡Ô5–0‚];ÉÊQò<�V«…ïûë£_¡acèY!6� I2 ×+Ê`ª·v¥6BEQ×Lv4¬¶‘“K¿b˜•Ú(QâGþ éÃZƒ(Ó‰³‚!¬iZÉ\ïv»¥ž\³Ù¤ßïï¨a8üü*Û ž?‚Òq~·~àÇõ0tÛ.œ z£Á�ôÚËËäyNžçüÒ/ÿ*Ï?÷c^ínߺ…í8~€nE†áÚ%ü/ý5ñù·®‡ûÑORûÏÿ ìÑñ²ÇuÇëZnḻñ9”£¦0HŠÌ²Ý:P0wl”‘f“F} )sÂ0)YZn“¾“ñ±GÎЊFY +¤fÃñ©çg‰å}¤ù�n©Ó îCJËXÆ��ç.â®…¸Ñ¾s“TDèq¡§›è}œxœÑÆ~ìÓÎæÈSÃ,ˆ.ú~*võVôðw^[•øUùV½GU[]]-÷ávDµJ¥R´î”s é©`1êúÃëh«aY–e•P¼Ôj5\×¥×ëí¬Ëâw»Ý]åÿC½G$w¦m“ ¾G;ùÀæ�~®ónPe1±8èÔ�­ß� X¢0ÀŽ^B›$ ÐÝQÒ`‘Ð:ƒ’§¢P˜QNªëº».ueÖF9fÍfß÷ïJß¶Ûí–�S–eø¾¿v°ì°m¤ëYø©Ží­U°²¤�åT1œúA‘÷qÌí1˺Vt>K³Uün€åÔ0œQ¾i€cδk&ØR§Q¦#„(í°zÖ0 KØÖnŸ_½ƒ ÖÍï_~û=~÷Á{ùþ•ëÌÔkùu¾ð÷~ Ã0h˜#´sŸTë£;³Là1—aXÁFJ�Žÿ iÖÀ4–H³é »[¾srD.p _oês8ý 2=@‹«ìqg@ÀMÿ&hb“8Ô™r÷kÊÃϯTf”- ÃpS}÷�ªj(•e U°¸ÑÉ5M˲ÂB ÝÔtzÝn©W¾Óõ‡‡R>PkÐuݲӘã8¥=ßn(8~�a¥OEÑËÜššäÞj€È3ª7yŒ&Äßioò¥ÔBài›g”#’ø³xâmt»AwÐìáÊQåÓƒH¼ÃÑëÝ*¨¥ßïÓétÖ‘ÎîæpÛ´¬2pŒ\#ÆÒó²Ô%�” Þ@g5 V±D8 ëX¸•:aª ¢ovtr-=GȘ0ŒÐu·R#NEQòÕŠòIÑ¢ Nx•zÙfW90Ã-ßin#žW}Þyû-úÐÃ,ÌÏ KK‹´FFéõº\¿v•äc´——9xè0®ëròÔ=ÌÏͱoÿVVÚ9vœÓr‡†;›ì”}“rMÓ÷ý’µÿ~HSrŠý›ÁÀÚ�–‚Lx¥ŽÃU<3&M"¸€}–G�˜±fYamó,¤e8îš®«”Eó�"Û›ƒÛ­•øÿaça˜Ûñ~IcI’k:WW:<:5Îñ‘&‹ý�0ËxöÆ-ê�Sãc¼tí&Ÿ;r€¿y÷ï-¯0Vqùî…ËÌ÷|~õÞ|õÝK¤¹d)èóÎâ2·zûëUê–Éï^dÎxevŽ££-:QÌ­žOÅ4¸¾²J’eüÌ¡ýX¦É3KnE1ž®s¸âòñÑ:OÌ.ñßìeÒ¸s=+[¬ZÜo¥ö³q¨$U‘}ιxå*QsäÐA8€¦iŒVF©0ŠŒ4B}S qh“ã�aQl¢l­›furi³ÑK4LÑ¡fdDZ�QÍŒ4°u“éÊ>�°’Î`茔ýæ F¼‰;SÇpC¢Í2úªÉÉ0ñrãûÛî,«Õj¤9$YŽ©ëdÙúЉ²å[]_�a�|eˇ5’×ÁU6Q©Q÷«ž§Ýn#„(•Tø�Bû'Æ$-é)R2ã€í3iE\�\fC‹üv¥ 2�wý*×u—^�Æ�4†‚ÄqLµj£'{ÈÃtw”,X$´>bóÇB”§J¥B«Õ¢×ëÀ›Ôú‡µÔÔGUó=Ï+³[Émum1©�Æ)qÜ!3§:îÀ�û~)9ãj)IºJ/îc;U gŒ è!òÇÜž˜§kWKó UÝñüB†¸f�ýÒô5œÕ°s¢þô}€cª”Ù¯Ýv:SÙ•ø­ßþÒ4åÞûOÓh4øèÇ>^èÎ:Ä0 ÇeÿÁCÎè«}X©TˆÓŒ,Ïñl³„m¼~¯×[w}¥æ¡®©˜ÖꛪêpÖA9W–e†á{•¹Pû_©xÔëuT�ûŸöÀa/$Ë6VR2¢‡ŒÖc–›ó¾KÿVÔr«™ÁٞŔ SV„¥­9Q¶mÓ÷ÛÔ¬Mò$D³$ÝyBûgèf’šQï¶W9Þ,ˆ±óaD.%“yŽešÔëu’$¡ßïoê¤u²œ1]§Ÿ$,ûž^”Iw“}ÚôŠ5�e×uñju²¸O’Š¢„=€ûÄÙ;<€öXFŽEŸÈ�È5—JµXA¿‹¥Åë:lm6 Mbh} BjN9¿v1E1_jæPkáõd^u–(¢Õûyþ<ÏÑò˜v’˜_ûŽ®‘æ9�©é8�’†m!˜º†g�ZÃ*QbÍf³¨NÁ:»¯2¿ – Ôäógn÷Ú!ž¶y–Ê9cfÌ´›�I�^º{ú­F"5nÇ~®ãê9–È©T Ü]¼üìô%4ÓCØS@J?0‰í‡Öe 6ŽáèU¦›á°†³Ã%È�óßO§3)e!Ý¥iÔkU’Ü$J çR™^ÃqŠß™†íuÆR×X“œ‰S\¯VHÖ IÎìääZzJ÷‰’µìC?ÊH3ðª­A†¤‡­õ‘RÜ!Y£HcŠt¶û$ÄZ�é�›s8{¢„à ìPBµZÃ8BLËB× gEà'Õû®V«ÔE'”äÖMü¿þs¬ý©}ö?CóCžçw蛪±É™á¡~Ïn[ªw`÷œ<^â¦ÔßkB£jW©[#øý˜X_E8Ë8ÚU}‹U\’ë}„`€ïZ»–&rÆŒó¤Îíuä+i°×Û‡gVi'óÌË«yäî*M}„½µ#˜Cmvw»Ž‡×�jK»“}ã|X¿ ]E�0NÑt ÛзUËØŒ¼©ÞåfZŠÃCe`­…¨‚¥ ˇ Ë/)x…’'²m›?üÃ?ü©fnÐ4cª[°õ‘WK™°,�]UÔª$z�ëé –™2aÙ„é`Nîóæì%šõI®õú4“¯¼{�ƒãcäRÒn¿� ÍB\¥^c%q¹ÑÁ­cÖïS·-fƒ>_yû"ß7Í¥Õ.?¸v“�BÓp �ÙÕ.M×!+Q„§Ùo¡ñ¿=û2‡G[èyÆR¿Ø³«qBeSx¿¤)€$ÍHÓǶ±�*aI’ah�iUœA7º$\¹ÃiU܆0ŒÈ¤V8©˜„QQqÜØ@gã(¹QHšëx•¢ÓYåTªÍu­¿5±µV¹ ªïöù5!ØS«òÒõ[œáôÌ4·z>§&ǹgb ;Ïh¹MÛ¢îØ<:=Åå•3µ ÇGGxcn�Û=ŸŸ?z€ã#M´yso¥ '6<—QÛÂ1 F]×0h86‡› fj·Œ:…,TSHmòÔÂ*·¢˜{k|l}—ØbؼÅýN¶8ÏsFZMZÍæ:IPe‹kN�ªÑ¢ï§„VÍìb˜·¨m q“¦ƒµ@.­AövmTô[TÌÛ$æ�Â…Ô˜ä0•I@r%9K–ƒÌ%†�qÔ¹Ÿš[�ª©Án3òÊ+'W€»«bq’‘¤9®c’ `i;]?M×w:3 cSÙ®á¹jo¦R3¬wkDRe‹•½mÛ̹ÍL;ɶÑD¡…S7%Q®þ”ÈüÌ`!¶˜ö2*ŽEÔ_Åîÿ«>†Ì@HŸ\ÖÑ[Ÿ%ËÅ®3‰°^2eØIsç¡ðíæï–,£†kfèDDaˆÐM¼J�8Ó ¨V%¸^Gß|Aé˜Z¡ �f IÎää¶lÞ�ó“Á|ÓòÊÅ’†«X&o’ݨ ;ʰ*ÒY·³Ê Ï=ËìÍ›ì?p t�&Viª ŠeY¼øüóLNMÑívxú©ïG“S{J–®úÏ0 gWað¾ÿÔ÷8~üDñ÷½.ݯ…äæuœc'Xù›/"£aš„gßÀŸÄúÔgîX¿Šd¤ È1»íq­Þì¾íð<Ѫ^í*ZB`ècî8VV§õH ŸÌôÑì6-7£Û8IOs–Iœ* Mã–5Gf¬e_u ¼dŒ™Ú^�p59Gšg©¡’ÃöýÔ¼ZÉŠVße·�-PÒ_Џp7†Y©“ÄIN–K\Û$ û;fG…XZD÷‘èTÂï¦in«¥¨†*� !ÊL­Êz(…��ÍKâ8.mÅýÑýÔa u3gÔÊ�ÛËëä4�„½^Œ&ŠjX¶ Ø3%ßY½Ê¿|ï2çº=¾67Ïí¤ÇG[MâLCȈùî2¯Ý¼Œ&/ÜœãòÒ"GÇZüëgŸç@=æ¯ÎǼÛø©àÉwW¹�ŒÐM2^¿=Ïës‹!xgq‰Çî%És^¸q›33SœåOž…÷–WpMƒ§Þ»ÂS—®ñ‰c‡±Û¥®ÏÞV“—®ÝdÒsyyvŽo_¼ÂTµÂ„·Ö›~#ij·°3ÏLð̘<íE Ž[Å´=‚ª…„íìp‘ÑÌщé÷C4Ý«ÔIJؘÜ6fh�˜ ¢µZ£¬:²cEUn3;¬ÆV¤±�ž¿n™iÖhY&zžqæÀ>ŽŽ�àH㈦]$FKiÖul:q»Km~îÈV=4)Ù[q¹ofÞ@a¨i÷ë:Ž®q ^å@½JÅÐiÙ£Ž]¶ÊBpߨã–Î%?ä/Ïg²Z¹ë†<@™­T2j;Ùb¥ì¢2 ª‚ l±eXŒW'1“i’ë=2#À²cªT�s#˜�œ†¥#l�”ˆš÷: :$îºkÙI“)g/Ëc>¾Á’œE +äN—I2Ý8PV¶† ´w#£7¬¬œÀ»±ÅÚ :Æ¡×6õ]9ɺU+$ŽÒLPp„[&†ÇpÂA9¹Ã•UÕ‰lø~‹ÎtÅyý�` ûÝ]×Ên¾•$–”Œê}FkíÔærß¡“è»èñ±ù0EÎa×§f ø¼óvÅAæèdîã·o“d>•Jõ®ËåIg*z�Í[þÖ? IDAT�n6_¥×wp/œ—¢Ï»¥gdù*~×DzkxÍÑR™Aä}vj˜ìšRö{}Ð+Tk­"­wqŒlÇì�šïwS­¢“[ñm!—‚(Ý~É —5Ç¡Õjñò‹/ðð£&Ï3æyå'/sàÀA.]º8Ȇ™Îן|‚£ÇOðî;oßGÓuî?ýo�;Ëèè×®^å¯þâÏùäc�3R©bíÝO|åéò"Y{™øúUœ'0ï9}ÇS¦Š+•JI:‹¢¨$�ì4†a'ê€ÙªT¨2�ʈ©ß¯ /jS†ÁTc’V¥ÅíÎ-Úé-R#bî�¼wA’¦ÃXäàá�ý5L÷eœÄ$5ׯÙ<ÕuÆ–ÒÛtÑ’*Ò g�J³|ªM«ÕÚ–46<Ô{Êç݉t¶q¸®‹DgyѰA×Hv’¡hPbdÌrÙ¡×Mh4 �ÉÝõ<ÏË–¼*ÈUŽúVûØ÷ý2Ðúi[ËÙëÄhš@hFI²ÜÌK)ÑdÊ!ÇgÊŽ9ï{,ÇÆ:;|;]å+·ø&ù‡Göðµ[KüïïÍòÌê�x3(ôÃh¥Â£3{xùêyF'GÙ_¯qÿL×5úqÌɱ:÷��qpd’goΡé:IžòøÁ½üdv®¼§ß;s?O_ºÆåö*ý4垉" œII˱Yíùìiâº./œ¿BŽä½Åežž@JÉþF�Óã#›â‡ +;•¨…(°“‰©ƒ©G„Á2¹p ™ÁA4KC°¶ÿ.š�xVJš­Òë®ÁÆú}Ò>Ž‘³±EöÆù+¥ûHgM%Eè6a’“KmÇósc‰X•¨ï†´§iZ©vÐh4¶œ?SqùÂ=ÇÖ9�ЧÁºõN(H¤išòqK#«¯´ÉÆÆÖéK+ïNc¶¡`+JŸ~£=PíFHZ†DQTÊÏÙ¶ÍLsšÑd”ÙîËéM´x’—TÅ ‡K}ŽžlÐ:ØÁ5ÚäÃP 53¯R³ Bã\~‘YäzŒ%]¦¼åÙ¯üÇqîÊ–C)Ûívé“ì†ô¥Þ�çyDqŠD★“ìyÞŽ°gÐbÚ6$i†k¼ %‰º›1L|T¼õ÷›Ýoâýfn[fÊÑJˆE´©iB ÙfÓ%²™�1ö3mÓìÁ¶×5bδ|&+¦iô–©$?ĬM‘óh¶KÚ¹Ià|)¬$^­"ÏóʹÛuÇÚ8¸¬¡>ê¦‚ÕºŽ«¯E ªÁBgXŽWF`q’“ç…\׎ѿ^´‹ úºaáz…äL’f¥¾íVCšU_×¼ÀõꄉFœì^²GeÁæænÑh4Y^^fîöm¤ÌYZZ¢R©’¥Q266Ε+—é÷û¬´—±›™™éBÍ¡çsâÔ=ܼq�CGóö¹³LOÏÇ1÷Ÿ~€‹ï]àСÃìÝ·�öò2'NžB »ZE¯Tˆ~òz­N2{ƒ¸‹<Œ¨þ·ÿ˜Ü\;¥TT¨Œ‰ú~j3n,�ìf —Y†K…Ãëp¸•¦*= eh•>¤mZ4Ý&5s„ö�çÞÊH’bNžÃJ;fªe’Ù&µÄ %„.uDbcËÓÕ½hB0_%–†t0ãõh_qx„1«ÓÔ簖b¯×[G>ÜMÇ:! ²@˜¤…Ʊm–ÑNY8×LÊN"Эzyw¥é¨*ªB ¥,ÉmˆÚ£?m)°i'fÊIJÁ(%s$`Ûƒß +´Áí”(× xÜ×úó,ÄŸœhðÂrS„™d6Œøp})3òz:¦žáë1‰Y.È�VQš�"ú}×+ö~ÐÃÒ¢Õî.Fš âÜ*$k ƒ~à£IÛØ|~�ØÔ›ctV—0EL’›å|uÿ;‘æÊgISÞ~ûõzƒ‰ÉIÞ}çmÆÇ'ð{=4McrrŒ÷Þ»ÄäÔ$íå4M£Ùjqxÿ8I’°¼0:>Á�³9zŒ—_|�j­ÆÆÇɲŒ«W.32:J–eŒŽŽ�ÛvФ$úÊ_ãÿYÞ�VoÐø'ÿ+òÈñòï„´Z-„¬®®Þa0…%žZIQÞíP *úVíUl§¡ “®ë|ïGKd9LOXxƒ«7úøAF®e´Žt°Â>7¯ö°*9c“þ¼Á�½ãžÙC?Lðý>©ÐÏVyõù6§OæÒ•&ÆŠµ¶wºA&ÔkA?Æ0 FZu–Û=¤L1�;÷@³ÙÄ0Œ²ñ…Š™¬Êk›5Q¬iÙQ‚©k˜º`ee¥„Џ®[¶¹µu-§bÅ¥A ‡z³¨>tV1µ„D�°«jÎðºP¥¸J¥²©úÀøøøÝEëƒñ.ȉ»óOn¸?`ÒI8ä…ÔŒõ$R)%Yž—* [ MÓh§ïú./óü»7øN쥛¦´L“?»2Ç×ãï�#ÍÁÐ` ¹¯U(MdiŒ¾ò-܆K."ìQd&¹ºÚä|8͇÷L”ïY­µ�†åó”µÚ_ê»Î}<ËbߨhI^tØW57<õNT0¹NþK30´ ׈ÖU·†íp…ôýt³ÈBFQD÷°ôtGØ—a¢‘ ×+‚¼¨ßÃÔâ�®íú$õæx±öV—qõ€8-\où²]>¿Ò�ÞXI²�ÇH‰3Œz¹ÎÓþbyQ*ÈE׫lKZ“R222B»ÝÞòýo5¿Ñ(Ú÷z½-»Î)']­u&ÝíP •íS6ÅÀßé]*'S×uþ¯wƒ�<Ô Q7˜·˜�‹xúÙ6'îX#70¤Áù7W9|ªJÔÙs™žãð�q.^žÇ´4¼fF˜ôy㥟øØ žúÁ9F.ÇŽLÐ^)ìM«é±°Ôò öÍŒqéÊ�šEµ²¾ûªmÛ%¹peeåŽ{×4­¼÷Í|åûôã”4Í©º¾ß+mº `Ôû&P»f‚5�1M3ÖH¹gÒp� L-\¯Rú;ùTÃÏÓn·×ícE8žÿ¾3·©,Ä&·#«(¡kQ¬]Ó6*aÜ1lŸ“H ?[ß<`³ÑË lÓtuâÞõAÆ&y¸ŒîŒw �O€¶̽k§¨a£ôO·Û]'9óA:�•¤³KЍY�8³¨ÖšӼׯ3 ìJ’A¥RÍ!Œ²]5qP�Êò4$³bA $g�kåPèéZn«()õûèÒ´éÍÉÒ>aTÌW}Þws¨hšÆÄäõF×u9vì8##£4šMÆÇZŒÔu웤ZçØ‰“ìÛ¿ÏÕ°õÂyªW-L-¡VõHrÁ¡ÃGÅõ<´Aó†ñ‰ ªÕ“SSƒ–~ÅI!0OÞ‡&É;çÐê &ÿàŸcŸºŸd({ª0ÒÛ‘Ÿ6ëq=œÅØíÎdÖëõO×éÜÉ>Ýl î–r’´xZS4ë&]?£OuÏEroúµ¦�ëš,\ÍéuSlÛä?~—Å¥{Ç'˜níáò¥eŽœd~¡ƒmëä9Üš[åÖíÁÊjȳ/^¢^³¹|m‘‹——8vty¶ÖJÕ¶írml¦Ês<«€öÄ™†å6{7À (Öp~ﺚ£Ö…ÂëWcêõzù=ÕÜÿMz©Î­ÐÂÏ ¢­­­á <ûö5)%®–±ÇN˜t,~Øîp©q©ñV' ›füúä^l¤Ç€XêäID¤{ÏZD³*äýy4§JØžEÔ>ÉT¥R²Û•­P¤'ÅMþê¾U)\J‰gèŒ6Š}A©¬!ÄNýn;�ÝA:K3d^´¿v4£Ì¦Rm�¡^ÏL@Æ„aŒÐŠ& qZT´vªˆ�êT–Gá:ÒY'ëæ§¹Àpš˜¦YÜkÖE×$ººHñƒˆ\®ç6ìf ?¿ræ4M#Mb\3EçÖZ‹w¿ƒ¥¯Œ†º(ì`.õ-³w Ò°Q1fË÷?˜?Üðe;Ù«áì©’ŽRëènœ}Õ CqjT»[¸ƒÊ„J)¹r=Á¶4nÏGÌ/ßãöBÄôáÒÚu¤ÌÛ´f@ï×hxMÞ½p )%só®\]âÞc‡¨9 ^yýHI'˜¦A­êÐ^ ¸rm™½Ó-V;!ó  ]Ðï'\¹Þæäñéu¶t;íN}( zBÓ‰’ ÓÐÑÄzÍw•õÞ(Å(ó×\ëlåv¹žüÞj©ö”d:Q´VQÙ®"0¼V”�ZGê^7výÀì® Óx«ëñòJ•8_Cº¦a:ú6µ)%"O9Qéñ¡F�1+ÙÖ½-ºgIò,Å^Ƭ�‘‡m„U#OB¹©×¶œ¯0J6Ee™6[Ì›I)Æ¡Â)¬än7ƒšßëõ°]�ŠWl(9”]I2 Ë)C†˜Z‘Ú†ÄÑú½¥âp¯6‘f‹ ±Hó�7³¡K*fŸ8X"ìûx•º3B?µIóB€,n)w”%ÝÂáï^œEÓ-²Ž4®_½Z¶ÐÛêù… ”®¢(«t;ËäYÊÙ7_'N%ýÄ Lt,Ûavö&ý ÀwkËȹréaÐ%‰#�Æ@jª8•4ÓåKɇJJ¹X¿üë˜ÇOáþ½/Lí%N’r¾®ëC2;Ûcª•|Y†eŸëf³Yn¼Ý µq7B¶Z‡[�(ŠØ»nÞ yùÍß}f™oýí"7fCöîÍHµ�LÄH-Aסe�p`ßc5æVHÓ”zÝARÀ.r ËíG�P­ØäR{Å.ÚÝö#>ñÑ£¾St½ÐFVïQ °=6] +++„aH½^d‰Tu#Ér2)±-�,-ÚOoœ¿qÖj5¢Ì"Éî\ÇyÒ+Ç,×Êu¸²²B2´6E"Û¸.„(°Ñív»¨24›X–uWßîïzdRp+´xe¥r‡[ :†®mÈk2å�‘òÏŽNq-ym¥Ç¹ŽÏ¯LN3¡5È%ØzÊ3^âGï½Ì˜+HÓ˜óí9þÏ·«ü«çzÄÞIÂ0÷A“È÷�R��‹ÿ$YÆêê*³«þàG/¡Y«qÂ?ûÑ‹äÁn ˜–E*ñ �vR<a)M£Ñl"u� ˆ¥d'/S•¹Ûí6i�Zu°^ÖÔ`ÒL`;kíΕ64‰gÆèù*~wÝ0©ÔLjd…(Õ6Å;�¢d›áˆ.ÝÕ¥Âɯ�S'ˆ r)Èp°í‚ˆ•FÝ;2»’Â!l·ÛäyN«Õ*‰°»B²$ ÛY.7Z#ÄT bË©•Ž©NxÇy\4˜H±E¿³°îú*£¹�âðû—R–ówc?ÔPŽ¥R/²,‹f³YžÉ»}ÊQRÿïºî¦Ê>Û�~¿Ïý§,ξÛãüå€×ßîòÌK+ŒŽè4) �"Cɨ6Y@Fª†F¯ ippÿšVtÓmU9ubŠ�>RHCAÌÊJÇ1‘ÀÍ[+ bûÈf8¬ÍÄÃK!ß'*So}Þmô\`h3„Ì‹®,¢�Œ1´œDºx•¢,«Øúðµ‡Ø¸a„ÐŒ‚�›é„QŽil/ô#Ä�dÍpö@𸡤R­ž¹‡) y)„Æ3ϼÈÍ[ó´Z#ÌÞ¼ÉÍ›7˜ŸŸ+º¼…!×®\!|æç™½yƒz£ÁâÂy–±º²Â­Ù›tVW1 �·Î¾‰m[|û[ßä¾Ó§yõÕטœÚ‡eÙÜš½ÉåK¹tå:ûöî%—ðò«ïÒ bÆFüøÙ—i·ÛÔ«çÏŸgqq‘‹.pkö&×®\aqi‘‰‰Éxà‡!Òï‘?ø0  {¤°WŠ}¹“aT�‘T)DaÀl»¨ì¦à išÒëõdù;lð<ƒë³ R‚® >õ‘Çö·0�8‹Èƒ–ÓdOcIš£kÐlºøAB’fLŒzT«�nÈé{÷Q©Øý¤¤½E)¦©3{k•4Ë1 �…Å.y.™¯’ÄQùßO[j%Þ]¯×lèœ<×6èu;TÌcÐPDÊ;³@Q‚j­Ðý £¯ÒH'õ0)ˆ¹D™�Êêg^6Vs€2«¾‘•«æÂ�rC?m)°�£Ð$•8ºÄØ@XÚmEmÚÔ9UóøöR‡ÿzß4òö”‰ KË8wû cŽÎ'L‘µ_â'Ë}ƪ›©ÑNþêmÉsó’­߸x•fçØS¯ñµ —9»°šÆÓWoðÞò*÷Œ�Љb¾xî<LOq¹ãsn~K7¨Ú6߸x•f¥Êß¼u�·ææI¤äé«7¹ÚéqyµËþVƒ/¿u�ë+^›[äï]a¾2ï÷9ØØ:ɱöN4²<'ŽúÅ~t+D‰$M3RÖÛakƒÜšÂÓ¦ID8ÀÓV…0‘7bF!Š€�,"Œb ÓÁq«„‰†íTʪƒ.ýuп ±ÈäZ§7UQºÛvÆžYÀò,"ŒlÇÃv*¥“úmìm:Ò©³(Ïbzýt]CEâÞ� PÙ?•ÅWYÛÝÌ5 £¬¶)ȃÂã*lÿNCe°¡PªQU¥»m'[¯hØŽÁíù¢ê´w�Ã/<>NËma&ÅwÑuƒƒ£qmËÔÈ󌣇&XZöÉóŒ‰ñBJS˜žA‚,ËiÔ]–WLCñ ’$ÃbF[––}j›‘fATÙo`Wªê]n†GŽ“ ËÔ!ÏÉâ®™¢k›Ûb)%qTpT”Z“Ÿ™œ�].ö×BÉÍÌaÔ²õªK+ JêKUÖU•l+•†Ílù¶Î­&ì„ ± L¬ åÌÍ/à96–¡•r~n®Tȳ ×s·ŽŠ¤Ä”Èl‚•E´,İ+˜ñjf†ÕýNs‚¬w Ý›€\CŒü2BwËÃ)ì÷YZZD沟/,a~~¾p*åšÆêpæáòHò+9w~Ð÷+9ƒ¦#d2%�{hù¯(ÕqoKöví°hyŒß�1M¯R£ ²4ÛU§2S—h2)$k4‹Z}Mr& V à 4Þ9™�â1~òÒ‹„ý�‹/�e/¾ðâ�7^gyi‰öò2�<úa¾÷�oá¹KKKŒ��Óëõ˜šÚÃììM0LÃ4xïÂy ä×í26>ι7ß`nî6Õj�Ñ‘&q’qåê ^ý Μy�ó.súÁ‡xå'¯ÐíöÈÒ¡ šU† ˆÂ�ƒ­sn³,C: ŽS–ÌÔPz¦ê{îDvRejõ{Õ†–ŽÚɰ—YTKãíHgÛ�fMPñLfçR>ü�Ç=Ç=,Ó¢á6©㘖 â9Œ7ÆiÖ][PñlöN7Ø»§�ã÷¼g²ˆ°MÓ¤Õ¬R¯ÙÌ쩳o¦ÉHËåèáqöLÖ©U-ìáÀÞV ‰Qš2r»=`^zá¹+=6Æ÷¾óm2™311N–&ˆ¬ƒ®�¦‰BI$:Er®(»[<ù•/bšI’215½&�ÔïðãgŸçÕ×ÏÑéö›˜.«o�;K…4šÍ;Êk Û«ºèl÷aMnè_ü‹ñwîÜêBr´Ñϵí°–b“ÙÐ"ÈtªFvG7±¢1Añá6³ÃRJFò„v&ù�1›}ž$CÇÏt’´ËåÅÛŒ::‹½6÷zç8±ÿ0ín�'Þ벿f25rœ=�:×W:ô“„Å `±ò©3<2=É×.\&—°ôy`jŒ8ˉrÉÕö*ËA€e˜¸–ÅdµÂ 7nñðþiÎÍÍ3îY¼zë6Bntzd¹äžÉq~xå:Žaðs‡÷óÖâ2ÿýÇæKo�ç#3S;—¨5!󂤛ûDñZ§0˲KmYƒÍí0Zá¦VT¢$Ç«ÔÐ — ÌÑÄæ]5×ÝÂ6&Ó�(Np½ZYuèuVqÌ5;’dú @[[ZÛ‘�,=èŠèƒë‡aŒåTJ…„~�ÌwÄ'¹Nškë’EÊéÜ-lOQfLUn¸D-„À÷{ÄQ„5H"(UeË“$)µNu]/²lg‹JŒÿ€tª|�÷§,irQ�Ô¥Vµ°L‹ºÛ ¢7ÁÈ«�Q«¸ 3ZMMƒ©É:­†C­ZE×4Qòç¿Ï±ã'Bý‘Ód€ì�”4]¦Ò$ÂaaÀ:¯Y¾^_p8úܪ é† 4�9Ž™â˜2�é÷cw-rzmcûÈSˆÂPåéZôm9UÂX’f9ÆN®Rfˆ“Ë©”÷›$i–bè!4Î_¼ÎÜÜÕZ�›×¯¥|×åôéYZ^"K3>À}÷ßÏ믾Ê#þ³³78rô8o�;‹ßë±´Tè\¶FZ„aÈäÄYžsòž{°,‹ù¹9„¦S­Ô!ð}.¼w !§ï?ÅÕk³8x�^¯ÇòRÏ8rä(ׯ]Ŷm&§öàyã“%9Ö¶–e­szU@³±«ËVl^¥¡·Ñ)P†]ið)}Ü­ÖÁ°2ƒÊroìá¶Î;Kyžcqªqx&!M“BçÏ0pL›¦Û¤bWÑ„V’¨T–9Š¢uN¶"V¨÷¥žWe“Rý©†’Lë÷ûø¾Wú¾_}â+ܼq�{ï;ÍŸü«?fÿþÌÞ¸Îÿöiföâ¹ç_ä•Wß$Gã�×ßàå—_dfzï¼ýÏ>ó#îŸáéï?Í©SÇÈçž}–×_{•Ç��KøÚ“Oò³ŸùY¾þµorüä)¾ñµ'Yi/Óëroׯ^¡Z­ñÕ'Ÿ MS®_½Ê³Ïü�å¥%lÛæúõk4[­­7ÿqÕy‡¼€½N„kH‚L'ÙÁÉͤ “êÜmú™†àhrˆQ EEíNXQ–¦|¬jaJ‰géL¹ £VÊB?Bä!sÝuy›3û¾|>aà§ÐÔr~22Ûéqh´ÅBÐÇ�î›ãüR›+«]&<—ºcs¨ÕàP½F˜e,öC2™³¯Q#ˆbµê\ZíreµÃ‰±QDžqnnž§ÆhºÇF›·{>7;=¦k54ëœ_lsÿX‹×çyüø‘í³oCvØ5Ó¢Ó˜žG}ra•�”Àïaˆt[nƒ\ÁÒ°?Ðõ¬“cÆb7Ê Z¡vb9ÕÒÉÓ “~$ n„&é'Ö–¼”�p”êß5!ñÌdݹ�KA®WpÝ"¡ø=*Õ:RXô£'¼ÙýK)SŒdðž• r w Ò1éÍ7^çßþ¿ÊóÏ=ËC:ƒi„ƒ2ôŸüË?æôƒaZVÁ1@�þ—ú?qøÈQêõÿä÷ÿ1G�Çàþu]çûßû.ÇOœ(“ªR¤2¿µZ­”mïG+8I\;Æ2¡^‰ËwoŽå0ZÅЊ*¯R PvRµ¯Žã¸„¾)~‡eY¥ÎövI“ae¦•••²SÛÝ4–j4$Yñs–i€”†I?.Î¥Ö¤ksàŸH)0ôB†Qˆ" O¥ÅœtXJà×÷À^NUÀ 爳S I­•…î÷û»JªßV˜ÑÓs’L∔S•=ƒ+�Ã\dÞ!á¥2>yžóØ/þßüëÿ‡ÏþÖ?â+_úKFšMꃟãõ×XZ\äÁ�á©ï~›}ûpûö-šÍ¯½òîàAò<ãW~íµ%z IDATí×±m¿×ãS�?NšDüøìK|ÝÊ™»q–ÃGsëÆu‚(çÄñƒÜ¸Ñíþ)QXll•¹Ë²BºC‚'¾ôEæççxüg?Ëí[³üÕ_üŸøäc|ë_'|Î<ü(o�;‹ë:|ê±OsöÍ7ø�¿ÿ[ņ¥K„³ ¥9‰é‘N©í«6ô®ÚÑ ò¬Œ€ 0Œ¹––Qh–exÕAÐÅ ÚnP�¾B’("’ö€^¡t1EQÖÝj~š { gø=¼J!T;Þ„OÿìÏ•^òé*žg¶M¢ÉÅÅ%&&'áÌÃ�ðøÏ~Ç(XŹ,ZZÊS‡TLúþ*yô¢ÌÀu‹HÔ´ª|þ—~S™Å|æÓCð‘3÷ ÏÜG.‹¶˜3{÷⺅†Úµ 7n ¥™¨²¦À¦-,‡‰.ÊXo6’$aii©dqš¦YêKË(l)°eù- Ãum¡·Ó‡,ž-ç¾£ƒóZ¬×ö<C+¶ºÂÄ©¬ŠrYJãU‘©jñí^¯·)äBi?J)Ë÷5¬ï»«»Ñl²°°À�ôC:ŒÚí6KK <óÌ3\½r…�ÿÌÏðê«o°¼ÜæSŸúßùÖ7èõ|>ÿù_äožx€ù¹yÒ¼ ¼ûÎÛ?q’ÓùaÑü¥”ÈŽY\\À´ ’E– VWWxóõשTªÈÔ§ß[Æ0 {üÓ˜¦É‹/¼€ßó1̦�8yŠsgßÄ0‹ŒÉ™GÅs=�g~„×^ù {÷í_GTøi�ÙÐâœ_ChO_~š^~ ‘\æ€u�#^k(#«‰Â÷ÑhÉ{´ÛOáéº «ç8ßIy¡]c.2VB`:Æ £5üŸR6`ºê±'y‘ñ:äý SãHež†{{ø™½SŒ;HÉŒçb&2Ë8±g’©Z•<˘©¸Ô‚ðpt S€†ÄÓut!¸gj‚‰ŠGwu…×bzd¡W™ñ*ÔŒBŽòxÓfÌMiZ)9ž^d<«ƒnXÑ€t&D!éVÿ„¶f‡7�d¤¶†ýŒ‚6Ao ¤¤Ö#uú‰¾#iL×$ž™bÊ~w �Tê£d¢Ž›ÎOr×ó ¨\¿‡g&XÂÇï.“fµúŽ» ¤oö…Ê–¹®Kš ²!²0ÖT+H}t­ ½™²C¯³„@P©�þÿ´½w�'éyß÷é~yòìٜ̆n/p‡À H˜$(™P%•-»D—-ÿc•þ²è²,U¹XåPe»Ê¶\–mªX6 “ 8‡;.îî݆ۜgÃÌNøåÎݯÿèßÛó›Ù™Ù9�|¯¶nïvßéî·»Ÿ~Þçùb¥R�ÞäP�¦#FWE‘ÀÆqœë–·ZhšF£^GŸ¿�÷ßþ+VÿñoÓþχô;Hi@P ‚€tk*å ¦eÒëvh5›hºÎ±ã'ð<�Ðj5i®®066Žçû¼õæ:|˜[7opûÖMZ­&““ÌîÞ�ªª|ù«¿ÌÂÂ#þÖ¿û[„QHàû›6l¶ŽÃ¤)Çq ýØ�÷BUÕ" ÎU{Ö†ähäØ[�Z­Vl¢¤¬™TÁ>v’$t:�"Ë¢‰<9† (†%%‡7;RzRÆâ�×éº.aœ"DÞÙñ}K pôANx”+54{=‘]QD!¡— ÅÈ¥*U.ôà›‡ßï¶ÀÒµM�¿qÈâ“Ä`w:|ß§Z­]ÍíÞ…§ÊêFÊ”"Äšœ‹Aʸ3i§$BÁK54î|ô·¯œ§Úeïáœþñw1L“ñ]{¨ÔGˆ…ŠPuŽ}öKÜøä,¿ñßàâ…ó´Z-¦¦¦p]—ÃG޲ººÊÜþ´šM¾ÿ½ï'1/½ô çÏ}ÌXCcïž3ãWo-±k¼Ì‰ç>ÇíÛ÷™Ý½›Ñ±1nݼɡÃGh®®òÑÙ3$IÂk_øŸ=ÀÁƒ‡Ùàš¦±gï^Þ¿‡ëº8pˆñÉIöï?ÀÛ?y“ßü�¯¡“¶»4ÿÅA²ð½1Bæy˜»fèýðû˜•2îÑýˆ�ƒ×ðƒ2,UQà(S"0N×*(aj`»ùÃêõ»ØZ€¡DDQ@’夯L1 Â\¤þi-®\r&& ÖHc~˜¢«k¤¿¼j[`Ž´,÷-×UA&2z^\œÿNIs²m½ñúSL‚0)Î?J-Ü�Dˆ×kÄ9E˜Pˆ5Òœ3 Í…Qй�9§†]TèÁÒ|R¡' AnÚV²,«€låt²ûlËÞ8†eb†‰Æ ±’.hÛ X‡å³6#�m–ŒÈ{Q©Tèt:Äq¼�6ŒeÚÌíføÞÊç{X3s+¯p¹š¦=µP…]33>z‚}ûpìØ1FÇF™Ý5N¥ZáÙSÇQU…kW¯25½‹W_ûã“»˜˜çС9óK_ú"“Ó»Ø3w˜¹ýûq‡™é ØC¥¤S«×YYmñòg_ã•Ï|–Õ•eŽ?Á‘cÇxöùç)•˼üÊgèt:9zŒÃGŽP«× |Ÿ4M9~ò™uP‘íÚ“SR`{œˆ3á�.þ_=úUþíÙßgïȦ“ÕÎG´½yFÜ ÷—Þ¦j—©ÚeÞøäf¢2….Q2mþŸ÷þKO=O¨Œ³š¬Æ:º*p´a¸Âél‹g3×åžGÓuR ­4IÜY·¿ˆª:蛋¦iEL¾Ì¹Æ£Z�¼$›s¸Ý¬©‚’•E/6©Të|tö 7¯]æ7~íK€ û'ߣùûÿ¿óOˆ——QtƒÒ~žûÿä?Du¦ï÷P«.I¦&:É&@i!D-›8 ÈbÛˆQ•<GTóöQô— Q9ú¡Žn•°m'×ÛL<=Þ6°®‚DC5*ض�W 㦖fµzn]Ùk/ãk�Í��\Ð{pþŽã¬3ØI`•I|”¨èf kpþqäS*× h%i­Óùi–³ÜM»‚a˜xýªð#ð# ·:žCB”K @˜è„I^¬ö<¯x163lxrýrB¢l#Éöü§iQ+ŠRàyåÏì÷ûŸÚBÊ츮[àu·k—›¦‰ã8EõJêAÚ¶]Ìi6›;b–v¸j¡(¹6äÓÀþò<‡ÍLLÓ\gØ`ê*�ö*Ê ³¡*?}ç ‹‹�ùú×…”ü9�ó•ÌÇÖ‚Ô¢R�´Vp�õëÚ‹L’4O¦«Õœ8·±µ§ª*�¶¶ÝnóÓŸ¼Å©çžÃqÜuç/ 3›µÿ¦LFÂþRÈ¿üþÂkû^c¹¿Ìde’ŠUåòâ%NcÉóÙ7ó·± ‡?=ýÏI³„/Ÿø7ÏáÇ=NÌþ™~¤h!*@ÕH™s&¬õ2Œù&U¦ +ƒwS¤ØÞ)7„Z…¤ ™O·[!*½öÔëÜø.Ê›©“··…B�¹Tk�¡jg(ÙŠS•83°©dÓCÇÃÔ‡» ~¢¯cc%¯i*xýÜÇÔs¸T¬T)—s{Öp“8¥*‰0±��ÂJ¿‹®뎿åµÎ?ÉL,'ÇÕú^E„d˜…Mt·½Œ3ä”&:A²+’F(š¦}ªXä1𒤶“WÖ|¯‡n˜¸nnØû+…øþf#LTaḹܖ×ï¢ba¢`8cÅ·ÁQs kš©t~÷ŸaNNaî›Cµ]0-âG÷iç�¨ÿGÿ)ÊÏ!íV } kµš©F‡4M(•ʹêŽa022B¿ßÇóú¹lÚ ›hš¾är‘õ�vÃ40ÍœÑj®2>1I­VËÏo`°ÓoZq?ÅÎ�¡„ÈM-¤Ù�¦i…F¯ì,îÔ<( Cæ cSÇ´Ûí�ûp,–VãA”'%Ç ð=DÜ%Ér£©áç"Í¢TC3+X–Eà{dqCÏÈ´zay+ëÞ¡4SéEfd…{v'a…ògl¦� ÏØ„Cb”åxªúƒÀd)Ô™²Bf-kC’¦–ó\¥ÇRlrdzð“AKh›ï¥©ÂƒÀ¢—šŒfˆ3T‘S5�›=—‘¬ÅxIC‰—1³ë�N!RÕª�¥:ÚuF6%ùÄq„a˜��iZ…FšmÛ�-‰údiB’ªMEZlH²¸‡µ‰C�š*pÕ˜8jÑ Ll§Œªºø~5 ª½VŃê/9Ñ.Nµ�Ê…(°Mr‡+q¦O &Rúk»”eÙ§r’;~‰MU”\€^ú½ï´",ñ´ò9®×ë[ú„Ë ‰tt’õ^¯‡60¿€\òj§Á}£Ï·<Îp€ÝîÜ�"©—&KË+\»z…‘FƒÝ{fq�ȃ§Ÿ{íåAËK1qÜd~Á£\®Q«�äïŒãϨ½ÁB5ÉÔ�¾mþß§?ü€gN=[Tñd’*7¤S®©ÚÊ"ñ½ÛØsp_ú,Y¿ŸCÝ–@‚ÓïáþÂWhŒäI~–¥ÔjõâÔu]×qñWjd˪é:Ú`�¤f®‚j­F’ÄœªÞY'5%Ieò]Þ¬;©°ÞEþ CÇ¡ÑhlËe–Š*²° ;b2¹Õu½HØwR(�E&©(qýió‡c±ïûÔëu’T'†žCvDœç"¦Èð�4S1µ£®©GMˆ“6½ÐÀvËhöX®U;€Ö„~ç B¼Tú�q ßœäðM‹%´Qþùvç/¿GÃ'¹ÙÛ‘ÎmŠB;1XML Eà • ×V;£¤%Ì:1Ž&hm’Ü �Ö™?áÀá£<8÷&•Æ(Í€Ü#ˆ‰fÓ÷#®¼ÿ—çÉ]®`©Aq Ê %˜ !ÖÚöÃØÉ�®&› ©j0Ü–‚OÕP5<Í&3]|¡Ò ÒÐGS•B÷Ó:� ·û·jUnÄqÉjÁðÈàþiÎA‘³‚ø^ù1Ùé|¹Ž¦™3¿oÞ¸ŽÈRæöàö­›\¹v Ó.óÑGçYZ^áÎÝGLNÏòÞçÈЙ°ÈÙ3giÔ«X¶Ã'Ÿ|ÂÊòr^�F°´Òæ½ÎÑíÇh†ÃûžgllŒ«W.ã{÷çï Ú€)gOŸÆqÌϳ´ô˜É©iáûþ@*,dttì‰$w˜”7Üžü›�“CWÁÐTzê‚T¡bÀñ‰Ãô¢arpì ³BÕp¨)qæs¿y‡ƒ»¾ÂáÙ_ewc/3£§ˆÄæØán¢ñ00 ²ÜéÌT÷|‹¥È`ŸRV#lï‡Xµ1Rï1ªUBD]q·:[¼ Ÿ&É•-ÞR©ŒP-„bÌî4ln™,Jù­0 H…’ëjbFk°' кœ™Pˆâ¸ÀgYF{¨jÞbÕM]׉¢ç°€­ïEÞ¢ Ÿ •R©J"tÂ8ÝlLSÁPÏ¿ø"W.]bl|œV«‰c;TkuÞzó/<ÿ<§Nì!p_|‘þÛo!â8ÿ…¤ý_¨ÏìÆÞ·Õ-xy‹ÆÔóê�¥'˜ZŠkøiŠivW*”�€ÈÉÔr!PÁŽTÜ5UP6B/"Sm\·L–¹øAo Œ°}’««¹ç¼,ágY†Sªáû}4 MÄ[Ÿ‰|a7V2‡ý�Eàl!®êvñòå®&%²Ìœÿæ>ëűɅÏM|ü^„¢¹”+Õ¢j—cèúÕÐ=e�ü$ÿn¯×ò,êõ:a®á˱‰L¶d@“ÕÓ­ ×S—ß¹b¤ ¨ë¿;¯†í³* [²½µS˜¬í¾åîUbÈ\×¥Ýn¯Û” ·ååHùº�ç°U»sxƒ ÃMç?M²Æ4M¼07Ÿxpž±±1–––¸üÉeÒ4cîÀAÞ÷:Ä^ÿaîdÓé>G�EÓ�æ"šª1>>ÁÙ3§9~â$wnß¡R­rëÖ>zÌ/üâ—X\\ Ýjqáü9Ûá̇0»{Íæ*Î÷ÀFc„0XZzL·Óedd”»wnsàà¡M¯Aâ½Z­\¾P¶9ÿ¦†ÃHý ø)̧%ýyáñ‹û¿Œª¬AºäôKs?‡ªjÜö,îxÃ9NÀÖ6¼�ÛªÏû&�ƒ +¦ëŒ)“V½ËX%‘%€@ÑL‚V¿\Åo·‹wi«NÂfCQ úø=Õ(8<Ïëc*OÛ$å¤'!úx½Í(ãVF Ÿ êãy¢©©®%Q¬ ºnš"(™ YÖ¥ÛMh4òŠaǤ™Z(Ùl5rÈUJ&ºô;ºU¦T%ð=ÂÈûahÉ¥”å C“^ÐÃ5üdûO´ŒEò”ÊÃë?Ì퉰(º/íÖ ªfRª4ˆÂ�~Ø{B.ì‰c®™�‰.^×팄½8È¿srº6Ú@Ÿ YxD²¼D²²Lx÷6æìîüÏ�CUU._ú„ç׿Áüü=nܸÎwþøÛ<÷ü‹Ü¼q�$Iq›ùù{´šMFÇÆxþ…—øÞŸ}‡V«Å±'¸xá<åJ… ¸sû6§ž­ñèáCVWWx÷§oãõ{ìۀӼϯ}ã7i5›|üÑžyæY>|H«Õ¤Z­†!õFƒûóóÄQ„aš<÷ü ܼqË´ˆÓ„¥¥Ç,//Q­Õx0?ÏÕ+—i°ðèGŽçÚ•Ë8t˜£ÇŽ*>ñX6dPnø7SÚÙª` [ûBˆ‚¸'yµZ�0 ·�9Ô,Â-3Oì-Ë"Vô¼.®ï‘£f¤™J˜jhCjO†ž‘¥k¹�¦iNƒ¾×CQRâM6h랥A±Æó¼&a¥;ÕJ–ÝUYìù™ìw;‰Îå~…+ý ^ö$®)Ë2tsÈíó|­GÃx2ÉQPö˜:p‚ówÙ53K¯ÛcttŒ=ûöÓl6Ù·o†a}>L–¦?q’G23»PØ»wŽ™™ÝdYÊ®]3<ÿ‹diÊ®™ÙâXµz�Fc�àè±AÀç¿ø å` IDATE±onŽfsC7›˜ M9ƃ‡�h÷ò î¾ú öñ“¤í&Þ¹³�¦ôßú"‰i|ó[$„x^§TE³GèE&q*d~˜P+)ܲ©'(Š@SÖ›Üu`Ôé…ÆSÙ¸–‘a«¹²@†;Þ:ýÈØÖŽ7—œ)•–^gµ°ãÕíú‘UHœm7ä)Y˜Ã,FCÍÛGkI‘Ü–×Vû„ýÂ0 T®ƒQÇ‹´m«þr8F Y.Ü-,¡×ž`3;FŒ­'Æ“vÊ’M-Äšä0v[UÕ-“4™ ¶Z­BòF¶ø†‡ŸÂ/ŽÀ6¿=ßš†n²䤲‚ *õz}GlÒáó�­^©1Ûh4 u‹á!5l¥l×ðÇqã9lfƒOJm5«k�•Œ(ÉHÓœqüÄIÆFª<^\Ä0ML3ǧ7uÛdßÜYš±onP¸xátMÃrë,,.pùÒ'ضÍ;oÿ]7¸pá"™LOOóÖ›?¦¹ºÊòò2–i195Åî={ó{�fì?p‘Áòòò mè^óoû´õ—ÏÑßä@:ôhÉbÇ�:‹¡ùDÒšßböÚ}>7Òa—mÛ‰Pò*nª²ßõI'½€æŒ’öPÝ Ro•À|é)×@b—7{6]Í0´¼e.»nišæP*±FLJ³×VAQùèz“ŽŸ«|xy™¼w�·>~ˆ¦¤xýž»–“eª¹²Á�Ï>EGú!ܼ·ˆm¤$iÆ™K÷x룇t¥¸ÏyKYåôåÇt#—Ÿ}D«„‰ÊûŸ,¡¨:Wîvù‹wïñ½wîò“�PUÛÌ8wé&?øÉG´û¥ê›*—ïvA1xïâcÂDEQu~ðÞ=~ðþ<ï_ZFèUî=ZáìÅ›^—·Þ¿€¢¨Tj£DJ±ƒ8kÏàêêj±þ¦i¢*Ù¦ vn‹Z)°ç¶b)Ý��® \ÅÏÊñÖÚörȧ¢(´š«9f²2N�æóÔŠKõ×T•èÞ]D’6W‰=@›œÆúÒ׊âB–¦ø^Ÿr¹ÂK/†GÐív¹sûV®=Ÿ$¼ú¹ÏÓ$sApòԳܟŸ§Ýja™&Bdø¾G&££cÌí?@³¹Ê‹¯|†»wï°ÿàAtÝt±Jt»]–—³¼ô˜Ç‹‹|î _dyy™r¹Ìá#GÙ·oŽüÅwñ=§äâõ=–——øù_ü%\×ååÏ|– È»=�‘Qú½¶ã ¾K›Á†õ[å{"-‰%±Ì²,�F�­-Ö\U‹DV~‡²,+Š.Ãzæ###E5tã� aœ¥L]£ßëàu—H“„Jm„X©áÇF¡°¡©Yþ=ÚüÅ©V‹‹9|pŽKWn°oßQ³²´ÈÌìnîܾÃî={¹uó/=w0×fí<þÿáÕ«ùu&#ÿÁ?¢üÕ/�È@i(º‹ã–rv{ÔC!¡Àh%_›4S‹¤ÏOl*µQZÍeT4³RTßD’c^žÖₜ”Ä*ºYƲ‚ÀG$½ÁGcýß “rm€^g[˃J«­„;8ùÒíK(‰WŽãä­ð¸�­'ë$Bb¥B¹\}‚°!ÈIs¦])⤶žlY=H2¡7HÓCt‰ ͪZxjæa9Y0Lt¬Ò(†aÐëõ¶¬²Ê]s¯×+X¨;� H€|¹\~‚tv.©ðÝ–ÅFóääbY _qžL††µåŽ~ØK{'CV¤Ö¯ |@‘€o·RÊf•†ÎÒ5ôÖ”òÊþWè }Ój·lÏIÒ™¼œLÓ¤\©Ð÷óÊPÉ6èušØš‡¢Ù$B'Ã,ü鳨ÃÔS4Í@ˆÜ§]Q4ü0Å)�äĤ^KóQ“Åå �yöùŠgXÂ/„Å/ è<(JÎÊ Ã°V—p”�øÜËñ×A(Ó¥šÈ§ˆÃ*0n†ì±}ÊÚìaUåahsÇ· ž¢�+Ç^'ä�ÛCïü�r5ßìgQÍ�À[YÂ+ÿ:(ë«2²r#×U&$›Å]ÍrÂijPªŽ¡( �v!²Â‚öáâ2ïž½Êßþòqþ¿^âÕg÷09bóß¿ÄgNÍQ«ØÔ]…ÅfÈîñµÏÈj•ù_þàuþá7>O’øQÆÂã'ÔùðÒ«í>{v�"Ð9ul?Ýö )ÿçwN335ÊôDƒcsø�Ï÷ÞºÈßýÕSt½„K7—Xnõùì©ÝŒT ®ÝksãÞ 'MqþÚ¿ôÚ3Ü}ÔdþÑ2_|é0ðçïò·~é$eGçüƒ÷ø­¯½ŠÎÔäï|�7߿ȷ~õe~ðÓKü{_{>¯[y,Ú ·aãú¯'ÐöÐÅéN2—Jµ�â "q’*D™…íæÉJàwÑÄö¤9/-aYi° $™¹6ßë +!†"Xù×ÿ½ïþY1O­Ö¨ýÓßEÈ»#�v‹ׯ³kfI’2;»›Õæ*wnÝDÓuFFF ³žù{wÙ»oŽׯ±²¼Äá#Ǹyó:““Sù&Õ²Ù·?^¿ÏÒò ðàÁ}Ž?A»ÕbbrŠ~¯Çµ«WØ»oŽûówɲ\]æÐ‘£'�FƒT�;óY:"MHƒ -#É¢ÔÈ‰à–…ïõÉ×|2¾Y™Jµž³:�±õ„$SèGVa3ÿ4"ºÔñMÓ´(8;žÿÄú '·=åÙZŸû¾Å]ÏzêŽmx¸ZÊŒ0mëôo‹) ýLçŽïò84¶ýÙ ðJ£K5}€ÿ³:AÒ}€Vš$ »t£9çÔ¶ç#d«v3€¦äÔì¬ MD‰6Hò\Â0 ‰zXZ ­�ÿëI|÷�¿ÿ¨|ýkOjgüØD7sRei¥Ã·¾þ<“�<¹Õ5•ñ‘2?÷Ân­|pá¿þó‡"#&ÿú�ÞçÿݯD IØÅÔ 5¥íÁëï^CUU~áÕ瘫ç߸…P4þ›ÿý \ÛâïÿæÏ1Ò¨Ñîz|çõ÷øÖ¯œB%åÊÝ +]~þ…PÞøðõªÃóGƉS ]M¹z¯Ëëï\¦Q+³´Òæ·¿ñy%…ÿá÷ÂÜì8öÍðü‰¼{ö ÷.Ç }?âïýÚó¤©À�tÔ¡M¦†@Û> )Šj Hwþ€t'„‚áŽXê=´´½)Ï#JTbJ¥<–~wSÒ\˜¨Î(aÐÏ7¨Cÿ?U\\·D’Ä„~-KñÿïC绌V­2öOÿÙÁO�æäó#ee ”-ê��Xå|ù~K(”ïûO˜ól6¤²ëºE‡UJ>- Ÿ—ÜLK¶\o?‡¢É õvCnT,Ë‚Õðƒÿ’0·³SUDyœÞçþ}Bes(¡Œå’ƒ!MuE¡T.¯+2´ZM\Ý_—kÈ$×rª…’.Aaâ%Vy]×éõº˜´Q€`H­HQ¶W™�¼ð¢ëú–Ê;O›ÿÄu¿Qà`)À&a¿Óçåz—q+ÞQÕÀK5®{%Nwêtý‰äU�«Äœ(uy¡ÖÅÝW©»Ýˆšãwi‰cd‘‡jTPT›Ð×H�“‘Eþ’sÏžþ°`c‹wKK[XQwºΠ5pýÚU =¯2ÛFVˆg+ŠB©2FL™¸Ò ú­oa9Jé—¿úDb+×±dFhY›^·™»]UG3—(QäÌTq‹—'�ºÅ:ZFÉ!náõZ˜–…S#Ìœbþºu-þɇ¦Š|·è÷Ú˜–ƒ]%Hí\†FX¹lJ–Ýu‰m&”ãû>ív»ÀQnf°ÙÐUp�%iÒí´°l«4†»¨Fi…ˆP3Ó©® \#$ [ô{],»„áŒÒ�m¢d}µIÕs�Zk¨Reh®ù«øž—EìwM�Â6â� Ûæ›0É‚í÷û”Ëk8À�¶Edò(Û*Ç+*¯5à€•1e ž) ^v7g³Û/Jñu]× Ñîaëà�œ‹|¤J„H“-Ýx†‡TUh6›dóóÄöðçà™¯Àg~ :�±o¾½i«K®¡œ/m0süªª'9 á�¾žk G‰°ð#…$ÓÑ4…ˆ4êÒëõøèìY„V¢脉F/Ô±,‡0ð½> Ä gÏœ)®¯Ón†!Ÿ=½Žè!pUÝ~¿�ªª,/?æÜÇgi5WsLx§C†˜ƒ�¤ü þM [ÍØë†¨YÂ>»Ï«�ûKÁŽá©P¸:œîÔ¹:Äbýý‘²Ûòx±ÚezƒÏðØç†ØY 'z½u�Ùé±¢«0;Õ Zqhu=„P”õ†@[­ßækŸã‰]Ý'è¯E¥r Œ:è¥Bì_Ľ- ̦žáj~o™$óõ3êx±¹Î"F²õ°KϰÕ>~o™,M)W8õ1FÿÑŒ¹ÿ µ¿óMìã‡vKK’˜…‡�<)»?¯H¥2‚L¥³äp7fcgF¶éE)ðÀ7®_[·~›Í•9…lqw:�uf6Ã÷a«cÿ^Qr%Œv»]/¢ö ᤠå3Ðét—߀$‚W¿ Ï|žýU”æ}ì…KÛÎïv»´Ûm¢(BÓ4ªÕ\z4ŠS2!°M}@<ÎèEfž”æëªÀ5"b¯ßÁqËÎ(~âä@½\³Iz(äjY<’k9l‚!‹äúH‰J 1Üll7³Q¨%hŠ`ÌŒ±•„ÀRS¦íÜO¼—ê„›h·n6b¡²ÙôR[ͰÔ�¶¯KI™µClMÐK4’¡êÁ´²øáŸ1ç&ÕÆ(Aâòñ¥€[�–; >¸Ø¥VáâùsضÍÕË—i®®05=M§Óá½wßfbr’z½ÁÍ×9î£ÂÉèúÕ+TªUÎ}t–,KùθsçBd´;�»ŒSªñî{gq]‹ª«1¦í"ƒ–b :éô–el¹{Pˆ3‘úDQŒí”P — D ¸åÚ@籇©øO´Þse€”( ˆâ Ç-£êA$YV¤ÛÉ=–ÓUZ¢…­ØŠ1¤Ø�G>QŽ[ÍÆ0í�8º‡&úë6/Ab� ‰^ÄJrÒf>å‡mäö’¹øwÎfN’ŒR¥Zì7 …o6tU`j1aà“¤à–ªÕʱAd„‰ŠSªø=LíI„® t%Â÷ƒ¢ že^¿3 Ôi›Vn�‚å*…ç¥`´„)ìddÇÁQá•JÊK¶Çsz‡mGl"7£(kA‚5ÑnYI”šˆ2iØi²-‡mÛ4ϼŽY'UòÄN û¨¦…’e(ªŠ‚@ÉR”4-_3ëáEÔÅë°ëtW 1ó@Õv?¿íyHF·tÖÑ4� J¸{ç¼û6+ËK,·nÝà½w~Šª*üåëßçþ½yææöóƒï�æê*ÝnUSyûÍsíê‚ àÇ?ú!ÕZ�™™ÙmŸƒ¿ª‰ƒ®JZ‚­& @ =aÌJ‰3‹gvãH„Êjl²›”´{ü»©¤Œ›#£—èÄCqx·rÀí£w^ÇiÔ‰�¢h¨fŒ£PzžhÀ®Þ)ÙCv€ä»$Ÿs©6áû>agÊ€‰-çƒB½¤Ä‚Ý3S¨º�Æ<^nÓìúL�•Q…•¶ÏrÓÄn“~�ñx¥Gš„ÔJ*(ú@þÊä“kÁKÏDd –:ˆÃŠJ’*¼rr–¥–ÇÔ¨�¥ â¦'FpÜ2Išk¼�×òb@Téóp¹ÇíG+«Y“XÄ¸Šƒ¦ Á*ùü0@Õ­¢MÜï®9�€Äìêl,Go| ä‡h³JSöf®® „HÉ0 ]Õ4Í¢4·} Ô"¡âüøè¸¥*I¦“eZn¡w·”@SH„QØÂ0ÄqËD‰Šnmn-±–òú7:•mçP5<$~×÷}ºÝn!º½Õ|Ó4 )Ÿµk¸ 0«2°Ê�ÂNã¤Uc÷öEÔÆ4¥ZƒèöÇÄKó¤«�H—î¡[6éÒ]Â[“®<@wË`•0¢úƒ àT`ù.¬Ì£L‚½Ï=õä5hš†¢jDqʃ\Jgié1AòÒ+Ÿ!ÍR&'§h®®âõ=jµ:i–2==�ã8ìÝ7‡9ð>ísŸçæÍô{=^|é%:Ý/½ü²4%Š"öÎÍÑëõrëç,ïm(ŠÊÉgNqåòe¦§§q—Z­ÎÌì,µz�[7®3wà «++^¹RÍ¡‘ÑQ<¯O½Þ(ˆ²z4|ÿªÉm"G&K±….eXJ´3n%™†¿C̬,6tņ͒\GM˜±C\-£Ÿêrûôo�áÎ�»,vÜY(qk>%L"jêíhõŸiÃ'¡:ò]’2D²º PH2�L¨¨ŠÀ6`jÔAWS¦FmÒ$A3löÍN1=^gfÌÆÔ¶ fk쟩Ruu cït•3¦Gm 5E½~D©TâØ¡==0‹eêÄA§Ð�UÈØ=YB!eÏdSÏãÐì¸i€„T+U¦&FñBðSŸùì6ö¤O}&óVXÊ–wJ쪗AdÌŒ»9¤& ™šœää‘9f§Çð}�ª�²†eÀžÉ ~¬±i[ŠõÛÎé-WqXÏUP”ü{”)&¦iæ ‚,KÖ9�m~ïÖœÖ|?Ä0ll·J”ä¸Î§}Ç’L-8»ÝÊ‘˜µ’,·"–Üœû÷î²²¼ÂÄÄ$££cܺu“ç_x‰3§? ŽcNðª¢'1Q²´¼D¯Ûav÷n,ËæÆ�kD=ù%D}zG*;RG6ˆS²LàÚz¡³<| B(…$¨tÌŸ£<'é{–í®É‰ú½�ìžJ�l¯¶4ì4&¿MÒ”h§›åa§2yo†Ç:�Û<ÉÔyZô³<É5•<¸V´„=nŒ£e4c��õØÍ†@¡“¬Æ³öæ¥f�\6l·1aÅwxïÝ÷èöúœ�ÓeƵÑ"ÁÍqv ¦�—òÃ0ÄvJ„‰J”¤hªÀ� 2±õs¸ú2ü@ W\3Þ´Åfå�Ín«ÕD×4œR•0VšO¬ºš¡ˆÏÐ «pJ“h�õßðH2m`i†!YØ$ LÓÅ-• 5�á—Ijn¶{Œã¸0O�-î­0pÃëò£=¬ì8qUp,· �² '5M3×Ç”z†[ Pã8¦wç2Úè.‚$Cé7Ñ$ý6ªS¿skj4gÿ)‚»—ÑÆv#J#˜Ë·P¯CäÃÊ<´àÕoaTGwt–Fy@ݵkŠÅG�8xè0/¼øËËËT«UnݼÁ+Ÿ}�é™4Mcÿ�ýLŒÕÙ¿o/­V›]»¦™Þ5ËØØ�‘¦¦¦1-‹Ý»÷Po4(•ÊØŽÍØø�Æ­f“ÃG�Q.—9pðwïÜæ³¯¾ÆøÄ$ÇŽguu•©éi4Mct4g98x˜é]»h5[¬2ÉJ“�0Œ±lÓ.ãG‘%O‚ÔFS²¢õ­(`ê)A”bZΚœl:ã�Ú¹I,�ïb[UÜ´N’ÄxÊÀ•‹”ål•†ZÃTÌb~𩨥¼k—Û h†Ce RÂôÉŠ×fë';j’04ÜQ“UÛ�#H Ür='¼vÛ„��í”ÐÅlÐÜîJ;Þ, ðÔR9ßà„Q”'Ì›�PÆlìIº¹¼ªStôdÜ»qýZ®g;?Ï3§žåÂùsôzya`yi!33³¬./“ A½Þ`yi‰( ™˜œ¤Ýj36>ÎÊò ÕZË41 “ýpíÊe 3/ LMMç›hÏãè±ã몴ÃÅ�­†´sŽÅf¸Õ{0ܵèõz„ó—1v"C’…ÛXÕª[!Zy˜›(ˆ çðKˆ8D±\0¨Nb=¾ �oA–BóÙô1”ç~Óv øåv±Øu]TM'ŠS ]ÅÐó°yÛä8Zö™1½§ÊK©ñc¬ø<—ŒòâÑE�ß³‰Ý� 6Ü “r²¤.‰ išróÆuöÍíG×õ"�Ȳ¬Ð’˲üƒEQ�ÝÑ4­¨2¥iŠ"Rl#^‡MM…B”h¨z {@‚Èb©l ò�KfãjÞºëK2Q G· IDATe�5]ç.9¾ïÃ@a÷´°£ÕÍÜ™¤ï÷yγ¨Ü&#¥¦V8jF¬Wnï[¡2 Tõ»Ë€Z�Î<¯O= -‡\ ´÷<�4¨ØOVðrÛHnûë{ˆ¸ÂÔÄ�Æ|/'�=ÍiMŽ~lQ®4ˆãxh~ðÄú©M¹š«Qô:ËØZ¾ÁŠR•$³°’Ãd!iOü´ë×u½xö6e„Ûª ¤´ÄÚîÏ) [²¥Çñ–¬êF#¯6¶Ûm¼ o¡ˆ Õ­�tVÐì"èQæó´Î¿�uò øçÞMÇš{QÏqëZŒúãÿš¦ƒÿò7 Ç­;y-›¹­•Ëe4ÃÄcL]CW)ðS²›Ðëukµ6·lF…ºˆ‚t�åÝm/JaźS¢�¦iÔë¹óQ§Ó)섇Ÿƒ\œýªª2>>±iì&BþŒŽŽî»nlg¿«)‚q#b�íáj¹ú‡ª(dŠÆBdq»o<%!Rx¡Ö£ª†O}ÆÎœ9ÍÞ½û Y©¥¥%&&&xî¹çxã�7xé¥—Š¿»‘™ÿiì`‡ÙÑfÓëõ6…=©J.ŸÍ 0�@’ª$XØN!¾×)HO™P R W_O(N3•T«R*•‰âˆ^¯Keà´•“¦¢uZÙ[ ĉJ‚�[ª�f K½G,Š»øJWqxÆ<†2‡E‰Jµž“zºËh¤¤Š“CØïO36Xûý®þ¤Öy&¥†îºüh©ÅõN�“ƒ„†[ªä�Ä ‹©ÅO5‚œ|ì”Gñ}Çqòª²×ÃÑÃu8Þ0Ñp*"qSéæŽ_"'õ¦J®Õ.‰NQª¡yLI“k@×4MÓ Ã°�m”¤¯œÐÔ+äÈê4E��r¥•¼�yqË4×,teºS¨P¬¿Ì=â8.ÈÃc˜¼Úív NÕtÐD‹wAQ(í9Œa—ñ½jeÿò»¨†‰}ò‹¤JÞå(ÅmøËÿ ºË¤ãè½üM2³D©T*º£ÃÄ·á!eâü0!I3Ê®I¿×[g€÷BÆrM”̸x÷’TE1ë1= V¶ÒŸŽôåºn±ö½^o�-÷NæË1‡d,ß6¹•ÃV3f-Ÿi+(¸ªª�ë}‡¥mÔ\-ãåZUl_²W“%Üà ì‘i²8£.Áêc¼Ò¯#Ô'q1ò¢†å§†Å«‡“�áQ«ÕÖ%ÃþÄ›}˜@×R,m½vk&À‹ó$-W=èA꣨:º’¢«ë¯ÙO,ÊÕQEa±õ�»é'Ì*Gqs¦¡çõP³ �¯zÚ�,FÓÎ�ÆZý&£L™UÚš |�è8屜�Ùic*=TE�¤ Aš[Xþ,lÜaù+EÉ• t%\מòS‡J5Gïµ— ¬­ü0ÅÂÄq«96Óëb¨á¶! Rêƒè� §”Ø<¯‡¥æ&I¦ ZcƒÍƒ‡7×Ü<°ª¤ƒ“ls| Õ0›7ŽãuBÜO“y‘ó¥7y’$t»Ý¯½2I–�5Š¢"A€õµÓé ’¡d) €ªA#T T×¶ÐL ¿Û&‰B„a,_×u‰W¢ï÷ðO}ש5ÂÁ†à.Éx2¸ËdÒ c²LPvL:�N±Î²Ê=¬ùÜëõ@èæšÎ¦`.–iäÁ?m£*‚0ÑH±q¢ˆþé÷H»]´¹(û±1êHˆ†ô3—亟ÕVYþLyLÓükOnåP�ݶÇn;XWlð…Á]ßf14¶„��›1§*[ãÙäЃKœùø2Ÿÿò?àÛßþ6~sKA«kÁÏÞ ž( ëµåyš rÜ ¸Ÿ�N´666jÞíØJïö;Ôò޳$·zxFÁ}~ÀœµÅŠ4 ƒ[¹ÃùÀe˜O >BñÎv@Gî_-0²«4Ò¯àÌ­€pPe�9ÉÆ5烔֡™®OW÷Ëúuµ@; éâ`© [8æNY¯´¬ä§¤” ƒGSI^^ °æ«—'8=zŽ�¸@[-rRÞ_‘¾€( °�h¦êT’+¹rð|�dŽ*©©E*ºu–…k;ZVqf Œ­êÁ^Ë^c{õ K΀ڒ; dÙÛ5XVß_éçyN�pÌt×êA”Jüö£áf¥*$¹ �z~�†A»³P±ö·é8îX¿Ì ëûN&©·sÿ“�Õó*ká½$Mv›¯+ Q½%][Ý"×J:Àë`¸¹¹y k2¸é“;Àüüú6±€ƒÇ]â<{®’‘Œ#ò4ØÓiL0P*U%©I.få˜$ E2À±I¡F“V«SŸ`m ã+$J%$…]ëŠN:>†YŽc쯒 ÿ¦cI”yXÅ{kŽ_»Þçç_¿ˆþÖ�Gx¢Ûä8}…Ÿ¸s��ÎWÁ´0‘㎠žïîÒQLs�tçI“×!ÄVGQZ�q,"Mb:ÝJ‹z8èá£= 7“óßJ’©‹-Z£Z›LÊüÍ2ßó¼úð«u¯ogl×*�ã)eµ¦q\Üg¸—ÉÃ^µeÜ\½N{p‰a÷Nü] t,ÖŠqW‚V‹ ® Û‹ “CÇA½Ÿüây�kÀ'oÀ_\‚/®Ã¿x抣˜Þ×·:>þÄõOÆR¥*=a-7:�vìZ«}·ù³¬ßæö ‘+ƒ›©Ã°0qÆä¥¾Q°âV¬ÛáëvÁιӋö…#Èô"�ì«8sG)³ÊaZ(ëmÜCn,Î|C³0ûµ„ÄdÛ`R Ïóº<¿ÛüBä¥Ä0T�?‘cå˜8Çõ�¦7ÆžHÒÒÅoT¸Ó+Ã7YãRý™-é²lµÈÓ˜4¯´ç0\¢$GPX=0Ç>ëY“啲‚�.qZ�‚fk|ÚõqäÎdk‹´S(¹ƒ�{ÐC¥ÿ�D�f‹\IJeâûU¢”„½©jËöï—¢RFPHüf›¢4I²¼ÆÚé‘ác£Õëb E6†–Tó5ž. C,Â}1ͦT$IÊÆæ°&j·…ý 2E%´^ °ßþÂî7_c¸tP)Šâ¶¾7Ò™VV˜Å£[ÎvV»ëº•ÂÁø3âÆ2BV³ÉÏÛëêùie¾àÙfí±¾Û=hF²iš<72¸ËƒûZð‘%èšp�óÅh‹Ùd®°ö�ÿüGŹû>„eÑùè2úâg`ãÖcßQº[­-ÃöJÎöÍè­<^˜ÛýF¦ 6r›©‹e”4dJaRpÈÉh[%Q!IÇP…;ý„e;fï0¬°¢hšg°š‡™o$ܸ~ž‹—oqk½Ï•+׸téGŽaqqqßk›$�íGzšdGïF <Ï[*A^H„PfŸ-l>eJe˜VÕ•‰³1¡Q(r<<¿êô\ž�ŠÃóF‡{Ê” Š0-Ïo¥‚²,¦HWA.øÕçÎñÔÕ[Jr¼íà™)½ z—•åé<]Á2+…˨:„%’'¯÷9Òª4‡MY"Š„ó}~ÿô>þrå¸öôÕUÞ¶8�%%¥RdJ! %™R˜Ròô�[i6I²Œ8M0mi{…‰ßhðKç¯S¢ø±»W° ƒû[>Ÿ¸¶FÛ2ùž®Y㉅J¢ ùqâgžbþ¯ýMÒnÕÔ*%µÃ´Kò¬G�Øc²�OÉ‹GJ(cŒ=d^J­-‚�ãú”¥K�0Iwà½ÂD‘å%óm›tlb1�j²Ð,�U …ï{õ! ×ëÕm¥Û ŒÛ«®\Ýn`ÕCc»&ß¡Y«Rª&JU¬îª�?+aH)UY`ºî˜‘;ĪÒëÝS›Ð(±d� À%ç"0, —V5aº$yTµ …$½¶ yFÞëá=ü(›¿ó›x�<@9ÝZÃ;txKï6H;»={Ý¿^¯ÿ/‡.6læsfƉq’«Š‚%3b¹]UÒwEÚª;ü: ¯‡t)‚›n…g�6ûD�ïC Ÿáp8%v;•0]l˜¬Âê ÔA¿ƒ~†Ãáo;é¬(�Z4ޖŸ:X©ت �SråÒh¶j½ëá•©8|L®à�ãp‰b­¸E"RÞTÏsk¸Äy'�æEá3 GiÁRÓåÿ9{�—o®³ÆÜ·ØåÌz×”üà#wðÉ×.¶PÜ¿´À»ï8Ê…�/ßìaKøÓ3—8Ôô9}«‡-%ßÏq~ãù7(©ãÒPõ:-z.}à>>wæ/JLYa–RZ(eÔÚ¥úpÔšŠÛIg®™ã˜9IÂîÒhTd‘4XÃ6+G±Ò¨H[šM;«ä À0±ét« (I’1›9›R@ˆr‹f»’® 7q�p¼žgì´£(¢È™%ý àð¼E’ÛSçÛMtÑmw!ƒÜÄ5 æÛ�¿[EJ'UZjì­°¡·³�÷úwsss;|Ë·“΀ë“C+Šl÷>/˲&+ìGºªîÕ#L2¤4p-ÉÆÆFM6Úí,Yà[ÓÝ0wiwªß½×ÛÀ2«ƒ“žŸ$ áKϱñs?‰uhÿ]ï¡ÿG¿ÏâßùoXÿÕÿ ûíïÄÿ»?Iw~Ã0ꃪÆÒ½òä$+]·Õ´ô›x‹¸…Û!”Í2¬”‡›ƒ©kÖ.`”1~úM¼N ˆ)‚5 ÛQ? ò?T'¶{ Çq$}mRJæææj|~íJv@‹qûØþ.M}¿0*=ò²ÄoÐJA&Úu†H˯Þ×8d”l°`›»Æá~9 R1Gä20MzŠ\í8ì´�’sý€cM…¢i Ò’ËÃ’�%Îsòhˆ ¡TÂð–�kÖ‡}<³¤aÁ(+h[ HTƒ°”x† E8†¤”ƒ‚+Èû–—Øô‚�Žk“%® ž•Ó0 .ÛñXi5k¸VN¡ >¾šðsçÖøµwÜÍwø&¢pf”aŒ2‰ãÏ×Õ( )³×Úê(¦¹ÄòÆDâJÕDÇé¬0ÈJ kì…”¦‘sc#£Ó´qlI”í?Ÿ$}M’ÇK †…É¢«jÂèÆÆÆ�ógídMÎ×ñCCöz~Ç©¡�š¼®Çn¤³Ýª˜º Q–%ëëëSß³=톟ÕE¹4/ÉŠ’†kGQÕû�N£ñÿ¯T‰oåSÉmQ”fßoVÒ©ÁÒòp½f�œêkØ/¯Ò÷£•„´¤æ[U©Ñ¤3Ý•¢R¥B ò'~âçÂBΤW;Ë�BpÌ˦˜‹a¢Ê{ø\û&†„²”v aš$W �PZË~þ$iÌuÝ™ÀÞ:!Žã˜Á`0å´¥“ÝYÁæ'¦IgZüYš_ÆXfQãÅ6ÑìÑO†diD–+Z~Çl¥%bL:«¿ AÓhÐ6ZµF¢4À2 Ò$&;�)Ã!N†(¦ZŒ@u�0òuüè3¸-HQ…‰tšaõCBÿÃ`œcë5°m{&ÍŽ–R†a]œÐ›üí�vô»0ùýU%¸D¾cÉbK«°ñ• R0àÊ¡&HOþQ:†ðL<�+\ZÆVÇf’4¥òœC­6ÍF‹4ƒ9 \©p$€B ÁR{ ǶQy†UŽp¤Â‘ IF—†¤Ûj£”A–x¦Âª‚ÜŽY%¶”€Â³L\²èB’t›M[MDQ`§À•Š$“,v™ó=‚`‹€©Ê‚‘Q"ù°gÐj¶°œ&a\‚*§Š»�´t±m‡8Ø$KlÇÇq›Äi‰*+ÌsaTÊ5EQ�Ž)‘¶•/ó˜$É«Žží3Š T™ÓiHâÜÚ“!¡Ÿ�Ôäï,Å"§;v»ÚMgvû|MÓyÄV°u\uœÄëA¨  IDAT‘îEzÓÑÝ,eg5ä©�%wùŒÝbÑö÷Ðóñ»\»v�f«Ëoüwyíµ×ùÀ‡¾·:Ù—ƒ©ûу1fãÚŽ‹ã6‰Ò�lÜÉe&~³Cõ+!é1·b3WIj^c´pÔÑ;_®êû T™Ç)¶ãW"ò ÒŒ}(gÓcò€Õð\\Ç©‡v#mmO^Ó4¥Å<¹ºÆ«ë=Ö¢˜+£€�ö„E�Í4ã|ÄrÃ#Sð̵U,³>µë !„Ø7 êgw·Àªa EQL¹èì×:Þ˱­Ñh`Èê™t,�Ú!3IêÔ×`ÙÞØ´z2U9É)¥H£ÞXÀ_3â+P% LÓÂû;(£€èùoƒRˆV‹öÇþ;Ä‘£5Œ`òà¡7†I+X½6³V¶»lo9¹ý?ù_ÿ̲“Ya)‰Š?[¶„â�Vˆ¡r¤”ض]ᦳË4’/àÌ-CY�/#]Ê”Ì8…œÿnŠ’ÛêhͲÉÖ´PQSzÎ:ŽkúÛe¦O~¿i; rPY],(Jf»&Tª´�)Õ”ÓXösŠE;Å“åØÒñ­Aq�¸)w¸1eYÔ:"]¥}w®2/È7N#[+Vƒxãƒò^ Ñ|K•ÔIù©f³9%5¡ÿ®1Û¥f9õôý•ðŒ0de[˜dcùª\Y4šcrFПJ¶ª Z!T6žoã7Ú$¹$/ªJä~—P%¹Pnl0ø“OæS”§OS˜Mʹe¿�iš|þóŸÅµM^|ñ%xðA.^ºÂ'îbaa�K—.Òívyú©' ƒ€ïû��Ï~æ³Ì/,°|ø0žïóÂóÏqéâEn¬^çäÉ;Êýzîì9n­Ý`~®C„¼òòË\»¶Ê÷ýÀG¹~í�¿ëÝ”y¸§B‚cé´,&ÉŠ±deA©¶UªÍ©…*² 0Œ­�E�7Û®6�³e°oÂ`ˆ‚Õµ‚¿ÑÂvfëèß^°&±–:¡¬+˜†äaÆo¬öùÊ0¦íجØr| ‚Ïå�Þ8Ç_~ä¾zñ*qBA¥Yúµ+«ø¶EÓ2ym½Ç?úy>zên>ýæ%¾uí+íŽœÞØÄt¾~u•nÃg”ç|þìyÚ¶Å•a@Óu¸VÉóó7×Y�^]ßd¥Õ 'RÊ:°ê“¯ÖûÝo-¶WQ4«Ù¶íJúK�ïXS¦“C¯áô5¸c;p=í``N½õA%�‰“¤EëÑw‘¯^'¿yƒùŸþÊ£'êö¡®ÚîÖ¶Óƒ>,LVÔz4[wª~þçþ-%·ûØÇ~FRZ‡ì”y» U’¨xkqø¸—rÈNjç¦0 1Ó³4Õ·±»GPé�|p«{”9á !VIÒ¸’5i }ÐÀÞ-ýw}èÚ¾¹N ’j3Msf#ýùqR)B4 „ô“*IË”5!…ØÃ1§?·ª$æä©N[¦C”ä0ƒ £a€eääÙä|—0)ñ¼r ½0UP'Œ¥DYU�Üþ¹®‹í6)вv,ÔÐ0Ëhê=ÐÅŽ4�)ÊñßpqÝJ0 Ø2©�íÕaRb[&–iam“�ôU±$Nó)[x¥ S.¶ãR$ýZñ *Öä¤iL–+š­N½�GágoýÛ�¯£E!Ò4i6«êþí¨ ”eµV›ª“µÉùëü“+ëüW6xjqw«Á‚¬þÞ²-FiŠoY<~ü(ÿöµÓœÞèqG·Ã3××xêÚ NtÛ8Òà�Ï^àôÆ&�­,ó+ß~‘ņÏZ!•âÊ àtÀ—.^å¡•#|æÌ›|ùÍ‹,·š\B‚¼ È ¾|é߸ºÊùÁˆ—×Öyp±REÒŽ›¦iVDÜ æ´güܾ0}Xô}ŸRA’Ø–D°;‰SïgúÔ±Øv\0r%ñ¼ ¦‡½ûz•ÏyBg•‰Õ8IÖ‰ºÆ×ê¢Én÷£c±vŸÓ™YÕ†4dT^7Ò±ë„R,[1�µûÜß qŒÙ08zXBq§� ª“I™õðã/àÌFå1éÍ‘íããÄö2�÷”u¨Æ¤dYF§ÓÁ÷ýÛ"�i‘f�}Ó§Û¶kÍÑý„ñÃ0¬å¿ºÝn­I7ËHÒ”,©Hk†aà·IUÓÙò\–ìþÝÒPøvŽYö G˜¦‰×\")›D�Æò+«¬ýÌOÐÿÍ_aô…Ï0ø·Ÿ`óþ4ÙŸ~ª®d¿òÒ‹Üyç î¹ç.^}õuLÓÁv„a`JɃo˜§žü:ï}ßû‡´Z-FÃçΞ­¾£ÇŽcZ&Iáû>�N CJzè!.^8OžÅ”EN»Óáå—^bcýI£Šƒ7'ÛTxfDnGQÕ:´+Ò—V.J ‰ëz¤ñpG5¥"�ŤÉVÒbY¹r‰³ý«_ƒ°ò›÷Ì”á°"�µÛíºÍ>+À]˱hÒ—~v…0øø­!û¥‹|s3àË·†üè‹øT¯Â@×±qM»,Hóœû�,ó•Ë×ù�WNS(Å¿~ùt}Â?µ´À·W׸1 ±¤ÁçÞ¼D üéé7ùÃWÞ¥ØH2~ù›ÏÄ túMþøÌúiÎ7¯®òÔ•ëÜ5×áÉ+×q¤äÅ›ëõo0Ðï÷kÉ7=f/׌¶ÀΊ’²TØVµÉí'A6y ƒÁ &¾µ;su`ÌÓÑž !ÒP8²²ì�Ø?ðâ>ñ¼ûßN§Ó© ¨û2¬ÇÁ}41V>öºx�L�&öû}úýþLëµÛ87HJ�¢#nöy[#À“·‡²äžÆŽX)…‘^®”i:‡)‚5òÁe¬…SP¤„ƒŒ¨ñ0œú7 Ã�V«UkKÎú.h¤&}銶Æûî'Á¦7¶ÍÍMò<¯ÉóŽ$I)óJ¥$Ibš­9°æp¼N}è5ÅßM¥ à!áði’Ðh̓5G�nÅ¢½ï½šïÉ�xt«’^jÏa�»!Y2ªÛå˜ô¶�ˆ¥ß!M–ÑxMØÂRD»›I*e0ìßBJYZó,¥,w~WõŸ­ù®Y`3$ܪ…ö")MâLŽÕo \¯²…Ýn“.„ª¬çÙJH…¸^›(ÛIà�Q iVÐòªˆg&�mŽãÔ„q=ß²,”Rô0ø‘—.ñ×{¼­éóL/ä¯=ÿ&gÒê>$àH‰e’(äh»ÅãwãWWùòÅ+!xêê*Ç!H3ÞØèÓq²²ä[×n°™d¼¼z“¯®r×Â<¯Ü¼Å7.^&- >}ö"§7z\ì¹Ð¯ºwÏw8Új�½¤Š�ÚÐd»2‘¾·Y†~5©9Ióêù°äL*1:Ó±¸‚ýTq Žc${ïë–,ñ̈<ºÅp°Y'ë�N‡ùùùúÐrÐ~¯ßï×jU‘Ît®—ç9ëëëôz=ÌWƒbŸnȲ� )9l…t[)—�‰½§¥ãä8ì¦ø"§PÕ¦X)~ü%œ¹eT‘­½ŒµôÂôˆ7®xeÎOÝ”>qù¾ÏÜÜ\ öžuLêkÎ� $z¡Ú¤ôæ¦e|ß?�t–†Q C¨JW1©*¶úÅŠ£þ`}i(<#%M7Åö˜4æ3c¥vèæpëŸýSìcwÐþžQ F˜‡Wˆ_™Þoÿ&þc>ø0?ôWþSjóއàÆFŒi9ãÓ œ¼ó.,Ëæž{ïãä�wQïûî¿À•Ë—yü‰wáº.ÿÁþ%ÖnÞäñGï§ÝjE!sÝví\rçÉã¼ðâ+¼ÿýßÉÒáœ;wŽGÞñZ­v�\ï[ýTl\ňhaØ ÚÝʯ¼HGVE\rvÑSÈ ‰ß3RGCò<Ãó›(Õ¨ÖOÆ;Ö// ŒrZ ‹¬4�-6·~™4Yh¿á8[m0�ô³{iòË×øë'–ù¡cKJñ[oðÏÎßàï8‰·MòÁ•’®4@)s¾Çw�‹žG×µ¥ÓAj²­¥1¹óóó5Ña–MƲ¬ÊF:ÉPT5Žg3�­ö˜çyõ!CJ‰å¶‰ÆÂ]7wªN€*úô&ê!‚ÚzSŸüº‡É$Ý4MZ­V½Ql¯bjØ•aµûϬ­ôÝÆ…¸Šµwy!Kv‚PŠ;â��²šº\ˆ\²TFNú1Œ¦¦iBÞ§‘?�Õ]¡nR7±–î‡"%$̓Ø"ïèõêõz8ŽC§Ó™"ÎÍRA™”aÔ‡`& &½‰ê} ÓéHúÉòË40„À0JlF„ÃËíÔ RÕJßí„ß.(U5_Ú šíyâ¸"M$ÃU,‹³�¢ð¶tI-�8+±Í‚87Éö��ègP¯·[i¥çY†-Ö 7 Yg²,£ÕY ޲,Ä1«ý^C[²¬Ç(qj}à(¡ØB`¨Ýcb©¦Óªõ‹ÃQÓöðÇZíi:–ù ôA�ãØS„©Dˆé}\ëšî÷û !¦d>õ!Jw’>yé&©RüâÛï¤k›|x¹Ë¯œ_å·W7ùù‹SUD�À1 ²$Á1-\ÛæÎù.‡Ü-u…ï½çNþùÓÏñŸ¼ýiQ)‘\Ž(�S‡¸Ðëå9¶!9µ´ˆ�âÜF� ÍXi5± �# b‡øÏdΣ‰ÀÚ±î =Iß÷Éò’B)\ËD±¯ãöõÔI¨Æ±B¥º©.Q2Â5Ó=»Ë–,1UH$$¦�çm¹Vuuõ k˜”ÄžYµŸó‚=ׯ” :M‹¤Ø¹±'Ir s¯=yÒÏîM¿rñ&ßdž¯®÷y¶7äަϯnðÃGð¨ž'Siø˜¦ä�ïaJÁ£ËKœÝìsÇ\—C¾ (=�Ý6o?´ˆg™Ü·0Ç·¯Ýàm‹s´‡Õ0æ�+Ëœšëpnc“ã� Ÿ ›=N-Îq¤ÙdÉwqL“å†O˱iY[÷®]`€Zzo’t¦×a¯¡!@y©ÈòϱÈǘÔÛq—BÔV•:ZV…ÇMrƒ²È÷|�¤aà8{®†S?ׯ¨ŸY®aò¤‰P“­v­¬¥iôø³8”åʰne¹�e”ø²@¢hËŒE;#Ç *÷–ñkÈ‚SÍ5^7¡rZ~ŒiG`4)£>æÜ¨žç9Ÿí‡|ccÈɦCÓ”¬&_¹ÕC‚¡r«óm‹yÏ¥a™Ìy.K¾Ç²ïr¢Ýb3ÍxxeQ–t=—»çÚÜ7ßåžù*>ÏûiQðð¡\ÇÅï;v˜ŽcÑOžX9LT–t›·-t9ÔôYnx,x.GZ æ\§ÖdÕ<­$¢ñé»q#ö:L²”ÀsLúý>†aÜ6žU«)LÂLÛ­ÈàYŽi¤…�”•‡bÌ�PEF‰]«-éØ̼èX LÅb=\×Ý›¼C L Å¢•r§VÎc@‰`3w¸y ó�Áõ˜—r# /¸½@Û»‰á¶Ày;¤g¡ IzkŒ¬÷£Ì¹o ÆþÀãJªÆžÌJvÐ•Û êö¦¶¾›µÂ²ŸÔFšåز¤ãOŸ@’²A³=WU€zëÒÆË&%ñ¨bÎðõJ â\‚ôðýi’�&¾•’¾ü:«ÿàg±aùcÿ€�õë´>ø‚gžbô…ÏÐøî²ð÷þqnñÆé7yô±Ç‰ã˜4M�ýGVŽXÉ2 ƒg¾õM>ø¡ïåÕW^fi¡C·eUÕ ×n ˆâ”ãwÜYËìÄÁ-¤€_9Ë©ûOaZŽãŒ%cÂ=}Ú·�$7VÏó(Š‚0ì�>‹s¿µTù–ûØlµ¯KUýÝ0}<ϯß)rnõÚ6é‡6�¡ô}Ó4wˆ]»®K³Ù¬[IÛŸ©uÿñóùoOçR” P,Û6¿úæ5~ÿ‘“xjwÌ‘þn½þ“Ïþ^1mͨE®'+¥6¡ñ{{Ýk§Ó©¥mt€Ð߯ƒ‘~‡öò"o¶Z„q†<Ç¢×Û¬¯ÙóôsíŠ}�¼'݆6Qe�ë·°í­€Gª±åî•´¬00½ÅšH¥[ã“-»Ûééëfå0˜ç9Ývßõ§Ö `iié-•ow“[qbŽ9Qe¯Kµ6Air&ðÙ̬qøÁVÈ;¦(K@á?M£c ìEp„øE@öBû]{&¶ÛÇvù£Ye èݙ†Ã!¶m×1ùvˆ{ZvJ›8LšH¤YŽc–´½éw#.´ÆqxØ» ÂÄ�QFqûÈ A¦l\¯}àü$—x­CuLrÅp,ãhãùÕü·rÿ:1©p´–aËí{OåJ©”"ÞÂ[­§¹ ÀÅÛÇÑK¤;lSw¥„¹G»Ý©(i`Y=?+ g¾–BTéÆT·L˧Ùn ˲‰Â¢ŒØÄX¦¤Õ°ö”þÒ÷¿WõnRæSC¶�/ŽRþ§ó7ø[w­pvqÂwøÔõ ÞÓñøÛ‡;;ââö8¬;Y�Fc_ù@môPS×1HcH÷ŠÅº�¯”¢×ëÕÕúþu<פ¹í±XKAæe•£ø®M–&S.«³È˜Á´”Y0¸…!«ßpr?¸xù2ßüösÜ}â(�>p¥²ª$?qáÒlW…ÌÁ`€ã8u,Öò_ûAF·¥–c±‘¬„!RHî;zÒ�ôûý©„yG£¦P‚©C?·8æD,; ¶¨,Ú «©Ë¹Ð'·È,¡8éé€ Fz™¯b8Ç(†«H< ï ¬Æ!ìÛ¸™É6¡¶ÃÝ«M89´H»>É…aX·xn·Å¶*¡7ç²,hmÓô�s¯YU¿Â`„o%@B8 1¬­ö|õÝYPa”ösÛÉnµÈZµSY9·Œáûdׯ‘¾y†øÕ—�‹Kõ\ûä „Pܸ~�'¿þUîºënžzòX–ÉùóçyøáGPJqéâE}ü ^|á9šÍQ#}ìq^{õ�&›ŒF#6Ö×yõ•—8vüÝN‹‹.²xh‰Þf�$-‰¢JÞgåÈaÞxã ò<ç±Gî!Ë{C Çm!Ýâh„(ªÀ¼ßòÊ á8ô6בRŽýà›DÑ“¤b<[[† ² ¦\Ë ¾•Sª!á0DÚMšíÂ0ÄuBJ’}ÔAö‚ªè�uÒ-i·çhÑüÈñy>öâyŠñËn‚_¸oe×Ä6( ^½µÉ»/¸Õï£ Ér§=X¥”œí ¸i¡>¼é–ÕdPÖ,XešU" ¦“g�MßÍ1fòÝÓrz�N§&›M’F£A:ÁÊ�ãhê ªÝi&-wK�´w’$ªr´Ë£„a\)€X–E³ÕA©öX�bˆeLWsKáMPèî�ÞÚíöž›ƒaläëňŽ5G˨ȌQñüês|áìçY™;Ê÷?ø,ªE’(!Ê#li#ÅŸ]mfr\K\n¦‡ì„;ÝÛ(ñEÆCÍ!ë™ÃÅØe4~†›²`ÙI)ÇÆ9fôžŸ d—¼w³[éYã¿×oºš»�IÜd»÷ $MÇa 3H’d*I¹[õI§¨I[ô²(Øî­goÜ…‚ž•I˜QnEšÊs‚(À•É�•LK*,âp£ž_a8Â5“)<í¤‰Œ(B0Çóe5?Ç}K÷¿³EëDCl#Å’Š$—Øþ8ù ,±õ\Û¦B)ÊððÇNcA4Ä–ù¾InV|¿A0@™`˜ šc§±(☾îî$C\sz=µošôÅ®×À0|ܬ�edS† {Ýÿ^Na“{µº¿³åp¯oóS/�¯ÿ¿Sm�¿r¨½kbûôêóžË=�olö¹o¾KQ¼zm•£ÝÝ1TG»FyÁÍ0â±Ç�Ýá7ºúhš&kiÆ’ë Š­C«ÆHëûÑÏÆäýOÆb�'I2uØ«ð¹’<Ë�†�aˆz_˜|�£(ªßã½b±†zDáÏÎ�œ,ÊHw¬ˆ!yþåטëv¹óäÝdÊàʵ5ŽY@RTaΖK�îÄLÆâV«…ëº;ŠGB"qnxCHîk�Â,Çî¡Ñˆ³kçøÔ«È_}×_'ÌBúaW9\]çXû8i™V°„݈\ 6r›[©�4*¿f€¦ÌYv2LF¹äžFÌœYU…„JðãÏâÌ­P7€òÈF#Œö{êö ÞxnW™`{›p·d²½: Ìþóh±iFu©À”•C‹ŒJAi4qÇ2YÒ¯` b¬±Z$ÄIŠi¹8^“8”y†yÀ~¨[de‘%)¶íá."½pŽì›¸=BüÒóXGVPiJ92ÿ7þF£Á¾ô$¾ß`cc� ñ�ïÿnFÃ!››ØŽC¿·I»ÓáÊ¥K\8ÿ&išòÐÛæÆê*·ÖÖ¸pá<–eqßÛNqýÚ5|èíœ9ýQœ°~ëI’ò•/‰÷¼÷;ùÊ—¿HÃopöÌ>ôáïåêµ+Üw÷TBêy–§•2‚i7“Ja7‡¢ «*ƒEeö�¦Ô Ö÷ͨ�îá¨�#w¯ÄÕ-Æ2!Š*ÍæuÆògyž�e¼lÒ4GJË4¦Zõús`é(!Dí~VI'UU`ÃINšD¤Y‰0äV‹ÌòH‹J>É4q.ñs;à"ú7ÜÎHÖ¶ØeYb˜¸ú{üú™_áÓWÿ”§o=IÓmp‡i­0L‡¼ïÞ÷Óõ:ü“/ücZ |ëò7yæò·((øWÿço¿eX°£|U"&·2»r=”¦P4dΊ›b &okEø¢Â:ù�âi¬Ö2yÿ†7”¤¡@øÖk7 Üdûo¸]†Ñ¶í]eµ£NÊ&çïÖbœµØ ÷Ã0FyÈ‹²fö—JPÊf-ƒ•'ƒ:OÉ(bÒh¶È•E’ ›‚í2Œ’F³E6–a4„"-ÍZO7 †8Æ.±P‚´0)Jöm±Î²þeP*QËHFI‰0<üqÒœ„›;lÆ5ìË)qœ �U·MU‡^iì,6(i­�ÐóJ ‚(Å´l\¿M’µ�•T‡;>G­L‘&Iµþ�¦áQÆs IDAT푦·÷û‡QŒ!%�N»¶SÞo¦¾ø®ùo„)£”»¿þðIîîVÉm^œŒd9¾mókϽÂ÷Ýs×”üìŸäh»Å‘V“Ÿúü×y`iž[ÃëIÆñ¥.õ¼xs�Õ(f¹ÝâÕÕ›´-ÉÅÁˆ°(d—‹¾[ÅbàŸ>õ,®msr¾ËÙÞ€×Ö{üÉ™ |ç�wà¹î¹¼É¡Ÿmå«ßc�·Z-Ò¢¤(J|Ç"‰ã]ªƒ`°%e–¦)e:¨ $ãßPŠŒ4‰¢”‹W®ñÄ;áÐÒ"gÎ_áìùË Ã «+¤O£Ñª ·:Æë c±¾Í_QJñfr†ÿñåÄ×n|…'×¾Æ3ßä�¹i-LÃäx÷8/ßx™ÿ쉿ʯ~í—QÀëk¯q¹w™+ƒË|æ�Oï�Üê‘+ƒõ̦—[8²Ä1–(éš)+nÊœ] ÊPØáSøm‰*2ŠÑ5¬îÝdÃU†âq’¼*eOj©MÊDÌ2t’ªõÐt;™ñë’»vqÚþ€ì¥Í9+,ŽcÂ(f~®[%i±B©‚\™4Zsu²åÊé¤Ç0ÆÒEELœf¸n•äEI‰*K c•Õ �[Pä1q’ÓxôÝ$/>ËèKŸEe)é¥ ä7o°ô÷ûî䥠7øîïù W._¦Õj³²r”sgÎÐh51Ç.esss\½r…F£ÉÂâ"‡&IVWWi6›ÌÍÍsüŽãÁ�×^y™î\—,MɳœCˇyìñ'¸±ºŠçz´Z-ŽßqŒžß÷¸ëÄ ‡3…'Î�F«’|J „˜ÖUŒs ¿Ñ&û8¦Ö6UXFJ%(!iµÚuu¾ÌF:�J\²,%K#\ÏÇu½Û’ÿº[ØeLcì,£uN÷ Ìx¢ér1Éx´åów�vIÇæn£Á/}ëy^ߨäôF�“�6+MŸ³½g7zqì0¶®óöÊkQu˜Z�™ñ\^¿»Ì(޹Ýòµk·8Ôlf9ðîúQL˜çüÁ{—xt®ËŸ^¸ÆzœÐ°m:ŽÅåÞ€®ïòþò*'Ú-~÷í Ì7.¯÷ø�³�Ôž½r¢­ *Ž©nœ¡k蚨–Û>‹-…ASdY!Ѩ‡mlIŽ©ö󲄇Ž� ŒÂÊs4âÞò ‡óæ»I²‚CT˜|«|ØÆAµ(ŠM±8*C~ó�ÿ�•x™ÿè៤mµy{í-îÆ‹|l*Ý/ß~‰Ožú$oßy›{úÇxçî;Ø–M^äºq?,a»!�õܤ7l1c¦œöF8Z‰IA9YËZ¾‚«ßA3‘­¾‹Ù:A™…Dù<¥¿8A†˜¦Yod * p4ûªÌ­˜ÕªÍY–eÍöÞΊNÍ6µ›Ui~?Á5Ë Û$ ×@³ñƒEáÕ¸¨4Mwd”BµAë"! W‘š‹ë5(KŸ(bééžIš©K -!A£ùKÿ5ƒþ¤ßGØ�¿÷Yœgž¨�fžîiÂ$ãù�~€¢,ùÔ÷}¦:ý—%OèOãygÎ>¾©ÍÜlµ9ýÈcH)'ÕÓ�';Ê“gO€¬Ú›Yi`ØÍZjm4bë†ÈyêÜ)�%²¼¿"a%’1á0AF,I‰¯&@3¼ÊÉk#Ïʉò�н®ëdF@˜VP��ÈFY©#t³bjq˜Sˆk0¿Q¯?å3¾Ž4Ob~¼iãë‚t¢ßÇ1£Ñ˜¹Àçû=Å¿~ímN´[œœT‚gâ_¾ö6OÏ$+K$ðéÇøß^~“RJ>utžWnßåÊz�Gf:mÄyÁGq{0âãGçù“ W¸°Öãö`„mè¼½¼ÊñV“O[àÕ;÷Xh·ö%£†j3+Áp!¦¡cú®rOÓc;È‘byÇáߺÿ9J Ia¡`RÄ!ÃØ¯\Ž&òajìÖrBÔUwUAv‡7{¯QÊ’s3Oò½G¾�?½ñGÜÝäíÞ;p€vÐÆÔ-ò,Ç(un¬Ü Î"à»þ¾ïáãBçý0`)µ9î†4ô“œ|:õä*Ž“ ´ÅðæÜ9Šh…H{©·‘SЕX†A³Ù¬ûU~P°-•$´Z­z³RݹÝÖÂÖ8ÞétjL÷Þq8Çu,ÊlÀ(5«ê¡ãÕ��4MeÄNH‘J £(z„# Ó0œÙê˜%�w/ YY ™é¸œ;ÝAÕq2tt½ (úŒ‡&Ž×¬±ˆqâš$H $�•e ß|é®câ:Õ9æÄ±.ž·!¾¯Šo¢0—k½˜‹—î’e9'�wpƒãóY9&ME-A(ÒD9 šVŸêû®šI`H²lÀxh`9†;CŽÐdˆmHriciÚ¶û™©KJ¹‘L–e‰ßè�èÄXÆöqXJ�ÔD×Ê£A‚a¹5ìo/;ç–‘a™2Ÿý~Ïój…£íöñ²,±â�_Zhq¬Ì(Ë Ù;½çæR–’8M8Ü ð'-UK×9Ü øâÅ«œ™›¡'U¬î´øÚµ›ô�ió­ÛwI‹‚‡:M§gÚò=™é ä²Ú[^¼}�¤(¸; Y�977Ã#Ý«q‚6õ>íçýSyŽR–PëÏ2 ìZÍžBTd_%‡ÚjUòyÕ»œV¥ï-#+Rs‘EÊ¡ŽF–¹·´D³ÑÀ²LÊRrùÚ VVט?x�£ ‡vÄ+­sUpìë,†w05“gf?ÄQÿµø\è¿O&S,*ÌîBk�²,iY-†Ã!Ký{è†ÎŒ?Ë|{~Ém}3�•Ìb0ló¨7fÖܨNÚÑ·1:³”qaw–O¼r‡Ìÿn˜:;× ôÉŨ‡¢ª­ûÝdÕ©C鹩°/颭›k;:›¨QL’Ë×,�2$¦žsR}Ã{�UpÌ)ÇÄãô*ÉK“„q2Â1vf„Wó%ŽY�Ì4éþÒçXúÇŸöOþg8Ÿø>Âd„®U-0õ Ó‡†é*JY–Ü»w�ÖDŸU2 CŸ8“H -ÇЊ*PO}ÎêZˆÐ"ž¼\kk«ô×V8~¤»�ÀÉÔï§JRK9d<ŒÐM¿Ñ%Ž#¢$ä¦áóõUò³VÉOtÖ ÌÈK ÛU fH1íNs–0#²Ê'}kh(p Hk¿lC—$鎫Õj‘¦é®rE¦±ÁâWXN…Çn·ÛÛ&¦iòt ë?WÏOдLDYò¾ô=v„•(Æž$w³žÃ‰N›+k=>qü0o/­à›¿ýê[<}hŽ(Ïù�×ß!°Mž[8Äk‹K<2Ó©*RrÐ÷04�ƒAuú¾°ºÎç>ñ<øã‹W9àW‹†mq{0âp3¨,,÷i 2×u+é¯Râ9V]M}v¥M©>×ñŒC°ôl0-4 Ó!OC\{Ò�#’qD¢ûø˜ °ézvÓ²Cº¨Ü‹žê>ƒ­;|höYnŽn¢‹*Lº®Ë÷=öò,g<óôü3hhi¥4 Ü`×ïúÿkR°œY¬d�Cæ&qXÈ7ýúìQòÑ2F»:dÆ¡AÞ8wßç(ù;%˜>]l˜Æüíg(“Œ Ø ÷:0NÇñ(Š‚ Æ¶ïTlPŸiê¢êìP�Æ ‰ðiµª {Gì¬+±1*Æ„4ÉÇU|Fã!:wî ùÌ'Žsþò:Wï$\¹¶ÊÉã]®ßêÑi9X¶Á¡¹€óWoqp®Åhœ°¼2 ŽS>üä¬mÇËÑ4 ÇqxÁ,êê¹ZOÌuù·ï_!ø;Ï=Í Û�<;×åùÇp ƒ9ßeÎóø‹ë7¹²ÞçÇΜâO/]ãk×nqîà,®Üè�8s`އ»L!Xhú*}Ü\JŽ´|÷‘C,E1WûC7}Ú–E?N('Ïóö-ù¥Þ‡²,É‹’(É0 Ç2j2öƒÂŽÔ›¦Ié´ˆ’–žmÊIÒÂDè`낪+ûÂ3'ÉJ“Wß½B§ÓF®­sö±GùËo~ßwéL gê7OÅ‹¨«ÑšÅ‚¿ÀaÿsÎ:v‡¢,j×VÏóøé�ý—ŒF#>uâ{ɲŒ¿ùôŽÐ®S‘—Åï�—r{Цáê%Ï·‡h“Êœž\¦©¿ŽAê'ù%òáM|œÒ:²ãç¨�P×õº4}Ù¯€3PŸ8Tò< ²ÞÏPzrÊYm;ÒY§Ø¶‰kØF> NbÂU‹(ŽÂ ›;ß—2B!I¦cض]1NÉÇXÆîóÃÜÃ0múÿûoÁ¿÷CGŽãM,�+àvÈx4¢;3Ë`"ˆì8KËK:4O¿×ãå—¾ÃG>ö1Âñ˜ÃGŽâº._øÿ˜µµ5¾ïûϵÈÓ�ÑhÈ¡ƒs,.Þ£;3Ëïÿþòc?ñÓëõ<ËöøÂ¾ÈG>ú1ò<§Õ¨ 'šPÖºݶÛ�©Š²rìq¼Š‰ù�.h:üëEøÑƒðÅexõÙdcRé4»”eɨ¿Œkf¤…FZX¸Þû5¡É¸ÖuÌK�Bk Mª¶R Æ™IQnðJYY÷)ÆïV,­”•AˆiVŽ[Ó©iU`ßk¯j5V®zj¾zÉ·ÛØU;S±Ðz½^ýY f£ªÊªE¥þ[ý;5ºÝ.RJÖ××ë–�‚óìgA€n˜ÄiŽeX†¶ Ï¥™Ó‰ëvC½»EQ°¾¾ŽeYxžW3³½bôŽ3·" ÷ãúÂ̦ٞ­ÿ÷tÒ”$ɾbÉ[£7ø_ÞÿŸ9ì扙§yuùeÖ’Uþ›'~…ûpÍüïõzõ�QÝO)%BÌtg0šVc;µ„½FÇÌyª9DLÚgæøÛ4‚u4gô#�½OÚ»ÍÐú~¤¾³B�‚Š©5¤Ì+â8®!ûŠð£°¼R †ý1Ó£8Á±­Ê8ÁÌH²B Ù38ŽS'Eyž“Æ#,-Ý“4Uñ&Í5 ááy>ÃQÌ«¯_á…Ï!¥àÒ�f}^{‘O¿ð_ÿÖu5¹µ8"JJºŸ†oÑj9\¿ÙC�#Â0ã»>z]ÓøÒ×ÞçðB›À·ŽNŸåÖ�u–WG|ì¹Ü^ìa:­¦Ïòê�ÅÅŸøØ dYÒ뇬÷BΞjñWß¹ƒëZ|ôùGùò×ßããÏŸâ•×.rwiȱ#³dy‰ãXäYÊ#'Z̶ �ò—%·ïn´™…€gŸðxîlŠ,%I®!µ*É]* þæðñ|e~d%ÿøàâÜÀo¨¬¾ë8Z…Ns tÇõ+žJ<Æ6ªŽd)) D׉876ÊäT‚·“²Æ´RÍVã)e?”4Ô~ìê=V]5_Á°¦çO'hJ©FiÐ !êX:-M¦~›ŠKês¤”µ–lEŒÇãZYf/5ƒéßÞét“œ¢(ixމ@]´Ê² Ã4‘“nÀ}뢼cÛvÝeŸÎ‰òt„­çd…†Ô}dâY›çR ½ƒçy|ùëI«Ù¤ÝjrñòU¾ç…�òâ+¯á¹.�<|bǸRŠ’ÿõÒoñ~ÿ]mŸa˜ ¸9ºÁ÷ÌšŸXø)lË®IÁjÿS¤<õ\r‘cŒ£œÀ{ .Gœƒœ@æØékè3‘²ÙM  κ”þá]?GahÕ©­ÙlÖlPõ°÷»Ñ*fŸJ,¦•ûª ¿ê —2lЄ -ô‰Ï½$Ç&˜àTÂÑR‚å40œJ<[æÎ7ºÓÐE%ž]}FÃÛ 0œ™I‹,Æ6‹û*‘i®á¸~µqÿà� ûòuÂaŠéTUì¯~åKAÀ¥‹«My}�îÌ,³³³|íí·)Ê‚4M¹qí*/\à¹ç?Êá£ã3Ï*I‰Æ_óEææPbò�o¼HsRyïÝwxëÍ7ð�ÿäǘ²ÈÉóœW^~‰á`€i™<ûìó|í«_!Ï3þÁ/þ 2¿ÿyêš$°SFá˪”OtaTÂÏ�o­O®¹Ð°ý‰EÖÆ–^bê1Yœ’P‰�ƒG1µ„R¸P&UšÊõl:±UÏ+ÔEUî¦Ç²¼o]“ÛµØw“ŒQC�¸�º½¶ÛÆ®*‘µ$Д¦ÒÔìÕò-ÓlÞé ¢þ¬ðê*yÙmío7”^aeS –¡1 BÔLXÓ4y÷üÞ¿x™sge®ÛÙ6 ©C�zW§[Ý3;&ŽB,ÛDæ£ûÞ¥¼SÕü¨f«MP1û÷Ú(Î5žäoùþàÆïsuxS3ù¹Çÿ>§º§abN£ðcŠñ{oþ.º¦ã™.›ó;~ö^# \÷ÁÔöc4YAVD1Ä×ѬÃHTò‹hDœFNðÂ; uøQÊ0*«ÃÞ~�<V*Ù…ãSª"ûŶïd"•Ϋ®W‡¨¼Ô�²JÐ ÜIW%‰z‚ ‚=ÅŒâ1¾•í‡lª”cÂQŒixDIÉâªàý‹‹[h¢ ‰ï™\»= /JÎ5xýÝežûÐ Î_¸Ã\×ÂÖsd™Óí6@Ï;zõ[Ì5xúÜRJÞxg‘0JŽªƒô½å!—®,óÔ¹#\¹¶D§í“åe½~•-»„&Bò,t]#KctMÐí¸šs(¥ÎZ/Æõ\î,§x®Oo¤±´šð‘gZŒÆýQN––¼u>â¹Ç„H&&Eá‘ç‚¶¡ó“ó`ipƒkcÈK�nmp4YÅCmÒQ”rD8ŒÑÌJ3IbÂd„¦éL­*Š¥FºE)AÅ«í*±wc“»Ø´×o·~”<£Še{ÉÂF\œž¯œ ·B%¦1ë5ÞxR Sݼ�béô;¤$¾¦]a³²Œzö»=Ï«]!˨!ªÍ¯ÿè _¶-žzü,¶u?¼¯îW’$di„íxø�â8"/2DQàlƒ¯NrƒFP�ÑŸæ)†£1¯^ãù= B0�0M“ W®rúä‰Z“{zhRãoŸüþ§÷þ{^]y€g<Ïß:óÓ˜¥U™Ó°zv£ƒ?xçßÒõº<}ø¾sýEôÿðþéç…m?%FÀÒ$O4Çõ�2’÷ð¼!Bw‘¥…¦…”¢ƒÞù y±·G»øžÆÁ†QožÊJw7<®:i)έdUEØ/ilÚ­I‰‡GQŒ;q)qŒS/É ËíL—C VÒ(yBœd•t‘å&A±'iL›€õó<%I <¿�nºŒ£AQ›H ÇÇó«J¥0 ÏÄ2u ­ Œ Ò,çڕ˸žGQäõiÌqfgg¹wï.†aTV…Ý.�F“¥¥»öš¦Õ›±ªæªJ°kVɰÂîªê¯:�=¨2ÂvkyÅØVõ[]3ÇÐî�Ã& dª5©MHWq¦‘ç“8¼[’[“®Rl²>ÈxòÜq‚ÀÅÔJŽÎû Ç)��êb˜g;ÆÁ¹&smÁLKÃs5θt:ñ´¸<ÐAI«éÔq¼Óöh7lÛàÈá6ÍÀan6ÀÐ5Žn'Ÿ˜Á¶«}Ì´,|ÏÀ±Jº�€ù…ƒ8ŽE«iØ)f\>Þf0L8|С۶ÈÒœÇçð‚&½\¼óÜ“Mf:&�Ÿöi¯„<{ÎYTø`ébZ6kQ—{-Î6àüJ)yÖÍjÆiU“­÷™+Ò�ßaP¤cL­*öĹA!··¯T‰àt,lZÇ2ï3lØi¾"�+ØÍ~Éã[ןš¿5–*ÒäVYD5ö2£R•Ò­Ÿ1½åy^C7·#?뺎$YŽ”àÚƒÁ ÎyÔ=ºtõ:wîÞåCO?I»Õ¤?’å9�@ÇÖ3ŠR4Zõ§~‹&$�•!Ê„(N0M?(ŠŒ¢ÌÑõ»T”ÝnašÏÇ0t:ˆmY|ó;/37;KǼwáçÎ<ºmr ` ‹§»æ­þëpðÏþ#ÚNe©¬öÄéýbi|ßøÔ©ïåk—¾Ê8 Ñäç«äV×´]_x5öcZzZ�˜eŠ—þ5V0CÑ»ˆÐ£µ‘8ZŸÔ†²-áfÒ’U,=Å6UÉ©ú;µ1m·Ñ*‹ÇéÄãßeq«¿WòGšnà8vUÉ,˺ŸJϯ>7 {X““ŒR6ÈÒˆ4Ï @wöáT&�ØèÏ¿JòÅ?!zå;¡Ñ<ù0…pˆÓAI!uœ‰ÜQްõdêp"H'–²i–ñÞ;oñ©O†v»ÃÉS§8þÐIúý>Ï|øYÚ�³³³èyžñØÙÇ8rôžïóðÉã†Áüá#èºÉçO£ �3gçÈÑ£Y8@žÅÌv›Ø¦Fwf†ùC3èºÎcgá¡cG-š­v¥n h”ÛB-6¼šxüá’à/Váëk0*àïµäI8b‰xÛõª’\]L*¬Â¨TÊÊQ¥(5òr÷ ™ ¬ŠEÚn5k6øN-[çO«z<¨+Ìv»bSïPÕü­�U…¨ä¶¦[~[çÂÆÚß.°Z–…ëyÄiu=ޥײ{jäyÎíÅ»èºÎCÇŽppnŽ7Þy�+×o kÇ61Mϯ~Ëp8@•榩•èª*/‹I`µ\LÛ'Î$eY‘Ó\ÇrÛUb†¤YŠÂø«Í±‚ÅlÈ_©à¸mk®”2±\ÜåÉ™GÑs“ÀÙÀŠ©¹ îñ¥ ƽÑ=lÃbÆ›å÷þ�ßû@Éíðsÿí糪JÞÿ÷𨠠àLbOìXE1Â/^ÂðgÉVÞFw«–ápä•3{*ÃL¯MEJq]·î|)蘪ìì´–§û¦+aêÀ¦äÁÔú|Lvš¦ þ¤]]9žY€€Túx“ÃZõ°jÓ‹ Å8I1mË ˆSÈ‹ü~[óû¾»ªä6�¼(p]Ûñ‰³’† †¶×Áqª¶´k„z•˜FÕ]³«úó¤Ñhøõs0&®NžkN,h®càØ• mà[˜†eBžÅ˜VÕ‰H2‰Ð Z­æD¬²¦¶ ÐEI§i`hË�t[:B¦„qŽe™\¾‘°´’ñîÅ1ï\³ºž1ÛÑ8}¬2þHs Ëi’Ä!Âü‹[6__…¯®ÀkøÞYx¾kM¤ãûTM¶®ÙZ†1.*§AÛ#J+§´´Ø»c¬b¡rjSUÛÝT¦çÂF±B‘öö«�£>c;ùBÃ06Ôöúþíb©2ˆR‰ÚvóU S2”†aÔD 2�(!+J\«*2l…Ô•Oºé– IDATeÉ[ï¾ÏÉãÇ8²0�i|ùëTï�ï:è–_«UýU,¶ôê0¨žež'Dq†e»˜IOd¥ö”–•4Àp8¨£Ú{:­k½q’ð]}ž¹Ù™]¥ð´\CH8Üœ#Õ8²Aà4ë{3}/Öâ5¾xþ ’Ÿ8ù Î/ŸGüîû¥ìSIjØrªÂ£Ižl†Ü-VRO/yv kkF¯Ópn" —lí<ÖÜ“¤½[ íF*�Ò-U¢íNíª*RµXôÚþT-õPÕB™ÆáL‹²+üÅÖ1�‰âÁb¤”DIJ3ðkœO Bâ5f+÷ªÑC·5eP8$©W8®4MI¢!ž•ÝŠåK¿þk¤×¯ÕÿŸ0 üÏü š?ûY,ÛžàK‚ j}Ç£•º=/¥ .ôºÕ“gIšÐh4ë—QáèöNÐ$�•"�¤…ŽÔ<\¯òˆB0ö°ÄhO‡ŸRV­ aTªI‘¥cÜ)§²¢d¢J E¾N¬¹,Výº„£Þ†Ñ@} ÏÚŸÓY)!Ê-,;x w¥é133SoÒÊäA0„»9�íg¾¢>(*Üíúúú¾ç뺾InIÔõõõ=ÇvÈf³YévæžmÖ8[‡¦iôU{Ñ÷\†£1—¯]çãÏ=Ë·_y…f£Á‡Ÿz²Ú<Â5¬ Ž}§‘äÚD¿1Id Y´Zmʲ`Ô_ÁÔ ¢lÃÊsz(«Ú4Ô³œeYr7¼Á]y¡å<Ý|†¹öI aÕÿFÁ>~çåßæ§Ÿýi4t¾}ë[üðÓÿþÂÜþîù2 S÷‘nÉjé�•Ó1so†¼=ðèg]+çéÆ�r‚µµ‡_&èèȤO™ô1Ú'‰W?B«ÛŸÛ9|©1]Xp&²RY–Õê°as ÷;$¹®[y¤éž‰Çƒ:�U¿¢8&ð½É3¬Œrd™ÓhÍ`£á“íã0(§±J€$Ñxˆ± éi§‘‚\:Ýåâ•{¾Åé‡#eI8\©q¤¥¬¹²B6H:JÉAV£(æü¥%~¨‹póö€…C t}3FÓ3SÆQÁ8’´ZnmK®ö2™ Ð5Ht­¬²rup›úsVè\¹còõ#Šb’?ü=m¿ê¤„™‹4IÆËØ&$šWw¹ª‚–Iß$˜ýšŒö”ÿR#ɵ >âUQ÷BáA‹¢¨×ñƒÄòé÷àAL4¦ç«ç§:ƒÁ`ßn‡[ó!uhì÷ûûÒ:žæm$¼ ¤ªœ{޹£K›ŠÅÝv‹ÕõaS”ÇæK_û ¾û…�1ðãQSŽ{ì­i®‘ Ï«ˆøI4Âõ«ç…!dë af"§b±‚qÑh´&EųÚJ¢KÒ˜ÙyF¬Ñ0 ži Ûñ&ZÇß¾ó‹ƒ;|×ñO"„àw^ùmô_øùŸý¼æx8¢äL3&G'*ªVÁÃ^¼“pÐN˜³sæ� [dÕ"/cÜô¯0›ÈW/`¶O Ë”qzˆÂ~¸¾•(s…­mV•ÔªEšeY]ÍU•³�t)U%«ªüì¬ 7]‰S. –eíë—f9æ„Y™Æc4MÇ šh†] ˜Ic—!&âßTÕh4‹ Ñ"Îôͺ‚%¬þ‹ß¡ÓüÔgp=‡sö ì§~á±æÉçf0 ߯•ñhˆ)4Q‘ÒÆ©µ©*©Mùº«J eY;¶«§‡¡ÉM³ÈŒ¼4ëÍ-I3м@ÛFü{ëõ›zYU¢ôq½&Q*(Ê]ÈÊF7h‘Ltm-2:Z\ÿÇ,*s]q½€(äE‰±�ï·ô‚"KG®ë=�¾±ræŠã˜^¯·k»ûïßê dþ´ŽáƒÌߪI ìÛÊz+Ô¢’‰Ñ*ázMÃ6õûHjH)±- Ë4I’„ —¯Ô€w—–Ç `8è3Û²¶%NC“XZFÅH©á­:�†8z„ ’]’Û$·Óm:¥ñºö$…äžy¬œÇì!"{b!Þ½¢ªþº®Ë±Îq,ªwìØ�ãü³ßøg¨rû‹ïï~®4=S'ƒC‡ñÄ}LåÌ;)m³`ÞIљڲ%|ñ>ºÓ ë]Æè¶ãaû3Œ3—¼ØûÅ2t‰gFäñQ8Âóø~P=Ïm¦çÒÆq*cŽ,Uò?FL¯ŽGiÏ�aœ:¤Åö‰Q)+… uýʧ;Š"�F]ÍÜ:¼‰™’ÖÚíþ©{¯¡’DU}hµZ5Žl·1­(°Ýü½¾[U)JAxÚív}bÞkxžG)+mNÓ¨p�ÊÂv·ùš�ØzeJÃ.xü‘“$iÊÜL—+×opúá“\»³BR¼øæU–û »f@&Mìɺ˜–ƒíÏ�Ks_×¢y½^¯>P·Z-¶sZ{–ÃqŠãÛÈ<"^»K”vˆï] Ý„ä<„�%î�Ì®]Ã6#vÛö‹º‰lœí4ó%ŠxÌq7æ!/Á ¯á@eY¢O÷Z¾ŒcöšI1¾‹Þ8L>^!±ž¹¯À0=Ô{¼¾¾Ž¦iÌÌÌÔ°/ ¶)Ÿ^Ó*þª ¯š?­bÛ6¾_mr[ãîô|õwê]ìõzÄqL«Õª×ùô¿I³œ–_iÚº芔ºéÕÆ9EQ4gIeƒ8×Ùk hbB fÈh°Š”¿9K"“VêN7Ocy-á»_xŒ~?b}}@Yf ÀqÜÊ^7ɇ)÷–†¸®U%æ�j2 �‡OÌb™Õótm“²,XY1ÓmÐÄô!¦Y]C#p1&�C?°Ñt�þ fyu€¡ƒgWdeÛ®`gç/¯c;E)qƒÀ³Øº.³4æÌñ˜…î½X'® „F»Ý!Ï3@ÜëÉJc¢Bq4ÂÒr<3ÇCƃ•*ÉkÎPhMâÜ ÜáþÅ[ÔÔ> ÐétêNãô°,«>|N·®§×O–e´Ûíó€�F’$ôz=t]¿/Ùn¨®±ê>¨÷@±ãïŸJQDí+£Ñ¨îv:�¢¹×ð}B"“8¦QË’íµ9FŽ!ª"ÝÂ\ƒs³ †#Ž^@×uÖú}l¯Å¥Û¾õúeÆ l»1OF)Å&½ãÁ ‡ëùXÞ Rì/'QJAý~¿.¼´ÛmN7žâ¤þ$ÇâUÌæ<Åø.I˜‘$.ñÝ7)†7 y¯âèË”EÄxí]LmHôÿçÓ?þù?^ã{Ï4+L¤_ 8䔵Œ=‚Jìݵ m™�!�ÑýCäržP;s_µ`§¡@ÖŽãlª )âΕkkt»>‹÷FX–É­;}îÜÐjzÜ^ðÞ…»,­ Éó‚·Þ½Íz?¦Ñð¹·< TeuMÓxû½{˜k0&d¹Ä¶õM�SI;©JÜ¥++´šwîñ\›¦§äW$�›wS,×DZMÂQ�2Vr-^€Ð¢d-.MC+H“„4+°L›è/ÿœbmóÈ1Âï|“b<Æèοó&ös%øØ÷`Fuâ,†XzFœÄäAcß-uíÛ±™Õ|Ë(q&XÞ¤pjvlö±ô¬jOìt³|¿A)ìšô¶Ûõ &-*-#Jµš4˜—&IV¢ìxÓBÃt*²PEhrƒ¼`hS¯|Þ‹²òY—šøÄWóóIržlÃÊ�®bMŸÆÕõO¹”ÄՃܿýVöR6jÝCE´œž¿µ²SõaÃÚv£u¥N͊ذ[ZU8’¬@–×1 ‡›Üqvª‚+V;L,=Ý6� `¦ÓÂ24nÞ¾Ë3OæÀl“$­Ôš ›NSǶtÞFüÓu×ÇÖ±-S/‰’Ó¬¹ª#'Ë ]«˜RkÔ„£<Þ°VÕ»Šô™a˜6¶Tv¾Y�¡A‰ ”Z%ýµM›z»ŽâvØi¢âÖùyž×\���©±Ò™ú-ªj»õ÷gYVcbwŠ…Ó˜ëi™Ê‡öÓQt]Ý0I³S×Ñ5X__ßrT·�¸Ö†²ERØøA“ùƒ1Üwâ$ãØ‘Ã¼ñ2s³sœ¿|ƒf£‰koŸ×%…�ë·&å!¶IUN2ûØkLïÇÓÔ¦ÓÁ2‡b„¦c9–g£;M�9”9BÓ¡¸‹¥ 1òEDy‹R;„îþ§çóƒ¼à›ë#Nù.'ÌJÐ;IâM­;EFJÌ១»eº×F¦C ÷ã¸ÁÌ·i§™€ªb›eÃqÊÚzÄ�Ûk4—Þ äá³¼öÖ-Î�™çÞòˆs�a0S”%Ož=‚i\º²Ì#§jXÃ7_ºJšM£(J–VF\»±N³á’f%‹÷†´›‹÷ú˜†dy-D¢qùê ÇŽtyåÍ» G9~àó�—î��ïrãæ ³-‰¥—"«’¼B#XÄI¾g‹F0Á㊌BÈÁ˜ô½w°yŒôÚ4×C¤×¯Ðúùÿ ïP¥£YyxWš�ºq*ˆ¶ÈG=yšÍ¬Ú%eQ oR`Ø•yÁvìØÊN¸»–x~ƒ\š¤Y¶ Ö°ÝHs Ûm…#²d„ÐL<¿A–ëdyN‰ƒTÄÄp¼~qAÝ?�Êç] Ïo’•q’“&Yy¿¦íÖëWë´Ï½çy{ªìvÿ>|;¨ƒz¦À}Šjì*Ñl6ëϘþnØØu]„VáÄ-SG k‚¨Jð•…ît0Ó„Ä�t=$P׫F²¸G·a0`Ë´xã�ó”eI£Ùä�ó—9qâ4EžÝ9I ?¨,"ÃÑ:¶‘£k ‹Œ0J@l<ÇýnpJo[‘F´â ”£ª[fÈ,B6º;‹f7@(]¾Ý(€äˆ_ù�õ�’ÛÙ¿ý³Ÿ+JÌû#86�Y*'5éM% a’Ïc&ßD³ÛÈÒD÷Úäãurïü õ�à2ŠÑ­Ö€êL]ººÂÒò�CZܹÛãCOå•×nòÌ“G‡9'ŽÏ¢i’µõ1zê83�o¾s‹'ίµœÏ_ZæúÍUæµ¹·4$Žs.\^¦Ñp¹·<¢?Hp]›·Öp� ‡wõÆ:·îôxäá¼þÎ=nÜ23ÓäÛ¯Þ%Éuò¢äÎâ:sm¢u]ÉÆiQº˜.á~‹ L½ Ï¢ ÜÁ°|¸Dʲ^ƒIaÓ�érx¾ƒïJ<3a~ÎŲ-‚Àe¶ë2ÓqiµŽ,th–¥aš• Æl×£Ûvk¼f#¨ÛÀs 4!90pìè�ÀE–9­@Ã1K,·Å¡ƒ]„,9Ø)9|ÀÆ2JÍ:8–ä`פÝÐi·=fg;´Û Ò,'pÙ$C)„@×õÚ¥«(5„Pä9eÚ#Ír\/@3<¤$/A£20û¸FrÿýÓ*nC™'5\¤‚�•¤$…¹+\h§X¤ð˜Ê�a¯ùû‘2ÜiþV¨ÃVØ×4ÿb«"ÁÖ÷¨Ñhl s]·–2›ÖÞÝ [›Ž¥ÓóÕ~�äeYAÃb;ÈÑô>dæ¤È�–×A×u¢(ÄÕCžÁü�.×nÞ!¸rý&£0¢Ñl•ŒçôÞ/%H£YÇ ™öª|F—"e0ÎÐuã�Š!¥ÄÐZ~d‰ÌcÊt„LCÐt4«�f¸U,€ŒÐ ¢„2Dÿ­_ÿµÏÿâÉ–’Œ·ÇO:-Ì|TëCŽF#F£Ñ¤ÕÚú—pZ2/Ð�(C¢AÆH<ú� 3꡵3‰ï¼øòuÎ>6O¥4›NËåÚõU?s”;wûÌÍzŒÃ„wÞ[d0Šh5m–VÇœ:q°~1nÞ^ÃuLzýÃиz}…†o3Æôû×n®1S\ǪO×_ùËó|×GNsåú*�žžcñn:´š>õÍóø�ÙÓ\¼²Âñ£3MGÊÊ0áÚÍšaqæÑ#üÙWßæè|ƒùÙªÂ/P2Œ9i‘—¢RÖÀ"ÙGG‰©ù¤³BwHÒ*™0�V}ÐŨ–uì¶,2YU%·ø¦; ûRJŠtŒ”lt¤2ÍÉ�®‡¥oÏÐ×D¥õ�&IýûÑœ vÆLàù-Òx8i[çÄILQ€ç7°¯&xkEׂ�®ÉêàV±Çõ s÷Jäô˜Nò`ƒ»_…#õ÷»Iîçû·j�Oÿ–�Š [ç«9ÓÕËi¼îNó·ÆÒéŽb•`[ Á2uЉjÉV¨Ïv=‰ÀšßS|\·ª�§QS/*µ QÒn¸¬¬ö|Ÿ²”|üùg±l�Bª÷¨ŠÅIaáO=ãQ¯‚žMF…­666T†-Ï«NŒf#×N€~�0Ÿg,O� ’¨Dæ£ñÂhA9 @ ä#.Jþ|¹Ï‡; ¾¹¼ÎŒbëV� ±m!«««˜"æ �!p(¢;h†A:X%v?S?”uiš~¨›�Aš,Ì·i6¶©síæ®káºöYZÝÌC›?Ò!Ërz½·ï¬2Ómâ8áá¹gNòÿ›™éú˜¦ÎL7 ÝrùæKWxü±^yý:�ž:PýM§Órñ= ]“ŒÃ’4UUЭp†'�Ïróö‡º3\a24å4V#L;Àôg§œÊ*;Øï�©1÷ÙÀÊo„/¿„°,f~î³ØŸþÁºª�'#LsJO´¼Ÿ4ò 'jXz‰kfäYÆ8µ°�F­{9�kɱ�†&À3s²¬Ç8±°œÃ�% G2Ä2¦·À°<’8ªu)5Q9•%É:EÑܤïš!v%í•R�•Ú¦ë÷}¿®tmwýº¦!îÞ¡\¾‡ð|ÄC'É…¶ÉƯÙl>�tÜvß¿ßû¯Þ!…Io4”ºÉ×Ö`�Ȳ >ì&¸ÅýNoÛÍÏó¼ÆÔîåW®f[Ý¡L³ÂoêB`ZF]ÝØîûUUÃó¼Ú�'+2Íð7’æKËCt]cvƧÝòé RµYY­ôW׆ôÇ�u�Sš�Í`˜ÐlXHBÓX_Óm»JnÜZçÿcí½cmËîû¾ÏÚ½œzÛëoæ½éC‡CrX%R”hZbbKöJdY±cÉ€àÀq @¤cÂcA°bHAœâÈ $ˆ`«!–#±H¤XfXf†Sß›÷æõr_¹åôÝ÷^ùc�µï¹çÖ7Êî>çìµ×þ­ßúý¾%l„œ8¶DQª›ÌÇDQÂBÛÜÓ�AEi�UŽÂã CÙÁŠä@e…²?âî?ú"þ‡?‚÷ó¿D«³P'Me:À³UbQMå¿vÓôÔó8‹ñ9hsw­wJب¤ •!­–RHHÓT™'Žu¸M¨.⬾è»Ñu¼Z¸üâвáVOð—KþZp°Î­þ -—³¹¹yèß®¯o6Õ'J @¸6ý~ÿÀŽ€²:6ð‚æ4A›(#�©½kLöÁB ¤Tx.ÓnàO‰}o§«ùf5Ø¶Éæ•A²‹Æ­–Ô:¬{m�ú~g}Þõ¿kh–¢^�ÃápZ]i`È>¤çá'?Á3ã ç®EHé¿9œðSm�ŸnºÛx ³ê2i2&—XZ(0óu†å³µã~÷~Øäböz‰E³PýAÄýµ1O?yßwYßPUÉF/FVÐj¹ŒÆ)R*#‚n'$Ë¡ÓÉ‹’Ñ(¢,sn­ö9y¼ ¨„,ËKßVd$!H“Ç6±-I¿?!- Î>|Œ4-p]‹Ao Û”˜†Äµ÷?ø&¹‰íµp‡h2ATŽUR+[0ÉL:Ý•:9‰Ç›xvQW1Çév�øÝæQã)´¶6*|«Pšµ@Tx´Ú‹ê·T“ɣЦðþ|ÊJ�–Òúž~¿(•}y^ Lw‘4MðŒÉ¶ùÈJÓ]¨-˜«ªÄq¦×W,S²瘆`ɳT¤…µ�4&„`=IYt�ZñEÛœ_Mh» S+ØQQbA” <�RÒëõ(˲®þÝ눳ŒŽ³7ùK'U³ó?›�tП–eÑétxÿwà³KðÛ«ð˧à� ü§6÷” �ÿ=‹‹êN&“ʇôX\\$›ªaxŽEUæ»òA¶¯Ä1 *ü°Y‹4dc2¼�oïË•¦¹�3í¦ê¹Õ1y4Ü$°¶[ÒÂ"-¬Œ )eÍñH’d‡•ýìXXXPú¼SÒ¯¾^Ë™¦Y“[G£Q}ˆÕ¤ç1_ÿ©ŸýÒ÷z#žiü½#-âñ¸Æ¹h‰ß÷É&w)¥‰ë5ÉY¡µ(¢Uø!„±;Hü°„™Ý®Õâ(Ë‚ ðévtÛÍF8 ôŠ�l‚VÓ¥Ùp°LA»¥þÛ4ÕÉɶ4N% <:í�vË¥, \Çäôÿ&Tž«‹¾•stQò=?Ä÷>*/J\»Â¶öÿžm‘åYL^0Ó"Úß©,ÊÞû“?†ÙYÂóƒ:)5mŸ$«¦6¶»ëÈÍÎã~íæùçPVFÝBÎJ/è`š&ãñˆ²È-*l²lï×ì0 å#ž¤ U¥Zl%6qZ!La˜帶£„)yÁlnáþâyªpÐA¨$kÒ¼Â[øŸ´°j5„½î–ô%Ë’è·þÙ¹7ðžx ÃõqÎ>†ŒÆL¾þÂOü“JÖ¸¸y]ÄwKÚ{]Eý¯e‚ ø¥SÐ0áƒmÈKÉ{œ½ƒ‚†aÔÌÜ,Ëjñò©B{žG)·pʭؽ¯S–�ŽUPd1i.i¶ÚµŽjO°X? êSA•’$)³fðƒ’þ*Ë|›ý£)$ vrf[}–e©Šø.ó0«¥9�MÖÐ)=Ÿ³(Ó´ˆ0½3üÓúkï –pásÿÁ_é�í�uüÝ•6Ñx´ o¨ŸEÝ¢”6®× +ô'!iœ!šÏî Y™½÷wã–E¡ëV+¤Ûñ E>K“�‚]¾MØX¦¨ÿÛ¶´ÜœJÄ<×! <ߣÓò(K• –e(p�0 *±L1­"ftß·±ÝÏs¦Ï\ eŽcÎN·,RÒTˆ$9ȪØöÒPN êTÔ2ŽEQ"ä¸méJÞad¥˜†ÚòÒØ‚B$1Ñd„ëXŽúýeY)‡}î;iNÝ¿ã…$ä…ÀBòd°£h‘IU˜ˆÆ=,Õ¤=Û ¹1Hø—/Ÿc˜—ÜÅ<Ôiqk\ÚÂÜ‹Z¾Ç¾}…gŽ.gÃ8¡4MÂ0à¿}á%>yú“¢!xõÞ:w£˜ïÞºÃ�¢ ¦iò¿ß†ÿêIØÈáo‡omÂçZ‡;,jBï¬éɃ´è-ËÂõ<²¬¡lv£(:°ìZ%ž]b›Åt2§ðŠ,Ë(òô@­x�G—EB’æc«k#¥¤È‹é{´•ÏXFEQ; ³±ÓqœäÁÙùš…ãÍ^¯úZe¨,ËúPifbyc?ÿK_úX§Ë?zè$Ç|ƒæ´}0 IH’«¼E·5  )E×m�Š£L"UŽÖ­Ëùñ /õn×΂¬5S1Žãp¶Q×Ïk©m·ó5êĘ2õL\K*B„Ѩ'n2(°¼§²* ŽŠìR(f¿4ê$/ËòºŠRB’¦Èã'ÉÂ6Íö¶m3(ÒE)Â&R¸Di‰”j£+h[ ¥hâO“ê"éá)ñ”¬„-²Ò$Í” ÃAIŠ=%}%I‚a£‰ãzDQ„kæÛî?)l­®ªTOxV6s}Z“Î²Â Ë !ˆ e7|ÐýëdÄímÐûWÿ‚æ�~çø ü÷¾ŸðG?Mqç6É[o º]ª‡Ývý¬.â»!�̓ýa²“>ˆÜO%/ô�„pÒ‡kø<äû¾CŸ¥Äh4ÚñØ•–t05l¸¶2N9SoËTÓ4 ¯L\WµÕ• GHZäE�u6»e”ÄÓMYŸã¸ ™Ë:A‘R{k+ÎÆ¢Ý0`YÏj k9]ÐnIÿüŸ¿;‡ðçÿî?Õ]´ÿÉÃÇèº&�N‡8ŽëM@W(¬r•n«O4êQ‰ŽÛ 0�(§ªi§`¯$WcÊß­¹®$kàƒÄaØÒÖ-g-ܾŠÐ†éAÞ´1L ǜڎ‹Ï¦Ð™¶ãâúÍ©�nyàZ2 ‰e”yBV”ø¾&MMñ†{²Ê"htkëoò!I¦IgqVQ> !‹ÆÆï†Õ$Lär«ó•Æ=<+£ÈUå–O’‡Â,³ÜFºó§®™”É6eˆ¬0ðÂnÝuqÅD+Ì‚~ò9ºŒï{üιwX‹ÆyÎï¼u‘Q–sc8ækW®óøòxá2ooô�Bð‡o_æÉ¥E¬j+Òn�{Å2]eø­[ðÞüH^Ãå ü쉃‹³18Öê,:>¦ØÑl6)JÕ©\›¢Èk�éý®·�ªVª1H3¨cVUUxA“ø”ELQçžçjM»®‡4ÜiÑMÊLÒ|ö1ÏÓÒØ߬÷Gƒ4�OWƒÓ4ÃúX3ào,¬pÄÊÈ åw¬¥‡’$©ebpžfm4¤a^Âõ6‡mLï(¾ï×Á^?¬ùIÖIêx<Þfú .IúUUÕZr‡m-ÌãoÂ0¬u-³,c4‰‘¥JTc‹—ßHXïå´›&Ï¿¯Á™‡”öb�ñÌ„,ʨ„òQ.‹€I<Á³Ò=ƒ#¨€åÙ%RŽ 7$h.ÇUáYeM6�ÕóLåš”c˜Š‡Q4Á6dL}L<ÊÀ eî’*ýÙÝ`[³÷¯¯�r­E\×%¥£p”†ºiõû ó1š�?Iš•DqÂýñ„/_Šù…çÞË7¯\ç'ΜâwÞ¼À8ËùÉG"—’ÿâ«ßâ±Å.ý8a’çõBڞ˙N“ËëT¾GZVx¶²+óîO-+8^ñùä©cS\°¨ïäv¯Ïõ^ŸÖÑ„ir¦Ûæ¹#K\îyt¡‹6ã„“í&ã,çã�`ÑUE¸Y¨îhíV Õï|š¦H\þë+àÈ%<ä«›Î#t,œ½^ÃQôaP¿g:Ñxì½®¥’bšišc™ŠØ¯ ½žçÑétö„ZÌr|²J)zH)Iâ1†LH?h"D@4ãñ¾°É´0 ›*RRf¾Nµˆ®<S”‡ƒjh©ÍÙ=EëdÏÎ×ìÐcýÌtSnÇQûÜÿò«¿ò¥³F„k›uõUc2,Ë·KÒáÛ²1m…!JŽ ˜J|_™@k]ܽnæ°mòÙ¡«*ãñ¸&м›Û¼®¥¾çbY&£ |å›c6û9e)‰âŠë«9§Žúx®¤Ì†*0Nå»Ò$QíòF‹¼²•ÓØ!*™Ž¥t“4Ãv|\¯Aœ)§1%Ÿ¥<е#Ød<À³¶Àþ¦!‰SI’¤ÛZ©2ó§U5­â ÀjÕºª2n öFÝâK¦-*Õ"‹³Ã9… ©;Q4ÆŸJîÄ™¤¨¬ZOWË�é!AUå¤nKo×<¬#€�H¿ù§d×®àœ<ÍÆ¿úŸAˆŒ&dׯâ>ûÌgÞ¿ïüÍ’ fõA›äî§«Øn·k]GŸ’§[Ï7J>Ø(ùh»äãABOö}‡æO¶³¿}ª±Ûõžça;Y^bMµ5u/}`=fñ‚Y¥¼ê¥”$“žUàXJr)ÍKë†8_ÑÃ�_ûµwKø_?ÿ+_•¼uƒ‡ÚM\Ëäµ{k$EÉJpªÝÄ6M6¢˜¶çòþ£+Üè�øÄé\ÚØd)ð9ÙiÓp] )y|±Ë0N8Õn‚ÇÚH)éø_½rƒ×ï¯sºÓ¢¨*,O,-’V’‡:ŒÓ”A’ðH·ËfÓrÎv;B°â{;Ö¡.ÍÃÆ|߯+Š£ÑˆO/ >Þªød»äÇ:%?ÙN±ódG ™Ý‡5 J'qóÏ^ßyþzP2ŽyY)c×&™K’÷Ò:7„¬Í§ÊJ`8íiÕ6E#kK>/+ Há§%cn/¯$HsVï¸�-’i>%°¦\¶ã!‹õ{ -ÑAÅb)åL³†‰è"ƒþ Ý™ª%9ß¼¹!Í2#4KLÃÀ”#Šö°§÷5Â0"J]ÖFËHûöTÚCoî¡”®DiŒã^c\¼é̶mÚm¥Ý¨ÝL¤T'¼YåƒØ×©1°—VC�¬¾÷JŸ›wSš¡:¦®ÞKqlƒ#K6yVà;ñ5¬o;!®çM+±çp\E)H*� hL[ñ#„Ìpƒn ¾ùæ¶3Êmò)ÆTσX�Çã¯6D…ix¾n5F4§’3£aψö ’È ƒ� lRÕ¤¯l‡ø9¤°)¤‰0Š<ÅòRPN¯×ñ$Ž ïm»ç¼4‰ò�ЗY€¹faxÏeÉà‹¿Byý2+ÿàó¬ýOÿ�Ïü$rú䡿ïAI{»]?KZ«ªŠ j¦ía®Ÿ‡´ÃKUUµ¸÷~Cwkôõ†aÐétÈŠŠ¢¬}‡ñÖ°ÛÐ�ž(RÞMOmÎiaâ‹8ŽªúZUaE¢ªSaØP]…dL`gubRTÆ´ƒ¡î¯L6êÊ”’Á³1U�ÔAR³„;<ÏÛáR§+¸zãh·Ur= vM^tR¸¼¼ühk\º½%yá7ÍË0‚-ïyˆõC°Ðb2ŒYOÎR+ØŽWëóΓÎ4Œ¶:{�ÒδzÂnkS¯�Ý:!°l—f¨ÜŸþàOîÓ•C—çܺ›’¦ïºÁc§ Ü]â°ÒÜ+À÷C%•�ñ¬âP¤±¢¤…²×ë•2Æñ[õÚ«Òõm‡÷dJš©ÃLB¤×ÎaçA‰A…ã)£¤8Žðý î8 ÷½�JB”;¸ÓßEê÷{v±½Š- Ò � ññ\òM%íTXX®â;è.I<ÞÀ·‹™ïŒSW…„Ò 2¦ß�Ó¶iQ3IRCP±ÕÉ©PÏÊš^³ž;øfI`–º ]õ<òœÍÑ{úb&†UÓÓ/…[@)%žãàù>Ù´›,%H±õ·ì gŸŸV)˜'44tåP¿�ŽÒˆ×Ý’½Þ÷}Y�å°âG„Ý>IÑ¿„Õy„ÍõˆõâCX–S¤µš‚ÆLhF¯Þ´ þ~7µ“´ÛUØŸáp¸cƒÕÉ…&z<ˆl†UãlÃ0äû¯%Œ'%�œöY\°1 Á[Æ FŸþ0ÛtÜæGQ©‡ëxJ|9ŽÆ2>°]¯G’HÃÇuÂ&¥d2\dz¶î¹¬ Æ™³ãúw›d…N†eT$¹Aeø4›íšœE븇TFˆ3a5†+KÆxÖ´Å%LÞ¼9æo^£?‰9¶ÐæÇŸ9ÃÙe ¤úüIÐé*É™(ŠÈÓ1ž©l-Ó©ÄÓAläÙ$o¿@r7óH/\¤ýßü*Α#d7®a--#‹’Æ�ÿe¿ð‹DqüÀÊ ÿ x³×û¾_'YÃáð�à³ï�n�ÇãC'y³×+É‹8+plI¿ßß·¢'„¨Ûóq4FT1˜J¨,K¢ÑzÝ}Øy½ Î ,§�ëùjS*&¸VIZz4Ûj] ûëöö@©Þ!Œè+zƒØo¡l3uìÐVŸzèg¨«õ{éSêñn“Ûµµµ¨–?Ê¢HpÉqGé�gWåKÁ"åðfû6ïOج>„iÙ5†nW…T…VÇȃâð^É™‚NGLwKðg7W U8t±a&AÀ¿û³!'Žz´B‹ãÇ\¤„¯¿°ÉÉ£6ï9›Öj1»�JBœÛØîV’X‘²í=ÄOÉKA^¹5@o´ãáž¹õ.•S¥¹‹2‡†°h}âÃ$ûž•ãZåTaÆ!lt¶T9;eöýýÒÝ‚ÅcL™¨n¡0ùáµ!ß|ë:Ã(a±ò©÷žå=Ç=jN'™M«³\ËNÆ#,"lK’äöô@ºÿ2M³Ö«ßo?Ž+“—‡mðT8âxSí_yž×–؇�¿ù¡[zÈÐÉ’¶Â-Ë’���šÈz˜ëgß5ýý~ÿÀ"Ãüõ›žæ%yQÑ œš;±ßõZþ”U2U†ßØ*2˜eOeUA\(OÛ¶‰£ T1¶YQ™�úœE;L•ÒÂ"ÎÍê¡ã¨–`;è9jˆ’.RéwH… Vkï0{›X[[›Â&¦,ê$Õ.Wé6î`ä÷ÂDØãAEþ¥: Ux³v÷©ÛÙS<ˆf²éÊÀ~än•XÏóö¬Ì_?_ÁÛê ˜š”×ׯ÷,~ðº:í•¥úwÏ3yâŒÉ{Ïæb+Ñ«¤Øq’–( ~‰W'©q4Æ1–ÿ‚­ÓS£½\·G&C%9£ãlÕv¯yÐØ“,Ëêöóî¶…’¦“Õ¸œ¸pi¶—ê–eTùÛÈ'™S ÒÒݳ8MbÊ|­^Ä¿þö[H �Ðcczç3ïcÁ—ä¥Àò—§•*ù™ˆ,‰©8œ¥³šø³ÛA©”‚WFm’ÊäÑs_#ø­†ŒU%(øKŸÃûÛ¿Œ=… V—rv¶±Û‚ &{évÙƒ|ÿ|‚< hcÐïP»Ý!Î4§áÛµç÷¡®·Lü@½ç†¡\¶Æ£!–í‹I‡i¤tp\ÕŠ�ã žÔëÂ,7ë*‚ªÂ¢Ü®Õfc€ŠF°W¢¥çpl!Ú‹ÊižÿôÙbƒmÛD“FãÚÖ¥D¹"‘iìû`Ð#˜áUĹ½«BËì<̶Šu\ÛK6ªád5”'-,üær­Ì¢dÇX¤ ÃhL!A�ZFñöæˆó�·Xh†¬ Æ8–Éßú‰÷q¬)¨¤ 0Úu%Š¢z?�¢ey0yyöþÒY¾‡ÜH| !ù@sÈé%•�ë"Öa‹V†ap«„—F1]Ëäã-sú·³•Ìö àÛUdý¾?PÑl:tQNk‡FaG�Z¥Á²‰Ó­"Ãaºy‚ ˲ Íš§TUãÁ:¾}ðAIò4ž]�ÄÍ–‚çŒë¸f²U�—jíD¹½í°§×¿NHÓ4­qé»�N§ƒeYÛŠ˜óŸº#iþêþá—´��e½1šÕ€BzLŠãT¸¡K1ºCì} ¿fºjb…®2hÌ­>5;ˆ-»µÈôCŸg’êÛ^¾Ò³×ïŽ_ÚmSÌ{Ö !Áp ·ïe¤™$Ë%G—>ò\›¼P›†�ª5“9䕉”)EmohËÈÉÒ˜¢J>K8$Y‰8H>K@IP;‡¤iJ6I ƒ,W’3³m°½æa0žðí—^ã[/¿†ašÄiF§Ù ªù©í¯!‘(aiåÖØéë·nñ½×ÏsùÖ}Ž9AY1•ìÙô3õ ¯$† ’`™P–9Qœá¸žßäO_½ÄýÁ˜Ÿþijü•�=KQ–\¾³N+ð9½è‘*¦Q›˜Iœãº>žÿ`S°ÅÊÞÍ‚1—k¹K& Μ]¤{ú$ñ÷^ÀûÄ'ññïQ™Ö6©’a•ëùׯñˆ‡‘œ1 ƒVKaSûý~�°ì%•²×wëu?™Lê÷çA$gÂ0D A^V¸Ž‰i(`ÿ^×›F¥l7¥À2$�“�¥ †éâL+‡Q!d±Ít×9ÊÂSIÎa³Êɤ¿s85í(*³VGÐ1@c9uJ»λDÍ:ÍVdgqù¶moƒ;4~ó7ó]an?ÿ…/|°’`Z5óÛF‡qz °p<ƒl¸Iü(kªhá×DQ]yÖ8`M Ö‡.�pê8<ÿLgã°NjuLÏÓ~›ŠÆ1êØ¿¿|ÞÎ8 °´`rûnÁf¿¤($y!ùà»[yH IDAT3Mžx´I2ƒí—:—¥4(¥Q“rµ|Qž+;X?h`Líxgítw¿¦а¾—0l�W6i®ëI±»:Ðì<ô†c¾ñ½—ùæ^U‡µ4§Ó )¦ÑEYa[¶©¤Ö,KIRNr—pÚzûòe¾ûÚ[\¹u�S'NgRA„�dêË&e%È %§–•Š÷a�3ŽbÕÕh´øÆW¸×ñ×ä9~úãïÇ2oÝÇwÎ.¤¥MÐPfÑx€oNHâ„J4­z?ýÿKqRZŒJ›“nÌÙŽYÆtA@_¿—.¨wøÿÚ˜ðÎßä[›#þxmÈ—7ÇüøR‡�-)Çq*ÓÖ·q3ëïwçбÔu]%Ý7íâÍæ#‡™?•Ø7HsµV|Ç® N{]¯5ê%8®™1ž¤jíO¿;M3LQ@�gO¦xöV[­‹4M‘ùk[![PT&•4¶)Õh ªª¦ä_»†�ÍçeWEŸf?CgqÆÍ£ù³ÿé—¾d ‰kTuÒigôÇ´[2�‰Å¢l‰Bv‘öÑm­ªÙ`ª\t%מ:ì趘ƒN’d›få¶išYÔ®ën#¹¦ztÈPm0¹[¢,9¶b’¤‚Í~ÉéŸýTßwp½€¢²È‹’¼P�TJ¥©™W&Ei*%›™$×9q’š¬`QäÆºrYaâÏJΔ#RM:ó$™ïܾÏÉ¥6g–`5q¦ &…"Û¦DÈœá¤ø “¸çISm» k笸íÖî¶´³ùÀnE«Û%üýó7øÌr—/>qš÷·C¾ºÖg5+øÉ%ENžM†Š…óÝßÙïêJðAäËv»]ß�RxI¶uMZ?a"1(ª Ç6©JÕQßëzÛ,ñíÇ,±Lux«¤sKŸ9ŽcÂFszH;˜üêý©°p¦RŽ ˆˆ³äy!”’‘c*0C7ÛI·:AÕ±X¯ƒùùš?<ëûÔEÒ$I5�æ?þÂþ¥›©�kTøÖTê%¾€gÝŰlìôaS�ljqªÞ\gSÃ0ê¨OZú‡d™Âðf½UŠ"à ¶´$g+#óC±­}¦l8íªBÍêë6 • TPÙž$Y•œ:pw½ä#ïw�¥ª@è‡b9¾:%ÏMªœž^ŠÊÀ4ªú¡Ûf…!•½i{xA“4‡¼(·A¤dj^ ’¨2í㘕Ò%Ì3’´¬“´ÃÌA%%7îÜçì©ã¼zþ|×áéÇβÑòñçžáµóïpo³�mZ¬õG]^as8d¹Ûa±!8ºÔ%Ë ^zóe‘q|e…À÷‰Ó‚qœQI‰eš Æ&qÂÑå²<Ƕ nßÛ$I2>¾H˃¬\ZÝäêÝ ®ßÛà‡—n |ú™Óø^XWm³¨·MÆÆ2 /!ŽÓm�éAØÈš•=«,�ç9¾¡ô:Çã1™/ÁšBlv»^³Êu·b¯ÀfFmV°W7bþz ÙÑ2I†iq»?æN‚eZ˜Tu`³m{×Ó»¢Ö��Fõú˜××Ý/ 4 Õš,K|×&Ï2dÖ'Ë$;³cVu5UWË+·…ãL�Ó>ESVS}gi+rÄ>�U!þ´z5ö@Ûô•­)\ döÔF¬t—óKjÍ Õó¬«išu%s/ /]±Ì²ŒÑhTØõ½Û<¾ÛäöKÿðW¾x?sí¦U`[*á£ïáT Nu‹0ÈÈâ’Ô8V¯¯ù8¬ q×qXW\\×etçLÛÁrü:yÓ2h»VægZÃW®+X‡í&ÌvBšÍ¦’-+%U¹ß{\²²ä2K>ò¬­°³3 ÇõÕæ¿#ÑžÆáR%¹Æt�X†Ä W±Da›¢²H²{®Ø�LU ò¸‡k•*.U)ã¨x ŽNž—ÜßèqêØ o\¼‚e<÷žÇ¹·ÑãSÏ¿ŸWÞºÈÝ�>®m1œ¤¬,.³9qtq�Ž/YYlc™&/¾zŽZÞn�ÝÝba«Õª;_eYqoså~�´¨hxé!bé^³ò�ûÍŸiš„�Æ´[«Š Ñx€Í„8I±loÇú ì-⤎«qaÑh*>Áx4ÄaL§–�´¦ᢎ¥»�¼4q‚­ƒ^<b;.~Ð"ÉU© “‚­‚ƒNt«é¡sþùk¸šîþÏvýwÃÑj•�7j‡;ýîÕ5|£äÉ`LÓT'A‰ç¤˜ÁU´�Ù<‰‘ÞÁ[,\�4ÎÃ4K;‚:¸ú¾�c™\~髸Í.”9Ùð~ xÞÜܬ³úÙ›ÑÄ„4Mk¶¢mÛt»Ýz4t@ÖÂÀcêv¶“‘§‡eYXfÅGžµ(óAÝ–Ò¿S'Ý »~FYŒS—(·kŸ{ØjÈæ}ÆÃŽëã7–˜!y©þ&)<KO×6¦IÊeÄ s¬Åãu¢±W‹P plå´æØ6–iªÓ³!¦r>§Žge±ËS�œ!Ísޝ,ssõ¦)yíÂu£ �FH#p¹ug•ë·oú.ÃqÌêú�$‡Þ`ŒmY¤YŽëX_ê°Ðnòä#§=�<÷P—'O.1˜Äœ»~‡¼(ùøÓ�°ÐhâùZ»r‚3P—@UuÑë ÛíÖ'¿Ã ]1ê÷ûäyN§Ó©É1¶mÓï%hµöd”ëÀÚï÷IÓ”Žïãz˜ë÷±æ0­º*0½ÖµŒã˜v»½íà†ê¹O& ¿÷Âüëoþ�?ùÁ9~ûkßç…‹7©*eC™¦)�N§¾FJYcmõû2ŸüëäfssPX0­­‡67± �çØ˜† �•\Œo8 ™ ×j<šã8T’º²yåàû�:aÇ#«Ä³J\1f2\$�Ö9 ’ÂT,빑Êè ‰Æ4œ[™ 7Tõ§µDZ…¤…Éìò0=XÎaá¥$Š"677k ¶n�íÇ€ÿ?ÛÆEvÔ¶Ìštr¢ÈA#0KÎø–P‡)ƒ ¿ œ&U2Àjœ€è¦Ó¨µÄ÷ŠÃÚ.T[\E�ïû˜¢âÚ+_Ç ;PåLÖoÔó ¡0óó6«Ñ©ßýùív{_†ù¡y¥¤ŽÃšô6? àÝ|øY›Á P¯a ÑZÑ»}F9ÅNR·æ(ÓNƒ-GD£ !ÔZ’M’ܤ’‚8·ðux‰£ ޹õ.«*°"*‡ÃZ£Ô4ͽã°!p¦ö²¾«¤ÎŠ<Çžê‡aƒ³§OÓí´9sê$I–±Ômsûî*¶)yëò-V×6i5;M®ß¾ÍõÛ·}›þpÄÝ�yiÒާ˜ëϵ9¾Ò¥Ó yøÄBÏ$Ooò왣ÄiÆ•;ëeÅÇŸ~„•vÃnÖñÃd'7/Íú�ÇŒF#|߯÷ây&“ ƒÁÏóêX~XØÏìZ´,‹ã�C }þÎé£ü̱E\Ó`ÙÝ CÒ×gYF¿ß¯c™&oÏ—’,ão]åw¾ù _í"ðÂëüÁ‹oRHQç$ó×ÃV¡OÇœù¿ùõ£s8¶Ià)cK(¼w`çȬÏxԯןe™äåv¸�ÒoÖäpCªŽhàä˜å€É¨‡eÙJk_ÇÒ]J~Õ‚ªôŽG4Ü Qô™Œ{8Ž‹,i1ï )§Îg;cñh4¢ßï׸jM<Û«hµí¹Ì�õ:rg×÷°&”éaM~@»5FØ ¤h!ʻěLŸ¡‚„ΔuÒ¨%œæoB›@ ®½†iÙtÏ<ËÝs/°ræ=·©ª–UW¢temÐ=ëcï{.~Ö/“&ììwzÔ ¼ª’ ÇÁV‹Mã{fO_úeÕ•šÙà½aå �´c–µ9‚Ya�ãªZ—Æc,Çß•‰X!‘•¹M!áPÊB�å®­OËRøI Õ™¤ÐjwYÛì±Ðnc[7nßæø‚�mJ$kýÛ2é¶<6ú1o]Yåä‘Eš�G…ÃÒâýáˆøQâP:—:�ú¾_ƒýó\ù„¿ty•o½u…Åfȧž}œßÿö1„àç>õ–›*¡×]íÈó¼~_†Ãa½õ{3ÿÝ ¬+Y–Ñjµ±låô&eEž¥˜eo[uU–Z»¥�ãie„ ¢Š±­Šªµ}ŒÌ6klc^‚eT•$-ײ,Æã!&ž¥aQ‰m¬Ü"^ßVÍÏ ƒBºx3Òy¶ØN´Q²4&Yi²›=‚úp»ÇÝé‰ã¸Ö÷�ÿœYV²îD­¬¬n!Î M(›~:îèk4=¤ÑD™¯1é$�Ï Ä—¶ÇaÃ0v­¾ê8™e·_ù2G{ñ$×_ú2g?ô²’º¡•o4\CÝÛdw”qE€a˜uwNW€÷z�´ŒXYUŒÆ¡¿9›gSK)ë÷s<×Њ١ù:±Ý�8(Pm[oêú¥‡"]9x¾zâhŒiÚ4¦æt²�;ul,(1¤ÅxF!á°ä¹²ª°L“¼PN–a`ŠÛ2'Ðh´E1­FˆiZÜ[[c©¡’‰AœbYMßa0IyãÒmN®,Ðiä•M§Ý%™b®}[bŽm0Š *)i6:•Á¿}ù:¯_»ÏÇž<Å'žz˜ hÔkgÐ_'°vVÏÆ™Skf×–Þõ3|PÒ”> EÁ`0x`…™Ò4ù¹·n²ì¹œô\J)ùó�ÿ¡e~¦íï›tÏ*|hePÄÑëk}~÷;¯#€ÿðÇžçO^z‹ÍÑ„yÏYžä8k½YQðÔ#gÔ�Çu\ï †œ¿tϱyèø‘m¿CïyRJ–m㻊¼)ÄYÁñ#ˬmöXê¶IÆ›3ŠI‚a\rë^�‡ŽÁ0¼)y=KUQìÆ�uŽY!h,L«¶½ZÆ4-`umÀÃÇÚSò»‹4)Š’K×®ðØÉníh–äAKUåwXWx¥Tð�JxøA“²,T1ÄÌ·áÙëX\X;Ðõº"«“[K敞41o?ÆÝÞCóóŸÿü—ô‰rDX~+lSô/cØ&e¼I$Þ‡´–êb»S�~PÚYBÿ�^ø¿ýÿÐ=õžërëÍï0Þ¼ÃÆõ7é9M^j³��Ng×Ä¥( ÒÉ:&C,»�e;5&®^,3ò,Z-…»M"e;8­dhžnãéÓ¤õ>g‡®À¥iZW’g±|»M¼j—V;ÿMä$iRÛñjÂF4î× yP y'¿B\82Ęª W ˜Óßošê¹¸V�ïT˜F…kIÒ$¥ÕêL«~†TøIÕ†’„ž…ï˜ %�k²²Ð¦Ópi&�+HÒ ÏóévºJª«*±D‰ïšøîöÍÈ b¥Ýb}8ásï?²Âvƒz޳¢¢(Êm-’¬´j&üì3€-ÈIµ;Óañ¸úp¢O�‡m±Y«·èý³‚麘­6²¬pŽŸ`üG¿�wæ,ÖÉÓ‡‚Lèç—¦im3¨¡/^¸�cÛœ\îòéçžàü�»”UE3ô8Öip³ÏËç.réú-ÚMîmö9uüX]�ýÚ /qëÞ:ç/_ãôñ£Óù¤yÁZo@« �K×nÒ \×Ãó}®Ü\%Îr\Çåü¥+t‚-èÀæ8ãO¿ž+·îqb¹‰)Š)T&äíkwF%ƒ¨dyqQƃZ¯”&þòEŽ,u±,ÇTÌý,/yûêm Ëö] ¢�NmäM†ÛÚ£°e¡ª„Ç%aØDSáqQmÀ©÷NÔø/=4nO?ÿºj=Ýdu+~?ÈÂn�Û¶ùõ_ÿõwKøüç?_ÊÌì6¡uÃiPö/b8.Åh�ØùÒØJÊgã°~ÿw‹Ã–eø>×~ø ‚…£”Ɉñú*7ßz‘ѽ«t��!+ª¶¡™áº*5oG\–%éø†ìcÚ!–­È ZöhÏ8,ƒá˜2O·Åa�®±ÆÓCÃ^q¶’öÙX¾qöŽÃŽYPd1Y®L |_�/&㎙" «1çó‹ôÊ1–ô°q·­�ÝHS³Ã˜> ÃPDË, e4áÛ•Òü¶ƒi’e Ë‹ÃPqØwL\Ë$žmpd±C§á¸‚ЕyŠíVÓrÈóA…ï<{nîdÅR«ÉÆ(æ³ÏSEÃŶU'Gbì ?•±+‘y/ØÓa5aµt¢Rcˆv%�í7„Rò‰n“ÿóÎ&ߨpnñ 'ù�–Z{๷_Ôp#­+�$ ço¯ÓŸÄ¬t[üÔ‡ßÃíõ¾2Jsž>µÂzoÀÛWoà9WnÞæëß}™÷=õáô°ûæ…KDq‚ë:œ¿rƒN«ÉÝ�¾çñû_ýs9}’µÞ€¢¬H’”H¸q·Ç�»ë~@’dyÊÍ;k¸®Ë¯ñÈ™GˆÓ¤DØ Ü)‡£LÛ´ë5ÔÇ6¯¨B™€S<¯9…úlÅârJ>ŸÉO¦qTGl7nÄ,4l/yϽÞÙäVâÄß#hZ{a·¢ &dþG@ì 0;ÔŒnýe³ óô3c|ï:7ßz‘£�=G6°xò1úw®’�6°¼)¬ZcS·ã8ž l)²¸O5¹€aT˜vÛvw$¹†aÔn:I’(c€éЉá,ÙDWu5o·ª×ìÐø`[’ [ i�²¬ÝíB9ÌPåT­[Y^�%¦Qr¡¸D$c& ˆûTT8x˜l�ã9%„Ä··´UÒ¡ÌtëÂóCÒBPŠô3ÿ»mSÔRB ’—*#IR,ÇÃõ¤¹Âlšb;ާª M%Ç;Ž(±ü.¾¯1•\ÏÇvC’\±¡ ¡ÉØ=˜Š¢¨Ù¬ÆY-`­%[Å�¤5Ã0ˆÿ� yF糟Ã>}ÿÏ~ü“Œ¾üGȵût~ê¯RLå~S}к°š‘—ï¬óøÉ#,µDiÆr§Å8N9Öm²Ôôyûê Ž,.ðЉcܸs�ï¾ú&ïêñú óêÛïðÙO<Ï0ŠI³ŒóW®sáê ¤„ï¿qǶyûÊu&qÊB»É·^z•Ë7oã{ãIÌݵ5:¾Â/sñú]l˦Ýôxñõk<ÿÞGyää2ã$çõ‹7¸xí&yžñÕ^æÄÑ#\¹¹ÊÅk7h7C¾ýòkܸ»I³Ñä?8ÇÕÕ{[Yækß}ƒ0hò÷¯po} SB–—–©p°e^�¥ ¢ï*¦àRùœÇ S¹–y^l ¬ö ¹¢’¢&ÝEQû¼Ïb9=Ï«áWóIÝ^ëpË÷¿ñÁä¶Â‹þ·ÝAØÇNª„hâRøïÝõZí©Ë^qøäÓ&Ú¼Ãý«oÑ9ö0¶à-ÒhÀðöEœ I%¬º­ªÉ:y™ÒðÈÓåøL‘bØ-,Ë©ñ̳�=‡ã©ÉÇì¼i6õl’«úÅáÙ¹Ÿ'j؆ú;ÕÎÝë•4 ”ÒÞÒæ,Kò¼@ˆ’+å5"“’0kT¢Â%ØVlÐ÷¢ñ °;&؆�esüfMt]—¥°c좰clû7­Ð£¸ Ê1³¨lÒ¬b^¡GUÇ*Žu\£DšM¦6(é!5ù9ËT’’ö¾¶Ö:¡Õ8HMÈÚ¯X «vZ I'/BºÒ£e>Únð§½1u¥ÃùôØð@Ê�F£†+ø¾Ï NÉò‚gΞ`'xŽM'ôÉË‚GŽ.`×nßÁ²Løç.]å}O<Šë*u—õ^Ÿ;÷בHÛ¢7³Þë%)«÷×9~d™s—¯ú«kë\¼v“N«A8æÊÍÛ †#>ôÌ“¼}ù ·ï­%9'V:\»»I§Ùä‰Ó]NYäÒ­u¢8æÒõ›L¢˜c+Kܼ»F«r£Ç·~ð2HÉýþ˜óWn±ÐnÑM¸zû>Ÿ<Â…««ÜX½K+ô¸¿Ñã‘Ó' Â&¥´ðuWj<Â�ôvŠt¨òÓTv¾i¾=�Pù‚"ÿJ©”•�-B¯–í„íù”ÎÉRi��$Ö:b‰r€oÜFØ!Rt¦¤˜¬‘¹¨á{ ÖAMk-SfeÉ‘'žçÙÏþMÂV‡‡žû4·/¼Âhí&í•\ûþÿ‹) Êl‹Q9 6‘ý0G/bäwÑ­i/“˜�3êoßý UzS%o¾_cÍ4¼ Ió]ÛŠ ˜Àý~¿ö'Ö|7‰œ½FEôz=…k™¶8´^›eìïŽ#�·ÞLƒ–eã7Iª«ÜªÒ䬉¬Šw¶}Æ<Ž'‚:hÌ.Ç,·ý–J‚a7ê“r<é3õÔÕX")âÜÜE¨gû0 ‰oçTiŸÉxˆãú¸Á"q�Íà€¤áÐiºœ\ q¼&Á×–Ä|+¦H6‰£1žßÀö‰r—}”ß¶Ý¿öéβŒv»]W}vÌ÷ú¢N:ðéëgñÌúïõ0 ƒôÂyªh‚óô3Ï}™¥õ ½|‘,š†a]ÁØïeÔØ,Ô5¦ø�œä®ñ�×.ò‡ßy•¯¿ú6Qšröˆ‚<ÿÌSHàû¯¿Å©£+œ>~wª«×ùw_;GQ[^Â0 zÃ1§Žá̉cdyÁßó£É„wîóÄÙ‡ø±�|ˆ¢,ùã?ÿO?z†f&eUMñ‚‚¢*±-Aàš,¶§óSUtš§Ž,xï{ò1�Ïëo_"JRÖ{C~øöU>ú¾Ç9{â¨Ú,;�ïYx®ÃÆ`„”&)ñx!¨ÛÌQ49PWa( ,9d<ÜT8Ùæ©lg[k×4*eVbVÛ`z$IB¯×«5…u8èÎ]9ÿ‹3½Žçea‚ÑAˆœb¸J|ìÀï×qXW=æãpYIV}Žg>ós­.'žþÃõUVÏ}�c�?Çê›ß�tD™Æ˜†A£2��úÐÿ:öè›Ù-�9P!­%Rû †£˜èÎW¨âK!k,e³ÙÜJlÓŒ(ÚYyÑóÖëõv´õõ½fèg8Û�ÑØpƒ�8ìùQT[Õbí4Éi˘ª¨NEÅ:7¹!ÎQ²[¯÷”Ýð˜ àZÏV=�3¸Æ<é�7”üTs�Òh©µ|À2T…”GŒ˜Œ6T¬k/RˆÖ¶wÁ0L¼ ÉñÅÏoÔfiã1–2)Ó‚°µH&›sxÎ=¾yY‚tN†ÆÓÎÎß~ j%'dÎÏtþã¥�ÑTµÓéÔ{ë~×j\¹†tM&Nv†QÌŸ¾ržßûöùÚÏóÒ;×yîìqÕ½Ír>ò¾§9º´Àëo_b¡Ó& ¼×ëQ–%GW–xÏ£gyïcgÙè Xì´q›N³Ià{œ>zßuÉ‹‚0ðyò‘‡YZèÅ °L“•ÅE:]Özuðp]¢$¡Âââ�ûÜÛèÑi†Ž,¶i‡.‹�6G—–(‹Š$-è¶»,w;øžÃ©£KB°Ôm³Òi°¶9 ÛjN JrK{õ¿( ª2Ù«ÆT�P �úØŽ‹.>Ù ×2*|;¯»e:Õ±VçSZRVÿû~æ3óC¿‡uåÖ‹¿�×n! 0ì&²X#UdÁ‡ý�º‚ ¥B´Ö§^ø�×TD¬…ãg¹wõ '#ì,rõ¥¯pçíïsâÉòú—›#�<‹cå,7nâèy  IDAT›Øñ+ØÃobåW1‹»U —¹ù5,{L)C «QÅ,WšiEm¡B]É…Ýq‹óCO¾di˜iVÛuå*)ŠÒÀö:ÓÓK„%GÈ2ž¶{›,9Ç òÙ§9 8.Åa'™J/YŒÉ I£Ñ¬uÛ«¶iå„í Ø)ŽYR iZà‡MLÛ'I+ :0ÑÐ-¾|Úâk¶:Há’¤ªE&LƒÓH«�çM¡i¿Öܵµ>p¥t´E5˦m6›uPüÝI—?†¼‡|£o’U’#åhÇõûé$†AúgB±z›rÐ'½pŽÉ‹ßÆϳŒ¿ùg?Àû÷ÿQªõA>óºj»¶¶FQl§4\ßõ¸x뾂 ØŸûÐS4]…‰{å­‹xŽCœf,/v¹yç>Í0 ,Ôó½¾z—ŸøØ‡xôáÓ Æn®Þ#Í3:~”›wïÑÚ­ÿ�¶÷²ìºï;?çÆwï‹�¦{rΈ€“H0Ф˜D*R%ªä•-¯dÚ –!Û�h˵[òÖºärI+-½Ã*�"A @™D Ò Â`f0À„ž™Îé…›ÃþqÞ¹ýº§Ó jOÕPÀÜ÷î»÷œßù�ßï¸29ÍÞ[¹06ÁØÔ NÉáæ#ûyá•ÓìÙ¾•щ)\§ÌB«ÃÖ¡I’3:1ǹ+3DqÂű)J¶E½ZæòÄ,ÃCC8%›ùf“áÁ~â$cÛæuÞ½ÂÄÌ<;6¡i093�iÌÌ·è«U諺”K¦])ȶ]Â�y¶q}Ü, ñÃH�l?Ì Ï¤4M&f­§Åå±ç½ÕÐõæà›Ö¹ýýß¿KˆÜtü‡°#d™�y4K'"µ÷oèsTayîMÚóœú †a0´ã SNQÜŠ&`úÂI&Î>Ïàöýœ¸ÿKl>p †1\¹„m4±‚ÍÇ1¢³é8z:�™äóObš-’´„fÈøSÄáVi¾ºúH’dI6 £ˆÃmu+‚Š*Vض�i•ðÜøÁ4üùöÙu‰šÊ2ùÆ×hýk”ïx²ð^|�¡þ¯˜ø/ÿçwâüÖ¿Y²}«ør HMÓèïïgvv–N§S ¨eñâ…Iž}ý2¿òþÛ¬8ÅïB05·@ɶضy3ÓóóRûµ+ßF1¥nk̶K4;º®aj‚ùV‡²ë`:a1ÐßÇ\Ë#Ërµ ³sóØ–Iœ¦4;>;·Œ…>"i¶¥—ø¦¾2Só@PqJ,õZ C×è´f(—4&ç4£ÄæMC\G#a°fA×4d|¦EÕ-a™’è˜ —j­OâïšS¦äÈ�À÷Zè„¶‚b�L+ẕbî rªuYÉn-Ì &ÆŠíÖ•ˆ£q¯kšçù›&”MOO{ggF�Ãý3\nGì³ÑŒ”hî-ó}äºÄ!« -,¶]׉ÃasšöÜ$^úGïü9N=ö ÕÁ­XN™íöSŽž@Mt[¾“,j“ÅvÍ®"4‹,ñ‰šó}G0*בˆ!ân^OU‘Îâ8¦ÕjäœÞvãre�Þ!„´RÖ4­°‹V‡H€0 ˆ‚¦ˆÈr ?1ºŒn£°xn7§)é‹x/2È„]HõÍzSÌfãkÆa]Ë »Ü ÖÉ„�£Ljü6†ˆ°ŒLÊî‰*åJ�$IðÛS”–)ÆÄ©FœYØé­ƒ�‡el G–S' ¼B"‰CHÚ‘®��N#›_±º%©(Qr*EUóÍØ’—Ëeþ~RçÙ¦àœCtøõ‘�ô•qíêú�:Þ­„ùVŦ•œÎz¡ãããB/á’„³®ð¸žcˆ˜°Kt(—kX¶ƒÞ•ÃÓ²¦ž‘#®b€«g×KíŃöº$-–e½iÌíÛ?ó™»‚`Ì<×1ùâóWØ10Ì7Nµ˜I¶±½o;±¦ó“MþÝkcüÍØž¦qCÅA_c/'�-'ÿæyŽY*SÜÌàÖ=\zé‡T‡¶ÑœºLz8Õ>F_=Îà‘Oãù ÚsM,=@ÓsôRa8äa“,\@˜e4£„îTÐDN ¼¤YZlîkÅa%ä®â°Z?½º˜ªÍ¼RµGùÙ+™•ð¸–-ÍxüHaÿ4Üò"ö/ crDÁˆRƒ4£8x6*ýÔ´’xåJ²ÊVTÄÇb.†iÞu¬Ä$ŒâT+tH[­ãjX‹$Â%$q@ç”Ë•«*a+�4äz…$ŽpŒ€0 HRpËRLÒÜ(´|#S_¹X¥k9qž¿±ŽÔUÏ£ç`òZ,[Ý7ÔàX ¦"8ठk«ÛÁn”´§ˆ¡+É0ªJàòJ²Jš•á�Ò½V9KÇäqˆ¦ël«YèBP¯×‹ëUq¤×€`yÁ¤÷ûUòlYÖ ?×uAè$é¢aƒ‚M-'í•+U’Ì ã.[&¥yž“ˆJQ厃Ì.QÍ¿$Ep…‰dÝX*÷ä3ÎܲìzFþ<Žcˆ¸[Ü[ä6¬—È{ïÎ":~ˆnX8å¶-1ñ^§…£K‰²$Ó®R¶Qc¹c¤ŠÅkÍC×uÑoÿùŸ½»ß‰™õªFÎ�{‡xäõ F[ÙÕèG^R>÷òÎû!îçžñ9škñ±á~ìuP™½Áµ×eLáSó<ǰ]öÞø6jýC „�Oß–=躉pH(ñÜýßavÁ¥¶åzR-ë —jh¦C2ŽÔo‘h;ñ9L(6#ȯrYIQ —�×+ ¥TÇE’»Ri¼×ue¹»Š ÎjáY–�eÙKÁÊnNâ5’L:í$=Bô½IÒjIžd‘/#׺¤¯<“&¦åRr«X¶œ�ïaÒYu‚ªÉ)[dR<Úq«±ÖU6Xú}9¦%4MÇÄ/ÊòYÊ]©ätOàMJz´êw‰IÚ€¾V§®Åû—U®/OÚì)Ã[p¼ çý�%·½IrÇ8õ:•co¡óÃGˆ/�Bš’zê¿ûG°s�|æ]‡^(…:,) *Ó4 Ežçźè=D `¸æ‡Â,ˊ߯‚´JV«@¯Õâ“kOÚì:¶AÜmÉ/¿¾wc)W*¤X’¨‚L5»^VDÚDï9ðÝUø$©LRsÍ&Œåõšèú’땦’ô°rQ&|™¤VjD©±Ägõw§æn„f¸ÝÊE¤‰l kÕPP—^èÑj -Õj•/|á o*¹½ýÓ¼«�$æ;v 0Õñ¬8xA@­¼�­• ¹¦ñŸ.ÎðÅÑ)j¦Á¡ªËW/MÓAðŽš‹X'«±7÷B°Ò4Å,•Ù}Ý[pÊ5L·NùXN…jÿ©Q&Ó]ž»ÿÛ̵ªÔ·ÜDâÍ ÒŒRÍ,“¶.{-mG‰Ä0@‡£^%§+ÅáåBî½RfJîL­µv@Æa¥<²<÷êc†�Ù7QûP–eÝëä�'êÚ‰*C�å°§^§­%ÏYä˜Ë´–—nòB“~f×H# C´´¹&.X*ì$ݵ áºU2,IzãêµÄ¦í’%�®ƒ`Ž %ˆeq¥Ú=ˆ{ž‡ž«&Éqª¥ˆ«Is+½ÇÕ†‚³I‰ñHc«31LGp ´vrÛ{}/YH½kõü -Ö^|s^·7–)Ø™Š¥ „2 ¥Œ¥Ò OÓ”Í�2tçšÚwlM™š¨ÕjÏbµ$]Ê*DÉ¢aC«ÕºjoZ>ÿÜr�0‘Na†–¦åîA©Ó^XÒ}(柈ºóǤ\©u ¸*–Æ©†íö-º…âÅ2³›x~ˆn(�x…<àêß.ó�4 É�y@ø¤YJžkrŽ­3zcq¯€*䨡¤.õ[>õ¡»ß²c€ñ¹Yšqή¾ ^dðòLÌ‘¡~ʦɟ�Îp%Œùä–ÞÚ_C×ÏÌ·Ù_q8`oLÄ»—Ñ«’uzqÉ�NsA©6ÈÐ΃\>ù4C‡î ËsL f/�aï%É«ˆúÍ„b3™7†žÍaV¶ 4AÌ{ÐL™ÈªC�Ö—ãwT°V�PkUp„…ŽÔÃl£ëFqmÜ­þöV‚—³Ûív±(“$AhÆWõY°¶l– âLG\¨Ž*ŽCâT+pl¾ç‘gÉš>ëòûåäÌ’€ Œ±K.V©ÜõyO Ÿé8ÕÐÍ qäQêѬM…�Nڢư¦éø¡Lrzï7üØìJ€‰â÷¯$»´<®®ëüíŒÍýSpï$<5Gªð¡ÁüM9�åå µ[nÇûÑ#!¨}þ.Ä¡£ÅÜ)ìb—]‡¦‚fE…ì”b«¤CÁ)„EÐV²,+È’ËdkÝ¿úþ,˨V%2ŒåÆd›úŠó¿÷ú^ M·,«qÚµ3¼Î¶¾òóTÕ«( Hs%cD©t.+‚ró*V®`‘�ë‘<ýlÜd‰<ÍJ#Î4ÌR­pOÔ4­˜»Iº±öªŠYËÙ¼êÅq\Èýñÿñ›JnÿÓ]Ÿ½ëŠo˜{Ë/Ìdl¯•pŒO^YàŽ­#L¥9üú×ÕÊüÔæö–’”§çÚ|z¤�ÒºÔO9zã°šSê�¥Z):¦[chç!¦/œ¢¶ã(y†È™¾ð*ÞñIªäÕë‰Ä6²Îeôl£<Œ¦ë¤Þ‘¹ aTÉ{â°z†ª²<«ÃÚJqXi`övÔT±¡7+mõ•†º^%ɶmïPÅážo-×ËïEzeÕÎÄ™¶¤ú»x­Œ£dA”¤Ú0 I“pݹ¼˜dDAšìÈI×>‡…:0je’$Æ1דÐL fq8Ðu�0ÉIâô*hZ�D©±DÜ¿w?íuVÜHGm›ÿã¢ÎÃÓðø,\à·´�¹Ý©ïWdIõýj¾ô&8 µR,V¯×lÁ4ÍâÔûÔu½èê<žîZQ߯ªËTkÝo,­V«Äi&!|¶Yàþ׺^=Û¶)¹Uü(ǰÜÂ’x厨JrÉåü‘±´F É Ò¥+äb5ù³ëËÒ°ÈìR?” GëåQ*+·J!Ã.9h†ƒ¦dÙÆõ‘W‹Å@ájYâÒø£yÊXhÎáƒäÞãùa¢LãÆ¡~tMã—_½Ì/à¾;®c<Œ˜‹~íù3üáÞÍ|v ¼Ö}¬8T _%ŠÕØ+⬠ѕc‰9û£{Þw#YÑžc`çœúf>A5û1Z2‡n×H­Ýd±O¶Èêï%ÌÊK¥Êø+•Jq│óA·zºú˜žžâ›_ÿt]çW~ís ’¦i!591Á7¿ñüâ/¶Hb¾þ÷ËOúg ‰”<ÏyüÑGxíÌi²,ã_þÎçɳ Ï÷™œçÄñøÉ¤iR´ÓTžŒtÝ(àZ8$]˰õôªVSë8Õ¡"W¥Ðo^%¾¼Ö�T(Š¿ÓÆÔ"âÜÂ0Khi³X9‚T8D‰ ^ï“’ùi4ÃÂu¥üJtp�M“îTÁ šŠ½#Ï7`bÑš¦áÔûHón2èYŠmhÅ»¿LÓ¤^—X£N§ƒišK6sÕ:SX®r¹,«™]R�ªÖ Õ\…C¼–Q¯×IsA’f8¶”ÝêuáZkØF"»Šã¸E° ü:~ᬷÚH3A�è˜vÛ.&a’Óënði&SÛ©u!=-‰!ÓW®ø©ÄòæyŽ×š‚<%ÎK”˲ò´\0|#CQTÑ{7Ðîz}S˜Û¼ýOÞX §!š¼Øª³Ó x¶¹‹½}} •,.§ðÁç_ç##ýüÞ¾mèþäôE~09σ·îcÓ:jW}_w ù¾_ÄÂz½ŽmÛ,,,ôÖ4ÒTnö/=ð%¼ýcÌ_>‹PØŠÛÂ3Ô²§É,ºU#)$�fÈÃ6Ií=„y}EdµZ-Jµ[­Öºï¢ǹÜ!MÉ ­Ç�7Ms‰Óe¯xüµ<Ã^…§T÷/ËH(-ƒ}Å©V˜�¨J¡®ëx�úsyùH2A”Z”ÜZq½–ûä »AÌS2OŒKgTëýç!ÏJ®Là|¯…AØ5dÑ–­õûU\R„ÕÞ_µVC7­®Æq ä²â8oj-ªïWªåF,k e ¡XR£^U…U§A%ÈJ ßó¤´ÒSVF/ªè°üý¯7¤þs?”÷ìØsss"LÉABŠÔWdvß÷IcK 6 "ˆzb©:ø¶[ ˜´×½>L4$·AîEmJz¸b‘f‚ÌèÃu]¢0$ög¤Ã¥&ñ¸I’Ï÷ZB¨Zê�«Ëyž£ÿ×ÿðñ»M­NÍ4¸‚™—é¯`ÄuŠ�SfâŒoîã™ùû+.÷OÌñ¹-}Œ“j饮0°:�+¿`uc°èDeYºƒ;�ðâ÷¿Â¾;>Äùg@gæ ³Dÿ[î0Y&HcŸ’>�iiè�ãhddæ¦ËÑÛP%m•® …Üûíoñ¾|�#G�21>Á£�<Ä£?D¹\æ‹ý—¦ÁìÌ ££yá¹g €›n>Æc�<ÌÓO>AµV£¯¯Ÿ“¯¼Äž={™žžb×î=ÜwïwxòG?dÛ¶íœ;÷¯�>M¥VãÞïÜÃ+/¿D»Ýæû÷�Ó§^edóf}øA:¼„Ù¯‚´Zd9Z÷”ß+9#HµELNäÏ…>Y.(Wê]ñå«uW†–ƒf‘„Í%82M7ä;XÌu‚Db|5MÃ봤䌈4‹r¹Ú­>dD©Îz­bUuê5±X­Eæº.Že’E!±ï!Ò²E<_ïé£�) C»DV¯Bÿ}ýýë^Ÿçyñž$AATu@mtjqf]Í\åh–§¾ïÓjµ °ýFïß4Mì’CœdhšÀ21ðë]¯‰œ²cêA¤6»Y–‘f9–dÔŠlÕ7¨ 2d!A”ḒèY£¯Ã—]ˆ”8 ˆb©*"ô~”¡!ÙäjD©FiY›ÍÔs‰¡ È3Ê]¼åF51‹Ï^VTsòOþäOÞTåöîû‰»*VŸiš1›Å()ÛÙÚ·“²!Ûâ†&xdÁãŽþ*}¶�—eYÎBó ›jlÜWÕRUÝ•Qm¿Þ8 ²í[©÷Qîáõ?ÀŽÞÉ…b~ì ÚóDÿíàŒHkØÄ6š–�Þ9�NLnŽä½Þ8Üëò¶V÷ wôV¿T%X½7Õ[ —Û{}žç¼Dar×2Zís`Qk[u\TE!)gÖ²"C”9”+žÑnÍAÚ!Š3·‚f8Q†È—Î商&@èIÔ‘DR§ŒnºdÈùlëA±–ò\ç6VÉÁ4­‚ó`ë©T¨Is·"×R˜¥bE;ÖÕ~–eE’±RGÍ4M)¹•ç´š di"ÿtßcž/:”nt- !I]" ‰¢×-sz­$ɶí"QÎx ¹7 ö¦Ö¶ºÕ1P•_EB˜››+*†ki¥/Õj•$ËI3YdÈó¬¨ ¯—äÛFJÉ”:Ƚ:ùy.5½ÓÜDäkK‘j ½Ë‹ÕOÏë –)‹¬4Œ®U-*¥¹E%ˆe°±v¨si|šZµB¶±õ]Ï cAuÈ…Q¸fnT–*+ôºÌ…aˆþ÷ãwGí Û&jN8ï±”¹v¸âðà\›/�Nóü|‡{Çgùð`�Oö—ד?»êF”ß9È€©´Õ ªIÑ‹}u’]ßžåÜó1²ïFæÇ/b˜& ³FÿmtÂDsÙ4†;€žŽ£w^E+m&Ó–VqUKUŽÖšPBŽ¿ðGŽ^G¥ZE×5N¿ú*Íæ‚üÿšàýü?~êIÚí6¿ò«Ÿãþû¾Ë[ßþNΟ{ƒÙ¹9:L¥Zå�³g Ã�é©I¶nÛÆ¯ŸezzŠ�ÁAþîk_åÖ·ÜÎÔäç^�f³I’$¼ã'Þ…]²ùÁ÷sèð6oÙ*Ÿ_áûÌýõÇ»÷[ˆË)íÝOjÛ8f¼drE©�[itÌ&%=ÀÔ3tbü Dh&n¹JœèÄIrU‹ªw¤X2L-FCЇ MnRi–F©L`€$·0,ôã8&� ŒŽ¡ç2É ¢Ï›eO2�%-šå’/kÁNV£nTòF×u¬zwS ËÈ¢Ë^[rFµ°”M¥º‡^°&Kw¥¡H jóV2ª­Z÷ê¥.ÑÈÍs†÷^ÏÖƒ7Óœeß­ïåÊé瘟E7 ®œ|’¡ý·ÓÉ·æƒXÙº‘a–l´ö hiì-äèKfJ0x½äÀ´,yè¼xâŽãðêÉ“T*U )®¿sç..]ºH–eŒ��Ñ×ßÏõ7ÜÈË/½(+mžÏþ¹råRW°¼Åö;yéÅã†ÉŽ;Ùð—/�rðÐ!Z­ÆuËlfÇÎ�|÷Û÷ð™_ùUY-ŠB¾ðïŸ}ЬÝ&��!�žÄ{ðúîx;qe�4I¥Æg.5>-[šdñBq*S^‘…tüËv(9U¼�u%-ÁÅÀ[v}„瘖LRý‰Ã1KØ]Í<¯½€¥-u R•¸< ‚øMë*ö’ÎÔéYµ-•�³"{©`Ö{ýjNe+UJ¶EÅŽ°,³k'›’$aœQ.WŠD¾ÀCgé7žÞÖçò$»R© y¯ZGžçô÷÷�ŽÞ@«î?Žc*•ʪÕ%M&+¶:Y–’„ Ò„# øKú[$íištT2º‰m– *8ÓzŽ’øIšc—¤ \œéÝ$yeFm”Ú¸å.©¥=G·ˆÅæµ "Éâ]«z «.â®›áb` ã·"Ÿy§ÝÂÖ«XB,þ1õœ,Mhu¢%Ê�¶§{­œçççù³?û³7•Ü~ð—ᮿ>m™Ç†Ê<2±‰T¯æ9'§ç.—Ñ€�¶ÁVÇæ[“óiÆÙ·™·Uìkþ.Ã0ŠŠ;\‡UhÅ8 lÚ}”‘}74gØuÝ[¹øÒ�&š¦1züQ†ÜA'ÛL$†ÑÓy =Æ,•Ðü—Ðây„=B.¬B¢)Ë2æçç ‚`]]ÒÞ¡º!*iò}¿Ø[”Öïj$àZ­›t��él¹2õ̅•’¼47bÉL@¦U(9ò÷Åþ†®OòÐ%‹tIgÒuoÑδwd¢ŒŽ�šf`èSD´=�¼k‡¥Q$õmÝ"�é`äÞ’¤E­%AŒ×%½UºÕåkùýŠôÓ»–Tu>MÓ5á+]¿V’£ë:Ž¡‘ö$é~·#¸éOÅ@%í·ÚûS&ªÝ�¦)YšJÕš$�\ƺ^{nµ¯œzõU†6m*¬e{cµú#»xC×±L¯ÓÆÑ=L-ÁæÚø¾‡×ñ sÛH —/!dgÊrJ¶èÈ{ „A©ä é¶”]VÅÍrÁÄÔY®c¹ l»Ô%ô6yêé§q—þ�MR!ÊÖ%�i=y„„˜–ƒUªD‚8Œ^™ ãy Ô«lÙT-¬‘Õgªë£8Á â‹Uë�ÞT³ÙD?òéÏÜÝJj6_9“0DDYÆ«S3Û<„ÙýP‡œŽÍi/äÏöl¾\,Vm¾Ñqœ%“LM Õ"ëeô.IH4ƒ¾-{Ðt ²Œ4KÙ´ó0�Ö“gž£3u‘Úæ#œ|æ$õ‘]háŒòºhµFI¬]Ô벂ٛ$¬ÖZRuhÓ&¶oßÁ¡#G9|ä(7Ýt3×ÝpÛwîb÷ž½Ø¶Í®Ý{9vË­ rûoömöìÝÇðð‡¡Z­²s×nFF6sìÖÛØ¼y ûàæc·°mûvvíÞÃíw¼•�»v³oß~vîÞÃî={¨Õê\¼p˶9zÝõ²Rrï?�z™¾�} {ßAì}©òçhÞû-’™ijïz¦S!ˆ ìV¶¼vÛˆ®ª¸+!ü$‹$ǰ\‚(‡<)*i¥îBI¯ºÞÔ3’$$ ì’‹a•Ñ »°•Ô²•�îêz]ËhûéÉ”kIr—Ÿ¾-Ë*XÐBN¾ò2Ü/§O�dß¾²òßm=©*©ªW*šÍ}øAöí?P$¹†a`ë²""z*k¯¼úßýöw8qü8;w¯¯«kØâ¡ü7Ý|l – X2¯Õ}ôJ֨߯Z¨*è ¨ËÿjÕµÙf9¤Y.5i;-,t7VyÈ£�8Îq{ž¿®%ÅF¥NYuÚ]lW�t\(«?®+e¡ÂX�f28ª‘v ¤$NˆH[Ýë»lð\V�’\J6Zºf`UIjžEø~Œi— y§8ŽI㯜<Å•+S¤Ôª² %9/½|šááMd¹Tyãõ³ �Œ ¾ïqöÌiZ­�F_ñît]çÄñØ¿ÿ@ñNÞ¬‰Ã¿ú½q×ùfh^ZˆhæC‚öÉìÅ—éÛ~#§_8CmÓnôàœ¬âj>Qó<‰µ›z£¯HªÕw\‹2ŠÒÁͲ¬ÀÙª¤\i‰*¡mÛE…¸R© ÁòêÚJ¤5ź¿ÒS–&¤±×•A¬JÈ(§Ü=py�¦vµ¥io± Ct£DÉ­&¢ËL—‡Å ƒ�üòeþþë,|ãøÏ>/“ºÝÛY e §\EÓ­â÷ÇÁ\ÑY[>l(3‚0Þ°�îòß¿¼£Ô‹É½–ëU’³RÞ¶tL‚e×*[û° MõX{e?×R™Q°7U{î™óÕ¿ù¸ó½ïçþû¾Ë£?ı[oãÒè(ÕZ�(ŠhµšX–ÅW¿ü%ö8HØ…�MMMâº.�=òQòý¾Çõ7ÜH«Õ¢Ý–×´¦™››£Z­`j_ûÚß255A„ìÛ¿Ÿñ±1ªÉ1™˜š£R®Òžï£ë—GÏÑß(Ójûx�6®c266IšeÔ}LL/†)¶m¢i‚o÷œ9ó:�?þ$G¯¿‰N»M§½@uøò—¾Ì»ßõ6&''1­ÕZãªù·Ú�P‡Œ4 ãÇ­ðú…K\¼|™ƒ{÷rýþÈWïˆHyFA^›B’ꌪ”C7,†J ºµM\æºáA6•]’4Ãí±ÎKÓ”ÝyÌl©‘úyWy£C‘—ÔäQ§‘^LõyªŠ 20J|\M<ÐØ{3#oåüóbh‚ ™›Ÿ¤óÐß²ûØ{™‰71Ÿ Ò¿ð%×"¶�°0ßIJœBR«ÖÝÏm6›KD™UëNÓ4†G6÷^.—»ÀøÅ§‚G__!ö^.—Ù³wï’ `¹ÛvÉóœ��Á%íò0ŠÐ ƒMÃÃKžmÛ·³wß¾‚×þñÝÀ>r= ˆÆ.¡uå]‚Ÿ'Y˜ qª8n­ÀA€)ü5¡$–‘aù år•4uéø-,-]Ç«³9¥ìŒOH¿y·,OϾïSZ§{wñ¶JžÅ²,jÝÀ±ü”½ÚP�q9Å÷}&ÆÇ8rô:Μ:ÅÄÄ8'_y™©©I>üSãô©W¹éæcœzõ$�ïóúÙ×8vËm¼ôâ Æ®\á#û8Ï=óc¦§§ùäÇ?Ä÷¿ÿOt:ïú‰wpøàN&'&9tè£/1qù ~ðÀ}ÌÏÏó‘�~œ™™i¾ûí{8|ô(~ÿn¹í6~ðAê�:ïûÀOòü³Ïpç{ß_aõûëõzä�u‰�ê÷ÏÍÍvÔŠ(‘åÆ –!“Þ$ 0-A¯ÉrÉHÉóaǽL­V'ŠJxaÀ(ó\dm´øŠ&äõIÖ¤9ïc•*R—1)†RrÈÐR¼Dh¸´#C°µ* á¡‘à˜)yÞÂkùèV…j­ß÷Èc�’‘¬X ]`è1AØÂ¶‹ÿžk÷wÿÈ'>õ)2tÆ&æ˜��cë¶­<ñä�Ù±cG/³gÿ.\8O½Þ`bbœoº™K£ùñÓOÑh4¸ti”›o¾…Ë—/qðÐa~øØ£»åVÂPZ¿664GWÕu}–fZg¤bóÎ]Ûh…»ëÕ%ë5Ž">V1ЄLÄJ¥«Ý ×JeºdË2�«Åá4M‹Šh‡�}»obäàmœùÑ=ØŽ‹ßœ¦37Í?~€Íoe&ÞÄB>Hßü³”Ê&QéFææ[hºœß½¢úªzÖl6 Òl–eÅý)Ø�Š­ ¡9J"LUúT‚«*°ËÕT…p¥Ñ»¨Ê¼Z‡ª•½Þ°Œ”’‘' ´Bƒ’S¥Ñ·ØnÏÍ\[úK×bâxžv`Rr«è%W*ò ›%Ò—O0õ¿ýgHbò(CÇ{îYÂS¢ïW ÇŒIÓyÚ-‡F_÷=�wõûWYKI¦IÈyQ¯T$_c#âù°xØVøTu@WäØ�^ßn·‹ ¿ã8E‚l¦ˆ`•)#‘¤1�–$Ý)EÕåÚHþ¢»I“¤)£/0??O…<ñÃÇ9î <¯C«ÕbÇŽ�LMMðøc�01.cÈ©WO¢ë:“Œ]¹ÂË/žàĉã¼òÒ‹Œ]¹Ì­o¹�§~ô®ëòSù÷ï’{^§Í¾}{øÒÿŠ<‡]»÷ðÄcïþýdiÂÉ“¯ò®wßÉ©“¯P*™ìܹƒ—_>I£^çÖ·Üʃ?x�jµÆM·ÜÆsÏì’C«Õ*È8*ñëmS+™2\{eBT˪78ž}í %§Äüü|áTvöµ3´[-æçæht+@‘¤éºÎÓO=Áž½ûŠÖ�a<þèÃìÙ»Oêv«ŠU)„VÖà{÷�Œ]F˜&Y³I籇pŽ^OëÁ@ß'>�i ¼0Ãqd¥*MS‚(‘^ëk¼>Á"¦QUÒÜr•\Xdi(ÉkUÒ€$Ó)¹² ¦Ú�In¬ÐâÈ�,Ó’EÙõVÓ5\kôžâæçç‹Ó÷Ù×ÎðÒK/259ÉÖmÛ9÷ÆYÞñïæñGaüÊöíÛÏ#=Èìì,?÷‹Ÿatô"I’pã�7ñâ‰ã´šMÞõîwó£Çãܹ |êSŸä™gžåúëòƹQ^8~‚©©)n¼ñ._¾ÄôÔ$ºarÏ7¿É–­'ýÒ‹Ç™ŸŸÇ÷=víÞCÿÀ�=ò0¥R‰}Ý*àòßïºn1{“�µF/žWvI©¥˜AÉ–ZŠi¦6ÑåÒK²Å§áõਢT ¿ !µm}e•ÕÈ“/ˆÑ ‹’S&Ã"Šáþv�ñªÎ»ÿ0eðÕ ›÷ @¿ßoêYWrÆ´ìR?’ *ËñàBhÄç.ÑúÎ}´zŒüÊæÐ0iÉ!ð;8N™o~ãëØ%‡-Ûwñå/}ÐÈr¸|ù ó M^~ù&'§yéÄ Œ��155Åî={™™žæ…çŸ# CöîÝǙӧxùÅ´ZM^{í gÏœaËÖ­üÏ¿úK.�^äáG{S•Û/üá/ÝufóÎ}×qrj†²i²«QcÀ±Ñ{‹®ëT4AÝ2‹Dm£CUôU ªV«…|VoGañ¹®‡Ã(îÞ—ÆÐ®#¦IØ™gË�cÌ�_`øðÛ0Lt‡f¶/(á¥Ò~Ûóc±#—J;aË’°¢ÔßéÎÐ2ü0£TZÄÆ_+ìmôâE‡øþßãÀ�ƒLMMb—JX¶Å¥ÑQlÛæŸú^ªÓa}Ë^*›iN^äÈO|ݶ ™Ç­EY’àû~¡0;;[Èߨÿ¦†J,<Ï£\.S­Vñ}¿¨ õžú§¦&ñ<�×Μfÿ�LŒ�w¡‚…….]efzš¡M›˜™žf``�c·ÞÆÙ3g˜Ÿ�ãö·¾�“¯¼„®xžÇã�=Âöm;8{ö ÃÃ#¼ðüslÙº•£×]Ïìì,G¯»kÿ!’Ñ $ããFDc—I;òÔbîÚ�(™D™Vh)ª\¥ÚG…xA«Ë¶\‹M™ã˜)YÞ"èø£BÉm>aÔ¦d®L’è‘rQ­OÂ&Q–¡™ªõ|¿CupÌ”$Ó !ò­PUÅ9 C\×¥Ñh ו“:<©ßœçyaŸœç9##›I‰Ÿš™™áÕW^f``€©ÉIžîYìî¼9þüóÌÏÏa[¶™aÛÏŸçÌéÓTëhú(º±Ô`hhˆ9cŽñ‰ Ž?ÁæÍ›Áõ7ÜÈèÅ‹ìÙ³Ӵصk7NÙÅ)98xˆÿ÷+_æ>òG+2´äá¤ÑhÐn·7Tyè=´ÅiZHÅq,Ù¥kJw ®“f tZåjQyJâÖ�{zŽ®…DALØ”ËUÌJ�VSã7¶Ãk¾t*zt‚ìêßbê9¦G^(…ÃÁÅë´0D -H…FûÁǘý¿ÿŠÌ[Œ'­ï}‡¾ÏÿÎÑ=D~Âæ-[ø…Ïü­V‹j­Á;Þõ^榥ÔàÙ³9z=ãSX–Éío}{ñüöîÝÇÁÃG8uò.]ºÄáÃG¤|`«ÅÂü|ÑÂ¼éØ±µÈZϪý;ðrÍâgí-þûò9!„(bе U9‹¢¨°¸U„«õ´’U <¼Jv„X¦QÄáʦ�ìï¡59Êuïúiônµ ”JÎæ"+LëJq¸·Š«äú\×-â°ªXw:�âïöªŒ¨ ¯úÍJYE]§’ú�T÷Pé‹«ÍÔ÷ý«¿ËWq– ìR¥¨ú¾·ØóZXz|•¦èòa99>a'"Ó\ô‹çIƯPyÛ;)½‘<ŠÐð_9AžÄxÏ1n·£âÐñ[ØzŒ¦A'²ÐDÞÕ¿z¬ÔQS�Í•†r‘RP!óóóضM½^' à ét«ï·„Oä¤ÂíÊ$xAk]Ë$R B_Zh×jµB-a#£Z«1<2L«ÕäØ-·„óçÞ`hÓ&*å š¦³ix˜0”ójÓ¦a Ãàâ…ó4úúäÄñزeÏ=ó4�v!¤ª®eÔª•¢Q©T¸ï¾h4êÔëu<¯C{LŒ_æ‹õ—Üú–Û½x˲9vìVÚ톽8Šçûx^‡ù¹y‚ ä=iûî½MÓ¸óÎwE!w¼íí|å˃iXÜùÞ÷273�e[8ŽÃÐÐ 7Ýx=ÿøÍïÈŠ}ÉÆÐ—Î¿r¥AÇ´ý%=)°ãÝ·E†N_�;n½™WÏœåmÇ¡å ´›¦]¦Ñhày^¢kFAZSýªç/„(4€Ç¡¯¯¯°CWݧåsI4ßøJV?¸¡üf†ªÚ*¹-EŠ Ã°ÀB^ËÐ4�8Ž L• bjôV@:yž×m�dŽ“¤ ;#hš†ïûT«Õ[¥‚´º^Iý€$�(©˜ÙÙÙ⻞}æi<Ìëg_£Ù\`vv–J¥ÂÁƒ‡½H†\¹|™;wrÇ[ßÎÓO=Áû?ø!¾õÍopìØ-\¹|™+—/Ñl. „ Þè+¾«¯¯ŸÉÉ n¸ñ&¾õ�_ç7ë·©V«D£¸ô/?‡¹uz¥Fpú$õOü óÿðU†þõïRºãV‚Ô¥Vï“¿æ4–žÄ]�Ñ’ôŽÎÙî]OÓd�»TÅìz’‹Ì¿ª]$&åÚ�¬ÚúmôTúmg9x±EÉ©`š�N›$ŽÈÙX@mb 3·’® ò§_þŽ:�6S““ ±uÛ6.^¼À•Ë—9zÝõŒ�]¡Ýn388„¦ëŒ^8Ï�C‡HÃÊŽE’føAÈè¥In¸ù6fgf¨T+´›3 ÷;´:WƦ¨Õ*lâ•Sç1í2›7o¡ÕjâÚ861ÂdÇÎ]�<<}û›ÿÈ/}ö×V\ ®ëz¾ …#^/†m­5dõz�N ‰|®½ˆ£ìt:è"¡dHÂÊZÃOJTëE[Ûë´1DˆmlŒpÒ *µ¾A«Õäæc·055ÉŽ�»h·Zœ;÷3ÓÓüîüáµ´î˜»ð€—¸Çœõÿæ›®ë›yaG¼mY5ŠDR׉»I‹:€m$‰yx™$Õ–ÄaÕz^/»]Ku•÷¥” d%Õ?Ž]×¥a‰Â+«ß�þ~‡Pæ 2Ñ+¼oê)®¹ˆåõ’µîÚi-ÌJ[òXGN·ðJF¸n%W>SðŽ¿Êôþz­ÎÈþ1ÓÿןSÿè§ð^z�öCß§òî÷Pû_£Ô_`Û³p]¤ä¹À�5t³LÉq‹„[B6>oFUþ—?'ÕÅœïW»^µþ×úîÞgšå‚ ÖÑL§{ÿIÔY1ÉÍsA”»TjRc½Óœ”•ôXǰ¤cæJ÷¿|èºN£Ñ(ÍT‡Pñ†V’2…EÕ›J¥šNœ¤Rž2K{Š7¦–b©$Èi !1$É[ùÚß}‹Ïüʯ‡L�E�0ô±…‡®Kó£æy¶äš¥ÏTÆâÞù“F-lCr3’Lãø«9xàý} N¿v–=[*äYJ'6IRQ¬#E�.Š-zJ¸ŽÎ½z¾JgX=ÃåqLL�ŸËs½²î‡½Ù¡ÈYªµÓh4ж\›ðr–e„QŒã”0º�Yé4*kÉ•FÁ„Í=6¹¯‘w.0Ã;ˆhLgœïuðQÄ `IpTŒ\uïcW.³yËN¾ò2QÒ?0Èàà Íf“é©)¢(bhÓ&vìÜÅèÅ ÜxÓÍ<òЃDqÄÑ£×óâ‹ÇqJ Üèë#I.�^d÷ž},,̳oÿîýö=|ôŸ,|Ú[�þ€©ÿú§dþ"µï3¿Lí§?N”ëXî ¦iÒi·0ó…"Uâͦ-[D¾×Ad%s},@œ ’ÜÆ*UŠVËl#“ÑF£XôI¸€± ô§RsÑî^¿Q— w¬ô]{Å¿•8·jƒ­v½ÂÓ­…ãѵ®O<=zÁH9Ÿ»h3†A-±ôÅ äUªÕº÷^˜Â1c„&Z r­DÉ)sit”<Ïhôõ¯X¥SD¯¸÷Fï¤ôW†&«¶–At:�«L0L-Å6“%ö¹j$™@XÝ�Ì'òçA“¿?Ë2¢ …)¢u ‚´LµÞÇ/ÃC30dÁ\S!üçÝŽ®“ÜÊß. a¸”œ2 ÿæþç_0ò{–M2=Iå=?ÉåÏÿ3â±+lú÷w“=F޾¤é8–e ûåɘúwÕæŠÍ«Ñh “ê"¨Mm``àM%·S“ãBÿÿ%¹U‰„r„T•·^²˜"umd ¦iFǔ٢WdÜõâpš¦xž‡FĈû*™7ÊLv!2SqØuÝ"aVqXÉ6*=UET÷¾¼“³ü^”ÄWÇ…I…ŠùI®–?O¥Ûª0ɪ ˜$1+.‹qª!¬¾.‰Ï'�æ re– ÂDG3+ÅÚÊâ¶±vG �˜åÊ¿ùIïå�w;ØO>‹Úµo i”U¸Îa}ãrÍ W—d– \EÅÂ*ËÖ2I¶Â–R~lµ„/ùŸ~øx¸¯MFYõ,‡a�Ï V©sl‡sE’¦¸>F‰cÝjÞ¶m"�ÅŒOcù5ìø™¾�0á(Š¢ 2Qbe‡õ•a S�G¢4£æß¯p `8:j•vmÉ’~‡Jù±$“DR"‹ ذŸTÎ-‰I¦iV&†a’¤ $$Ê¡Y/Þ�¨¿¼ê¬ °åä1aXh};^�(Õ�™Dˆ�ã°^ó‘óK$—?Ä>t„èì)Œ‰)T¢dŽû‹ÿ ÞøTÅyÐóþªCúð÷+™Æ… äG‘_*ÿ|­V8PÆ7³õšBÅ)®ãᬱ•/G®4Ì5\�µòSº0qý©$É2gÔpœÁg&­U r9_f1A”â8ëË�–Ý2Xßh¤Ì†eìN�<ÁÙÓ§¸{÷{öìt; Ò´°IÎŽhš'Þ—ÉÉ)„б,‡^��kº®§ !Œ¢ •iV )3²4b™:qëвQÄ™†”�I…i˜$Y¡‰|åÚmR©°œFσÞÈ�8É <­éD9¨|S‡Š[1àFXvÁ�ˆÅí»÷p™¥ìÛ1ä¤ÒXyY�§ÎWá±7¥  aPÎ>:œKŠ/ùË¿±é'}ŒQbËvÍzR e‚RJ7mäNRü½×Y!P”óK ÜJög°A ·ÈÊ›�â‘mÌü.–×ÀNΑk5z±S¿ÒU×5æiŽŒ�ôGà àÆ�ëÔjõÊ5*ŽãUE×unÏÎÐh6™¹u“É©©JgµüÝK‹ ,..Ðúä2gdt”¥¥Å* ܽ{‡‘‘ÑêzüAwåÃKLNM‘e…ž¹w?Z¡;Sÿý¯�b§+’3ý^G¬ßr)gVˆ_Ë|È¡FÝçö´Þ0t…©§ÄQétÖ(ü¢UÉ¡mlM+t…©etƒX-¾½Õ$·Ü ÊJ.ð@™—ï¿ËÓÏ<‹?zée„¡qâý÷‰¢ˆ çϱ´´H†\8–(Џxñý eçÎí\»9Ç{ïŸ'Íu¾õÍo# ›$¸uk†z£Î™S§‰ÅÄÔN^üþw™¹u“‰ñ1Nœ<Ç�¹e ã „ÃÛïž"“pþÜN�:É¡ÃG˜˜œ¬Ú±ÃÎcëI•×?œ¤Öëõj>@£Ù$“y¡|☫$—Êù%é¬Ì>qV´¡J�œ‰Jt¾ßïcéE@Ö´�Æì@—³Ò˜•‚,+*Oåã+¼îkÕÆeÅ‹|®ÑæÇj]žuú³j¶�iû„IQܬ \Jåí.Á›¯#[-œCGè|ï;X‡ŽŸ;EÔ~áÀÈ8/|óOX˜Ÿ/²ƒÍX)ÅÌ6422BÐïp›^�óçÎ kÙúË$ËËËLLLpýÚÕ,¤Ëââb!ÔéðÊ�^âëßø“�™Ü~ùo$¹]+ÁXê|–•޵ÅÍâpœ$8öÖâððA¨4iSU#¬|«6Ž•œ'WýÔÃ4­ rdFuà³,‹4‰¨Õëü㯽Às;§q„à—¿÷2Ïì˜"N3LÇ¡æ:,‡/ݼ=Pï0@˦†´ƒ€$/¤ ]§›I²,E¦)KiÆ·/_ãÏ/]dzL¶Õ}tMc`K7Íp A¢¡Ìq ƒ^šÒ Â�[˜]Çq|–œ\»CnX4l›nÐÇr Ë4ƒË±ÎLªãš‚†™¡²ˆh £hÚ>Q¢Èó�Š ç±Çˆ/_¡ûý¿)IoÝ@¶ÛLüê?Ç?|´zŽqci²(/HkQ\ju4§­òÏKö°ãâƒ*â†Ðq-…%Š}$MB’”U2„å÷z¾A5s%I%�é‡)¦iãzõUØ ×Æñº‡…F’‰UZçÃß_¾Ã:Ê]9ÿ½·ßâK?ósŒ��óæë¯2;;ÃÌÍܽ=ÃÉïsíên\¿Æ­›7¸zõ ËKK¼ñú«Ü¸~�³gN31µ�Ûwîñ½ï½ˆaú¼øâ9yò —.] ÛépñâE®^½ÎÙó—xëͷؾm�Œ?þ£?&Ë2^{í —ÚÜ™[âäÉ3œ>sŽv»K–iì;px`×ÜÇÒ‚ÂHAdÄQáTçùu”n'››8è8A–Ä$™âìÅË´;]Ú�Ÿzê1LQ(+$R�«õñÜk‹5e5v£<`X¢®T0Y[ìúKnK–²ZPZ�®M<Ö»°µîaœ ôÙ6¾1J^éåÜò;†OS½^�ÜÚ� f0ò%L'¿‰‘wè¦M„QànK’Â�þòH)™žžä½wß%ϳ33,/-r{v†{ss†ÁòÒ"×®^eÛô6Þ{çm.x‰G;Æ_ü>KKKbUÁ>ôk5d&yõ•—9pàï¼ý6㓜xï]}ì3·n2só&ׯ]£Ûiãû>‹ ó,.,T†Û¶ï¨N�ý~ŸÄ´^ ãðC›ÔvW˜ÍIû�`{XíP“å¿VGb¹=m4´Á|�„ ¦:¹‰A‚¦=XY!W:©4+Rΰ«ØGq(±,«".–˜»õæ_ùðͦ_D„àƒNÐl6¹{÷ûö`væÝN‡Ïá‹Üž�å¡GæÌé3D DQL»Ófß¾ýã\).]ú�éé).]¸€:išqäèC\½r…gŸû$çΞ¡Óépåòe²‰-M3z½>Rf,//³oß~ü޶„ ”ky=-Îõ®X²Tø†A’JLC ¡ÖmV÷G†dÙÀÂÓò¹›X¼Ðir&ò9š\ a�ÑÃÖÓ5óË�%!ŒR ÓœÞ!è"¦Òĸãõ{Ý¢’=øZ¡+L‘‘%i–ª.a,�|s§²ñq·Þ&܇#8ñî±'‰.œÅÜwûg�^…ŸþÌgq‡×_{…{wçØ±c¯¿ú µz�Þ�¥¥%Ž>ô/~ÿ»¸®K»Õâ­7_çâ… ,,ÌÓi·Q(^{åe¦¦¦ùú×þˆ/^dvv†Ý{öpåòe¾õíþ£In‡ÉU%G¡Lø€n·Ëñ§Ÿáâ…óèB§µ´ÄÈè(�?yœÅ…<Ï£ÝjóäSO#„`ÇÎ�„! ©ù5æïÝ£Ýîðܧ>M¿×ãÏ}Š«W>¤9ÒäÎíÛ<ó‰O01^ØÓ,g~~!O=ý ÏŸ/ìáóœÉ©)¦¦·1=½­èèÆ-Ì¡Jv™„a‚Ò �,7ˆ‰Ø$ÈÑ™[ìÅ)º®ñÌñ'¨ÕDqñüiÀzÂe±fX_ÖÏÊ|r­¡QÙQÑuýo&¹-«AI’' ™ã DÑ7bW®­B•ÂËÝ~€mš ôƒGYå,["¥ é2ùÒQY4iíG Πem„7ŽkEÔä9‚H‘ÑÀvùì£ÌÝ[ Óé2½mš;·osàÐar)™c÷®ÝÌß›gמ=LOoã¹³ÌÞžåèCsëÖM\×e÷ž½ìÚ½ÏõXZZb÷ž½tÚ-æîÍU¦åap˜ þQ�Ú ¸O�h —çÚÆõ9 =Ç·RL½¨ò¤RñõV�WZ‚tRxq~b\Vr]÷=ƒÁÆ’gQœ¤AÛ+Ü™,¿jý“ÞØÒ(’\CK £©À¯5‘X;ÝTL qèñ[¯Uðœðƒ÷cãLüêo’˜¦apòƒäRÒíuX^Zªˆº!8þÔÓ¼ò£—xúÙg1M“kW®`ÛÎ =ÊÓÏ< Àü½9ÆÆ'°,›ÖòyžóãÏžN§Ã3Ï>Ç�kWùã¯ã?šä¶4 ) ™Ìq]·ú÷�µÝ€<Ïé!޵~�a½ù°rÐ-ã°JÛ¨¥qØBZ;ÐÃ+h²…pÇp¬_ž'ŽRFpœÂª=MS\Ç·{�Üä�Û†n1Ûé1æ:™ãV«Ãr‘æ9ONŽáÛ6¾ãÐNºqJÓ6ÙÛ¬“ÈœŸØ¿›îÞc)Œˆ2Išç<29†©i´“”Ùv—# ÞŸ[àÑm“œº;ϔﳆ<6=Á|?$•9MŽsu¹E7Nqr¥˜ëu@åìl6ÙÕl`�¨¨>9=ŽÐà¼ÞåB?ÁÔt|Cp;Jxq¡Çßß1†¯ËU1904‘XëVÒ4ÇÂ}ò)ú/¿Ìè/þcÜO}eÔ«öl’ÄiM ’dãbEœš¤ƒ–ñðA§,6”‰òƒ†Rªr»ë÷û•Ì[é4&¥D²¾Nûwÿ%É�kd ó$ׯÑù%Dsûà> ‘cèh™fP¯ ©Ü¶DK²\T yžc˜Qœ9 �~b‘J�ÌõûÔ!†¯¿$ï–Lý­âA÷îÛÏÅ çyü‰'8xèׯ]åùŸø"i’0>>ξý ~ÍXAôX‘ <|ô ã#5&FlêÍ––Zúöî+´o9JÝ×Ù¹cŠ];§9|øK‹ËìÛ½ Ç1èöB:z„k×®c:ŸùÜó€Æö»Ø·ÿQ2>1A0dà³úÚPUäbP�3½€umà8‰&x÷Ì[=¦§&iRsM4 ØZš½­ŽžTš çê*©]H)Ùœ–ö1Fyòó}Ÿ{ ‹œ:w‘Ã÷³gçö-mÒ¥ÄJ&“ããdYúÀöÆð\(Ú%i"è÷‘ñ5jN;¾…ì½Lž TžƒS' æÑ »9ÉÎè �ÞU:ò8ÂÛSU/¦§ æÿî= £€F£I½Þ¨0:¶m³cç.ò\röÌ©•dDƒóçÎ1:>Îòò»÷ì©ðc¦YT÷îÛÇìÌ-,Ë®®áË1„A½ÑÄ÷}¦¦¦8tä(ñ Ø W\,˳u\3E±"h^’áj�QâØ#Šú¢ �iÊ¡k ÏÊ�²�*”ƈÂ>dØ\[CvPJCjnY1�»˜¦A¯`;>ºî=tc›rÀÆ,Èrù/(’º(Šðâ2>âiµn€’(¥Ø³£°SΕ H ÒÈÔô4A¿Ã£�æø¡ò”]?ûE^ó$Ï>ý8ÍZ�?ܽg7^­ÀUÿí¿ó%ò\¢.‡!ŠB¢hšrðð‘Áóß_솻 PejµY–UlÜ­¼¦i’å9J�imÝU_lcÈžÒ,LrUç³c}øÌ(ü»™�äÒW�R¾+ ÈððüFµû½.5kãõ§iàY’\õ ººéSkŒG!áD›XÔŸ|û+¿Å�ßüUònsÇFùW�ÍšžG’$|þ'¾H§ÓfûŽLLL¡P]`;K‹ ^^Jɧ?û9:í6SÓÓô{},ÛbÛöív<‹ì½‚”zÁwÇ�á"šnâ4ÆÙ‘ܤݽEG>Žãí#M3ºÝEÖ2Y’2ê7yzÇvÞ™½ÃžF�NÑŠ"44v5ê¼zëa–ñðä8ý$%Í%BÓ°t�ºe"4�×áø¶)..,1ê:¸B€RºNÍ2yN&3¾{ù:µý$Á5 ®.µÉTN,3f:]ZQL"%{šu¾wù�8âKGöðGg¯#„àÙ];ºN;M‘Â'Óqï6ÇGjÔk‚ÿzïv:YÆ{âCÞí$üôÈÊ}tÌ™÷èwB„åã×Çøû~APÕ�=ÇÄúÔ§Éy„~ #ãÅ¡" º˜º¤ßM¦ƒß/ªáƒw©˜¯!•¶¡üRÇU± 4„Ùh »À•§üûžçÑl6 /_dùkÿûÈÕé€ C–ÿßá=÷ ôFáÈWHEvh·¢Â–½6F†äi€kÝoPŽLjXéµ(Šû-„éàÕšƒ5\°s¥­+?9|=¥¦j)Ãø ÒÛڹǢ�~ ¤8ÇÞšÆ>ÿ&ÇkäyÈî]ÛÐ�v6>1QTÒeíٟЋ< 0±°ÝÂ' ûhyt “SìÚ¹‹¾÷íÝÁîÉíH™TyDyÿ>Ê!a­‰G«hÚüüüVö¬-�gcY®çqcf–Sgϳsûvžyâ±-Õ•R„QŒm[]¯Ú¬[Õ÷†ÀíŠdáU´à†©¡›>JIò¸ƒŒ–õ� iäýy o„EÚ_¢›l#ö>MŽY´NM Ó²Ñ5�dÈÒTJ‰>h ÛŽC†ŒŒŽàªsYšâz/|ëOù©/ý�Ud†²�³:iQHY€Œ�ÉDyÖmMr[üU¸áä&Í‘Br¦Ó^B&¾¿â†Ç1iÔÁé–¤¿�I�$/ä» «Ì¦¶:IŽ3�íÊ ½nƒ.¹æbÐ'Ít2<¿V°¬ÃnAF’æËðZ(Oÿë)”RWeÕví½fE'ILõ°×´¸r¥¡±úDºÜÍÐÍZ5¿Ä�giˆg¦9C�l…Ì5rc”Öò «kEAê´z’4“lÛ¶½ª n´†K)³aV÷°²ÁV4IÇÆÆˆÓ‚Pã°¶%±síü¢j{?Žì÷¶ÑÎ o@˜Ã wá—vEìe×ÚØ�fx$™ŽáŽWbûa’Å¥dÌV¤çtâÌÀrŠö`Ðï¡dT äË\CŠ‘Á³MhÿÅ7éüÞÿÉöÿùÁ<°§°þÚÔÂ0¬´l=ÏãÔù‹\üð ûöìâ¹§žÜÒ­”"ŠlËB½RiYO‚o£áºn…Oæ_AO" ݪq8é!ÃED}'š¦#û÷0Üq4a“Ë´Âbÿy²\Ç6 <]¬LÓ"N“ªØK3L¡cë:ý¬Hf'FF²Œ,I° �cÛô‚€}”ÿü�‹üo�à¾sžß9:É›ëßS9P60­–m†}TV0ãûý>Y#M d'�F£(0ÅK膃®BòÂÌÆõüBO½_(ÜHUT/7{Ëw¡\¥|ÓðÁ0 :�Î}ØÔ²Ø¢¾ûm–ÿÍï2õK¿Fã§€¼×eáßü.ýW_bô¿ü'øŸûd5/WÚÀ­ªèÐù¾�bP, °�ûy˜ÙÔ›…Ca¿³€-brI&ÐÌúªXð 5<¬¼S*¢”ßÿ wÀ4M^~é/ùìçž§Öh'Ù–W.åüÒmïÕ—_âÐáÃ,-Üã¹g©ö•\i¤4¨Õëœ>u 8òÐC„ý.¦ž¬1´Q¬´ý‹ÿ½°°´´Ì®=ûhŒLV9K¿ßâš›WS öñ¢Š«”" »˜Z\}”8þ8¦irîâEöL×V=› 1È”Y)R}\…¤²3�$ ívû�óůýÚ¯ýÆG‘_ØìAP-‚^rcf–GŽáñGŽVNKªg™$ISÇF Z`Ã~ã[º !XXZæÜ¥iÖë„r ­öÂ!�–É“ºicxS¨¸ƒJû’<飒.†;Šë¤Éu};~~GÍc5WÚÙåæWþ}ÀÂö} 0=hý p8OË N‰!,ÉSo¼þ*{÷í¯6~!¿ýÛ¿ý±` ¿ò+¿ò•<ÏÍ¿Ž8\¶T¡x_Z�.3³wxî™ã<|ø ®ënH+G‡í‚µ +qx«Ê †a0¿¸Ìɳçi4ˆ‡Ú“³ŒÃË+q8iâð4yÖ'�;î(žf|ilÃç2fvoä(2ËPR Ûaé:ÆàwXº†gÛøž‡¥ëÄA™¦¤I‚ïº¸Žƒ’E5Îúj8]G\“¡i˜š†È4Åd qeYhƒN‹cŒÁ{`  Çñ¨{zž£Ò5S"dL’�˙䵥·Â˜IÛä½V�å$á—ö5±µõ;kIS¶ãa95 CàúšÕ Ñ,:OQÐÆ AR‘‡ûAF’fø~�mORù‘aOå^\ª åy^iÖ–’hk?¯ŒãòÔ ’³§Èãˆúó?‰&ɵ+„ï½MråCÌÇŽ#÷`<Qf"s}U7 MÓ"I7=Âd€+Àî²\ǰK‰Ï‘÷ ²ŸV`J‘1a”•àu`kå(UTÖ^ÏF¤³áù½^——ôCvìÜÉ{ï¼…m[¼þòK4›uò,ä­7ß" CÚ­^ºÀûï¾C¯×edd”™ÙnÏ-’kï}pŽÉ©i|ßç/¾óQ±{Ï^LËá­·Þg±pkvžÓç.SkNpæünÏ-‘H�wÞ;ƒ.,nß™#“pýêU®^½ÌØØ(¶íb95ú1ä›xË<ÀÔ³�RÇR›0ÉÑÈQbE�©nIl³êš"G#J-÷;Ý}·m…46LÎ>˜–0Œ$ËÉó"qŒÂŽ‘Uc¥«$TžÒZC÷*ÍmõùD>߈ù™ñ„_˜LØ¥Çõ¤¯Xß‚1‘Ç`°û=,-@S1Q´Zò(—Ù¦„ÇJÜ;ޤ³:šp°l·2„1ä2©Ìöƒe`[&h:Ré´–—¹{÷ï¼õ&swïÒëu¹pþqsêäÌÎÎpô¡‡¹3;C’$:|„?{áÛ8¶ÍÙ3§X˜ŸçÕW^æÔÉh4¼ýÖDaÈ{ï¼ÍÜÜ]ö8ÈÝ;wxíÕ—?¶ZÂW¿úÕ¯èºnnEg³†a±�ÔjÄIŠ0 C�$)¾çVp…õÈ»y¾‡‡U`ýƒæƒâ°ã8¼ú aѦ''HÒl‡è?JÇäÁu »†°äq«84—qXÆØ~GŸÇ±ú¨ða>�ã�U›ãÚäbû¹Öè¤Ü\׳�Õ4�îCö�6YŒbnt{L{nu°)ãpyíBþìò öŽÔ9qo!tƒÎ mÓ…(äòr—Ý ]]ÍÇj5>èEüé�ENwúÌF1ÿâÈ6Ùæ5À ¶=K¢;TÓ%JÔÀÇ$Ž#DÞCQàh3YÄaMƒDšäŠû˜åÇwX®°Ó�ô€Hb¢W_‚L’-Í]:GçûF|ñy¿‡ÿ~Ù#ÍõÙx5\bí÷{~ †˜ý™rðüâþGýÖ} º^$\½ «’Ô²à6¼�2±ñòAÅMÓ¨Õê,,Üca~žÏþøóüàûßÃõ<žxl?çÎ_bÏž=ØŽËÅ‹8þÔ3\ºx�ÉÉI¦·mÃ0 Ξ9 šÆ;o¿ÃØØxñ{š#¼÷îÛäR2>ÖDhŠËW®’çEÇò‘G�qîÜ9¢0âü¹sÄqÂOþä¸7¿À7¿ñ ¾ðÅ¿ÅõkW‰¢ˆ#‡v!“~eçüQ¼¦P­(Ü)¾ß SæŠ*E¿ƒgƒÂ…ª /2׫bß0io=Òáz£|VQÑétV­ƒÄ¯ÿú¯ÿFi+[.ò�“à3muað�ïÿ€¹…E–—[<ñèÃÀê*Új º$•Ë40„Øðž?LvN�LÓ¤„ÌÍ/`[ûöìÂ"X%IBœ‚Ý<Œ¨?NÜ[DEw0œæ �ÔBŒìt²î †e£éa’~‡ ŸÖ_Ôeey=�Õáä¬üíÀ¦ ýáQ&‡–eÝG¶ÖN³Œ,äJC×d]’˘0ŒÑôBàܲm Ë%L)ð¡�Å«k(•öݤÞY±d�Ú˜B‘æi–¯xDkT ýây¯€Å=Ï«ô,·ºîÊùåú)É‹kçëšti´j#¸~í*Æ@b¨V«Ón·1M‹‰‰ æçïaZÖø?A½ÑàÞÜûöïçìéSB°¼¼Ä¶é nÍÌÒlŽpëÖ-vìØA§ÓÁ÷}<ÏeïÞ}8x€Ñ†Í�¹e ÃDÊœ0 i4ìÝ»•çv¬°®®Õ™˜˜dqq�ÉÉ)vìÜE’ÄÙØÐ¹qýA°k×Nê5›ë×g�2£92Âô¶í´[-–——°,‹ƒ‡qúô<×ãáG­ºUqrôð~L!Zë)2·0,‰I’ÚÐú Ú ‹ Ó {FÌõúLù¯Ü˜åñm“Lù–ˆ3É‘‰Q!øÂž(MãwÞ<Áß?ö0 ½S�Ý8av¹…måW_|…ú™g96ÞÀ1Š˜”e’“íŒ?˜�ùŸŽìª’+)%QÐ…<¬ôA7ÃØö’<›§}4á'[k¥ëÞVÖ@éÓž�ÓØð^n ‰k¦„©…0=äÌ–ûŸ“ÍÌTŸc<Ìø¯ýÎŽ�[ú~kð™P:e8^³"ã.-.à[ѺÉm?- ]©­<ì ù 1\Ü*92Ù ÈàÙaÐÇ �¦R@CÖS h)3i L¯*ÂdIˆi{•=±J–±,³È,ó¢ã¦ Pä¹"ÎDAþ8…©¬�iReáûE0–¦E®4“÷/œ¹F9SãŸ~¦Áh#Û²S^˜9Ôc«FA¿‡©X¢P¦Xu¯(’†*ÖÞ¿µ1H)ÅÈÈH%Óº¶pX楳a•t:U¦R£­d¹–Î#›-r¥ Ô²m. ƒF½Îåk×iøþý2^š^üGØ9]_i|²BùÝåÉéÃ+׸=7GÍ÷ñ=�íÓ“øo“ù–eá»&ô?@ç.A; v?ƒ>z¶H=ú3ÌÚyÜ¡£ž s^õPJ¹Š¢2Vâè[ùíà õõ@ú›Í7M³ æÝnwÃ$Y£°0,ÜB­@Gé^¥¨”"M⨋ ”pQÚ�«ˆÊÐd4º×¨Õ›.©×®’ån·û±®cX”y³ÙèhávU’֒΄–á[)Y®#õšìb ‰T‚v?'Iжm‰Ÿ.“ÄR^d½ï×4EÍJV½ð…G÷XU}–R=\#^EZ S“dF2PÙRiý ß¿ññq’,'Ír|×$èu°´î¦›c™¤j†‡ç„—r-šùò¦mÒ\Aœ™•…eôÆ/ZZÕ”Êâ[/IîΧ«^ÍÇŽú<ÿ©â8"‰z÷éZjš¥¸-U«lEƒÎ¹Ò‡,4ûh²Ÿ•t"MâÌXEœ)«xB˱Û.ˆ&ew¥|&­Vë>mZ¯ÛÅq¶mÛþ±’Û~¿¸®ëWKå“RÎk³5‘eYEä©×똖…ïyœ>w�=;·ßŸè­ŠÃ²²Îóü¯‡/^¾Êüâ"5ßÃ2Mvl›Þt®Äa ­�YÂv�Èý1”h É6�𛘵Iò´O'{ˆÌ=¶ê3ÊNPi Üét¶‡5­ðºÿ?Þ:Á?9þ(¿þâ+|~ÿÞ�½ËícÊsùÓó—ùÔžüÁÉsüæç?MÃ(,�ÿðüe~êà^Z2çÊb‹n³ÝsÉTÎ�F�×gï²FüwO=Fžg…ÒAžÐ bڹƈ!°ÝQ¯~_±A¨ËPè†[<«ò7çi•¤”*¥­V«rÑ‹¢¨‚ lõn•4UŽÇ^Æ­õHgu» 0'ÔPyŠ#b²;÷˜ûgÿŒlqc÷^¿ú›ä£ãÕü;ß1²U÷aRóŠ¥td€; d®ÓO¬u tÃITžçÕáè£Ü¿’@ĺ¦aa÷¶±9q+“©²°œú*“(,HÜJ* �8dÊÆókþSŸF£‰‚n§ƒ­uV⹦qþºÍKo«â°ç þáÏNโ0è“gÅýVF(õ�‡mÙã(DÆK$¹[}y„µ†ô·Ñ^¨‘cÇ«¯*vY–E³ÙDÊÂHçA÷XFV›ŸŸWåéÕuÝ*É-¥‡ ÃØ4¸–öŽó ‹Ü[\âæÌ,®ëòégŸ*$·ª_¿L«“K9ß6“€¬J–?J«Z)…¦ ^{û„Ü»‡©‰õ«Íw] ß³ˆ‚.½pÅYÇí~¿&PyF7#ñ?·jnÉâVùÍÜJìZœØƒFù2I)+,ÒzÖ³bÀŠ_ï£L ÌŽëW:–a¢ò”¿}¶A;…ë!ôàS£Š¹ç.q¦áÔ&«DHɈn_C7F›6Jeë+”•¦µó=¯ ¬eL'©iš’Æ=tÝ@¡áŠÕÕÐD ’ÌX-E¦òéÌ[÷>ÚF†3P)°ýÉ"�÷ºäi„�ëùEb—ôÊÐK¬UÕÄáÑl6W™‡”§õ�Zbk‡ã8¸žGg¡c›ÅÆ ²>Ž‘mš B‘¤&ԩ׋uÜívQYÛÈ·¤l�J½pª�TíÑv{O„«ã½–Í7¾òü§Féös‚PÒëKfïFü£Ÿ/Ú[¶Sl–d},#@k ‚B/±ð꓃€Ý¦f´߯dn�ÇÕ´"1 ªúþ| ?—HQ= SH<3%“Inb»�ê gYÖ–+éå;<>>þ±’ÛR-¡ŒÃeÛ®$³”Uý½O¥¾õÒr‹Ù»sÜ™»G³QçO=Y9­ƒ8,@ÉUqX�iIŸÑÚøºê$[¹º¼òæÛhšÎÇ2>:òÑâ°cáûQЦít·ý ü¦(ºƒ¸ñS«æWËùGí@.Ä {ÇÇèKI„„IÂ¥¥6ÏnŸ"’G:I¸³â&¹œ¤LÔ|šõ:ïÏÞÁÒuŒ6Yêö05HsE,%uÓÄÔu+AÓÔà` º^ì5I¦“ë~Ul€F8Kùé³5–¸ÃnžWü¯»ï’f ÝÃvœ¢0´ÑÈPº‹i9»hò …šr!ªàÒÒRõŒËw¡lÕ‡A¡ý«.¦êV–èôyæç·ûͯ"·ï«Ò6¥¦Qð:ÖËôA‘¡|¯•‚T+,e³,#è.�cT± è÷0´Û�©yŽ·¥|¨”’n·[ÉHn¦p3!)â°!öíÝt MÓÈZ@ÿ5„€fsåȵBÚ4„jÝwý%¹¤|‡,˪䩶úý¶UȦ)ÓÍ{ÆÇøôÎit•ã �bÔ2W=ëQË,Ô4�#�ÛM�Ê2vŒ�Rw]<¡3j™Õº +J–‘ci=Âî<ý~¡–bÛE%.–ð‡OÂCµâŸsqÑÅÈ4§€Öä9QØÝ£N®ŠN^¯×Ãó¼ÊÑi«kYJY­Ãõâx })÷½µ1:ÏsÂÞQàùM¯I.Sʽ3Ïs’íÛ°ìsäc>®­yC¢þ"qR¯×«g«TQ5 ³M,MœAì ƒ¢ëã›i¸Höðü†3J�:$ÙÆ±°¼Æ²bØjµˆ¢ˆF£±¥\ÄqœBV4WB'ËRÂ0Ä«�¢ÌQ‚Ä$ËK e#‹Îïÿß,ü_ÿ‚äæuT“Ìΰø{¿ËÒÿóû°‰„¦cæD1t]GX5ú‰±ªJ›ç ×L�YÝïóùO�⺅›¥g&¤áA¿‡ãÕÎXqÿdqÿâLàx�ê~ÙÆÊ÷ùV†oôˆzódYF½1J¦�¤2_‰Åž™â[ ¦(àÖàÐc9Ž{Å>,„ Ïó-‘Î`…|½JÆ·¬DQTI5Ôëõʇܲ,ô��py’)qo¦iòÊ›o“IÉÓO>A–¦ÄI‚íúhP”¸óÕU‡2±mËÜÕ® PôXÆÔªªÅ###DQô@G4�0йôÞ :|ˆ§Ž=F§Ó©„§·trU7¿€–7 Ï�yOV-È"1×44µº½`YVµ©”•¾ag–�Rý(‰$ek©ÙlnX‰-ƒ‹®ë«*ëe‚äû~uí2[�#¬7 â™Â5d¶L·ÝÇvëc �¾„_>¬83¨5Š@!ož˜¹›"eñæÌÎżôF›ç?iá:£££®¸Hµ7°å3¾ŽR†¨¸ÎhÝdÂ1³�åk�^7Äqøõ±ªÅg¥Ú`CSHÒÌÀÔ‹ö‹Ð3Ò¬M/6±]¿ŸNÓHB" üFY™èU�Yè OÏ�y›~·�åÔiŒŒmXÍ/j ’/x‰-*™µè\×%WE 2 QÀJ˜ù²ý ‹©%XÆý÷L)Ð …5�vQ2§¹ø~ ¥Üb¾žî5<³8wi:NÑÒk- „…WÀV¢>®3VÏüð�%zAqÏ|Wp`·‰NºÂÕS’´E?¶.C� ‡®$µæÀ}0îÆ%¨BI†´�CÄý%^�iV¶ÛeǧÛí®"¢lµWÆá2An4\ŽãTïNùޝ5?‚€4‰0õ�ÛÓj‡m#Cåmzíî ó©‚_Ú—?#ζ[$˜},-B×"U«b…”rÕu”ÒV[e’Ðjµî3qXoß&6C©Q/D3ëøµQd'}L=A™ÖßûOÈ5ZŽi¦2�ªLrÕ#ìEh«öà 0’i2×ÖÊ�WËÃÊœÀ6r” ˆú è.~}sƒçèºnõeupøúÇ©r‘õ U‘!+¤B=Û¢ÝîUP!Û¶ñãDQHšöp&kG"bq�ö7ÿÿÇžÇÚ½b¸"[Ëô^úÞçûÀŽ ;r©Ô1ìZõþ†ý¶[£Ö˜ ú�õqLÉ�Ý.÷ ÂARiéäyΞmbèþõI‚ˆ\s©ÕGHÓ”nÔFNU”yC¨�^|!+ÖqaÂÑëXN §>A$Ig •.ôo@6\ }Ð5½*2”Xú�ÂOZ÷S¾%æ¤ÄŸ•²;%€¿Ln¥”\øð ‡ìç¹§�sýæ-jµ:™T½.iÑl6V�¾ H‰Œ¸‘ŸE ž=ú#¸Nã8¦Õj¡iÚ}U´êt�™Ûwxõ­w¨×|>8}]תvB¯×ÃuÝMO®fø>V½IÒï‘ÙGªàØï.!èÉ­ÊɵÕ™er2,E5|òM’ä¯TÁVU‘Ë!„¨ª¶ëé öz=ºÝ.®ëR«7‰2gU­ V�»…®p�-]22—ûðZ‹‚¬40tˆã˜4ɸu;áÓO7ùô3#|êé&GúÜ�Oˆb�~ñÖ{WxííkܘérõÆ2º.VZLúJå"%Ëí˜~�qo!¨ª½^�åv²j œ¿tMÓ«MU×õ#³ Xܸà[Y´D *yî8aæ²dJpñZ4 ËÌ�6��)¾£’e‚~Çqh4š„Ò#LªĤc„êß׺zq2Íã½nkÝj~y8)µ×®�r öz=|ß¿o —Uì4h}zµ†»armö(afÝW=ˆ¥Yám£°‹cÊâú�€¸¿Hæ[£„©]�¾‡G’‰J‚' û¸FŠ­÷{ …^c”Tk’åðÙg [Ý,Sd™Â4Ÿ|B/Úäƒa‰œº’G ÄQ߯ã7ÆŠ{Ç„•Ž¢¢ÀqÉ�ã�Fá´fÑ#è. TN­1F¦×‰2QUñ×uÚí’¤(hÑï,�ç²Z{ÿ!Gš¦,//WšÇ¶mW¤Õaµ MÓªêä©sxâØ£ùˆG/\¿o]0hHœéð&MSõˆ _Ó…�ºKhën`à~Ž¥ˆ½ Q¯2­àzao�|Ó{pk,ËM:Æ"]a2\Þ�‹ÞîÞ¶F?}j1sz�IGQÔI9xžG–ç¼ñÎ)~ð¸vp’ïgþNUU ƒ®�ïû>WʳäB'† ò.æ“=·,0 Çavv¶|ƒ^˜ÃјÏ<õ$W¯ßàÈá»7·¦iº Þ´7nyÖfLè\AɆ\AYëBÁLÞÂvt+LV)vt¥Öá¦5¿UÉ|:I�Ž»m‘M?»ëºÝ—jôra£åìæÏžNnÂx€ðæob*V�½e%εžÈøÏJ².fZ/ý?ÝO—`–¹&i qhÓ‹lî9òø‰�hOf‚û�,0eÜ{÷,“´ä�w®rö YÞðÒkqåÚ¸MdK®\ròíË,Ì'œ9¿Â+'/qs9eiyD–U<÷âœ|ë£QN^4\¹6æÒÕ/¿~‘¼P¼xò&ï�YãÔ‡7xîåë4Rpúô5~ôÂ{ŒÆ—¯<ÿêMn®Áp\òÁù ãÜæ…Wopò½5–†Š<©›#×VDNN“/‘¦¢(&ˆç»à‘¥£M.1‡çH"7§É—ÉÓ1I’Ðïk­d³vª¤LW_6·Èâ8¦n�”x®ÓéLo¾ÞT¡êº&Jæ�ö€I©�”Ç[?¬Ø*Ÿº¶MŘl¼¤ÕPz³4íõ&°*8ë-ªš´R]}±åãÑúÆ6;ãó³Ÿñ=Á gñ×~Ú% n},¡\Il�I'Ǩ€¼Òz�úBKŒm ô¶¥Ý»Ye<\Á²‚XC-²Ê¾¥P4~¨çv2ÑxåÈ«©ŠtÃÆ>þÿfc]YYé7ceÚ4M+!”�e™Váˆ#nÜ\âô¹ <þðC(Y·qx}ƒ6•Xñp]—‹å”BWàfÔ^f’¹îÀnŸ“É„áp¸e»¿‘’¢(ùô'žàÚõ<öЃ4MÓ]?½†§ãð†$Læ„â4‹LÞ�²âõÏ¿�ݪ¸¨:GGºë ´ÇHm5‡f26ªæýÝíw` †tg@f7'3Ó×v…’ɤƒËdµßÁeôüY[2øKቂ¿¿Or-‡7‡°RÁßÞ·žÐçÙ¤K&%^7ï[ÝKžç F»…îM¯�4Myçí·¸øÑG×ô^üý§¿Ã•+—7%m‚>8«�ŠN¾þªÆÖÇ‹”*Ù@.º±´Æé³Ù<,Ë"òÁjFLÆ:„ÉŽr9Ü«±8›Ú·&@6f’$�Ïðésç¹vã&7––9¸ƒ~oË*˜i÷O·ò\ß§W¾�ÛëQŒ&TÁñö*…“¿G"Ob…Úʯ*JšÙ£Ì&³EÑ�öng `^ã±=;;»kÒÙæg7Ãít·{öþ`¾•c[Š¢ÖIŠ'-üfîHɊߘ¿¬¿»ÆBY qÒÃè$º~B‘gÜ}Ðᙟ¬’fúåŽB›ý{_ Û¶°- ÛÖ›ÜcßÅ+'/påÚ˜}{Þ?}�}{õf÷ö{—9zï"¾g³¼<ááyÿôu†£Œ¢”èpÿ‘š…ACèfÀ­$Óª*›0Ò?ÏR¤lHíç7ã³µýsÛÄ^AU•-Ô¡�y6¡Ðò8 „½ž [rŒe+*iÑ(}o¦ŠiÚäwª(ð2 Éw2f`Oãñ¼óþ‡ß]gç×/a'‹ÔÙrö!f’™n=Ãí½æaýˆã˜ÙÙÙ]“ζ‚ …aØá�o§p²"OÁe«¡’ÚW*¡ÕX¯f)YòÛ{¯º+Õ'=„ÐëÒóò´Â�ðiší±‰]—lj-™½|7kYÁŸ|ýk 3|øÁ)þ“¿÷k ×ÖhZó¦¿ð<Ÿý©ŸæÍw/�$Gî^¤®þùÿð?ó;ÿÝ— Ã�ÿñŸÿ3þÅ¿úWÜ\Z£ßï377Ïé³çQMNÓ4¼þúäùã?~”><‡cÛ”UÍÓOŸûî;Æh4æçáç9}æ"�>þ++Ë\¿z…C晟�vÖʶÎT,ˆâu}àíÒÍÏo¾oûò<�ªÖª;¾k“çÙ¶‡ß颓†.öhšˆI6ÂóµJ„ÿðc`ÛÏcéþw¬8¦¾q¥ zôAš|™mX!eDšj[{eG¸®©æ�p§” Ö÷¡”l\"œ�™A̧¼òæ„cw•Ï»y ”€v-VÒ&éé÷m4âù>–2œŒpÈ5lm›)4°1Eª¡VÔª-…¤ùÏ®p,Eݼ ×åMn{hÑj7ª�¿i�í­ Ÿö—¿üå¯î68L‘9íã‡åÕ5®^¿ÉÒò3ýdǪF.Fœ«ßÖU(w–Ùh�¤™¡¯ö0“Ìî(ßb~n\fF“”ë7n²oïlÛæ¾#÷츰M€1z|‹Uã‹sXjÂXG9  j¼ñ³Ä¼‡σR(Y’ÖûÈí#϶mÍÎÜä&µÓ0'ü0 Ãð¶V›ŸÝs£-»•SÚv×O‹Q�²TµN¬eQ5:Ñu¶ÀUÚ–N2ü ì~¦×@ÀœËÙ�´V@Xüô'CܶEdYç.¬pïáy®Ý±wO� Wð\‹…ù³³ýÄc’–ÌÍ&¸ŽMž×dyÅÂ|ÂÒʘª–ÌÎj=ÖIZ0ç,Î÷PX¬¬N8°oÀÜ\Âp”³¸Ðãì…eß¡ªÒ¬" \êF1™äÌ |ò¢¢(%Uý~Lš•Ü×Ç÷_�„[à )B‚0î0�Q¬+UÕ´mÁ�¾�hIáâù›j`w"ž^×µvf“ºbæ{yžíú°„Ôz ~ eà 6Ë’ã ò[›‡6�Ð:‰y^ã!®c»U•%²\Û–lYàX’²Ì™¤93}‹^bS7¶Í6­,KCJ„7Yå˸"e’i§3/èQTUÝìXAý÷=»¡*sÊJé„ÙÈŠ†Z ¢x¦uÚá‰\—J´íbÑÂ=u™ÑÔQ|« íïÿþïÿ÷;ÞÄ6ãË_þòW„Ø„yÚbE”ªª°mmÆ�”,¯¬röÂGyN‡;Æá”5.Tï! ›Øë± iæ°À ža4í‡Íú—RR”]¾ÂüÜ,–?vôŽã°” b‘3iŽ!�= jÜôbù2^›Øê8¼‡Ü>vKÞMrb>ß`+MÞ,¿Óµ@Ï3d—²,wǧÅ룸×9E !ZE‡ZY[š9Ø–Ö5u}½E#?ˆ¨¥CÓÈm+·Û=‡Ñ§ß�µ½ëº<ÿ£gù�ÿößáøƒQW5Ï>óCž{ææçç¹pþBÀ«¯¼ÌK?ù >ò ÇáÕW_E—._¥i$�<òÏÿèþâϾ…øœ|ý$¯¼ú: ‹{9î§ÞŸùù=¼ôÒ+<ý½ï3˜™áüÙó$½„É$%KsÞ;uŠwßy›ï?ýæùÑ�^à¡Ç>‰1¤Ùi˜w[lªaåßÉ~¬”ÚàȪ”À÷4«Wó¯$M5A!ˆã~Ëq�̶«K47—ôšX]�²DNÆ„�>Fï—ÛVØ¢ni‹0î!…G„­ÖëÏÊ·Ü�„ÐÅ›ªM Köíñ5§](™¾Îðj)À�Åu½Î²©²Ö)¯‡å„d…BlãZÙ}>ú�a©ªí´8DqŸªÑäì�8Ñdµ"]ųu±²Q‚ZÚëó/ª2,’ÖŒeº°é$I²k€9p ÙAañÔã�êÖËxLµƒTƒ¤á²ø�‚”Œãt…=â0óÉ\×ÝVúé–Éiÿ½×êñ]¹v�0p-½±›—¦©†*Ä6^ò‹dkQ»óØå%üñw B;˜EÕÂö(Æ#ªÞ/ë*Y»©˜Å= vm@0]ýø8¤3P�Èû`0Ø5áeºµ4]}�F: AÕØxvƒ³‰€VÔVKîL&cd•âú=ü `~6àoþÒ¾þç×iÉÏÿ•�0X°J)}èMÓpäð<–€ì#‰<Î_\aÏB¯m1x$qJ8ŒFGï]ÀsÇïÛ�°lfú!H¯ðÀLDy¬ 3>û©£\¿1ffrìÞyl[päžyŠ,ç/rc¹`8.yðØ€ƒûb®ßL9rwŸ~¼¨-‰½ÏsHæg\·f+Ò[Õ¶­5„bÕ�qát`ùt2AÈßÝ^|½ll¶â5�» öNªHžçá8.EU#,�m‰ncº]‚«ÛsP‘V‡=³m›Lz@qÛ$ѱ¶•Sæ%ÊŠé™*t–áÝF6LEà*<²¢AY½þeY´¤¯ºÃ‚Æòz¡‘ôá[šÖósŠº"-<-Y#B]½ ˜" n3ŽÄUòI-éÌ$ u]C“v¤Û’$^IV¹Ø–Äk™½ŠŠ|R�ÞÑ;¸Ýˆ¢hׇäi¨‰QPðý�O>ñR)VVVwLlkJ®Š3äL8£N’LæØgßÜ‘µÛFçy«ûÐð¬Ë,¯¬"eƒÛv•v3Ö+ˆ^òWÉÖ.P¹³Øõ üá_`G‹¨¦@Øe:¦Œ~ƒ³7mI¥ÔÇa0j@wB:s]·ƒˆ…›;]Fî/Š¢[ˆÏ�´¨¤uK¤‘ᥚ’ñh Ï�ºXnL�¶"ym7Û‘Î6¯!D7O«k«œùðCÆã1y–1˜™¡×ïsèÐ]Te…%'zÇÖN�{÷îe4“¦)Iséâ%”Tôû=�G[u£²n¸÷èýäyÆéÓg)Š‚ÅÅEæfg9t×!î»ï7n.ñæ›oñø“ŸäÔ©wñ<�§žzœSï½CÒÓZÙi>Âo ]¶R�œR»YYY¹íóo†Ô]7©�¯Ï¦Qí*§ò�ß©)ë1uíw9…帿öëÔÿË¿dò³Ýï�>Êì?ú‡˜Ôª%Ý�ÈÆA<׭ɦLÞm0©BÑ u¢[ÔaÔÇÒÉÕd„®&½‰®jëÆa[d»×©R²´ ±®Ä–!“|„ïìl­n[ŠÐª‘r�t”j­òd¡›÷<ËpÄz>éÙ º‚ë¶DpK(Y3eØn¼�ôhÿ7ÿäŸ|uÐ(L[ÞnH¥ÈŠ [ ¡Ée&ðÞ:×ÄYƬ‹ð"e� ä^\Çë’ÆÝÜGšåü๸ëÐAN܇ì'Iâ>ãB`— ëghš‚ œgåëIËñiÒ›Øþ€j²Ì$üE”£�8Œoùd2é4øL{âNO~;yTo5ŒaÃp8ÔBß›,4wØaýÔž$IwÀÝ(¦Ô"&Š´žn�¯â;ºŠŸ ¾ïc[Ç–ÌÏZ왿•�ø­v¨«É$¾§m–g®#ÚJˆÏÌ ‡l›îwægè÷BÒtB;¾�ëZ€Â÷uðœ� èõb’$¤®*fbÅþKH\Ïc¦ïãZß³XœuqmIèCä+5I䳸8‹TµBÊ�Ä¥¥Š‰ZMá"]ÕxT»AÕ9yQ„1Ž‘úz{“�¬Te÷‚pƒ5¨éˆ„aHE;V‘Ì&nYŽmá:6édL‘MB Úi ®«IXÑl«ƒ9¦* ÝâÂ#/%µcW´sâøZp¼,KMnÁ£(%Bhs„Í´øüúÔ8”¤È3,Û#NZã°ÑUX%…ì¶sRå«aÄœþM%¸¬¤Öµ´òB¢}Òw’PÓxj‡’4/»J�N,u³nG-Ý�ÏL© ­~ˆŠ<ÓVÔI’ð»¿û»«rû;¿ó;_I’ÄÝmÑŒFJ²¼Âs,ÇAAØb¹·Šƒ Å qžËÝÿ/DÊDé©y\Ûít%o‹V‡#~ôã—8vï=¿ï(÷ÜuI¢± »éF!°ª+„å÷�²@D�x6ö�G{Ø~Ÿ&»‰å÷¨Ò&Îg‘Þ„ÐÆ?¶m“ç9kkk4MC’h#™ÝÜ»ù|¥T— î6OÛ�hÅ´�èn÷  «"'IÒɾ)) Ý[5ª‹Ú#éiÞK:^#rs”,ŽK‚ èöá º8²›aŠ.Y–m°ã�~SÝ�™›ãòuÆã1ŸúÌgx÷í·ðŸO<„°Ÿþìgùàý÷q‡'Àwi–ñð#�p÷ác�ÏÜÏ믿A’Ä<ôð fgzD¡Çì샙9fçæYس�÷O�" žxâQÞ}÷=Nœx�7ß|�Ã÷åì™Ó:tûöÎqÿ}÷�e{÷.bÙ.aÜ'¯´�±½E<¯]âDwj as»çßnMvÛ²ð%%«+Kíõ;çT:1Õßo�ÖçVJ1®âºá`矡<õ.Íõ«øÇ�³ïŸþSDt+©U�×�â^ו„´Ôq̱Âj˱ºµázÇ.�ÂUõp½HïJâØº(#¼™ÖT&ÃUî:k*Áºà&,·«Ä–U³åüo¾זȺ ¨TGn–EQ)d£agºó©ðœ�±Øj¯²`’Ux~kªõá™sÊó\f}ÂV<þ¶R ÆŒAÖ9¦ªª.¸šE2]‰±Ä%ñ>’�/Û!uœ»gޱ¶¶¶®ë׊øïTÉpÚ“óµ7�ÇÚ¿¯#y–ïŽU,ÕåOö}Š›gQþœz 'ÐUµjxwpYåŒåýTÑ“€ö 6¬é¥¥¥Ìlt&ã8þØUß÷;·—Íß�9%š¹1ÒQæß,Ë" ÃÎ>Ñèdîfl¼÷‚jÓ©wÚ¬`4!ëß©É+,H‡Ã! Û²I’ ­ðÞ¹‰Ãf‡Ó¶¬ªŠµµµ]=‡Ñg.²®Õ�7κԈÏi6˜1èk5®XŠ�0ÒÕô"âYŽ­Èk›01† k8j¼A!A)m0 ¬uP‘ ;hU‚0YÀ¶mÖÖÖn! (5e [šyhƘªn°„–ÊÇ7u ±¨THm]…2F(A¡bzýÝ5êë‹ÚB8I§[]Cw{-ö?í˜d’³}Kyã﬋Ѯ!;ôF ðæ}oédHèÖÔÒ¢¨�®rm¬—�® y§&B¬Û¡fYF]×�AÆô3O¿3f/4]ƒ…γ ®(¨¤C--ý. …m‰ÖÞ[±NGÒ†,eㄚ[’e)4)�«‹j�èű. ¤ËxNÓêêVQÛ�×9¥eé!3|Wwt¤Ô–~ç |ó^;ý=n—ÍÎÎRË–Ôè:¤“®Z£Q‚¢qq½xÛu°nX`ãEÚJ<�L°šUËd@%ÈË—¸ú•ßbö¿ý ñу[~_Ú)l}]”Ù ¿íxåé˜3ço²6Ô²~O=²ˆïé÷^©„EQY¼ýþMž<1ËÊXðêÛ7™ŸK¸÷îyN¾q–Dzÿ¢NÀWoÐó77%:Y#Ûç÷õA3ÏRT“ÞÖ¨nv¸€çùÖUY–ÙGd·µf/j›¬Òînöo~áK_ÕAP·HÂ(ì6ç­– ²î^D)%+++·gé ‚9=J)©š’Ëâ}*6²gÔ^'Ç)‹r½Óêæ­c±ä–ÕÈÁ`@àûØBÐKâîŪªªkõì„iuÊ3ÄÁM!jìj 7^DÕÕÚ9ÜþA„°˜3”ñç@ˆ®BÚMøÔ¦1�ÔOWRw{r}Ê3X.àÌw0]0þñÓŸmîɨXÜ)ž×Ü»ëêSoQ[ÔµÖUvB„�Þ±TÚ�J*ÑU¤Ôž¾ïvD #·›*ÆôsLW”�aÃp8¼-«wÏfY–n‘Hýío [VD„hÛí¢&Ër‚(P+—¼”8nÔÍAS¬ÝÛ�Ê¢hœ®o A”ô©¥KQêyn¿Ó#ÝêàfªHfýL»©"Y–¥57+ÙamÓtŒgݩֵ4Ž©i Izë­v‘[c ¥ Z|i–Žð­¬=ý*lŠϪ‰Wy%¨ky ž¸llü¶ò«½Ä3<[bQ’e9ް2,é%‡ö÷9}~…¥ÕŠ÷ϬF!ç/¥¼f…åÕŒŽÌ2Nk.^rôð,®ŽëS”Šûfõ! Zm‰_®õm§[¦ZüÝ%“4GXÇUÔ6å6Õ€¢ñ‰[Y›Ñp•"O±,Gë¦ú>¶’—Æžvýia,�³ÚE*ˆ?.æö7¿ø¥¯TUí¦iÖ­ý«ðB´†ßóº÷}mmMãáGc1}¿ëÊTMÉEë=6ª5Ë=ÉñvÝ4]Îó¼+6{éÍ÷13à{¾çO)nAEbÄæëíò<‰ ¡¤žßâ N4ƒj*ªÕ3¸½Û#Í|ÊøgAX™ÍÄ–éwisîõz»îh™ëͳO“ö¦ŸÝÈ™™Ãçôµpg�˜ÍŸmâ—‚¤7 nlŠªÆ¶$5a+œ?��J«Ý4ÒêìZ ÔÁHxš5` lw¢Œ0�KžV0…³gß¼q�~«Þa®7qLJI'4JWñÖá2‚ñh•^äj쩲X]Ë } [(®^»¡ÙyIF\¾|�º,�(éwØx×*ëÞf€¶—J´k°$Œbl7"- •t‰{ZRoó^ºù{4’››ñÈaâ¸F!�¢Ê×t³íô(Y�ç%~°qh“ ½'Vm7PJI–®¶E%p­š"KY)jl)‘�}’Jy¸¶º¥ØP©õÊož®:®US–ºF œ]â¾£{°Ü\É9i‡ç‡ìÝ3Ûï-sæÂeÕpøP�Ë9“´àÞ»ÛÃ̰dv¶ßvOSb{„ú�Ð(KM¦Ôô^ÒhC¥¢Â"\?&+¹5TD)AEDê¹­-S× ¶cbq€yÙ`!7ÌA£´üœv=ÓE¥ö¾ø[_…lñ£“4ÓŠQ¼uP³�ú%Ùê%7d‡iÒ™çúxuD# ¡OÒ.‡�„^t @Ñã> ‚ ÐÒey‹Öt‚¤_¬x]¢È´TMT=‡›Ì ³%(Ç8ñ>šô&õø2NrËKÈ'%ÞÁ¿�°ÜnÃ1/SÛ<̽EqÛ�a«kakÒ™9E¦óVk«gßLx¹Ýç›y÷ý/H¸Vz¼’GœÉNKÊc¯«m;Íg˜–ž!8˜¤ú�³]›t»û0§S�PJÝÑsLV N¬®µp¿go�G4-Ñ‚Ý-Û#е¡…Æ}ñDqK¢TK«»Ûv{Ú·¼®EcÚ-[T3”ÒNKEQtŒ\³±èMJ“äÇ)©Û€ÚÝ?m’**Šóä~Þ:u“ÕµœÏö0g/¬rìž²ˆ#Ÿw?¼Æáý!Y®+RûöèM³(*„0 êÎß¿gK�%y^à¸A˜PT:šNÒ)ÀÑ�˜ª,͈À©¨ M’°l“„HáQ” ¥$yívÖ‘R *¹®ôðq“Û/|ñK_\©eKŽÒÊd ¢�)0è˜kâ°i¯šVÓ4Hg®ãâÖ©Ò�àÚ8Üç<‰ï[²vJR§ãðfŒáæÇ(¼lŒÃ%Qù<^o€*G¨|79€,GÔ£‹8É~l@9IqüÂÖŸßëõ:«aóÌ›‡‰e&A7E˜;é$M'w¦Á†êVcó!}·d­éë§ì0êq£ôx¯Š¹RÙ|”)V‹9»éÔ0¦ÿ>Ð]/¥Äu5ÔÄÀî´ØÐ4MWd0qÔqþàÿü7ÌÌÎòö[orüÁ\¾tQïµMCY ׯ_#Ï3^üñ Ü}ø0×®^%I®^¹ÌO~üc~ø²¢áƒÓxï½ÓÌÎͳ²6áµ×Þ`n~šŒº.ùÖ7¾ÉãO<Å•+Wé%=Î�?‡c5\»¾„ëx޾W�½¶‚ЩhÊ”ªÄq)üŽ{;s(ÓE\^^î ‡~ë×ëõ(ëFÛì.édŒoç"“Iòê2'/ÂH_ß4 R6Ô�E”̶Ç¡½Ž“ÖÅ ‰êèý(ËgvnžZy•Ä:ÁÓœ„™¶B�â¨I—ø9–ÂE‘sáò�ëKkÃ’Å…>uUñÀ±y^>y˲xâá½\¾6âðÁ˲q‹—_ûˆ'OÌpåFÊìLŸ^¢ßõ²ª;Ó†ön·œ?ÛÒ�u™QVEqS›`cec㇃ö9&x"ű Š"§jÀqÜ6É�´ShÓ m,v©¥…TÖ™SDzDg‰ÙH‰@»ÌdyA úýÎ]¦–ªsè2®,¦Ú8ýBÔu½AªÁu]ö0[.pyrŽëò™L:¹�n?éÌó;‘+™&œÜ)élZêäO†³ühUðúî�m®6ÿîxÍ uÇôß°§e‹ ž×T¢v+�fÛv—¯®®vº‚wâPë„SÍNÓ1y% Üõ ±‘Ö-nìžW#J±.òÝHA.m‚M­jm !©¥Õ‘¢ÂÖ©l¸–3˜ÑzŒeYî¸L2b¤…¦eøÂ0$+* ßq˜Œ–ñÝ­;Z#¶¡‘#ÆÃ×�‰z yFMYUÚ[_ïÚ ×.¨Š’\jŸo¥´Ó™ !餿F·@<@'˜ž#9qßÌ„=Þ9u•s—3‚À%Í*.\Ñ´›ñx\±6*´m¥´¶ B«�äyFÔ›ÓP¥bLìV[Va§‡ÆãVÔõ ãÜoñhi:Æ!Çw$EãÒoemŠ|D`3 ‰Rc²q�°#¢vSóÏÕô7¹È¢º›~2ÃÚÚÚ¶qxZºÈ8Vš8 ì“¶�Ã㛸Õû¸Q@¶| Wd8ñ>êÉUšb·wËëQ o0Ž~5®‰"�¹¹¹ íÊ�,9§ã°‘�¼ 7 §É¿&©Þ aÈ$©FÕÌÝn {æs²,ã�Gs|YðæŽD+UÀ?(�"¥Äó¼ P53L¥ÕÄ?³6ÃU¶æ° ë0©~¿�RªSˆ°m›wß~‹W^y !,:DUVìÝ·�çŸ{–ÏýÌçÉÓŒ¯ÿñi—É^�#G�RU5`ñ̳/ràà!)ù¿ÿí#„æµÜ¸~�™~Äž=¾ïqãÆu~üüs\¼øçΜáØ}÷óá‡ï…¿öwÿ:Jê˜ìZRW³-‰c7¤š0¸~²!—Øé¹ã8&MÓNMašt×HEÝH&r*,K!E¸nHR¥xísøvMÓ¬1f¸­[Ÿ>¨¶Ž‡¥V„h¤Åæ�Ìþ¿ôU0¹·`ÝûYQ•nÑ*I'X¶ƒ’͆ªív'g€TޏœŸÅ‘~{Zš æ™øs»bw›cl ™Ærvu ®sªÑ)’ÄÇgXËbnx~@äÔ£ó¨¦ÀI œ�|’“&e· ðb› ®ßºr´¤ÝÇéêÅ�’ŒÔÈ´Xñn‰ÿ!P ó·_ÎC>3#Hø/ï‚wÇð 35¡Òk#`¾Õ½˜ïÍ´wLõ`º’¾Ý0I–e]B˜çy·QÜjxÔ¶¨²R¡¤D¡±BÛá2«Æ%îéï}<âû®‘lh±˜L"ZÿxºŸ×h Ã•oUÅ1XÂÍ^í¨ÔRá¹6J6”ùxÇ*4°¡E–e%QÜ_O†ò!®­v¬ƒÚ¸vE‘çÔâ¤�çG‰LVkÛâ âÈc¦çàÛy‘³wÏ ucqßÑý,ÌGdYÁGf ]Hb›ªQÜ›„! óÌÎD”ùˆØÑ¼°õÆ�·�ë6ÒkëÏ_S9e-‰¢aeƒöº Ž-GZ]¦¢ay–#±°m݉0U¬­*`»rû¥ßú àZB •ê"qÓHòްdé*¼%�müé÷û!v„íŒëU®æIp]Ïõ™ ȽÌsÅ·u=ëqØ$Æøc'Lîôhê’jrž8ö¢Ã<âúpNÃ*‚†ztU�q“7¡­’Æ¿Šr÷Pcly7и^­×½;Òl„;}œ8l:IF�áN¯7sg¾³º^/ìt­Rг�_X8~óxo ¿8Sªªk“ï´š*¶‰Ãæ°¦«ˆÛÇÓ}4zÊÓûÉÅ‹ñð#�rùÒEfff™��ã‘Gçò¥KìÝ¿�sgϲ°°‡…ÅE†CÍ—øìO}!Ží°¼´Ä=ÁÙ3gI’Dèüìç޵µ5æ}—Á æÊµ%VVV8xèiš†óóóÌ/,rßý÷SW’8rÛ9STM˨·Ö;J�´p½¨Óˆ7UØ­örÓí˜>¸ !º¹.ª¥ ô4ÖÖ@öÿÛJªUiù.œ ‡Gä[r„hI—b�4•å®§¡¾¯«ñãñÆÊïæ13ˆè‡`©)uÚq}î½gž8Òóqä®� ýÄåæJÁýGæqü„Å…Y‚À�j ¡R²¼iÉÓ1i.QJÞ +Zؘcén»&à(‹¬�$½V†1lzK€k5¨&g’UXÖ:Üf½¾E÷æƒÓg•~ùŒËˆh_^óx†!Qè“ÄÚ]ÌlnÛ�ø’«Î%ÞLï¢h|+¶-~vöµ„?s*ÝíB077×�ˆMUn§à`U— ë×ñB›Õb/kùA,[;‹©r‰ùòOñ{3éìv‹8Žù×WCð`kìöo>‚ÿéhI¿X낞ñâ†u;ݦi°,«;õ‚öï‚ ³ß4¤Asú7÷bت@G"3N8FþÇ$¸Y–uL×逹‡­æÁuÝîzÝN*ñ¶0/h¤@xóøA ÛWå�H[yªq_¶ãÂG‘[á¶ÑZ ,¡«–L&“ ¤‘é*Žã8„axK7Ãq4Y$/k©ˆ·s§š&½ÝndµGÒ_èæ¦»~Šô¶Ó� ²:`03�Ú¨ÈÖ´½æm$ÀÌõEít¤³"Ï¨Ë ‘Ww檱Pî~»ÆÝææ:±±¶È/HºöRKÖìfÒ`#bÜÛnÈx¸D`oßž½J©?Û™ Þsš,òq ež9—*E(•îž K ä:ÑÆlrQ‡>Q¤lc‰¾]¬¦äº�ñnž¨ží“x6Ÿ€×VšŒ´ãnc‰eY¹hš´¶s¾FP¿�TŒê},gw#„Æ”S-1_~Çnpâ=' �È¢_A:óÝßp³×è-Å Žt1Çt³î4›¤Mwò.Ý q[]Yafv¶Û{”R]WÒó¼îð½,ëôH\‡ü¹gH_~”Â=ñ(îç~–Ú½• ïyÿúfŸÐ†#6ü¯çá_-T£.®ï¤M<=Lü3�/‡·JòŒÝp8ÜçmÛæÜÙ3Ø–ÍÞ}ûèõzœ={†ù…EP Ûq(òœÑhÄââ"+«+ô’KKK8p€+W.Eûö@)ÅGÎÇ!®Õ�f9Q„¶�¸ŽÅÊ3s\8ŽÙ™%+µtÈK]è¸ÝçIßÅ d•SäkðWh¢ãØŽ"ð ÜòvyžÐ¾Š;ÿs`Ådm6°òÛöÂ�$¹Ó¤³íÈ;—ÍD�;u:3Ám+}^Ów‡CnÅOòh$ùTOò+s w1¦jU\×Ýpï+à ./±oq†Sg/S× Ü{a ÞýðÙOQVÜXñ·~éS|÷G'yôÁ#Üsh/ý^„@ð·~é3€âÅ×N±0×ã§Ÿ:Îw~ô'î;H๼þÞyªªaemL^”Üsh‘}{xíí³\¼ºÄž…VG)wïŸÛqŒ2‡ãhf}Ù8”•NrkiãG3m0œà’v/œÀ�¶tÕG¡m§×€TEmÑkµ8Í©róçÒ›Á±m¨¦ ^V ð]›t¼JìUéͶ½VáB´íznI2Ë& jµ\ótU[|ªuÒ•&Hc[�¼vIZˆÆd¼†#Çذy¥!/·sjë ­>°DØ^LZ‚ö¦Hã AN´ƒŽ#Ê"§ž:`ä¥ÔLÚÛl ¹ŒˆÛCËhmÛq°Ýˆ²R¨m6&Õ>¿VjØš°eÛ6¿÷{¿÷ñ’Û/ýöW¤’®VëÎÖvÐÚÿ­‡ÀjÅû«º¡njš¦¦Ù&î ¹Á q�óù1>9ã2ïÁ¯Ô Òç,Â:ëôYM‚cþíbÉ´í®)nq’L‘ßš ·xÿ?üþÓ|ö§>Ç×þèÝ•ú³o}ƒ••eþü[ß`2ñÂóÏqýúuÞ<ù:/þøö8HÒëQók'_cæ¯ÿM‚ãẛÁôwþé×°ªû±Ot÷gº]wy >’»}Ƀ‘äæŽYòtÒuãvS$š&þ B£p:ÞòbÛvס3ÿmªºï¾ó6³ssø¾Ï¥‹Ñ X]Yæõ×^aÏž½Ø-”mZ¦Ò@Ûz½ã´âåWNr÷¡}`Ù4D„aÄÓßý6wÜÏ+¯¾Áž½û°m¡µ¾m½©;®»¾Ÿ�Æ� 'ètÚ§±É›a�½^¯«‚o~4ÖVÏ¡, l9Æwª–ø¤ˆâ>JødÛÄ¢F ,O»‘EÑ‘Xó"§i±)Vì ð¢i%)¸ÌV©Š´+VÔR+üØ·I§¡Y ½Šâ¥t)kAëý"›¬8ņƒ£¬®Xarl×'Œz-ywë}hzH%ZB¯&ŠNÆkxABƒO9EšÛê:£ˆaÖ0°a !pFŸ®=a¢ñD]ŰÝî•Já96EYv¤QàmX¨9WćHd‡�¾YÁÏÌÁ›C°”ìÀøÓ;¦ýnÊË›1Qu­Póo&¸é6‘N~ema-ŸE”K¯�ï×’9Jâ;cdåPO–(Õ<•÷8u|Kúzu/åø=üà<‘ã@ø9°çº º¡*!´ù@Q„axG„'óï›IgÓÊFït+ÂÆÆg_×V5‰ÿ®°ÈÛ8Ó˜–›”?[ãsÞ¦‹¥® ™ßŸ&Ì)¥¸ûÀ<¯½s×qpì­ 7JASkljQ”„�×axAãé&YA¿âØ–4�ÂulÛb8Îè%7–‡$¡×à':Ê+ožfïâÌŽÏ?=EžÑT)®ôÈÒaѸd5ÂsnMø,¡Û0*² kÀÀe¦m;‘7Ò4íD×§�Š@¯�²j´ާ« žÝžL[ÒZU—AØÃ"&­Ï·iQ5RàFÊ'Ãm]_ôõeµJZêÀ$eÈ$¶î2ëï`-®·Cû};¶Â¶ ʼ¢Aë2*“¥C<»¼m‹Ês$>yQP¨�8éO9ìŒ ý)Ü1åfDK˜“jL>ÉÀŽIz³EΤºÍ–šŠEmubçiš¶A» (,7FX‘&:¸å-�uú¯™ }3aë㩤þKµú“-¼Môµ„…ã:¨F“f×!M3Ö†#|Ï%ô½�‡_2®‰³�žíÕ~jÒö+UR1É'ÝsL?ƒ!Îm…ýŽc½Ì»næBJ¹1–#œÕwÍ B„]VJâ;k®@VMºL)Tî}Tñq»§«Àò^šÉ)üÈ&vlðŸg±‹ÃÓìÉÍïàn4fó<§?蓦)Ÿxꓜ?w–ÙÙY^{åeŽ;Æc�?Éü_ÿ÷?pœgŸù÷»ÿ„ñÔ'ùË¿øs–——ø¹Ÿù—/_æÚÕë=v”ñxÌÍ›7Ù»wãñ„§>õYlÛÖ’‰-ôOÏcÜ=ëVü¶mãû>½^¯Ó9MÜ´l[«¶ cYLˆÚ¸©RòqÎz,ÊÊ1¾³‹*åZ2¯Ìlj5teGzs¼„¸7GšŽ¡¾vU56AbHÁ“ŽãÑÈé(Åöz$ý9ò<¥¬&·Õ—Hü©†äãáöHú³�-»+6ª@Hµq]i×ËšªZe\xøA‚Fj!óm¡Eí’ ÖÉͱ[ÐT%µ ðü„ª©©ª ¾ÓÜ’$oþkfÏ5é(Šp”R8¶­1·J¶/‹î‹ ý­#›†²i¼õ‹cQÕ5k£šÀwñ}K®‹óÔèÍÓBr:…ß?}r gÏÆÛ2z±ÓLN#œl¦©î«ÃÍC“w�éY¬f‚-J¬Þ¡õ‰hJT�!ΈgäïCªõÄËg×uIOPǨ'Izw#Ж³; cçÇñ-VŠ;�é$ÕT§ öËœhoÇ®ëšÕÕU<Ïcff¦#y܉ü×ÊÊ A033Ó%˜[µÈÍp]·cËNß{„DaÀÚ(ãþ{ðê[g7^Ø~ý+kc~ôÊ)çú4uÁêj�œª:AÀý÷à''?`emÂÜL‚ïéÄ'Ž,!Xœëssy„ç:íŸV>8Ï{§?Ân±¹»!þ®–«ëŠQˬ7‰Êx<&Ü�´Uµ$°q ˜6°!Ø<ÌfÖût‡BW,©×‘%@V#üM÷ãÚ×.(ó’‚€(ê¡TLšŽp­’�^«Î,»Å}²ñ½Õ0±6î"Úƒ�Ô.jŠ¢¤,kÂÀÓï‚€+â4u«gëZŠïÜ„—Ö`¥†q e±ñ>ý fƒØlåêy^wèÝŠ+!„@NÞ§ÉÏãÖ)BeØ-—Ðdä:CVÊ^@DO1röÒv¿3­¬ ž¤Ì�RMΓÄ÷ba‘e[¿KæYÖí|׋;Å€½{÷qþÜ9žÿѳ¼ûÎÛX¶†&ø¾OØê“>|ûöíg03ƒR Û¶yìñÇùú×þ˜ÿâ7þ1O÷ÛÜsϽÌ/,pôØ},ݼÉ�ë×éõû„aÄÚê*/½øcº 9üi{ó[²û¾ïsj»Uuïíõ½~{¿uÞì ‡ËÌ�ÃEEZ‹ÅPQþ²°c#AÄBDÑ–‘8Qb9 "ÀvlÀ€l˲AA°ÄU\gÈÎÂΛ·ÏÛ÷~Kwß¾[íuNþ8uªoïýFÈÈAßs«î©S¿ó[¾¿ï·Kyÿé�kPJÒ‹çiñWõßú=”,¯—©¬7lÛ®3íƒÁ` +ÄÃðë;GÿêD‡ LNÇôû}Úí1h6[º<|ü…9ö,ƒAŸ‰‰ ¾áSÓÓ¼ùúk|áK¿Ìƒ÷IÓTSºº–¦ ÷îÎÇW.]¤ÕçîÜׯ]#^úä§øÉ«¯„!/¼ð?~ålÛæÖ­›XBðÜG?^÷_ŒžgeYÖðÁÍOBèwkii Ð 5#‹†!Y®ñ±¾ç’&1 keÂÂ0Ô(ÕcÐKð|ÍPECTke-OC Ò$ÁÉšù¡›S­¤o IDATK 3-+ÃÊŽ{v}³›uùÝRqmß–“]©K#hÑhèë ™¬Q½® [@ÓËég ¢ Ça˜6)²ˆÐ3UÉõ×Ï0ìdYN”èj¢R!qÕ—2*ë^J�ÓhÕM©.‘Î$Û ›˜,K‘xþqš`‘àWê”–P4½Œ¬´I §NzH©§æý.]QæÁš�««~UËẪÂ�yAYaK ƒ Ñp±’ޏG‡»8V@§8²EÓG¡8lÇåúY,!DmXGKN†Ì¾ßﯛ³³›„Ñw°m°›3K¿˜2íQ–á AP¢A¯×Û²teà b6üVÃ8ã£49Ûì`H³AGä…5ÿä‡i:3×6ð�õæ¶Œ(ŠV]M•ŽëUÙI§;dr,DXêÇŒ·�\ìãØ6SÍš«1[¤YÁäx³.Ýš›g¡ÓeÏÎqì*•§Eõ"ØDIÆx«R*êELŽ…$©ÞŸ»g¦·ÕpÒò²š L+VMÔt1Y–‘Ä}¼*SiF’»8vIR8k/“á2`ÿ¥¥¥ X!tçùèþ2�Íää$Y!uéߨ²À–ÝM£p©Ya¡,Mß•eYÍ7EVÑÙR±+)\<¿Â·GC„Œ°¼qšÍ¦Îä$‹‚þK)´TãˆR™Ìûø«¢o»2¨æž5¦KrEAÔ_Äru)IbÊl¸®ðÆz£�‚\zx¾–G5êD¾+W(í =<Õ[÷;³Â¢¤�ã”EŽ,"BoåïÍJ›´pVz¥333ÛQGÆÅËW#¥42"óÜ]ˆð8VëJ[Úac�L (¥dqqq[ëiì°y�7²Bý>wïÎqøÈQ²4åÞ½»LLNâû;wî¤ÈsÎ�;˾}ûq\—4M°m‡$ŽiµÛôz]ÆÆÆ¹rù{öìŲ,˜˜˜dÏ޽ܿ�»ssƒîwsŒw^j[d°±I’, 1uç œ�«yJ ÒÂ[+f¦iJ™ H±lÛ¦×["°6N@奠Pr�ÄÒ5ÍÓ¶%k;“•x;u`EäYT+�¥q›•Nêf#Îm}ÿA¥&—ñm�¬HÊ­±6ô*´âVÛq(²G$+®/•>o ·ñòú)-¿«?¤êÍ`Jam4…eáÚÐkÕ,Ya–%÷ Û¡ï£lm\;Üc�:ZKìn§«tµq64lBhöÿ#®ë`‡;�*ÒhHâ>Ck׋8^³þüê®Ï͆é-Š‚~¿¿m•X+¥ø0܆&ë` ÿÃ:©£ß³ºt³!„`rr²¦x3ó«çK)™ššÚÐá7ÔUÛ‘ï4Ãð×™Lu) ZÍvͬ°^™t«±úl4ߊ–—!„"-„mÍÐëvB6Ûä¹–l89¥ÔãfÃðqƒnà6f-L“¤Î0Ãd&&')äi†@+’éX®�¯üíÚ08þA¥µ´´@è¤[–¨@³¤Ò§ÙÔ†Ñ@FºK „ÎÖ 'åà4Ú•c=D”Q• e¯pR¸xMm°½¡= Tå®™?`©ÏÞ¼éÍŒ¬°È•ÎÂ+¥´ŠšëÑj·5Þ}8OÃÙŸ¨€¼°(ð>Yã�à:r…Ù˕Nî‡eK¸|õz$¥ ”¢²¹b9{_ÙdÛqp%Kм@ªå½T–²f¶B?¯Ð÷(¬”9q™!K̪'90y¤–ØÝj¸”ç-; «%gÍeŸfÿk¸ž…ì!PeJ>\"vž"œy ×_† m×°ÀÝnw�£flÄz¿éÃ0Ô˜aÊÓ¦’f*3#k¾.CÍ©÷XúƒHøñ—@êÌíÔoýmü³ÿ“ñÿú >ýùZÖÞdÈ]×e0¬°Ã#Çñ†Ì0£¬Ûùí–eÕ¬iš’eY-‡n‚} 4¿ožbžÃèYË{ ¨rÌg—×Sx%®()$oªj–KHâ!~ ¡Uqa‹×.ˆóFEç¨ÖÝæ\Éó¼ÆÈ›ç0úM’¡Ó鬨[áÆ4‡Íi–“ÄÍ0ÀVѶ8^AÛáB5Ÿ¨�Ü"Yܳ�T‚$w±½°†®™Æ·­l˜InS m˵¬xŸÐɰ,U5Q/¯YT4iŽMë*yÿ¡›ea,ÅQ�†]¬Úìþ³ÂGWÒ$ÖpŒæD�€#_ܦå|,'Ķm²d@Ã^ɤÕÉòÞqû¿ýûÿÝï+LéK€ÒT¦&QÞË¥,ÊJ’UOVJ;³�áÒ䊼(q-—1kŠqµ“éæ E^l;9Ú¤`\!Ä:¼| ?~ ¿í£²>–Û¢ˆ滉[éî#lŽÕY`ÐŽÃvîÃùM3×z�W›�Ѧ3!–»â·Ê�6F˜Žà‡eF0ãašÎLyÜ” ŒXOaÇ·7SJUØUÛèóccº$ý Ïò¹‘^À-|,¡›Lc•�algŒ>S®3óG °ç”¸¶$/5½ŠëºZb²ìÕ`{„MØÒÌI¶ñKmÖÑt0›2‰Ù£2˜†ÒÌئ9ÃÀôá’VÒ¯ã“(Ѩ•²6ËÀêw×Âñšõ5=Ï'ÎR›2À²”oœ$57¤”’4‰4ÕÏ6x ]["GšÆŒ£’¶e¸'5-ŽQºRy¯–°tmI‘guÓšã5‰3E)·æUÔê8…¦+­ö�J¡i8èѰ³ÚŽå¥�B¬0ò¢ú[hKÛñ±Ü�4£V×¢Z'[’•:Øùë*”™æ1sFÛ¶‘·Ô™HÛ²)Ê‚¢,*gBÎ`Û ÆVJ©Ù5lŸI±‹¶šfgk�>X·¤�6Î˜Ž‘W_ÉS­ðÓ7ð›•GXn‹2í1LZÄ­¿Ié$ÇV8Byžo2äû,Ì?àG?ü>û÷¨™,ËbaþKKʲ¬q¢£×1toFZÜÈñ–eÉ©÷O°gïÞzŽyÿÌ·Û:cuéâED•p1ô‡Æ)[Ý5Úhå8÷ïß«•Ý äCNïDØÑ÷¿M~û&*MˆÞ}‹ðW¾Œ÷k_Á©l¦�|lw³Ù\aÇM¶q#:Mc‡GÙy`ã†;S‘e¸W\§wi:ú]­t6ꤎ:—«M³—Œªå¨¬¸ù;(|;G!ɤ¿˜ö;„nJ–Åä„ÍÂrÆ:[¼Q`cl.P³�>£¶:[mx“Í\ãëÀfHY䨊�£´+[VÁ–6ÙÆ¶¥(•‹„õ¾)•Cš—+zÖ†šU@È”()hVýiš"‹tËæ]Ð¥~¯:ÇZ–>“æúÚÖ qîâ�Hªö°jþ•8¢j:S­ö…ÔrÊ[]j�2ˆR·AدáMÉpi…ƒ^T²æ£_Ysü*­ŽæùZi®( ¬JµÒœ7¦Ubÿ÷ÿàw~?ÍJæôh…>–¥_Ì{ Za@QH’¼$ di¢°XìFŒ�…I~„[K%æy^—†Ã°òþ7áx5Ζ¢v´Ww›‘¦)�N×uëõ6Ô:ŽãTå�It¼²Èë=Ô†º, šíi kŒ(s)åÊgç.aS;ÙÃAŸÐ+hº1Y´ Kk­1¬Æ$Qî­™kF!-¼ o•$1Žì2èÎk…›±i kœ(_{í5k�6l¡;$,�e Íö¥3…ë�×¥:O$#Žírã‚@3+øVLÔŸ'ÏRší)”;Á0ó(¶¸¾Äï¡„eÑßA"ÛD™†h|—.ke…³B£´�mz¶ì’§C¼`Œ\´‰sG S”}Âü�Mïc³qíÖ<ç¯Üáò�ûœ½tG«õÄß}í4i.9}áIÕ©­Ì´}}÷Ì5nß[âÆMJ/•âüå;¼òÖ9öýA§Ç»§¯rãö<¯¾}NÃK1oŸ¼Ìå÷yóÄe,Ëâö½%ÞÆÉ÷OpéâÌdÇÎ�¼ðâK\»v•Ÿ¿û¶msæô)Þ~ëMŽÏó8}ò$/ú³\¼p�4MyûÍ7˜œšæ�Ÿ½E–e<þäSìÙ»�8ŽxõG?Äõ\îÎÝáÈÑcôû=Þïç|þ _¤Ýn3??ϕ˗¸woŽ�¿ð÷îi<é›o¼ŽRŠׯ“gwnßæÐá#š^K-+[v»]â/’x@è-C†”5žUÏÏ–ç;.a{šD†›Þ?h{n¹Ë q½^·^»aîç:ÁPJ‹8w)¤EV.7Œ™á9’À‰‘ÙR*\‚¤ ÈK‹¢´±ò{êößúíÿê÷K©xâèîÎ÷˜žhá¹67æD Žë ¥b×Î ÝÍ®té«üägçxêÑYÚaǶ9{ùGfwsûÞ"³{§±,‹‹×îÒj ã”ã‡vcYWnÜã…çŽqo¡ÏX3 Õl°Ð0ßéó±§�Ñ ¼:ëj°B£Ù/ƒ¥´�Ü äÑ<±xeiCj`«å!W—ÌM¤j¢ØÕrƒ«ÇFQðÃ)ÇÇ”¯MyglNÔÑù«¡›EåV5�c4¯”ZWŸ|t~Qug³ æzsG£fó|<Ï«³£YN#�ià(is9>‰[¸¶·"“¿•œïè}ÀJ1 Ïo"Ñüx&j¥Á2#+r©« æŽB6€e°Í†á¶, ß÷ëL¸>Ä]¤TX(Ê"ÇqlG;ñžç�ÌpWµAØ&ÉeY`Û )8-U–¿Ì–%r[U%¦%Fø}³r�ˆC"Ã%¶%<»ÔÍRóãÚNcEôîl�èì�'R½‡m¿>€¢AÏΗE4”`u.ÂdQ-òj¾§ñÐ¥Ež/CFÇ(Ô$IÕG• iuðüp]~m#ı:#Q“�çšW3/µA÷‡¯ŠôO¿ñ¡2·Ÿûâþ{O?vÀ=¼'ÂñûI³’[s ,õ‡X–`fjŒ†ï!‹¼²Á nÞ]äÒ�»¼üÑGQ²ÄvÎ]¾C»ÐnZô£TÜŸï2=Ù&ÍrŽ˜!J2zƒ˜Ï½ø¯¾}–ç?D)%WnÞg×ô8Ï<: ¬„*™Œ›Á„×™7î,`QÄ DÉÉ“'8öÈq&&& û|îó_Ð�_“SìÚµ·b]9|ä(ÃÁ€ýûèì¢esøÈQÞ?ñžVëêvi·ÛìœÙ¥¶¤d÷ž=Ȫejjšþ ÏÎ�3u`ž$ SÓÓZ>¹²ãÝ~‚oß,âȱÃÍdí��4ß½ú Ûl¬>ƒŒý2Îàèk`¦Rh’AJ)®Eç(²ÏòJÎw½ß±|·I › Ô�Üq4ÀñšwZã+]Ë|ñ£•Á²,W>m„í6 “�0¿×|^ eÙØ¶ƒ*ôzhœù¬£äiD!-Z­1Jå‘fËåþ¤p š:™3‘éµ#Y^V"º¢&Xɵ]”Ž?^Û0Köµ­aWÔ" iV"ÔÖ­e®ñ˜8“4[ã5óFwVà� Ðêù®]Rä)IVT÷ŸdºO`=Ø[Zº„­j-}|k@–Æ”Rèd‹½|Ž›}ªF„8FEzš£×9Yš‚pIsÃïÒh¶pü†K’æ`YšÀ¾Æ ^xî8ßzåç<~te‘ãºI#¤`ïÌ8Bf÷LQ–Y©¸9·ÀÂÒ€(NÉ‹C8Ž…°q’ú ÏÕ, ŠR1Æ4.y^r`ï4;§ÆøÎ«'øÍ_~¡Îæ­æxÕø©†?§9u œ]àÅžœÅ�’:³g²ªëqÄš1±,«nàŠ¢hM–a½1�Î_-ÿ¶Ñ0/µá¨5‘¡(í|Ýhþh¹ÿaøuÍ0”9aÖeòÍ~³&S9 jÌèÃp+¹ÛÕò½f½ LÄÌ7™ถG`µ¸X¼ËXw‡‚Ç Cí$›»Mgæöû}œ‘Îä$IP²„uèy=»$+µÓcžá(·§¹3Øh-ŒSÚÉ LNNR–%‹�ÂÒ\¾e^9HK"Ñ…†ë †•S�о¬…Ñp”Œµ´SšÆý5MBèìG){ û‘æU›Ör¶Å�†+É «úNA4âYË”c)+§(—ö]<¿�ì ŠX2Âs6vrMƒcIŒ.Ë’°5N4´±‹ˆÀÛ—.�[P=©îæ5”7¶Šjì€R„UðP¤=üêo¶H ¶½ÜQ? V<·¼´)¤…kIJ%ܼ–n8[¦¥‡�^%°ï"œ›Þ÷fÃqmJ©(J‰ãغz%3;Æ ƒ—®Ýåéã�e‰°TžS”%‡öïıíJS0¿8àþ|�fØàµ»|âéëV\¼>Ç‹Ï_Q™‰“ÏÓ�“RðòÇãöÝEÞ|ï"/ìxmGW7#)¥ô{ÈÁuÂÉ£`OAãq„}g˜�f9v•‰\o>,;Zæ4ÏÁT§@ÛáÝ{ö²oÿ�ÚiRJqðða¤”<û‘çëïùÄ /êŒwÅÛj;33»Ø·o?böÐa¼æ9O=ý,qóìsÏëýP5<˜=Xg3MôŽ�3(¥Ø³weYrðÐáú>”RìŸ=Hžç|æs¿PKÙ>zlÅ^2öb55�=‚P’1¨›y�mßÊ–:ŽÃÒÒRí ~ßíÌ]m¿ õ˜ Z€:Ø6É$¥y®Ÿkh·¹%/p¯�=Év�í­eÂM¯Áv�\óœã8fll™ë:K#\O°šÕÔÚ±K ·þ�æK¯$-ráâFïãû¥>C÷ïžâÖݯ¾uŽ];ÆizO�·™h<ÿäaÛ¢È ¾�븕-Ø9=^áO$Ã4ã©ãxìÈÎ_™c¥Œ·|f¦Çyæ±YÞ:q‰aœâ7\ßã?=ÅcG÷jj(¥¸s¯ÃÅkw9°gšv«U—*×ëµ²ÛîˆÀÞ‰ôžÀr&i·ÝÚ)6°‚Í^°ÕÎøøx�Ûøêù&“¹º+~³ù¦<²c‡>Mp5&v£aœZïû0ÆÍ+³fϳ™ƒî8N� urG„íbaGµÍ À¨ŸA)r”Q�P‚þqfŠ ‹> j ÃÎÐh4jÇ};C�mÛµ±÷<�Òžbð¬lEGh!­5&“ßï÷™œÔDì›=»Qƒ ËTqæ�™œš&/J†ý>²j2Ëò‚‰VNiX2BŒ8ÞŽ¥hy ie\š­vÝØ’¦)¶Úظۖ"´rÊr‰A¿�´°¬€8`ÙβˆE1XWÄB;¹iÚ!MøAËÒäÝ6ɺ̪ú—©€±Êéìw°,A#Cˆ�A<ÀRɦN2€cK;#ωÓ~ÐFˆ�(êã�R*‹°r’¢áÏ^všuØúB&£Á‰ª¨fiCg¯m-uœ•6BFÉkØãÞ±øÄ3GùÉ;ç‘RòÜã‘Rb�ßàcOeî~! KS‚f”Ä‚†ëÒnäE‰ãØÜº·È>õÓm~øæ™ŠÚQ°wfŠÃûwræâ-ìžÄ¶,î>èòÝ×Þçå�>Z;^¯¿û8~xo�ÑÚ¨ Í.îÐpÎB4Î^”÷¶Óblܯ›…Œ<èf6aÔÑ2b>K9š-ÆIµïf¾Š¢ˆ§žy¶þþÑaæ�þo§Óarr²~§Wß³¹‡õΤQžàQšÄÍì ©Þ˜5u„�C´Ñ|c¥”UVp¸†ãv»‰k$È4™ZcC¤”d2¥é¶´ƒV=—ÝÁ,Sùnò9|K'7°Í Ugq GòÃ$ìª1RJ©�Ĩ�#V2Ähl¼³f¾�îMLL¬&6²ÇFd–ùöÍžm·Ûø%dyΠ»X)ºäy†8˰‘Ú–Ê.ÃþÏà fêßG}ü è…ÐN®T¢~Œí†´Æ¦I’„,à‡k˜ IDATKDáZk³ÐB@àHÕgØ‹°=Íx�& ÃL‹<¬×[¡ÐYaÜñú¼ˆ/$hMëÿN4ìõç/__ÑpJC¢~‚U9øiš�¦CNA*uÂ%‰zø•¬º‚ÕP#&bø…Gý°R ÊÊg¥M£²ÅE^:XÅ"�<‹åi[,CC/Ú v»Ý‘¿‹º Ö ƒú9Ž�µq]O 6tø”‚¨™˜ÔÄèI“D=B7_ãd®™‹¦¾’V€´jÜÙ ßÅýmQ�e¥5¢T¦ˆ£ž•¬‰þ³Rà…3¸®Ëp8Àg� Îm ‚ª .ŽøNº%3‚InWÙƒVm³ \:|Psó–Òb˜»k°\°,ymœ•õ†J+Â’�þh=„ã£d޵ëïlï…]5æçç#¥J)‰“”hê/Ƕi¸®O Ažåu”*„Ђ JâùeM ¦IÇP*¹Q*UÓ‚ R*¤T¤©îâ]{£:§Ójµê—e]~T•¨p|�ÕSyÌ0?@aMÖŽ…iÖÙ�)ˆÊoÝnpò¾–þiŸ†§) ºÝî ¥¨íêu›—Ù8\£´3›Íe)0ÑËj ÑÃ`Z æhô·¯7߈=\óVÔeBˆZMk£ß¾]ê3Ó<ŠÃSR2:¸ŽÍƒü ò=È[ȲķB„²jƒj“y5kgÛ¶6þž»%Ìd{ £‰øÍ3Ô­I¦(×Á+öJ¿ß_�ç Ãp›ùÝæ³+ö¥å „²à­7^£ÙÒÆáôÉ÷i¶Z´kÄѰ�$È¥…_uåfiÊ7¾þ´ÚĹ…ç¹T¬} ,~úÖûœ9w‰R ¦§&l Nž>¾¡ýè«.7¸ÀX|•1wË A• úɲs;úÅ3;wây.q“�lÆ¢Ô�SJJü À±¤,±mÃ÷W©çT޲mU�ä«öažëï´­•Nî¨!ʲl]£$Ê�} ËÕ³Œæ‰½AØ+ŒšRjCÊ’¥¼Á?þ�Å�/+>¸¯xý $¥ÅGØõ‹µ¨þ0³æA˜¦33ß�å’„aPÓ•Œ¾`†cÏ8_[WSÖ27ßeŒ£Éì˜éÆÑBÐëõF2è› &úÚªÄh¨dF�Š™ãû>qæ?ñÞ­Ÿ³-2;1;rô÷ÊëK U‰%l2+bIÞg¾»Ä®æn\×åOþï9wÿ7:×ðÇ�Óij5uÙO¯¿ÎîñÝŒµÆêèwôšõj¼åêghö€¬ß8¸Ú ŽB«›6Œ”¥ù[M5cYÚǨÞ!Gü‡?ù·Ì?˜ç©§ŸáŸþápôØ1n߹ω§˜=8ËÏÞyŸó®‚p8wþ"'Nœafïa._ºÄÛo¾ÁΙ)¾óÍo0{ð �†Ï‰§9á*ûÌâ:‚÷'_ã3Ÿ}™?ûÚŸóäSÏðýü˜A”pëö]‚ äÖÍ›?üþwiµÇ¹zý?ûÙ{ÄIŠçùÌ/,1>¶ ¯޵Üô¥°5Kj'µ(­º© ðD„Puã‚c+,¥y…¥›Æ²Ò^·émÍþ3ï‚ÔΩ뺤…¥¯/×S¯¾ÿªcº×ëáy^Ý@iÞ‘yq“\u92ü¯5 BPÆ]Οá_üÑ¿üPÎíW¿úÕß\¨Ûf×ÌN¢8¦(J¥$e)+ú$IêL™®¤é^ Xæ\Õö¦Úï#Á�’Š,/tp¸ªûd´ÑÖðÇ®¶?BÅ„â–WÑêÅûi°t×ü¨Ô©qp ü©.ïãò/æñ'ï*NÜV¼z æc‹ç8DÃ~m‡?Œ“iÆz G¦”olŽRàûËte£*Vq}oe‡Í0�Á ;ÌÊ#Ñ£�Ä[%JÂ0Ü´²8:ÔqM6X–E·ìò�Ó_çÚüUö�ïÅ÷´P€c[ä2áfz�X茮BQˆŒŽ¼Ož•4ìßõy=ÐßѹŠë „£hycº¬že(¡xëÆ›ßóè†É†ÍÀGסÙGÊõF&H�RÒï÷Wœ¥«ÏrÓgë4ÿZŽ®FÈ‚ÐËpÈ Z“Œ��ë=RU¹ ÇêêQ”7˜¬¥c¬¢K’Ä`édI^Úy¡¤6ØÆR °µœ}QDÃ�v–-Ÿ8•¶ qÄp}—”EBšøA× ‰Rª¤ A³¢¾Œ‡\›º‰Ìp…kÇ‚ ÙÆñBâ´Ô<²Ûp²•Ç ëó×¶]’L’‚Rmn‹…XÖ6ˆã¸V*4Ï^!¹+.ãfÓ‹¸¡®¼gÑïÉ__ëÜgÊpº9ŽC)µ²GÃskC™¦)¥ÔW*E‘—Pefd©é½tfͪékª;B*Iž(Àuìz“™îÿÕ‘³1:RJ¬rß�Çr*'!VäîckfÔ¸šŽqrÿøÁ™»Š�ÍÚ|ú¨Ëåyɹ»’çö*‘¬0P†cÖ²4>GX6y–Ö%óRZ¼wê'NÝ¢(áÞƒGë.ßÑ—àîý!×ov˜šôy÷ý›?¶oE&T#÷¹ÐIXê&¸®Ã•k‹ìÜÑÒY·$Æu5TâÄé[ÌìÐI†uFa£²›9XŒQU©ÙÎábèlF( +ÄwÏÿ¿òø¯òísßbª5Í7Îü%‹É"&fyõú¹Ù¹Í©«ç98y„—ÏpöúE§Å?{å_ðÒáO1Œó­3ßà·?ñ_òíóßd|lŒïœÿ7—n€%ø³_Ãs¸–Ë¥ù‹Þy¸†2˜5åA\¯J0ºFa£Üjƒºz¾RªÞ‡5ÖV)†§Nÿ›Åð?ü1ù›¯áXÎá£HY89gÏ]`nî. ß§»ÔåÀ�t;œ;wŽ¥î�sçÎ333ÃÅ‹—øàƒ‹ì?p�³gÏsúÔIžxâIÞ~óuºK]ÆÚZæö�ÛüüÝwyäѧð›ã¼ò£177Gše<õôS\¸x‰ïï‡ìÚ½�ï|ë›<ñÄ“|ï»ßÂum~ôƒpwnŽ?õ¾ý­ïpëö]öìžaÇôøÆï@½[dêdij2Ûi,c»ÓÞ2¦¹¢ã2MÜ-Sâ$«Y!â m—6ɤ¥6ÚƒÞbÕ˜¢e,‹¢Ø4H5%bÓ\i�\×Õ™L™1Ç%Žô0:Û%y0ð9É/ñÿèþk;·°|Ø ¨Jix—ëè¥H³ ¥ôg�çF]«–I©3îVµ¥�áÕ69ËòŠõ`ù µÃÆá2Î�ÁÒÚž׫$vY,ɼÇY�Éån6N®ÉâüøšÍ_žQì³ø•§\æz’ó÷$»ÚŠÝ¡ÎŒm(nÇÉ5™ÃQj¾0 ‰Éû§osùÚE©XèD:¸ !D�Ü0�Ìí¹>ƒaŠß°8ûÁ=ÜUWÄF‡¹�ûóe©K®÷î˜×}ÇxžG«ÕâÔ¹9vL…õámlãFMw£p5“á5Ù¶­†Iê¬æY/Š‚v»Í…ù …ÞƒlÀ{·ßãêÒUZ�—:ç‰Ô€“WÏQ@éðÁÍ«ÄqÊ«—~Ìõùë|lö㜿wžB•<¾ë1þü쟑Y1¯]y�ƒS‡yãúÜëßãöÒm’"aª5ÅXk¬^C“ðÙŠeƬƒaVX�t1ïízlC«³¿�µ°aUÿ6¾S�–A¨û€z½žnXÇÈ Ý1ÊÒ¢” W�æ—’$Ò|±®­2#Š3·A#h‘æB ^­c˲Ò%aðÄ�,�‘X4›c”Ê&Í·Ï5îÚy–�åŠ0l"œ× Öp ›Œ¸ …Íü$IÈ+f%•“½± �RPZ-‚@¥ýÞ"¥T„Í6–íÖ¶d³w¸ÕjÕ窩~˜JP,‡,r‹#ƒ«´[ ,T‘p#ÚÏ%>±Ò¹�ðLÉÔ¶,žn2Ëò‚Rj<„ßpñ\×±ix ‚ÊYÒ0°+\ÞʨR‰úLnšéìU8âÄŽFΦ¼S+„e]å>\;!/l²|éîÞpqŒó6j\ÿíÛ þ›Ï48¼Ãf²)xïfÉáIÉìøZŽ×Ñ2“ÉÒmTî/¤Ç›ïe¼öÎ�³—2¤tÙ1e“¦1–‡îbî^—�?w€KWæ™»×e©;d©sñò<¥„±vƒ \ºÇî™q®^[Àu'ÏÜ!ÍÝ~Šs÷zt–&ÇCš¡ËɳwèõSÍNqñê7nuh4<Î}0ǃ…YV’åàºÍõ› x®Åëo]efG×µÖüv³†yž×8²íŠHŒ>£°Óªš¿yúë, ÈeA!s¾ôØßàìÜY.Í_`¦¹ A! †Ù�$Ï8´sVï¡Ââ³Ç>CZ¦|ûÜ·™ëß&‘fwÌRÈ‚SwNb{Š}‚kó×ùÖ™oðë�×öX­tfh¨¶¢íÚp/EMD¾}›£Í ý}�ÿÛ?Dv�Ýj8 }÷-Ôý»Ï„†+y÷ç§xòégùÞw¿Ã#Ç! \^íu˜Åõ<†Ã˜‰©ÄÑÛ±9úÈãÌݹCÇìÛ·—ù÷éõzìÙ»‡Á Ï™Óg˜œ˜àБ£ìÝw€7~ú:_þÊopúÔÉ—×étسw?YžW¥�.žçrì‘£ ‡>úñO‚7~úS~íË¿¡�Ð-”΄qreÊ *h·Ç–×2_©°£” �6 «.ÛÊ™²Hk¥2·Ñ$JJ®U*ËK‹Fsª"L�qÔ€†S§IºL¿·ÜÀ<'ÓD5ú·¼ÌHä€À Ù�IÆÊóØ~¤ÃEÞU_fÈ8_û£ô×vnMÇ6,ïMǶ+¼¬V“R⸾çá¹6®mã5ø¾>°då¼ê %:Ù �~Ùr!•þ¾ßàØN›ã3¯].óáÙÝk�ǵ÷±1 b^z¼s*ç­1×nIÂÀ£ÕTUEÊfj²Me?¶“³Üåöܶmqg®Ã…Ëh5} ›[wzœ½p—£‡f8sî®+8uöž×`q)FJF9÷ç‡LO6ñ}—§nÓ¤ìÛ3ÁÕ‹\½Þar¢É™ówx0ß'N :H9yvŽ;sKxžÅOß¾ÂÌtÇÙØ›Š¦aUØ.TX‘76ìÖÒ-¾wî»ÌçùÈ��ðþí÷ùä¡OñÚ•séþ›~š¬Lxïú ”„'ö=Î[—ßá‘™G88~”fç3\â¾ÏÕ¥KÝu˜¼(I˄뽫ÜéÝá³G?ÇŸ¾ûïñí€'v>YŠFÄÀG…‹¶û;L6ßqœM)î0¯½õ öìçÖí³ûtFìÙ'÷sêÜŠRòÁ¥ûx®Íµ÷Ù»{’Ë×ÂtæëLÃÑC;¸rm�0ôˆ¢”çŸ9À‰Ó·xâÑÝüù×ß'ŠRÆÚÝ^ŒãæÌºvµšèß` Úív�«zÚp8¬š^“§÷<ÍwÎ}›,Ïð,��ìûøÊ?áÿøòò¯^ÿ909‹[:øvF© "D`žå1´yñé§IdÄÙ+—(JI”EXÂb‰{dVÄcûŽsêþi^_fii©Î¾Blÿx,_·Û­÷€i|Ù¬ab”P¼ìuYø×ÿœÖKŸÆÝ³J…{ø(ƒW¾GôÊ÷h}ê“”Ï>Å'?ý9Ž?ú;vì`¬íÓnú(©��G{œ÷-‹³gÎpüø£<ñô,bzÇænßäέëüò/�«W®33³Çu)ò×óØ9ùäËŸáâÅËüÚ¯ÿÓ;¦xíÇ?á3Ÿû�?ÎËŸý,W.œæÓ/‚7Þx›}{÷p`ß^ÚìÛ»“gž}Žñ‰i�ñNû[vÓêŃF…q3ÝØAkŠ8àã9z»vYA *9ïªé˳¥h“ŒLø4›mÊ2$еD²k+òÒÂjLÖ8À"Ó´9R J¥3ŸÆ´Z­ú}ÝÃFyõ~ÐaŽKŒÅ� Îá´'A)T™r+;ƂػåþÙl;lŒºbM3�%aР(K’4#/Ê â¥›ÊÈ ,KC¿�ã4õaYý3�2Y–ºÄ\É©—²Dáhõ5·�Tk)Í!ÔéthÓ»geôâ€$)°-›µè¹•cÔ~†aÈD¨³C{'l.Þ/9´CÛá±Æúrª«ïc”q4‹iY.ßÿñ�ÅŽv”Ã’½‘óùO¶Ù³SQ–I’aY‚v«ÅäD‹gž<À›?»H§1»o’KWçùè³û°,Á“�îå½Ó78uö3;[œûà6ͦO×ÓЗ,Ë9<;‰mÁôT‹n/¦zôz Ï=½ŸÓçî <ÿÌ.¾öï‘¥9î¡¢8c0H ›{' gÓßn“ˆú0vx”Ù¥xvßsL·¦¹tÿãÁ8ûÚû˜ëÍqdÇQÎÎ�fÌÇU�Ý"løØÂfÚÛ…T%žPÊ‚§?Âã�"¥âk¯�'÷=Á 4<îs…‰V›Ãû””µŽ¡‘Ô·¡Šáf5K‘IlÕ0ï–a‡0=:Ë_¬aa ±E‰k—D¹ÇX[ßcõ #d•‘&|°…RKŒ£«ÄEQ ÊÁê/ÐÁ¾k§¤ñR5}V” ð¬œ\ºŒM„õ5ƒÆ-&“SK 3—F0†m‡ÄQ[ 3BèLì¨(P«=N’4�ù b‚ј`C·¥ƒ`QÏ×Ì =½Ïo¶wèu/"ŽÆ'…GØžZVƒ•XT‚9ÖŠjèzÌ&ÕétÖìëyq“nù€}é{òSØÍ)�Èz\,_` tÅi6›5ø{”[v½aY‚ÐoPi•R.KÍŽ�¤ú¥K“Dg|�-³…YU„Àót¤"¥D ·âxìŠrýDAš•,Ý¼ÎøÔ^Æ&á4"’dëÒŒ™_½^�_y¬Á?ù‘à«ÿ)®áàÑ]‚Gwä¨-¨ù„uÂp šŽÞÞæîe¼üñ zÃ’aTÇ%ç.Å<ûøe¹¬ƒ¬”Âulš±sG›éÉ&;§[U·  <ÏNóÎ{יݯ)jvL·h·|Þ]5®.ÏsÆÚ>cí¶YVróέ¦–*8°²^ëfÓcÏ® &Ææ»Û€ZhŒšé"÷}¿æ¤Û®Q2‡vÇ|ñø—ÈTÎßýÔßC)…gy4½&Ÿ{äDÀç�ý"ÊRìß@© <«ÁžàKq‡ý³üæÓÿûÕ>�?ãÑG¸rç&_zö™jM€SòäÁc„~H¯7d)�gÜŸ®i�LfÌu]&''kêœí ³Œƒ<ŠE^=F�€èüYŠùû8ÓÓÏ‚ragß~plTQ�¼û<õ<ýØÇBðôÓ�áWÍ {wR¯…T\ºè°kfšÏþÂ/09¥»]'&&Ù½£É =J1õ‘'0!æ—ÿæ¨=2^~á)’¡èÆ©¯üæž^1è-ðÉž_ùõ/V¿„eqóÆ5~ýW?ϰ7¯)gÚÓ$ILšEøn±a£DV:4›Fa§�P Aå¤õZŽkËÚ©Õšíj�R™RÑ CØÍöY–2L†ØŽKX5šE�#t€žË*7ƬR£(ª»ëW¿ ɼ¸…R³ƒ[4šM”*Âa0L8'~q[ûf³a2C—mºÌ×{'Û¦Y5eyJµÌRÅ)¶­¡C–e4°mð\ôI£ÖR–ˆJ%/$yžP ÄÃÝîã“{Ÿ:ŒÕ["/¶ßtk¶ø|ÿ<üÁwãê…¶/Ìný]Æf�‰‰‰šeîAI–Ižz´Iœ*:ZäÔùˆ}»‚:`(Ê’¼È5tN€íèŠäÄDÈÄDX;™ÓS!qš3??`j²ÉX+ Ýô‘Jqçn—†ç0³s¢nüšž …° N2, |�4ÓÕ¾™íú»mÛâÀ¾ Ú-Ÿ¥nK;,¥V+ì÷û”e¹fo'Ho4µJÔŒ¿‹ûþlËá �~��›(¥Ø5¶›—f_"ÉSNÞ;Ég�~Žýãû÷ÆøÂ186õ(ïÝz—¶?ÆìäA&›“Èr@_txþðsdyÎá]³š¯[ä|ö©—°„ÅÍÁe6A)ÅÒÒRÝÄ'„`||œáp¸mvŠÑþ㬸ÆzUU“%6ë˜%kKÈ’†[P”¯¡±¹išâˆ•¾FÑx*&‹RJáÓl�×p‡$âY›ïcMÿ5$&`7iµ'ÉÒ„†íÔû{”n ´]Bgqµƒ½HZ1ÔH2Œøvº)lKŠ N$à Ïoá·5W¸Ê"B¯¬¯šW­øo­T–熩‡ç·p‚i¢h€(c™¹*Ïs-þ$ÅÊØð�jÕ¸õئ 2:ÜÃQŠÉÁë¸íéÊ[tâ×ÄGëÏŠ~¿¯ uµ´ÞfÃÀв¤Q�~ËRj©:ôÆk†kåmõU-ŒiY­öI®£ßêËAæ¬ö4MÉ!ôµc³5�ï‡M›%„àõüŽ -àÑ]ÿã}Zvº6šÛb ,Ë"¬š:nßMøËïuøÊ—f(¥brÜåÁbÆw~´ÀßúÏÆ(K�^/¥ÅÌÎIƒ˜,ûÿh{óËòë¾ïó»û½oUÕUÕû6Ý=9wŽ$J©H´"ÅJœ(�b؈#9!d‚¤˜‰Hÿ`p $N8‰!ÅA$Á’LQ4)‰")rfسr–î™îé®^j¯zûÝïýå�û~·Þ«®­GÑ=è®[ïÝíüÎïœï"¥F¥âroy‹ZÅ@×YV\&C×ðý×5ÙØQ¯9Ø–Nšæ 4¡Q«-gßÂËÔH3èõξL¥ IDATfg*ÄI†méDqF»Õ Ë¡Óé„3í*–eƒÌJMÂ=Ôñi76xR[ö {0)³³½½ýÀñQñÊý—¹<…º]ŸÒTT# eÓ©ˆ]IóNò){»Ð¨h0ÇEç)’$) Šô¦^Bµ@ECé¢*C ×u÷”Ò4­ÔÎLÓ”þ_}“íÿö¿A«TXøÍÂÆoÿSš?÷Ÿà¿ú£ïü%•Ïþ$­/ü:«+Ë ΜœÃµŠç1JŠ ‘&rTRöS‹Z}¦¼V«+÷1õŒVÝa}³ÏÂ\)U‘'Xßèpj±YžG†ÍÛ×—xú™gË�Gv°Íéw@%%Û(È éX²Æ´‹9 FˆÜ@ß6—‚Üh⺕±Ì6¦ž“KA”jÃÃu+DQHït‹âxݪbÛ;9 Š®Lu°Œœad? O<êBÐétø÷u±Ä6÷97Ø`Ñ*œÑ…ÉÆÕÁG¹«´üÙÿðÒ“ЮÃÐ×4ÍU›U¥œrXäRFqáØcè¤iVB¾Ô{Sq÷ÉÚù£«iTªENÓÂ&™Cž>�‡U·Ð±5 Ó ^ŸA×�#k«ªBðúªÉoW#J¡b ~ýÇm��J†Qåa5®óúˆ\ xþ£M)1ïů�ä1�l�½Á#æ³øéCÛQ¯èõz¥²Â¤²†êÀJÒk8ŽÇ’ÊMb´üÑËÌýùŸ“�†x~–Á_|}î2ôIî,QýÉŸ¡öØ“|í«ÂÙ³çxé¥ï“J�å•m¾ýW/"…Å;×ßÇ­4¹úÊ8~ò®ërïÞ]^þþ÷I’”«W_Å­´èôè†Í‹ß�“'OD1ø‡‚ÐmêÍY¾÷â«4Ûs¼þúë…z‡”¼òò÷±Ý:ß¿úh6qZ°ÝmS‚DÙØ QìHÆÈ,$ˆ Iêà�1d*±†™�W)FEÁ¨�m$ã{«ìt Ò™¦�qd©(ì|÷°ÓÍ¥ G”X0SÏIÓ˜œ�wWmЄf3 3²œs‚ꘪ…{êù$bUÜd>J8)7Ðí*yÔG3+¬ôïè?‰d'PÌí¾ð…/êºn–’9acŽ’‚lVȈ ¡‘f9–i¢ïGkVão¡ãx®c‘¥IIJ+n´1ý³††iÐv—Èý%‚¬E–É),úQ1ù§Ú:‹m›×îeüò§5>yÞš"�e£¹ï¹ïÜØî&¼scĵ#ÌٶÆñcJ¯´Q/dºdžŽ7ÅöÄâ Ž]œ‡&Š)ZAdÑHuÃ(ò°¡+…ŠIû‘ç& §4MI£^ÁwAi�f¢‘’K�4×°ìêx¢¡gƒ‰[QfQ­MƒÁ ‡®…-z¢‘¥É6Þ»CãÿÌw5Ý0‰’âP˜ZM®83È¥6žní(Ì„acòn’¤ÉÎgK ¹^ÃvŠ©OíØ²kZQÔÇQ1=w½*Bw£ÇÝQlD u0µ ?±ÝjYû•äÇT#Š”h샡 &JA)šL¾#zlˆ%Nû=Ž#„n"³¡ÜÌq[ÿ$“�xý×ý׿¬H1ŠÑ©ØÌ‡1ÙÔ—Úùÿ¢`[æ…íN†NµZ!Œ M[ò”rn�,Š[­ ÙÒm,K£*Þ!âÒ(¤>“¶R©0焘Dœñúe§P‘ÎÔŽò¨ÉµHÄ13-“·ß ‰bIšJLCðéç\ô1nƲlÞ_Ú¢ÕôŠLœ3 Rò\²±Õ'Ï%“zÍ+ ,Õ��âú¾÷þýAÌ씆¦i¥Ä×îï® ²½|Ë'åR”Ãdr¬T*ûêÛª{ Hg»ï�TW;òƒŽWiuNª°U²@¦i²ݦËC:ìM˜ŒqGwÙNÖXlœ*¯�º>JSvÄÑÕîy¯û¯:»ˆÉó¨V«¸®;Õ�È¢.fÝEú!Ñ;oc?Atí-Œf“¬×C4šÌüý_Æp]^|á{ÜzÿçÎ]à•—_acc“cÇæYX\¤Ûíñþ­Û|ìÏó'_ù#¢0â�×_åŸø8÷ïÝãôé3\»v�íÎ6++k|úùolš£QD–K:݃Á�û÷–yå•«›Ÿç¥ï}]×¹ööÛ\xäKKw¹qã}._º8Ö •¥ºA’ë£VHt–žÇ!q’JZ¡o›çÃiŒe~BD6@×(G\êu7õ™G„aŒeWÆD�¼$j¨7¹¸Ì‚ 5ÇÇJRiR©6Ê{0É öÆ>õå3åp§L<Ôb.„`KÜÃHûœ®cUfH‡«èî ‰¿Å‹Ùß&ÓŠ´¸ý�ßø�/V*S×õò´Ùb¯Ð¦ò°@× mcã0½(m´£$-,˜e¦°S�}\ä dêC²�ašÔõ·Häq¤yl 3yÙk2*• m;"ˆ2ž›ë"ó´Üh*§«½6Õ{…ÊšHÈr�Ka”FÅ$á“Ï9ã© 4£”ZÕ¡ßÚDFFa”⹎cíyš¦±t·Ke4êNùl)^¤Àn�ZµáÝ‹@5¹Ùß«Yà8Nùw»ÏV÷BLÆwKN?)¹¤ B¥^ò/rŸ;ù;ŒèÑ—›‡¶6§Í+lÅ«T�žãMé(«k§ŠèÉfÃ^Í&u>ªÉ0¹æïVúQfþ¨�¥‡eN 2‡CäTÌCω3³\GƒQç@ÆÝz±zÖƒ<ÄRÇ-°cÒÙA†4¡¬P©ù ·UÜÇJÍp§ŠL•/õ1d+͵¢0çg‘GŒ‚ËrÆú¶E-�J×¼Fl­ÐþVäRU$¤³Âá±R­“æaœ¡ïRf2©gz)Ñ(5ÇÝ!/•ÐUÓÄvÜCk35AS *�K$«â}qÄéì>†Û$® {³Œ]^æçH…;õ»ô/~ñ‹_VÅLñ’O‹ð5¹>l¨nžaDaaψ¦�G`ãË-w‘¦e8®ÀÑ×Èü±ó ªJW@íœU±¾ß‚¦v¯y1o�ÊÝp²]JBF1ÜFã¶âB«ar5¥âêüÄ�4±Í�‘y’i|÷Å46¶¥!sxïýM†~ÌÊjŸ(Né÷GÔj.½~‚$ܾ³M’äèºÆ»77¸{¿SŒ¿†1~˜â8×Þ]¡1òc®ßX¥Ó YÝṆ!¦Äº*Rw'Gõ0*kÝý®Á^÷@áº&Ð~›…� ÂΘTÝ;]×ÙŠVñŒYž²”½I~H"UabsÖzœíx�ç4îx×º×ØsRMamÛž’œÙ+¡îwÔ" 0ôpŒ�ØW®ß_aøõ¯‚”$÷î’Ö¯ÿÞÉBówùþ=>ýü§Xº½„ašœ=wÏs‡c2†ËÒÒyê™gY[Yfmm ]ט™�¡Ûí¢k:�F“ûËËT«uÇâêÕWÊëœçy1 iµŠ�IµJ£QgvvŽjµB{fŽÕ•e.?úDÁÈwŠ�z‘¤Ó\/ÿÎPlÜ0BJ¯Z'f9ò‡½‡)ðÇã@Õ}PÊɸHV݃BrfGW1“aj’d:Y.°ÝFy�¥”ôº›$IZÀmØ!ôíÕ SyÈ÷ý©EALȦ¼ÁÅá-*µ™¿‰ÐL„nòÞð$÷&à*>hqû[¿õ[_t]×T0—0 Ké§£¹ìŽ£ü´”…y�aèEÎó1l,ÿEG4MKñÜÏX! ‰œçPN‘êÚ)MÒÃLpT×-�CN{= ]”yxwoo3žýãØL¡º±±•Òl|î‡XÆN!Ùí'|ç…÷8u¢�Ì3‚(ãÞý÷VzŒF1›ÛC‚0¢Ùðˆ�ï'¬n Æ]5ÉýÕ7oo'êô4]ãÞý>–e2E,ÝÝ$N$Û݈Šg!�S�ÕÝÒ�ê´B�ÊÃÊâ K[•v¦y;f>Jú0Š¢ýÝ÷ÆÇ«k­r¸¦i˜¦É è¡é¦aq3z�˜£¹¿œÐ.¢ “4O8V_,²�Rצ› 0=Q�ìÚîE€T×Q�»Â{çI¿,23©f W÷ѵœ\R@­,‹0 Ðóá�]Û0ó¨T u8êbééX!-s™7ÎeQ\�êwï9ÃDÇ«¶ÆÒ_Cl=ÀÖÂ0 Ë5¼êX+<ɧŠ\ÕÉM'š J•¡Ð§-&jºé"4³ìÚ§q¿” Ër ?1Km\UšÓBæ oO³©]zྈ4Må~/UÕÆôab³ØívwþAÓ) [Í$"1ʼnh2êwȤ�çÖ÷·©1ºÂÞì…VÈn·»/FLé—ª‘rÇ{3{Ešf¼v-c¦¡qù‚W nëºÎK¯Üåäñ&Ë«ÛÁ WobO=y’¯ÞÆuM>ôø ^zù6�^Zd0 ¹{¿ÃL»Â¹33|û…›|ìÙ³Ü_îóñ�œ/w­ûYïu '�s€=Y䯬fUªÛí>Ô3¤l,£(b%ºÅ¢}Ž[ÉèÊõ#oãrÙü(BÀv¼Î™Ö#Å®°ßÅ>qfg{OÔgïÆ�)–oÇJˆ)6³Úͧáviÿ ƒ˜Íö?â¿ð=Œ¹9fþñ—0/>1&¦äQÓ€4“HY¸ºVŒØ‘dÂCh:–eŒ¶±�œ8‘X¦Vüûx�˜J�(´Zí�ø8f†e †¡ VoI9bë Qœb™EÁú�ï½ÆéÓg9}öBˆÂtA{ÐB2¶lc‡PV`Ù j�ÙrŒ޶pŒÂ±0Ë5†qqmŘ™kéŽüØÎ7׎,"‡8FJ˜š¤cd…å.¿K2Ñ„$ÏÁ߯ɘĆªî�ÂëMÆ2×X\cÆIA¦dþfã Ý^Ÿoçÿ)¡xPç÷ƒbnó<÷…n¯×+»y ‡¦ô‘ww§þÿˆ½óð¸[+Ç…í8“ÇXb“D´ñûë uìÊÜT×X…ÂÁ:޳Ÿ�NgJîg2T�¯6»a–‘Ã"Í$ß|!æ±GL.œ©”ßàõ·Ö˜iWȲ„ãóUtÝà[ß»I­æ�ĤÝòð<‹µõ>Išóø•“¼{c…8NyüÊ"W_[¢VuyäÂo¼uŸ+�,Є¬o p]‹³§Ú|÷¥›|èñĉä©'O•àápx$"îdβ ˲ØÜÜÜ…E<øøÉk°½½}äMPN3…¬ïÒ°fHeÂÍìUŽ µ‹à¤v™YëÛÁÇ*'Jòfl“�»{…â´LÚªA@Ç´ÛmNþ  «šF !è÷:¸VŠ&v>/LudckŹø‰E½Y¨ û›%¡w¯ˆS ӛŲ,|„H»tg¥,šZ™¨à�í…ãpˆ=ÆÄæjTk�BŸ¸‰=a³[ØñêhfµÄUËtXæRI¦�ñ¸æÄZ§™V¡^/òU¿ßÃ�£�)\¦—Å­©gSy\}~”Ú¼š¦øC4|†Ø*IfŽã”ÐF(6.q!D^˜ùŒ7Wj³¤œQ‹k$Ë<½»‰–“s�78Û¿N«ê�G}d`T�³Ú‰ùžøERähªÃ¹yÁ÷}úý>ŽãLÙ†îýPr¿“àÇEÙõs‚¤x8Ñ{—@&‹6Œ5n.÷YÝg‚áøgȳâ?¡!dކDÏÖ‘zo“%1in²ÑñÈ – $ãz-L¡;×n·[zI¹\(|Öj/å~'á~'¡ëK’lg¬ÑétÊn£5îpM2ß÷‹$‰9{<¡U ¥t”iÙô>›Û=6·†H)Jk]רVl„€¹�wo¬qöôL�ç’jçl”¤=Ï-Ô'î.o“�‰VBX–�mØ–Aœìh!NB 5Šêv»åXaw‘pØñ“‹´¢„5²,c0`š&ç�‹}fùxC3хɉڹñ ¡ k×L¨Xñ0ò³{½^é9®Š(u öêºL†ê6I)‰ÃáT²®Eûþ2úì,­¿÷÷‘'ÏîÀ/¢!YžÑFZŽ©gXzŽFŠ©¥äRà¸Å+ŽüBh-}ˆÐ-ÒÁ}ŒÚ â°Ïé�îYØþuBu•&ÇÔªª&j�FãÀwPA”ÿ©vzœJ ¬29x<ˆDj}?a8 ÅjÊ% ˆ;]\!st ;Èx�4I ó*ƒQH%dc¹5uÃT�fÅÒëõJÙ.e£d' ­VIœkDYñ}ÕÌg{{»\]×¥Ýn—ÅÎ~!¥$ }»�QuŠuL­eŽãqy›µõ.ïßÞ§œ0L¨zº!0M� çf¹úêNoc›:ž£—Åi£î\Cc¶] +k=,«ØZ¦N«éR­8´š‚pg*¨ V›‡t®4ƒ9B!ʵN­ê�?j(!ÀÉæ9lÃa#½KÆÑÔe@PÑ…„˜³Pê«ÆaÛÈpÍ„ª•2€“¡´hÕH¹µ©Âv/ÙÒÉP�#u ‘ò_¾?˯Þjó¿o´ùÍ¥6ÿÙÍ:Ž9žDeË©�>ÄÔö'*K ×¼káh¸'ì@ˆBåÅÖŠ\–çÕz›Lk&:IfàzE ƒáˆb­J1òƒ~Ã0p«³Y•0ÑK‚–©gxfB>Χű9¦^˜¿cÇÁ ¹^'H r)Ê�…¤èÄ#{ŠdW|~Hnrh•¦Ûb”ب>Ù^ÏeÇÅ5’EÝ8év»å³§”ŠdĶí=Í”z¬³0Ú¦iç�%dÁFeØáò³{¶ú—¾ô¥/ï5(nʃ#Õ…ÉD‚6¡0û�×7±-�ï¼½ÉÅ ¾ýæ&×—‡Æ�É)ÒyÄÀÏJ,¢’XSp„òšî!c¨ðça–ãUMÓ°m»´�úHDÐŽ¾ƒUi‘önaTBç½þ·�b¿¡ÿ…%|éK_úâ`00÷*:ö÷ïõêºÎÿúµ[Ä™dµ“f‚¿|k³èü¤’¥�€Å–Í×^Ýàì¼G†Áÿý­%~Ì·:¬u"fîX+W£䘺`­›°¼¾ÊÙæ}6‚6I,ÉpØöun®…ÌT ¾þƹŸñ„9/ßìâØ&¦Qäå˲)&¾zö›hLò‚QÇ”üör•Ï´ÁÑ`Ö‚ol þÞ‚2#»‹D~SÛ¿€OƆ ŠÃaiá�ð5î/œÊ’±SY ÍpJ·Ð,îï+ã57Hvàaœ#Çv¸<,c` xgœÙ¸•‚“0ôÓ²q¼a"ˆ“¯]&5’\‘‡w h¦–âY.¨Vkå}PÍÏòÚŒÝä¦ÏgÚ¦)¦iNMωv2bB¬¸Ã\ú†Û"éݨ�Bæ¯ûO³ªxßëmL:@ìSvv�®ë–¢»a EŽŽIœælõcü¨ð¥¾~¿Ï§›#N%Ë[ß»Þåí»Î-Ô 6œ„š#° �(ɸ»9â•›Ûü{?ÅÕ÷¶ÈsÉÉY›[«Û¼µ6Ë»÷Gt1?öôiò,c®aÑ Õ€•íB¸ÿK=Z.Ž©ó/¿y‡vÕâÊ©:¯½ßa±åpj¾ÁG.cu{D帖à¼ÛK6—=ÞX°Ú ùö[`B±W(Ü•’HÛ­I·ß³£!yž—V¦ƒÁ d­–r)GTæPð–eq¢q†¹x�M•5n—Þç“áÑ aÎÐ ·˜«vA óŒj}†À÷I’–‘¡©„`Å%†(ë ª‘Ëîî·’Þñ}—‘‡˜êÚFAרgýû?K,=¬ñµÊâ!BËx÷úuNŸ>Ã×þôßRo4ØØXÇs=lÇ%ÏsZ­6wîÜæìÙó¬­®”�‘÷Þ½A’¦üþü Y–Òž™eksƒ¿óü-Þ½~�Á`H£Ñdue…f«Åçúgx÷ÚuîÞ]âôéÓܹs—JÅcyy™f³AµZgkkË49ñ"W_z‰+�>Æòý{üÛ¯Ó´¸w÷çÏŸçÖ­÷yâ‰GùèÓ—ˆe…ú¸£Ç¦]%Kcz½^ÑAn4˱ÔîÅ=Ë5F±UŽÈtQ°lÕM×ušÍæ£Y%ktP¨û5‰EÜ2OiDßŪÖÉ‚-„n£YUÖ;×ôŸÚ—ñû× Õ];(TaEQ‰Å‡øY[x€N’ýÙ8ÍAÀf?âã—gIÓŒ·ïöÉrXéMö]æ›.Ÿ¼Òâw¿Ù£â¼~»KgsrÆc¥ôF ®­Ótém„6b³ñücǘ­ ^½ÕçÂbÛÔxï~Ÿ¹ºÅ‹×·ÙÆ´ª6/ß’æ’g.´xs©G–K~êãg™³wŒa,>ýè Kë>Ï\hòûß]æK=ü(óu‚,Ïù¡Ç =juÿLÓÞ7 IDAT,å�öÊEÅDLŸº~žçQ­jôz=‚ àÑË'ˆã˜ÑhÄÂ1ÕyÒÂbu}Àù³3äyÆÂ± Ræ,ιknÆ+sY’$œ:Q¨�¨ȳ”3'›H)9u¢]vªÕ¤@iÔî†ÎíJ~Kaf•¾Zƒöƒ*¨é†2{Pï�ú{5�°±�„SMƒåp½`ÃÓuxo‚z‘æË©ŽÞÇûwms)ȵJ™ç]×Å¥yaHsPF ñx»4›ÅGa)³Xlôµ=»Ù–!1 ˆü˜\¸TªM’$aèð¬x¼Y(šQª£YÕrZ-…Žii„Á� ÐÇÄg¯$Xïn6‰I$$¶‘–°:Mƒh<ó<�F£ñÀ³s�Œ¡zŽÔZ œÈö‚¾š9Ô¢«˜ÕYÒá2ºÝDèw;wôOx�5åqÔIôz½¢¥Ül£éEËÙ24>|¾ÉlÝæµ÷;|ôò,×ïxåf—vÍæ©ó ÎóJ6\šåôøä£sÌÔ-;Õ Y1Ùì‡\9Yãñ3 DºÅ¹cÃ à™“#Î/6éùÑáÉ3 þäê �¯"sIßO˜oÚœŸwˆÓœQ˜2S·‰Óœù¦Ã'�cy«�9[^ï‘ë;XÊ0ëbÎÎØ( ²ÃAÃ4qª³Dy•0ÙY,=£jžâ•ñŸ…¬Ú42ìm–»ÒJ¥2{Q]A(^p]ì½óBPñ\¯ÆÜ±Â¯>ðG˜Z‚¡kœ;ž,ÏhµÛt:Ú33<ùá§xö¹�`÷îÞáñ'žÄ÷G<ñ¡S­»ðÅ'˜_XÄõÎ_¸ÀææŸøø³ÈùÉO1 ž}îî,-qüÄI677Ê�ÆÝ»wxâCÆrΜ=G·Û'HM¼J±pþO¢çt]+É1Ýn—<Ïi6›å˜rw$™Îh<"Sc¶ÉÎùî8 †Q7 ÊiB½^/GµyžSÉÞÃ6z�gda£zœ`Ôãuù“¤Â>ä>X(!ûÃÞõ3ƒÁß÷©ÕjÌTçÉÆÏY½bòÄÙ&[ƒ˜k÷ú,¶\^zw‹�AÌbËåâb…ŠcR­T@ÀÍ•>ùÖŸ}z�Š­óñË3¤YÎíõŸyrS×8ÝJ¸°è°ÙóùÑ +ÍF¢cé‚fÕâ…ëÛÅIy[î¬EÁ ñ£†ŸhWfðýZÖgØß*¦�õYÂ|·VFhŸ¹2àíeƒ§Î|ûš`˜Ø^¹¿ì§R0)ÿµß˜jr|;YXìÅÈU8²ÝŒàÝç39îŸÔ<ª2‡J®aâX.s•EjÙ,"Õ Ä€câ,u½ÍV¼ÊBõäNáˆ?–³û{ÇŘܰ\‚(³Xe9ÚQ]©WË÷#z¸fB–„aŠ®ScRõÝTbMs�Dyoïu¢Šç#Ã(èN.B`;æèõ‡\¼pž“Çi·gh´ÚÔju¹t‰~w›gŸý0Ë÷—Y8~œ“§Ï¡ë&�z�ŠçGþÐÔ«.¦©aÛNœ8Á`8¢Z­2;7Ã|Ëf~a�ëï¾Çñ'ÑuÇv�†$iÆ“O\¦Ýž¡×íñÈ#ç‡\zä"W®\¦Ýjrùò%:�.—®<Ê`0äì¹sÔêmì1‹™¤[Â>Ò,'J(¯MEå$á 1ia�»3’�ÔžŒ4Mêš|†�ÂË]Ï{Ø£o ;Íñì$2OùÁðqVô§8LƒàƒÂ~í×~í‹ÕjÕè|'cò<ŒÛœºO•J]×Ù­¡KÇðˆeÀl}ž,Ëèt¶qŒ kì—­"L4¤æázÌ# ‡ØZ\âžÂÔ R?Vàªú]mZW7N5¤îá¸Õ)e E^�ÉÕóz˜J�ÊÃûÁ¶�½Þþ†5»ó°êÇq|d¥¥�ô°Ïßî<Üu¥=fœ¶¢5N6Ï–kY0ÅÈ—@’j¤f/yž}L-Þ‘ìʹÑÀóŠ5-ö7§ÔgÒ\ç^VA•Ư‚&äyŽ?"³{ GƒÂö9–^©V�6 uõ¤ÅO¼|ŸyÛâ�ÞM€ŸåüƒÓÇøò9dNŽF˜ òÜ ×*ŽM…Èt„cNà âLÇpÚ–ÅZ·Çœ9ã�a[¥ëâ`4"ˆcjv†£e¨9sAÕèF)3N!¡:H%†0È¥…;6RP†Á`o,Išæ½°è�*¥šèJ)K‰µÃž�Ýêå½× ‚ó0¡êD!#äÊ¿Àôj$ý»hvÍt¹Þ™áMýg� 3Ô N‚ÍÆ«Z�ÛÑ[ÌE§hxm�ÆŠD`ÙΘ`“à‡ šPú–EÕÞëõ@·hÖŠÝ�QyäÒ—@žï^vf]× 2‹¦Çã�ª&ÈÓ”\ö¤y&‘B�J�Q‡Ó¸&õÿ“þÆ GyT6+Pzœ«äjÛ…ÅÜ¿;wBøùãð«‡ÿ……ò<¯Ä´¥“; Ø-aµ|¯ã+[u�Ó4-‹äþƒz�•'´r–y˜"u·ÇôQ­<'Ç´†a0S9VîÛÕ¹òwCÁ �3×LJ…Çû{0vü½ý¨H6ºU/ñJš|P×2r$C‚aˆÐ ©—I›`! bKµÖ“(FÈt„¡I„±CÐ̳4Hr�85°�tJÅ!‘ÕñÈ& àx “…ˆ8LJ¯q)+~k\¬G™ŽW- È0â™; ÜÔ%†VtA"éròÔéb£ëƒcD{³€¡Ü(T¬”8Ç;U4“alR³’\›­©®›Ò1M’¤|þjµZùüíŽý 8ª{”ÒV*&›¤ž±lû;˜z€Ì4dêc4ÎÑïwù�ö G*lÿº¡`< Ûßl6ĵOFFÊ�á,çhT[HYÈ^Yz^Ú¡f²Ø@Û–A'Dq2Õ�Jâ�n'Æq]�N`i�!¸ƒ›Þ£/ObµÀ †®Suu¢8!IS$`é¢Ïš,Ô‰§ªÃá46S½Ã¡?$3Mêµb’¡¬°�‰ÏÜ/¤”dŒEUø¯.@˜Ã?< ÿõ»_ÿÉ\2e¥zDLêîãUá0�9Qp˲¨�2ª³šeÙ�}•‡£(*Mdàhk€ŠÉkø0Ïßä¹ëºN­R§. <ñBõdÙM/Ö�dœ‡¦Ô2r,¢aD®W𪭓 ±´„XÚÔœ"¯~g—úŒ¡I Í'EDza$ Š\µ‘)òp�4õŠA2*´h3·¦`T#,­øy?1ÑÇxÓ«ý˜µ(áOÏó£sM~h¶Á­lñË›ü“ §‘yÊ¿xýúØéñT£ÆœWã‘csØV›µ^—ª•S5%Q& s ‘åèaÄ¿zãMþÑG/°î§Ìº&–™Ò;¤±ÍWo­±îô‚€ÿâ¹ËxzñŽR¸3ø7×oñ�?õ$[aÈ¿~÷�͵9Uw‰Ò”ÜòX¬×Eý¨h(8z^B¼ xoT#@­[ʺø ¤š‚»Cmr„SEò^ÜŠý"Ëòõƒë¸da„†n×Ù茸¦}îÈœ‡©ÙÞäË¡Àê‡wÒ4\Qc {˜¾C†#ÖrˆBŸ8ÎØeÜ[êðcOy,oøî;›,¶]?Y,´…Å¡ÆÿûWwø¹OÇtª¾¥Ú¤Lâ²&YˆG9^á!•t�ò ?ê÷WÝË,ËJ²ÃIŽ”t¦ðàj\¢F†êø\ ¢Ô@˜É¸ëX<Ž™‘åC†ýÓ®R©Ï––¯4Ä1öî 9,){Œú>†]Åq¼È,ÿ|£A?©ƒ(0i?¿(™O}ÒLÇò\+d•b‹Š£k9qªa¹…[ŽïûXûÈÓ ù,ð Fè•Z±HŒÂ!–í–ð=¡í*�…�Xº$wÀþµz“8Žð£¶žLáÐ&;àInQ©*_ô.–mã8s FÒtouŽ4MKܤ*r'Ÿ¿I�x5vÜ“æ3–e•0Uä¶@ !pÅ2zÍmwÞÇlœ! ¼žü¾>³çq±×FUmtz a¢ �QÞGô‹}£Ñ(I{Bþâ�M>ÿ‰‚àôòõuÒ,g¶fr¼m—]ï^�ñâ{+|ârküþ×@ž Š®‘Û³S¸NUpiš†ëØ8ÒæÆÊ€ùz±0ÍÂð@ˆ Ë3ÂÓ¬Èâ°ß}>iš–òõz}Š€u”PùRm”Á⦿túáòð$áer³uTeÅsPϫʋG=~ò=h4å†ý¨ß¿Ü°��¶H�|þ<Ï{(ÒÙd£DMwã�%¤BÈ©M¼mædù Œ •Z»04à Â0D'Ú·¼±� )D£€P¯–ª5jJ¬Ö¨U£Ê_t]¤Ì�v¢ó£õ„0ŠIuCËI2�%�—çEÍp¹êñOß»Ïß=sŒ?XÞ$½8#HR¾ð‘K\ßñÞö�———ùÊ»7ùÔé“\]Ù j™|öü)^Y^gÕ�°Mƒ�ŸG’ßyë.RBÓ±¸¾Ùåà 3|êÄ1‚8¦jlŽ$wF’¥­m ®ÅÊ`¬Ù¼1à¥û«¼·ÕcÎsIóœîÝÁ³ž=>ÏKwï3W­DÿÑ•ÓÄ{à�Õó¥¤Ó²,c8bÆT³M=JÊo÷ïPÒ¡ªIgÛöÔ4à0â¤aÑ „X<²p ³qŽpÔá ùÓ$ÚÑeðöDäªETUï“ÝÌÝ_ìí{#ŽkóõWVyö\„eh¼vkCÓùѧ�óG/ÜçÉsmÒLò¯¿{‹4͈ÉÅ…*üýULCãsOC²\rk#âÎf�ŠSìºâÄBÊÿ7 Û.Ûƒ˜cM›¾ŸâG)‹m—¯½ºÆ©Ïž§áüśۤ™ä±Ó Þy¯ƒiô¹|²Æõ»=>zy–›«#îmŒXl;¬uŠ.Â3Z|ïÚ†.øÜSsxN‘\•›ÕQwê%n6ÿóè¢{¸:G²$V×R@}EÎ:è{L*3t»ÝB“q܉?LaòóU7ºÝn—®8‡�©ÔE½^¯ì^(w³£vbww/vaÙm+©°jêØ4×E6¦ž•„1(4+VNšvF6õf»Üð ¹BÝù,pŒ„,éÒ�\¯>UŒ­Dðõ-¸T¼Ú‡O6ák9?_+:¨É„ö«¦åhZŽ2áà�wÕqàŒ£Àëþ @3+Tëíòù ƒ•ýì$…Ä1SâÔ(Áþ®ëR©&Q<Â5S„`Jè;Ju,§^v=cH–ŒE.Ž[Åæ`¸�Ê-Jnor“­ºžç†!–e•‰UM &]q¶··KÙ9Ã0¦Š¼=ÊjùêŸbzÍ‚‘ëÍ’÷‡‹¬ûKÍüMÆî"Aå€=¥Î4�WßЬ/0�¡ïã9×î®pf¡F³bóƒ[[ ‚”Q˜òûß¹ÅÊ–Ïó�ÏáÇ9_¹ºÆ\Ãæ¹ u²\Ä9/¿ßg½¿Í=¹È·~°Êé¹Y–7$i�ó‹UÖ:!à åù'æøî;äNͺ|õê*_øüdžñÍ·¶‰ÓœÏ<9Ç_¾ÝCˆ>½4ÃZ7 ê˜ä^¹±É¹ù*+Û>RÂÇ.ÏðÝwÖix&Ÿ~´ iµÛír=:j‘«° `ñÏ–ÀåP5vŠò£ÜUd>ŒÂŒ ³QšØj³veõùJ¡ÕjáºnÉ÷8¬@Pöñа¦lw?h‘ªò°R\8j£Di¤+²í$žWJÁ(¶0´Ç(x 0ÖUÕRrÙ§ß ¨T%Ù/ð‡xæaðÂ,—=†½¦SÃuwºŒRJþå²àv ó­m�çšpm�_I¨ A– ²|§p3´œgkÇmƒïwlÆ ç*6¿{oƒŸž¯#ó)Aׯ–ߦŽmè|öâiþ¯×®!Hù[Žsñ;o¾Ëß~ü ll²ÒâEòÅ{k<6×&•9uÇâÇÏÍ%)º€ùj… ޹Óé��$eô¢Éq}³ÃO_:ÉŸÞÔ Ò-29Ùjñøü,//Ý¥åèüÄÙþÏ7oâÕfà€†¥jކ�ëºåDm²Qî‰ Ó¬BMZÔïé÷ûe£Jéä{žÇh4Úw£Vs3äê70*m’þFõ82�x3xŠmãÂ�÷}wì;k›ì¤�F#*•Êžlºª£óç¯m"EΛK]V;!OŸkqnÞã�ÛÛ\:QçìB�Q˜�¦Ÿ¼ÒÂ4³MÃÐ1 ­0*(?· A,où¤YÎÇ.5éŽ~æSç)/¿·�”pwÃDZt¢$çÔ¬ÇÅÅ­Š�iT‹,‡Dê4«6?òáEnÜï³Ñ‹ •†a„eÎÍ×hT,>÷ô<뽈Þ(¡7J„Y •8ŒÑ½WH)ù®H~çCðÏ ø?Oùž*’�RVxuµj6›eáºßç*•‚0 ÔC*#L~¶JpÝn·Ä:î÷ÝU©Š{%Þßï÷Ënö^øœÃÎ]1¢;÷ÉPFAÐét0ò€Áê }°R4t‰¦ëS]*Ë+X¬qzøxZ×$ÊR à˜ÿñqøü1¸|À&Ô�ë’LÇq«åbQ­5ˆe• Ö9ìR(ño‘õËâ±€KhøÉÞ².d¡aG¸f‚®*ꨌ™ÍQ¢•ƒb)‰t1­±Uq4ÀÐ$¶‘ãI°Eî°²÷ 5"K’˲J v¯×+;›››lnn–˜áðAœ²*°···§l<�F靯æžç!:‰akäñ)st»Áv?äí¿!Ù¯‡ ußw³ò§ž-)ÙD\¿?àõ›}V»!o¼ßå§?vœ;kÞ_îò™§©W,–Ö‡œh™ÿ|“—^‹yäx•®m‘IɳÛ ÂbGSutÖ» ×îûºÆ0L¨W,6{¹ÔÐE†kéÜÛ ¨W,’,ÇÒ2/Xìßzc™L =Ó¦áiü̽YŒeYvž÷í}æ;Æ”9TfÍU]Õ]ÕÅ.³›lNâd 2L–Ÿ½0 ûA-´ Q6`éÅÌgÖ!0B–!B4§n²§ê)«²2+ÇȈŒéFÄ�Î|ÎÞ~8wŸ8™ÝmkQyãî{ϰÏÚkÿë_ÿò]‹�QÊòœÇ�­ªh%L]_Vé¶¾O’)vF ß»¾Årßef|xo„”‚ÀµYèÚìŒR¾{sÄÅÅó—KKžë°¾Ÿa[‚hgÄsËi­ï{–b• h¹‚.£r %xÚ£�­Ú9¥iz¬hñI÷ਸ਼áqt3 E¢ù·&eâQµÆšU“§¥IM»Aü�"§G©ÍTóYÌœ»‘Ý:í4¯�A¹Žòy›çž—18ió´°huº³±t>«E§;WóÀަè�Z)|Ú3´Á¤J Z€Í�^ëÂ{#è+[³®dZuáU4ÙE#«ªÞ EM°t‚c?¬ÄpèX´K{¶1˜NF­ZLâ1®LgT†³×*±Lz®´Í!ZŒ£)¾ŒÑüVuÿãhŠgeT-ƒ¨JG„ãË­›Ñk=Î :e&7÷3ÏóšÎpÒócæF†DQD§Ó©º¢KÆ IDATκâÔãÂëˆìSð»”Ó8ýgˆÃ1ïª_¥�g[Äÿ¿°f6å(*Ôôí»C|ÎõæÈrÅÝ투P8¶Äµ-¸’›ã’$˹°Ø"ËK./wy÷Ö.o<Û«³#Wï�xõÊZ·kNîã´ó…³sR ÑÌÙæ|4…W&Ý–š³Ù7]º ø(e­)•uTYÁøáÇ­«1㛵%sss§RµÖumÖŠæµ;ê‡ %‰r‡–›Õ¾LiV«>Ÿh²‹.ív‡² £ ®•«"`,-­v·>FS4¥ñ¸ï,ÃÚ)lÓ7/?¿¼ÈïÛ¿ùþM^lyü/Ÿ¹Ì¥N‹8 ÑIÄo|þ9†iÁœ·‚@c Áo|þYl)�¾µ¾Ï/¾ð,m×å×?÷£(æÂ\ŸÏ.�ÃÖ)‘JXðm¾°ÜUeåù¢|)X¬¼ö`áJÉk‹óx¾�Î3–Ÿù@£´O»3�B“LG¼øôYYr}0âÇ/.áÛ y’’âÓnwI¿lòq�Þ½Éεo=­pLJYg=Ï«›�u:�Z%G�˜¼ƒ%Æ lT6Áé?Ãd¼Ïòï>Röë8ÛÛÛú¬©cM¢pš¦ì”Û;g®| Ç‚aTbIÁ|×ÃóÛH¡ÙÝ¢´&ÊàüRϬm Ñ–ç*Ñû´ßsÙ§t›8œ`[pk+á“ûcZ¾Í/¼¾Ì8�…ŽÍ4ŒE+}—iRâØׂ¬ÂãÒR›I˜°9±ÔsI I”æôZÕŽÁµ4q.ˆÓœ…®Ã(V(-™kIò¢@¢)•®ø½³JVƒ žTì%„`a¡j¨`4›öƒôJ‡¶I¹6•ºÝnÝ?ý¤ÔÙ£*r�«qjÍñÍ&QÕÔ­5ûûû§"Ëáõ<N»Zkúý>®ë{ ´Ö5‡Òäï À¬T‚Böètº‡*gµ†¸p�v嘓$¡Ì¦xvùP�çAg©.hˆã¸Be‹_¿Ö"WàI(4üγŠÿ(š'¦%p’[ÝsضÍt2ÆeŒ•£Íu@Ъ8ÙI<Å·ÒcƒíR „·ˆçUÏ«]î’’R´hÍ"²dBÛ9_jQ£(¥’X³îmé¬Á(<ÇÁou(ËUæ´fŽ2žìê‹~øú &©‹FÔòHgQ1âõFyôÂ>­rüáïÖ‡Š|*I¢bçpÛ]òÉV°Þ>ÏMû—y”ì×qö¤j ÛÛÛ‘âLÑ´ñÃ�¼Tl©5á± –±¤àÚZȳ+Ží°8ßeoœ UÂýAEíxöB­JÞ»¹Ã¥¥+ó-J q¦Hrf4°£ñˆ^`óÎí1w·B^¾ÔáÊr‡ÝiÉó+«ÛÒ\UÚ¶Û1ç= 6†9ÂöxõÊŸ®Iâ„ËK÷)BÀ¹¾�*�'YÛ­Š/,Ü܈è·]º¾JUо}p;�Î#æùùùªr4z¨#c«Õªuo›õgµæ=0œñE¦Šü4¥šæ|4RL'ùaó=ù5ã�úa­u}Î�Rª1Y‹'õÃBˆ:H9.À7™ƒøwí UÂ\;ÇҴ݃cN ‡ »„eYL'#\*”0É-„])ÜdYF–Lñí ëP 5$ªE¯¿Pïþ~¥”ñÏ'‹üñ@Ò²`?‡XÁ¿ùlD+?(®hjÕñdªM»[3ÿêþ³�7:.%>~«;»SW]Ⱥj‚½Ôai~ KJ¦ã]lbÒÒÃoUÙ¤4㊤’ƒ¤Ê.6éÆ糌£RpØj܇~k„,ö‘r$q®è¹U+m˜iˆgžç×ê�šÿ¦»cSVÎ4Q�Ç�5ÌǨܨdkï�pÚóä£{ØÝKyÂw¦?3“`||yžë'�܆,> 'Lò}Ú�~ëZkŽ}дÖu‡'Ãå„*˜êõzõû&“IÕ¹G FQÁ|Û©å`ÌM<Å ‚ N͘É<;]�3^£V³ßEÕG]Ï´ÃÌ{„¥ªÉ �Óœ«Ù­ŽçÑÝŒÙ�k­‚V-[c‚ÌÇA3M gš ˜ Y“xT°|’slþ½ÓéÔ­òŽodg¤”µžîãœCSºìq%wšºA/LºÅdN¼Zã¸Î¡àεrl©‰ —vw±–çòD•0Vªª Íñ;¸®G…ˆ2ij+”IiAªÚôú•ü�¹J)„”ä–‡q}AàÓu$º&ÛÛ²¨ûƒçtgÕ½ñt€o7ùjÂŒeœ|JžNñ¬¢R5�–-:½ùjh»I=>Êl°»øA@’Äè|Šïæ"ô*鲬´h99R(âÂÆ²Ûtf‹Ôd€ŸÔ‚2-ìºR×,pMÙ¤“¬£E³‘:«ÌÜ¡ó™=¿ŽcÃð´�uT6•#ƒ%Ö÷oÉßxbu„' n“$‰Â0 §êÑgx+ªŠRº,zŸIc7ýðQÿažA#Õì¿¿¿O©÷)qVòÂ…B«Cò�Çm’=Ï«˜C¦ ªÂÞêßU›çÚß"ª6Hb†ÖèrösØ?ÿcøÔM{T€i|± r�ÚŸ?îZhø„FÄP_ �ìQã�æIõ†‡~ôÍx#Ei|`­>t†c? (9ë¹Qéëš@è¤ ÍÑkG!ž•âXªâ½Zsu÷ª4àÙײ* ¶�Nß÷I’˜2ñí²®ˆs› ³øÈY>·³ªm²ãØt‹Ïu4E–β�%m·=’ÂÆoWŸ³³³Åv4bÉÑtÇ±É AN°Á•)N£�®Re¤Ó²î qN I¦Û´Z�z=ª‚õƒ™zÖ1ÓØ4«”@'§(%A»jF£”b<Ü¥íž I‡™;S ªì,ò_U·ÉŸ+„`iié×ö¬ÖºlK ·þ^PRƃª#¤ÓæÓ½.Øÿéûb1 ´©(|]?Ë–tÚ݇vŸÆ±�ô ™“3NÇäyÎÂÂÂ!$Îp.�ýîcP¼fkÎ$IŽm"pȹ f¿›VS‹äJ›ê ” rt{›š‚q×ßmйޢ¶®ë‚αȪÀDØu;_à�|ÑãÜÔA•&xœêâ¦slîÞ´®ôæå$ Ñf}âSñI$wšã› ƒI�˜ [Îø£Yiz�M+Á$·�N‹ ¨îaž…x~w¦ ™’G»õ.ú¨U¤UgI"uŒÒ’Vw©&ÚŸð7WÃOŠã�‡(-h÷–f›Ã!þ‘ ûà@”ÛØn§vN*¯Š¾òÒÂi-Î�ì)ŽbA’Â"-\¿"ýÇQ„Ô!î T¥E½x”J ¬>­v§–)Šã]D3ÉÛ‹fpÛ¼‡G7YM$Îpp›Å˜Ap úð¤–D#TÑsw°âOèöÛŒÇC¾Qþ„âÜî“·Ãá0j·ÛÁq�Ú£¬ù -Xjy'ùá£Ï T¨§ùŒÓ6‹æþu:�‡‚Ì………:%y,‚)ä�ïTÁ¬**ß«õìu9Dõoõ0Øpœ6ðââb`]?,˱¨¤ �,».z�ŠörRã›Óî�I¹š´íã¢YÇ�‡áÐJÂI&¥¬3†O²4)lÒ®uÒÃpŠTzšêN½.õ‹“hLFÆÓ ùqQ¤S\« £{ ¼2ÞÁ•B.׺ò¥XU�›e)E:­õ|£Â§×¯TWÆ£]ºntÈ¿–ZeN…H-Ò4¡È¦øvq|ˆYÐkÞ$·ðgÙŠ֕�ÆÓ‡$%ÍØiæ>Ôõ Nœš]N�:þRJ=vp{p,šh¼IàFt­Ûˆä~w‘Ýý1_çï‘‹ã¥Ïb5q8ŽÇsšíñŸg…v»JÄq\7l8.%tôäšâÏBÈI™¿�´“Æ7,ÓQˤ Nå´ Yý˜Òa & ETï±ì‡{E0rBˆz“pôx oÅa‚%Ò¼�Õ®«Ùଷp�2ÿãp©šv”.077wjˆ&:oø˜OŠÄ6ïá“?›ôˆRŠÝݪ­càä¸VY�ÍPG€Ž—6x]+�ö!y“Éh—À>Ý¡šïn>;eYÐjUhÖ£dÇ*q­‚B{xÁ�²‚á“ú¾_uŒ§òÊÀ4qppýª{WNRÒëUó"™l8ÅA¡«þè³¥ Ó.¾‘ '8"=1¸HK—Vw¶�‡(Uàù� T-2¾s0^éJkØ!ô¡¶’G7‹æù™N§uÐa®Íâââ™e‘Ž3ƒ¦W 1ƾK >áz¼ÂšõÅ'þ\øÁ›8˜ õ4Nòq6d Ÿ ­s5Êg Ð,Ë:6ÈkZs£h*Ü›‹·A5OBË�g¦ÚŽfÏŽ1Ñ^�O6~XëêwóúLÊ]V OMÙ)×uIÓ´.xišïûXj‚-KJ-È ¥Á²}‚V»öÃfžÕ›ÂUÃo}ÜÚ‚£€OÓ›5ù$3•èY–ÕšÏ?¨>«2Bs|u�<�+®WÍålFmRúÀ?Wc«àP:ÝI7O‘ïK¹R3µ¥Å±™ˆf'ÃSA p26–Û­ÁÃ6àßt2Ä!|hÓ~ø˜ +,¤Û«éEÓêÌÍþbûõùªµTU�®Òç.®ß™Ô†PÆøNq"IJkHéÒíVYËñh× ðü€4‰QEˆk²Ò")l» W²Vê9 p2�ìŽJÚîö“Zžç„Ó žï“ÆCæÚnþ>ßK–=ùxêGMìììÔg|tþ(é©’‚5q� úE\üzr› ö8�Qš»Çã´,Mç´ÓRMáêÇùî‡jœ«8@o›ÎUÚ¨�V‡`žT»®‹ÔÞ1;PMµó’öáO†�û¨¢³&·,˲C‹ÌãL¼&—ÊðO»†GÑù`8É�:ë8dY¨0 ‘*Â;ÒÆð–ŽZVHì`±®î�LÆH?4^Ív½G+{•Rø¾,­æ8“¢â—‡“%‚öáÖ²ÓÉKM±ÜK I¡½ºS�‚ñxD×>ŒÚiªüÑÝ{’K”ðiµ+Îq‡öÃ|Þ¼”w¾F uº‹méØ#hUÅII4Å–é±<4= vóÒj(/p÷÷÷e”Ru›ã³¶=ÎÆãq­æQù<›�IĪûʦ±$¸‚f€c¸ã�jD¡(¹#Þç)ý ®öëEÞ¶íú™8³/„º}öqÒ…§m¼›(]òÇÍÏKIN»F\§Ó)e6¡å>¼9Šs§,šÇn¤ËÌü= ÈÐrL§FI‰‡tÅ!EQ0ïÒvÏ–-*JA¦‚T˲f Ã6�Ó¤h ˜ Í5%/¹:ÜqÒ&Ƶ>‡f§Ëéd„+&3úœS…¨""pT�öš,œ¦âàL,˜ç9ƒÁàÐ0ª Z?ºƒëIfžQ³­Z%ÃzñÀyõVª9ô´˜§Ùi÷ûýS%°Æ èêE\Z¹5wGRÊ3§MZv41 ÝpyŽ;¦&òÙäƒ=Ò´ªÒaæbªY0[ýñ€žPfÎVÚ`9`9h! ¼#Ó²ÖL!R lq²ø~à”¸L‰§;DÑ´–Œ¾áIÎÅ´ºÍ²|Ö;»ÚTL§ÓZ›ñ¬²3æ;&“ q£µ Ûí’Äj¾×p­Ì=oÞÃ'•�ɲ¬.@8«t™i}œç9I¸K8©*‚½öâC²1ǶPÒ¯ûlO'#|?Àm->yy0>=F²¨»²}í¤c?º“ö�’¶=!™îz^Ú�.¸ó¤åñi¤£æÙ ßJQ[Ça’zä3ñîÝ‘æÖý”[k)ƒ¡f0<¸&¾£¬ˆ$�ç%…ì3 ÃIÕý/×^íÒx‚mƒ6E¡‘:# wI“˜V§‡tçÙJrUÑC&³=‹˜Iõ4•*„¨7̱yM•RAð¶yž×"ãEQàº.{aκóâæÓÌù†aÈh4:ÔL夹´ÇôE<ZÏN&“CÅGæõ³X–eìí훚7Å|Çõ”?úœ›ëü(Ù¬Út9£%T…dèrV1c~ËYö¬Lgíì5iƒtÒÂ�Q¼ŒÚ†Ùà³, ›ã¼ŠÆ”cëãáq\´­V‹………š>vìX)+É$¥Ãj\ÇŒÇc<Ï«ˆ³úAs+}ojïÑñf3hê.Lk€‰8ŽIð=‰>*£xÚ¸šƒ=Nöª¦*½E¢¢EZH´®®ó‰E§ˆ ª3�EÂ< mÈ0šù¸c8ŠVÅŸãŽßø c®¥h;!*Ù9ԼȶmZ�y’Ò§P�öö¥i9)y2ª¿W)E¦ìоP};Ó†SÁh*ˆR‹,Ÿùl3>Þ' 'øA‡Rö™fA„hIQʪ`ØéÖ›XÊ�À¶À9¢Nöq=�Vw‰DL H´®T*ÕzÔ™2›ëhbÁ,Ë.À¡Ö¾OjMê�¹Æ[SýC lá�[ó }Áùùù‡¤”�}±É+‡ÆÔ6I’!ÉóœÉdr¦EÉ\ÔÉdÂx<~ȹ„´ßïr®Fæ§( Æã1{{{µFíã;Wº˜¹�HÎnp™Ï¸_€ÖضK^jÂ8#I³CZ�&¥š�ABãY²‡CþꛟpýæÇ pH)Yߨ/w¦-zëî[¿ø½«|ç�Uþì«1Øây>Ýno¶“­Šæª‚ Yÿ~ðš¨Ò¯ûQ\H˜ûjÞW]Ó*XX]ÛŸ}–¬?ã¨6bÝ’õŒEÃá𑺎‡‚ì¸*ˆj9 E²O…øAË_$Ì‚»£ViÉΤ¬âWD¤ÑIÓj÷Á™gš¹d¥|)hZs3e¸ÞÇ=;•vâág¡JO�®ÏÕ ×AÐê.QÈ>qnó¨Ë˜+ßoq{5æ»ï�XÛ„ ³D&úD™ƒmK6w"†ã ¥áæ½k[9 ‹Ñ¶öJ<[ãŠ)ï¼{‹(.ø“¯Ýf’¸|xcŸq$Ø•h Û;CFãˆÝ¡æÏ¿¹FœU¼º8)ØÙÙ'šìQ–ï´Í·ß°?•\»9$Í%÷·rrå¥��d¹®Ü£E¨†vò¤¦u¥jÐ5ß÷‰Óœu±L)_jæGiÆé�Çãz£xœÎxFB(F‡ü°ái?žeY­|VeãÃ÷ööŽc6X3{Cý0ò?�£ï}Pà++_«õ}¬®…˜zö ,�Eš—„a•Ò7h¿Yœ‹TJ‘'#òx�$iwzÕøÜ¥(«ŒÏqGa²ÇMmÜÓtÒ�žøÁ9Wa•ú`ÎÌxÄ&PÇ©ŽC=â2*-V€�Û÷72¤3‡¶ç™¦.…²ØØ‰ùóo¬òéÝ«¦¬nÄDiE… �@Ó²b’hŸwÞ_eg/ãÛïírg=ãþfL˜Ú(]�ëp8A)ØÜ-øô^Äæ òm„ÎÙßÛ'ž­‡ÿö/îpw#g}'çÖꔢ´‡ ……¦B�«ÌVuGiEeYÖ<ð'5ƒêÔÖ÷}&Qʆ}ᇦ-n}å+_ùG§½ÁHð˜Â£-¹-ïÑÑ ´8HÃ6å=Œ\Dš¦un–euÑ×YR4M­>Û¶Ú�)¥Ýn·®Ø7»WÓËÚ¤º ñù‘)ó1sœºªÞ­Ñ‹Y‚V …$M“ª²O,Qb‘¢µÄq½ªr=”Z`IÍi‡ Ø–à­ïß§Ûñ¸}gÀÒR�?~Àæö)-¾þÖM®ZçÎêב ¡ßóY]ò…Ï_¦(q’óÁG÷¹ÿ`Ÿ…ùï~°Æí{{¬œëñö{÷¹³ºËÊrŸ>zÀÆÖ„$-i·}®ßÅa˜â{’·vˆ"Å�Õ=„�ܾ»ÇÝÕa˜0™&8ŽÍÇŸl2Ø�Y9×™]>Qw 2)­õ™;•ÁAA‘)¼3óÏüݤoM1œ-u嘤Æ9išTÅY�>%YVü I_kAN»æ¦Ñ×V8–Æ9Iƒti·»ä¥<1Ûn·ë4–IÝæyN–eø¾_~YV]Ñlë°gÔ²ÂÂö+¤=IbÊtD–%hª]²ëzØn@’ ”*Nä¡åôøÚ[¾ýΈµ�”OïDlïæ<ÿL×oS*Á4̘ï{ÌõnÜÞ'ŒrÀæ­wÖØ§�K¿c±±F ¶e1šf,/Íñý7ÈrÅ`oÂǟܧTP*Xݘ ¤�”‚·ÞY£TÛ†¹6¬>˜TNÓrˆ’‚�o PV×Çlí„ììLˆÓ‚›#–‚š?oÌ,P?ˆC�¢¨Þ¸™à~ì2r–xÙ¯ãì_ýþïýÞ“ŒûÊW¾ò»€sôuƒâ”Ãø¹¢(Ðh6Å-zz‰€ƒæ–eÕ~x<×üIãû’$©¹u�²¦5™¸æßLÛd¥T½‘múa­u­¸rÒ3|¢é#A¨0…À3ÐAȃ3¾çaYIšUòaB")‘:aÍ€M^ˆ*F–§/£R@ž\¿9`q¡Åµ›,ŸëóáÇØÆ” þòë×yæÊ9®^ÛdwoJ'¸®E+p¸v}‹ÿÂÓ v§ï_]c{0aa¾Ï?`umȹ¥~ô€ûëCV–û|øñƒ½ˆ,×tÛ>kÆììNÉÒ‚À·¸·¶O–ÃÇ×·�›®ßÜD�C‚ÀãÝ׈âœÅùV}ŸŽúas¿~X~¸¹öO'ãjý“•¶ENžÅJÐîôPÂ�é'«CÅRqnÓîÎÏæÏߊk?œ& ¥¶huzÚ&I�_Ç…5Ê ¼ß4MëÖÜæÜ-IÝm±žrˆYk`Ÿv§_ѺFûäiUäk”+/ +-ò¢ê¬vÜULKéôùw_Ýã;~+âú­ˆ~ßçüJŸ¤�´›Ñ8åµÏ,‘å%«ëc>¾±Ëü|�>ÙáæÝÏ\î㻂·÷Ȳœ0Α–C·ÛáÝ7YßbÛ’®Þc{1 s6wböG óý6ßy÷÷îïóÜå6èœÛ÷FLÂN·M¥\»¹Çþ8eo”3™¦\»¾EÐòY{0¢Ó’‡æ�ù½ÕjýÀÔ0×uk �Rš{Y—Ðî?zðí‘fAÃj÷Ùív™c…¾>\UÜ쎥”ªy?¦¨Á욌°úY¬BÓªNSÇiAi“–nÒš)>“2»Ï3!@†oËŒóe‚ÜÆžÑ÷\|ÏAé¥ \™"…¦À%U>¥†2�°¤Æó}Jí�Óë8S¥"KbÒ,çî½-ž¹²ÄKÏŸ!yêâÃQÂtš2†„ÑÊ•%ï~°F’æZ5M�c6·ö9¿Ò#\Àbi±M·ã“f%Ji\×¦× øäÆVXhÍâB›w?XC H³ϵMR4ðÆç.³³;aQ ׳£c‘Eƒ<ªÓÙQkRâ8®©æo&…†!A”¹uºJˆ*ÝïŠ�éx¥­î"…ìfö¬ÀÉ¢Ý>Ð õr3UʪÄ*Ç„“½�ݤ$¡ t§Óé!äe:�2™LhµZ•Tî±v ¼õ*ÓJC×·RTºÇt¼_«t{}ÜÖ9â²õPŸð¬tXßVll§‡:ÿÝ�ríÆ˜8ãùŽTü¶Î/wY˜ó™„_ùür—Àw@+|nÞÞáµWÎqíú ó>Z æÚÌu}^xv‰i˜´æ*åˆBY,-uXš( �%–мùÚ2]«äªâTÑj·YYž«(;¶àþýó=·V©Ÿ¥jîדZY–¤iZgwlÛf/ÊÙuVøa¶?J;ž²ãÐÑ tY<ô^ã‡ã8¦,ËZîÉp! ¢÷(-Ô¦™nƒÃáð¡û`›ÅÅÅC]¹šÇn�Ü$I›®4K‘ÍÄÐÇ 7·Ë¢Ì:äå)®•#…¦.™öº Ìc\[â¸>iéçí%޵õ�!W?ºÇÖöˆ?¾ÇKϯÐéôzm.®Ì±¶1B Áýµ]¢ø°~ïÃ5¦Ó”É$%Ir66‡Ü_°r®G^(”ÌϵfE1y®he©¸}o—ñ´ºŽŽkqõ“ Â0a:�Y˜o³³Æ9o¾ñ ·ïn³¹=f¦,.tØÜšœê‡]×}"?ܼ‡í/¹¹öçEI”;…‹ê¹¶Â!áxP©)ô(g™(¥+µé´ëu\”aýTÖ~œ ád€ IDAT6i¦ÐªÀu$Yšqá\Àƒ�=Ò¼$/ŽmQ�ocÙ’•åyÎ-uIsEZHž~ªÇ\ß'/®¥ð\Ág^X䃫÷IsÁþ8«T~—íÁו¼õ‹ó~ L+Š¢nÃû¤–¦iM•2üòÍFÎâ£?†ÉÇ™ÜF�9Éb.ö¯Ðiuë¿7[íÅqÌ­ô¿wõwù�oý]þñGÿ�uq¿&‰Ã™Øœè£LëJh{ooï!‡Üœ¼'íæLJ²dœÆck|q#ý5Ó¾E‚t@HlÇ#+J²$¢cG€À9{Š-K4’œ 1(ó×±Ànæþ±œ¡¦--¶xæòËK-–]nßÝäÖÝmæú-´†~/ ß¸üÔ<½Ž_3�çðùÏ]ä•ϱr®K·ãóÌ•EúÝ`¶T�z·Û&ð=ò¼ ×õ�cæç®}ºÉk¯^¶%ós-Fã˜KæØØcÛ¢ d]iIÛ¶-6¶ÆøžCQžÜ1ªèApö{À]ÅPæææ˜››;´ˆCµF¹sèÚVÅ[9¶Žw«¢·Þ©jc»ÕÖSÄ1•°¶¥h9E2¬ç�I/u¨I’ÿÁYYîÒ $ëö‡!®c£€Vàáº6O]šÃsæ“ãX¼ùùË|éÍ+ô{>–%yúò"­ÀEkUsA;�ª½–;ÜYpñ|Ÿ·ß»Ç«/_ +çzܹ7àÒ…9nß µ"Ë ìfÑŸ®q¥4Y~üýl®…Óéô!_ö(kÞCƒÔ›B:³F›58)l’ÆB MË-°Õ˜é¸*2 :‹dºC”¹�¡—…µŠBÓ,©i9*¯æŸïû‡Ö�&Øa éŽ"ŽÍs÷ƒ^{‘¨8ÜV½Ð~}>y2­3d–ÔøVJ‘ì1i€ ½^·µHRu}FZ:ø~‹;÷cÞxµÃÏþÄ<¿úKçxãÕ.ž+ÙÙÍ ß#šŽèt|¿‹´«9Òíx\¾ÔÇól|Ï&ð+Þùùåçž¾Ô¥Óv¹ri�Vàày6A«EVÀd’áy{û!íVUȆôð<9Ûö:W.¸\Xèw]VÎu±—^¿‹ãؼöÊ s]Ïå�R�)23¿?‰™ûây^MÝœD [öù'Ö³=ÉD’$8³äIĘ »¬ðl-ü›$IÝ > Cn�oòO®þÄEÄÏ]üyþêÁWiÙ-~çsÿ� Î¥šnÐŒÞ}ß?t!áác¦*ÖTÃã©#4eo[ò¤VVÐ8^€ãXè² Ïb¤P¸2Ê\YdÊŰeIZz82#)ÛFk…Ô1®•?Œ É4´—,Mð\ÉhR 4´»=ÒÜ¢Óñ™Lb„dzŠÜŠ¿è8Õ1 !£œ²Ôôº"P–×µ(K…eÙt»mvvÇäY†m[\½ö€_þùרÞÙÃudõy¶ Í`1×ï�ei•·]¤�äEÊ`0¥Ývñ\Srú=xRÙ3vn®j”0�Núw­òP¡@Ó²B’Ï •`<ÚÇ—Ñ¡àÖT²šO�fn%ÙÒ˜yžr¨{{{§>øŽU"Q`µðƒJŸ7O§éÒëÏÕ]¾ZnöP-˜Šn Ëéà­ÃºŒEN·Ûç{ï�Ùd´}‹À—HK0ç´½’Ÿþ|hJ-‰sa·ðýJW±ÌBð ‘¶KË÷@+Â(Ä·2Y f…?™vqDN®\™å>���–¶ãP–6ÉCšp#åÁVÊúfJš–üÌ,¯@éÚ%A«‚8N°¿Õ%I5yQ¢tÅ—«LSj‹RT�S–e��ñì]fDq†ëùõ=0ˆ^^h¤¬ÝRÙª’a’T�³ ‹œ8IÑX¸^@’<\xÙëvˆ¢|6oÏLÕ¹á…Ú¶ÍNX°i_D‹Ó3'ObOʹý­ßú­ßu]×i·Û57û¤yTRð@|ʱ´SNu»ÝZÂo2™ð�á·ùÚÆ_p¡}‘ßùÂÇíñmîOï!¥äõ¹7ª¶Æ2c}¼Î\0Çv¼…«]ʲ¬k„ÜÚ¿ÉR{éØM[Qµü @’'\Û¼Æ$›Ðó{ˆ#›…b+Ü¢ãvñyÍfñ±ê"´FHIkF Š“›¨d˜„гìYõoK–Ø¢YÉ,Ú¢(Ò²–7+ )ñ@¥ÐXN€¤ pR(æ{6íÀB[çWB3טŸïÐnû¸Ž=Ë®g:m‡NÛ™ñ}Eí“¥¬$™Ê²`eyŽ…ù.Ûƒ ç[ôû”Êg�\E‰ó<‹K—èvÊ¢â¢K|Ϧ×ë0?`#uœ] oö¬|JÇq‚ ž³Íß…8–Â9……Ê»VIœøA»Î§Y(,YIdÅ3?¬´Àšx¦XÓOwŸ .ª]á�Ýë¼³ö½V�ww¾ÏÕ­øÞæw uÈúp�¿¾ûWLò l¼OTF|ëþ7Ù7˜æ¾¹úuö’]É€o¯}‹A8 I¾së-»‹ØŽÍ×n•ÛÃ[¼µöm ÞÙx‡Ow?åþh•{ã{”3�DSMÜ”<9Mî¥ií–�ï:dy†Ô9%‚\»´\Å?ÛYä÷ù—£þÙ`žo¤}R �´íˆŽãȪ¥d’(aSXs¤*¨;¸(mÏ&óál��ãxì s¶)ÃQT�Nèq2='ÙQºÀÂ|—¥¥ùZÔX³»Y=m(SQ977W;™³~ÿãÊΘ`ÎËÉ÷¤xd%k¡�:=;î‘giŲæ3—´°ê&qî<ÔY ¨7E@Ýúò´s÷í¢F0„€–S ËJv­™Œ2»>~!*Ϊ‡‹_¤¨„ÇÉö˜Œöe]æû.?ÿå>�_›ç þÃ/;,t3@píö„ÕÍ”Ûk1÷Ö&xVI §clÇ'-}®~:©u5’w®îóýÖùê×?!�CJJ[dE%ë†S†Ã žÌ8Yš÷>\§(ª�ò{WðöøÎû{$¥O˜”–hR—”ٔ럮F )-”ª‚b×kQ*Åw¾ï±EcaÖóɲ,Â8á�Xù÷N8�¦6à$é¨ l<|ª”¾ñáæ½†c»›n0ïγä/s©} €�¤z}”�øÃþOö³=þâîŸñý�ï³™n’éJãüöþm´Püé�?Ų,Ö§k$ªÚLÞŸ¬’ë$¬NVIòjc÷鯧|wý-Éÿæã�Š{ã»HK—ëÓuÞÛx—R”ä*¯ýÏѺˆ³ÉfiÏÅuJ¥PeA‰C®ZvÊûÙ<ÿûîyþ`¸ÈŽz|=^ ÖÐвb|«ÚH…¢(r áSˆ>iéè}"@ØH£U¹²ë4z–çµ2~`nnîì” ‚L£Žqå©%.]\:6{iTxL‘˜ŸeY­çúX*A¨c˜¦=g•a4±BEµ6T!ª¬Íi–—¢Vª1Úæíî8sLS§¦˜•J’v�=kZ¥tXÿ8e„úý )²Š*Q⊠Óñ^­„Q–%J—�Úr�5ûG>+pr,5"š Ê>|æyŸË2Ò¶ ¿ô“®•�°øÖ»»(\>üd­JN†it—ØÚ¬n¦5j«…ÍÛîòµoÜàúÍ t#¥…”‚–¢ ëb-ÃK¶,‹·ß»�”˲øþ‡üå[¸³Q’+›ªÍµÀsÁ·s†û¶SV­³\Ñgº¡xç½µ³gXffb C/³,‹Iœ³e­üÐéÆjµã MúîhE¥¢d Ö8¯ŸÃâ`ah¦÷׆wÈˤä{;oq¾u¥5·Ç7Y›ÞçW.ý*ýK|wí»|áÒ›<¿ðÏ,<Ã;ëoóÒò˼÷à=¾qïÌù}Þ¹ÿ6y™3I'de†Ò%rýßÑ ú¼µúm|×',Bnî|Ê•öîîßå£��Ø�÷xéüË<=àÆÎuÚ~›oßýŸ¿ôüñ¿Å±^X|ñ¢`4ÃÇ{zbh­H£®UP* [äXv‹¿Ëåo/ÃKmðÖXòÏï¡5¤ÊG#ð­K”3T@Rj‰E X>6‡[ôå¥@[süå·†|ã{#®ÝLXß,Y\p:®; ™ fs^g1ãDM1 ïûuqH3ý£”b:�;þ´ùs–ï?I] 9^k]wžiRi̱tº]J’´j=xôë‹RàsضSíú™ É*Ç$]Zí.Zs걤ÉXEõÃk®S…6Nð¨•Ú"hÏÕæ¶mãú’LPeÝŸ¼Z(ªß›îUJ(Jp¼V­bYs}�ù9›;«1ã‹Ï]Ì1)Ê~ÏçÝ�ììF¼öÊ ßzûƒý˜vàðµoÜÀsmÒLsgmÂÒb�¼€Õ�ˆ_øÙWè÷ںʅî¾øÔ—xnéy¾·ö]\Çå›w¿ÁG;fÞZ}‹¬Lñ]ŸwÖÞæÆÎ§Üßgu•¯ÝúKŠ�h‹¯}úU^Yx•ýh�ûÃû<îy6'ÜÛ¿Çúh�Íé&ôáñôüÓÜÙ½Í÷î—×/¼Ž=Ûd˜çäq” Ò(¤áGæ(¶(Éhñî¶øÒ¼ä—ÎUR[¿ó©ä7Ÿª4Ä£ÒÇ’Š–•`Ë‚R[¥®T,‡R;h­@¸Xd‡@¥Úî±±£ùã¿ð­·§¬n(zIUôl6à&È:+ÚD#�Z/½9úýªŠßÓGÍ áÆ�ž 77ム8ÑC…Úš¿éCã‡+?ÝCÍj,q<Ú—é`æo5q8$°"¢8FH—N·_)±4Ä#ŸÖºÖ†êßßßǶí× ß)Y©†Y�ªè»,«¬p«G˜Y”EQ+5ýðÑŒš%JÒBàzºÿžçòôS£qA•ü­Ÿs¹¸hj†4`síÖ>ãIÆ…ó=ÞúþÛƒ)ݶÍ×¾yÇu(Jɽ+K-6¥¶ù©/½Àx4f°;åÚ­}²\rõú.ia±¾9áÆ­-&“˜$U\¿µÃÎ`ÂKÏŸcs»*4ü±×žb°±?)ùðÚ…Å�ûSn¯ŽÙç¼u�VàqóÎ.Z[lîDܺ;`ù\Ÿ;wwxúòü©sé¨ fÓ…¶(Jîä}b»ûèÁOh5q€ã+*#=&Ð]#›f�†»ìÈUîé�¹Ø^â'—¿Ì×7þšÿùýÊ77¿ÎO¯ü,?ÖÿZk–ÚKlM7ÙOöx÷Á÷éû}.ô/"-‰¢d¡½Èëß@//½Là|´y•´Héû}~üò—xý=Ú ¤ÙAAÚ—žþ¿þÚÎkK¯óÞƒw¹0‘B\ì_¤ëv°¥Ã4�û�7L#žÞï÷BLpG!-§âwuœˆÀÎ�¢À0)`\ÀŒ@\H¡hÛ–(‰Ê€RÛx2ŵU UI^”¯8»¥sˆs©d›ï½?åÎýªªV)Íް௾=¥TV�BU‰³ˆ�ŸvLÑG«Õªïói:™§ÍŸ³Ë§.ÐD5Ç©QÛ£üÄ&Ь”¢Û_"ã "×X)‚Z¸È&UêQj§€bÂp8¬Qðã`»þ¼†d]³"·)Ü~,ŸO °;³‚¶ QNPé>ÓéÇká´Î1Î:$¹U#r†ä6»ÌXN»¦MGÛ„Ó1yž³8Wòú+ós’QìÔ…3ž­˜ŸóyærŸá(âür‡W^X`{0ei!à™‹>~¼J§íÑŸ_Dº•šÄƒÍ}þŸ¿ø�Áî”Ͼ|Žv«’p‰â AÉKÏ/£”f}sŸW^:ÇÒB§æ€V�ãXܹ7 ÈK¢¸d—¼òò IRpñüç—{¸®ÍÓOõëb™é4�QxìJÀÿ ÔC‘2Á…‚A¬Ù³—9öÿo3È‘ifcžA€ »tô.Aýþæ|œDcîYW¹‘¾ÃEo™×ç?Ïÿ}ïÿ⟼ó�ù?nüoœÎó7–±ªspâ"·÷n³,ðãW¾ˆc» ¢|;àJÿ ¥RœoŸg>˜Gé’�ÑËáååÏf!�ÛB"ë@k’LH“”_ûìß!×9‹Ý%JUòüâó¼´ô2«û«¸–KËyÙ4ç>�©¬`šÛ¤i‚#R<+§mÇøv‚#2´†½ þl®‡Õb—•.R”´í­Ó¢UILYŽ¬ä©Š<#/5™n‘k¯â³6¾7+&¡ÅŸ|mÀ`¯’ÛÛÏùê·'Œ¦V­ÑmP¼N§sf$´yŒ¤ Ñk7­™ }ë$àÂøaãGOZËNûnàÔ¢³æœ;®pË�…š\ôg©ƒ÷奬QÛ4‰ñ¬¬Êh¹%²×:û§5nòfòoÆL-ÏQu3Þ縕¥M0k›GSt>$œìWëMûaÙ%Ì@½£~¸ú ‹ Õ™e7CÆãªCžš7^õxö²Càä –Æ¥�Íí Ÿ}y‰;«C^}q‘~ÏãÁæ˜ç.wéw$ý­\yj áΑä�ïðɧ||cƒ0ÊùÂçΣµ¦×õÙÝ�G)_xý2ƒÝ ·ïmó¥/º¶�ï»Ü¼;"J_xý"Û;.]˜ãüJDZ¸|©‡c[¨REíNª]Võ�2Ó]¶‰®o&’©ó£à”&G+*Ïw/³,ó_Mw¬<ϹŸß@I…’±5àŸûYÞ8÷yJ]òåó?Ãß{ù7³Åõ ßd”ŒøÆÝ¯seîi>{þsVÀ+˯ò·?÷klM·è=Þ¼ôf…hå^¿øy~ís‡£u×ç‹Ï~‘ÍѺxvá9.v/aSÉ|=¿ðE^0ß^àó—ß íuøõÏÿ:ŸY~…¨8Y†Ìœ»‘>k·Û5Tk] ‚EUDvÔ,¡™–ðT¯v`/7¯W\0Óò®mG€&,Úháàˆˆ®3¥í$]uQʵO¤»dÊ%Îü Ã§wbÞ|­ÇO¼Ùççj�ϾÜ!Í»ûN' Ãza4è†yÀÏbBˆ1‡AP㜥àå¸ùÓ¼†g\§3)å¡ û´ñq×Î1è,’ªqn‘®?#ÎÇŽ8X *Õ‚ƒ^ä“ɤ o. ¦#š±f›à“ª‘K¼C�Πª¨ ‚JT= §xvUxÕvòx—8š´:H‰IÞ&Égœ_¨Sd…:pÊi<Åw =&šîQä)¯Ƨ×ëÐ�;G.æªâݶK·í²8ç±¹3å“[û,/¶ üŠcüo^¦È6·vé´:Ÿûk»tÚ>í¶‡m FãdV ¨ð\ ÇR´‡•s]®~¼Á4LˆÂ�vÛEP‰Ú߸¹Í3Wñ<›ås]Z-�(.j•…½ýß«6CÃQUQ–%­ÀEÊŠ—kIñÈ 7Š¢ºØTA˜d<˨ÏöGeÍÍšI5/y™×罯é‡WókH))­‚=û¿úâßäåþË즻\h]ä¿yõОiâ¾´ø›“Mþå{ÿ‚´HXéžG"¹Ô¿Ä/½ôË â7ø™{ÓX˲ó<ïYkíùLwª¡««z"›Ý›-q°[¶,$2$$A~1bCHDô‡þ™A‚’ 1â$d�ÉŒ¥H%ŠÉ&Ù$›=°Ù]ÕU]ã�ΰç½×Ê�uÖ>ûœ{oÕmŠljɪ:÷î³÷^ßú¾÷{¿÷剭'(uÉa~ˆþú~‰ãÜî­A<àÆÁu̲zŒƒ„Ï<ñ>zñcøøäeέÛDaĵ�kH$ÿÆsÿ&ìÜ8¾þÈïÞŸÊï·šûªE¾²§KlÏ“CþýËðÁÄîo%ZÊÖ·ÿFV ¼ �$Ó R…(mUo|Q¡—`CEBÚŒ¨´G£~8äõ·r®\ yþ }nÈÖÄc<òxë�º‹_‹Å¢£ûyžÇx<îbÉyŸ¿+Z���‰¢h-¡<ÏçûTGÛ‚ó+sô�Îú2Œîtݶ³V‡=�Áx�Ò ¨–Ne}ƦZ¬ÍHh#×è*g9õõ)”N©É]»û|?ÁRÚhïû ƒsù ”&ñKLuD–N Ø Ùc^-�Ö–ŸU.ÉPIG1õœHÌÉû¤é‚$Ò|êñ`›ÖÛeQY…0ìí ÄŠÉ(àÖ�9û£aˆò$¾2üܧ®òÊ«ïpíê7núý”R %\¿yHY”<ð­ùƒ”ßS¼sëÐÎghÍÓO^âæí#îÞOyåµ»ìl'ìlxòÚ.BH«?,J ò¼FIAÓhîïÏ—CƒJI¼ ´¦Rm…iW]ÁÓÖb±èöª‚iVq_]þ±™5œµÄƒù–ÏÌ;ÁE†ƒUUu‰…›’¼wü.o™ï¢z<6;9ØðÏÞø¿ùµü:Ïl=K†¤iJUU'¬!Ob3ÆZæõ[ .Ùñ<�Åbq¦”X7)¸ü¼£X;ÃóËXlNÆ;Ÿh§9˵êMz /¾´ÃÕ þ�‹†_ß]ˆÒNë¶C¬ *´Ú`´fàÛ�i€F{”µB ÏÊÖ,ÝÈþÉïÜáSŸ˜ \Ø ð}É?ÿ½{üÒgìmŸ$î{ž×ùl;‚»CÎZ}/l‡T÷)›Ïð½ÞÃ÷:‘ëŠ çwï$éγ<Ù"„"JFËiÜ’ÁÀ¾K‹éƒ5ª@Ñx”ÜÚ>%£màú^ IDAT[²,c<SE‡>ôÚûÝe>'TµÍP½mâ$¡, šòè„ü�1V"ÌÈ„8RWU1³r:RÓhI-·I’UURgÞê¹¶ZPiŸ w\à¦iȳ´�g¹¼U#hZCÙÿö¤ý¬†ŒÆcêºáèxÁ`QWÒ„ž&+ �/PÊò€g…�”мhð=¹([ºM¡ cò<#Í*_Ѷ MS#„å�5�Aë†Á FJŸª®ñD‹ò|¼Þ@™1†¶­ië áE¬ì²édçÆÔjÍ[iÈ�¿žþ$Ö�ª–ðàÁƒ zpìÆ2ÆP‹‚­Á.¾çw&!RJ¶·í�ìÝé-n˜ï#7øºÒüÏßû'üÚ³¿ÎÏ^úLGûmÂÓâA?´mÛùEA]×kô GgêÇ]X9n9§°(Š(˲+’Ï‹�±’{a’çy§®“¦)yž1 «µäHH�ÿôÍˤ-L<SßHá_,Eè�†BGø²Á—5•ŽhñP¦$T«d­Ò>u+ÑØ8l´!N¾ôÕc0påbÈ¥‹‘zùû ªªá3/œìöõãؤïQnqžçuƒ}eYvƒ®Õž¦é¹i_ýëH’¤3½9kxü¬Ï;ôؽ7Óéô\Ô7�ÆS’(±gIš¦«s|>Ãg¾¢]Á¢Z·ïŸã}w+7(‹ÅâLðe3Žù_Tøž¦h†ã=¤”gªæ�Åö÷—ùœÈ+ –EU^{„ÃK–K:›ÉyïóP4 é�:ÙH­5Ežbš”ªnIBk,rg¿"ã�G«5RØ8œæ†ñΠ`:M¹sÆÞN ºdµTìlMB”´ÃßG ;€þà e< —’bP7‚EVsio@Q”Ü{°àâÞ�¼(EQ¶())ªOÁÖdÀñÔæYã�¤ÕŠ$îé�/c¸A Ô:÷Ùåîü©ëš7Šmæþ{£5ü(KÜ¿ß<ìå®(¸'ÞæªyŒE­ú/TY–¼¾ø&…ZOX´6‹‚¢i<É$Þ₼ÊÅáã(©X,ïycA°æÐstttîÏ;éy7¶ûÓn^ÜÆã1a®%W‰_oLƒ ¤Ó'µ­&­l€4ÆJË·-tŒ’Š�ê”VIÙxv�IZ£%%ñ­¯ý #+ìïŒBI ~õ—Îðë^¥$IºàšeYw^j·øâáÊÎðB`ø/³^wˆ4MÃááa`À&¹çšlÞ¸Ž8Ž)Šâ\(p¹6¿+Rʲ<ƒbbUKkñrˆ+¯ZÄ�|Xš. ™á+[u·F�–á™õ§;F£Qg íTBÎó=Œ1ÄQH'�¸ød2¡i ‡ £‰•P™�ø§_GÕHZ‘ ƚ餷¬Çé…NÙúÄÃ=”RVú«ÉÐ""Œ:é_V/¸Ö’¬ NýYîlÊ¿Íçó5×+7EÛq¾ëajüàä ^]¦´Mƒðc„\ý gÛ슯ûóŠëÞÓhñ“\诟Tr[‘s[¼É5ýa|µâ{»=UU¯Î¿A©Ö;ZŠ´äAzÄÅx‡�èOÆÏ¯ÅÁó./]’ù^ Íþç�\™ë.�÷ó®å¿ƒƒ¬:J½Q î›!êU\+ÚÖH$ ¡(A@­}” ðÌ|É ]�­–¤u€A-»XoÝÈùÆËsªZcŒ=/ÂPò±\½|6mËÅa—ð»Â V_Îòå×PRð·' m iÇ¿u÷{8v±ðÜR–Ëåd<ó<OlŽŽà G—Pžõy_µh#U‹'5U+h‰.íà›¦!›ï[JØr嵦Íy?AØßßGkÍÞÞóùü‘ÅRh©ªŠº˜ã…Ã.&Õå _º–ûúÒFP¶!Qb)UYÐTsÕÒª- `TÙþ©:½�4&$ˆFUp6äDþéªE3šìZPf¶O«¥íêIIž-�¦ òWqüQ÷P)Õ%ڬʲl­»Û¶m÷Þi­1­uuSÞÉp•Ï1F#üA4cºŽƒÛû·æšwý'~â¨-€Çñ �Ðþ:wÙ2í–°g]×loÛÌ»,K´Ñ4¢Çr�R&¾ö‘RÐz5·ÌÌæ\RO2l¯!ç!»÷+D!¬`óœ†žXÚÚÚzèÆ=‹Ž•­•€r­z S•ȲŒº©ñ¤èDƒÞ =l{LI©J£Œ­ŽÐ%%Ò4„²>ÁáˆÃ€8ôÉ‹’ç?àñàÀ#»½Ú?ÿ3É™‰­»WÎcÞéJ:&Ë2~ï¦ÇG‡ðßÜ€x þ—[‚ß|\v‰ØDÖUœ³Ù ß÷�F‚pÀMÄöŸA–eg&©ýÕo½ºÄ2ŽãÁZðšÁ%ôZá´õZÂ0¢’¼œª†ª=Ý«¼íu]wß× „çN”ÏÌÉÒ2™L8œ¶üÁ—xph¹fO<ð7>­%©ÐDxRRæSŒÑÏ»ÐÝó<ω¼“Znž¦ÖµÝ+hâÄ*yãmʲ$+D²Dö†,Z-@ z­ºœÈ×2òE*f0²ŸO˱ߜ@½O»‡N™%Š"F£Q7|â–ÖºC†¬}kƒ§Nþܶ©ÐmƒðµÄÖ9cu¦eÍ]qá}Ilÿ2KJùÐ8x(nsÁ<�^·Ý=8J÷)ÄkòR üÈã²·‹�‚,˜òzó5¶�ãb|µ‹ƒ}G±³–»6'«ƒò½X¹Z´5g8v{ø¬Ï‡žÕF®Z+¶è\°ïÊd2±E¶n0²�).Š9O©µZ£(uDn<ÓØóI·Ô:"”�Z/0«ÖrÞ=O0ˆCªºå±‹’‹{>¯½¹Šõ�]ô¸zY§')ƘîÌqsî, ^/CþÇÛ±‚<Ak oË¢ß.~º{u||ÜѦÞk¡Ò¿ŽííínÆâQÏPJÙɾÍçó== ¬’Âyë’Œ¡g(›ríçyሬL—zïâ̤ Vg™£D8C›ªªÎ•à !ðÈXÌr¼`Àp²‡TŠ;÷+¾òõ#ÖLFŠÏ¾0à‰‹˜õg©|[H6UJ.ÊŠœdI/+²9¡<ýù{ÒàS�—uU.�£Ñx‹ºPd3jå +, ‡ÃNÆrs)¥:ÉMa¤: 24UagBT¸ÖA˲¬Ó¤”ÌÒ‚ûêêû’ØÂR ì,ù¥†Š‚Œ!+ÙUŸ@Wu0y�KíS¨v}pHy ?ðPÞr’Ñ4ŠÛ¼®¿ÆëÓo‘•)ãñ¸ãñ<ªêrdz'åZrg‘ÍÏZNòä,;U% �g=£¿&öjƒ¤ª®ò#3K6OF{äÚò!uRhbU0R3Љ‚ØË0Øá†Z/M)Z+Eå’xË-˦äç6àòE�(”üò/Ž�Î'nïˆöGGGݳs÷þZ ÿÑcðw.@,WíD—Ülç•�Û\Ž‚²9tö°a!D‡T9è¾ÓYØA sª®bÙDñ`Éo�±˜Ùа´ãÝ´±=íÞ æ‹li’“ÉdíýiZC7Ü+m»,Ä|+´zÝ”Ô�á÷¿¼ÏƒÃж54�á­›%_ùf‹é©‘4­‡ôÚ¶!T5‰_¢Ë#Êb¥ÐÔÅ’¿uƵc‡è”É­…ålŸt©xEãÉ.Úߦhü®Xµ~wÏ«|N¸´%Ø ]U+.èp¼KeF4íù‚–;¤ïܹs"±u{ÙYd m5¥×Ÿ…¦©r�þZÌ .ô¯;e@æ�Ïu]?Í5�;úÏæ>¨(¨©HXÿ®Ø®ªŠQ¸ÅÕ‹Œ›=„˜^4ò}�0B+sT’s[þ€W‹¿àÖôíGíl.W|ÔuÍb±`0¼'—«GÍ5¬þ�ÕA ½†APzÍÚÌC:Û§Èíïâ-ò6>×;¨DK¢R™b„¤1>˜–ÄËiQËa_{fµZ.“Û•b]ÔeÆÏ|Ìãé'ì»öÔµ�O¿pVbÛ_ýÁeçr5™Lðƒ€à\³<á_ئSqçsßÌ �õ9Ùç•ÿê�¹ÁËñxüÐ8Ü¿ÎÅÍœöl&¶ö~ d°â·N�\¶v¼Yýh‰>¥T×5v´»0 Oä0v{©V#­b†'5¾ÃÐ@3·|æiÍÿóÿ=àÝ»%U¥ypPóÿþÉ‚;A÷y€RÇH©0mÁ ¨Í1‹ùñÒL§iL[÷w? »GÕÁ�ýÏû¾ßµ¨ûïçiÏ0ËÒS5nË6`4\=ÃAX¡uM:ÏðÃãÉN×&?ëÚ“$a>Oùƒ?~“½Ý!örž~b{-‡1ÆðÍï¼Ë'>òøæwnѶšŸû™+üé×ïÑjͧ^|ŠAòò«3¶ÇO<b„àö½’З¼úÆÇvC­%ʳ”D_X„ÓS†Ú°–ÐÉy>#Tõ‰÷ÏZFè¶$\0ÆÌÉçxƒQ-;‚E5GÉÈPEžz+�©5bÙUf­+=ÙÚy(mos¹{ÝçËK)­"„©©* ºÆ 6T—Œ¡.3�Þ ®­âsïÐA¦".d[žK2\¢çÜ-,�§eÐCm]åœà¬fYF†\Н±«c¿°Áµ%CmNá°Ðr[¼ÁAy‹‹Õ“\Œ®²½½Ýñ�ú\,'ûÒoËœ– m&Ý[ý�íàù¶mÉÒ¹uO’†¦•„Ñ*¹òDÕû¼$%yZƒ  Æ–•Í T}&—±6è–P•€ lMB$K"U¢�  ¢>a¸òìî¯$¬øÀ‚tayFQœ –4ƒóW×fŸN§ìù»ü÷7J@m VtU›Cmu/7‹¤órªÏ:àÜ÷õ<¯ ¨En‰þƈ¥ñæ Evâ÷£ñ6eQ�– ¢ E C¥}F£¥àz±B!=iðdMQM™áC¯ÝÔ9J >ñ±«|ç•Û¼{gÆáQJ« }þ ß{õ6mkxâê­5Ï}ðwïÏÈÓŒyZñ‹Ÿy’?ùÚ-^ô}À¾GƒH(ÉÕËiÞrãVNÙ¤'hµDùVö,è ¹hwrhN_2Š"£]ò<£¬Rbßr[-1*�v�P½ÁGa½š¦>f^fDɘ .[\Ž‚R âžÛQ¹Tï?¿ÓâÇ£ö`UU§ò»Ð-ÒS`$m•!ÃõJ¿­ ;™ïy`´Õ%å¤SV]×Ü6;%ÍN[n¯9}Ñíímk!ZÎhMCÂdíß;y¬>ß²ikÁ;30#‹-´·(eŠX¶ ›�8¬i™‰}ffŸál›kÞ‡ÙnŸÉåtÝ3GsrqØ©:8ºÑ{áÔ÷?ßÿîMÝâIëÖj�×Cü¤ÎlT,“Œ6g6-ðƒ„áØº]UFä�L2Ü*Ú!%‘\I±Lh!R%±Êi´¢ ²¦§¨cÐMÊ'?¬ÈûþÎ’»ÙR–9èG£¸°JL c^š¼4ƒHÚXü�žôˆ"ïDByÚ=„uÀ&I’s�õ»qÎHc“ê°2ñI—IÿIù/÷ ·¶ì3¬ê”Ðk�ÂrNƒhØ�5ž(:§¯DÖTõ1Ó<ìÀ¦Ó®Ý ²¥YÆdñ7~þ9~ï÷¿ƒ”‚{÷ßawgÄödÀÛ7p÷ÞŒ�ø1„€çŸ½ÌÛ7öyçö”­IÌ ¸û `{[P”š(R<ùxÌîŽÏg^óÊë �+‹4 h‰-ÈÐä¨å¹^5’0/ïIÖ© F;–~W/ºï–Û�T„2íÝ7½šÖL™O³%×Z’ëÈ:>º.‰hS\83@QŸä¾ö‹œóœÃNGº?÷à†GÑ%2Œ�U¹¼ÖõîfUX£!ýåÙß᨞.éÏò‚;ò±÷]©æ„Z‚›ju“íY–uÎ÷}&“É©CŽ›QEg‡ZÔ9wê·™‰TæÑ�.2CžŸd8°AÌ=ÏóØÞÞÆÃááቶ‰ka¶m»FwÓ¨çEr7‡Îšj��>ãÉŽmyÎõ*''ë–À]6‚( ‰ûû«"%òJTï�h´¤•cd»ÀïiŸYH«›Û´íA7Y»y­®ÂÂNT*/`�ÄÔ�¦È˜s8'¯ÓÿŒs;ïr¼~‘ô^‡ÎÜÐbQÝ`QšÎñô ! u«ð¥FU㡤¦l¼¥Ïº&ˆÆøA@–.0mA”l.‡'D}¸vØ#˜W�Eiz¹ýk·Ö¶‡‡‡|õ·ØÞrawÀ"-yü± GӜã”�툓˜+—F,ÒœýýGÇs._òÖ�cŒðøÕ¿ýqæó”õ¥)Ó¹![N�$±âÚc>¿ò×wºés¥¦I» î¢Q$£‹kŠý5Ú½“¥s< ´1Â#îMïž¶êå G”Œº¤³m[ó)¡Ì»{æ�[Ûª=ùLûêgíÁ¾\�[. ú¾OÛÔè*Å âµá£[ªl†ÐÖˆ¥µ¯‘Šãã9Ñ2±5ÆðîÞõ¯½oü.·~en_; ¾4K©«U�éy“ÉcL×’vËRMÓ¬Ô<Šû<07ÉÄŒÖ<|²Ý'bÛ\æ²ÿ$ƒdØ�`Ÿ×ÎŽÕ¦tZÒýµjaZ*U†çî丵ùÝíDyŽžmÛ ÁlzDâ­âa£m»ÙøUë­†=ó ݤD^Kÿ´ÔŒAçkê�ö¨L€GM j´Tbb“�ÖÑýå’)­ ¶Aà!qQVežqÞ7BK�7Û˜Z;óøè†jÕ¥<ïÚTøQ†ÎÜ3t¿w4²ƒ¤ùü>�ÒT­êοºUH©©[…Ö „Ñ`õ Û„m½,fû�Cœ[n€ú¬8 0™L˜ÍfäEÅÿð >øôEövܺ}Ì_ûÙ'ùÊ×Þ&ôŸzñ)¾öÍ|öSOÑ6iÖðÍo_ç3?s•ï¿qŸ4kyîC�óÜ/óÚùúwœ[Ýò»G’ç?˜ðÜ3e‘â6¶xÚ l Ç[§îCËò<…6'P†Š¡íìʳùí­T&"ŠGk]¨4MÍt 0«ZEQû§R6Ïá³À†4Mפé:JÉA.ÒÂ4V>³6¶MM],�~‚1Ëó](ŒP]�6O¹‘zÜó?ó;ÿ¤Ö XCJÉA}‡ƒcÁ…ðÊYݵ£NÛdNVÆó¼®­áû>OÅ&«¯q¯~‡cq—ö!ƒO#vÀp‰uîäSœ#Œ32è#ç:ó”À7 ÒyŽò F»Ýçò<;1hà6¶6‚ZK«S*dó鎷) W½¶Ö?œt…·!ê/°Ú·ög)jb&½ÒL‡lN @7óYIFŒFcª²¤¬*‹p=dõÁ�;�ûù£Ñè‘m¢µë+�Z§ná&ãÏ‹¦»ês<w|ÖªHñ––ö(褼üeÀ‘ÂP×G,Ê�(¢Ô +~êÒ¢�­–h#ðU»–¤m¢È£Ñˆº®ñ}¿+æ.ì�ùès—Í" yíÍûcøð³—ùÎ+ï‚€>m[Eo]?¢Õ-Aàqa'æÖÝŒèªMöÛ9óÓÿò�Vë(�Ï|Òù!~hMªª¢l5Kz¤?ì*ùÓæóù2¹Ó4 ¦iÐUg‹Øû§ žÉÉs…ï[”P)Åx²MY&å_T(i‡D|©)¯3†è?¿þÐØöööZ‹Ì¡}¤Àñ»À¾×µ!Õ‰©Ü¦*^€Ñ­uŒ2ÓÖ4eM(ûžKÅ"¯¸/¯¼ï‰í�k94üAv›@Fì .�ÄIG?pšk÷—CÀµÖÝ�ØV´Ç¶¼Àa~Ÿû晘a8£«DA+êºé4®’Úo�Ÿ–غ¿ßœz¯�ŒFWSÒÒ'NFˆÈž;ñ åúùÓÀiµ²Ê3õ1‹Â'J†¨h�,],åël’[ë# �¬7~VƒGƒAÐIÙŒ& R æ³ÔÆa£»˜Ú�ÁÆñ¦*˜Wqœ0žL(–CØ�¼—ºáCb Y®I‡A@UUç6‘ØuèÌ þ¹ý™¦ "Õ.ÇÕææÕÒh‰¯Zšº&-û EÒ¡üY–u­}W˜è ºÓiÝ<ç”VÅS×vùÄG.!„àÞý·ïÎìuE>ïÜ:`:K€T7nÞd{Û¢êz毼qÀWwÉÒ”§«¹uGòê›U÷„¶'O_©0F2žì,sš9ÁR‘ iQ²êžlîC7€m;, Y–â)Pœ­ ÅŽÈ)Ó Øí\ ØùäÙO”øJ,(ÊÆ£n×g~6ÏáÉdÒqäÝì[v»åœÝ@¯P>µ–Øb M™"T€1ÚjÝ º¥(k"uØç ûÞ•‡~çŸÔ:‰ÜÒðCñ-.›ga«,7aï&zO R®zíº®‚Œ¢ˆE1ç^s�#q½A¼�ñ„ùèšµo_vè˜Öš ð›œt×~^!òëŽ S·’F �íÇt:E™”ÀÓ'ŽLmEãz j™ð6ZP6>Adš²,E7%^w’3›«5‚f9™ë';$¾'l�HSK.—j-¸n.%5‘×P¶>Ò_VÎeIS•ˆ3‚«Œ±’ªÖ’�ªªºêャMù¯ó°’þrèH–-ð)Éi{Œd•G2Ü!Û²�ªš²µI™ZÕÓHwí­8::²ÉÞxBž¯�¬¶5Ë ´Ú&ßBØ¡'!R VÛ5Iø¾Ç|6%”s‚SŸßýRI þÝ¿�„.è ªÖ'Œí�Gž-hÛšñd)%ÓéôLKOcLÌÜ~¨‹¹}?ÂCÌk�d´×iH;®Óe,ršrAä×Ýh´$¯ý5MJ·¤”ŒÇcÒ4Åó<îäבyˆO¸ÆïÒZwH›+²Œ®‘F£|kÞ uK[åøÑЪ$È•>k‘ͬ{™�äyÁùˆ#ÿÒ#ß‘ŸÄúqI�5T¼-¾Ãó,‰w(Ð —Ÿú³úè-¬ÐPÇ�»_Þâž¹N-NvÓBž4ÃÛ¨†ÜÐŽ“÷yX¢ê@‡OCáB¯!Z&NE-þjèx6=F‘uú¢ý¥� «}¢¥Ò `5œ‰ˆbçecˆV#”IñåÉ=d°H¤1€?ÁB|%ɲÔR-„´ÿ1írâçä²r„ µ ð£!rI©;ï\„›‡p…¹kñ:>ö�‡]Gì½ÊEQ´fe{ÞÙ·ªF€¿Õ½³é¾°¼å´ ºÄø¬ wíƒÁ€û÷ïwËY^á{.Žîï§\Ü ¥àÁ¾5�‰#GchQÊc2òɋ𭭠RB¾x@¨Zíñ¥— ¯¿Usõ²Ç¯þ‚"ðjZ-(š€`‰BçyŠhs¤o9Þ�’$B°µµÕõe¾XÊ'>üÞmÌpl»$Y–ujAö(hÊ9žXQn´”�wªÒ„;ƒ��uV¤¤zŠYx'dݰ^†6_BC[£<¿Úº Ò Ðmƒò–CcUA‘Í ¢!M]‘Uš7ª]ÞÖC¿ëOjmŒˆˆ²²FÛl‰Ÿ5Uë6aÿA» r>ŸÈ�§“�òœü,³âòJÍ“k‰­û¬«\ò<'Žc&“I÷ ]2í†{øýk{/V°A0²Kòœ±ï’7Ö¡dí& Câ×x=;>OA…¨�ÈGAÈh²KÓœ+×–Q Qà+8žÎ©[Ãd²EGÖ¦,‚pʆrz‚¿Äo�(²¾ç1Ž�^p*š/§‘}_uZŠY–.§êg]k~{{»C�λ„°Ò-ÇÇÇq¶2ljï±|žu]“Ïï[«ãdˆŠvÈêˆúêP^ÔéaæyÎ`´…ö&4Ú­–g&¶Ë*Ä IDATîÚ]ûXÊ.­Þ/›4°ä[¥ûüm¨–P,ȳÃáß÷–¯ùò)†QÁ³OK>ö¬¤Õu—$zÒ�øMqH–Σ„ÉÖ^Ç9|˜W}èÃÑJã]9!«ü5+b·´Èž�/ÍÙ³˜îS, ’$0ïQ3¦l,âí}6—+LÝ`ËqzÈ�¾�l½t§¥ì¨6BúÚ®BYÐT9FX4¡ÊŽ©‹E÷{Âhˆn*šª`?‡cï™÷æ¯ò*uÉAq€Áð ½�(CÆk¼r‡äY™´Ó÷P_ÂÖJªªârü‰þ—ôÓ'’Ø‹æ©æ~Ø„y4uÝcLw-n"Ý£©ë»\�G§o�*ÅJ¢,K’Ááo�°rµÿÖ0 *|¹ Ï{9Mq@–e #¼xcZ”8­[E^ûÔÚÒ¼<)˜ÎHÏg²µEà{Ö•IÈeGíôïz-C?Ç”Gä¹MP’ÁèìϨ֞#Êž{n/Χû××u$]Û÷¼Ë!î}KÚóØ¢»Bc ‹¹uŒŒcT´Ã¢ ‡a9»<Ç˲d0c¼Igg[/å6v ®0rêq¯êB.]H–±X³·w¦ÃΨe˜hü `ooßWäÙª¸‘¢æg?»ÛŠO}Lâ-}% IPbêc²Å” ˆˆ†{ĉ•à{ÚÃp8ìœêʲd0Úë„%|Y.ïhùÌ2D3%Ÿ÷ß�˜d´‡ö¶Èj¯£GnJˆõï_Ç�:ÉBsXß;qÝN­Æ%ÁŽj€ÑhCUd´mC[éatKS¦èÆ2RyDñ�¦Ì0Fs¿P¤Þä´Kz_–úÜç>÷…n(Œ9ûâ&×ÌGðXµúY×5óù¼Óè«ëúŸÎÓ »ô— ru]‡1ƒ«$Œ1&lsÒ=( Câ8î’ã²,;‚µÓ×sAܵÅ?²`�î³îÚ7ÿ^‘ï�0jDÅVK¯8B7M É`„P!EÙ"„îÈâËo‰@,“»©¥_¶ÔUAUÃ!µö(*‹òºÏ7ZR6öž;I•ù|ŽnÚ¦¡¬<Ïg8HìKUW©ìË·Dq}ÕvˆÐýî¶))kC'ø~@Ó®�_%u' ­‚A`£*;$Z„)­é@kï¯Cá�1M°”R��¾  ®•îtv]›is“ !:i <�zžh¬h¶ †cZ|ò¢ÅSë\:·ªF ¶—"Õ |3£, „ ºÃõQmÒ(ЬÙAQXÙ·¥øúî}õ–�2´Zà‡Éê^HŸ¼lfÄQCÚ�ÆGºÀíIƒ'ŠRw6»®å|ŸÜk6²XJ}¹ÄÈɲÄɈ²¶ßÝë ˜U­O<˜Ø€šÍUi Õ¢Û‚¢¬Ò["ŠÊPÕ}Š‹·{WR_ˆ9¢ˆåª8rï…ã…�ö<ì;.í{.�”Ê�@,ÝÏ€¶Î™—pƒKÔï£ÜÌæú�ÿîÿãåsŸÿüçë~zÏÿÖ»ßäÊîöÍmÂj‹´Jycÿuv’]ÒvÁ÷ï¿Âv´ šMÛŒµŽoç^·\’PUFÃ…ácìŠÇ‘ZQ’3b�=®°©eäPD×ÁpþI’�$I7åíâ¯3ypÏÔ½§®ÈtF îÝì/!LG0@m’ÎQ0[Q)Rù$[`UM‹’æD ÂÑÆì_(iðEEžh$I2 ¯ZÛ¡S÷ycyíc ‡ÖA«, ÊÂR êF/;™Mm-zW:˦û݉_w?Ó“šPUTeI£í}Ê:P9 CI`é%„‘ ÎÓ)±W€®Èò„ÄóüîüsqèaËNýç_UU÷Üêº>“2Ö�ýU…h§„ª¢È­¢Ä`8¡1UÝZ ÚSB¡1ÐÊqœ, Óò¼@y>Ãá¨Cƒ˜ Ž��»®”Kp‡ÁR%"¿A‰–ªõºwÓì9Ž¡Ñ‚ãyÉî¶À“È!d÷n(ið•=ƒ�aš<ŒnçöƒK€Ýûïyñòý­7Þ_ûÎH–êAEzLèYÀJ˜‚4«ÖÞ� L(�n­Îø&xeŒU\jšf¥�Ì=â|‹À[ÅI§¶âÌ,6ï©XrZ7éÓ¶õR­F…VIJ¸ç]ç‚wäÕŸê@¯(ŠÂ¤iJÓÖÜ�ß%fÌeóLïK‰ÎÞѵB];;O¿RwÜ׳–³t•‚ÖºK”ûËÙûº6Øæ:ípw?ÓIKméûDû>ÔSK¤(Ÿd|Á&˜³C"i‘kmE­�~B윦ÊôÄD®Z"xíFË[A^{øá°“û­Õ­+ŸÖˆ®äüÄ׿Ÿ@z>Ib'7‹9Më‚«fàgVo`ÛôZYó†ºi©Ë’H媥6ÑÀÞïÙlzbÉZ¹„±M€Ýós|ÊþÚGò_ßxÀ—flûŠ¿õÿáî¹l¥=jè̵ ªª¢LÖœ[ŒYYÆqLžÙ�Çiv÷¹fÄpdÕ+²ù"¯¡Õ’ÅÒAËþÏ’®r öþþ>ƘŽwèZ7˜@°l­j#hÕv7¤˜-Žð÷þdÙŠ¿Ø'ö[5ƒ_µÔ�Ä‹w;zMÓ4§:L¹ö�s�9-ñ]ñÀYºèø[¤ÉNm›•�D‹„d0\£«ô>ÜïÙÚÚê8¢ÍmýCÆùe|µ*š]’S×õ#¨e’Ò–øÁjà±m«{],(Êš·²˜ûÁÕ3~Æû³~TZÂl6ËÞ>x+þö�oóü‡®0=.¹÷àˆÛÓwùäã/òÎÑ;ÜžßæÙ‹Ï’æ)÷ÃÿBˆŽ—yª™Ér(ó¬åï\mÚš¶9ùÜ=hs€±CÙ—ISÿÿ;„H:­ ~eL,ÑW) U«’=»Os|3EˆeÇ­IžeH“n´É¥°Ý”Í8Ü´‚RGÄKÎd‘-�øÊ«�÷С=„Äó’8B· i–¡µ±…˜ÑDªZã£öW£Eã#½Ahy´uUȚد1HdxÏ÷m2\¬ïÅFÛÁOGwº¡¿Í¹!¯Tšß¹wÌAÝòs[ owD � Õégaø,Ï~)%Óãok´�¼ðÃAw–é:#òWqì�X<º`miçSBaå­æ¥u|sÔ‹‡Iö.'ûè¨Nýîêé@ ƒ) yí3œØs}1;!—Ž‘…åE7íÚp {‡œÖ2@Ñ„ Æ»8î°£Rõs wo'“É©T>‡Ýwϳi2BOS6ŠhxÁÞ“Å ŸÙ‰^«þ ãÕ;à”R6ï¡‚ñxÜ B֔ܫn°Ó\]ëîA@ÇçˆÅÑVøaÜýÿ¶i-'>�’ך7‹-ŽüŸnM}îsŸûÂph+]†zO¬ Ç…«ªªCa\bã6¼sÉq ¨*;m¹Ø!OýdÙqû\KÓ%Égñ4Ïú3cLÔ‡Ãa×*soùƒEwÐK)Ñm…'m þd• 7ó.qk×j*ûy� FT�¤n¬5�ôœžÄZ%%„x(J+[†A4¤¬õòåpß³¬�n©ªš¦µ<Å0hj;4f„m9Ÿ•àúJ£¨(Ë „G[±òºÑ˃ÂnT]M×tbaÙ”-M]P”–óèdºR¯µ¦‘Šðê-ŽÍÏïŒI”â_Ü=f|< N<#׺qÜZ°±Bf³™ÝÈZv•}tI^”x~D)jA]·øÊP¶>Ép )¥uI1JjŠ7ÔIœ9”iÉO’¤sãsÂñ.pl^»•wI ¨º¡·R‡ †öûd‹#A�4yQ"UءțտAÐhe¶d@’Ødz>ŸwCN¾è®m8vò`gí—²,­púpˆP!U# —m¶Åñš–"ض¡’f)�VQ9ucPÊë�üNÄÜX{L7ü#„ g†)±t?S÷ �þµ�XºFW…¶ŠöÐÑˬۆªÈ8*·¼«˜÷Ynfsý¨Èí¾ð…ßJ›Ôçðžºð$ºPgÇ„~Ä W^à�ƒ7XTs>tñy®M®²—ìuL—Hn¢pn(ä´åxxeYv{W)ïD×­ÿs7éiî,èÿïþŸÕuMY–�×&"tZ'GšÚr9 h9$Š—¨S1Å[&y®£`š‚¬l‰ã/�¶#¥¤é$¦–Wº‡í™nXdö ·”­˜4o©[;™0YÔÖi§oÜ=‹”U5HÅh0@IAå†&…·”0;¿¥°H¢0%EY#ý�(t¢ý-ZÄ+³™ÅôÄ^”7»ø=]Úf ¸˜|f,nkŒn¬þ¸çuCfm«RÐT%mSq7÷¸ï=†ù)»BªßüÍßüBQ()Ù^Â~÷€\Öï ·œ µãì8¨Ý¡YÀ©AÕÝìþÀ—ûó¶µÉ]"g'y�Znã–eÙU§}„ÑÑ$º–ó`DY ªF2Ùó´à.¸jt[‘¶‚q»muG´_µÆì&qƒg®UÖO°¢(êîkUU�Ј4]’k�Œ†”’Ôu�6ªóG?íµÁUctEQ6H/$ J KPg£îJw=ªMyâPÛä»( L›uµÕÒJû º‚É% ÂTÌÒºCúInEÌf3[`Xè#†zgíœIȦêÇjLSbª/ ‹9Bz(ÏòµÍa©ò9E oë jpÆÏzÿÖ�šÜþÆoüÆoÕMí¿úàU¦SÆá„íx‡­x‹‹ÃK “!W·®òÖƒ·¸2ºÂv´Ýµ=…]L ]’sVr놶m×ZÂ}9E‡è:àb>Ÿ¿ç!¦‡x(éÿ}Ÿ2æyi¨Å`4±(ÛbF(Ëmo)!P�°i�EbUd|Ù¬�.º¨˜ÕÖ}ª_¨ŽFcÔÒ¬Å%ÛóùÃ$ô mSST5JùŒ†CŒ±„->Ó o.—ä¶uEÙhü Bz BúxJQ9ʤ§&ÈàèJÅF¡ÙßSŸÿÁmîW _øðüÇ×.r§(ùÝûÇüÂΈ˞d^7ÜÏ ¦uÃÈS�"†KÔ*:ŸÏI�’F+<µ¢ã¹Bã8+¸9˸0ÙBy1ye¨[Éh´,ìÓ¾ÈQKÕ� ÷‹’£¢Dê´îŠŠã¼@#HÕÔ”uÃa«ñ|Ù{§7ãpŸîª¦C½+’ -ÿÓ�ëRØû˜š²|8]NbEisù�û].�pT!§‹þ°ÕJ´Ö GcÂÐv¦ÒÅŒPUÝ;¯—† ²Ga�K°!Ï ê–.'sžhp]¼† ÓBVn¢®�Ø”][Æ`t ufÏÝ:C BHŒÖÖ ÑêbÁ¼4\ç � OÿYïãò´høaù:WŠ1 ;ù÷…7¥_ÜAè*7¬à£C²ú�ÓÔÓddÜrŸ™LlPë;Rýe–K ¶¶¶N�0¶m±Œ$±­l÷�i̪UK~¬²ËW_•TeEV†ÄÉcæùœ@]›Ì}®®}«Jл�N®#Žãî qÁöQ<$Œ¦.sŽŠŒ(NŽw(òœªi0­�’H•§*3Jã«‚y©ðTBàùTU�FQk…†?¶½n'ǵ Ÿçà ˆc›\5ž‡Æp3/ ¤dZ·¤MKªd×®üÆí{üú§_ TŠW�޹ŸæìÆiÓò+Ï}�/¿ýó,çÅK{Ìêšëó”ƒ,çññ€·�ù…«; =øg¯Ýâ;c¾uç6oòÊþ”ôóŸæÏß½Ãwkþùw¾Ï‡÷|üÂ/ß?æÅKzvó6/>v‰¯½{ßSì„ÍlÁ¥­1…�|ïàˆ�]Üå¥{|ñ‡ïðâW©µá{Ç%Ÿ½r‘ëÓ9ÇEÉS“1—–û!I&[;äy†©3‹¸.‡»œ Ø@U-�eÛwiëSi‡¼mÛ2>.€9Ù½ÉÄïÍ$¹?mݽ:K„¢?g“¢e›(½V�³T‘´€Æ£s(‡ß²ß�©MÉÿ*E[t×¢uË`é¼çöÀÉW»�¶¶ˆp1C(£m2­µ-„«2£i[îV1 ÿ§3‘ûã\IñËÿySϬ錇C®^¸Jžç¼°÷É•QJQqwÔ.'¿5�»Cw3Ž8$í´6°CÃõÆ6¼çÇÍåžñl6ë®m:�®¡snèÌ÷}Ãq‡<×u�ÒÂ3kºâkßÇדRfF޶¨Ê’y9'ö­r‚`•à–Kkóþõ9XE]>¯Óº¥È3Ê"#Ž¢ñ¶uJl Úø€$På©`Cä·„&#¯ZŒ?"‰ÃއêŸã~‡^‹13Òy�òP†!7‹ŠiÝðò,EøætAk ·Ë†BÅ¿|ó:—‡†¾Çµñ�êaÞÎÉŽŽyúâò¦áþlN¨5Š’‹IDY·T& lsF�dì ZÿÓ˯òÑ ;|õæ» $çùg‰CÅ­ÙœqðO_~…ÿì“àN¦ÑÚ0ò¥Öüïß}�O?~™?{÷.÷¹ghf3„ïs¬�¶áå;÷ù[�_â�,çë÷öùÙºáù� Çe…v¢�w)ÃÀ‡²ìLP¶¶¶ÈóŒ¬Êð•Æ Gݹî‰xcÕí–éºÉŽŽpàæŒŒ\þÒŽœYÀÛåÞ¿ªªº‚Âó|òR- ‡éÒ°¡n�×t�Áåêì��²•ûOUU]‘ÛˆŠ‘Ü¦êÉ‘u† Y¦­ ±g�nj0Ж2ugK‘Ϩ[¸S)ƒŸ>È >ÿùÏá�;ø„ ÄäD›@k}¢q´—ÄÂJÌ ô¥'OÑ=ì‡I)e‡ÚöÑâ¿ÌrÁÓ½p›v)aT”µuúr«([Z­)[ßj϶ m$R®Á”´VŽea+¨d0B‹ˆ¬Ôˆ¥ÓY¢QqbÒ×ñoçû¹¿_ÓPÕ ~Eš¶¥iZZ{Íâ$á¿j}0Lb«ã¸ÈðƒTLQƒäôÖZ)i(JC–/ÛCaÈïÜÞçߺ´ËÏL†Œ=�oÍR^ÅüÊÄdýÓ*6pmkÄß¼ÎåÑ�W0«j.�†ü‹ï½Î$yP”|ûö}Þ8<æ“�]äÿüîk ƒ�—ïðâå-B?à�Þ¾ÃcãOL†Ü�§L¢ˆã¢âx�í$æB’ðÛ¯ü�Äyãð˜�îmóÊþ!osg‘re<äúñœ·Ž§ŒÂ€ßÿá ¦YÎ ð¹1KÙ�ÒºáK×or!‰ÙÏKþäÆ-ŸŒxmÿˆçw¶ÖTF„Ñ�0´(Jž¥ø"[¢¹AÞ<\ûTšÑhÔ=7\èø«g­áp¸ä­5ü៟o}ÿ:o¾s!$w'üéKopqwÌ[7p<ËÙÝò/¿Å7_¹ÎÁÑœ ÛC²,åh^òöoÝšòøå=¤¼öö=¾òÒ›¼~ý€½½¢@R¶ÆØîÎÍ»GüÑŸ�›wöyúÚe”’|ñËßæ¹<ÁÑqÁïå;¼úÃÛܺ{Ìã—v¸·?åþáœ$ ùêËoò̵‹'‹¶A×)Ê�1ME0ØÆ “U2¾tO›–‚wäc´â¤[Ïû±¤€q‘gùà¿ýßþèeûâ–µ£µÁ+7—°ÙÅrñ·+œbÁb±è�¨þ°žv0ô<Æ ÁûqÆáªª:žàiZ¹¡*ÉŠzii¾ÚSR²: j=j­¬~©X ƈ% 'ME–H/ NƵ¢ª[Á‹[Cn¯Ì3þáã;ì)ÉK÷`€‹Ã?8:æõƒ#þ·—_e;‰ù³›·ùöí{duÃxƒ¬ªYÔ ¿ûÆ[´H¾ró|㟽z%à�oÜã™� ¿øä%~û»¯óÄdÌï¿õßx÷.UÓð¯pƒk[[ü_¯^祻øè…]¤üÑõ[L¢�¢iùúí{¼pù"ÿë·¿Çþ"ek�ð`6çKï¼KàûÜ™/¸~4¥2†×öù÷o"¤ä;÷öù½\çc/{ª£ÃDQD”Œ¨µêöA‘z ÆXÚWÙ®:©‹ÃÎÊiøŸþD‡‚Îf3-øî7¹y瀻û3¶'v?½ýî>w'¼úÃ;Œ‡ RI¾ûƒ[Üy0%šº@*Å,7¼ôýw©µÇh6¿yóˆi¦ÙžXÓ b9ˆ.„àîƒc^~õ:JJ&£M«yó�{\»r‘{÷R¾ÿÃw¹·?g––\Øqç�-([m¸y÷ˆ�É)‰iSbÚÚª-µ5áèÊ·{D к¡)sö Å»ÞÕŸAžå�í‚”Twj§!).ÈõùqÎV>ç.1+Š¢“~�FŒF£ŽÃø° áh .¨ýX¿ø²js�}ùRSµ’d`éiš’e ’á/Ú$Z¯·´ Éj;Ö_‘ß«ùbŸ¦©Œvhäó2 ÕË{µ4è/'eæÐ çþ³½½ÝMvžg™¶!OÌÒ ?L�&`ZÍ€J¯ÜL v@,C”””E�0-JBY„Ñ�Fm“Öñ ¡þþjµìÚÖ¾™²UÍø¯ž¾Äÿqë>¿úÕïñ|ãU0†ÿòÚ^÷^=³½Å¿ýÜ3ŒŸí$âÊxÈã“1{Ã�ÖlE»QÈ,·|¦Ýá€ÇFZc¸4ŒøÔ• €à0¯øû/>ÏÝyÆQ^±'üàð_ÉÿŸº7’äºïü>ygUÖ]}ÏtÏÑ=ƒcp@IQ”xH¢DRòJ²±vx½²b²Åõж%¯Ca¯7VVl¬-Y^YZ�+-)‰o‚ 0@sasô\=}Õueå�þ#ëeg3Ó‘"ö÷0dåõò½ßûý¾D9“�b™ ‚‘‚ÅÃ÷šÜǃ#ƒ¬;.^âöGY’ˆÂÛ¯V(猴ê9Z°È™øQˆ¡(Ôs9di{’ÚétÒêW’Xø¡”jÂú·yžbÜ‹÷_«ÕîhØ Bˆç'Bçï{×QLCçO=ă÷íG’$ÖÖÛ„QLÇvèÚI5¯Ýíñì»�!®ÜBQ¾qò,ï~øÃUÎ\œIˆ �÷Míe|l€7¯,bûFjÁ+Ë2/¾v‘üÒ›|ei�ªpŸ™$Cª,óÄÞŽÖ«¨²Â“cŒ—K|èÈ^¢)2Ï��&ê:²Œ¬(<³œ¶ãÇû*Eœ "ˆb~þÄQŠºÆž¹Ä±ÁSuÂ(&§*TL“û†Y´IfÐʧëЀ•ã}Æiõ;ÀHAD1aÅ1º"ó¡Ã“t ?Š£˜gÆÇ¨˜&—W×ùðÔ~¦jÕ¤=ÎÜ¥Óé¤Ý ßeN¾''P±}mÛæF/æá0 ©T*)áNFRB™@T|ÃÐgx ÌõÙeLŒ`®çsñê²,sþÊ-zNò}]¼:ÏèP•¿ùƫ؎G³Ýå‹ß|�Ñ¡ß=w�µfÒE¸0=Ëð@… Óó\Ÿ·éúzú,mÇçÅ×/1X/òòéK¬5Ö±{o^žAVŠ–‰®*4š]F‡*¨râÈzê ^;wEN€;Ûî+ Pô‘×C5‹ýuŽÄðzì@bžúTÁÒàÐ¥?çÐÍ/±×¹�¼"ÍPŽ‘Ùøà–+Š"ÖÖÖR·¨|¦•(& ]ÄE�õ:ìn1±Âô ûç»#¡¹ú½ QºÏ^ƒ%,pMÓr…ß!'wéµWR˜DöÚ…ÈwÇ5¶M®�ÓBtZtZ«€D¾8€G‰žŸˆ,‡[’ññ ;‘ˆ ã±0ínr�‰¿·NÏqÉ[Et³ˆ¸�‚äð#�HÊ¡j9#Á06¼eä¾a„)uñ{ çiý> IDAT¢0ÀÈ—ä2N°óäªÊyÍÇTd)&§züÌ�Â`€‰œÁï=r˜£µTBl¬Tàk×f˜n4¹ FE׸dˆcCƒäe‰‹« .¯­óôø V9V¯P+ùر#ÜjÙ˜Š qÄžbŽ¿¾t�zÞàÄHCÕ828À\³Åd5σ³�Ÿ8v˜¥žƒ¥'°ãCuN-.£Ë2ÏNì¡ëûÈ’DÝÊóÈÈ Õyvß^™[dÈ4ÙW­p` Æƒ#C [9TÊì«”ùêÕI"½CÊ^¯‡™³PÌ=w•ì·IÒr»÷/aº�ÀY@–%<Ï% ‚„8&ÉÄ$ò7â8Y–Q…Z¥@»›l<;]+§S+çéØNZ9<óÖ Þ8�#G7]‡ë„aD>§óÈñIJ–Ù—šJ§N»ÝÇ<&ÖÔº¦"rôàK«-íA&$Ý c’¾ë�`ŸKJbÏ&æ•0ðˆÂ€UWa]¸ëóü¾‡Òà~ÔîêÛþ‰%é¥x™Í f1g�¦áEpD!@�I`£H!6[¾�åJÜ.9EH wªïUL£alÆå…‘Œ}i!·×† I§µŽªjÛ´Yã>‡!Ñ[Ýœ0*rLNóˆÜÝN Ýȣ尃^(öEïEˆÄDÀ01«ÝnÓívÉçó)ÿdWßpz.�v ?‚B©‚a˜Daˆtƒ|Ÿ‡!ãEù¼‰$±¡rG(A‹^·™T¤Ì*½¸˜Vé¶�OŠÉ©A_a%ÂP\2<~íà ‹ŽË•n�‡Êÿâþ¨ýõºb|õêMþâ­+ M’ØS*¢) Cù<-»Ç¿;s�=%‹²¦RÔ4ê…OîOžPÔdIâso]ãÔ Sµ2Õ\ž+kë,uº´zaÐ# ¦ê‰ê†&Ëh²„,IQÄ_]œf²Vá¾Á:pæ<ª,3R* S3 ,‹ÿïõ³M3)8˜ª,3TÈóäø¿ú<§‘¶?²E8Q‰×sºžŽ· }^±Át'ý沪 [C˜—ˆ¹[•%†ë%tUe¨Z Z)oóâ<ªª0µoŒ¢•£ÝqèÚ.ª"36XbÏp•NÏKñÄ7n-ázE+GöRÍ.ySçàø0~ï â(Ķ»CàùTˬ¼ÁØ`9™­a;>+�“{ëľ³1qè#ù‰ �¢%ן…†…QÌ’«ÿÀÌD(à´aÿÃX³¯'˜Û2-±XÂF²åû>ÍfÓ4)—Ë8޳ ƒ% ¢j B`ý¾Ã–,Ë},ÌÎÒ0â¼bb‹ÙÛ!”Ý.z½Õj5ÂO~ƒÀÖëu1”D£0FI,lû-Ž�®Ý TüHÞäŠIËÌÒ}‚`�®§cä (fb÷E8AÄ­”ì³ï@Q”M6–w—êHäntºôº.’š£P¬àûn¯C¤æøùËEŽ$öðJŽX£üãŸÐOT#4Å'ˆÚ8]EÏ£åªô\5²1”­–•›+ Šñ³C/­åøÄP…q…”t†!ÿùÃ÷§•HÁÚlêV»ÍÇä]#ƒÄq̉Á:qÓ\_çÄ`�§÷íÅîvèy]& ?{ÿ8ÄàD:?÷èƒDQÄá‚‚©zD‘İY Œàx­œ.^jÇúP‚8ŽùÔÑIŠ ÕKªEår‰O )óTUöíۛʾ\k4):OîٮϜËåÒŠ«Øé†A©o�y·Ê™ø ±Ðv:�ôýw»ÝMmRý¹Ý˜xICWY\mÓhv˜œJ¥n._ŸãÚÌ"O?r˜(Ѝ•˜¾¹ÈÒj‹ÁZ1•á{ô�)Î_™¡×ó(ä6˜¶¦¡a3 .LŸå¡# ÕK %€l²U(PÏu¨—mF+¨ŠD,ÈQˆït‘t‹Ø·Q +q‘5‚^‡Àé"iyTÍÀw»t}‰yiˆx�Ý¿÷ˆ!’T$ÿN$Ð;GDD‰�D]t±€TNH`Ew«Ýnoã5mé­›#á:'$¶{§q#ª¶bžP‡ïUW’ËRá¢'æ÷(–°úNb½ž�.»(rL'2G,ž‰¸váL¦Gr‚ —6´cu5B§‡×s¥þPê| ðþ}{Ò�|«Õâ`a˜J¥’òD‘Ì÷}>¦k¬¹åeÓ dlOüź.6x¦iR(ÕR¦»½Ãl5_|?bÎæ@"oÚI‚Àóü¾…»LF8nRÆIq3¿´J³mcå“?‡QDÏ X[ï02PN:ßHìŸf¹ÑÂÜr¿Å‚‰ëøaÌ+§.òà‘ñþßÄ´;�&EQ(–Ê„ù<Ýö:{‡“M£,Ëèfßµ‰Pžƒo£çJx½fÒUh¯"©&ŠfK!�çÐraQ{‡Ø�¯Ý"¾ðXe”_ûåÿé³ ˜ Ã0ÃÇqñ<�Å…^|áyæÂ÷ýþN>æ›ßøûÜd¤�UZ˜0MÓp'%8˜¦™âWÄ%È(Y=»�Ê¿áy¥R)ý}a›$á6Aœ8We­ì²8ž­ŒÊ¸Ïd û–®Y<®,'-°ÐwpÜ�¼UH`“ÅNI¼HÀD‹N°I³ÒS;…�0‰c’÷é%zy«ˆ„Ì¿šQy®u *œm+|||ßA뻳$„ɵkº‰¬õÚ⤅x»áìôzL Ç )òðƒD|«|XÇ›äÏÂ0doÑÚñYÂŒ™Kt»nL†D1ùjª÷éz!Q¿M—¶ê2ÏP( Ù$H6B¥B°^ÅæØ$@_(ØW*p¬VÞ¤  ÞWö~ÒjcŸ¦ª*Åb¡’ YŠÑ”ˆXRSR‹�ÀÉ^{Ëž5lÈF>gR)æ2X³w´Îìü*ÃU:6™àÑŒD]áøáqjå„D±ï óËMªe‹û&ǒ͇¡S*˜Ø;„$«” ¹M‹â�ñfæWª²g¸JÇÔ«†je‚ IŽj” ¹~¢•(+-]‰�Èw“ 3p‰}I–‰I5 ß—B‡Àw"™[nžÆ;À‰ÌP 4û]¤|™Øwù7ðÇo sûkÿô7>£JjºZ ® 6ut²ìj¡q+æ±IÉ‚ø†Ä\* cbQÏJð‰¹$«nÛvšhÜɰåí„H”ŠÅbRéï_¿H¼ÛíV‚©z¾[î}' ¾0–ñ£„±ÕØA‘A‘‚>YW¡Ð—½Ç |åÎÒ_·gåß�hÇA,#ã{>ž ›&f�ÿ‹—4~jŽ`Ňï4M>TI®I!QˆPäÄÊ7ò]\L3O¤äpƒ»ó"‚Àg8öÖUöÅ612J_áFÈU%p¦0U³‰ß�júÕÿì|,L Ã@Ó-/†ÈG–B‚8G.o¥;ˆ€ÇOÖ¢­ò]‚‡#æa¡4°¾¾žÎoYGF¡p”|T…=VžÃlÜú ‰n²ëº›dwÂTËRÒ¡Íåò)ä²Óé¤×’�‡‘>kZ“7vÏct8)¦(r|óò-LŒ091Œaš,­6¹9»ÌGÆ©•óh} ê³oR-[Ú?BGtz.÷MîE’er¦�®m¬?†¦„1gߺA¥h12X&Q¸€‘z)×’„iæ¬ + FÎÂq MÆ2dE!ôdENæá8ñ”�ã8q%#&p;D¡Oxx¡Ä� FW)Ýv þ}…©@qéMâÃO!Í_Dúƒßÿã‘‘Q=Ê×¾òeŽ»Ÿ¾õ<…B�O|òÓ¼þÚ«4›M:í6'§èÙ6ËËK<ñî'ùò—¾È“O½‡±={xéÛ/ ª*G�ÝÏ«/E‘ùá„ ÞäØýlÒPmVÓ4ÓITL.Ífó{Ž·Ý)$IJä…�ïíÌ"²!&áÛ‰G+ý6ýNŽÊt½ ±p]7•Nº[ˆ*`j °‹* l˜¨F�÷¿¡óϧ`2/®ÃÉuø¿ŽE´›«äµíR`q,á†rbö�³€×é’“{› ,D$DŒ0½GIJî?”,ry+­†a˜JÝË3ÈúÕû¾ŸV…“ÝñÝ�ï Ë0_]]%ŽcjµZÚšÜéX±›×4m›xºpmºÝý8€ f�·ôd‘WÌ�Tü>›`o½v íŠì湉ºP(¤UªõõõÜh6ª;]»ho ñýjµºaØÐ¯ìˆŠI61º¼~–¢ReÈK-˜ã8Ñóìušè¹²¬ú.�ÛEÑó€D×¶ b CŽ�‵žÄå ÞˆD– M†�` hjU>2©¾­òE·ÛµmÛNÁÃÂ8áNó¡èzduÃ=ÏK c°Q°ï ;¦³Ç‹¤·Z­¢ªjêùý Qi¥BÆLTÙvFnŸ·*‰hJHN �v˜‡ÃH¢ín8©A�º}ífþ!’Án·{W¢ôÖkC60óžý®Ê?‡Qf8Û†ß<Ð#ð] ©ÃÖŸŒcè*‘”Çì›ÄACv·¸en<'ÇMæ­¯‡ê²–'Ÿß°PÎýæÿn‘Ý�„A€ÝëR*U¶�Ÿ{y‡â=´Ûíï|'I­ÛÍÃ’´a@q§u}§ã…‘ˆèˉaƒ˜'³×ž5ÀÐ4�V«µãsÛÚ}–e95ÈÚi®ØVIÚn˜ÛMPdY¦\.ÓX_O¸#QD¹\NçZñÿø¡G£»BÍÂ4Ìôw{Ý6v§•ß ‹( œQ$�´(¢ÕuÐ55ö ŽYpt®©û ·m-þþ#§ÂžÕSH²B£tµ¹¾Î›çÎráÂyLÓä/þßñÄ»ŸJ„…ûU©×^}…À÷yøÄ#,-.2{k†Soœ?w–ÀxïûžãÜÙ³H$ ¾ï34<Ìß~áoh4Ö¸ïØqÔ8ÂÿÚ—è~ýK„�Ú¾”~âS”Þõr¿‚ ^À÷;â8¦Ùl¦‹H»–íÃ�V«•º¥„a˜¶MÂH¦ã¨JˆÑ—ë­"?TÓÝ~§Ó¡Z­;kßéüžç%>ëù<Õju“ÃÏíBWBt%$"â(¢€*A,ö¹J^’ì´w:oŒ©†q·ç€Z Ÿ/â{^à`(Þ¦„>Án~–š¡Æmzí’š8]mråÚ"Wu§g ¤ƒ ÃØ$ݳ«V¡´Yú§R©Ðl6‰¢(•¿»Ýd˜…‹eѦŠã8Åsß.áÌ’ÎDXF±l¦òa�ÛÆP#¼PIqÝâÚÛí6•J%MXïFÖ„�äTà¾lÛN+#YÙ£Û½qíY¨ŒpÓÕa‹™=FÓ4›s¬É·X�gXo/2¦NQ.T’Vs©JÎ*Òi6ð=UÓ1´*B¢GÓ|”($BÇõ$nQ{G$¶~órNðwh2ÅqœÂž²2BwJš6[ðÄ)û.ÙPØ€Þéøz½žV3¿ß‰­84›M*•J:g5¾o7l�+•ʦyPtÒļ—Ó·�¦�nªÕ æÛYZß.ºÝ.½^ï¶�¡�Bpü(Æó º¡Ê¾Ü_€ÓýŒ$+ÄQÀNÜI‚¼Å-쮃nX¨¹*®ÓE  ÕßÔM“$‰œ¹ç™Ó|¢¸E·e£ê 3—Î ¢#´›Ø Ù¨Tj© Dºw/ï0Š"VVV€D­`ë¦ekl…í‰w�ÅOß©º¾Óñ Q ºÙ—Âsˆ=È$pb쮯¯§…9Q�Ø:†v*À†S«€uZ–µÉ%n[¾ýXØ�ËçóéºcÛvß7N1ûâzçe¦}…YéK�*#½ƒ †�d«ˆ™/Ði5ðÝŠª£[¢0DV¤ ÀÔ<�BrØ=—9yø‘Øô¸R~8ùC Ê?ðÜgœÄ÷ÈÔáÃXù<�ï³w|‚ù¹Yòy‹Ñ±1V–—9}ê öîGQUkkhšF­VçÒ¥·8vì8µz�¼eñô{ÞËoÿëÅGìÇ©  D½ßýmì/~¢ˆ°¹Ž‡t¾òEÔJóð}IÕjK›ìûYÙ1ѶÛIH§È¶©D8ë„År"ûÔŸd·ÂD[-Û"¼Ù‘äxž·#Tbk$LdâDðÿ‚-q¦ _Y�†ÏÖàD1¦Ûó‰$í¶ò3’ªCìá8.‘¤'.k¡ÆVŸö�¯T%" }$ÅLɈóܵzž}š¦¥äQ ¾—g(Žº›µZm#v«7{öجS™˜P{½^ºQ×!¬b³ç–€(膖UDRŒT>ÌîˇiJDËDñƱßå8ÝnwŽòN÷.Žf[vD•u7c_T¼DlÐDõ-žçq½wGJVoGê°Ïô"´ÈDSœ¨™·PT×± ÃYVˆ¢MÓû×2ßSYцá�€ïÚ!Þ®‰Ã/þâ/~ÆóiÕK±Á¼ŽûûâÜ@Ú�y»ó ø�X’)©®´$%ÒI1²MÙÊöV—·»�ØPu(69Dít¼®„¨r„hhFžßžU¸lÃÏé6<\‚÷Õæ{ˆ‚"…;Χ’”üVº¸ž�¤šhF!ÙGá&ìíÎמ$ÚQè⺞$·bó»Û¹@<ïïðí¬eÙN–€ÜÙÌhó\$ºRº®§$È»™@‰ã}ÏIß¡¢šx¡„e%°…^w�¼æ¡ÊÑ6â"$sžP%¹—yTä �N'-†d]âv;þ¬D³ˆc…Œd6lÛæjp†ˆOrh°ˆã:h�‰¦&.±†™C3L|×Á÷d-1Љ£ÃÌ.~3NަZ¿ã5þ CÝ¿ÿÕZ�}øGøÎK/284̾ýxóÜâ8æàÁ©ÄEÉÌñÀC308H„LìÛÇ‘ûŽ‘ÏåQ…÷=÷Â0äБ£i«ëàä$S‡_8‹ýü×ø¹_ ò<‚Fƒò�’ùÏü·¬ýþï ¿ëi:’œ’Öî¥å~¯!´€8޳ɷZ`tv[Ó4ÓJª¨À›°Ÿ°™�,Î#Zã‚4v/IF–l!*k[¦ž¯áÊ�ËçPd™ÿã`)LpmA$ã“L ù]ÛÆ—sJ€qñqUŽQuŸ jbwL4#�lTéyJdc(·OPãb9—bY1—ËaY¦i¦»Ò;=I’ø³?ù#:í6ÕZ�ýèÇÉårT«U�ׯ]M¬|e™Z­ž’b²mw¡4I›GLPÙ6ÕW¿ü·ÌÌÜ$ŸÏó©ŸþéñÞD·Û¡ÙX!�€X�OÛ YRö¾3ˆ ßã›%Qß­é^æبæ …”à"`?wЬ¶¶€»&°þ]"Kª]__Dz¬ÔHh7íþ­óàVòmÔ/=_Ûv\–,äûþ¦5 [E»ÛùES´Òw‚J@B@¢÷š34N?â{”þ<,I’¬£k*®ÈyLÕC“¤j š£).~èc{&†™Õ ëõÐ%M¾s¡ ŒÌþ3°m;Õù-‹›ˆSw A¢õ<�V«•v¤v;~…ª�˜—e']ú�"[I-±A»[(r„©(’G¯ã€’O�Õz½Š”Ìo[%1E!67bÝÏvãn7�fǼ㢊}¯ã’¹xmmmÓŸE±GD†Ì9WñÙ˜ b"V¤›¬³ 4ÆÙ“›ÄÊ[ȲB©:€ç¹´××?IlÃ�0‚5;dU¾ëuý C}æÙçR¹©�|ìÇRm·w=ñdZî®ÖjéòØãOìøCõ�Í’0€eYœxô1NŸzƒÇ‚ ÞÄ4“äza~žgž} ósŒOìãÔÚgÏœÂó|~êS?M¹Rauu•gÞû>¾ü·_dmu•¯~åo‰£˜ƒ“St»¾ôÅ¿áÄ#�ñï~ïwå#û8®ã°¸°À'úg˜»u‹K—.ò;ÿößðÜ>Èô•ËÜš¹É}ðY^}õ5ºÝ.‡�ÞÏ?ô#é8*•kôz=|×Þ´¤‰*qö°Ûñ“e)gÿûÖä Z­¦IÎ�ÆŸ®ë›˜å;Y8:ŽÃ\0M…¬°Š÷É‚qL¢¡•œ¿é5Ó[»Ÿ¨;~Œ©ƒêž¦€Çùð<í{+KõN ±ùß²è,muKºSˆw)`SYlö�ŽÏÎIY·°R©”ª~ì&ÑÛmˆ ÚÖsï´YÛM»_Ü» 2 ,¯H:vºö¬"‰HBÄ�-²ì†U/Ά! Ë]‚P'o1uwÛú``É{ œ.FœÈåi€íåpeYÕ)™&=ÛÆ \YÇ”ÝÛ&«š¡ÊÉZéK9t#G›8AMÚÙ¥2Ž%ŸV÷EÇJ`ú…ë¡(4Ýîd7 bMŢݎß\.—bkER$z÷2þÕ>®8Š"<ÏÛWÙ)Œ~5ÀTCœ ,@Ó4œ°DÏïn«ÚŠ*w£ÑØôÛY§³�æá�r€ì³µl’|§ñ¯(J*û':Œ;:½6Íx‹ VTA���¹¸\»ÓÂé9)á^–e¬R•¹ÙÀÇR–è LIï 8ÂíB†�E/› 9ŽÃÂü<óósT*n\¿¶«t‡ÆÚ*ãû˜Ø·?•ÿ’ú‰áêþ?‹ó4ÿòOðgn$Å1DK¸¨4ˆ öÞt^ïrÃýÉì»çÞâk/½ÊÉ7Î"IRZeЂV«Å¹+שT*†Á­Å•TG 2ñg®Þì·”mÚív:A*ŠBÛîÅ0¿Ò Ûshµ;›çV·—²IÛí6Ž“hŒ Fçnî]´3Ä¥\.§XœXR0tEI®/ˆ$º}}È8îk‡Z´N¯Û@V5J¥"º*ÓóU:Až º½@³¡Fµx�DÆÇ0 Õ*v˜#Ì´Ô£X"–74…SâHfaj6›A�@š Â?ûìßüqÿïßB¾p¥ÿr¹NÏáäK/âºËKK´ZMn^¿Î}ÇŽqÿñxö}ïçÄ£�ñ¥/|�‚Uà[Ï“ÆÚ?ô¡3sóŸüôÏàôz,Ìϳº²ÂÜÜ-4Mç=ï}–W^>™*œzãuÚ­ž—´~oÞ¼Áêê úÇÿž‘‘QžÿÆ× †Ó§Þ ÑX'—·xêé÷ð7Ÿÿ�N›�ÁA>ü£áä‹ßfúÊe~ꓟàùç_àÖÌ-~þçŽW^~yð¡× Ý\K0¹¥jJÒïàNŒîf³™Ž!ë”…oÜŸ+IGaÀ’%†‰D¤ìïrH66xÈ;HÅCóCOja+Mle[ZO¤´ú ØÊÊ ¶m³´´”TáÔ Ú,No†¦:t×oà?ÕØÚImÝf³™lvJ¥ô]ì&D¢Öl6SlõíŽídáN)މ¡ïû”Ëåt.ú^„ ½­®·xõÌy¾{ö<^æÜbìv»,¬®§•¼f§Kt—¹p¹ÑJ¿qí"Ñs}Ûî¡ô�6³] I’�ûë�°j¯T*i!äN!É ®Â_µÍ翼̟a™7·”Ê™õKVPTSOJ§ç¦nŠ"C: º]ÝÌS(” †®¯Ó 6ϧ›Î/ #¡.~¯‰ï{(z_®`æ6�r/RÉå·Z„ô§P{Å¡‘�­ãVTJ³ãOŒßÛ�MP.˲èõz»>>fÚæ8v¯G«ÕîoÒÊ)Æ5»Ö˲„¢ôñ*€ièFž0ŒèÙ:­5dYÁ´j(ýÂ�$IH²L>oá8.²²=é�¢dƒéº^zí’,R:§ RÉØ‘û¿ñí‰9 P(¤¼’­!æâl—S’¤k+" CÝU†¼ƒŒ¸‡(x˜~‘\X$—É…E ¿@$�••šÍ&kkk,//c&õ–2�ç^§«–ïú.~Сü¿ð Ÿ=wî …B�“/¾H&¥ü·Î¿‰eY,ÎÏSàóÿñ/ÃDãͳg1 ƒ·Þ:ÏàÀ çΞFQTVWW8{æ4Q³°0H膑Hox½“/@cN¦sòÛ˜<Œsúuˆ!ÿ©Ÿ#6“�©ëzŠ»-ᙽ[<Ô�"—Ëq~ú:�?xŒz¹ÈüÊ*¯Ÿ¿Œ¦k,¯­sîò5J…oœ¿È͹EjÕ «ëM ]ã•3èØªªpæâ47ç"N_œFSU.]¿ÅÒj÷ â8 IDAT xéÔ9dEáë'_C’%‚ ä� —¸µ¸D×v¨W+\›]à+/½JÎ4XYor~úÕR‘0Hª »•�!>Çq]¾¼EA!o÷u!Q@ð#… ’Éåò¸žƒ©øhr@¸ô¼ÝÈ“7u|?À d4T)`§Ë�¤¤’«É>®ëÆ2ºaÄ:^£Ê!n �/$,ÖN»Mâ³ùÇ¢(JÔ Î�b鳿‚{îþÜ-‚×pN~ E–(?ò.TMãäK/222ÂÒâ"ûösmú ·fnrü�‡8}êu†‡‡Y^^F’$FÇÆ˜Ø¿Ÿf³É‰GåÕ—_¦Ýn!I2ÓW.³ÖX£X(2=}…^¯ÇÈÈ(õ�.¼yŽZ½ÎÊò2V±À­™\×axx„C‡�pñ­ ŒíÙƒïûL:Ìää‹ œ|ñÛìÛ¿ŸÙÙ[¨ŠÊá#÷1??ÇÚÚ*Q i ssó„tm‡w?ùv§…©ºhr„톸®Ÿú„ )°ÛÙ?fß¿À+ŠB±XL Qîdãlj,›�dË¿uü‰Å+»Ñ‰´¨~5›Í ¦i‹òU–µ´”%ÚÊ*e�޲FK]¦©.*>Cù=´ ö�d��¾/s!|„¶ôÎÅw‰x»˜Û_ýÕ_ýL¡PÐ6$”’ïRÈȉªünI¨w;^´€a;>QÌÃAl²¾—óß)4MãÌ¥iö Q+�e‰é™yè' ×fQU…óW®áz‰"JÏõ0u�óKØŽG©hqs~‰¥µuêå$~áµÓLNìåÖÂ2K« rºÎÍù%ü(âúì积3ÿCµ*ºªÒhw¹rs–K7n1:TçêÌ<²$GaÊ꿯aeMæ•S6ž%õš(fiÕ§�—î+3„1VÎDU•4y #9±åD/ ÑeCqñ</L 9º¦âyN¨K jŠÇÝHÎ�ddbt%DÁÃq}bIE7ò‰yO&XÿXµˆa˜ý÷ko»!_(8š¦¥Åñþ³cj'|kvüeuŠ³Ú¹…B�V«•J]› ?[�¿ÝøÓ4�ÿýwÿб¡1¿ûgßd¸^&g¨ü‡/½Ì¡{¹rc‰Kשñí×.óÊé+œ½¼@½R¢li|þ[—yõìUN½y•‘zžz1&ð}þèo^!Œ`jÿÂ0ä›/_ "fúÆs‹ F‡6 TUå>ÿm¾o?¯ž�f­Ñb Väë'ÏS­©×ÊüûÏ?Ïp½„Ú7zëê<‹+M†jÅô7 ÃH ]Ù< ËIƒlG3‹9Òª�F%ÖÐGêÐÖ–iès,«×i¨s4Ôy:Ú*¡êa+MtLrš…ïv�dYM² œ^�?J ½ó;hʧ?õÉÏN_¾Œëº¬7®_C’$®_¿¨ªÆèè37o0::ÆôôÆ'&xá[Ïck««AÈéS¯ãº.÷?ÎüÜv·‹,IŒŒŽ%ÉèÀ ᥠD«+(Åîôe´�A¼éËä>ôQ”Çß�aÆi‹SȵÜ5‚( \¨¤X•·C:8˳—§YZN…‚0"o\›�'#J‹|.‡Ýsxàð$¯ÞàÖÂ2=×åÉréÚM‚0b|tˆ…•£ƒuYæÚ­yü à‰‡ŽqîÒU C'B M#Ÿ3¹ïÐμu…É}{¬–yëê Žïáü•ëŒ ÔØ¿g”o¿v†zµL·ç0\¯nJRï•t}MÂ0ÆÊçÐU%�ˆlÃñ$UОÝCSbd)F‘ctÙ'ô¼@"—·0t Ï—IÚr‚ì¡àÑsP4 Ó UUÇ4MÙÈo‘ׂ>ajóýh�Oã7þr‡�Rxêô‰”~äãÄNû_¥ðèã(C �ŒR.—yæÙ÷±ÿÀA Cçñ'ÞÍØž½ 19uÓ4yú™gˆÂˆGœC‡ŽP«×ŸØ‡n¼û©§åàä$ƒƒ,.ÌóÄ“OqüÁ‡Ø;1A±XäÙ÷¿Ÿ|ˆR©Ä#�=ÎäÔ÷��>ÊÑ£ÇP5�ýû�·,Š¥c{ö¢kO½ç½œšblÏ<ȉG#ŽÞ÷ìÓ|ç;¯òî§ÞËèCIå>h£Ê11~¨Ç„A¸—6©XDDUUHœÝ®†aFGÚ6þÄâ¾õüC&죃 H[ÏbQªé#h�‰»(’††AˆODHDH�1 ´µ?§’_ÅTÖ‰£€v4NY7‘ˆ±c-ÅP¾ãí&·¿þë¿þY–5Ñ‚Übsáº.†a¤íÚ{%íd�$ ]×S�çld�ñI¦ëµÈéùTéíâq…žéÌü³ ‹x~rÏ­ŽÍùéë8ž‡ç'B÷­N—ñÑ!¾ýÚi:vÇ€åÕ¦¡söÒ4AbåóäMƒ+7gªW¹>;ÏÂê¦iðæåkÈ€¦ª I´:]t]c­Ù¤k»”Jß9}ž�j™0¹6;�"+œ½<ÍуûRC‹¬|ØÖ"Ëå1ù¼Êƒ÷82eaè2EKe½å3\h¬÷hw}‚ bn~�õV�RÁH¿5$¹ÿÛ>š!IбC4É'ŽŒ"iرNøN0йC¨óós(ª‚ª¨©F窢°¸º†˜†Ž„äL�áz�¢•Ûô›‡&pÐÏ{Wé2YAQtMI%G„ðÿVrU t=U S;]] Ñhã÷lò©¦aÏ‘cI–SÓ‡­¡È1EÃÅ }\['g•P”„0å:]L5$Ž¥m}ƒ«— –Пû ÆácÄž‡6>A÷å‰=—îw^D>t•j•j_Ê`rêpúïûÜôÏ©ÃGðý€¡áá¾-nŽƒ“SèºÎÈè(�´Ò?ü£chx8Ýl•JeJ¥rZ�8rô¾TQ¸E5›M>²i=08ÈÀ`b4P,–èv»äryrµåJϳùÄO}šz8Á;5WRù8YŠÉk>OO'7A˺$Ý-„.­ÀÔŠêËV²Ãíðc"²8È|>¿�("IËËËé‚%"@J¶Q=´ö§Òßõ$;)ÜK%­BØ8‰žÓ�1ä¶}�7y€ öhmjJ�™ L#|çWî%îñNìpñ<³ïÂ4Í{¤f�/‹)äe+ö’oAé¯b\ßâ"‘2áÜO­0�Ž«{í¦ é$€û lYœ½r­¯Åª0>2Äêz‹ÙÅe ]c¸^M¯¡Ùjqp|/º¦ã‡!õJ ­�³ÌÆ@µL†˜ºÁÔ¾½œ¿r�‡ŽL%¸ÆžCѲ(ärèºJ&†>%Ë"ŽbÖ[mrÇ&÷§k@Ï»ñXU’¡ìyyCar"Ï�ÙŽ�$Ñuæç›ø~ÀsÏåÔÙ,­ØÜšmส*³b�®Ýc½iS+\¾ºÂøX‰bAgayŽ}Ct»Ͷƒ•×80QÆv"–Vl†òÌÌ·8<9Àkg1 ËÒyýÌ,ý¡£¼ôÆMÊ•"²$#+ 3~½eóÈñ!¬œÌµ™&££5nÎ6èÚ¶íqôÐ0W®­Ðëyì¯ó­“—ùá<€,IŒ'ÕJ1/Ý-¶Ž?¡´´´$†Fbã¾›ã³ÄÁÄVZ!gêtm‡7/Ï0:´±.$Ú4Rš A€"+¼qa†vÇæ?ûèãÈš…$K¼ðÊ›Ø=‡÷?þ(qéÆ"šªÐî:,,7¬P¯¨”-&ö ¢*Òöñ“Œ©Ì˜LŠì}îNœÀA)J @ñÆ= Ëí�¾i‘ódçá,DM�¦WWWS̬®ëéï¤.‡Z%Щù{ú'ŽY—“j0*%³J¼ü—˜–Ž$-c¨‹ÜjkÜÐ>ÆÕ¦¢8ÌE–ë[lPò“ŸNÛA°ÿàä¦*€Ù?ó³ÿØp–ÎXª¾kS5);,/-¦ƒ5ª PúÕÿ™Ö¿øç³3äŸyŽÁ_ú ½~ZœWíc¡ûrEšÁ!Ùa,I7hºË º [{©V«Ûl(ïwvâè!¬|Òê}üøQV›-ž|ø8ªªpka™GŽÆq=LC羃û˜šØC©�gzfŽÇ�!gš”Š  ­¯žýă÷¥;Ñw=x +gref–§N<€•Ïa»Ï<öŠ$±ÚlóÁ§Ÿ`fa‘ÇŽE‘elÇáO>ÆÜÒ ÕÒf÷%ñ\³Ê©>ê�ª'’BÎ4�%‰õvß÷SF¯H�Ä»J’xR�HK÷R2] ÑâN×Õ¢X®à¹.¶ã"Ë:¦r{²ƒ®D¨²KH²‚ï…ÈZÛ“ òvLnã^¢ˆÖ—¿ÀÈãO²ü¯ÿ7ª?ý7Æe»Ýï¬0}å2‡�¥VHǨ¨>4ÖÖ¨ôõ„Óߎã´Í%Èw°‘à‰Dw§‰À•Ë—xà�79üܸ~�J¥ºí|â8E–Pã6Ý–�f°Š5ޝ¥x5oÓ4‘uW¬\áužµƒ¾^°˜…žé&wŸÌøÙJRÛY¤ Kd-\›Í&Š¢¤.ƒâº…  _¢"$0Œ²+Æ!Dà„N|]à —Ðý+œŽ>HW.Óõa%̳O[ç°¾ÊZ˜cÆ/áÄ·Ç�ÿ§©k7IêÖwñv¨"ŒãDŸy+ñJQ”tŽ¢ˆóØ´@‚Ëñ«”›ÃŒë‡ï¨°Sd«KcCuJVb 3\«põÖ<ÕR‘(ŠXk¶Ø;2ˆÞO®'Ç÷`:Ãõ*o\¸Œ¡©9p]Ո∼‘à ''ö�3 ªå"šªbêÓkëŒÔ¨ñÖµÆG‡(ä ]çð�}œ}ë2Cµ*õJ‰ùåà>.^»ÉÐ@uS²•…Ý,ª¸÷‰1…¯Ÿ´™¾a#)äÞóxŽ˜ß9}}‰uºÝ.ó Mò9ƒcGÇxé•iÞ÷øaN��E’"?±�¯¿p‰‰½uöŒ8{~ž¼g?ßZ��™ù±¬Ñl¹\¹¾ÆÑ©:s‹4Mar�ó——1t…k×Ù7>@ ¸~€ë\¼ºÎž‹õ¶GÇ^F–%nÍ®3P·¸>³†ªÊì­ É0:TFŠC–VºØ7˜>Qi¼Ò£xnÂáË4MÖÖÖîyü ¶dLÇì©ssn%±§í¿‡=#5^={•N×á�#ã„~�(òyê‘£œ»|“™Å.KØÿÏ=q”—^¿ÄÜr›‰‘Wo®pÿáqª•¯Í3¹ívCS©• ”ËåÔE\»ah=8JÇvÒ1/÷!®ãàxÏ¿|ŽÁz…'šBV–yëêͶÍÓ�ÿƒN:°M$Kø[[[KU/²cWÌÅÙyF‘Õô;¯†céÿß èÒ¹¸�Nß^àBü4ƒ2k¡Å¸ÖfŸÖ¤®ô¸é—iGú¶kýA‡÷ïFàZDé{·Ö·"!0 cWÒŠ¢ ½õ&Ë¿õ›”þ×ß+ÑÍZŠß ß6k¬H·èÑÚô[…¸Ê¨²»}\BjJHΈëUql¶šy»ƒc7.5’$¥BáÂé$ŽãtpJ’´ëêKöüYËâÁ’‚ªé”‹V"é‘qz šp„FéCްô�ÛLQ,áF²š@§‡ãùèJ„¡x;2smߤTI0“Íõ5" 3ןäâpsb,/ÎÓøïþ ±ã0üOÿ‹ÿò¡üñO,ÎÑ=ùÕüKXþ8õ¹¿äã?þ Þºpžõõ­f“‰‰ý\¿vEU¸qã:ï}ö9fnÞ Z­177 $ăR±D£‘ȧ½ï—/]BÓ4t]Cî�‹Å…öíÛÏââyËÂÐ &æä‹/àyS‡ãy.ó³s Ònµ8pp’«ÓW0M³ÏÖ989ÉâÂO½ë~â(À%B¹@±˜óbKB›(–�¥/P‰ûã§\.oªªn;1‚…lwL`‘¤ 'Ÿ{¦i²²²’¶³Ê#bœ uAÆÿM`qÛv+é5¾D¨ âFUÜ8t_k,…ºQÒA’‰PmÆÕpÙ¯Ñ ·3ƒPñ©Ão£GÄqlÇqœó;Wб!ºwYl¿˜—ÅxÊS¢Ä®Ô#À#QÉ èJ â$'�¼Ü�xU.—SñzQ@ ° >†aÈ­Åeò¹Qa;.ž÷–í÷îàúÕ¾ÆéNk€¦itS×i·Û›ä­„¥Ýh»Šã7æQƒ± Ó�˜™w9qŽ£ebIÆ4 *¥<†Õšª06R¦ZÎ!K5‹jµÈà@‰Åå&º®pèÀƒµß{t²ÆòZ—�Ö¨Wr Ôò ]�± ,«Àèp™ZÅ¤Ó +1RSA’™Ú_Æq*Õ ãC,¿×b jàyIGml¤ÆÞ½54M¢^Íc;>£Ã%JƒbÁÀÊëT+yr¦ÆøÞ =Ù$9@Q-yÄ�Æo.—KÇž0ßh4éøëÓÝB^!ÙÀºJ±X@‘%âx³FÞÔ1 5µ¢×ÕD'8’ôT�Ã04LÃÄ0rAŒã“JÖI@­ZIÇŽ"K}hâöyX"îw=dE¦T*aèZêdª© †®¢kÉüøVΠ^-ÇÑ=碃§ªjj‡.Ʊè¾hš–n"ņD|+®ë&:ýÅ:Zçòòu ê"ye–ލûÕ:”d?Vðb'VY,�`¯Ö&/4#ãSPžxüÑÏ~÷•—¹rù2G�Ãs]¾ò¥/òð#�R©T8{æ¯÷Un\¿†Ýíò…¿þµj�/ýÿ̽Ù�%Éuæù3ßÝï[F[­YÅUTQEAKC­é¦ûa€¦Ÿf€y¡^øÐ|×1À`^{0-µ4l‰”D‰l’Å*²¶¬%÷=3ö»úîfóà×<s¶Ìu»,-/“&)÷îÞa0èóæ7‡»wn#„àô™s_=ν»w0M‹‹—.�*ð´¼Ìh4âÓ�¯qþ… ‡ ÓäÂÅ‹¼òêk•4ïâÒÍf“³çÎóáûïñ'ò˜¢@…h–,EÁ¨¿I–嘦Uq�jª$Ë2q]ïP9_$è!Ã0*µ¦ƒ´Ù5«×ëaYÖ‘ºá5.LËfêêƒ~¯5õÎAkW)…c&½ïaú­’,ñQü%îªY/äʤc¤œ´GFF(be±Ux¢à”=ÄŠ¡t㙃ç nÿüÏÿü;I’Ø@µ Õ;žç1í â8®x^ga÷t¦G—vëãS=4±é°‚O‹HŒ(È èpœ‹DÅ«Ùm¦Çkå©YYRÃ4Ùî—Yü0Iy÷Úg\½s�n«E3‡1žëPÈ2ðE®Ý¼ƒe4› „i2…Ø–I!%QœÒétøÁOÞáìñzÃ1¶Uf°Faò¢ Ï2ÌÉ{pP€>Ëô!¡Ó„q¤¸rNb˜`Ù.͆‡! IJøžmØv™UlÛ@II¸�Ë©]”Êñ­œ…–À6 �²ôë9 ”Äs&U6SáÛY– L?è¶=%ÎBÛÂ�À¥»°„ï9¤I„%Bš>,´Êk‰’ß÷pË6h‰i–I*C€ïY´š>­æÞL·�ô»¯¥_g­?]™­«BjR-ý|Ðú­›Þ×u <ÏxÃ({IeâÅ &½�$1j~X'ˤ”UvkQ•ìš<ß÷+¶ýßš{~¿{Ñ”^ZÀeÞ¨bý,ë�Çùâz²Á3ÐÿGL¯²p‹÷’/sOž%U-#å¤=¤m¦$Ê"Vé’*kü&ô¥÷ÿ‹f3óÿàÛß=yê4ƒAŸ.^âçoÿŒ‹—.ó?ø>¿ýæ7ÙÜÜäô™3†iÐév ÇcÚí6ív‡«o¼ÁãGÙÜØàK_ù*†iÐj¶ˆ¢�3gÏU¦3µú¡×x=HÕXØ+§j`â0ÛW/`ãî?½¸u¶R×,º’›¡€§›;¾Çƒ'ë\½rǶ“?¿|ö?øé»žÇG7ï°¾½Ã…3§x¼±É`4¦Ýl²Õëóá�[[ì²¾Ýãæ½‡\opûÖ[o}°g šŸø(ô[:HÕ™ÔzEI7òhXÝê�®¦9ŽóL¢ÁÁ§Å#±Ã1u¿¿_E ¨2d³*I\¿M8æ“Û÷±-‹Çk›\½r ×±q•ŒÆ„IÂ{ŸÞbF$iÆí‡�yº¹ÍR§Íþþ?³Õ°´°À�ñÃqˆTŠ�ím†ã1Û½!ŸÞ¹�aš\¿û€Ÿ¼mÂ{jòö×8±¼X©&Î+ÉnÆDÊ»Çò‚¢Ý pÜ ¼~Ûªû™†â¡²I’¦M&M²4Ç4T©2™[J` cêrL!+ú/)lšA€Â”²šfºø~y`Éã¶Q‡)Ò ÏÒ’áÇuq—B–° ýSº*„¨* »ß±KÝ©ÅÊïqöVkâw†k7îà¹W_¼À§·ïñðé:—Ï�fż|á¶µ» õ‹©Iü§¯]gwöÀ,„@˜vI%6Ù\4]Õ~/–Æ?ÕK„Ó¼zPƒy°7ȵŒ½t'–Q`©„a”㸶e"‹Œ0•(abNÈÇ“ÂzF¯[;×É“õ­›ë•Á­i˜,´›Œ£˜†ïÑn6x´¶ÁÝGOyõòÂ(æÌñÞÿì†0¸tö4ï|ô)÷Ÿ¬ñæ—^ã“ÛwùÊ+W°&‡B}؇Ý.]—àëMɲñ=Ç2«ƒný3uÓ~x27}Ь�—Ê +JC¨==bÒ¡Š„8•8®�ï–×8 c C`‰rþ¥DÙÞ9B€*q× ðýro·lÓ3Òéñz©y¶mÓëõö”Þb{©¯_]UÔÜÙZØæ Q=š% Ñh‚Qòô¦‡ï—‡÷,îc›»ûX%¬‘§˜Ãc™Û¨¢‡aÇótôŸÕ9iµÚêôêÏé†üéÄìÆ³„¬´OÖ’éZöø¨I†ÀI±WÁФÏÈü ײÏúu…`(]zÒ£c$¬Z!¡²K‡žôh™)ÇÍ1Ño0À‡zH!DÅ©º³;œ›cV³h‡<+ƒºŸé@Fs´êL¦fôsiÛvŧÇë—³Ðé1ú…ªß½éG—Üê³Óݼ:c§ic´‚“i–Ê4i–óËOn`çOŸàÉÆ&¯N8g݇¾÷:·bý$»ûÀ¬òyûåË¢Ë=¾ïÏd´Ðe& æŸõÛõñ³¸…]+Ç5 D͹J%ÈhÒh¶‰Ó¬l1`FHYX1af£fЀí11áÁÉmaZØæ®|â÷þæ¯9uæ ŽãòðÁ}†ƒúgÿ†f«Åÿõþ¼øÒËÙÜ”v§Ãh8âáƒû•ÂØ8 9~ü8žïóæ›¿ÃßÿÝ÷8sö,½�ÛÛ[DaÈÉS§�Flom±¸´ÄkW_ç_þùŸø­o¼ÉÙs³çLcœT‘Ø)In"� jöI’„"ÞÚãPãÌ&©ôËþøïi, iœ«‹ø WÉjÕ�iÓ• -¡©ç°ÎÌ1�*ŽÑºôè~VïÆOÓ´ÂÊ;vŒ���*Ã4ï�uò¥´²á«¼–¨ßã¡ÿïù0\¦8l]M#å%g‹ ƒëÉ"‘²«?K”Éõt‰ä×\{^¶„���ðúŒÎàhrý(Šä�6½6tvI3ÆÌcz³,Š¢ê �Gdé|‰Šf³Ya ‹¢Ø×Mb\`ÒZªïM˜TIJiQA–¥{Æè§ ô0QrŽ·;d! úe)»Ù$Ž’8"JRÞÿì£qÈ—^¾Ì�‡Oøúk/îë‡ë�åu¶´W& Ëe¡Ý (òꨩ(§1ô^¥ßÙY¿}»Î,? e6Ak™L–PYä„a (vD’— ™Rà7[8¶² ?¹6ÒjµH’„Á`€eY;v¬R1œ×ê{«b�‚×~¦›Ô}+%—€�ªª5è÷p�qu8PJ0ÎlŠIâÅJnÐvo`¸mÒ8'Q𺯒²j¦›eív»bPÐï©nþ®Ïa§Ó©ÖÃa÷®ñò:¨ÁX__GJùŒôîafÐÿ5¶«ÂD©œp$¸ëþ[>»‡Ž·„ä²½MËL¹›uØÈ˜BrÉÞ¡m$ÜÊØ*tm¿3ßzë­ïÎóÁ$Iªr¯Ö©Ÿ\M™.@‰ýÔäÍõ,ÐA¦I‰÷+“Ôx¥3 yžWÁl=ƒQ/³fú÷5žh—WïÙ{Ð÷lF%±¨K�º,î86«K]V—<—c‹Ý}¯£~ïõrŒî0®î]–M+ðŸ)ƒi,ŸêÐó¦iA¢1«�÷<¯¢Ý©Ê/r·L–eó™T&�VYòI¢!*�äà Ë"J’Ý2Ø~Öjµh·š`YضSf=…^°±±ÎòÊ1 rÇqxùÕ×ô{¬=}Ê«W_çé“ÇÜ¿w—?þ“EG|ãÍo2�ùÒW¾ÂéÓgÙÚÜàÔé3ô{=‚¥åe’$áµ×®Nªi–±ººŠe–äÚÓ5Î�aß9ËóßÎ0 …e* %p½F•MÈ ³l$3JXB\Ó˜²¤¤1F L ËóqÄqï†{’ Ù}FŠZT´Ó×kS¯ŸºÒ™çyÏðö3}¸Ó:MÓª‰LsM&&R7)%Móvú†;© Œ78ÈÇéYb9_@š*“�ÂcÉŒXµÆŒ¤ËH:ô¥Çвb…ô �üsdp-¡Žqø¢2·³¬.ò ñ¸ót¦Ã^Ÿ4˜öt°1¯éõTu†ûÁ\˜ÔºìªþíY°—y¬ž­Òªý‚]îµ-«ÄQNÞ—²á®ÄkZ¦ÉR·Åås§q›S«+»õŒßž.k½gØL›fÃÇ2ŸÝêÈzÞ÷ý}E<êï°†JL—é ]M¥¼n.MP`:í’ ¨È G=e¢Å‚8)(&¸ƒÌ²LÚÍß*‹Çõ‘ª\ó¾¶mW8Ýn·[AÆŽÒ¸§+b‡®1®A¶Ê,t†gçØ¦B¨Œ\Z8Ny¨N'8cí‡se�æ»ÙFe6£O0E‚å8¬÷n¢¬e­¥gü°¾W½ONã�ësèûeÐwP£Yý^ê Iz�Y\\$ à v”$ƒRŠfq [=ư&Mˆ£}ÿ�¹� Ÿ#É ì>¶�œ¶‡ `P¸lKßÈ9iI•UшýºLlll¨y6¸ºé‰ÓeHÍM7k�ê Œã¸ ¶„x–·sæÅ‰R­c?¬;o÷S*ëv»•$è,iÀÃÆÏó€gC@Ö³\uÓYWý™yyõX�IugoÜ(©¿|w—ÇqÖø: ¿vºG!`׸SÃ0ª¦”é{ÔÏ Iòx Ç”¤…I˜9e�”å•0NÊØVíÒ€iÓ˜,¥TŇˆ0p½Rò1MRÆ£Aà³¹¹Qv8ç~ ”b4â>Ýî;ÛÛØŽ�m;ôz;ø^YMX[_cii™��õJY/‰ã =™ÁÖæÇV�Óï÷h5[˜–E† GC–––év»39fa/WpVnÙ¤Gq<ÄqË€(#‹˜\–ï“L nb¹Â@f L„QnBi”’YWqÛWH³Œ0 ÑŒ ú™ïÇ� ”ª2š%ã(ë/MSlÛfii‰���Š€}^SJa)�ÑÂʹ*Fñ7¬?ãVÔ˜û»´y"碳ƒ/rîd]¶ Ÿ†‘qÅÙB×Ó%BytÇÚ±2.c®�ZDsÜÏ›¹]__…s§7t%Hgà5×õ~1]=Ó> v™5ÂÞÂn@1íCë%)åL¥2 “©g°êW§ß:ªÒ™Î„醷Y G�NI߸³³óLó“fÑ•½yg];P°¥6±�vÃ'Ëfï@ÅS®|ÚíöÜ\Óõ,Þ¬J¤6Ó4èvK®ëÑ`όȥÁ8óp=ßµ‰¢˜8�ô¨g Ós†!Â(Ùm„–×ýû˜.ɯ¯¯Wâ°K³•¦i•9l-.,,ìé¡‚˲Tí ì {Òïç6�öÊä ×ǶKºÂ(a¨…I’k_ü!w±!d™@² ²(!/ã/¾†æ§Ö‡�ÅÅŪJ»_ü¢”bii©:´$¢3kl ³hÐl6Y__¯ÞÓyM)…+vÂÿ+è‚RÈ,d��ãCþˆ§éÑøÃ §ì!§¬!kyƒ»Y8g÷X¶Bîg]ÖòÆ©¬ý­må³nGÁÜÉ‘e™šVרó�ZÙDÿ¿þïé2YÇÕF«ƒØ:‰¼'¥¬H—õïNôóÐô]Ù,!]ºÐ‚ -M'¢¡ GY`:—RVZ°aš8Öx]æ:j€­�³†:ŒÆ!JX,t(ÄaãÇ©JØÑEí7¾¾1Õ7Z<@ÁÎöŽ™brO©G)…ãøžKš”Í(U9×iHI} ”%G/0„AGÈÎÕqœ c¨��ÞÈë4ýwê%Í™og¤…I¡ì™bÏÌÁ”$mÃI+I-ZÝh?›‰í¯™”²bØÐ‡%íg 6eü¼ÏAwç§iZ™:¹qX€ñyÄtt¹ÙlVTOʰé´JƘíííC÷€:Gõ~�ðA¿¯÷áYÏN7R…aˆLû8VAš[ŵ›( 8¶M…d¹, ª&ÔQ›Ã���=×.̲F*EG(YÌ =ô\è¹é÷ûÛŠq*ˆú:Ñ>4—Æž¤‰öå³`W{èë I³–dÀîV‡ ™l£PdÒ#h´ª½LfcüøŸñ›”ìIÙ”mÚÂöwã")‰’"¸Ší¯V0¯ihجy÷}¿â{ÖxÚÃ'µéJ�†vhŽê£¬Sä4Gÿ7ö¤Ò+€pó0ø÷|8ê2_-åY;a�8c÷é·³repÎîsÜñ(kñ(o)Àõ�‚¯´ú„Òäýa{îàÖüÿÛÿøÝ–½ð å„‚­~È;Ýa0ŠY].›T~ññ=Ž-u… ·¬ÓðLî?ÞäGï|Â�‡¬®,âû.?ÿà6W.œÁ²L¢(bm³Ï;Ýa»7æør)%op›ëwŸòd£Ç¹S+(¥è c®ÝxĩՅŠZE©ýUbàÙÆ+MA#ijt%�Ÿnœ™§L»�V:»qþ¬ñõrÿ,°ùa¿­¡¦ãxîž2Øaãu7«eµÆâÍcu¨„Îj~^M†!iš‘K“L–ݸõñ²ÈI² Ëv ü Ý‘,ÿέ©¯M7 E¾§£×!Ÿ(b•˜»ÝÆ*&ÿ_àuyGJ¹‡WT—z3¯˜Ü ƒeYU†¥¨ÏA½«Z�ãÛåø´0püîd]Œ±‰Ê,#'ŽS²\Ñl4ñ[KH™0HBïÛäÎeRû '�ã§ 1ìûnÛ9F~Ÿd¼ƒ×:�1�Ù쇣ÕeÚzWî¼ëO)µçÓí#5‘)I#ÿ9ŽÙG˜BqŸ¡ñ5®çWÎh\˜×ꥱSv9Ï;Ò£Wø,˜q[˜‡žÆ5$W›C¼�T»Ø/>¾Ëíûë|tý�ÖËLêO~ñ;È$Í«€æî£M¾÷Ã÷Ž“Ê—jÎÝýLûkÝ@»¸¸Xób{ëã*¥yLû"MÌ ¹ÑÁx¯×#MKÁ] ™w|’¤DIJà•Yô£ˆo@ùŽF#šÍf…�Ÿ÷Þ¡ôe½^×uét:ÕVÏ„æÒxÆ JNÓñhÀ8*ýh³QªFZŽWõ¶ì7‡(²„ñh@!%�FËñ*¾i=PW[›†ÅmooWɃf³É–åP(ƒLÙ�2a•Ä1R>›„©Ïa–eÕ¦…A!K†ÇkVâ?Ž×(ÔŽUV_‡c=h|‹$ø3ïë(ke4(œ3Äþ·_%ê��yéOLÞ&–zTÍ›ŽY¦çPgmëÕHý¹ñxÌ`0Ø×UœA•é?²/ίa=„a#„�ÌBB®p'¿@8'kßï¶ ŸOÓ%|‘󲻉+ fmemNZ#ÎÙ}Œ9ò·'�˜†™ó4ñØ<*LB!‰Ä€BåÕe€»ÐxóË—Yì”/ºRŠÿò¿à‡?ûxO°pûþÿôÓ †äyVåºtûµ«øôÖ#­m㻥´mVü·w?åæ½µÉ·>Ùâ•K§¹~÷iù'“‰›§ñK/½ 4¶t^ÓAŠvpÝn÷HN㯴Å‘‚T`�Çc�F•Á8t¼axB”eÇ¢(7HÍnqÐoê†=(;xûý>žçÑívg¾X}W½s[ZóŽWEÎx4d–x×ÀuˆÓŒ$+8ì„'ȼÄßJ)i¶Ú˜öî‹�çù�˜bíTvvv*ýf³Í§·ñË�>ὯG¾�ÑrS‚š4qýÊô溳³Cž4ÚKf?Л̨”-®™Te#ˆ¿‹ëKÒÍŸ²³½ìvŸ¡@ÚÇ ƒ?e�\&ôQ2G8—ÂDë;K>¾C»Õܳöuùs¿,Öô¯ÕjUëOg ´jS†‡ª�M?_—œä§˜þâäÆ ¢Ø§ü1wâçÃaÍü-²wÒ.+fÈ‹Îø4]&R6/»›tŒC`NFA¦ nEÁv]‡Y&bÖÄFñ`ÏA3+;ƒ_»z�?ù½«4üÝuý“_^ççÜ¢�»k1Š6·ûX¦n2-ÿ•ç9EQ°Ðn2Çœ=¹\UÇÆaL¡râ??»ý˜/½|ž[Ö«5PÝõ• IDATL*#óØtP9¯Ô¦}‘Ôtï0Ó�4èè26ÌŸ¨HÓ´‚ÑÕ‡šaâ96¦aT4Šó\»N2è„H¯×«è3�²i UUNÓ¸Íuï€Ìƒ>išÓj6i5’¬`8>ü� €<‰�F%\«ÙÂõˆlI3ýÌ2¥ƒÁ ‚,˜¦I»Ó»ËßþÓÛ¼÷Ñ'¤YFžiºi¥2öÌuLæP'\ší%RZdÊÛåC�‡XÆTÐ( „J ä/ñ/¬€0ý™~¸°O1öÿ”At–d8@ɰ0½³dãÏÈÇwé´[ÏÄB\×� ·Ô{æ´Ö{±~Ûív%ÐsÔÀÖW°Ó÷1œI•&� ã€÷M&óÇ;‡ÙPº|š.ŠWÜMšFÊüÅý¬Íª5æ¼Ý;0À5…bÅI‰¥É�ðè{„Ðe·rbý~ÓP†`gñ£w?c»_.n!ÿÝ|…ßÿÆ+{°v§Vyý¥óå /.ËÅ÷o°ÕÑît¸ýpƒ¯]½Hœd<Ý(¥>-ÓàÕËg8}b‰N§Ãõr1öcnÜ}²§Y`ÞS°æRŒã¸Â;é@ï(�š~9æupº¤¢+´ævUYgîLì$ ¨3ÚÁͯ”ÂsllË$Žã*�ÓÙ�‰�5VÃO¦ñÔõ ¶†wÌsíŽSó5U[«Õ:ÒøÒ¹¦B�d–iÐ𽲑J^&@–D5Ì\I×2>xàĤ”Œ†Fƒúƒ!wï? ÛnóÆk¯Æ¿üäq¶YÐtÊ 7pÒ=ÂPR«˜rÈx¸]=¥Yî)×AI$®²|{U¤Äâ2° ðâFÔÕ”ºsÍÝËŒÝ?a˜^AÙg éM7þ÷y­Ë±Jqq<3ñ½’öJÁæöˆq´›=»zå,/_:…Q“ª:µºÈ×®^,;ã'2Aùnv»]ßáw¾ú"Ÿnqëþú3-xùâ)¾üÊš�&£(åÑÓmî>ÞäÆÝ'HYfš¦³þ™çy‹Àp8¤Ñh)©?§«qÚ�6^+=é±u~Ô= Š"úý’>ìÙwpöOsÉNö€y®½ÜêïÑÕ0½Í`³Ûˆ T{ˆ~&s�G•}qBVHlËÄ2M”°õÃB’$‰£˲A•=éuØx�=Ë ý“ë7QRqüØ y–QH ·sn‚cxV¾‡o][–Œ ÊÞ€V»ì›Ãל‚þ(A’[XÉg8�CÉÅj•IuçÙŠª0Ƚ—ÙÄpÔ%çÁh‚Šqäg¥Þú>äýj5lⰊ´Ö 7yžãº.–eUBGi‚´�7ú–¿¨IÖ\5ÿˆ›QkîÞ‚ym,m>M–É”Á‹Î fÄ“¼Å½¬Ã1+ägÿW*ÁVfsëMdu3ÿ—·þÃwO¨‹{ˆ¸u`°¼ØæÓ[�X]Yà™Uò¼„ ,/41 ×±i7=„a0E F!ÝvÀâb)ñt}›4Ë8¹ºD#ð¸~ç Ývƒ‹g�a© 7 CÚ½aÄ×߸̫—N¡”*Uòtî—Rã½”RÕ&¬1…ôT½p�…ÑxÚýÆë¶Þ5«q@ë¨3ÐóØ,,Õ³84�eÛ´[eð;M9RQöLÀêÓX.í§qau œ»B‡aàêÏ Ë² C7ûÚg[x®K‘g ‡#„iÑ |P<ø!/²atÚ­ ¢‘å°(d�šÐ‡ÕÍ1‹É‹£›À2l#e8Né Ǽxé"�v‹¼(è B=Ýàı„QŠQBa›Ë�(%0„¢ádX†¢�×ßÅ"ÛŽO˜–Ms¦¡H “$7ðâŸáµ\’QFâ~„±gë ={°€ÂÂk�Ķ]d²F1xÃv†�íH¾GÔ{€Ão.U‡Ÿy±|šKQ“„릻#ÑÍ(I3ùg7G˜A:Ú$ þ”-µÊ�8ø•ÉåÆRñcÖ˜E3b§ðÙ(4ŒŒ“öˆT™3éi2eÌÍŽ0mÏ‹¹ýŸßúŸ¾sR]²MvUœ”R()‘Rñöû·ØÜrå…˜†àñz� g�a™&ƒQÄ™‹ŒÂ˜k×rûÁÍÀãø±%nÜ}Ì/?¾Í£'[¼|é k[Þ~ÿ:ã(å¥ 'i6<î=Þà³Û�ùäæ^ºp’‡O·9}|‰o}íEÖ¶ú¬®tj~ù_!DÕÈ9÷¨$Ö•Îæý®:¶–RZÝts]ש)ÔfÑöÛ°ëG5×ûlL¬ ÓiaM`^u±”Ãî]WU¦«Kõk¯Kbvíš“\7Ð>Ïþg‚n§�eôClÛ)û" IQîC>žë ŒÉsÈrò|Ò15¼¬„‰=ÿï›iVðÎû×xóë_åØò2®ëòÁ'7yû—ñòå0�‰h�!qÌòßR ”Øf�oçØfAZxÞn€Ÿ¤ÅÄ—¿å62�iÈŸc:.ãä$…svÏ>>-'½ë‡m û$Aû,†!(Â�0ÔÂضÄH>#ÜYÃm£ÙÞeQ8 ¦ºÞó¸¸Xí±GñÅ&Áøo°}19¤$£>aãÏxR,ñ0ùÕpÑæl>�‘qÚ’a²ž7H•ÉI{H`dìþÌ}`'wŽ„³­›x¼ñPélÁÌL&W3ÖU=M9£³ƒ:ÀÒ�“¦�ªsÖ»� Åȼbº�håÌôw qô®Øúx ê0Ö%†yº#µV¹n4˜×ôKô +„i>�WvÃîw¬C4ô³×¢¾°^¶XW}��búèõ£»v÷›S�iBÐëõv7Saâ>ŽeÅ i–—ÀX¦V«UÁ1”Rå÷(Qe.â8B¥BšmH'E*AZ˜¥|°!Ë,ƒè Fã1išðèé:Ÿ|v“/_}•÷>ºÆå g¹xf™†gOJRŠŠ£V(¤ä¢M£Y®‹h´�þDÇ<‰Gä¹ÄÈžÐ6úù—‘Ω}ç .¤ YFÊ.ø‚bç¿m¯aXÛÓ¶(úw±‚cdQ龄Ñ}¬ö¾ômÓ–ç9yž³°°€ëºlllà8NµA6^)E3ÿ®¼Žá–ÁN6Þfd}“ĹÄû£ƒüW¯^㋌ËÎ6–�ÜJJ‡&ô4÷²kyó ƒVf¡;Ø"æ“ÛkJpçÞ¤Rx®ËêÊa8æ÷¾ö2Ƚ嶤°ñ›Ëå)}°ƒkŒQJg&˜~x¨ø>�}Ÿpg‡(øÃ¹æAPÄw1’wQEB:NˆÕ”ÕÆŽ>Â3·°+åßE1¹óÎÒo#…·/7µRªÂh6 ºÝ.½^�0 ñ<¯ªŽÄKª”“ ÒÁôË€£ˆv—(š¿Å�¨ÁÝø‹ÃwfŽ(¸ìlÓ7²ú…Çy»ÏŠ5æAÖæqÞúB~çW©P¦Mgæöóú}ÜÏ'iñ�¸ÐôKu›~_Kæ“´úþýL¿ƒp8â, ÇyMˆ]®o­Ô¤…f%7¦ïí¨j—Óã÷8iN³ÑÀsöçTŸuï¶mWïàA*‘Ó¿½¿­R»\×ußwÐøY÷~Ð*À¶,‚ Æ JÊ=~X©’=G3$(¥ªïQ,ÛÅu�òÐ”ÄØæ.xݤb’ h—ìq�¯ßæÉÚfE+æ¹.£ñˆo¿ùUd‘²Ð|ö°Ë­vIe ×±MI’›+À÷'s8Ú¦ÃÃ4SÉ%r÷òÁÈ.O~Ç“Cƒ@…ï¢â¤£1‰:‰0ÀÊîc‹ì扉ŽÈÝ×q–) F¬¿º/ît:4›M666Èó¼ tã‡VJÑ(>ÅÍÞ­zòh‡!_&÷_á“që×Ö8k 8c8n�XËÜÏ:´Í„Ëö6#ép#[üÂ\±½½­ž'ƒ9‹ß´.±¸ŸÞsýåªs3Nk�×;ö=Ï{&ý®]M%¢9K§9Qº�z&ìy¸uÆVcÑŽÊQØl61MsO 8ïoVyížcí‘Ù�w¼Þà’$9�*gÖØéù×4R‰4^g‘µ`ì¬K50-‡À÷QJÅ ²ÈË“édýéß™™½,Û+�k–A>Æ·vÄri Íç\ @”4:k;÷­³±µÍ믾̻ï}Ào}ù*Ç—Ê2—’’Ü\˜pO&dÑä;(gÍ1©³ÌyÓßÙD7ÖŸ¡ëº“Ò¯‚ô&$Ÿ �ñ�82Hì×�—¼ƒc1½²H)â…X@4®’û¯U��Rj.W‹5dYÆÖÖV•á›õîL›Í€`ôWØÍ%@ ³!£äIã÷I‡_;sIì~‘f ÉE{‡Žs/벑œ�8Ú“LÂç…Hü:D´ŸÒ>Ožú®‹ÈÔ)“ê¦Ç»în¯…ë¦9n5Å–¦Wª'´Öòæ:3y˜_Ñ´¦œuà=ì9èñ¦iVxÿƒ’Óã§«!G r=¿�0L¾SU­æ­4êk¯_÷Qð“ÓÄ£,öË@B /Ó‡§}«�ÇÃ÷Ëì~”¤ Pò™$ìµ 8ž_©Ç™2ĵŠÊ$¹�ß*£AGŒ0L© ÂTðÿ|ï8sê$�ïÓ 8sb•‹g–0EY™CIâÜ$h›$úxÆpòû0J )ª÷E�3Þx—Hœ1EI7Ž#G¨áFI¯UD;ÄIƒÄ~ dŠŸ¼�cǘn™…¤q‚ážÆ^ µÎ¥b¤Q7òBY‰Y^^f82 ªØ©^¡™Ã(¥pÙ$ˆþ+(×F‘ôfçÈo²•»|8:ïìç53Çœszô'\¸žÈ¸âl“~�²é"I5¤ÎcÚ1h.R z†Ù%¡Yã5ŽHo˜I’T¥$ëÏÂÞŒA½¥„R»JVG 2ŸW©¬^z9L!h¿g ³ÈGša`Ú.ÝVP)Œ=�Ò™Þ„Ïl�óŒ×ƒþŽÃËg�×ëÇ4͹².Õx¶ãø%.I3š� ÂÚö=B€åøX–MšÆ2Â5sRéÐl—e½aÏ ÷†; +,®ß}ÊVoÀÒâ"BÀÙS§øÇý˜+ ¼úÒ‹Uó ¿M`í¥D S‰Qe×´ªÎQÖO§Ó)ÕÍÆÛ˜&8VÉ5�}@€RÑIÚ@u~Ÿtx7ù®aØ ¶äoËå*ó^of1 cÏ{¥ß©iйýʬ‚ŒVø7XA—û·¶ñ.ÝÅëM”áðá¨Åö@ýõì€zÐuI^øX ˆBÉÈÿ6‡ŸH䯷qVÛ’qÑÞa¬l®§Kx"犳E¦L®§‹ÄêóAÖÄúúº:, s˜¹®[5 5‹Ï*�i•�y”Ú•ºK’Ó4Ÿ[N÷¨e2E öîvÚUWìtÉ~›Ærê˜L{O¬N†á\÷®1ÊoûºÁ›_ÿ[Û¼íc:íY–ó¿ûMâ(B¦;Øæn€‘Iƒ0µi4ʲ�ÎøÙ¨›®äyNogTN³½€cò1Ȳ[¥l/ŠBu ·QXÇQÖF¾ù€Ü>Cî\Ú“M×PÃ0°‡q ¯¹ßõh¨M™Eã€kí°¾é±ñp�þÖ6ç¿þoñ–Îó0v¹~q×ç1Å kÄi{Àfp7ïpÒqÒò4or?ë<÷õ=op»½½6 ÿy Úèîé£8Ó’êišV"'ó0Õ(¥öpd×Ïs`>J5Ͷmü À˜ÒŸGÖ\Û‘ö€I`ÖnøU`ºŸJÖA÷¬•õïëLêQL@P6ñ=Ïþ§æôõÏÛR F–ýŽm"ËÄ%ŽUV%úÛœ`k¥‡í–Ô{Y<Æ  ³°‚‚~o›Àš5—‰ÉÚö˜á8cF|õ�«ü×ü!'WW £�/¿~ßózxƘú# 3›¬0«ƒ‘>˜Ì;‡J©=Yîhð·±B£ ²[P¬ƒ�Ê@IŠ\�ˆWI¢"ßÁÊïa2&ä"™si”Ð(Ͱ£×Ÿ>4ê*ÌA<öú�ÇäYB;þ{l>þåSžÞ½Ã©Ë¯³úúÿ@¡ n† üŠšÈæµŽ‘pÉÙ&S7Ò%„P¼4‘Mÿ4Y&ú®ùÖ[o}wZ]æ' †òô¢Í›zç¬é«tÀ²¬Š6ã°WŸXê�¡:Ð;*3B]©lÿ®ØÒÇáÇo¿ËÝ0 ƒV³,c×;ÛçUšÑ™³i…ž™s ,Ç£á¹dÙ®žµJôAcž{× :k~T…žº¶úÎÎN…cÓ8¡yŸ»–_Ìó¼R˜›Þ(‰e;(Ԥ̕‘çû¯Ç,p&%0ÇÌ¡HHR…ßhà{E! Ç=\ë w@bgO”R´ï~ð1Až—�=/œ;K�1EŠfj’Jç6¦éìaõÐVW:;hýè,CÉÌ¡°l—W÷yÙþâ/þâ;qÛ³–æõš×JŸz”&0½hqÛ¶«Þˆy|�®Âh‰Ö:KÊQÄ ôuh_¢³i¬nÞ½Ç{^£Ùh`bÏø£ú¢ù÷�°lZ !JnYÔK)«lø<ÏNï_zÔÊbó^»®ÂiE˲Ý¿f™ÎÔë,ëÜJ�J’¥ †å`Z&–i�d9Q?Óø[çÛ9–)±�œ�ë8äéÛ¬‰ÒHƒ8·h4š8ŽS1ÁLï…½‡ú@¨TI;§ Ó²‡1¶ýÆ"ñð!EÜÇ$D ßÁã"�[çIüo€s Ãé`šÖ¤)TXc<Ï%ÉàÎýû„QT®ïIåd?«³ëxñϰxJ”¯°vÿ1E^ œUÜÆ‘ÝåFôùaXŸ×eÑ—KfÄ1kL¿ðX/,™+VÈPºdÏ Q0ßzë­ïÂ.툦 9Ь¦€�FDQTItõåÒ”+ç¥å÷ v„Ø¥œÑÀzý=ÚÑ5ÈÔ‹[7Óìç vú¢8fg§ÏñÕš5¢ðº$íQ7)M›S—NÜãàL›Fàa™Æ3ÒÂû;-E¼ß³ó<¯ÂØÕl!æ£ÿÒ¸iÝð7M›6Ïú©C4§ßQÖ�λŽ�a’´¼ÞÀ÷�èr{íQ:Ô:¶Ë4t€ìcYI^ 1I3Y~N›Ä’ã+‹~ƒ§ë[|ë›ßÀ¶mÇ¥À&I òB�ä6J•òŒÎÐÖçdÖú© ˜è5­1‚ú¦iZÁL¢(¢ Àë\&³/gFtÃTØÁ"Ž9ÄN?ÅŠ?ÃNoà¦àËÏ0Œ”�â2†aq÷ác¢8&ËsV——zÕµ*Â�†Õ<àŸ³ùè>ç¿ú§ø‹X½üen„Mzù³È¿)K‡DY·Æ´Í„Çy›HZœ²‡xFN¯ðŽìüŸ7¸}ë­·¾#„°u€TÇ¢Î[ÒTS:c:Ó`ÚÿÌ’C=,HÕ¿­³ÎÚ�é SJy$9]Má¨ý`Q³ïCîÜ{H0 Ë2Ž[v)ëXó<‡é=`ßþ0q=ßÝõzü´ÕУY¿-„¨šhë÷®ý¨®ætíõ½b0ÇñÁ×¾�iŸ¯i¦ôxÝÌ}Øx˲h5ˬg–KLCà86y¡ñÙSЉfQA „Ë”X"#Ï%¾ß �Š()÷rËx–>¬n†P˜"§ÝjðàñB˜œ:qœ—._*n˜$i�!ʾ‡Tš˜¦S%g†Ãáøã<{a}ÍçyŽas‰Îþf…�\!á aä`e÷±¼Žàˆuìè¤ëˆbŒ™ÜÃþÿ¨{ïϲì¾ïs_~¿ü«ÔUÕqfz¦{zzÒrf¸�aWKš2W ²)ÙaÈp€`´ “ AÓ¦EÈ (¢)’’E›6-‰&).7ÇI»»§{¦§cuuåðË/¿ë?Þï¾zõë ¿ê�<@3Uu_¼ïÜsÏ9ßï7¸Œ¼†ïûtƒa"xõ�·²ö]§rˆ¨GÞbæßà ^C¯œ¢³±¹x»ñfi’晋¼Ó«<0Ýá·Ú"©ÓJ]ºÏ¬Ñ§“:¬Æ¦Œ“º÷ÀnÜÂý¼~ ttP&Ͷí<)î| ÷2åXÔùU×qœ\sÈ)oZV•ÐF�3îl¿ëuP*¢ëë›[,,Þãã?ôaêÕÊ}Af1{Ô ì8ç]ï@‚eÙ”]{_Ýòqtµ�P=žK}�vÓtlÞ^Û¤¦‰|aò†÷}»ÛgÂÉ(EP˜ÚX¨¶’k[-6ûfë5ÞÙj£ pu�›�a*©˜;¥†½ø�÷ºvuoûížÕïû½.�ï%)%7£3JRrçjñ}¢ RB,J¸¥¬ç¯Ûi�Xv™05‰¢,€=(È5tpJuÎ=ú(–eåÓ4±ì©Ô‰â„Òðž*›î—ÉW�<ì8c…V4uI’p§s�µh�ŠÌÚuJ�SxÆ9¼°Dê-#Ò>†SÁ´mLKC7M4ÀÄC$=:¾Éí{›LMNðì“)•ÆØhÈ”rð5ZmØ\ë±vçí�‹78ÿƒ�ÕÀâöwPñk\H“NjsÌè3e XMªtS›y£GE‹h§Î‘Ä¿™àvÈÆU%fßMîˆ3§*8 ‚ §fTˆë£TÓÔü3 ˲öQk€:wñÅëP›Øq j|ÑÇkšF§?`»ÕâÉ çyèÔÉüÚŠk™Êĺ®›S*�»¨Š¢ pâ8Î@­¦MµìÀ0k·×xõ ÃÈ+5{U"Up«üêgnßã‘f�K«Øê ¨ExqB+Š�€=Ìâ©Mm‘^SJIß÷ùÌ­»<{êƒ$åKw9Ó¨¤’——×8UÛaQ}·qç`pÕžT¬$*¼×óS\îš S¶[m$‚J¹„a˜™L|¡¼dE÷ùÔ(Õ±œLÕÍó=R ¦]Æ‹tdš ‹û¹Ê‹fê073ÁäÔÍfǶs1(Ë.áG‚0Ê8ÉU’Aµô‹ßa’$÷}oBˆ|­RLžçq½ó6I˜R+OáTçñôsø}‚et-Åp«ØŽ†-Ö±-ÛÑ0,‹H6Ð5ÁÝ5�(ÊæüS/�Ž‘M׈)ÇßàÞ½„^7åöå7Ø^oqüÉåØcϱä[Üû.·#ŒZ‚ÆVêRÑBŽ›]Òd1ª1¡{Ì=:©MxÄwWp«L=¸b™T—£V•HUYF�WÁ!ì°Œäùâ®�ô†a`:.o¡Ä¿ÝvùTÇå϶LfâÝÌâ}|3A樃ªT*´:¢áNÚ¶mÊ®‹±�ZÓX™ØÎ ;ï T*!…AÙµ‚CY ö{*ÀW=”j7úÒÒ*øÎ5>öÈiþð�÷‘C6V IDAT8?3Åçn.' ¿÷Ö,ËÆ‹bŽÍï.!LMÓxñö]n·ÚÌת|qa‰vÑ #ü(bÂÐù×o_e#ŒxþÔ þáW^åXµL?Ž1u_n´:¼z÷ÇJo¯oñîV‹cÕ ë�Í^ ¿Ûêñæê:«}�{½>§êÕü}«��Œ™oMƒ�TB¹äæÎÕÖÂ]Y[Ȫ]ÊÀ]¾×ÇÕú„„Qš…agúäi𢉽3A¬ã”†TD½"ÉZd$;Á�ëºyOâhÖ}œù£6Š ^©1A�ÿn»·Á½ô}fŒ“H$7ƒËÈPPs'°+óøú£ør–(ˆH‚qÆeüdšÀxYy’[‹[Ÿ›ccs‹ùcÓ»6{ù)3Ú/'½BªO±põ*�µeÎ|ðo0÷‘O�Ä:ïõËßåv„ý,f&µ¬4¶ž”h¥.sF�šÒJmRÆ»öoUp »³x‡UbŠÕ³âï‹þã(U,eŠSW±ض�iš\ò-®†. ‰ÍBâd›A¿{€m¯µä¨Õ´ÑñJ Õ´,nÝY`cs‹‰F×¹ŸÎh4Ññ —û²Ð20²M#ÏÚ4^øª]n´¢¥¤XUpûËŸý �L6yg}“ùZ…¯Ý^äÝ�-¶£˜ßyã2�Mcèo¬lðÞv›³3Ó|ñÆ^Z¸Ç±j™ëÛ^YZc®^å‹·îòd³Æ¦çó¿|ùe~âñGy{mƒÿïÝœjÖ©»W6[T+eþôÝëô}„à³·)Y‹�]ãÝV‡vœòÊÒ 7[ÞZÛd¦R¢4ð“ Àп¥¤ILDhºN¹ä"4�8I±´è¾$@ ]ÊåLÑï·°Ä€0JÐMÍ(áÇÙš®k{×T$¦õF×qèw·ˆ#ÄN»£eYX–•gµcõ(~‡ê{“UrAm*T6xµ{�m±ÌŒyŠ-� ¨–&0«áé�âGeR µ!ñH£�(ˆ CƒTTi‡ L§ÎÆÖOž?‡=¬B”¨“RRН`i«øQ…믿ÊÉóO2qá¯`•ê$f�˽Ú)~}»-E°�ºX"á¸Ñ%–‹q�ª2kôóJÛ¸¶gp«l4@*ªl©FxU"Ý HT .‹»×£ô@%IBz$‰D&ÿ†à§fáÏ×ágçáÕ|È9|rŽ™GéëT“;Š"®Ý¸Åû7oqgñó³3LONŒ… >(qع¥”Q‚e[8–™Üã˜zÅR§r¢Å,Ì�V‡õ—×6è†!sÕ*~óµ…{4›G'|áö"ÛaēǦyqq™í��-$o­n0])³í¼tw‰c•2–¦1_)qm»ÃVßcªìòþv›ÓÍ:_ºµÀd¹ÄݾÇçnÜæx¥L”JþìÚMÚ~@+YíXx´ƒ€¥v—0•LU+|uá>>›èEÝøbiIYšÄaˆÐ4Ê%— ?�Ú™XTpÝlyÛºD`é)" ³r–á L?ÒHÓdWÿ–”‚d˜ùM’„$h Õr[[[èz†N4Mslª׈0e›Ðïbkðƒð·æ¡¢‘šc�MæV«…mÛcé…ïAJɇ_xǶ9wöaž¼ðxþóqÇw»Ý]Zë*@9Ì4ÝÀµÍÜA+­ôqÍÒ Ù¥×ÙDÓÄ.v 1,_˜Îh­–»}^[ZÁÒ5аmlC'Ib‚(¤Z*aé:q’0W)c:7·Zlôغ¾ ? <;?Ã?yñüð©yÒ4 JCøøÃ§ysi••^S×xzv†“µ*qšÒ 2‡?QrhØU]ÃÔ4jµZ¾IQ¦ÿ=M¦„þ€V»ƒ”PªÔˆ´~b3ˆLÜRvœA¿»›n0õ”ªåc$ÛD~ÓvV“^ä%™3S=?†7èaj;×áÙ&ªÓéÐjµòLy½^ÏçßaVÌôû}Z­V.%ªhó¶뤤œ1/²Þ¡KÆ<‘Ó«¼›¾ÌÕökxÁ€z½> –wæ¯išl·Ú¼ðìÓó4o¿òeÄÙØôíïÕÌQ-�:WÂ):©ÅcÖ&Žˆ¹Nb‹˜Ç­ l1~ tTǧiJ»ÝÆ÷}ªÕjÎŽPôÃ�W}íqÙ˜xxÝ5¼Avž_y>1 ?w Âñï[KŽzªméÉ ç0uƒj¥ÂÓO<~$ià0 ÙÞÞ2e0E£5ÎX4Ç2цϳ^¯çT–c�'%lá{=*• µZ-§sTfë:?yþ,Ÿ¿µÀæÀãúÖ6%ÓÀúã( Ñ„@3L2UIÕ4)›©”|õÎ"5Ç&,‚j\˜žä¥;w©:–¡S/¹´¢?I9Q«r¬ìò•;‹hBP¶L~øÌI,]gË÷‰ÒS×p4AÉЙ(»ˆa�©mÛ¹S¬9{Zšàõ»tº= ÓÀrëø²N�˜¤R€ža8’$Aƽ][IÛH¨Y¶‰ÂÛ©Š½È&Nwþ2ÁÉÁèiÔÍÙn\3F¤Ù{ït:ù|©V«G[‡‡÷«¾·R©”Ót9 oyp‡’¨Ñ4�±Þ"Ä#%¡%V¹š¾ÄíÖ{ø¡G£ÑÄ)Õ‘z ©•‘Â+këY ®ë<ùøù”Wœ¿QíòÃÈ7~—Ûwà�/}�Gž~–W>ýE&.¾@0û›¡õ=…yØÏR‹q�;Q�icÀ³Å�°I'µyÔÚbJŒ•wÖý×ý“ÀػՋU,s�Kÿ¤^LðtXSõH èÿfÕaÒ†óex·ý>1¿?èl¯k€£—ɲ>ßì8sú$ýÞǶŽÜ´?šÁ í5^’�·¬aLÉ�‹ªÕ„¤dFèšÄÒ³,®nº9ËEš$hB0á8œ›hÒ(9œm6¸ÛéqªQåüÔ›�3õ/œ˜ç¥Åen6x|f’†ëàê:5ê,vzÌTJ<>5Á„cS6 lËäÉ™I&\—Ç&TL]hô“”§æfXï÷Rò'ç04�¶ïó}s3Üítq ƒÇ§š4l› 'ûW³-¦km‘”r_²ú¢é"Åd€¥˜–�QÏk¸ŽHÃÎ>Ô_ k`ë1Q'Ó.‘`ãG)†YÂq²~¾$hc²ºƒÈDÊ.Y5ïŠýä*ƒt3Ǩ"‘:Žjˈ㘕Þ"®(cê&·“ËÈ=:\ѧ•¬áûU£‘gò“$áí«ï"†×r|v–½¨ÔŠ0Ó4©˜mÂÖ.¿üù©Ÿfz~–[ïopìã?Æíöi¢ï°PÃ7kÁvâbˆ”ãf—�¥¸Ê1c@S÷i¥É- š¹ýä'?ù«QºòŸ¿b·Ñ4m—$óaã‹ÀaµA:ÌÿZŠmfÀMüË•hÀ‡ð^.wáoÎÊ#ebä: ûêõ:Í&›››œ<>�ëºGbé)–švœ5D7-*®�oTËŽ’*>l pÍÛH0D–Mº…=ì �¢,h-Ù�6jœn68;Ñ „ÌVÊ<:Ñ`Ëó¹83ÉéZ…Dhlû>{ä4eCgÒ±i:µa{ÆÃÍ:'ª¦\CÓ©Ùç¦&xzfŠºmsaªÉÛë[<5{Œãµ שּׁñüñcþ_30mîdz Ä_"ë§þð[Ö#¢–H˜7»ÄèôÓƒ¹ÒÅÆÆ†<*?kQÃ[±<ˆºÌaÄ݆–R²Âõ¦¦&˜?6ÃñÙc‡gI©zŽYª°µ™ð�Oÿgžù8KÛë˜øY–S‡>ËïUHN ÅÖã2ë‰ËYk›T Þ '÷-�=(Ïm¯×ضí…£»¨õ "2Bì’öóš�T¬1lãÑ ‡¿1ÅqL�[ƒ?|š\Òu Úâu(?¦>{­%š¦áù÷VVY[ßàÉ ç©U+ÄCU‘kýA”ÎTÛÜ~<ÃBÓi62’Ê€«ñãpÅkBR¶vzþƒXÇp𸾇iØN¶ŒnRöò©ŽãäÝnwWegÜõOUQUrD�ƒ�Ò.¥®C™|9ÈŠóÉ�-R½„ë¸H$i’y[¸{È¢�Z*^d õ¬�Z#&ÌüÞú.*Ç~hïSA*òû¦išsÅßwÝÁ†Q‘Ó4©×ë@ÄÛ¬R·&y?|ƒ[{?4t,,is‚Ç©W2?ÿö•«,.-S­TxêÂù] ¼½LJ‰eHJ�ÿŸã¼ò©OóÈÅ ÄÚ<-ScsêGéF•�ñ½lU-äQk“Áõp‚cFŸi}Àí¨Îj¼?gºX__—RÊm}˜s�r‡¬[ 6ƒÔÁ`ð@Jg{w—­CÛy±a¢ãV�eNnÐÇÒÂ(%ÁÂ-•ó`Ç÷ýœg\“rGåã¾ SÚ�.·îRr]Î>t]ÛípŠã¿¥"šI¥r°n¹Ú(•ŠŠï¯ÔôB‹jm¯»†c¦ø‘†ÔÊ”†ïÀ÷}^]^ãщuËduàs¬”•{º©dÒ±éd&5Mc=Jxd²y(ùyQmEêR¤iQ·­±¦¢CM’„Á`�«Tp8:^.,j�N¥ Ñ›X¶CFX†FÇD¡‡­ùèâð¹%˜� íÄøþCzØF‚’~h‘R’Bì(ŒÊ îlhµòqRîèÈA@§ÓÉ¿£ Øð—Y’׉Än'=/Î2c�d�v)‹ÌkšÆÍ;wÙjµ˜h685?w(�½”’²¾ÅÒ¥?'ÎàaâÐàÕ×îQ~æynÏ’|�ÈÆ5Ì=NÚ©ÍJ\áŒÙBɵp’AzÿÆå›‘ßÕ4ÍÝÃ8X%Iõ9ªMâ`08t“»Ÿ {w£þ#‘—“Ö£lS¥‰˜ÓvÊce#'˜?ªÒÓèuìµ–hºÎ½åU®¼w�g.>�ç{œ:>Ÿûùbþ7³)LÄ. ¡a»%ª%'ÿÞF�;º†Œ©¶ã.?8•¼~ K ñc ËÉJóêÙ-{>Ÿ§§'Xój–‰£k¤¶ƒ¡ëˆÀÏSÇq�†Áj§Ë„¡¸Ù—2ñ)ªBÚ¶M;I™°Ì±¤ˆG“ *ëyØ{�O^db—'3itMàûdâaiÑ}àß½,L4‚Ä¡Ñl')ÞÀCKz8fö¬ãT£œåSí=ŠÛY½µÞŸ×^²ÄJ1RUÖ3ÇJ�eqƒ˜Ý붆ÎiíM3K"¨XB× ^{ûÛÛ-f¦§xìá3‡n¤”T¹Nwí]3Óhļñµ÷XZÙàÔòs\oŸùžc©9ª•´ˆGÍ-t‘r#œ`bÈ‹{7ª±W÷¼?ý—~é—>Y Æ�WäÀU£ ³"mÖa6Zf˃–8Â1“¼¬�JA,Ê8NÖÊ Z"@1B†ô©Üáf,NÒq¯Cµ\Ëdiš²²¶Î§¿ðe;û0%סY¯í‰ ~ž×¢�R°ÅI‚fXT\+ÿØö»võþTÐSluH¥–�£†—K ]7‘ñ]ËJ=º–rm½�iZخÿøú›|ìôq ÷Ï¿ÈÇ>…fÙüò_|�ŸeÝËÞ×»[mÞßnóúÒ Û¢R.QvKÜØnQ±Lb +ž�©ë,õÌOL¤)××6° �ßû*�ÏL±ØîPÖ5V˜ÙZ•–糄Ôlë¾�»V«åÏT)àA�£™Õ{/>wMd¼ŠêGAbQ®ÔÑ5 ¿ß"ûHaã¸%"ié¿zuQjR*׆šf ™ÝL’¦Äéx�ï M=Êï¬@Bˆœ*MYñ[Q@º"þDeŠj:�Œ�è#ÉÆž6Ÿ`3^ævr‰v²A"c´Àáú­Ûܽw�çŸ~r,C´o|àÓïtèl¶°gž'~dš»þÂä/o¦ hýÔb MæŒ%-âNÔ ®Ì ùGù¿Y@Ù¸"EöõþG[­´UªöÒDšõ+ÿVÊ �~¦îòˆ+™O79ït™Ô=âÐà S ÃÌ[nŠëÈa¶×Z¢®C•a_yí ~àC/°±¹Éñ¹Ùû2œj-Qãt-R �œßT·¨”]4Á¾-Pj P­£ÀeÁÐ ÿ>J¶[%ôûØF‚©'¤qÀZ?¤KŽ5êüö7ÞæÙÙê–Éo¿y…MÏç'çù�—^#Ij¦ÁòÀǶLþ÷×/sÌuøúÒµ*“•2«½ý8¡dšl![AH?I@7˜¨Vyo}“8Šxmeƒ›ÛmÂ(f¾^c-ŒI¤ÄÒ4ntzTm‹Q’2ÂNõì0д ëŸUAk’ 4«ŽãØ$qD¿×Â4,§‚$iŠ®%öX ÂPl I A$¤„‰qhI^Êû)H8,Žã]T‹Š*MYqC¤2ß D?Q™¦’L@,ðÄμ±)sÊ:Ï­à2kɱŒ¥ÏÚ½6‹KK›™æùgŸÎÏ¿Ÿ–Rb ¯ÿ+ÛÜ»q‡Ù“ÓTý«pnŽ…ÞcÄߤŒí÷‚e\¸M=`ư’TH¤Æq³:éýL)÷ñÜî¨*'Yä%-–?”c(*�I¹7¯ß~¦‚ˬ\[Á!Mct! ƒR¥‰¦i úlÝßiWHu¢DÏÇ !†$úûs3dETëÆV‹KWÞå‡?ò!`™æž”3ꌎ?ŠRL±8Š"LÛŵ- ]»/¸Ùoü^Î9‰£]�]*@CK4M‚OßZãNÛãó7ïP1 ^¹·ÂO^|)%/.Ü£V.³Øé²Úíq¼ZáëK+|þæ])Yé Ä1©”üŵ›¼·¾ÉFòââ —×71„à·^}DzX÷^^Xä�Õ5Ö>/.,qnj’W—yõÞ :’×WÖøÂ�%¶Ã� ÇÞÅ‹«²Û@έ¬žÛîù³›ÛS×d c Ómæ2]f‹‹H¼0BÓ-œ¡sM÷áVÌœrFýfÚF“>A$1-t‡4I‘cÊì Ò#t]σ\µ`Žnl”€Æh�[q“Vv+L•f±£ Q‰€ 1‹«UØNW±t‹›¯o³±ÞÂužºpǶðý,TòkŽÎ_)%y�ÐïróÒ%zÛÔçg£7€ÆyÖÇà{�jæAÍ—ÝÔfÆÐÐ}nGMÊZĜѻ�žæ› n‹>ø ÅÀQyØí¿Ó4¥V«=�Ò™â7­ÕjH)ˆ¢8£^CR·‘K-› ÐDö¶%?yÖO1+¨žÌQ*Sk‰çù|ú‹_æÂùÇxïú ]gîØÌ¾mK£JgGY‹T ­øM·ŒÐôûö »×…-‰‰©ËB[‚�e;wѵìRð[_¿BÇóÙò|^[Zãù“ó4,“÷¶Z¼¿µÍ3'�óù·9Y«ðÖê—V7Ä ¯..óôÜ /ß]âõ{Ë«Vù?/¿Ç›«L”þìý[¼²¸Œ©ë|æÖ]&Ê./߹˧®ßBA'yse [×xmq™Ë뛬úwÛ]®oµy|²¹ë^ (ìn‡8ŒBT×2*/€0µ)U² Ҡߦlú¤q€¦ØŽ‹nº±2Aß'‹ë'&åJ!^¯q�8Íøqcl¢øàà¸hjÞ)HUë]±ª]´Q®ve*ÙU)U™pŽe~Xf~xR§ª5¹—\ǧ‡›+&ÓSÔªÕŒ6 ¹Ëì¹YPá:—¾ú%N/|àÞxû>}’’{8²·¤>(·¢$CÖ †e‹U:+—+„qVv�Ô\dciÃþM¡ñ¥…u~쑇Øô|›šàÖÖ6ß?;ºÎÝ^Ÿ~±ÖëS·-¦Ë.� `Å<;wŒ²•©zýÇ�>Ä[«ë,wûÌVJ4lÍÐùOŸ8Ç¥Õ þê¹GXìôhy5ËʽIÂGNÎsimCÓøëça¦\BÊ”µþ€§ŽÏQ³­\¦˜µÝ/à/f¿lÛÎh³ˆ1õ4Ó.—.¥aŸqàµ2 ^²^0KËäxý0Ųmt³„g·»ZdR‡R9SÇóúÙ1t ,-&‰Ã¡suÑt}‡X~Œy£@gªD äÊ7Ê©³¶éÔ"S/7™4æ°£*kéæ4†´¥ÏÉòYnß]¤Q«29ÑDâ¾ ©ºL’Û”¬]ýËw™}ä"qbO?Šþøiî´Ï? \â÷²…R§�ÚLè>ºÇb\ÃÑâûèi¾UT`£YÈ"Wx‘@~4‹T«‚ÔqdlGÇÇqLø¦‰ãV †ä÷è.¥!å]äµ0õ�o‹2Ô{Ñÿ¨VÚQ~l+VÖ–¹øøyî..ñÁçžefjr¬Í~Q©ì¨k‘‚(ŽI¤ ZrÐ4‘oüŽ’¨(zA˜€Ì‚»MÓ‘‰‡>ìàYDô#øþ³\ßjaé:1G³Vå�•5žhò¹·9?ÝÄÒ4Öû}&K.SåAœð}óÇЇÈMîv:<>=ÉtµJÍ4ÑüÈÙ‡y«ÍTÉ¥ãùô¢ˆgf§i86÷†YÚ S5Ò4¥Y™dҘà ˬËʢ΄>G;]§Éßxõ ÷–Vðƒ�G‡í£> ¸Y(›ƒÕ×™:ów®\Æ´lžúéÿ‘–«½‡þÒ·†�ZŠ 5üž0»tS‹Nb3ov1„¤�Ú¨•v_ž[!$+B“žbZNNÒ|Xqt÷z9U!$e+ÂÒ"¢È'ŒRÊ•–Ú9õÚXzd–j„‰1rŒý•ÎÔhœ2™”’ù¹Y,Ëbr¢IšJÊCàÙ8¶W&õ )Æ]¦›TKY¬Õj勌Çl¡l§UÂÁv*ø¡Ä²ìŒ³RS÷!©ØÿîÝ;<7?Ãr·Ïù‰aÓ‰�šàD½Š�’ºmñÎÚ&A’òÁ“s¼¾¼Æ±j‰‡›uÖÏÎÍps»ÅTÉ¥lê<ܬ³ây<6=…EÜÙÜfÅL—\¢4¥lY˜šFÅ2ùÊÝ% MãN«C/ˆ8ÛÈÊk*c«Šò³{=w• U;_Ͱðƒ„$8%Ń˜ežF¥&$¦ˆHâ�0&ËÜè~$$D‰È³WžçaÈ~η(DÖæ¡‘âùÙ{²ÐH“øÀ6‡¢9Ž“;2µIS%@õ;¥H´Ÿ!Íê$š„ALÞÂ.'��Ê7Lb~¬ IDAT“C�hq¼š¿Å Â`å5–o\áâ‡^`ùÆU&N?‡þð Öƒ)ÚasŸ«ùöZ’¤ôú›=Z­>õzé[~ŽHêl'.u=`VﳜTÑ…dNïáIOšßrž[MÃ'|ÐÌŒ)„8<;ºÉ=ªˆLÙ ÒÇ",§„í”±lgX=ëaé^î‡)ƒÑ-ܨҙâd·š¦æðܱ&†à¹�ïá ÛvÆ 2G3©c+•iŽë`[FžÉ}I÷�¥FŠIÆhz–iÒ¤Ÿû�šmñÕ…u®nlóC§�$)'+%â  ÆüÈcsem�ÎÏ¢ ÁB«C?Ž9Q­Ð &K%,]ÃÐOÏNs§ÕÁ #žœ™Ä²LlËâ‘© ¶ý€ªiðÎê:Q’ð¡“󼵺ÎDÉåÃ'çù‹·¹±Ý¢îØ\ßÜÆ5tž;}"§ðTøØ?(ú�bËX8¬DÒ¡TÎTÒ‚ÁvždPfh)¦ ÀDê”Ü ‘´ã�–±¬½,ó[ƒ~j–�£F„ÄÝÄ´]Ò4ø×ÇqœWÔFMUƒUuä g0ú4M£Y�¤.§Dªf=5™tg9}ü7ï,ðCza×±ö‹#¢(¤uãs,^¿Ak} ¤Æñ‹?BÛpÏ;M?þÖûÀq-IR’$EJÐÖ¯?²IíÔA’9£G ¶—9³‹-Úi&›.ÖÖÖäž=u¦‚8H£™ï|U9xÜ KTÒ^ÌÊF�?q*Àl⺥œC/û8F6‰û¡5V?�RQÍï ™~P�nYµZ-óŒ‚ÆF‘ëã<‡±Æ Ûq©–�ûš×•´ßQÀ†–bj1n©ŠeY´Û- 9À2²ûƒH:9ç¢ú  4MSz½^®’¤…4VMñÊ (‘µ‰h6›¹î¶ïû„aHš¦9ñ·ú;õ7Jb9Ê Ú¶�;€ííí±ËŒIÖâ1jPÙÜAg=d~l’êeÛ!Œ²Œ@c¨’ÖmoP2ï/Sv›T*СÀ°ltÓ" |Ò8:йQÌ›››Y 1Ìž[F»‡™Z”,Ë¢Ýéòò7^ç£?ð!tMËKq‡�wÍ�ÕKÌ�w¯0{æa¦ç縱à#Ÿ»ÀJçÄ{€¬¾véò"®kE –¥sòÄ–õíé5ÓEÊÃf+ËàF5Ê" aÜ|ülå�<ùúúú¸OÓÐRÊV6¿ÂXC˜uJÃ$C§Ó›a\f•½Î àE:†]Ï9u;í- ,#«†x‘I”쟱/‚½Š ³Ã¾`7J½=²*ê¯��€h¦C£šU’¶¶2Ôû¾ ³CL ÑDŠi—ó�ªïõ1d/¯D¦R0ˆ]e•¦iù¥XSTUÒó¼ü*_Z'©gäÁXµZÍ3èív;ÿÛÑñ{ý·ò�J޲MÃ84ŒÙq®»#Më8ΠÕGOZ‚ÇR)ðbÝ*c™&~�FÜR5_Oe¸½«’ Æõ›TJZÆS.4â084ÙPí*ê¯bÆZ­wAª:j 3 è ”Ü�z�…ÅEÊîÁÒ¸*žqµ_þ?þ>ÇÏ]à‘‹ç¹üÒ›”Ÿø(�{íïû®€È¤„ËWî¶m`š:ç›û¶œKÇŒ§Œ­Ô¦—Zœ0ºl§7Ãú¯ýÚ¯}rt*€’¹ƒT ›ò°/¦Ûi‘D!¥relÐÂAeŠÝ'BbÀOÅswÚÛM§T®&^�"Ç�ÅTA’*÷Ž :S=«Ez”¢”âQ5ÛGËdªA}t“ Œ ¼÷ë–� 8)škF˜ºDÉ0 ŸÉÑJÍÆ R4Rüx¨ý=tvʱ©]b¯×Ë™Ô")¥Ìßq°+°-r*T¿ºv•EWÿŠÈçâÆCÍ™â9ÆÍ Wìm:¬T2Çœ=GS“l‡?×Ô¹¼9 éZèBð~; bYt£˜Kk4,]3H4�mÏ'‰cÖú:aBÃ)¨�%:áp¡BHÉkK+¬úÌÕ븮³§F¸4�Í8e²\âÝÕu.­®cè:–Ló~\}¨)¯Ï�2ÿÔü1 ƒ;‹÷¸³°HET+åñÆo¼Åöê]Î=ÿaâ`ÀåW/1óÑ�±ÌÆß�¬-@¯ðЙiâ$¡Q+ ŸÏ·§="ãÂu0eÌœÕg;vHÑ™7{üæoüãÊÜþʯüʯ¦ijîšdß®òÃabæý‰ÝnwW�uØØ«L~PR†žæÅD¸¥ZT¹n)Lü &N5¢äà�D±å&Š¢¼W ì{RæÕ_^ÌbU*•Ðטß@±š¡üÊž‰ÍȨþL=—Y-ÞÇQ[> MR±#H9*«+4†tFYÙß´CçbÒÅÔ#|Ï'NÅž ³ý¬˜^×u=ÏÄ)Çàšq†Ì"Nv€?¾ïaÒE' ï…X–»#wz„�ûh™LZê8Êöë¥-s)޼£‚Æö-“iY»cí]v.–Ü�øAý¼º�yvRˆl‘2L‡N{¡é”+5biE÷gíÿî’ðG+[|­ Sž·'"xÔ$(½q™Xã h¨�ìd,…CÀËÁ¦–¹kÅQ»ó®²6 �ùæ»Üëz\<6ÅßýÔ‹<6Ù¤jÛüòg¿Î3s³üΛïòô±I>sãÏÎNs£Ýã�åU^^\âáæ$^€k\oytB Cwx{cC×):·;=®llah·6·(™:7{ªn™~¥)bE|öÎ=þÃû·øø#§ù�Wßà©Ù&]s¤E@�ÎŽŠHWý»ßxëq’P«Vyæâ…¬—ý�ïWK»¬\þ4Q³|ãçŸ{–è±ÿ‚›þI‚¸cl4¿]f™:Ë+mlÓÀ0uª•£©o�cR’—Ú~BW¸¡1oe}·�4øç¿ù(¸ýµ_ûµ_µ,Ë,~ÇÚðÛUÕ3Ýn 7‰Äm’È'ˆÊÃ@a¼¶¯Ne¸_R]�¥'8Fœ š¤R �Z.µz-‚`€Ð JåI*ŽØUýÀ£ý¸ª×·hJ¶]±BïC�¾DÚ}¿ñ;÷!Ð-'lØ c¢Æ�ãag£¢ @¦V…n·“e3í ý@ÅÙ¹G­V«±DüÁâ:_îú kLÈ„¤ÐÏ{Øœ÷ýAÎ2£üó^Ï~Ô¤”9HÈ«ãÊÉ[z‚©§¤2{©¬´¦› üMüÒç^ãçf¹¼Ñå·_Ÿ>=Ë{[ÿìµkÌ×ü«·ßçëKË<\+Q²~÷�Kœ�lò¯Þ¼Ât¥Æ�NŸ)×f�€”&Aj¤cˆþÑ{7ùÚÝ{¼´°È³³Ó8¥‰Ìæ¤@�Ÿ~/NøÝK漣 Çæ}åUº=NÕ«ì0‹AgGeæ�R„!×nÞâÄüµj…j¹4VaÉMÞûÚŸpê“l--ðÌ_ùq¼3?ÉrÒÄ�&ønz…Ï©TLSg¢YF׿=ë‚”�¦’Í>ô¤Í¬åQ5–ã “º‡þ‹¿ø‹ŸTÀ5ɳ]d¦ÛI7'ù­œÊD�‰]ˆÖq>Ø›vKÊ”4ÙAåDìqn‘}$]/!Šâ#3(Û t†añúÀd!qXJmnû‚R §÷OØÑfñQ*o ÕRÖp�nùhb¿M†ì’”M¤‰ÐLt9@'À÷4þOéLóZßçg^y—¿Xk³èGüþÝ ¤aðÁZ)ÏÂì×S=šuQΰxíêoBuuËU)Nµ7ض�—GKOrÚ™05qË�a¿`u£a;\ZÛBJÁÃÍ—×¶SI˜Âb§OͶ8^-ñC'œ�˜¢Zi°>ðø­¿Á…©&yè4ðö»l)H�Ï޼Ǧ²ÜíÓ #jÔØò¾²p�vðøP þúÆ^ó¥…%îvœiÔq5ÁsgNñòÝe^˜�âêú&·¶;H$Ç+¥ü½«{Wˆ^¨–Žƒ‚Ü4•´»]f¦¦ˆã„’cÓ¨ÕvUSö›¿V÷‹ä8I¢±ró�°ÉÆÄóôc“ïf` 0ðBú½€0Œéõ&'¿uTdI"ó€ ¦©aYBtS›HêÌ™=b4þÙoþ£ nþçþW¥”fñ=h¢Àð1`´õ,Ó'SIo�µŒó•©Dèï�ã׌v)õ‰E¹še”½®fÔ‚iV ±,;oW8JpYd·)&Ôµ+ú%]×÷í//VÃÀ¶�Ñ0nm·8wöaš�Æá•ãø/!Ì*o~á38®�œÿï'“*ßm¦šÍÍ>ë]‚  ¥ÒÁÃGµ$•ø~B¥C_¬k&�ÔaRPÑ"Ö“r¶" ‘¡@[­Bê�&z��ífNÏóH©‚§�±š¦å‡e‘Ô‹ë÷ût:�Ì1U'ñ—0±ŽåTöwŽ?úÐþË3Çø½»Ü“|�ìv»ôû}*•J¾Hª]­j¾ßë~ÔG¬Ô~êõ:®ëîzvEÝrµMÍÇq¨×ëy˜ReÓµaS»¹S~ÔRË�¸†ÏÞŸöÀã^·Ï;}ê¶‹&4¾t{™‹Ç&iº6Û^@ŠÅ+ËÛ¹Žü�?zŠ–?àÞö2•hšÎ3Ç�Ót]NÔªÌU+Üëì ÙŸ™›á'Ï=Â�­wZf+elmˆJOb¦k5,· B ‡ ÌOÌñçæý­Öžs!Z­ý~¦Šæº.�F&*±Ÿm·Û\¿}‡k7nrúäqΜ<‘'MÓûæ�2-ÞÂÑ×Ñd‡“g>ò‰¦~îG‰©øNY¥ìÐéfÙÀ¹Ùú·ä˜I"éö"‚0AÓ•’�ãèŒ¾Šµ¤Ìõp‚²/k³—¿õLÛefejËɾ+ßó0EX'w�U>`ô;:è¼êý !h }¿éH E'Á"Œ¢ ´KD eF�Ôív±m›z½ž#㘔™hÄööv^ê+ªð£Wê|ôr�ïÕä#¯ÀûÞþóLù›^¯G·Û½ÏŒó €|¼eg]–aäe÷ÃÆK)s?X*•rV—üüR@Á+ß© �m¤¸ú€ ¿IÔjµ<ˆ"ë/þ×iZ&¿÷ü9þðûÏó_=4Ë¿^Üäz˜îZöú†¥””J¥áZº[QÝû`0 ÝncYV¾Œ>;uM°S¦W›ív»�ïû»®†­5FŒ;¯ˆ·”ýÞ÷z”­‹.^o›?¹z��Ïß^!J>5Çço-#ɲÝI*15p “©j¶Ñë‡!%··ñˆí0Áµ\~êü£\Yߤb™øqLÕ¶òë15�’iÐ #4 ãùÌ—]>uý6h:B7¨•ËØ†NG4,“¿óÜSÄiÊÕÍ!õlƒ[[[y&¼T*Ñl6ó¶Ç½LÓ4^yýM^{û?ô¡òü³OaFþ6›M,ë~Žw=¼…¥­óÈã ~ä?ûkT&O°¼áÝç—¾[fš:Q”Ðnpìo îAJðü„þ «dÙ¶N¹d i"¿ï~jr5˜&”:³zo‡-!϶%!aèã8%Jå¬d•$ þ`§/&‘/2sú­ÑR»"q§e@�©'Èx@0ܽ Ý%‘F~Œâ¹‚ØÈ).Š­j÷ªh“ÆÉ ÂNд�\ó þ»Óàèðá&x±äIû~)¾½î£È�8š g¼fXTJvî.“nE 6U&2˨K�Z–™0D�ÿL DQ@fýP*@ú{Woó‘©:‹~À7Zœ);¼¸ÙåbÍå±áÄ-fba§WY•jÆAõ«ld1 ,¥ÜÅ¥¨$žGÇy›†*³ÆQŒ.b4^lS©fÇéw·±‡`D„ÆívŸ¿uñÏÍNq³Ýg®^ョNP1 Ž•m.L×Yh÷ùÒÂ:OÌÍ1W«Ðë÷™p==Ã+ÛL•\‚(æµ¥U&Ê%šlòúÒ*óÕ27j¤Þ\Ý`©Ûç£g޳ÚÐ CækšŽÃ”k3_²qÝQœÒñ}ªUycu�7–×øàñYšöþ»ßýZmFéRJŽÍÌpîì#LMNÐéö°Ló¾ã¨~ò�ï¬ÁK8.M' Zæ“T'™0C¼TÃ;Lô�0!ž‘¤)ë]¦§ª„ÒM‰ç'„QŠm騶Žij‡.ž4é¦6ÿæŸü¯”¹-òÜFQDøX–�ãVH¤™¨¼~k—/ôc3Ô¿££²#¤IHúØN Ë©àà…ðÙ/¿ÈÝÅ%&›UÊönÅÈ8ÕvJ¬c¶Jíej|E|½oñHYаà&¡¦ÃI+å˜6^�Yd÷8*ϯ”’(�JÉA×µœúé(ãÕ\eҳ矎TÐ,:"õój¥¡Ktáy>B3¨TªùæþW¯Þæ£Ó îx>_Þhsªlóâf‡§j.�ˆâ7¬ÖÏ"€n?àSÑ�FQtß³S-jÀžtÅJ@‘[X¦Q^=“¤QÏû„Ó �¡e�k?NX÷þöųl ša2S*ó•ÛKü�‹gYî xvnŠ×–·ùÚâ&Ç›uê¶ÅååþÛï;ÇW–¨Z6?àv«C"4\Ó$ˆê¶E79]¯rm»�Ÿ&l<š®ÃæÀ£†|ì¡Sü_—®ò³OߢâÂ-iQ&¿[üe٠ѵ/2¨Ögò@±×mci>Qj§Zž¹Ýû¢–sµÊðœ©"Ø(SºÝ.zÚÍQ�‰ôCëÀó+Òå£ÊàÞþñ=‡Ÿ™Ïì•.TuÉßžìï©5}Ðý«Ü8Rš�Möj­Žet;Û¤IŒåTÆ?zþ¢œ²7èa�9QiN%cožâ(k"Ïüüµ—®ðPÙáÉz‰VœpÒµùŸ/ÝæŸ_<ÉœýweJ M3™Ýq39@.%¬ÀSÀ}ò³Ý»rÄAàú}œR ×ÍtÛÓ`k7¢Vˆìe©ÔñcÃÊøq}Ï#‰û8¦$Õ³lX†$þæ�óV"Ñè…6//­ó}sÓBôllË £K*¢–Õ‡©äß¾wƒŸ~ô!Cº3?I„aÆ—+äÞ@‘ýl?I¥àÖh4�RæYžýž¡:ަi :K”ºÿ/Vu¤Äëõ¹eýçTL�†«‘JXhÜñ:ñw'È•RòÞû«Äq‚®k™óâ;W’WÉ�‡±F‚�[®qká.o¼}Ó4ùÁ.Ò(©–#Aw?,厌íAœèûÙŸ,ÆgJP5àóðS“1Oê�C¹mG¯Cµ*A0V5LJ‰SªP-»9«‹BËõ>оÈ÷}Ò¨‹c$H�Ì6{û´$„²D­ž�4ìk—y¢VⱪK'N8åÚüO—oó/ž8É3{øáâ`YBdÔqã®cBˆ°Ažù… ÌxSÐèúç ú�z¤Ôê“hšF§½…«ï<ÏX ¢\]Ò�5¼8£þBÓ ¢>®ÆÍvD¥Òd®V¥ßë„j–F�Jú‘A*uþŸ«×ùè铜šh’Kíÿqçò#IV�ñß½qoÆ+³2«Ì 7b4š00²…‘lX±eeYòÊ;³`cyaÿ,‰˰b‡,#�,�`…¬°d!¼àÕÌÀ0žiè̌ˆŒÇ}°ˆ¼‘YÕUYYÍŒ8Û¬¨›yãÜs¾ó�ï<k$žµsT�e¬q$yP®�4¥1|çþoø§W>Ð󯑔MKg ÂvÌëç=w’ø _ÜëËçç¤+È#„87V=ô¢\Ö x1Ž¢ü9Sñ¿DÉœeY(Ê£O1ŽU? tã‡W"? PV ''%Õº÷a/½x3µ„¦µÄ�P.\4‰G¬V+6¼Ž,™îp^`åY>œÓ }R–KL„†� *ʧ»ÖÜ^�8" ò%u½Æ¶‰64F �èûÖB ›ëÐYëo‰œÏÜO+˜*Xø»gáÓÆ av]‰êâ÷Ø 2wUΙ¤YNž&´mCW�0RŽÎJZ7"Íú@ÿ&òkaýól“ÉuÕóæ‚õ’U‚H:›�õSMæó9?XÕüÛ/ÞD„CLÀ‡& _yé=°U¥­ðÜ‹¢¸Ñ¼û ÿ³ëPMT¤pý@†xLœ$ƒ,Y¹:!‰®—o3NÐZMœEõº’­byJ­wþöɹå""NÓAfËÙîr&”¨(b:íKé�Ã^­kÔh3N²kÁŽàöÓ78×ðìÃûp•Fð¹ûØìŸ¬þ!#ù{DãÚÚó?æã8`žÏÞ• ^]Eüt>"RWgÖï¤ÕMG]w4�a½nùó÷Þ¾2H팣iJ ´’HÉÁ{sŸ=mp{zzZYkSØÊpõ#ÇÇäãé€Àö‡\à€†ˆ¾å> !’ŽñŽô—u¯_ƒ¦íøË�ü¦©E}ÜEmö—Cg}š¦TUuðûû�õŒŸ”Š÷fpÖÁƒþñÝð73?H8Þ$Y†-ཫ¾‡Ò#¦G}sY¹:a$[jéœ4Ínt»cªªÄ› �L{Õ›è<’Ú n¤·�bêºæ»�Îø×Ÿ¾ÁîoþÊ4áË/î÷Ã!ˆ ÈøeC?ö™÷žãããÁ�Xk‡†ÞC®U‘$ËǪÇ1M]c/‚ ¯¥v#_i­¥Y¯ˆTÂx6åCF;ÿ£ê4Æ) cÈ£žV#£PõðÔu…·—kÜ–ÆIÉ,K�ǃIg{Ð̓ëZàðä ¶<åÒ„$+Ðf%ïºç¨µ&Ïb܃¯g‰)r_ü=¿hïñl\ó¾¬eG”�çG§9ÍŸˆ2f­ãW¯=äέ1Mk¸{g²W¹Æ{Ogz©¸¦±h%Ÿ* ½hj˜ž$<ɆÓXÍxÛYW±¨Xk¤Ê˜Îfƒ$Ëu¶Ë'M’„ÙlFÓ4çV-Ïo–ÖjÒI¿v±Zà]‹Ž'Ä“[=‚iÚkoz·Œ‚Ë4M÷"ÈÞ{žŸ(>û,kƒß„ð�w´­4oƒ6ã!�Z(Õ,—K´ÖC¦ö„6¢ÔŒ6<åuU�m´guäP²a¹tèMGîM´ûõ Õê1RgL7�uëú‘´žÞ)X'‰¤#Ëû .è?~|ó/÷ÞÅ^ý�ó|ôxÌžöZ‡JZóù|È>Ã!wU€.ØŠ¸ìŠ„!m)åPrÜwï©6ýÜ-°Zo%ÄPÔ]G¬ž§»kJz”l©×§ÔÄM{�Þ¶mñ®! Y÷@{É/¼¥. ¤R$IŠ÷1M½Þh+ö‹/DÄ×-øUÕñòQÁ?<{‹gb=dòÖtèQŒJR¬iéÚÃ4MÃ3;;;4½¡\Æ[¾Ê„¸ö QßGLîÐÔ–yüA¼é“¢…Ñüx¡™–†ÆÀoß,9ž�écZIÒ$¢íz~¡Ò�ø£×e¶X¬™Ÿ•ıFEO:ǶµtÆ3Ò½Óϳèm hß ô/[?lùÑFº©X‚­ Ê8:šHС�c�Oy•ÚõÃB*ÎV-­í¥çš¶ïÈ'3šzͺ+÷jÚgY–1›ÍªB½œ{¾ùîo€½4‚gT¨¡�¸,Ë錇 4¨Ó\êÃeÄ(N6Õ®-Z¤ðdÚ`Ý’³³†4=ü>v­,KÚ¦"IÇÈdëËØyŒ�í+G£ÈB£õhUþ6Uüó½»|ñµßoüpÎçß¿ßCTavƒóú}üg° ëxݽëÈ“�ÖÔU‹PãA5§ij®Û=Eç׬‹¡2òÉñV§¾Z‘ì¶ÖIŒ�ðxòÍßÀÛŽ²èP:&Ëò u¯ÞHDвÞIDAT‚mÖ‚Nk^­ w ¼ì=ͦY¹Z,zešQi†i›¾¢và祥�&¨ ýshnÔ€¿zøK¦Qâï �»Ãëþ=t^ðF�ò»&Ṹænêy|²¦hwn%XןªJ‰ž£úùß`Q$™LŸ¬Vý>{÷sç9çéŒ#Š$mk‰"A$%yööi“‹¢(|UUÄÊ’jƒu·ØÐ¶ f}2dXu§h¬ÚÉB÷gÁ—Ùnݶ5“x«ãh�ÀÊ)ùxÜoŠê„XYŒ46!ÍÆO�`ZN/ZV`­e>Ÿ_úyàP9«0ŒâP ™kžçÔuÝ—¹dÄ(ɘ曮×îôÜháÚ(¬“¤º£5edY~p™m‹AÙ%ßÞ8%¶-ñlK›‡sþãÁœÿ~¸à¿þêîåéÀ½lý[·n!¥d±X \ÛËPü݃Y Oª;£Q< ™°ØäÙêº:„¯º_€ÊŽ™NgÃáÊ;ëª`$ktt=úSu1G³;ýsp޶mh×+be�ÒS61öº!"@¤FäY†µeY±DðéŸý?ÿ·Ú–æ>r”óÕ½�´Ý"CÞ{„”Œó¼/•­k¼í7”è¾}ûö€žäö�÷gTü�Iò¡\WRØh’Wøu=æõõ“r[Æöü¨“y�s�Ä’<ÓëPJ¢"AÓZŒõhÕ Æ8TÔw½Oíx�é儤8ç7Mýá‘& ç=ÑæówÂ2ŸzÿGK��Ö Œœ>«.“hÛ‹ü·5J‡r÷¡¿e°8ŽÏ]Éž64ŸÊ˜ÿüÖ÷¾ßG_ùÏÝÎhŒb”ŒßxÞ‡X(U]ÚÕju)К’…,—[BÄCW>0ø‘§¡l]†dK�0�ä<«Å£sÅ@‡‹#ƒõšdSM»²wÁ0‰:'è|Êt:ë§,VKF²E AÕmùÓÓé­õ9*�Cðïo�òíG+¾ñ±—¸+û ùªµ“$öOØÔö¡Ø:²X'q¾—[ C Ú¶¥( ò<ôß÷�Ã�æbœ@Æw)¹tõ’D۽Â+`Ôs(VKlW’j‡žÖF¬»ýd<'Ä£¦m¨ë^)ä{EËç^{À[uG*Ÿ¼;ãK¼‡ßhÚBØ7Š$í§†U î°1ÒÁÂþ <ʲ¤,˃ߡtù ò‰a×sò¼)^äÕ*£Û9ƒÐÇrÙ2kNç JI&c�”¢W BÇum�R�Ä 8ð«\k­¸}+e¯�Ô‡1}/CÝX´–Ãðv[ªàùÈ=�wbOIEND®B`‚python-pot-0.9.7+dfsg/docs/source/_static/images/logo.png000066400000000000000000000103451525521106200233620ustar00rootroot00000000000000‰PNG  IHDR´�Üà� 9tEXtSoftwareMatplotlib version3.5.1, https://matplotlib.org/Øaò½ pHYsgŸÒRRIDATxœíÝ¿oÏÇñ'þ�Îrà›@’oïʦãGM"h0Å·C2H&Jû•ø?PZP¢à‚.…]}¥|h>‚Ò4I�äû5‘�âBÐ�ͦػïÙŽõygvvöù�VZé†enÞ¯»±agö\QH’Ôv3Mw@’¤:8¡I’²à„&IÊ‚š$) Nh’¤,8¡I’²à„&IÊ‚š$) Nh’¤,8¡I’²à„&IÊ‚š$) Nh’¤,8¡I’²à„&IÊ‚š$) Nh’¤,Ì5õŸ;wîÏ�ÇÀ-àðxücQÿÕT¿br Ò£&Ö}2ǧ=’«UQQ`x|ŠïŸ�g@/v߃î1jbÝŸ\ŽTkunع(Î�;·ü–r6çjo�{ý‹,ÏαtÈÎ`Àî§O£æ¯�»EQü1Z#øÿc7€ à °loFͳƒÔÄÈ¥uŸÌï†öH9˱'´çÀßþÙÌ ÿÉÍÅÅoÚüòñ#¿ùÃïù߯_¡™'Ñ:ÇS`.ÿ üõwšüø5ð?Ï‹¢ø»xÝëžH¹´îøÝÐ*Éf9Ú„vîܹËÀ¿óÿðW¿ún`G~ùø‘Çÿù_�—@.?‰-€ø¾„‘ßðøUáÿ)—Ö}¿Z%é,ǼËq˜_ëõ&àæâ"«½”ý[‰Ð·XV€¸Îä 0|ýÀ<åØ)Œ¹´î“ùÝÐIg9æ„v `½ß¯Ôøþ¸Ý•@ýiÂð½<¬ØüÑèäví=ÑHŒ\Z÷Éünh�¤³sB»°<[m¥ÀÒ¸Ýù@ýiÂð½Tý^�\ЕbäÒºOæwC{$�å˜Ú€ý£ÃJ�Æí>êO†ïe¯bó÷£“ú¢RŒ\Z÷Éünh�¤³sB{ °3Tj¼=nWuäÚ`ø^¶*61:yU{O4#—Ö}2¿Ú#é,§~—ã`Ø�ÒÉð.»À|ŠwuQ¤\Z÷ ünh•¤³œú:´ìÖâŒÆ Å5]#—Ö}2¿Ú#å,7ºSÈZ¯Çz¿ÏÒìG‡l¼;¹À�¢(>ýàr­ôí*ûë”w ]¦ü÷æÀÛQó,Ç 51riÝ'ó»¡=’ÎrC{€=#±=Àƒn1jbÝŸ\ŽTkõ7´ã†»4oR®Oh~—æ8é‰Që>™ãÓ©Õª± M’¤:ù€OIRœÐ$IYpB“$eÁ M’”'4IRªmoÀðvÏÇ”‹ó¿Ý3'9ŽmŽï)´¶ŽY[ûÝEÉÕª¡yÏIlA^GŽc›ã{rÌòêw�TkÕèÖWW{ Üë_dyvŽý£CvvOnos·( ±^Á·ÛÑÜ6(Ÿ[´G¹;ö›QóVŒ­y9½¶æÀZ·GÊK}sâ-àI´¶ÛS`#Å C§e^¦ÒÊXëVI6c©?>æ+ðð'±É€ÀLŠ�t˜†y™J+s`­[%éŒÅ¼Ëq˜_ëõ&àæâ"«½”ý[‰Ð·¶[fÊ]¯'Œáë׿)k’*órzmÍ�µn�¤3sB»°ÞïWj|ÜîJ þäd8F+64:¹]{Oêc^N¯­9°Öí‘tÆbNh–g«­X·;¨?9ŽQÕÏ÷åÑÉ…}©‹y9½¶æÀZ·GÒ‹9¡}Ø?:¬Ôø`Üîs þäd8F{›¿�|Зº˜—Ókk¬u{$�±˜Úk€�Á Rãíq»ª#×eÃ1ÚªØüÅèäUí=©�y9½¶æÀZ·GÒKý.Ç/À*°¥“íu ØæS¼óhæe*­Ì�µn•¤3–ú:´$ÖÈ´ÁhlS\2-órzmÍ�µn�”3ÖèN!k½ëý>K³s²=ðîänwŠ¢øôƒËé˜oWï_§¼é2å¿c¿ÞŽš·blÍËéµ5Öº=’ÎXC{€=#±=Àr8rÛß“c–W¿»x¤Z«¨¿¡7Ü¥y“r}Bó»4g$DZÍñ=…ÖÖ1kk¿»(µZ56¡I’T'ð)IÊ‚š$) Nh’¤,8¡I’²à„&IÊ‚š$) Õž×ÀpýÂcÊÕæ�¯_ÈIêc›zÿºÌÚœNˆñ²gÐÐ óç$¶Â<‡#õ±M½]>¬Móãe Î~4º—ãÕÞ÷úYž�cÿè��Á€Ý“ûµÝ-Šâ�Ñ:Øbßî¯vØ |ßåãÞŒšG[kŸ®Ô³“šY¶õH}·ý-àI´¶ÛS`#ŰÁÚ'.éì¤&P–­A RÚWà%àO"“-€™$ŸQdíS–tvR(ËÖ &1ïrÜæ×z½‰!¸¹¸Èj¯eÿV"ô­íV€™ò1“> _¿0OY“¬}ºRÏNjBdÙÔ$æ„v `½ß¯Ôøþ¸Ý•@ýÉÉpŒVlþhtr»öž|ŸµOWêÙIMˆ,[ƒšÄœÐ.,ÏV[)°4nw>Pr2£ªßÿ—G'ôå{¬}ºRÏNjBdÙÔ$æ„ö`ÿè°Rãƒq»Ï�ú““áíUlþ~tò!@_¾ÇÚ§+õì¤&D–­AMbNh¯vƒJ�·ÇíªV¹Ë†c´U±ù‹ÑÉ«Ú{ò}Ö>]©g'5!²l j’ú]Ž_€U`?J'Ûë° ̧x—”µOZÒÙIM ,[ƒš¤¾͵�Æ6Õu,Ö>]©g'5!²l êÑèN!k½ëý>K³s²=ðîä û;EQ|úÁåtÌ·; \§¼kê2忹¿ÞŽšG[kŸ®Ô³“šY¶5‰½×å"Âg¸_YçÆ6õþuù°6Í�—58ûõ7´ã†;JoR®¥pGé¥>¶©÷¯Ë¬Íé„/k0½Æ&4I’êä>%IYpB“$eÁ M’”'4IRœÐ$IY¨¶etÃ[SS.$ìä­©]ƒ®¾ï6°6§b¼¬Á4´ ñ9^<ØÕ1èêûnÃamš/kpö£Ñ­¯®ö¸×¿ÈòìûG‡ì ìžÜ2ænQ?zly+}»ÅÍ `ƒòYH{”;n¿5Ïf ¬}ººšÉi…Ȳ5¨Gê›oO¢u0ާÀF×6!µöIëd&§(ËÖ ©?>æ+ðÈå'‘à0Ó¥ÇDXû¤u2“Ó ”ekP“˜w9nók½ÞÄÜ\\dµ×ƒ²+úË 0Sî¤=)´ _¿0O9vmfíÓÕÕLN+D–­AMbNh·ÖûýJ�ï�Û] ÔŸ& ßËÊÍ�Nn×Þ“¸¬}ººšÉi…Ȳ5¨IÌ íÀòlµ•Kãvçõ§ Ã÷Rõ{úòèäB€¾ÄdíÓÕÕLN+D–­AMbNhö�+5>·û¨?M¾—½ŠÍß�N>èKLÖ>]]Íä´BdÙÔ$æ„ö`g0¨Ôx{Ü®j•Û`ø^¶*61:yU{Oâ²öéêj&§"ËÖ &©ßåøXö£t2¼KÀ.0ߥ»™¬}Ò:™Éiʲ5¨Iêëв[k1ƒ®­7±öéêj&§"ËÖ �î²Öë±Þï³4;ÇÁÑ!ÛƒïN®°¿Sŧ\®•¾Ýà:åÝM—)ÿmüðvÔ<›1°öéêj&§"ËÖ &±÷Ú¢\DøŒïWÖÕ1èêûnÃamš/kpö#êohÇ w”Þ¤\KÑÉ¥»:]}ßm`mN'ÄxYƒé56¡I’T'ð)IÊ‚š$) Nh’¤,8¡I’²à„&IÊ‚š$) Õž�Àp­ÅcÊ•ñµ­µu]5˺žžc^ˆ1¶ngÐÐ ûçÔ¼>Ôu=š=¬«c–âbŒ­ÛÙ�F÷r¼Ú[à^ÿ"˳sì²3°{r´»EQüñ´×…Àåóö(ËðæÔ×U³Bå%g~ ‘KëV�ÔwÛßžT¸ôS`Ã�ªó0/9ó³X \Z·¤þ<´¯ÀK`ÒO" À`Æg å#`^ræg!°@¹´n5‰y—ã&0¿ÖëM ÀÍÅEV{=(û·ò“ë®”í®39 _¿0?ì�Ò*/9ó³^ˆ\Z·šÄœÐn¬÷û•ß·»ò“¦Ã×VìÆ£ÑÉíŠ@Í•—œùY/D.­[MbNh–g«­X·;ÿ“¦Ã׫~�]>Ñ%+T^ræg!¼¹´n5‰9¡}Ø?:¬Ôø`ÜîóOš_ß«Ø�÷'ú£d…ÊKÎü,„"—Ö­&1'´×;ƒA¥ÆÛãv?«òðõ­ŠÝx1:yUñ¨¡ò’3? á…È¥u«Iêw9~V�ý —¾ìóÞ!”�€yÉ™Ÿ…ÀåÒºÕ$õuh•ÖZŒ®ë޼„ÊKÎü,„"—Ö­�î²Öë±Þï³4;ÇÁÑ!ÛƒïN®°¿Sŧ\î‡×-o}HùŸ§ï)E{êëªY¡ò’3? á…È¥u«Iì½¶(>#Ì^޵_×£Ùú:f)!ÆØº�ýˆúÚqÃ¥7)×RÔ½Û~í×U³¬ëé9fá…cë6½Æ&4I’êä>%IYpB“$eÁ M’”'4IRœÐ$IY¨¶etÃ[SS.$¬û¶ýÚ¯«nˆ‘3š�µ4gÐЂÄç„YX]ûu=ºqÄÈ�ÍçQKóqö£Ñ­¯®ö¸×¿ÈòìûG‡ì ìžÜ2ænQ?zlù¯ 7€ Êç íQîbýæÔ×U7„Ê夿̶Wˆ¼˜�z¤¾9ñð¤Â¥Ÿnì©iÌåqf4�òb>j�úãc¾/�I?‰,€½ Ó ˜ËãÌh&åÅ|Ô$æ]Ž›ÀüZ¯717Yíõ ìßÊO®»R¶»Îä 0|ýÀü°?R¨\gFó"/æ£&1'´[ëý~¥Æ÷Çí®ü¤éðõ‡»ñhtr»âPÞBåò83š�y15‰9¡]Xž­¶R`iÜîüOš_¯úýrùDÔy¡ryœÍGˆ¼˜�šÄœÐ>ìVj|0n÷ù'M‡¯ïUìÆûýQç…Êåqf4!òb>jsB{ °3Tj¼=n÷³*_ߪØ�£“Wÿ€ò*—Ç™Ñ|„È‹ù¨Iêw9~V�ý —¾ìóÞ!¤Ó ˜ËãÌh&åÅ|Ô$ÚohEQ¼gø£Åoþð{~ùøñ»íFë7†þ©(Š+Šâ¿'ÿÊŸ~dù5eÁ¿g´†ãO×5 ™K3š¡@y15it§�µ^�õ~Ÿ¥Ù9ŽÙ xwr…ý�¢(>ýàr?¼nyûëCÊÿ<åïí©¯«n•ËI‡m¯y15‰½×å"Âg„Ù˱öëzt㈑3šÏ¢–æãìGÔßÐŽî(½I¹–¢îÝök¿®º!F~Ìh>BÔÒ|L¯± M’¤:ù€OIRœÐ$IYpB“$eÁ M’”'4IRœÐ$IYpB“$eÁ M’”'4IRœÐ$IYpB“$eÁ M’”'4IRœÐ$IYpB“$eÁ M’”…ÿãİTc¾ÇéIEND®B`‚python-pot-0.9.7+dfsg/docs/source/_static/images/logo.svg000066400000000000000000000234731525521106200234030ustar00rootroot00000000000000 2022-03-30T17:25:32.476826 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/ python-pot-0.9.7+dfsg/docs/source/_static/images/logo_3ia.jpg000066400000000000000000000607051525521106200241170ustar00rootroot00000000000000ÿØÿàJFIF��ÿÛC    #%$""!&+7/&)4)!"0A149;>>>%.DIC;ÿÛC  ;("(;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ÿÀ¤"ÿÄ ÿĵ}!1AQa"q2�‘¡#B±ÁRÑð$3br‚ %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ ÿĵw!1AQaq"2�B‘¡±Á #3RðbrÑ $4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÚ ?öj(¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€8}âi·óØY؇’(ÒJÜgØñ®^çǾ"¹È‹ ‘ŒEüNMRñOü�:—ý|5eW¯N�5ì|½|UiM®m oøJüAÿA{ŸûꥃÆ~"·bWS‘óÚEV¨¬J+_gÇ?·ª¾ÓûÎïLø�pŽT³I<ÉÊGàzþuÝéš­–¯j.lgYS¾:©ô#±¯ «ú6³w¡ß­Ý£àŽ ùd_C\õp±’¼tg~1©jš¯Ä÷:CҪ隌¶� õ¹Ìs.@îp}Á«G¡¯1¦�™ô ©+£È¥ñ׉ijÄܧ¯Ò™ÿ ç‰?è ?ïÊ…aOÿÿ¾ßΣ¯kÙSþT|“ÄVþw÷³Ò|â=WZÔn¡Ô.DÉ!”yj¸9Ça]ÍyŸÂÿù Þÿ׸ÿЫÓ+ÌÄ¥�#èp2”è''vQEs�¡Uµ Ø´ë ï'8Ž.}ýªÍp?õ�±A£ÄÜ¿ïfǧð�ÏŸÀV”¡Ï5 EeF“™³à¯ɯØL.™~Õ ‡pSÊÿ‡á]-x¿„µ�ì]~ ݱ§Ê›ýÓßð85ìàädr+\M>Ié³9ðÝZZî…¢Š+˜ï (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€<ZÑõ-SÅz’ØÙM?úKÊ¿(úž•b‡:ü»L‹o#�òä�Èõ|b–ºþ·4’Hó[M·)6îydÿ 5¨€1Mk1î•þb¹­CL¾Ò®<‹ëg�Ï#pᾇ¡¯x¬Y[Þx^ðÌt æFçøX�OÆ®–*NIHË—SPrƒ³G�ÑEèžè¿ ï™íïl’#e•=³ÁþB»ÃÒ¼ßár1ÔoäÇÊ!PO¹?ýjôšò1)*¬ú|½·‡W<{Ãq/ú$ÿ|ÿË&õúS>Ãyÿ>“ÿߦÿ öîé�uK0G_߯øÑý»£ÿÐVËÿük§ëRþS‡û6Ÿüüþ¾ó†øgo<:µé–#‚èF~ozôŠ­m¨ØÞ»%­ä²Œ‘�ˆ…Y®:³sŸ3V=L5%JšŠw (¢²:®n#´¶–âfÛH]‰ìxn«¨É«j—ÒçtÎH݇à1^ƒñ'Xû6�—bK£ºLvAþ'ùW™×§„§hó>§Ïæu¹¦©­—æëþÖµ¼?ÈÙ¸µýÔ™ê@û§ñÈ×�WEà}gû'Ĭ�ˆ.¿u' ?Â?çZâ)óÃÍø*Þʲ¾ÏCØ(¢ŠñϨ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ *½ýõ¾›e-åÓì†!–lgºV$~=ðô²,iu#;�ªÉ?…\a)+¤g:´àí)$ttQEA QEQEQEQEQEQEQEQEQEQEQEQEQEQEU;ýVÃKo®£·yÆqÖ®WñOýN›þôŸÈV´ §5sâjº4œ×C ¸ñLJmÕ�öŠÈGðÆŒÄý8Åq*ñ¼šä&ÊÎ&‚Ìœ¹cóIŽ™ôÉÑ^”0Ѓ¹àVÇÖ«W¢ò (ë^‰á$f-OShå<4P£P{#ƒô­*T�5vaB„ëK–&Ç�4GÒ4!$é¶âìùŽUÂ?.éOCKHzñ§'993ê©STà º8ÿH—ýöþtÌT“ÿÇÄ¿ï·ó¨ëÝ>=�·Âÿù ^ÿ׸ÿЫÓ+ÌþÿÈ^÷þ½Çþ…^™^N+ø¬ú\»ýÝ|šî±FÒ;E±=€§W%ñYûŠ,blMzvœB½ùð?XÂrQGUjª•77Ðó½UmkZ¸¾9ØíˆÁìƒ�þ}ë:Š’$¹¸ŽÞ!ºI\"�RN{i(«#&ç+½Ù¥mø³A_ê«of†H•јç'£~¿Î±(Œ”•Ðç 8Ët{?„µ�í­ ݳÖ³µÏ±ÈØ‚ôlç q÷Oó�z½yéòOÈúlokE7ºÑ…QX�EPUu JÏJµk›ÙÖÇv<“èsMÕu;}#Nšúå±C8XöÜ׌ëzÝæ»|×WoÇHãåŒzñ®ŠGw±Ã‹Æ* ËY3¯Ôþ'¶òš]’í‰._øÿÄø�ó‹¸ÐgøaZfƒàÍO]EœmjzK ûßîŽÿʺëo†ZTcý"îæcŽpB þUÖÞž–ýO6+_ÞM¥÷Ä?ñÿ–­ëõ«£GÚß]ŒqX¯«ÛKÜ÷Š+?Af}Á™‹1·BI9'�Z‹Vv:£.h§Ü(¢ŠEQ@Q@Q@Q@Q@aqñ#ZŠâXÖ <#•FìÞ¯AÑo$Ôtk;É‚‰'‰]‚Œ ‘Ú¼Fóþ?n?ë«3^Íáoùôßú÷Oå]øšqŒŠ šDlÁî¢ô u?‰Ïé]z~Òzìpã±�?wv{r$Ñ$±°du ¤w¥>¹‡ZÏÛ´f°•³5™ÀÏt=?.Gå]}eRqgMª­55Ô(¢ŠƒP¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(®'â>�{€³´šà£>ï) màuÅvÔUÓ›„¹‘�zJµ7Ôð›�#S´®4û¨�îð°þ•N¾‚®kÄþ±Ö-%šÞ‚ùT²HƒÏ£zçÖ»¡ŒMÚHò+eŽ1¼Ï"­mÄ—þ¸m!x ýä ~VÿïY8#‚0GQEv¸©+3É„å sEÙží¥jvÚÆŸí«nŽAÐõSÜqVÏC^iðÏShu9ôÆoÝΞbF1ü«ÒÏC^=j~Îv>« _ÛRRêxÿññ/ûíüê:’øø—ýöþu{'Ê=ÎÛáü…ïëÜèU长 ÿä/{ÿ^ãÿB¯L¯'üV}.]þî¾b�“ÀâÞ*Ö?¶õéîU³ .÷GÄäþ5è¾:Ö²´Ž6Ä÷ºLuø�åüëÈ«£OG6qfu®Õ%꺿‡z_Û|Aö·\Çf»ÿàg…þ§ð®R½OÀ£OÒ¼<�5í²OrÞl€Ì �Ø}?�oˆ“�7n§š�e}–¡ñKûf†·¨¹’ÍòqýÃÁþ†¼²½ÊæÿI»µ–ÞkûFŽT(ÃÎ^Aõ¯½¶ûìÖÛÖO)ʇS�Àt ÖXI>^WÐß2¦•ER=H‘Ú7WF*Êr¤v5íÞÕ—ZÑ-ïF7²í”z8àÿ�ã^!]ŸÃ}gìšœšd­ˆ®†èóÙÇø�ä*ñTù¡~¨Ï/­ìêò½™éôQEy'Ò…Q@kñ3Uiu4´oݾdƒÕ�OÈ:ÉðW‡×]Õɸ\ÚÛò�ïËøÿJ¯ã _j%‰8—hÏ WoðÎOÍ8ûÒÜOÐ W©'ì¨+=ýcù¶¿äv ªŠT*¨À`KEåŸBQEOSÕl´‹Ss}:Åàg’ÇÐæ¬\O­¼—6ØâRÌ}¯×õË�{R{¹ØˆÁÄQçˆ×°úú×E >ÑùXÌRÃÇMÙ×_|Pmìº~œ6öyÛÿeãU`øŸ©+þþÂÖEôBÊRišû�FÕnµÚÒ7HÕräzœô­[Ï…ö�ûüÉ  @e'ß5Òþ­Êyëëó\ëô6t/iz㬵µÉé¿Åþéèk¡¯ÔtëÍý­n�Å4g ƒ×Ñ�¯RðGˆ_\Òš;–ÝwjBȾ;7ùô¬«ÐQ\ðØéÁã%R^ΧÄC¯xî U’Áì$™‘U·¬€‘ŸJò—mîÍÓq&½c]ð-®»ª=ü·³DUAäò.Çeë´‘]_gËîïÔáÌ=·?ï6ÖÇy¦üH‚ÇM¶´m2W0D¨XJ8ÏJéü3â˜üKö�.Ñíü�¹Ü᳜ÿ…szgË;í2Úíõ …i¢W*ª¸®—Ã~ƒÃhòneŸÏÛ�à c>ŸZç­ì,ùw;°ß[æ�?Ãòìn×3¯xëLѤkxÁ¼¹_¼‘Ÿ•O»J‹Çž!}M[[WÛuw�uDî~½…y·¢ÝkúŠÙÛ`n’Fè‹êh£B.<óØ1xÉÆ~Ê—ÄtrüOÕó�¢'£ncùäV¦—ñ6ÚiDz•™¶ãÍ�·(úŽ¿Î¬ÃðÏFX•ežîGæ`áAü1\—Šü!/‡J\E)žÎFÚŒ2Cþ5¬V£åHç›ÇQ\òwG­Ã4w¤ÐȲFã*Êr§×›|7×$Šñôiœ˜¥áýÖ�>£ŸÂ½&¸êÓtåÊz˜jê½54QEdtH'€+�Ö¾"ØØLÖö›Ùáœ6ØÁö=ê/ˆÚô–v‘éVÎVK•Ý+ÈN˜üOò®DÑ/5ëáif `eäoºƒÔ×mqçžÇ“‹ÆMTöTw:DøŸª rö6�~èÜçŸé]O‡üo§ë’-³+ZÝ‘¹È÷Oô¬È¾éâ&Ô.R>ò…PÓšæ¼Iàëßm¼†c=¨a‰TmhÏlÿ�_.§»—>6‚瞨À½âúãþº·ó5ìÞÿ‘_Mÿ¯tþUâŒÌìÎÌY˜ä’y&½¯Âßò+é¿õîŸÊ«ð#<¯ø²ôýLCáÌzŽ£q{.«(yä.@ˆqžÝk¯·‚;[h­â]±Ä�z1RÑ\2©)«6{4èS¦ÛŠÕ…QY›ÿ�5ŸìÊѶ..?u¨'©üJò�7KºÕgxm{ÇHÙô·ãÍgûS_hclÁg˜×‚ßÄ>? ê>i?eÒeÔd\IvØLÿqÄçòéC÷yº³À©þÙŠä[#ˆð¾°t]vÞèœBǢ°zþ\½¨ÀA¡âÞ+Ò¿²$†¦¸’ 9ÈÇzã§Ë̹¶=ZÞÓÙ¿g¹ÎÿÂÒ¶ÿ Tß÷ô…ð´­¿è7ýýáOÿ…]cÿA+ŸûåkÅ~Ó¼9e¥üÒÜÊØŽ&UÁ©8®ØÇ'eúžUIã©ÅÊMYzð´­¿è7ýýá]N�«¾¹¦­ñ³{Tv>Xv°õ¯%ðÞ‰&½¬Eh2"<Î?…_Äô¯j†(à…!‰BG…Ugˆ…8{±Z›àj×­yMèax›Å‘xj[t’Íî<ðÄp¸Æ?Æ£ðߌ¢ñì¶±Ù<8üÍÌàç�1ú×;ñOþ>´ß÷$þkUþÈrïþ½¿öaMR‡°çêKÄÕXÏe}?à�TõmEt�.âý£2ˆq@pO5r±|aÿ"ž£ÿ\¿¨®H$ä“=²q§).‰˜VŸíî¯ ¶\ªf‘c edã=+·¯Ò?ä5aÿ_1ÿèB½Þº14ãM®S‹^¥hÉÍì`ø›Å1øh[-ãí±µÂíÆ?Æ£ðß‹àñ \�lÖÂÝC1wsùt¬ŠsLúÉÿ²× o¨\ÚZ\ÛBû#ºP²àr@9ÆkZxxÎ’}NjøÚ”±?etùî±ñ.8'ht›e¸ qçÊHSô’?*è¼)¬Ükº*ÞÜÇHdeÄ`��õ5çú€õ-bÝ.¥‘,íß•.¤³P¾ŸZôO è§@ÒE‰¸óðìÛömëí“Q^4cXîm„ž&¥NzŸ ­EWêQ@Q@'JZFû§é@™^ÏP³Ô#ó,î¢�}Q�Ç×Ҭ׀¬²ÁpÒC#ÆáŽƒ×Úµ­¼aâ ]»59\/A.ù×t°oì³È†k·¸öŠ£¬j–ú>›-åË…TS´wvìy{xÿÄl¤}²1‘ÔB¿áX—Ú•ö§(–úê[† ¶qôÁÊþó\Ò¾âw+»vvêÄ“øÒQEz'‚tßÿ }žÏöóôÚkØOC^gðÏMiµYõ_ÝÛDZO«7ÿ[ùצ†¼¼[N¡ôylZ¡wÕž?ü|Kþû:ޤŸþ>%ÿ}¿�G^¡ó¯s¶ø_ÿ!{ßú÷úzeyŸÂÿù Þÿ׸ÿЫ¯ñ†³ý� M*6'›÷Púä÷üMyxˆ¹V²>‹5O Ìú\ó¯k?Úþ —ËlÁmû¨ý>ñüOòÏÑEzQŠŒRGÏÔ›©7'Ô)0=*ý®‰ªß@'µÓ®'ˆ’¤dƒŠ›þ�wþ�Ÿ÷èÑÏÔjœÚºLÊÀôµ©ÿλÿ@‹Ïûôhÿ„g]ÿ Eçýú4sǸý•Oåf]Iò[\Gq m’&‡Ðƒ‘Q�A ŒÔQTg±îº>¥¯¥[ßEÀ•2G÷[¸üêíyÏÃMc˸›G•¾Y?{¼>ðü¹ü z5xµ©òM£êðµ½µ%.½BŠ(¬Ž“ÆümnÖÞ-¾ 8‘„‹ô WYðÂõ_K»±$oŠ_0fâ*?‰z3M:¼+“òæÀþx?�þuÅèÔÚ«ìCrýÙcÏßSÔZõöÔ,·>u¿ªã–ߣ=Š©¦êvzµšÝYL%�½:©ô#±«uæ4Ó³>…5%tQMwX×s°Uõ'�ÎwÇ× oá+�§VXÏОA^sá;Ô|Meo*†�~öSÜ(Î?JôŸZ5߄[eǰ<þ™¯1ðÞ¢šWˆlï$8�}ðOëšô°ÿÁvßSÁÇiŠ‹–Ú~g·ÑH¬C)HÈ õ¥¯4÷ŽâuŠI¦Zß…dRùdú«ýE`|8¸h¼NbíšR>˜#ùV×ÄíN1kk¦##?œàv3õ'ô¬�†Ö­7ˆžã$ŸsÀþµéCýÙÜðjÙã×/tz¥xÿë¥ÿ}¿�{ýxÿë¥ÿ}¿�F yf»CçúÝáÿù´ÿúöOýV�gxxçúv9ÿFOýV�qOâg­KøqôG’|B¸iüW,dœA †ïëPxcÅGÃIpÅnr2æM¸·CëV~"Ú´(i¶án"WÔ�´ÿ!V<a£ê�ui¨ÚÅ4ãüä¯B?OνKÇØ+««=j�Ô]�ÙsþœßôOûþøšÏ×¶kܱÎ&T‘,1]oÃ]F9ô9,7-´„íîU¹óÍzU¿ÝÕ¼� e�—6ú�•W¾´ŽþÆ{IT2L…>â¬U=[P‹KÒî/f`«3Üö‰¯:7º±îNÊ/›c™v3/]¤Šö¿ ȯ¦ÿ׺*ñF%‰cÔœšö¿ ȯ¦ÿ׺*ôqŸ<,¯ø²ô5¨¢ŠóO|+Å:ÀÑ4î”âf\#ý³ÓòëøVÅy_ÄMgíÚÊØDÙ†Ìa±ÝÏ_Ë�ùÖô)óÍ.‡&2·±¤ÚÝèŽkO²›TÔ`³‹&Iä Ÿ¯SüÍ{�­´vv‘Z»c…(ö¼&Ýn•ÄÖ«0e<<@ä~"­ý¯]ÿŸ�Cþúzï¯EÕ¶¶×¥G©D¹’Ðáð:¡ÿ�Ö¸ÿë?Øþ ‰¤lAqû©}zÀÿZÏ’}fhÚ9$¿ta‚¬\ƒøUFF*êU‡PFª…+S䓹±Õ•X«3è+ÁºÏöχá‘Û3Áû©}Iâ1[ÕäÊ.2iŸMNj¤—R9çŠÖÞK‰œ$Q©gcÐ^)â f]X–ñ�ØNÈSû«Ø}ƺïˆþ átKg뇹 þKýOáYß¼?ý£¨�Ná3ojß #‡“·å×ò®êT êHññ•%ˆ¬¨Cçýy—ƒ|>4-|Õîã1ôô_Ãùæº (®IÊ\Ìö)Ó�8(GdyÏÅ?øúÓÜ“ù­Wøaÿ!Ë¿úöÿÙ…Xø§ÿ:oû’5ª¿ ]W_¹R@-lp=~a^‚ÿvþ»ž$¿äaóýP¬_ȧ¨ÿ×/ê+j¹Oˆz¤V~{=Ãμ!UsÎÐrOé�Ƹi&ê+¾&J4dßcÌô�ù X×Ìú¯w¯ Ò?ä5aÿ_1ÿèB½Úº±›£ÎʾñOîiŸY?öZæ„(¢Š(¢Š(¢Š)-%g€?úÇÿxÿ:mz†©ðÛM»f’Æy,Üó·ï¦~‡‘ù×;sð×Z„þâ[k�ž0åN=yÖ½ˆâ)Ë©ó05àþú�Ò7€óKœ~@ÕûjÌŒ–³ûŽZ¯èÚ-î¹z¶¶qçûò»õ&»}7ጲ¾¥zÓc¬p�£ó<ÿ*ììl-4Ûaoen�D?…_së\õ1qJÐÕ�´2Ú’w©¢üHtm"ßDÓ"±¶Tå˜õvîM^= -%y­¶îÏz1Q�*Øð)ÿãâ_÷ÛùÔuÞÉð¾åäwþÕˆnbÔŸñ¦ÿ­¹ÿ ¬_÷äÿ�zÿX¥ÜùŸ¨â?—ò"ø_ÿ!{ßú÷úRø�¬ÿik¦Ö6ÌYAŽ…ÿˆÿOº¿x6ïÃÒ]Ìš„RI<\gÊ #g žy¬vø_vìYµxÙ˜ä“ äþu‚©KÚ¹¶vJ†#êѤ£ë±ÁÔ–Öò]ÜÅm î’W£ÜœWoÿ ¶çþ‚±ß“þ5«áÏÿbêéqx—RŸ-#ëÒîýšGcÉ™¬�¬j:¦«h××’Ïþ�˜Vo”|àé]¬? ¬Uó6¥q"ú**ŸÏšè4Ï hºK -ìÕ¥%”ïaôÏO“­F+ÝE¬&.£^ÒZzšò"ËG"†GX„ñ¯xnj »Y­$$Ã/ltûŠözŠæÖ Ëv·¹…&‰Ædå£YÒ~G£ŠÂ¬Dm³G“è>9Ô´HÕ•ní—†Aè§Ö´îþ(^I [M>(ÿÈ_A�Z÷ß ô¹äßis=¨'”á×ðÏ?­V‹ám°`eÕ&eÏ!c#ë“]Nxi>fµ<åGɧËþà�ïu}Cs.®îêÌk×<#áïøGô‘˜7Só0õì£éþ5cGðÞ—¡/ú¸†™Îç?�oµk øŽuËެ& ؾy»È+Ã5Ý>M/[»´‘Jì�”Ïu' þUîu“®xkMñj/#+* $Ñœ:û{�cS‡ª©Ë]™¦7 ëÁrî�?Ð|}w£iëc5ªÝÅ’ûYG§C‘]†¼iwâ {ì�k¼&|Y‰ïÿÖª§ál¸Õ¥Ûÿ\F�mx{Á–>¹7QÏ4÷Jn|öµIÐiµ¹ËBž22ŒdýÕè/Œ¼8uý,û]¾Z,ÿªþ?μš9.ôËàèd¶º�¾Œ†½î²u� éZçÍyn<Ð0&CµÇãßñ¨¡ˆä\²ØÛ‚ö²ö�v‘ÅYüO¾Š ·–\8}¦~£¤Ÿâ�Ó(û>—7s$¥¿�r_…¶¥˜ÃªL‹Ÿ”ÛîçU”¯ ˆgù×M¢xgLÐTýŽea†šC¹ÏãØ}*ÝZÖ S‡ÆV÷jÊÑþ»5yjöW³ZHT¼.QŠôÈëŠöo ȯ¦ÿ׺*˾ø{¤êÓÞKqv¯;—`® }>Zè¬,£Ó¬ ²…™£�)cÉÖ£Z5"’ÜÛ„©F¤œ¶,QEÆz†v½ª¦�£\_67F¸@‰�~uâ_¾¼ºï$ó¿âÌOøšö�{ÃÖþ!‚(.§ž8ãmÛb`7|ƒþMgižÒt½Fèå¹–HNåYJç×�]”*œ]÷<¬fµzŠß 5ô=.=G¶±Lf4ùÈþ&<“ùÖ…W#m»³ÓŒTb¢¶A^yñ3GÚöú¼KÁýÔØÿÇOó•zUÔ´è5]>k�LS.Þ£Ð�zÒ•NI©âhûjNO/ð³ý™¯-¼�ˆ/qg oá?Óñ¯Gñ³…¤K{& �¶$?Æç ¬ðÏGRnïƒ�C¯þù­]s¶ºÿ‘öË» °.Q”{±ã­mVt§QKï90ô±¨Êסã·Ëuq%Äî^YX³±îMtúg�ï4�>]:ÐEàd¶X÷'ž¦º_øVZ7üý^ÿßkÿÄÑÿ ËFÿŸ«ßûíøšè•z2Vg<.›æŽæ'ü- Sþ|-?6ÿê¼âKŸÛÝKsQ\(県÷ªð¬´oùú½ÿ¾×ÿ‰­½ÖžŠhí$™Ä̼Ò0;` ç«*>âÔíÃê'QèbüHÓ$»Ñb¼‰KGË€?€ðOáÅy¶Ÿs¦^Çyi'—4g*zƒêµ{Ë*º•e ¬0AW¨ü6Òî§2ÚO-˜c’Š/àJªãòLσ©:žÒ–æ|NÕŒEVÎÑ_>ã߬xíu?µö©u3:ÚÂÒI+8¢»O†JêúiÔuEP™üy®«û&Í4§Ó ˆAlñ˜öÆ1€F úÕ:Ô¡ü4Dp˜ŠßÆzv¶ßøßüEf£ÿ>¶ßøßüE[¢‹ù+îTó5ùõ¶ÿÀ†ÿâ(ó5ùõ¶ÿÀ†ÿâ*Ý_È9_r§™¨ÿÏ­·þ7ÿG™¨ÿÏ­·þ7ÿVè¢þAÊû•<ÍGþ}m¿ð!¿øŠ<ÍGþ}m¿ð!¿øŠ·EòWÜ©æj?óëmÿ� ÿÄQæj?óëmÿ� ÿÄUº(¿�r¾åO3QÿŸ[oüoþ"�3QÿŸ[oüoþ"­ÑEüƒ•÷*yš�üúÛàCñyš�üúÛàCñnŠ/䯹SÌÔçÖÛÿÿˆ£ÌÔçÖÛÿÿˆ«tQ å}Êžf£ÿ>¶ßøßüEf£ÿ>¶ßøßüE[¢‹ù+îTó5ùõ¶ÿÀ†ÿâ(ó5ùõ¶ÿÀ†ÿâ*Ý_È9_r§™¨ÿÏ­·þ7ÿG™¨ÿÏ­·þ7ÿVè¢þAÊû•<ÍGþ}m¿ð!¿øŠ<ÍGþ}m¿ð!¿øŠ·EòWÜ©æj?óëmÿ� ÿÄQæj?óëmÿ� ÿÄUº(¿�r¾åO3QÿŸ[oüoþ"�3QÿŸ[oüoþ"­ÑEüƒ•÷*yš�üúÛàCñyš�üúÛàCñnŠ/䯹SÌÔçÖÛÿÿˆ£ÌÔçÖÛÿÿˆ«tQ å}Êžf£ÿ>¶ßøßüEf£ÿ>¶ßøßüE[¢‹ù+îTó5ùõ¶ÿÀ†ÿâ(ó5ùõ¶ÿÀ†ÿâ*Ý_È9_r§™¨ÿÏ­·þ7ÿG™¨ÿÏ­·þ7ÿVè¢þAÊû•<ÍGþ}m¿ð!¿øŠ<ÍGþ}m¿ð!¿øŠ·EòWÜ©æj?óëmÿ� ÿÄQæj?óëmÿ� ÿÄUº(¿�r¾åO3QÿŸ[oüoþ"�3QÿŸ[oüoþ"­ÑEüƒ•÷*yš�üúÛàCñyš�üúÛàCñnŠ/䯹SÌÔçÖÛÿÿˆ£ÌÔçÖÛÿÿˆ«tQ å}Êžf£ÿ>¶ßøßüEf£ÿ>¶ßøßüE[¢‹ù+îTó5ùõ¶ÿÀ†ÿâ(ó5ùõ¶ÿÀ†ÿâ*Ý_È9_r§™¨ÿÏ­·þ7ÿG™¨ÿÏ­·þ7ÿVè¢þAÊû•<ÍGþ}m¿ð!¿øŠ<ÍGþ}m¿ð!¿øŠ·EòWÜ©æj?óëmÿ� ÿÄQæj?óëmÿ� ÿÄUº(¿�r¾åO3QÿŸ[oüoþ"�3QÿŸ[oüoþ"­ÑEüƒ•÷*yš�üúÛàCñe ”S"…|räÆ�EVêQE!…Q@Q@Q@ç:­õâ|OŠÙnçX ðƒ�…ÁQž3ŠôjÒtù}Ì(ÖU\’[;QY›…Q@Q@W5â/ÿ`j–ö?aóüõ ¿ÍÛŒ¶:`ÕB›´LêU…8óMèt´QL›Ìò$òqæm;3ëŽ*KEqÞ ÿ„£í÷_Û~‘³åó±÷óü>ØÍv5s‡$­{™Ò©í#Íkz…ÍØø»í¾*›CûÏ)œyÞfs·ÛÖºJRƒ‹ÔtêF¢n,(¢Š“@¢Š(¢²Kë¹~Š(©4 (®-ÿá,ÿ„ècÎþÍó{cÊòñüÿ\ÕÂ×ÔÊ­_gm»èv”QEA¨QEQEsw¾.û'ŠáоſÌdw™Œn˜þµQƒ–Æu*FšNO}’Š(©4 (¢€ (¢€ (¢€ (¢€ +Ï< }yqâ›øç»žXÖ7ÂÉ!`>qØ×¡Ö•iû9XÃYV‡2V +‹²ÿ„³þy>Ñçÿgy¯×W—ƒ·ý+´¥8rÛ[Ž•_h›³VîQEA°QEQEQ\¯�ÿá!òí?°üý»›Íò>öxÆ}º×A¥ý¯û.×íßñõå/›þö9«p´T®c¼Õ,ôêZ¢Š* ‚Š( Šä>#Ý\Zè¶Ïmq,,n-•$m>•µá‰$›ÃZ|’»Hí,ÌrOÔÖŽ� §ÜÂ5Ó¬é[djÑUï¾Ñö �²cíSyYþö8ýkšðGü${®ÿ·<ÿ/åòüügw9Ç·JJ‹•öªòÔP³×©ÖÑEÁEPEPEPEqž.ÿ„«ûf×ûÏû6Ñþ«wg�ÞØÅv)»bïÆìs�Z¹C–)ßsU眣f¬:Š(¨6 (¢€ (¢€ (¢€ (¢€ (¢€ (¢€<ËWÿ’¯ýw‡ÿAZôÚó-_þJ¼_õÞýkÓkª¿ÃCÏÁüuÄq¾4ñ>£¡j–PÚ÷Äþ%×f�ørÎT³ŒàJ#Ÿñn?U¾( êöÖÿ¡W¢YZÅce ¬ 8�*�TÜ!N2å»3J­jó‡3I/…¶ºN²K™ g@®�èq�¥z y–¶¡>)Ûífx ǯéµ…$•®�pR�§ ;ò»­ø¯\¾×¥Ñü?S ,ª 1NOTÚdþ9³Õ­£Ôci­e•VVØŒOºô¨5Ÿëº7ˆ&ÖôfY˜³"à°ÏPW¸Ï¥ZѾ!/ÇZ³6“3ó?ÞSÈ­š\Ÿ»Šjß3•Iª�ÛMÅßNÇjû‚6ß½Ž+ÆüI>»6«ëùWAG’¡@ÈÜqÓÞ½š¼×â'ü�:wýsOý ÖxGiÚÆù”oJ÷6´ßM¬C­h#³!¼ÆØ£)ÇCëŠëfb�HËÕT‘ùSª;Ÿøö—ýÃü«žRæwµŽÊptàÕÛõ9ø›S×oxÙ"Œ2ì�o9Åt^ —S‡H•ôxÄ—€®Å ŒŒõöÍpÿ ÿä+ÿ\Gþ…^•[WJtG6R«‡÷ž®úž3§ÜëÃÅ3OiíT´žbm‹Ž•èþ¸ñÄw?ÛЙY|¬(ç¡>ÕÉøsþJ…çýu¸þf½.´ÄÏen†8 NÎ\ÏFôèsþ3Õï4Mn¬™S2¦]w ¬ø[Q¸Õ|=m{vÊÓI»qUÀáˆéY¿äYOúùOäj÷�äP²ú7þ„k'ìësxÎ_[q¾–(øëÄZŽ‚¶fÁã_;~ý黦1üë§²•§±·•ð^H•›¹ÁüSuÎ�~lHØöâ»­7þAvŸõÁ?ôDâ•(°¥9ãSòîç©ú×Añ+þE”ÿ¯”þMZ> ÿ‘COÿq¿ô#[{D¨'e¹Íì\±r\Ïo郥-W ë…qx§TO.޲EöC2¦<¾pW=k·¯1¹ÿ’²ŸõòŸú®ŠOšýŽdåNWk´zuq>&ñmö‰â˜-HÖËj< Ç–Á'vÐWm^_ãÈ–ãÆÖоvÉHqèXŠ0ÑŒ§i>s…$àìî\»ñ‹5·{�ÎhlT�Œ±‚\zåºýYð_Œ/¯õ#¥j¤<„.B»[pê¤~•wE$1 HÐU€y¥¸ ñi‚Œ´±ãýÃZÁ¤e[YõcV„á>vîìû‹¨µÊi× f»®Dlb\g-Ž+ȯ.uçñ\S\÷V ›#Ú:ãåã¥{5y®­ÿ%^ßþ»Cÿ ŠXYYµn…æmEÝî—ü£ðÅߊn/¥]vØE�(B(ËdzL×Cyw �¤·W²(T³ŸaSW)ñIÂÅS8’tWúr˜‚µJ‰ZÇT¯B‹w½»˜Ÿð“x«Ä×nº g·ŒõÂñþó7öë_ëþÔÒÏÄ‘o…Ï.T~ð#‚=ª¿†.¼Yo¢Æº>›m-«3#–9ç?0ú~íwOñŸˆaŠ+Ý&!bÊce‘ŽìkµÆÜ­+~'–§UÁT‹“—¦‡¤«+¨e « ‚;Šã|iâ�GAÕlá´xÖ#ß h÷½�å].‹ öú%”@‰ã�@Np@çšà~'Œë6#Öÿ¡W5ÅÕ³ÕØÊ“Ž™hô&¸ñgˆuÝOþ$6ó¥”n)bÃ<–'�ô¯FªzNŸ—¥ÛÙÀ¡R4ãø�søš–õÞ; ‰#ûëú㊊’ŒšQVF´!8EÊr»Ö‡ âj7«i^L²±C"¦ævvŽ€ZŠ={ÆZ[­fÕî,É÷*åè~´|-†'¸Ô.2ª*“Ô’�¯A¸†+‹y!™CE"•pzzÖõ% räåÐä£ µáí¹ÚonÇ›|9�Kâ‹Ù@ <,Àw鵿 @_ݪô0÷ЯO¨ÅÓ-þÌâìüM©Íãétw’?²,Ž¡D6äs]¥y®›ÿ%fúí/þ‚kÒªqQjÝ�0S”ã.g}Y皯ŠõíW\›LðôeV+¹TlIà Õ½"ãÆÖºÍ¼¤m5¤¯¶Gج`÷^•ORðö¿áíz}_AS$ÿȰ¿õðŸÈÔÿ †/ A$`o•ݤ#©;ˆþ@V‘å�6®îcSÚTÅ:jM+ÈñWм;z‹­ÂÒÂç•‘dwÚËÆkÑl¯!Ô,¡»·mÑL¡”ûV'� †_ ]´ª Eµã'³nùUþÈïáUV<$î«ôàÿ2jgË:|éYܺ<ô«û+¦¯©Wâü€íëäè&·|)ÿ"¶�ÿ\°¾'ÿÈ×þ¾Gþ‚kwŸò+ißõÁh—ð#ê:ï³ô_¡~þW·Óîf�㉙r;€H®cÀ¾#Ôµæ¼ïBlØ›zç?ʺMWþAŸõîÿú ®ágßÔ¾‘ÿìÕ0Št¤Ê­9,M8§£¹èuç?ïažõoÕnr¶ÑG >ìr}1^�^_à;H®|as,ªìë#¦{6ìgõ5T\dä¯bqr¨ªSŒ¯Ð[ýoÇVñ�Bá&¶·È8®Õúƒ’?ì¼#âñ’f™.!}’…èOPG֯늯 ß«(`mß ÿºk�øVO“©ö¢þMU'”œ­fˆ‚�LaÌÚk©ÛjËk§\ÜB›åŽ&d\g$pjø‰x é°©\„ÆŸ¡æ»ýGP·Òì%½ºm±D¹lO õÇCâïk{äÐôXü„lo•³Ÿlä ý*(Þͤ½Y¦)ÅÉ'&ŸdVÐüi«Ùë1éZüyÞá ºmxÉèN8"½¼Ä·µÆ¿hÚŤV×*¨6ÆAÜ»¸=O½{†axÜé,ǹ9ÈÚ¤ø©Ãi¿I?öZëü7§C¦h6–ð¨Œ;œ}æ#$šÕ8B”dÕÙÎÕZ¸‰ÓRj*Æ’gË]ß{5Æx—ÆwpêØÚ"k v¼›wa¿º£Ôw&»F$)#“Šñß O¬ÿnÏs¥ZÅsxU™„¸àÉ#Ÿñ¨ÃÁJò} ±µeXG¯mÍ«�GÇšK}{ûØß ¨À½·‘]¯‡µÈ|A¥¥äKå¸;e�9ØÃúW3quãÛ«imåÑí r¡F^„cûõkÀ.©£G|š…¹…d(cÕ²FsÐý*ª(ºwv¿‘� T�eÌâû­ŽÂŠ(®3Ô (¢€ (¢€ (¢€<ÓVŠCñR'¹_>›iÇÝ^õétQZΧ:K±ÏF‡²”�÷w<ßâdr>¯`R7`!9Ú¤ÿz:ýÑô¥¢”ªsEG°S£ÉRS¿Äy®¹‡â�³ˆÜ¯�ÌãµzUS�Nt—`£CÙJNû»œ&¡ãÍKGÕnín´¡$1ÊÂ'ù£%3ÁèAâ°¯îïüy¬Z­¶›ä* ¦A“…Ï%›ò¯VdWR®¡”õdPª¨¡UB¨èÀ¤kF:Æ:˜O R§»9Þ=­úŠA\Ä�.íî-5KxÝÒ$ØåJä§5èVTê:ræ:+ÑU©¸3„Ñ<{ª_ÙØ6š�¤p’̬HrqŽ?:í®?ãÚ_÷ò§$qÇ�ˆ©““´c4ú'(·x«*sŒm9]žoðÆ9T¾/¨0�¼¤zEQV§´—0aèû|—¹åzÈÔ<'ãIµXíËÅ$�"3±Õ¾ðÏcÏò®³ÂÞ,¹ñìñ½€¶†8ƒÜ[qÏ®®™•]Jº†SÔ�iUUT*€ tt­'YN6q×¹�,4©M¸ÏÝnö1ªs„•£F•XJó�ÎâŒnöº~ÄfÃÉ� žÂ» £Y0~Μ÷E\¢”ª^ Š�ZÒ©}ÎKâOü‹ ÿ_ ü�s§®ø[LŠí-Þ›w—gälã¨û§�¥tÿäX_úøOäkCÁ\øCOÿpÿèFº#5 êúœ3¦êc+³Háµ=w[ñ´‘éövF87Q2A>¬Þ‚½BÒ“EÑíì·—æoï1äŸÎ¯"$kµTz(Å:°©W™(ÅYt0Μœç+Éœ¯Ä-:âÿÃêÖÑ´�o(‘•FNÜOëYžñMÄïm Éh10óƒ€9ŒW{MXÑ™QT·R3MU^Ï‘ –ûok[¿™[TÒoäùÿ šâ~G">£½r#Æå#Ö½Š˜Ô´{—:õcRû ^qðú9Å�hÝA�ðJ‘üb½"Š!S–.=­i8Îÿ KYè—ÀOÙäàºkŽø[Ç¥½rbÆå#³W}E ¥ áÜ%Cš´jßcÆ:eƫị{U-0+" þ,ⸯxÒëCÓ†’ÚSÏ,l|° VäçcÔרÓv&ÿ3jïÆ7cœUBªQå’º3«‡”ª*�•žÝÏ#×쵆լõ Q?xCùH§÷*§ð¯^¢ŠU*ûD•¶*†Q”�ïsÍ~#Ç#øŽÄ¤nÀB¹*¤ÿ¯I-§Sš*=Š¥CÙÔ”ïñ}ñJ9$:vÄfÀ“;TŸî×q§q¦Úç�ܧòfŠR©x(öQä«*—Ü+Í5ÝVðξÚÖ�I±‘wlÏÞV•étS§QÓa^‚¬•ÝšÙžt¿ï yØèfœHqŸ÷qŸÖº� jšž¯§Ëu©[}œ™q„* àsÏ'œó[B(ĆA‡=[o'ñ§ÓœàÕ£J�XÊó�þAEV'PQEQEQEQEQ\Þ—ãu?Ë£­œ‘¼EÁ�¸ í8é]%T¡(»3:u!Q^.áEpVZΫ¬xþ[K{Ù d\`ªqúŸç]íUJnLŠ5•TÚZ'`¢°4¿éš¾¯&›n³,«»k:€¯Ž¸æ·êeEÙšB¤j+ÅÜ(¢Š’Š( ŠŒÏ“Ê3 “û»†*’€¸QEQEQX>&ñL^æKG¸ó÷ckŒcükbÒà]ÙÁr¨š5p¤ôÈÎ*œ$¢¤öfj¤ÜÕQTõ]At­.âù£2,\ 8&©xkÄQøŽÒkˆíž›fúJÂÛrLS.åÈÁüjÚ—-ú)SuWÄY¢�� ³$ö®Sø›kopÑiöfåU±æ»íVúpM8S”ߺ…V½:*óv;š*¦•}ý¥¥Zßy~_Ÿ}¹Î2:f²î|e¦Zëë£È&ó‹*�ŠÇ ëžô”$ÛIl9V„b¤ÞŒß¢Š* BŠ( Š¡¬ëÚœ××{ÌjB…A’Äô“DÖ­uí<^Ú‡TÜT«Œ"«–\¼Ýö�ç侦…QRXQEQX~&ñ4~ŠÞI-^àNÅ@V«gr/, º PM¸RzdgNQRèfªAÍÁ=Q=[R½v�qzÈdF\¨8'Ÿá¯ÇâKY®#¶xO°†`sÆhP“�7@u ¦ Þ¬Ù¢Š*MŠ( Š( Š( Š( Š) ÀÍgêúö›¡ÄþàFX|ˆY¾€V~�ã� R¹[xî)áÉ´1ô¥qzU‹xßÅ·W²8·L³yÛœ*�Oÿ]uÚ‡€t;›6ŽÖÛì³û¹QØàöÎO5×*t¡îÉ»žl+â*ÞtÒåóݜυ?ä¤^¿qÿ¡W¤Üy¿f—È˰ìàÇ­yg€#’/yRÿ¬HäVϨë^¯K¥Eèv´_«9oxfçBK©õ,Ý\0FÝ…õ÷$þUÔÑEa9¹Ë™�Ô©F”#±ËhIá?íùÛI ß�û† ÏÍŒð9®¦¼ÇÀßò<]ÿ»7þ†+Ó«LDyekßCŸ>zmÙ-^Æiñ–5ì“t>Ûœy[®3×éT5hZmÉ·’ᥑNB›‚ŸBzWâX®gø‡46nc¸’XÒ7IE®Âßáæƒ—“4RM)Îd!³êâ´téA'&õF1¯ˆ«)FšZ=Í­'Z°ÖíŒö‰NHÃ)ô"¬ÝÝÁai%ÕËùpÄ7;`œ¼ÛÀ[ì|eub$%6Iµµ¸?ν&îÒ ëY-ncó!”muÉ…gVš§;t7ÃV•j\ÝvùžYq¬éò|DMYf͘•X˰ô Œã­z>—â+Y–Hôû¡3Æ»˜leÀüEyÍÆ�§Çñ4¥·Å™•TŸô+ž¹ÏZôm3ÃúV�$’iö‚‘v±Ç#ñ5¶#“•o{hr`}·<¶µÝÿàU‡ªøÇEÒ'0\\—™N8Wy_¯aKâýVM#׳6#�‡ð–ã?€ÍpþƒÂél×zåÔ2\»°ÊI =HîMeJ’qs–ÞGF#(ÔT¡dû½ŽçHñf�­KäÚÜí›´R®Ö?OZÚ¯%ñZhOo}áÛ¸ÖPÿE?È©©×X_ ÿäyÿ_û(­ßÈ©©×X_ ÿäyÿ_û(¢?îòõO÷Èz?Ôí«–)á1âýÙÚþgÝÃã~:ÿw8®¦¼Å¿ä¬�úùúEóskÐÓ>NM»êzugêºæ�¢Ä$¿¹X·}ÕÆY¾€V…y•Äë_äµÕ 0«•D'…\ªþ=}óJ”۾ȬMiRŠQÝ»,?ü=3íiæˆcï�«]}–ÆðM6ÒÛ|¶½EcøÃÃÚ,~¸¸KH-e�3Æ¡NsÀã®iŸ­-›BKÓg\¬�œ'ÌëÇÓð­)º|ñ¹Œj×UÕ)[¿S±¬M[Åú.�1‚æä¼ÀüÑÄ»Šý})þ+Õ$Ñü;su ÄØ ôf8ÏáÖ¸/ Cá‘ —šõÜ2\;�°ÌIxú“ïSJ’qs–ÞEb12�EN¿w±Ýi>.ѵ™¼‹[’³‘Ê»K}=kn¼ŸÅ‘øz6‚ûÃ×q¤Áþx¡'Žá‡§5éZ%ëj:-�ãýù¡VlzãŸÖŠ´”b¥ŸqᱜÝ9ÚëªØn«®éº ˆê7".v|ŒÙÆ3ÐQPê~'Òt‹x¦º¹ÿ\¡ãDRY�ï�ñ®Wâ§ÜÓ>²ÿìµ7†|k¨iÐêzÙ’îk„œ€‰�—§=*•*jšœ™ÄVu¥JšZu7ô�èúÕÇÙ­ge›øc•v–úzÖÝy/‰´¸|3â«S§–HÎÉ‘wd¡Ý‚3éÇë^²@5©Æ)J;3L-iÔr…Mâ2yá¶…çžEŠ$gs€ss|CðüR”Y¦”âHŽçŠçþ ß]jõ¶…nHA³åÏìxÏÐcõ®‚Ãáö…mj±Ü۵ܸù¤wa“ìJ�8AJ§^Ær¯Z¥G )YnÙµ¥ëÕ¹¸°œJŠpÜTújõfèú–„³Çbc™÷”fÈSŒqY¾;Õ¥Ò¼:ÿgb“\¸‰XuPA$�ÀV\ªSå�ÒêJ�.z›®Äº—�´=2àÛËre•Na]û~§¥YÑüO¤ëŒRÊç2�“�­�\wü+‡ð¬·ÓÖãX¹·–îBIŽ\‘ì1Ó=ê�ˆÎ�§jvº�†¯pmÍdát#=�¥u{7ȯ~ý?ë•cQòÛ·S×(¨m'Vp\/IcWˆÍM\'¬�Õ³5éš_·Üùnà”@¥™¿Zu‹®h:6£ÒnuÝM´Øæh­ßç¸#¦Õÿ‘[Mÿ¯uþUœ£j)ÜÚ¾&q²Ñ-z•¼_­ÙiÚEͬ×;››w¨RKc¨éÖ¹j¾Òlç“R‘íäÀ/l&BÇ9®¯ÆšM�æ‰u{q{‹hÄûˆÛøgÍøúN±¥ÜM¨Z äI¶©.ÃÐÖÔù{œ•ý«ÅÅ+m¥ÏDŒ£F­6 àcŠæ¯cð™ñdfëÚÛÓjáð[)8ã=+¦DXÑQFFôæºÏü•kúïò•óIëÐêÅÏ’Ñ=Vç¦T7w–ö6íqu2Cuw85y�Œ..¼Aã´H\¬q:Æ ôÜFY�Ð*ŠTý¤­ÐÓ_ØÂé]½Ò¿Ä_¤…“¸–ƒ[Úv§g«Z‹›Öh³· ƒèA¬[oxz9-áÀæI$`Oä@­ME´Ð­¤¶³ßå<†L;g€:þçì­î^äÒúÏ7ï-o!¾!])ôy¶qö0FãÏ<ŽzÓ|6ºBi4Nm76Í’Ýóžj��¿äP»ÿy?ô!Q|<ÿ‘N/úêÿΟ/îy¯Ô\ÿí\–[oÔê+Uñ†‹£Î`¸¹/2œ4q.â¿^Â�âÝVM÷7PœL@Ž3ýÒÇü+…ðœ>=æ½w —.çl3B�SêO½:T“‹œ¶ò'‰”j*pµû½ŽëIñn�¬ÍäZÜ•˜ôŠUÚÍôõ­ªòoGáøž ïÝÆ“ù℞;†�+Ó4kÆÔ4k;Çáæ…Y¾¸çõ¢­%©GgÜxlD§7Nvºê¶9Šñç§ÿ×GþBºíþ@võíþ‚+‘ø§ÿzýtä+®Ñäaÿ^Ñÿè"ªÀ�ÌŠ_ïu=‰¿äYÔëÝÿ•sŸ ÿäyÿ]Çþ‚+£ñ7ü‹:�ý{¿ò®sáü‚/?ë¸ÿÐEþ½B§ûä=êvõKSÕì4x×÷+ Ÿº%¾€rjíyÞÙx‹ÆÜkW‚ ,FT*�Lõ?�gF—;mì�qX‡E%ß}ŽÖßâ‡ç™c3ËŽHˆQø×K‰,k$l®Œ2¬§ Šàux| u¦I¥Å­½Â!1ÄÖ»ÝGÄšN›d×RÞÂà ¢Fá™Ï  ×ü%¦ø€¬³‡ŠáFÑpHô>µ“¦|6Ól®V{«‰/66V2¡Túdw®¹J•OzOŞOA:pI®�±Îxv¹ñ«\?Þ•%sõ<×ªÖ —„í,|E.µò™%Ýû¬ƒ>œ{Võg^qœ¯|Ѧã.áEVaæ>ÿ‘âïýÙ¿ô1^�\ö�àû]X—RŠêi$�0(øÀÜsØWC[ךœ®Ž<)R¦ã.ç™ÝÿÉ[OúîŸú,W¦W?/„-eñ0×�ÌÂ`áü±�¼.=3]*³RQ·D<5)SsrêÛ<ÇÂ_òQnÿߟÿB¯N®{Lð}®—®Ë«Gu3Ë!rQ±´n9=«¡§^jrMv”©A©wg˜j×Y|R[‹–òâI£,Ç G5èñ_ÙÏ*à Ü2HÊX*H u<}Ecø‹ÁÖ!‘n$w·¹UÛæ pìïPøoÁ6þ¼7‹y$óʨUäŽß‡­\åNpNú¤eF�jUd’¼[½É|s§Ë¨x^áaRÒBÂP r@ëúf¹/� _Yý“U··[Åcµåb¢E=9Î2:W§W)«|<ÒuÚxK7|–€PŸ]§§áE*±Pä“·˜ñyºŠ¬}au¤øÎ?2â;_úêI?@k¥³ŽláŽÕB@¨<µãŠãm~éñ•7WÓÍŽ¡ ?Ì×mki *(P=…EWYI³\ºŠ-"b@£¾Ó’?,×àÁá{ËSi«ÛÀ·aÎÉ%bŠz çêÊêÿ´�Ná®"i,ärK°TŸ\éQJªQp“·™¦#7QU‚Míf:G�ìâó.#°Eÿ®¹Ïà tvQ[Áe vŠÝPy`t Ú¸Ûo…ÖAº¿ž^y¡­vÑF°Â‘' Š~‚¦«�¬¤Ù®3M¹AGÐà¾*«Ó>²ÿìµ×ø{þEÝ?þ½“ÿAWÄ~¶ñ ·ÃänÇ—Žs޹úV¥�ªØØÁhŒY`Œ fê@¢S‹¥õB§JqÄΣ٤yÏÄ�ùl¿ëŠÿèf½1~èúV½á ]~þ+É¢@¡S89î=ë| QRjPŠ]…)B­I=žÇšxêô¯Zë ›‘ö:œq¹#òÅvúw‰t�NÕg†öã,’8VOb [ÔtÛMVÍ­/aÄÝ�P}Aìk�›áu“ÌFt�<« c�cÅ_=:�JnÍ:UèÔ”©+©t:ëVÇS2‹”¸�¡Èû÷¬ˆš|·¾óbRÆÖQ+ýÜ,浜W%©ü6Óï.š{[©-¶æ�he¸éŠßðþƒ‡´ógo,’«9rÏŒäãÓé]åNo�=N$+Ò^ÎKEÔæþ(ÿÈ.Çþ»Ÿýº ÿÈ­¦ÿ׺ÿ*OxvßÄvðÁq<°ˆŸx1ãž1Þ¯i¶)¦éÖö1»:@�7SŠ™M:J=M!JK*�f‘KÅ_ò+j_õÁ«á‡ü�.¿ëãÿeÕêV)©iÓÙHì‰:,½FjŸ‡¼?‡m$¶·šIVI7“&21ÛéBšTœzÜR¥'‰�N‰ÕæzÏü•kúïòé•ÏÝøBÖïĉ®5ÔË2:8ŒcoÊ8ížÔQšƒmö ])UŒTz;�yw‰ ¾ñúj�x�Öeÿhckï×ô¯Qª:¶�c­Z}šú"g*AÃ!õµ*5%®Ì¬UZ‹Õjˆí6°—Qð½Ìp)i#Ä¡GVÚrGåšãüÔ@?gå\§Ã;ûH,o-渎9L¡Â»‘Œqšïd�%�£‘C#‚¬§¡¨®-þX}¹f†úháVÝå G9OŠs‡³p“°b)ÕöÑ«M^Ú¯Q^Agm§è¾.šË]µZ†dË…å_ŽØþuì‘®xgMñ/Úã+* $Ñœ0ö÷Ʀ�EÓÙ—‹¡*ª2Žë¹žº‚š!2Å`c#;¼î1ùÕïÃáøÅÃhK7•¢$‚@ÈäõëÚ¹Áð²ÓÎ$êsy}—Ë]ߟÿZº� @³ðõ£[ÚHÛ�¤l’qŠªŽ<ºI²hƧ:r¦¢�J(¢¹Žà¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(®?YðÞjm©i—Ícpͽ° »�AWaE\'(;ÄÊ­UVš8gðŸ‹.ãk{¿~ã§ÊÌK~ŸÌÖÿ‡~[ÃÿÛ„      ## /.../5555555555  !!$$!!$$--+--5555555555ÿÂÍ»"ÿÄ0 24@P!13"5`p#€$� 0!1AQq�‘±ÁÑ"r3 @Pa¡2R’¢Ò#áBb‚p²SñÂð`€�$ 0!1q�@Qa‘2`A"Pp¡±ÁÑRbr !1Qa0ðAq�¡±@‘ÁPÑñpá`€�ÿÚ ×‘ו6­n¶�MY‚×ÖžëÍo$úŠM�ž]ªYöˆd?|çœsójPüõÙ.|uO˜›SµÊ]ÇU:Ö¢š,k­î‰z¦ƒ?ŽhôìÎ –@gnÎ/Ô^!´g‡ÎKZ{�6˜Ó¡aöU>eãVºÿ:MÔ±¹Ãû=™×qÞ¶úF¶~½Þ“v˾öÝ{¿}œÍMÇ#K<λú6òd59OF^‚:E^l–—ý�iÐåµ8í¼w=’ë´d‚Öñ½RNdE `8öóûæE?a×ÎÑ+ÖÖ]ü[AÏ´}lÝbꀩÛ+2Ç‹éy¦�«Ÿ£ÔmÕ,»ô}8Ѯ֟—€Ÿ×¡ãï‹¥æï›²ƒË®t6t÷ÍžwóÛâ›!ÇV'‡Ý�ç <ÞùéC;æe &PÂe &PÂe &PÂe'ÇAç |CõÌÚŸâš;ë;üë�žúüöî­LÅ'°GØ\øše {2†(a2†(a2†(iyô¢­ÍÅ—ñ¯£¦372ê¾Üwë¨ö&ccš•¬Oœóôc²="6s6Oß«#KùÎnJ¡¹“ýË´Üm>‚)tæQ£KçaÌ7޹éE½e5§ðjž½4c>à|Åu亥4Z »Ç4ZTe,vxè”±÷àIÀzíñ<ößiÉþ«M±¨·Z:ˆûCLüõÎ8éÏk,=-§æ~mЩdù¦IÅš£YùЧۉbôßÁ/mÎÐK²³->ç´C(m v¤åý`El‹�h¶s'Ƈ͈.d�¡Äò¡ôa_@NÁ[%«xŸ$jÅ”èÒ±]ñÍ.>ý˜žñç�¿çŸeÖ›©Ÿ/èëÏ?¢Õ¤A/’}åÊщν~mLûƒ!�x!�¥Sîýlàž «WZ<Íiò�Ž£ï”¡n¸��sî±íÉôìËþ¡^` ÚëJ×Í £[OÌ4üÛ¯?Îg~ˆƒR€uà;ðxÓ&qí/6ô°«`‹Ëö<Š–÷Od‹�h¶s'Èëÿ;âÎþ¹æ}XEhz¢é63fÆ�ÍiטçKî«�£NcvÓèhYŠv>Îz#k2ÇiÎ||ô)®Þ{¼…`ª}Ÿ­œÄ ^}‹²Úå(Ûñ{JvCÏ@Ì"nÔ�|К03 çÝ“ö¯hÇÓ:!ó-‹ Чð.Õ[JÍ}Ô­GFë^=~Gžëý¨×œ} �°g=> XÍú`.u¢ÙÌ�ˬTΣ šà5Œ«`¹‹ú.á¹G¾eÑ{,-ê¹§Õ¦2Ì>Þ¾ù²ÎÇÝ"ñ?÷NÈVœ U>áOÖÎ âY;ýi m¬ëÁÇ@mªåZ4‚å`f±�é4­N û€/Ô(V«ÕÆ�:ò<sß Û+¹ëKùã¬Ùx¯K@š0é­d7Úvm:è!&øs.D2>Èî‰í–¯›™ ð†©�hº:r€ U>áOÖÎ âï=ñÝ&¥sî~~”ì€çȶõq�¦d~y8Ì�´{�q'’·�GõÛSR¨xïo=Ë£6O W2•ª¯W[äq0ëµbϧò¡5 •O¸Sõ³�«¿\Ãß»3/„RÙq«Õý@^O+ãïXCLÅ÷Æ\6³–Óó ?6èT²F©ê~¥³ëåãer뇬fЖ þWׄv.u¢ÙÌŸ6#¤^u—ÅìTZ;õqW[K™òúµþ4:ˆ¥Vt µ|SK!Ï Ùq«Õý@5þÜ{aꇞ€2N3¾rѵ–¶Ÿ˜iù·B¥�œwdȯTó‹õ×ûq퇪zívÅ]ËúàŠÈEδ[9“ãCæÀ¥Ôö*›¢Þ8/¾z•é^ »âh´ŽòN4¬ôhÞŒÅçºß«ôGÞºÎ&á–Ù�k<ð…ê ký¸öÃÕ=dœg|å£k, m?0Óón…K “[É.Õò� `ký¸öÃÕ= ö»b®åýpEd¢çZ-œÉñ¡ó`ü?|êmºÖ ÷Êý@ï�ýü< 5þÜ{aꇞ€2N3¾rѵ–¶Ÿ˜iù·B¥�I­ä—jùF…05þÜ{aꇞ€{]±Wrþ¸"²Ñs­ÎdøÐù°'æwù¥H-Ö?m˜qÐ1­—½P/Ô_íǶ¨yè#$ã;ç-Y`Kiù†Ÿ›t*YdšÞIv¯”hS_íǶ¨yègµÛw/ë‚+ :ÑlæO�›xýò‡ûù³˜òÒs�‚•Ÿ¡Ÿtc[.5z _¨¿Û�l=PóÐFIÆwÎZ6²À–Óó ?6èT²É5¼’í_(Ц¿Û�l=PóÐÏk¶*î_×V@.u¢ÙÌŸ6¯h¢OTkgïÕ²ÝK:èS²1­—½P/Ô_íǶ¨yè#$ã;ç-Y`Kiù†Ÿ›t*YdšÞIv¯”hS_íǶ¨yègµÛw/ë‚+ :ÑlæO�›�h¹½ªðCNˆí_×óît­ keƯT õ×ûq퇪zÉ8®ùËÆÖXÚ~[©fÝ –@&·‘]«çÀ×ûy}Xz¡ç ž×lü¯®ì€4\ëB³›b4=Щ6`¨ J4|âr t‘“¢1­—½Sä_¨·ê�’ÄÔz,ÕrN3ѳ˜»WÇö û�JÐ×r õõ@5o|<Æ. qÐf°²Q¹?bL¾Ð®“и /—^°ðõó³–€Ð,¸íÿ:íŒS² ‡@Ï4)ój€¡Xóý'D ” }Ä5‘«ž\Èôºvf†uÐܳCÏ4¨…ºàhZMÛ'D ”åóܯ‰‘öÁàf¬ÊÉ_O¿ Ý{PÌ5sÿˆ@jÇBA.›õ˜#“@®A$�÷ñîš?÷À>´,í›'‹(VšëkÈöäý,2Í;!»WähSv¤ÏÃ.Œ24@)5 ˜mŒÐ–0&tÌ{_ιô)Ú;LŠÝ8eý`¿‡­‹ê&[[âîP/ü¥�HÇkg^Óç½ôŸ�~]üwâ×TѤpÚ<îN€A4&kr¦êP 0ïÁãeFÉbj‡ž€çê#¯3^fÖPz¨e÷z³Û†eð®‘�ÔØæ(ï€ÚÝœhú_0瀓5ÕxÏ@°×´è‡|€~Ù8ê#Gõvͼæå–êþåg�R€{àÁçÒ*Xê3ofo ÷ñ·”x›fs£ehy€O¸f¶`„”;׎¶ÏÖ. •Ι�ôÁ_D:ëØæ�sX]Á3ιËâ¶X땲µæ.Ä5¤Ï.ø‹J÷{´ÊGÝnýë¯4<ÁRÈsèyèúXñ¿Íb¿v­bòOBO«Èu–_ŽèÖËw¢¥��²¥�sœúçk+Õrf²2f²2f²2f²2f²2f²2f²2Í{Éì¯0W˜>2 ‡Éb™¬¬C“5‘“5‘“5‘“5‘“5‘“5‘“5‘ã˜çÒ� ç¬ÎZù§·“µ‡2dí`dí`dí`dí`dí`dí`d÷;/Y+÷²ÿÚþ�K&`™@Ò@6Ú“ü¶É!oqF7”òˆ%d ëya!å„6k4²‚ˆ ŽCÕ&•’´î«ø~)?SJI qÓPÏ Ë¹‰ «åü ‚øQÆ÷DZ¾7¾6¸ß᥸¡¤±¢¡¢¡¢±¦±•Å Å Å Å Å Å Å#,“1¤±¢¡¢°hQPÞHÞHÞHÞHÞHÞHÞHÌ�¸’ðÖPÖêO}Œ²ZVƒI†<ä+Ñ’üŸ?LF`™X&2�HIQS)0¤qäxßâ A¨%”–%6“ lÓ‰ÂÉX™âdANáGÖUèònš-*© ·’@ÌÔm§tœVò°5ÁˆÔD ò x� –aÆ÷q6y§üM·¼ˆ¨¸ÞX^,lñä½ãÂ×Ò<� %ÙF[ÚŒ!²HuÌMp`3" xgµ³Í8 ³%'#ÀÁúbqË"ÊšÓº{]/ÇŸMª,Ë•x|\Ï ·³ÆÆ%–iÀß'\4ƒ3< »–5iÀ~øHŒÁ24Ò4’ �i2Ú߀k�K$…¬ÕB=4ñb‹ 'ši/‹ øŸâÂǽ%ñag‹Ã‘dX_?Lé§ßüXX÷¤¾,,ñxf‹óÄÿ¾ôÓï‰þ,,{Ò_x¼3XŸ,1ñžñb‹ ø�ßjø°³Åá™ãÄêsN1Ÿ¾Ôqbß Õŵ\XZâðÉ<�­àa&XÝ,•µ¢üñ>^˜X/Ç¥ùÑG‡iY§¶“ ÒHg�h% êR�Œ7 4al²N'Ë×c‡’q‘æ^ Ý2<é8æñ¥[Å�Õf É~8̲=¤Yž7‹ñØñúchÿÛ†�J#Æj" pÕ±µîž'\ËiE�âÉ[Y,Õ�E™ltÿ,lŸ‰#2 |¨1¼‘¾�n )ðfg�+4‚y#}#y ÝA,3~/µçˆˆÌCM#q!Ä‘S?lM{az›¿‡uXˆ³4–Eµêg퉯l/ScâÃ7âðæyž‹×ÔÏÛ^Ø^¦ÇņoÅáœáÄ×¶©Ÿ¶&½°½M�‹ ß‹Ã;Éœ/SW¶&½°½M�‹ ß‹Ã9ɳÈð=Œ½¶«‡8žÆŸm­Má–_ãá”Y–4/¦XÛ<ÓµÎM{áwß|;³ž ÈŒCMCtÆé‚m@šY`RÁ´cuCtÁ6 –‹iûiÒ1¤cHÆ‘�#F4Œ!;¸�á¤cHÆ‘�#F4ŒiÒ0^Á”óÉòG<‘Ï$sÉòG<�ô¤¸�êÏÿÚÿ—.:ÓIvû}ÆcÿE#4÷Ã]ÁA™Qß/è÷^m”L¿­AÇ\uXHÌŽ%îS",Èò‘ý6s0Û—1ùnñ$ISºa»-½ë`Ú-ËvóF%[eÅØÓ®4»mÕ‹ú*\¦â±&C²]û¨!m8§Ùx8ÚÚZTi;UÄ¥·ýu›ÍÈ{Y ¶OîH þ츸¸omwMÙ »":hZ@¹[‘1¥¡HTwÜŽôwÐû?Ð÷©Zž3 )IBo]Âì³Ùºf[-—y6ç!Ìbcý4Žß•’ÿ¡ï¯êMØÜ´1ÝWCÞ«4‹’áØí± ,„›lExíÇ!¤X®‡XZâ$2l?ãbAC>æ«ðZÖ˜u,²ûË}è×6\xíFgq[`íùg*Ø;�­ÙbÚæ¤èWÕºÀ±';€î' qÙ콇ºY'-C³œüq§üÅŒó·ÿBÉ,ã‹ åöx+ôò î)AÆ¿ÁPjt7¼ ‰,FGÜÛGÜÛGÜÛGÜÛGÜÛGÜÛGÜÛGÜÛGÜÛGÜÛGÜÛGÜÛGÜÛGÜÛCWH/9�kB ÛÔÃ�Æ�®á–cïg�½ž ÿ8‚;�Ð×pDPfdWë¹u€ÚþæÚ>æÚ>æÚ>æÚ>æÚ>æÚ>æÚ>æÚ>æÚ>æÚ>æÚ>æÚ>æÚ>æÚ�Ô†ñJ»CŒ$_¥¸yçOT¤›Wií7Å>î+»:SÈÌŽ+å"8î[qŘ,}ÄQÒÛ�º�—Žäf:T¥)VèNN–Ûhi±>G1-´)Ç‚B*¹!†ƒ—›z�ÄÑ;‚Y…Ý® yç+±6S?p¬„i±¤•[Ã:°+Zc†TèÑJMþBÃŽºê¨ÆºÍŽ"^â½VôΔúÖ.ƒ©¬DDÛ¼™4¬ˆÞ¸b¿ÆßdXfn¨LˆÌÈ÷+d‹{â<ÉQŒ»’ðE&ép”AˆïHvÍiEº8½LÐŒ,QµeQRÒ‚víol;ÜMvý9aɲÝðéQ¤á_\@iÖÝE5¤–—m¹VÓûªRP™÷ÕR”£« é"!ĚĴRî&snµ‹ Ûrº·�yÇ—K·Q›ØœB\DÈË‹!*4ªÙpLÆ„ˆìIji:“~Øæ�!ã…ÚÓÞ8È�dµ™R\”òRjU¾!D�G¸øü ‘•8sŸ†äI�Kj�é­9õm?±Ù"CQšŸrzb¼/:Öë›sFèÖ¬ Ö.ƒeÖäQ¥)J§Û¨Ê>;¥¼¦2di6�q—-÷feÓm‚dæ""lç¦8,–ÝÚ}ÇÇTˆÔq¬RÝ Y ´ËM…%*+¥�ŠQ%»è²[’Í.âkñ«iýˆ“%¨­MšìÇ|n-µÛg¦c4 ˆÉæÍ§ªØº>j!°ë‹uÊ–TnÛè]-E$”•!B%îK‹Ä‚V•�Ï õÊ "Wp8 µ­Å ]œÖtû�Ž™™Ã±¾è� 4RÃtŒQ¦RµÏ8�‘‘•»Z¶ú¶ŸØºên|ç&=á"Ér+켇ڡ{kN}[@µ¥ ¸LTÉa#rÖÖ&»l¨§°ŒÈù™µ«lx¯ÉT 3Qê÷(VÙ2ζ4BÇÜM~ìS5¢´ФšURÓûÍÃ]ß a™¸åâkÒ­‹ ¿M¬„﬋"§"Ï ðïo>Av‹ŠG×N µ\°LP�aˆØB„Õî>: ¶ã«ƒbBP½£zßNƒ�$Œ”Tn�éϨۋm~µ©µ°ò_gé½û}[d„Æ´8â�r­¹󼇸øñÁµ¿,âB�®)ÞƒRÊþ¬=ÂÞR«±\€×nºaIe¶…Y-ÆíÖŒIµL�à;}ýøØä7ªÅUÉ3‰ZÄ�éþCÜ|xR•)Vû""*r 8õ;yí×è÷?ʬ;D©"-¢z­ I'Øu‡*Ø]Ü�BB7$VbÙ9ðßn¼`»q }¸€¾Þ’Aëdæhvâ?ÓÈ{��HOË\kÓU^©©ksN}òs�RÔD«…+„LehRR ôæPº'váRßivaF·D�ŽD²D»¨ J’¬¼Œ¢ùqñí·YÜ’e¦QâÚVã´o%�º¥§ö4û‚.ã•{¦G™c½–W–.‚ŒÈå¦uµøjÛfFå¿È{�Œ%*QÛ¬‰G�l÷›¡u,íõ-?±§ugVVO6±ß‹þýKAIIJÓs³›;b#N/�÷xÏIrßlfxö>>‚¥§ö4Ô’RL²:Œ|8ïýuKARíiÈ4�÷<Šål9ª‹˜­ù ŸARÓû’ )øqßúê–.‚©Ùãó~T¥%$»”_-ä¸!ý !÷ðA_`MÞÜ ‰QœðL|4.}KOìjJêj1ðã¿õÕ,]š¿.4r¸PAë¼÷B–µ�ä>Ðj÷=°Ïp´a‰±dVcá¡sè*ZcRWSQ�‡ÿ®©bè<ÎTèÑ.ù%Ðfj?ÇÃBçÐT´þƤ®¦£;ÿ]RÅÐy‰™O¾ä¥-^9�†…Ï ©iý�I]MF>wþº¥‹ ó­ ¢åurYÔBµ3džàGníØ ûzWn´·Y�†…Ï ©iý�I]MF>wþº¥‹ òõ(’›¥ÉRܩƷCØa> �†…Ï ©iý�I]MF>wþº¥‹ òûåÃ}U,ö²IxV>>‚¥§ö5%u5øqßúê–.ƒË®s9HÆfgNÑš•á˜øh\ú –ŸØÔ•ÔÔcáÇëªXº.¼Ê×™RËF†cá¡sè*ZcRWSQ�‡ÿ®©bè<¶cü¼ZˆI©HI!�†…Ï ©iý�I]MF>wþº¥‹ òÞáwv5Kzw¦øv>>‚¥§ö5%u5øqßúê–.ƒË{‰yÉ©lëü; ŸARÓû’ºšŒ|8ïýuKAå·ÓÎ}H*Ý™áØøh\ú –ŸØÔ•ÔÔcáÇëªXº-½þÆ¡¤ÛY8߆cà¡u<­õ-�}I›õ"t¸ï½}KCå·ôå:­’F¬/ ó‰Bôy[ªA=Ùµyª¥¼óƒŽôyÜjvñÿÔòÞãGúU´Lå¥Q2Èê[½…ýYA¨Ú·¦ú·Y«gVõ»Í[Óêvâ½<¶üÖü*Ök‰>Ý“ºõK»Ð¨wÿ ±—©•Í{�*öúó‡�åê=S·—”¿-}¢y•$Òª¨Z�«uṊávf)-jqu,2t¥Pî7¥Õ³9¿o¥sv^Ýsý1JsJ5[Sšw.¾FÑ—^-Þd`×pÆP+ŸÁÝ­Ä2ß®Fi8†Fe“Óá²$÷ H4³[[nNjΫۮþ»‰Ïô«gwNáŠøîä ¨Q¡hQ->[t‰ÍEðÅ ÞOÀ6ƒqÂ""Øë„ÛFfgVÆîäúW‡u.[Y¶âTJNâwÖµ¥ÝH]{�¢ï„ƒ sr+jˆ´) ¯ecVvÛÓºvúÌ;¤ÿ½(�—n.µ¥ÝXo.ê\+vë¿çå䇛� Èox(ñÝ’ì(mÃd_¡î9^Í—‹·¸�õ¯mwZ ³ºP+öë¹·�J$¥Å›ŽV²;§?ËåEjS3`½ ÏÒÜ…˜mìy¤<ÔÈŽD~­¢ßÌ»‚ðî¬úýºîlÐî'öG´çà»»¥».N¤ÉEåï2Óíϳ½¼#φYi„`› ©�JˆôWiÛ­®Ì[M6Ë{\Y6Ú”jU{Ús¨^^ÕŸ]¥›n%D¤íî'½<¥íXc2Í@•k™œ[4ÇÄ;TX´ŽÌ†æØßd4!kT Œ! Bp^ÝÓ�àpÚt�2ĵ’µš×à-›–ý·‡ugøÝwÓÌß·C|;Û­_oÌHUšâCêî­W ²ÜL#·¥k·£$14z’!E’íÖÌ.Ã9 íY<žâa¾ß˜ ÏoGHf3 '�4ér0‡#r0‡#r0‡#r0‡#r0‡#r0‡#r0‡#r0‡#r0¨—#r0‡#r0‡#r0‡#r0‡#r0‡#r0‡#r0‡#r0‡#!¶ÛNÕDˆ¥rP‡%rP‡%rP‡%rP‡%rP‡%rP‡%rP‡%rP‡%6à ÿ"?ÿÚ?ü�­5eJ/çÐýT…Uû+•Z¯áL[п.Ëâ5Tî­<׈_^Ë‘ ÷Rô&Af²ºäÇuý—ËB¥Í§¡KnavæŠ Xv*Ø{ah^'²ñ=—‰ì¼Oeâ{/ÙxžÊ´e²ö¢¸W á\+…p¨[@½•›˜Y.…W¾#¢uSä€mäªW5a”Up0XQV®þ—Nn/Ó •È43ãÉ|†«¢¾ŠrR¥s%~î©TR4mWGŒ—E!bÍúD˜_¸SÛÙTae̯ˆÕÁ³+ëÝ�²*M!àtª�‡&dýÅå³ íú+«…yä©@ðÎeU§7 Ú78W^êð'É„<žœHÆ[{¼¸´>AU’6y .�U=•••u\GWñv�Æo;*C9»GL[À€.¦`îÒÍã'eÚ>c^’©^ø{ªÏ%»Hc7Œœa—iÅOu81›ÆN0Ë´âfnêªutg fñ“Œ2í8�‘ÑÓS,ݤ1›ÆN0Ë´â$à9·v�Æo8Ã.Ó‡7v�Æo8Ã.Ó‡Ñà·v�Æo8Ã?‚eVîÑåƒ7‡œØsãA�1fÍç­X µy„8Œ©ݕʷxWBXòàðq0'ÃÍLB¥…”àeL3�&,@�—ÑL>«¦!|@��Y”1ÊMØvWW á]}GuVS²­¸Wì©LAVVVVVVVVBŒ4VVVVVVVVDá9«««««««©Ïò·ÿÚ?ü�¥pº¯¢:*ÑYYrÂFþ…ø©�0å’¹_në¦ô&x~Ë™ÂbËãÙMè@ŽHbrA—Weá\+…p®¸W Œ«/Ãwb¼7v+Ãwb¼7v+Ãwb¼7v+Ãwb§»i‰2‚C™¢ûo쯻ôþ•7÷ t rfXe�Äj¾J\Ñ/ ÂðkUGJ¸Ur‹´éÙúŒ>;DÊ™ûný4nü¥ÞŒš’ë…Ê­è_ER©Wuq~«ª¬·vÝ_¨@ •/rìÈ –nê˜ôS:8¶Š¬-š›AyÎ$Ø?ØIú…ÿ¡¹¶OØ3.¢²÷ÛË:*C”ehæÍ§‘Û·Ûå3”9ã2ðàÒù•FL]á»O0 §¸È/¦ÝwK˰ Êy…÷Û®ßéOiž;»p%R†r~®ÎÏùÔ6`ȯŽúnçìpÜy“ÀžJ�D2ý\!ìÉÚ‡�»ý‡Ôÿ/5f[ ¿W{2v£Š¢¯_«„=™;QÄÈYÔUVVBPËõp‡³'j8‰:JLËõp‡³'j8‰¸–ˆeú¸CÙ“µ9yh†_«„=™;QLjeåÂÌ�¨ü <0¼¸<3gü‡’áÈ�"ÉpF(Æ\ß¼tâ3ƒí ÈÏ^F;sŸho#/À³R᧘×R]ÚHÙ£?)…w†²sˆ mAI5(R<žÂ²(X™I ˆK­ö“ÿ“B^GeB§ô ¡=·uEË<kÓTž“`p(Ì”ÒIµ�ŒJ>5DÔWw££†y¨3 é4{m�ËFû �'6�ûhй`ð+⪥k"í‘ýаÐÑ¿YàB>㬻§ƒÿÏhÑ[ØÂrðN�FÛtâÌ ‚×þE뺯DÂ=öR¢~é¦WˆSä|b\`v’{IÎ8Yÿë»C‚Õ¥^àRJT&7ám*´¡·‡Øg½f'è;ŽwÔN 6j*Üž¼®Ëi*7„ð·—Jœ%Fü7ñ‘÷šM¡dà„°ÈÑm`8ºM ˜{Y"Ñû“ÀÙQ�mûjþ5uMÀ—?Èž±G*üfðêäþƒ8«I'«�'º•™xšµh§ €=\!óZ”�å9?ÛÆS–YRT/� ¼%»E ç*´T0Í›�=Xÿ Î‹hV. ©9øšÆ�ó’Ý�E0Rö÷p2?#‹€m+ÿ\Îh¬ò0ÏŠ7ÞUf�÷•Y¢‡‘|ÍŽ'I Ä„Öà Ö“6eV Ññjåày‹. �vÇ\Lh"¸CªÝ«QˆÍãLN*â£á6g k9´j‡ì5¾¢»Ý‘‹�¶û‰ŸÅÿ ‹cKÄgäšî¡7T"—�¸ Ä"�5\È�VIºfϬ�uSäB�ª>Á{ë§Ã§Ãa½õ�fÐnN2˜×c¾‘­¤‹¢|SǶêIµ<Çä!o+E$Í<ÄÓ]ˆÞùUé�ï•^˜ÞùUé�ï•^˜ÞùUé�ï•^˜ÞùUé�ï•^˜ÞùUé�ï•^˜ÞùUé�ï•^˜ÞùUé�ï•^˜ 6æ’ÕPÑVQ!¤µ‹dͽ3i"~º{l“Ò£7P5P�„åŠÓ‚+O†)7FcìƒpÍž=Ä©cª=§£jÎL¹mn̤™ˆÑVhÞùUé�ï•^˜ÞùUé�ï•^˜ÞùUé�ï•^˜ÞùUé�ï•^˜ÞùUé�ï•^˜ÞùUé�ï•^˜ÞùUé�ï•^˜ýŒ«IOHÇÇÑ*Ó_u4á5DÍÒpœ&'ueg¤ÏÅ�&clE• JÖÇL!—Ö]I9ËK×ëë‰Åbðû…7lð��ìüŠn/îëàæhPÛ�ÞƒÑm¨-¥$Î8K? ‡_4iŠRœæ ”gQ¤“nñÑg´m&É„¶�2R:�Çl«pP!-¦µ�øJI ÒÞã‰EÒo4ÏâüG¶Ò•´fÅ�ì_ytàH·í½lTnˆ-:4T%´?È’0kd‘qÕ(`2ó¡²mZ£®=ÇR›€œÑKÇj¾EÔÏ”F£‰UÙÆxÖh‘m:ؤ] rs“‹ ÐÚQ¨]�]g,¸rZ–#¢U“mZ>-\²S÷V1–U�¡8ž£pÉÍS‰ì+%È(P™I3*Ê¿1=ÃA‘x~Sá¦Wö ÕSÙ7{h�]õRxþë`žõG‚¯Œb{‡µõ‚• ”+е÷—ˆ üÄÅÅdâÔg­W 6ÒtR,X…÷H8$�èÑ>a*ÍÓˆÊ'䦧5WtUÕ*±È9Òˆ Q´©/pL« ˆÖÖnÂÅWíqü§8Iæ&.+'JDäÔ~PÒU†ì ¶ùzUl"õÌ¢U›§”tYHÒÆ™dÄb�º‘*6•&R¡:M`Á{ãÒ×Ü‹)úp´Žê:¹‰‹ŠÉ­¤Îz‡I�.ÛÆµäÀ�‘ŠEí™VnœFP¤ÔDÑ4ª6F)°2Ê�¥JŸ“ñ…­„£¼@ÃÌm�=‰ç¢zãõ´&M“Òy…#‹Û2¬Ý8Œ«‚Ò•ŽU#€ØeFÒ¥‡ÉI)˜…h §×:ˆH¶cYäÞ3âž(QUÄœ³GeÃxz£°æú¢¥àâµãš7³]c¨êqC‘#db‘{fU›§•wmXåQ²1H �–Tm+�§ya=pW|vô¿%QÔ#y -"Žºâu’£lÓ#í¸¤\$E*ÈeDÏ6SÒšsG´àQîÔpdlŒR/lʳtâ2®í«ª6F)°2Ê�¥s¤îª›£z4YöQÑÚèÎMdòÔlŒR/lʳtâ2®í«ª6F)°2Ê�¥s”æ€+0Zø—ÝôÁRÎ’�dòôlŒR/lʳtâ2®í«ª6F)°2Ê�¥s‰ZΊSI&?[zŒZ²®“+¢€T«B˜œ¤6?#�Oãׂr“¸³rlÑBÜÂ=1ªò…ÐhÕ|©›)–FÈÅ"öÌ«7N#*îڱʣdb�,¨ÚW8(Ì$Æ‚(a=‘o¤Ê‡>LèE„}Æí¨Ñe£)äˆÙ¤^Ù•féÄe]ÛV9TlŒR`e•Jçõ:©ÞfÕéQòŸÆ–ÒltžžL�‘ŠEí™VnœFUݵc•FÈÅ 6YQ´®o*Åj¢í»Ñ9®RunÛÖVAÉѲ1H½³*Íӈʻ¶¬r¨Ù¤ÀË*6•Íå ê5ª.Ù2©WÜî±ÉÕÉѲ1H½³*Íӈʻ¶¬r¨Ù¤ÀË*6•ÍÎ=e"‹µ¹P‘ZŒÂI Ü�#‹Û2¬Ý8Œ«»jÇ*�‘Š@l ²£i\܆¿È®¤ÿÌ«óOQŸ”#db‘{fU›§•wmXåQ²1H �–Tm+››Guá?IVv¹B6F)¶eYºqWvÕŽU#€ØeFÒ¹¸ô%#,«'óN>P�‘ŠEí™VnœFUݵc•odb�,¨ÚW7.âqJ‚+—K‹ôòvöF)®e¬íJ¸#ŽU�„â�;"WùœœÝ?yã%‚i­Sr±ÉÙ6МBEÞ�î¬Í8åI·*Áü0 ¤èÑþÑ*à´¹ú‡72å°S‚œ²Ó+v"-J²°Q"y`c2©_tƒ‚QÅwROT³]ŒÈ<)°Q*ú6N>nÓ²Ú�¼hË/þ»‡ÝE_�Î$V›J#®WBËj"ñ¦E”[$à›<³Kï!'ªMãøÍâÕË,¤÷VzÀ�ZûÊ' žUhï#ܶ�Þ‚•PEK Ì¡H"o�‡­ØW׎PÞ»Ö¬ °¥«´¢IºeKJì¼<©£¸ž³L³vÑ:zóIèwÔ ežjØ ÁF^;®wROT³FÙÑñQÍÿ°v¦ýž@?ì@ûU�×êƒÑ¬2Fök æ�ð¼ Ä#ÛJœ8\h¤þ¤ZMw̸P ŠA€—”xW=ôƒÏE¸÷Hèžs€DßþKÌ! "b  ×x�+ò›Ã«’YÖ­¡~ƒŠM¦­£~Œ’ÍÚ\é7êëã¨�„åÉ,+Iœ^€¡R„âÿ7�âu‘vÕþN~_Ø£õÃÈR�Zˆü*+pÔ€U‚˜œÖe‚l8 rä“rÒfH½_\²\ ‚/SB£Hã2ÍÕC,»FÊF‰þ4bæÿöÛ•ô+ëÉCi¡5­V„ŠÈ)ÑŒ(L¤™ˆ¹È£ÙkXäâ.ÚæH¿_P—C½Å‚H¨Ô‘9…,Ö¢I¿.Ѳ‘¢�¸Ë´‰’/W×.ë6ˆP¿AÅÍêiÁ:UA ªR{ ¶9i¡:�WI�ÖŠOÞ«g€|¤ Uн«wù’·Žë–¦n¬âyK³£¢�.›*ÕʃÕÈfÑ è8¸¥F¡I…8kY$ߦ]"À§(ÅΧ*°lƒlF‚éIì®Áä: ¥XcA¨ö—dð©§éU K«íU±-û\Ëuô›\W-#P^¯¯�8Ïté ÿñ"Ó6çQ½@ä 'ëW-¯P_¯«�!ÁZV G8ÝN’Mˆ.5î3Ö.Ë…¿í7kî9 6ÒtR,qZè?j­ýn‹†Á¹)?e‘Ú^A¶ÆŠPâ)ÅT€I½LÔg<€&À§(Å"»MÌ�z¾¾@—h ‹Ð*PœqbêÎ!—�¶l¤hŸãF.r+G´å±Qº"u#IôÒ>’s”þ¤w•�W@i¹ßVAbCõº�$ÿåPTǺݯ¸^³ †ŠR�@SÏ—@ÿ®ù€„ Š€â¨Yp„eÄ9  …`‰íñÔ³RA&ô)j­Fs�·=iÕÁWWËHÔ¾¼…Ön(b<é;��.ð õG´éOB„ø¦�U!wæÆ#u=œñ¹TnOTnôn¨d1®ârsš=Õ©Ï(ËËa=6pše=æÂ�z£„DìºSС?Xš5tWpçš7&ñ�Ê£u„§*þŠ4þk|Ó¤õ¤±}/ñJ.Dç°`%r¯ê+úŠþ¢¿¨¯ê+úŠþ¢�€ócE “›[æ�à<šÄÆÃûEh¯íý¢¿´WöŠþÑC M‹¥eȾ—ö5¡Ýû¡ñ=`/;nWÕù îR“Á¾¡Âî ¡Ä`|Ñ�ÅeÈ©ôÚç&~,âƒÈ¤p9š_ÒPñžgõX7Å4…´Mjð½ÓŠjåÜ Z@ŽÃp×ѨÞ_7ºáñAqRîUäß‘÷j›?6ü�Cd‚®ÓìëdÉë?'KÇ*QŠç»:b1xŸepg‹Š.Íd�•‡V¤-â®wúJ&Ç+>O›eÉNk©À/q™,€A#Lä½á¦–¦yñopÖ‘ËqÅú( 6$7Ö¿v³}˜E¿"€+pRIÀÀ´fy aË­EBý¯ª¹ç造͸«÷©ú¡…y¾­y>l�$Ò·Õ�)eU×Ö\ãô²mx4ŽøYå~m§ ¼´šÉFñ§^ NV$na·äTì01çlÞùP’ƒ×ô¬õ»ÍE|Jÿ¼*çû6üŸ6.£‘—:ZTø,«Œ­$…îVbŽFÜq¤ @kBÍÈ­�û«¬½“vÂCÆ»65Krx’{yÍö>OŸ@ °Ò¸>M·qåj'¤ü_eÇ6>m é2Ò’•ÖÂ1gÂÞ± “’–”‚Zãt¿j75¥rrkõßµ7=Lk_ì|Ÿ4T®G—¾ã†Á¸ëi&ë7y/6÷ m\®+áÂÛ�ŽåkȲJn''ÐAdÆø ÷p/§T•Øî:ÛqyØìÞmîÚwžM·�ŽþבnAÅn}û@V]+jâ—‘kº×;æž :×ýR° ¡"9ºÛqyØìÞmîÚíKn/;ý¯#Ýp—.mÜ^v;7›{†¶»RÛ‹ÎÇkÈ÷%Ðeÿm*)˜F­í)ú…ÇW—1vsmÅçc³y·¸kkµ-¸¼ìwö¼�q3À;³´l° aÐÒÆã­·�ŽÍæÞá­®Ô¶âó±ßÚò=¸*+d8jÊ~,î:ÛqyØìÞmîÚíKn/;ý¯#ÛÍ¡/ÆÓ7m¸¼ìvo6÷ mv¥·�Žþבíñy­ßi%�Ç[xžv;W›{ƶ»RÞ';ûkÂûuQ-ÉÆ7w�m˜,–ÁžZٽѴqò¶`rV®«Jî³íôñØ(§*ô8X!þu±6„¶ùQ�›P';h5oùõX*Y¾Ò…Ô÷Ã�çÖÂÿ`çr·¯²Nw©KÇ[bÅÄÁ(3{yF{2ú_eón#‘? zOÍöùÀ��Y‘ó°ˆÌ·qàÁ4 HìTX An'õD#Ž<öq…ǤÌ¿VÒDxÔ‚àÅ�`Ca4óׯ83ñ°�™£Ü# ëõDÉ%²eE]…Ùsçéw8ñÓZdm^ö:ßLn­ a*ä,!\‹°ÔA=f†HØ_fñî]”�p:Ÿ¥dwPØ|Å!�ÌP³r¤nƒ_Ò””®¶1åÙ°§q/’…Ãæ¯í˜>UvgÇÔ$­mkk[ZÚÖÖ¶µµ­©Ðˆœl_¤¬V¶µµ­­mkk[ZÚÖÑ@«èâ Ù­%i+IZJÒV’´”n ¿ËÿÚ?ÿ˜½¢\>”¶r=ðjÀØrðÊ·#‚ì”÷®²ãX yßZ/ŠÃ/iJ_Ñ+¶ø\Í©·œè qp3¬)†_µz˜bñ“HXªXr¼j?åÏÿ—sPp •fÀñC’Eú¤Mh\eMzçö©†G³WÝOüƒÊ»’»—Š…î¾=aš¼�áy¯²PÆ’Ä|Ò?ð×3⹟j|Pün¡Äyl X¯íý¢¿´WöŠþÑ_Ú+ûE½+ 9Ò|g‘\ÏŠ3¥`AØ ƒ}Õn÷Õn÷Õn÷Õn÷Õn÷Õn÷ÕÂPw±Êh¿uņ;µýj8øêxŠ=’ª4l3Ïx«´Í&©TÀoâq=$™ÅõS%�qΠÖx«ì‚,¸„9иÏ*èiL äRت™ÆØ¥ã«*;зºqµ1˜�†? ]8ÕÌ\Ó›\~Lš»prµžA>Ëx×>€dËA�æ89>ì˵n9Ø‚yÔäñ0¢3€ÁýÒ—‰©û¦à¦’Óò9˜üPB. ¼x`TâÞÙð¼[RÒ°ô«Ÿ6EJ# ÂÔCYù·�@^¸R)Rì/+~&Ìïà‡­Ï‹x×4Ê@TY{ë9rµvo“õ`°kÀánkX yzcOBú»pÉû¬äûµáx²Š´÷ô(€ƒÖa�Í”@Ä£!ÆÏ8ñlÇÒ‘+zì…8T_Ë�ˆ·ûý[ÀÞ¹«�¹éÍÖß<¿öÈI+Žú4†+¥õ-к5À#�^­îÖü//–ìߪ&“u̵~ç¼²&Y;VyR¸G:Ö¥àÜÒý„k#¹cPWI·¤‡ 3@ÖçÉ?*û Ç +-Åráo]©†¾l™äx¶%K†"€€!VÔ6Igú"��8µŒo?u’4ý àB“ Ã7î£O1ÄæzòJ|cáx¤c©¡.½âì>ß­Ÿt·�i~­–î�ZÀÞ¹²Á‚TLxâM[_/±G7\ë@ A±û~­˜;fÞ= ÇÎÙ�c´ÚÀÞ¹·4UN+¢ýûEV(XO%+€ëN‡År>(Ê3|WçLF3Ž-˜;fÞ=¦fÙ�c´ÚÀÞ¹÷GrûŸ RTíÌ ³oÓ3lÀ±Úm`o\ûžø9ÚvÑñÏ*šjºH™¶`Xí›xö˜9›fŽÓkzçÜBGŽ<­ Z–>ß«fŽÙ·�iƒ™¶`Xí6°7®}ºÀ¯ é‘qmM�#æÏÛõlÀ±Û6ñí0s6Ì ¦ÖõÏ·Q­u¾òÏÛõlÀ±Û6ñí0s6Ì ¦ÖõÏ·Âæ[WMŸ·êÙ�c²m÷Ö°s6ÌV;M­ËŸ·2´FÜ,àÜÙû~­¹M 9-µpÔ´°ó¶¥´,@2ðÚ™s.ñíõØ;€ÊLÛ€Òë°[wž-9&Eµ:Wz  P|Ð-sòõ/÷¯ ß½…Ø2k}iÒìX4º�pá}·$ã‰Jàm&–^—Kqj1ójDÖ>-òƒ?>±<¾Õ´’�¸ßÛ”\xR"�ɱ` i½Åú¥eÃa 8·úLŒˆ¶0Í)2l)2°‚#Ö\ 9ÿ ÷}_àáš”�Ý�47Ûó~½"IƒÕ"0Ü–¥As¿¬†fv .±3FÂ##ë ±NŒ ÏľãǹÖ£Hfå_Ä¡E#':<\èPXÄKó(<”–/⿉HÉÎ�{äáêUEi+IZJÒV’´•¤­%<¥™°Ð†"´•¤­%i+IZJÒV’оô]XŽý⿼W÷Šþñ_Þ+ûÅx¨J0…K‘ÿ-ÿÚ?ÿë�0êé•E |ø«fOÔ}©�Ä*~fˆÝšžT& qPû�ª}Ĥ¹Í\øÿÆ ÉØÍ¤�ŠæÜu¤¯ª»Ú<íà #£J&¸§Z¿@c]þð/Ñ›J$Œ�!÷ép�w0¸¦—šÁÉvCµ77«æ„d¼â¾!G<3æ�ïC1¯úÆ4Øq àmþûï¾ûï¾ûï¾úmb"& ÄNâxˆ|µ6¸®ç’ƒ&PÁ�a¨Þ4» è~í9]è_¦€Ýzþe`Äø°|Ê{Tpã ÇmÙ·—×J!1$fßï¾ûï¾ûï¾ûïŠL”BðÆà6Ð`záø5 ¥‚xÈø+]Hß ²)ç‚#òTX &]~ÊâBåÖo6ÜÑjãòP&„8%píÐp�¥ù™†"v?ç£>bƒ)Ë/reË 2B'¢�-ÁA´®p °Ï*DÍV•Lª¼Z•0r|-óZˆt†AñJ·-A,ÆÝhIVª ºç–B ·ÅCŸ Wxƒ‚»¡Ý]«*ÁO�ƒµaa�î‘®<›âÛÁÇ0Ÿ–쨣ñ;ÅÓòT›Š•Ï5~Úé“å™ö°¥æ2oNìêdù˜o›Ÿ®�K¾Ç P@écGF±ÐÝ~S§Z$¼pv�Ô}†ßvgfêõÏrŸtº¤ãÞ>wÆÊï$VÚq’þ éåèqâ¬ðû¥þ�Šx˜ Wi4Ĥ]2É8œ=³p =‡­]߀J›ó¤LYùø�Ff±ç.qZ»Ä4°» Ñô¹iÏê=êMd>»(­8 >Z•pYî8Ô¡—¶™B çÌ]ªx=ÆS°+l”B9‰O�à à\ècÔ›A²XZk¿%JeãUU¤rz—¹˜si(UsWlpfžãŸø«ÊÓ$ú>öp è>Æßvga`»…p‡Å5 €ÈöSŒ�RYá!«Ñg9í �à.D¼J€ã~‡·¦(Ç.9Ž"fPKÆO!è0â~÷Ϧš3ÆšDÑ{qÍ®•w¸Gü¬Ð»Ò%ØcÃQ¤#‚•&€½Và*⋺qxüa²Ýû*ð©;á´#ôëEVòäè�¤8@uG�ö0¸pçp-DïK–îoظL8Ë$âUÕO ‡gÆÊ:'«—Ô�¾ìÏÖZ¹‰¹AH™ª«Š»MzIØb ¬!Åsá­=¢\‰ˆÓ�T†øS a«åOŒl$ b¡l^vG(ËZ�—€°_.o¢¸`<\øl÷F{iÖ:*èïOÛB Ž-ÓÐ>f‹€2//YÄz5 ÙÁ!à™lð\¼Õ ¼â˜†:†Îs˜ƒ™õ{,:9Î’½è7ú³}š;P,D©r"…Ÿ‘±dR™�c ¨ÛnÌý tŽ\¯•žÖï¡oä;lO€%÷8> ³ä#jdAæ¸,á%óë=�#˜\Sê ñº]]®èÏgzî?ÑÊŽs#ý[MŠ,áö6�8Ï/VË´ÞB/á�¨Ã¶…ƱxÀ®^XoÇŸ�; ŒÅໜx¶ìΜŒt9ó÷=6ˉ€sQ@@hlÐHoJy-ô�oöTë’± VösñR¤Xîk s›ÃR³Å—¿Žô˜gÃöÏ–�]@`r ¶èÏc»â–®�ŽO?†è€.Øñy/à»m2\N.áñN\FÞ;(ô‚AæíMB€1 ¨èû…¦ òT•„QŽJ_Ðݰ»©è#.æÛWf uš>«;n?M?·F{ `Fܱå¥Àèã¹ï­ž‚|'鵞� ¿½(.â›}a¿â),�Êž8ëHŒ7%¹2¾ó¿‚wFvw•†Hõ}T�NA/å‘¶(ת¦×)dt;#zmPÄe!%Èc³hÀK–æÕI©…b# µ¿XZžc¨]�öÚ„ªÆ†'ì£Ä˜è¸t¶S3À\÷*érû�rpì¥DØ#¨Ù�±C§Ø_‚ÝØÞoà\øÐ0ÃùÝ}ç ;¨vXzX%íîaØÀ2¯uðÚ®*Bs/ x’uØJ3›Í6»³=”:4‹·[‰£RC;½ â±zñ—ð;Gà·F~”`øªð¢[ì´Â°÷ÝRÌo'cÂëðžæDÏQÞv“l‹^ªõñgsëk»3ÙœbÀ¤MJé_ˆS?YH…`žÿ‚ÝÔ‹žõàfp(MÀ_O�çð·&Ç¿y=Ì,g’ä‘H‹‡¦×väØnœö½Ùž×]!°Ìò̤?�\‚ŒC# ̨o=ë™øÛ“cß¼žêõÌ)ä¶»·&ÃtçµîÌöÓüÆÅúnÊ_ÅøÅùjéZcéc`CïƒF%=ª±Ó'5wua¥eôEcÙ’‰ö[·&Ç¿y=Ô-ûŸk»rl7N{^ìÏòÙÈâer2©å«‚ø¿%KŠnÞ�µk |»Žƒ>£œ cª¨õüX? Žõ<Íá�¶íɱïÞOu ~çÚîÜ› Óž×»3ü¥Øl›Ç#ìÝK·…Ò¥)ë/È•u]¸£%Ƀ¶Ý¹6=ûÉî¡oÜû]Û“aºsÚ÷f’D€%.âÑ(+‚t8óiâ¦Tªê¾ÿväØ÷ï'º…¿síwnM†éÏkÝ™þD׋ƒ)�°Á»PIXž…)q‰î>i®aÎVãJ$k×>UB¹:¯Ò—¸&M^¾kväØ÷ï'º…¿síwnM†éÏkÝ™þ@0C«K‘NDÍãjÕx\Ø;Ök‚½Ç¯´Ý¹6=ûÉî¡oÜû]Û“aºsÚ÷f�”¼W\-ymg÷”C�fá—�k»rl{÷“ÝBß¹ö»·&ÃtçµîÌÿkø.¬zÒTÊ·ªí c 7üÞÞßväØ÷ï'º…¿síwnM†éÏkÝ™þ>ò©òá÷]Ój ÓhÜ|ºû}Û“cß¼žêýϵݹ6§=¯vgøìâwóý¢•UY[ÕÚ`>9‹2Á A·Ý¹6=ûÉî¡oÜû]Û“aºsÚ÷fŽQ˜dLÃö6²é$ɸáî7nM�~ò{¨[÷>×väØnœö½ÙŸã²ä±µöï¸Ý¹6=ûÉî¡oÜû]Û“aºsÚ÷fŽ…Ìô‹¿m¬Ÿ�7$¸Ý¹6=ûÉî¡oÜû[Ìn�†éÏkÝ™þ8¼Ø>;[²�GRúíËP‡·b…ãѱe™O�Ú žë»TSÞ«j… Æ7§`ÖnwÚ¥dà'Âürà_;`O%\î'᎞܂Ág]� â‡wëk8`OÉ&×VK~®×F>!ôØÎÜûڛįÈþ¿cž`m„k'žÓ²ãl’ymtø|ï×cü �£kÃnè�§ðÒNÚSÄ7DvØhóÝòÚÊÎõ‹ãñ×8½/Sá·#rá¼Çcüš‘µnè{®Æ ø™È ³Ž2Ðìõ½|ÆØžñºh�3°ã=÷ÌÚ¥†æNA†t«ÁH”YKQÁM²ph´"V D+¹Yhø¶áaesgŠbåO IpÚ‚(€|¿!MŒÜrdŠíhóz7D�ÆÎ 7¸i/0ÛFÎs«Æß ¹Š;í§&ZÁ=ßÇÊœr�Çﯰ È�ƒàx¢ƒ5Cä½Ú‹˜èIÙHÜ’‹N¸ù¯v£Ð6x�—â6ïJ°‰xц”_@NT‡ãã¡ÿ`Ô¦¸é†ïÜq=ŸHx)À(íó;–G¢à‡]ÐïìN!ô¯ëbí¸çì4äÈËßÑ�:ϱì' r} ,¼šµß–¥-¼¢Â§›òFõã€Â„#x‹éÌö'n†.kê¢-ÁlSÁ‘ëÙ©WèbápßÞÙŽñL°;Î^-™XÊ y=„´Þ|ƒ}Š3¸‡É÷>ÂKc¨G€³+#©�'°À(�SJLˆ™�çä 6=ps 8BOI‰©¶Æ˜—Š`=¹¡%†æ¼[*nKÏîY”Ú‰à涀Ч�å|Q Žß• Sÿ,MV_a#0·Ÿî†Æ&2øûïµ)¦þH™‰%‹¤â‡—±Ós*ë!áù'ïèßnáH*.I›Ç¨Ù ·Ps�)BÀò~/-ƒ5äâ9¬F’—� -~�Š@\�Èì]ÐjzŠxã^åaȠ⥕:à;ÝÏc…9© €“­¾Ói jû6ª_bË�_' ¢:ü½‹®Ày§ô”m“c‹f±žµ4é`üÒÕÑÂøƒ½dvjý鬡ðÖׇ–±Ð?diÒ'™ø�Þ¢•'KÝè+ß)=÷vÐM.~’rAŽã*áË’öÓHÜ’¿œ~鸨¼Ò Œ¯‚ôÈ˜Ä ÃZ›Œ/y—½m�`8L?d`Áƒ 0`Áƒ 0`‚àÀ¶èÀ"#ˆžÈÁƒ 0`Áƒ 0`Åú²bHé ‹eZ¯}ŠÅ‹,X±bÅ‹,X±aà2©¬ÿ"ÿÙpython-pot-0.9.7+dfsg/docs/source/_static/images/logo_cnrs.jpg000066400000000000000000000154061525521106200244060ustar00rootroot00000000000000ÿØÿàJFIFÿÛ„ ( %!1!%)+...3D3,7(-.-  -"%-0---/.-7--------+-5+-+---+/----------------------ÿÀáà"ÿÄÿÄM !1AQ"aqs�²#23r‘¡$4BRb‚’±³Â5DS¢ÁÃÑC“tƒðTcdÒáÿÄÿÄ(!12AQRaqÁð"BÿÚ ?¼4G�À%BMÀ#Dp P€DpÑT ÑG€I°i6°Pl®Î}%tTÿ¨n¢mÉÇ¡·;�•?”y][ˆ“ð™Ï'º(ù��²<î·S¡L¥ú �щtcyÉÃ)nÖËð©ͦãýˆ{î ø¦x*qIK#Œ¤Èûu µV€'€Ÿb„;¤É%f^â’ßJµìtMdvê-h>õª—ªŸWTïZiê^ €˜¢—H[“' WÏüy¿Üù^ˆ1º¶y••M· ¤ü4—†Él·ÂIG—Øœ[+ñÂVFÿyn—½HðÜîÔ6ª’ ‡‹£uºŽ�'Ø«›"Ë—\_ÁÚœ—É{a9ÍÃg°‘Ï¥qÝ;9¿}·hd)…4ÑÊÐøœÉíŽaiê#Q\µeëÃ1)é]§M4�;&âõ›±Ýà¥KN¾F÷òtþˆà¢8Pdþv¦e™_™¿Ä†Í“¬°ó]Ý£Ô¬Ì()k™§K3d·œÝ’7ÖaÖ=ŠyW(ö>6F]=À#Dp P¸;DpÑT Ñ4G•4G�À%B„ ZÌ¡Ç`ÃàuESôXÝ@ o{ŽÆ0oqÿ'P¡,ƒx=8ž# ,Nš¦FÅ6¹çW@Iܲ¨Ü¹Î\õÚPRiSRl$M(úÄy­?Dw�v ²ÊêŒVm9N„,'’‰§šÁÄý'Ûk½– B²ºTy}’Ynx@ОO<@œNa p8€'”àl‹'Ù@ ²,ŸdYc²}<Ï�í’'º7·Ís k‡S†°‹"ÈÆÉ|êËŒx‹yflåcH:^Á©ýbÇ «W Ä¡ªŒKM+%�ÛØo¯x#h=Zæ"»Æ*(¤åietN޶ȲÇd–YlšBÆBi ) ¤ Ú¬‡ÎD”ŰW¹ÓSìl†î–.½ïg¼tê Ba %%†u8¼£©iª+$OkØð \Ò \ÂÚEAdZɆÈ#“JJ7žsv˜ÉÛ$Õ»úößµ •Œ’'Ç kšn:ÁEen ²RFT! g`©¬õåys¿á”î!­³ªHÞv²¡©ÇìŽ!YYg�·¢š¨Ø¹‚Ñ´ü驃ªúÏ@+—f™Ò=òHâ÷Èâç8ísœnâzI%QD2÷1Ï @žBôRS>W²(˜é$�†µ­s‰Ø¬‘ ÁÈö«¯$3Q-l¸�ª&:ù0|ƒ: ½!ëÕѽXt”Âa†(š6ØÖ�ÔH•éuÈøÐßg*5ãˆö¬�uUE,r I$sÚ=…Br›64•-.¤khçÝ <‹�F54t¶ÝGbÈêì×C]pO шáòÓJø'aŽXÍœ¸ƒ¼¬à©GaPÍ�–0ò¢4¯`4AÙÞ¨\“Y=‹,‹žïɨ¤ l,d<à\à ôlnMì½Ô84�¡IúOç;Úvw-Ú!꣌•ÀOZ.ŒbÐG–§ÉØeÆ/ÜZ9§­¿áB:¨·ÊÁ-–zªWÂòÉœßaˆ;›æ«§ª}SªblÜ�Œ4?[FŸ)¤HØO4.%-«,® sà�Y! ñDz–xLtÐAM#œß(Ö µ¡ÀºÖÚHw¯n ’t`rpµòõ%òÆçSzšOùã�¿áy(GI,ºt´ZÖÔ©¬æ²�õ¬ŠŽ6òÍMÉžH-Ú‹€½ÏHá«ançŒ:¶¬ä‚�˜Tã Ɉاò¯áówÎïö-äPµ‚ÌkZ>¨ðTm –ª+¥’©)¤+^jv9F÷*(D‘r€sâ×Ößœ?¯rÜfcmwýï&T·I�aiÐVLÌþûÕ÷—� ݲñôYRÈÒçÖ´IØÖIUSg¢g¹”n4ð`à*ÿ¬Ió5ôîSœãÌæaµ&ÅÚ >«¤hpïꓲEO–_tÚxFÒ ©¯Œé6¶¢ÿ]åãØû…ïÉ*MO�÷.q ¬ñq¿NÞ‚£D)¦Lü–>·xܪŠY<ýTžÃ>/ˆ hËȹ:š8»ü(MV3Q!¹™íèa-ÙýVû-Zta?4ÖCmø9DÊÙ3�=qÛ¸öÒ㕉\áÂC¤·_°­ÕfWäšDÎù¬éçt{úb¥c+2ÆÊ˜Iå¡…0§”°a¾È,sà5ðÊM¢�òrðÐyçÕv‹»ŠèÅÊN £²5xu,Î:OÐÐy;Kã%Ž'¬¶ýêmDze4KàåÀžxU›Œ’ƒ”¯¡fÐê˜oÕÊ4Ÿp+©×2fïö­ÿŒ?ºmI¨íiúg3åÝO+‰×¿„îgû^Oô-(^ì£ùuoüÌÿœõá •Ò'—lxO �<-0x s�ž‚^Óô5A‚œd' —´ý ]G±ŸL¶ô í…Ê)†úh{Fx•巠gh<.QL7ÓCÚ3ÆHÝ/‚yžO•Sö'ÆT[&>UÚð9JsÉòª~ÄøÊ‹dÇÊ¢û^%ÓåÔõ/Ûø&óy®ê?‚~f}ê‡ûÉ“y®ê?‚vf}ê‡ûËu?áþŸ÷ì‘ç3ölÞ´˜ÕLsg7ölÞ´˜ÕL—G’ë½ *G’U£� Ž»é7§éuýª6JF¼´‚ÒAÁAOOÖAN8,*ÚFLÂí<6ƒ¸Ž•IJnh®c³>¯œ:Û¿ºëi†e;H ¨æŸ¦ÑÍ=`lî÷)2µà9ŽiÞÓqî]ðÈT¬¥à«ž-¨ê#ŠaV^!…ÃP<«?Hjxïÿ*Ž`¦ç´ò‘}-íõ‡õü-W|gÇLÒ90§”¹0«ƒ1µºTõPè¥kÇ@‘¶ücwµSÅXù�žÕuQý8Zï¸ûq.å˜1•Â¥Ô.S*Ó¾9Ÿ, 1âUí?ú™]Ü÷—�s‚Õ5ÏaÄÝ-¹•lkÁݤÐ#xþVŸ´¡!>1Lžkf@ž0§9«É¨1 ª>–(cn ÷²Ï{މ»Æ?RÙIEe„V^ q�~‚^Óô5Lr—7¸l4uRÅ¢’(žö¸M3¬XÒáp÷�Aµ¶(vAú {_ÐÔU5.…jàãL™oèÚ ”S ôÐöŒñ…+Ë�@ÎÐx\¢Xo¦‡´gŒ.¤s¥ðO³Ëòª~ÄøÊŠäÇÊ¢û^)Ny¾UMØŸQL—?‹íø—O”?SÔ¿oà�M滨þ s/ûïTÞM›ÍwQüæ[÷î¨?¼·Qá“þéÿ~És¿fÍëGù�TµÕÑ�Ù“zÑ~kU)tº<—]èRSIA)¤§ ”úz§Ät¢{˜zÞ±°÷¬d¦€k$·Ê�7êkŽ ñ©¤ýa»¯gR’½�ÀµÀEˆ;;AUS•™„Ê_.v²èÚOIÑ+¸² EJ8h¯1ºƒÏ$cÍíõN±ìÙܵÅI²í¶ž3Æ1îs”`®Y]rÝÆ•<Ì—í)å$üè«1ÐÞ¶¦O¡�ß‘§ô%ÛᎯÚ+ì´¤ä1*èöZ¢B=W¸½¾ç© ožª.K‘À[áG'x#ùaAÂê1LækcÂè Íã⦀S¹ÞZ‹˜AÚb:âwPoØ\þß&1é°ê–TÀyÍÔæŸ6F9Žè6÷åÍ�Ý\öȿ󅒃¥Ðikj!%йÛ/nsw5ÂÝD®Ö\ùˆÐKK!Ц'Á ù² wƒ±Ã¤\.ŽÉl«¥Ä£Ó§�i�Ï�ÖÆz[¼}a¨­Ìôì�ZF5ãƒÀ#ØTбÆŠgZŸ(åœ:’J‰Tñ¾i±±�#Öx“¨.„ÍþLÿÃio!ÓÊtå-Ù¤EƒAÞ&çz�ÁNÈÅ£c81¡£Ø‡Ç©è"2ÕJ#4m{ÏÑcv¸ÿÞ¤N×>V¡Ë#¹ÚÅ…>ø�´•dFÑ¿FáÒ­o´ ½½¯èjÐevRI‰Ô™Þ4Ñ£/}^úø¸�dõ €-þ@ú {_ÐÕU0Ú°C­–è¶dË�“³´(–|´=£«º8B#¿~ßõ5X¶%1:CI›ž4õý夫PF«{ÁÕy�†ÀMÌê7ü-Ú":®?Ùþ†IRðÖ£~s¾kG^óЬxb kXÝM`u`œÐ°ÃP 5”9LÖ4ÇLàé¢áæ³�Ž÷{‚Þ…ÊR¹á#A•µ‚Z§è›¶0:Å˽äŽå¤)Ħ¸/Œv¤†•nf&’ÑVÏôÞÈÇØiqüÁìU]š¼?�©î,éô¥=R:ìþM›Þ :•™ÏüAPj¡©’'u�Ï Šž ¤³±…ü+ ª]ð3wú#¤ÿäÓëšÁE1 Ö%“(N O ÂLðJæ8=ŽsÝaÌ%®ˆpÖž‹88¬@5µÒ8â¶9Þ{K�yQ@SÁXÒ}š›]ÊŒáâ² ×4á²&k[qÜTz¢¥ò¸¾Y+Î×Hç=ç­Î$•ç8‰%Ñ®Möe{ðìZj}.FMí¢Í#Vûu­p)À®ŽZO†m+ñ™ê3Ú ÀÑh×b7’±a§ËÃÚ3ƈë׃µgŒ,fÅ%Ñ`g«åt݉ñ•£«|/Ft^ÛØØ ƒ¨ô§yì?¦ìOŒªðÅ^ݼɛy²†¥í-tºœ,lÖcÒÓp\r¢‰Î},¦"ñgX5ÁÀl»\Õ¯_IZ»¥ºíóÙÄR�ž +–Õq:‰ùHœA#“�ºÚAÚÐv€´WLº.„’èÖÛì}ÓIIt—A‡ª—šE+Ø8Íû§Röæj¿ã#?ú­9) [“—¾Ñë¬Äç›T³=Ã…ìߺ5/(%4”’])„¥%0•†žœ6…Õ3ÃNÏ:wµ‚Û´ˆ÷ žåÔ”Ð6626 FÐÖ�¹­Ø-™\–«’±ã™JÝtË ¶®¦i}ð®Õ%òËÁUÂÈÙ \k�„¡r†Sa†²¦•ßèHCo½‡�ïaiï]b©Üýd÷ Äc- ÖàI1<÷—7í5KÇÙ·G1ÉO„ðV SÁV™Oc8”àV SÁ@ANb<€dz°Ãåàícñ…ãz°Ãåàícñ„3Qag¸ün›±>2« U‡Ÿñº^ÀþaUÍ×xC-öÌ—KuŽén»>èºeÑtû¤ºmÒ]8”ÒR]!(IM%!)¤ %5­. 49IJIÔÉ) Vgrgá5&¶VùC̾ÇÍk�¸,zËx’–Õ“cÏ©‘8Ãè¡§ÕÊ[JR7Êín×¼ M z„/=¼¼—¥…€^,g ޲žji…ã�…®â/±Ã�Ä /jr>5…ÉEQ5,âÒBâÓÁÃk^:Ò: ò¯\õd‡Âaôí¼ô­´�£[áïÒY¬õt* èW=Ë$Ck2‚œ Æ p+³ƒ )à¬@§€2‚œ Ä p((+Õ†/k�«Ä õágËÁÚÇãjÆiaçÈür—°?˜UqucgÐür—°?˜UmuÅ^ݾْén±Ý-×g]@ºK¦Ý%Ю�”Û¤%)) M%@h.$€$�@6’w Ãß�á2×TEM»å;OšÆ�9îè_»i]/�a1QSÅM³"mµíqÚç;¤›“Ö£Y²Èá†ÁÊLÑ𺀠›ù6íÑ´ñ<@ h£ºÍÏ ¢Ê«Ú²û! #�B\ë�\‰8mG/NωT;›m‘Hu˜�í-è¸Ý¯¢—“Ãb«†Jz† "”YÀûAqÄÄ™\ö<œN K#œ �åÎGÍ„ÏɾòA!&m©íú.àñ¼w…\šk(†I§†d8Œ ­0Ê PV0S�@^¼,ùx;Xüm^W¯ ?ƒµ�ÆÔ>�E�ŸcñÊ^ÀþaU­Õ‘ŸƒñÊ^ÀþaU¥ÒêðŽíöÌ—EÓ.‹¦ 2]Xî‹ ÝLºK Ý!)·M.@.W&iòÅ£ˆV²Òx#pÖÀÕx?<�ƒp×´êòæÃ7'™]ˆÇkYÐÂñÞ$”üzÏo)n·þQUU|°B¦(!B€!_ŽàÐW@úz¦Æÿ¼Ó¹Í;œ7Íùq‘•LÚ2%;Ï’˜k¾«¾‹ÀÝ¿hßn ^lKЦ'ÃQeŠAg5âàÿ‚6‚5‚™]Žôejhä0S�V_f¾zéèÃêi6�5ÍúÀyìHkÆ«šìtd¤²ˆå†e(+)AZrezð£ñŠ~Ö?W„–šsØñbXæ¸_eÚAö!‚,ìýŽRöó ¬®¤Y{•ßñi⛑äQ†éi’n\ã{ W6·tê�]qZj)3»rmº[¬wKuÙÀû¢é—EÐî’é·[Ì–É:¼Mú4Ñù0lù_q8ó¾qú¢ç«jÆÒåš“|#Q N‘Ídmt�y³ZÀ\ç°5’®¼ÝæÍ´Úx‹CêŒqj,„îs·>Aì&ÄI2/!©p¶é0rÕ.|Ïî–°|Æô gy6R•-—gˆ•WN9`„!N<„ B„ B„ @2Ë5t•ÅÒÓÚŽ¤ë%�¼O;I|z¬OÒmŽ»›©úÆN/(ÇÖËM‘ÕØi? €òcd±Ýð¶7©Ö+CuØŽh ‚.ÛìPÜ{6ea.ä 4‡çR�Îò˽ªc¨üÄòÓþS›®–ê×Ås#3ni+b�nmC\Ã÷Ù¥ºb·5¸¼GU ˜q†Xˆö9Íw¹9[ò%Õ5ðD.–ë&Bâ�Û‡Ôw4;ð%,Y Š»fQö€oˆ…»ãöfÉ}û¥º˜Ð毗ΦdŒÒÇodeÇܤØ^d¥65u±°ol /=Ï}­÷JÇlɪ©¿‚¨ºÚà99W^í:y%ÖûZ&ñÒ�óAè½úï‚f¿ ¥³Œ©ãçU?ä3ùTÊ8Ã@k@kF ,àI–¡|!±Óý•vJæz(‹dĤù(î!ë;SŸü£ «>š�‘1±ÄÆÆÆ 5¬­hàÔÈ…<¦åÙDb£Ð!\�„!@„!@„!@„!@„!@„!@„!@„!@„ÿÙpython-pot-0.9.7+dfsg/docs/source/_static/images/logo_dark.png000066400000000000000000000065551525521106200243730ustar00rootroot00000000000000‰PNG  IHDR´�Üà� 9tEXtSoftwareMatplotlib version3.3.3, https://matplotlib.org/È—·œ pHYsgŸÒR ÚIDATxœíݱN[Æñ�5��Ød_Á‘ó;¡ý N�`åСßÁ™Ó‘Vø6…ØÙ>–W"#`¥±Å$®54œÃ©ê:Õÿ_:ÝsOW}3%|»ÏìÜÞÞ €ÑýeîÐ � � � � � � � � � � � � � � � �ÂîŒÿí#I§’þ�´/éZÒ…¤Júï|ËrE âñè }ŸF}ƪW;···ÞÿÍ=I_$Kúë–×7’Î$}”ô?¿e¹¢ñxô„¾O£>ãÙ+ï�¶'é_*Óü)—’ÞIº1]‘?j�GOèû4ê3ް½òhkIÿ¨8þ«¤OFk™ËgIï+Ž_Kú`´¹¤ïÓønGØ,{´¿Iú�¶ÿyú˜[Iÿ–ôÓdEþv%½–´SqÎFÒßÅÿ;°â‘Kú>�ï†q„βç]Ž'ª ¬TŠv`°–¹¨.R©Ù‰ÁZPxä’¾Oã»a¡³ì9Оóï­Ûìw]żZ¯eÕsø�\Ò÷i|7Œ#t–=Zk!^u]żZ¯…®�\Ò÷i|7Œ#t–=Úuãy¿º®b^­×ÒZ;<Í#—ô}ß ã�eÏ�vÙx^¦uëµ\ô\þÄ#—ô}ß ã�åèw9n$½‘te²"‡’¾©¾K¹Ûm¹¤ïÓønGè,{n}õCÒ¹êž59S¹57‹ïj«Á¾Ôæâ‘Kú>�ï†q„Îrô�BÞ*ß7Ô �žÐ÷iÔga{å½Ûþ�Ê6(k•?C·Ùܽž5°Ô �žÐ÷iÔga{5ÇæÄ÷ŽT¶[)À.Í3¡ñxô„¾O£>ãÕ«9ÝðŸ€h€h€h€h€Ї'ke¬mÆk²6jÍF]÷…ìUô­¯Þ©<•ާe¬mÆk²6jÍF]÷…í•÷@[«nS˯’>­%›Ï’ÞW¿–ôÁh-½�—z£æ€^�#lÆ¢ÿ|Ì­ÊŽÚ?MV”Ç®¤×’v*Ήþó$ä¥Þ¨9 ×ã�1Ï»OTX©íÀ`-Ù¨.`RéʼnÁZz!/õFͽGèŒy´çü{ë6û]W‘Sk�V=Ñy©7jèõ8BgÌs µâU×UäÔZ£È_ä¥Þ¨9 ×ã�1Ï�vÝxÞ¯®«È©µF­=ñ@^ê�šz=ŽÐóh—�çÍýaAk�.z.¢3òRoÔÐëq„ÎXô»7’ÞHº2YQ‡’¾©¾¶sßÝ6…¼Ô5ôz¡3æ¹õÕIçª{ÖäLåÖ\Lû®¶ÚFfyi1jèõ8Bg,úN!oÅ7Ï•±¶¯ÉÚ¨5uÝK¶WÞ»íߨlƒ²Vù3t›ÍÝë¶NÆÚf¼&k£ÖlÔu/QØ^ͱ9ñ½#•‡íV °Ks2k›ñš¬�Z³Q×½D¡z5ç@ ~à� � � � � �‚çÖW¿;’tªò´ùìÏ/$½¶Ñ×·dô¦ŽE½èA£9žCÛ“ôEÒ±¶op¹QÙûë£Ø  VôÚF_ß’Ñ›:õ¢/}/Çw*Û¬àiÑk}}KFoêXÔ‹tà=ÐÖªÛ¥ù«¤OFkÉæ³¤÷ǯ%}0ZËcÿ=zSôìDc‘ezÐAôßC»Uù‰ˆŸ&+ÊcWÒkI;çxþ½�+zv¢±È2=èÄó.ÇÕ…@* >0XK6ªû0H¥'kÙ†ÞÇ=;ÑXd™tâ9ОóoÃÛìw]EN­5Zõ\ÄzWôìDc‘ezЉç@kmÚ«®«È©µF^ƒÞÇ=;ÑXd™tâ9ЮÏûÕu9µÖ¨µ'^ÿzo/zv¢±È2=èÄs ]6žGÓžÖZ£‹ž‹˜@ï㊞�h,²L:‰~—ãFÒIW&+ÊãPÒ7Õ×6ò]ŽôÞGôìDc‘ezЉçÖW?$�«îù�3•Û]1í»Újëõa ÷qEÏN4Y¦�Dß)ä­Øâå¹¢×6úú–ŒÞÔ±¨=èÀ{·ý•-[Ö*2o³¹{�†Õ‰^Ûèë[2zSÇ¢^ô ƒ96'¾w¤ò`àJì(Ý[ôÚF_ß’Ñ›:õ¢�æhtÃ|R` R` R` R` RðÜ)äwG’NU$\ê­©K­ÁR¯{ô¦ŽE½èA£9nÛß“ôEÒ±¶ï]¶QÙÖå£ò><¸Ô,õºG@oêXÔ‹¼Pô­¯Þ©­e.Ÿ%½¯8~-éƒÑZ<Ñû¸–šÉVY¦Dÿù˜[•]ªš¬Èß®¤×’v*ÎÉð3ô>®¥f²•E–éA'žw9ž¨.Rið�ÁZær ºÐJ¥f'kñDïãZj&[Yd™tâ9ОóoÃÛìw]żZ¯eÕs3 ÷q-5“­,²L:ñh­M{Õuój½–Ñ¿Øé}\KÍd+‹,ÓƒN<Úuãy¿º®b^­×ÒZ»(è}\KÍd+‹,ÓƒN<Úeãy™šÖz-=1z×R3ÙÊ"Ëô “èw9n$½‘te²"‡’¾©¾£ßÍDïãZj&[Yd™tâ¹õÕIçª{~ãLåv×,¾«­£‡–ÞǵÔL¶²È2=è$úN!o•o‹—¥Ö`©×=zSÇ¢^ô ïÝöoT¶lY«üÉ¼Íæîõ¬ [j –zÝ# 7u,êE:˜csâ{G*®´Ü¥—Zƒ¥^÷èM‹zуFs4ºá>)0Ð)0Ð)0Ð)0Ð)0Ð)xn}õ»#I§*OÆ÷|ÖÂê}1/úZ�šÙ³¨1}k4Çsh{’¾H:ÖöÍ87*û”}TÝÓðVï‹yÑ×zÔÌžE�éÛ EßËñ�Ê–0s½/æE_ëQ3{5¦ox´µêv”þ*éÓ3Žû,é}å:>T�yXå%3> ö,rIß:ˆþ{h·*?»ðsâ˜]I¯%íT¼/¿%ŸU^2ã³`Ï"—ô­Ï»OT©4øà‰cTÝ­ã¤òø²ÊKf|ìYä’¾uâ9ОóoÃÛì¿ðõǬσ«¼dÆgÁžE.é['ž­µi¯^øúc–üÅ7«¼dÆgÁžE.é['žíºñ¼_/|ý1­ë�«¼dÆgÁžE.é['ží²ñ¼§šÖÚÔ‹ÆóàÃ*/™ñY°g‘KúÖIô»7’ÞHºš8æPÒ·†÷塨¬ò’Ÿ{¹¤o�xn}õCÒ¹êžß8S¹ÝuÊ÷Æ÷%±Yå%3> ö,rIß:‰¾SÈ[=o‹«÷żèk=jfÏ¢Æô­ïÝöoT¶lY«üÉ¼Íæîõš†Y½/æE_ëQ3{5¦o̱9ñ½#•Wê¿Û¾Åûb^ôµ5³gQcúÖhÎ�@7üÀ'     Ï�B~w$éTåAÂÞ·í[¼/–Á#?d4‹^’�Fsܶ¿'鋤cmß»l£²­ËGÕ=^(úÖWïTž Ÿë}± ù!£yXô’|tà=ÐÖªÛ€ó«¤OÏ8÷•ëøPqDFó°È ùèÄs =ç߆·Ùáë�Y5ž‡\¬rÙzìC«Æó`Ç"/ä£Ï�ÖÚ´W/|ý1­ëA.V¹l=ö!2�E^ÈG'žíºñ¼_/|ý1­ëA.V¹l=ö!2�E^ÈG'ží²ñ¼§šÖÚÔ‹Æó�‹U.[�}è¢ñ<Ø±È ùè$ú]ŽIo$]Ms(é[Ãûr‡$»\>DFó°È ùèÄsë«’ÎU÷üÆ™Êí®S¾7¾/A€d—ˇÈhy!�Dß)ä­ž·Å‹Õûb<òCFó°è%ùèÀ{·ý•-[Ö*2o³¹{½¦aVï‹eðÈÍâ—䣃96'¾w¤ò`àJýwÛ·x_,ƒG~Èh½$�æhtÃ|R` R` R` R` R` R` R` R` R` R` R` R` R` R` R` Rø?úØ$Õ½»IEND®B`‚python-pot-0.9.7+dfsg/docs/source/_static/images/logo_dark.svg000066400000000000000000000223271525521106200244010ustar00rootroot00000000000000 2022-03-17T17:25:30.847142 image/svg+xml Matplotlib v3.3.3, https://matplotlib.org/ python-pot-0.9.7+dfsg/docs/source/_static/images/logo_hiparis.png000066400000000000000000002316401525521106200251040ustar00rootroot00000000000000‰PNG  IHDRÐ3�w¼|tEXtSoftwareAdobe ImageReadyqÉe<3BIDATxÚìÝOR"ÙÚàóuÜùõ›Nš^AY+(0.+\@á J'LÕ)«W µCkÌ@je­ é‰áìzWð}yȤEåO™ÉóD¤(jJ¾™ ðËóžX‘ÿQ€ê¹Û>ØM.¶’ex9ê]vùëÙõýlyع¿¼UE r,ד‹Z¶,êݘûÃ$ñ¾ð÷†”¹ç~� ”ØÝöA ë! Ê?„ü‚ƨŸ-?’%k·;÷—}UgÇt3Y>gÇ5«ÕK–ïÉÒIîïÊl:@ÉÜmì‡4,�—µÿù~x Øz6r>¶O“‹/aúQâ,O¼oÿ™ÜÇO•Ø$t€¸Û>h&CšÉuHÃôka: ß10¿ i7Š%ŽD?T`SÐ *›Ç<ŽÆ�¡yFäv’åÛÎýeÏÞcÆcý"¤mÛ)¦¯ÉýúX€M @(˜¬E{ Îë%Ý„~²œíÜ_vìM¦8Þ›ÉÅ…JÞž“c€Mð›C “å¯PþVÖµd¹ˆÛ’…£ðš%(…ÏJl#ÐÖìnû ž\œ'ËnE7±ŒHgò±£å�܇½~*Ït€5¹Û>¨%KmÄÝ oj-¤#Òf�) }T‚R=f¹ÿ•÷¯²ÜÐìÍšº]¶��û˾2@)+›! ªX­‡d¹öXÉŒ÷×Ó�Îs¾µA›O¸I¶½“\'÷™GÂÆÛU‚R‰¯ÇzÊTYá[ðeaPœ±fw­U'¤­WûJ…|¬¬'+=VRŠûënvÝôà0†ç‡ÉýåÚQ±Ñ÷‡ÿS…R‰ÿãN•¨²Â¶pÏZšÆv¦¡bh&ËÏì„ X�—qÞä�•…y¬ü+Ù'GJÁ„ûk3T¿]û´âÈû«¤&±µû–r@)|P ê 9=™ß\öfj1ÅÑbe€µ?VnA\‘Åé/•�‘ûk<Ù¥©cÝfÏ/n•b£îñÿ×O•(•^r?ÝS Ê 7]x^ F¢C!\áy‘5ãÈZe vÕÉžÛøß9Ùpnt5Ú,žïP8… гÑY1òfZñ]d';ëy¼8U ot€ÙÅðܼçëub>t ot€d­Û÷U¢tr%@˜’Öí…SÓʽôn•€" Lï(hÝ^4±•{MJëA (:À²�öD% é\ €<Ц#+AiüPŠD€0ž¹ÏËk7;`&t€r¹UX£˜ËÍ ÀÌèåò °|Ú·W‚ €™ Ð^ª+AéÅ6î[ÊÌB€ðÒG%¨mÜ€™üK ^¨+A%|H–Ž2@nn•rÐhÍþz£Ûî)ÀjÐFdóŸký] 汇íÜ_>¨¼¡ÑÚÊž‡îf¯+>Œ<7ÝZ`½ÃÏúÙÿ/ÿ é nv€üОª+Ae æAúð�tÔçÉ’Öþ#tÛ§PÃ󰼓Ô“ö ¾ýÉÇÚÆß—ºí=(a4,��-¢!ùtj#ÇàþÈm‰o³%ë½d?é0:ÀS5%¨”ø&fOˆ#4­ø¿á<ç5Ç�êë†Tñ,YnBþ!zñÃóÔñ’¶Ÿ²HOÄùÒмhÇÁî“Û”†êñ¹ð�d¹¨LG€ðÔ%¨z±õ•X¹nûkh´âÿûýœÖ;�ì%ë}Ø�ú=$õû”|ö3ä7Òö,Yogƒ·Ÿ¢k´âãÅÇìq£lû½ž-'ÉvÄÇ©ëð¨ëÔ0ÆoJðDM *åw%(´¾krœãºÎ6.„JGŠÿ™ÓÚJ×ú>Ýþ3w£Š‹Ý*bËþFë?ÉWWÉÒ å?ib+ÛŽ8Á’m»J–£dñ`„ਗ਼TŠöª¼” ×9­­³¡Uìltýb'ƒ´ûÀfJçþ®ê¶5“%vˆK3T»Ó@Q§´ø+ Ó›•Þ·SÒ s·} l­o0Ila¼h÷ÞÆ¶@Ž'!4Zq>åEŸ?}/qâIÍ ½ÿTkšœ44>J–/+zþï;ÙòkŠŸ�]•j#Ë2ìgKußÜ7»íë°�è�„­Õã¤&¹Ía?Ôpáÿµýoÿß þþÃÈqX :!­ÞòƒóÛlùõÏçyœtÓhÕ³ãåCHç7ÏûØi–F+Þ?¿…Ø)"íܰèÌãï…×PîP®—,' ü~ S÷–rËÒ€u1<Ž'Cüž]Vÿ$ÄFë4äœßfÇÅ�°ÌÝv/û¬“mK-¤Aúǰxg�Qµì?IþF�þͨt`ÐÕ• zî¶j;÷—}•6ÆcÀ:‹Ç`4™ƒØÏ•{Žœž\pòµCóoƒú­ë„�ôïvË㾋'äy"DÚâ=•þgHG¥?¸³U$@ êj¡Üía¡n•`ƒ¤Áh'¤�l=¤£�ë¥Þ¦4XŽÁy#´c}â &µº-ð¾‹z Ò›9?·>�£í*ç7%à F˜lª8š=m5\Úmh´bhþWX<<�ÿÏ’å�¤&‡… Ï_î»ÛÁ팷wØî€7 Ѐ×uÛ_“�ïC™NªŠ£Î­«ä³¸,2×ùhp~ZºÖå±Åûc�Þs0¼N€¼-qG£?@NÛ—ÿ ‹�:�'”38¹ÿúY7�O9ìÃ]w ªÌ�¬‹yµ lŠÞ¶-X­ê#_ÕÂc‹ìßG>¯Wøh‰ÇJ3 Obl´b˜þM«w`Zt€%Ú¹¿<¾Û>ˆ£Ïâhô-™(¾©}˜�xË͇�Nª7ÝJ·Ý›¡Æµ�†ê[Y-þ�]nU¨6Ã.E Ðãù½5Ý¿â~¯‡8jß<äð„`Évî/;wÛñ�ú«à ÊqŽ“}U­ahÞÌam½dé'Ëßÿ|Þm÷³¿SO>Þlt­ÓZô³¯®Çì‹a�^Ë–w! £ëÔJìûN6B]ÓíÚ‘`²yžßó¢�Š'šï€,¨�Sž|óŸlÖK–! „oC·íÿË¢ÒùÃ{öYÜO'ž×Tb&ûóGvÿ[%Ý™($:ÀŠŒÌ‹þ=¹¼›ý¦áYR�SGÀ†KG�ÇЮ9ãoöÃc`.,_‡8‚¹Ñú[!*³?ãhtu€ @X¹�ûËë»íƒ^HGmmØæÇí>6ê`Ã¥Áyü?XŸò7úá10ïýÓ†ÈO Ñ:À:d£Ñ�ï¶¾…´­{½â›Üé¨ó޽°Á­fHƒóÚ?ÝK–ï! Ì�xU]×Ùó í¼€B ¬Q6{ïnû Ҷmb¶2Î ºì7Œ‡�ù¯xi¤9ôž}ÝI–3mÚYšôØœlhdzéöÝC²ÏòX“'È• „vî/Ÿ¶2Mdó¦×²%¶²Üùzýl‰ëþ5ü:k'o‹#ƒ­8ýH<Áë̈sV~ü=ŽL�Ï‹†a:ÅÕ‹Ÿì`:¹ T„ €Bè¶O�‡ñyQ/4ZÇAÀZdý�G·€x„vȉ€jŠmÂÓî:SìPÏa=5¥ /¿)°ç´žšR�:°·9­çƒR�:¯é)K‘ß¼åuÅ /t`]z¹¬¥ÑÚUJò @Öí‡ÀÆÊ«�{])ȃX—ï9­Ç<èäB€¬G~ó ï+&y ët�ËZ-!:  ë”W÷�J À¢èÀ:]ç´#ÐX˜XŸnû!ä¢oiãÀ¢èÀº}Ëi=Ú¸°:¯yP–®ÛŽ#Ðû9¬©­-`^t&Ú¹¿¼UV$¯QèGJ À¼èÀºõ•H| ùt>ù¬”ÌK€¬[_ €ÐmÇðü:‡5ÕB£ÕTPæ!@Šâ,§õœ(å ˜o¨ :P Ýv?¤­Üeú²¥á¹•#@Š$ŽBÏc.t£Ð—ë(YŒ@*G€G:úŸ9¬)ŽB?RÐ%h´jÉÇ/ T‘(–nû4ùØÏaM'æé^Šó`ô9PQt ˆsXG yÏ•2G�V=ù¸Ÿ}ÕS jè@ñtÛ½äc'‡55³Ð—E¥£ù/¨2:“ô•€5;ÎéÿÑ…Vî¹8 qny€  0I_ X«nû!äÓʽ´r_L:ŠÿèÙµ T�X·[%&J[¹Ÿå°¦ØÊ½© sHGï_�Ù7žÇ•#@ÖjçþÒÈ%àuÝöiò±—ÚÎC£µ« 3‹á¹øÀF eð),>½H �ȯ>ƒFë4ùXW`SЀâKçC�!ú¢Ýk„èÓJ[ÞŸLø®öí@% ЀrHçÜþ”Úbw!úkÒV÷¯ü„ix€J åÑm÷’�‡9¬Iˆ>Ižß(°‰è@¹tÛ� D_ŽÇðü­š�T’(!zþ¦Ï£_B Šè@>VBç¢ÿ•È›ºïêaúð ²èLÒW¦–†ÏG+ÿ»iˆ¾o)ƒã8½¹�û®„çt&ù[ ˜J:ò<°·kùûÝv/¤!zÁ5Åí¸H¶çj#ZºÇml´.Û<»ž¨":P=%€’z ÏãåíÚnG·ÿöûœžW쇴¥û~…÷Ûp¾ó¦ƒà‘˜ÏcxÃØ‡Ðm÷×z{ºíxâHô¯9¬-nÛU6½V±ývš|ü™í7FЀÙ5ZõGi?†°·…¹mÝöqÈg^ôh8ý¼ômÝã>k´â>;Éam·î@ Ѐé¥ófŸ‡Ç¶íC? u;ÓyÑÿH–ëœÖxÒ ý´t#ÒÓàü&ÛgyÜöÞ`´?@ýK –çnû Æ¿AßhÜäv‰ñͶÑ+·;÷—Þ€(²44n&Ë—ð48ÿç9]ánsò~Êæ2¿˜p»gÿd°4Z�äò[ÔuŸÕ³Û[Ïi�ýd9,ô6,H€�£»íƒø†Z3Y>†üÞ¤Ú”ÚÅ77{!¹t½sÙW Â¶#¹‹>Š7 `ëÙóÛ·N-nKïnû:Ù–ø\3†ÉG9­µ9X­ø¼5Žr�aúmöY-¤mç㉵œÖ·ñ,Ù¾Ž».Put€œÜmœ†É£qx[¬Û~¶œ'õì$—ÇF¦Ãèÿ„Fkøu ^‡Ï{â寑Ÿ}Þ½çÑ,#�Ó0|Òó°a8þïìóZ˜-|}HnK¿ÐOOV8Nêð->ß ù�ðët4XÃôxRèêÚœ7Z»áñD‡zŽkŽÛ#86Š`AÙ¨ó8—à®j䪙,ûI}÷vî/o• ç÷…×P†Ø“Õs^ßóç’ûSÖ°(õ(Ïóµt”øÞÚ›Gµ0 ÓÓýÓÏjOˆè…ôDƒÛï7µìïÄÛý.»Ìû$Þx»çÀF ,Nx¾<ƒ“î¶Þké@ÎòøßýN sYG¯¤Ûÿow£'~”î§£÷{K Ò‡jÙ²Ÿý�Ñ“F�ýç]†ÇØî˜u-S=ÿ§9΀M&@X@Ö¶]x¾\1D¿q”°jº?PerXG}c«GŽçó(¶›î•´ ûîFùŸñ4HÿÒNH«P/È1Õqþö:…oð¿)À|²Öí_Tb%êI½ëÊPi}%(¤% ’ÒöÇyU[ɺšZÅ£œÖ³_Òc(î÷š;Óå?é*éÝöaòÙ!¶/¯öó“ø?þk²¼O¶ù�d9ž¤èók†üçd2'+TÛßJ¬Ðy®ëJGcoŽô„¼þ/×’õ�–lûãþ>q7zâ¡RákÜ–4PŽAzì‚Ô Õ8©,î£ahþ¿Ér¼ð|줅;Àü>*ÁJÕ•€…5Zqätž£žc˜z6eª‘4<¾ ùžDx’¬·WŠ9—Óí¿ FŸ?WÝvØÞ=„ìÅûÇìyi¦qzÈnû÷Á¥æS ̯®+µÛ¸ïÜ_ö”€¹¤m·Ï—°æz²î«ÐmÚ€*ÆðxÁáURÃO…ÑÓ‘÷Âóñ~lÄV>†éÃã!¾xЍÇÛOføæs Ìánû`WÖ"Ö½§ Ì¥Ûî„´3ó×ðýo{?¤scÃðxxúx’ŽP�ÏWÏ.k!ÿ.ú#Kœçvð¹Vì¹ ÌÇÜçê�FG¨�J[ÿïŽ<Ÿ�ædÜØ~}4¿MÖÿ ÈË'@˜OM ÖâƒP*iðݹæZQŠë7%˜KM ªE€A€tÊdW €e P&[JPY·JÀº Ѐ"xPÖM€A€À;÷—=U6‰‚`^” Zèè0 @€ @€:¥r·}PW`èÀÚíÜ_öT€u @â_J¬]£UK>Æ¥ºí¾‚°tÆé)°r�V=ùx3ãcÕm²|Ývo‰·ë(ùx>rÍC²ü‘ü͇Ôàv¤×9ß–ÿsí^nµNó/É·y÷Ù÷âÇ~¶�óß6˜@ w€ùl)ÀÚÕ“%†Û7¡ÑºÉÙeø2æ¹à~Ajƒçf²\%ÛÿW²ì–bÏ5Zñ„„¿²ý7é6מm[Ý!À² Ð泫…RO–Ÿ¹Èéújc¾ó±€5¨e5Ø/ôžj´.BœÏºmñ$‰¦C€e Pqdx­œg· Ï®ß_âˆ÷E]ö¶5Z§!U…dtŠ®7æºZ?2<^G7Ÿæô·›¯|/ŽôþZÀÄN’å°P{1=±áË„ï¦ó¸?zÒ®£'C†n»ãîÀ2 Ð(¶n{oìõi«òØüùˆó8jütá¿›®ÿµÑìñï|]s ê!Žºy;ãm?,Øž¬O¨çø`< ÜãÉ'AxÀŠhá@Ùl)0Ðm_'�Ç|§–S ó·æ9ßÍ}ÎõÙkÐK>~ûœ) ׋d\­:ƒñnû!YN“Ïþž°*tÊfW *ëA ˜Ùä`µ¶ÐzÓÑÏÍ)~òsjÐ+ñü{Šíë;ÐX:P·J@�ìOù¿j_©òA (s Ã¸Û>¨Oû³;÷—= 4–צ|ÜÈòoÉÒOCóÚ`®ô´�|Õj° õäöŸ†8—|lÙk&@‡Š¸Û>ˆílwCÚ¢.ž½½æhq›¬gøi<«>¾pý‘½9p»siTÅ‘¶Y?ó�‡…Úš§ó§×Ç|§“½V~>ê<Ε~½¦ÄÛy•{ –#ÖèdÌõ'ƒ¥ÑŠ··ҶϻmïE°Rt(©,0�/’?d—[9ÿ‰aø^ù›ñ"¾˜�¡úµ@ Ò~(Péåç~i�=îõð¢aöþØuÆÒ�V\÷Å‹Ÿo´Ž—:‚ºÑºsm-Lžë½S¸ýÃð4$¯Oø‰ú˜í~Èö癹ÐX:”HšÎ^È×Öt3êÙr’Üžá‹Øï;÷—×ö°A�Ç5UX©“þlÁ¿÷eÌußCôÑ�}+ûº³ä×ãӺ͡Ërœ,7aú�ñçšáñ$…Ž»Ëô›@±Å�&YN“å¯äËŸÉrÖžOz{•ܾÿ$ËEòT]M ëp¡‘Ê�Öî„ç{£'Žóý�Ùþž*ì|âiKö½ìvÎ"¾q‘íX:ÔÝöA=Yâf18? Åk†aúÏävÇ¥i/°B10>Ìa„òç1×]? ¤ÇuaÛÏæN_çöÇQç{…ouCônû}ˆA:j–Û{îP`™´p‡‚‰ÁyHóz‰7#ž G£Çíø–,_wî/ì]– Ò@ûÏœ‚ãæ˜ëžŽ8߯=d_]ÒvöF>¯…—'ÚÇÛ;…y>N·}FOFHG—Çí¨'Ë»0~.úúàDó¡°$t(ˆŠçÏÕ²mú’lßÙÎýåW{€9ìM¸¾Ÿk�ÚhÅÀöùÜÜ1�7â<†êÏÞ8wúr^ûvÛ{#·3¾wp3æ§âkðÃÒîå´½{Ô˶³bÛö—ja¶Që05:¬Yœã<¤íÇö+¼™ñ͇ód[ã Ç;÷—×ö< ø] `ÃtÛ½ý¥�^Óþ'4ZÓü~m0Šú1^^=­X“ú³ï4“ëÿ\úß_Ý~ï$ÛßKx>ïyÜîž;Ë`tX£»íƒÓ�Îq¾¿!›\K–«d»o²`Þÿ'TÓ­°6�V Ê›9¬éóŠnñÙ„ë«6G¸)áX):¬ÁÝöÁn²ü ikµMTO–ŸI Ž Àˆÿ*k”×ÉíÍ•ÜÚtT~oìkî´Å{q5ZçSÝÆô¤†Ý1ßqÒK#@‡ËF�ÿœðp“ Ûº�@ä5r|+›K}&�B/î û�VœÓ<žP3ø¼ÑªMø¹xýEx9'}$@`iÌ�+r·}_ð]…—ó“mºX�8ýÐÜè¬EÖŽ{½~ÞkOÆünœK}ù¯q'Ï…žŽB_ÝÜñÓÖ9âÍ‘kš!�·½Ÿ\öG®Ÿ4ò<ê%ÛÕwа,tX�ز=¤áyM5Æœ\�ÔélçþòT9 ¡§À7bü!tÛ_ßüÍñá{ …�“ß_ÅüÝga|øR¨ÇòFë(Lno_ Ó¿grìp`™´p‡%»Û>ˆ/o‚ð|'I½®²ÑúE¶«•òeÌuÓŽ Ÿôs«iã^ž¹Ð;Ù²ˆÃd{µo`©è°DYx>i¾.Æ‹o0ÜÑ�‚óP�V<9²6æ;ß§úýt”ù¸ýó ·bÒ\èç…©s¬S·}bþ´]û4bhþ>ùýŽ€eÓ–änû ¾H=R‰¹Ä7/n²yÑ�Y°9bÝ[ÃkЗ³ÛžeóoaÜI–�ÖÖmÜg¯A:z'Œ;`±¹Ð{nßüÒ¼“Ü®xýǬþã:<ÝfË·ÂÍå@¥ýOnÄÝöÁiHçe¢œz;÷—{Êð䘎£Î›*±°ø¢|OˆþäØŠÓÔU¢œ’cùT¡2÷EÏ]<÷À}kÄça=e6‰î�3áy®âÙûq$º¹†X::ä(›ó¼©¹†èæ`ȉUÀRÐ!'Yx~¡K!Dàùÿª©§¬“r�µž/W¬ñ�2°,tXÐÝöA-vWe7›cr'@‡Å]­dW©™µË€\ ÐawÛç!m-Îj]dmóX’�ûËž*›F€sºÛ>¨'G*±6WÉ>0òX—JP=t˜CÜ^©ÄZÕ’åDÈ‹æ[·ý¼~GY'X˜f”¶M•(Œ ­Üȃfw®…R æ¢ t˜ÁÝöA jwU¢pN’}SS€�úŸìqÈ�¦”µ ?Q‰Â²o6KM ªgçþ²§ ¬“¦GŸ›k»¸šwÛº07:L!}þE% ÏüôÌM€Ó1ú¼êæÄÈŃ›H€Óù¬¥a.t€ÅÝ*°‰èð†»íƒfrQS‰Òh…¬@] ªG€o3ú¼|šJÀ¬èðŠl$s]%JÇIÌL€¯û¢¥T»Û>ØWf!@‡× aËë£0 :Lp·}°›\ÔT¢´œüPmþGW׃°.t˜L[n[Ú¸TZM *ëV X:L¦xù}P¦%@‡1î¶¶’‹]•(½º0-:ŒWW‚JØÍN†`6æ¢6’Æ3ú¼:êJäɉ9À†ø¥À& ÃxæÎ¶/&q’@E Ða<áˆ} À† Ã3wÛµäB{Þê 0:¼TS‚JÙ2_1Ó ÃKu%¨£Ðx“Ø5%¨œß• ²”€u ÃK” rjJà±�Òø¥¬‹‚Ø º ð&:¼´«l8sÑI€/m)þOîV €M$@˜�N%%@€ @€:: Ð᥾@áÕ” ²”€u ÃK}%¨œ[%¨œšø¿ y ›à¿JÀ[èð’QOÕ£,o ÃKF+W�“"fãD`# Ðá%akõxÈÓ¿•¨º�ûKω€�$@‡—„­ã ` g»JPMtxfçþ²§ •"<`*tOèj_°aè0žÐµ:~)Óø—ÀX?’¥© •ÐS`£4ZGÉÇwÉRK–ÃÐm÷Ÿ}?^_O–xýCò}'‘dè0^O *áaçþÒAu·}PKçû*Q½ÿßJ0FzÿL–­ìšO¡Û¾ósûÉÇóì«ëáyª™,'#¿3|ü¾~ú‘]Ưûv`“hácdoÆ÷U¢ô®• ÒjJPÉçaB€qÒ üpäš‹Ðhm�ùÉó‘Ï�gø q]õl9É–«gë¨<:L&|-¿JNMÛ¬7ZWÙÈòé¤#οf_ÅÀûêÙzOÃã fgFŸÇõÄŸû#YöBÉBgä»·3nËn²ÔíT *è0™ðµüœQ=¤#»÷³Ï§×mÇQå·ÿ¬'�ïi=±5{/ åÿùÎqrÝÿ –Þ‡·G±N–›äïÿ'Y.-äÇ�Œ(s Ã;÷—×wÛq8/üËé:Ù‡æP˜]_ `éFGy¿›ã÷ã¨ñá|èç¡Ñê…4Ⱦ~�£Ïç}=ôø{ÓÍ}¾Ÿ]Æ¿ÝÌ–èÇ`þ{²ô&Ž„( #ÐáuF0—×w%–ĉU@Õõ•–, ¦‡Au}Žß�÷ÓÑùÐc+÷£ìóXwf\ã‡g·m:±}ûcËø~ ßÓ`ý"YþJ~î§6ï@YÐáuß” ”â›6N~–eW €Ü.ôÜâé|赑ïœ-øZjû#ŸÇÖïÿÒ¶ïñvõŸ=ªÙå@há¯Ø¹¿ìÝmô½Ð/íÛ€åj´âëÄ ¹öG²t¦lY¶ž­«>˜�|Vq>ôtd÷0„ï̵žÇß¿�ñ÷F·½—ݦÛl=ÇÏjä$g èð¶?CœS޲í3€ü5Zq:—“ðØ2}T}ð½Fëë Ü~]/[Ïð÷zsÞ¢øwn²ÏÌ¹Ž­9êP oïé‰_ÃãHy€ÂÓÂÞÖ ³·±c}z;÷—·Êä. ÏcX=ž÷C~�¾n¤mÚûÏþO™z}1lOC÷ÿË–8¢ý"«;ÀÊÐa ;÷—ñM€ŽJžÑç›eK *Ët,@}ùü8AþR·}=òúq4hç×Èçé‰a1Ho´ö£×cø&‡p>q OoÇ¿çØ®Ú¿3ºMß'Ä9ßãhüþéHüøXÞ{sM�VÜîŸÙ6î>û?ß ±e~ÚÞÿ}`%þ¥0µ³ìM/Ú‹Éès`™jÙåCè¶ßzíñ-¤áïèï�}3òùyh´Î'ü|}Š×«QŽíꇴóG|­;í L&lC ôãú¾fËëÒðüæÙëìxB<±à]x ê÷C:Jo≠9 Ã”â(ô»íƒØÊýD5 Éès`ÞpcÀÞh…)~îväçjc~¢ÒPûÇë9�{kºíN˜½ãÚèôئþ{6ò~Vq¾ôax·óSÀ§Ò¶öW!™—x‚Á¡CX&:Ì&žAÛöÕ”¢P:FŸ䦯ðªÚ �øk#¡Ó‘ÕÓŠ¯eêÙ}/~G_ßN1Ê}=Òm1Þ,�ÖCvû¿‡8Šü­‘â�Vü½Ý‘Ç�—£Ëc˜G�§-ÞkÙß9{²äÌè0ƒ�ûËøbþX% Å>VænûÀ4À&ø[ Øé|ãõl¤ó[FÛ›½ñ³£#´ûoülQý¿ƒùûíÃdùZØð<Š£æãíMow'<ŽÈÎ÷G•ÿ'›¿ýµº~ùüì•9åÂÓõ»\`™Œ@‡íÜ_^ßm\‡§oˆ°>ÇÙ‰ «à [¨‚š§íÀwG®ëÅ×Y@œÛûä•×ñúÚÈÏž–FkÜÏvÞY]EiÐ=KØ]Ïqo Ö}€9�sŠ­ÜCúf ëñIëv`*q¤x?ÃcxÞ÷Ù¨éÓäó½ð8WysâzÒÕñg¿¾ò×âz> æ3ç5·#û§™Ó:ã WÉú.”˜—sf;³ž|œíÜ_ö”xSÚ’ýdäš82üý“¹ÇÓ`üÛTë‹?ƒ÷þÈ^vBÚÚ=¶-ÿ4hcnô4þùü<ë°È~ŽÝö³¯öÿi­0#-ÜaqtÖÊ}tŽ;–+Î{~ª $>(Aeé2ä'å�V ·Ï³kêƒpõåüÜgz Jø¯ ¼ð~9 �sÊÇP½ÒQüµìçzo®+Á~4²ÿö6bîy`)è° �ûË[ó¡¯Lí¯ "T߯ð8Š`qÝö×Ðh}È[j!mõýéŸï§-¿‡# �_Ý~9Mjÿï�†ßÃùçŸv HõÉç¶:Îæa‡õi´ê¾sëä€â Câ|èYˆnžµå‰/0Í{Ì+¾f«‡4¨ÝÏF-÷²×qõìgú!mËΪÄvø�Ö÷�çõgßݯ�D�†ç7#ל%ëë(ê ÒžOøî·ÜêÙhÝLøÎ�Á‰ùmOÜ–Ý ÇÚÞëXK>~ÉŽáÝ7~ö!{ìùâ ;£ÓI,kÛ—?Þ[Âmžïd˜ô±ýóšîQ³ßæU³¯ßÏo³iIòZß²}ó8À*Ð!';÷—�»íƒwá±mù´àKjÜW `.qÔg£G�ƒ¼á›ÿÃé¸bèñÉèеì›^ˆ�bBÆe7Û“ƒ¨tŽó‹‘ý÷Û×W~¾™||7sPT}±~õ ßû‘ãß©O¼¾ÑŠ^^�v_ù[ùK�ÙxòGsÆšïgËy²Ž¸íN5]A‘¶}ùûkÞ©kk¬ÃVA÷ÛVÎck�5þ`~SÈÏÎýe|!ÞQ‰\ Ãs-øVÃó.ª+ ¨†!ëVx ;âu{¹�eÞýÓì£Ør?½|íd†8�Úè¨Íá<ê/Gr>Ž–7ýZñ\ŒÝoE×hÅüg˜-<g?;vo²@€53r¶syx·}rx…ð(ž-%6ä9TWÚ2¼Ã×N©G%?�kywŠç+½‘ÏË9s:g}}ä1ë6<¶Î¾tŽæMÉ›‘ºôÝ ù;†è{¥9ÓŽyOã�á¿’uN"`mè°Bô\Ï�"ÚU¨„8¯ö0Tm†Fë[í“—'�[Ëž‹¼ ‹µÏ=y¶îøñ6{ [ãöKQë‘—ÍÑ×�ƒ9‡ÓP½G¢Ç}[c_…ÇðüZ ÷B?Ï>¯Íy_¬ûãÅÿÂùà~)DX:,I¢ÿ �óê1½øæÍ¡ðXŠ4p=y½G¿¾/Ìè×tTy\>„ÕÌ3;r†¯Íz! Ö{k¯OZ—Ñàòx°/Ó}z˜|ÿÇÈ÷ãexË=3¹jâ Ì8ò\ëP`ybH×hÅ€:Î?\ ëýš¶�·cx{Šb7[޲Û9 Ô¿ý\¯ÖC¶Äå1<ï<Û§�ä6_ƒo…ÇÀ2^÷©”íê7O}Ý[Óñ5Í}u?¼Þ•êapl¦Ýžýî°sÄÇðzÇÂy�Õo!=ÙeZŸÃäP?Þ·þ.é1Ô›ñç_;QiÖ:ô7ä~·ó,§÷fˆ/üFç\c “8r_€‰¯?ê! ½¾­ô/?†æ1Ä*Ë41��z£Õi‹ôÕ…éiç€÷!†�“F)wÛ×Y‹íÑVõ±Í{ßá^±ÿ=ááó+߻͎µ‡ ÇæCvŸ¹Ìsžž˜ò%<}¯èëÜÓ'žžæTãÉúN€¢ùM `ù²VägIN_D Ϙ‘ÓXLh½,« IâÞ�V<Áú¯�Žzß-iõj! &ÛóW²e#l—½Ïú¯†-/ÃóÃÂŽfæµçxW½mõW¾w8uè.=ŽããÏuvímrݱݰ~F ÃŠd-É÷î¶NŸ½˜ßtñ�ŒOI}úJÀŒv• ²´ØVg#“ÓÑæÍðr´iUÄíûöî¹m,[0úÕä£É\N†^AË+0(n¹¼S+°•(•”*‘½±à²+0½«WÐìÄål4+˜Çà ¶h™ A ¿¯ -µøÞ{Ð<÷œ{•¥òÛÝÁÏ÷ Z§vþ<ö—‹ÂY¹ÔE{ПW5 (O;~ï–óéüó2/ ß×íõ *öôû‡ó,Í06>Ë.íñ\ðxÄç$`;D@÷ð4ÖãŽló˜H½ ÕS:YÊJ�2Üí Ûz”µ›uì]îs7VI`ûÙü÷YÌ�ý¸@Q£ $1É£klŽ:ìAq aŸ]úßž¼Ò°;^—ð±FõþOÁÑU¥uÙ;º¨°Q?¬’±ßöidø¦ŒäÕL~·Ç~ïåõX`4©c”µ.x^µ"ž¾˜q îå}õ¼ûãã&Ÿ|AôùÏe&]åî €ÍPÂؤVIÏÓ.u¯ÏWq<\c<- ¶Š ^óÚÐ럧1™ç£×‹æsÁó�:Éf/Q·Ÿ«õ�J»w‡Õn/Ç—Ûëæcl«.kÁ¶8^ð|5¥Û§÷÷ªÙè£ zYŸÉÆŸ'´Ïç¾6y¾8™sœ·u §óßÅœñÎö›u <*é5Ú3ns΀ ЀMê•ð÷ÙíåòÏsxz� ž¯âçÒí³iz˜L€&S6`¤xw‰GF6êñR¯ùc Œå,?yáÇþï/™�™àëÏ�ydGÖû�ÏÞ`{–O¡ÚsÆ»%Ï«ŒƒøÜùñÑ_[Yª†p]x"Ez¾³™Ç»b^•N>Æö[{ TW‰ ]M“  ÐdÊ^n¯ž&€s{�ðwÁ¥çKe?§@×G�¾²“’ÇÀ2Ùèoó5~Ë÷„ŠríG�®Qà¿àù¦Ç_5Aæ¸ü8ã³gg8NR�óh�qužEöútÜT^{w®qéιW'KÞN�¬‘&û¼ÍÇVgÎó]hx˜îš¨…ÛË“ìð4JÊFVf{ʽúY*Þ]á•"(jÖjÞ ú ·†1ý{��Îì§ZöW*%?.KãõßK�s§ú�/î}v’­{9†4cLœxÔhÜ=ÏÄ9ÎXÛ½±B^fiBÈ"F×È¢â|öRƒÀlèŒô5µ‘2‘cë­é¹_Ž­ƒÞÒà…ƒ=ÕŒ�E³Ñ÷‡ë¡GƒÕ¼{�x�‘ ü®’÷˲ã¤;èû_³T5`�¯s>x�ÞšÏ÷•_Ôq,÷òª ×k_–ž€X€‘¿4;%‚·—ϲ”M-`µ˜õ”nŸÝO‹®�þ6Ÿ±ŠQf}wøš4Ìl±\Ízèiì?_Ó9£Ÿ¥àæ�Ýé±ÜŽƒòÇ×�ñ‹@XÀ·'¯ZZ¶TISPìFcÌÔÏÖ]º}zÅçTŠ@é¬ÀÒu^j{Ù×é þûlðóX–fã¬#è8iŒÜ��3Ê ¤§I"‚›ŒŸ‡ÒDžU�– 0¾ t€ �ua#×× �l<{~6z++¶Nõä±@�áj×vN猨Pð¯üu»Y±`zŒ³Ïφô*ðx<ÇDžH·Äué.KŽžå>€~©ÃN|{òê|åܰI½§ß?h¨ä|ù?­Ð\ƒså/ZÁ1Hé.ÇÖ¹fØÊãíãàÇ‘–pLÁÊFÙÈE‚Sé1£u·Inò uêÛYk£Ë¶dS㲕¥‰í)÷èe 5QƒòÇV\ç °’h †þÈÐ�rD€µ�ž/PM�‡óÁcºY”Ÿ¬ØÑÇK?úð4Îç½Ò:‘�~xz3¥�®²TM |)ˆu–¿f+ÿk?K�Ñ �Ñ—ú4ÀØ€RÂ`Ö@¯­j(•l%¶K tF–òþ`û<øÿN¡Ç?¬»}¼ãÇÇñÒÁïÃÓöà¿QUäkþ{¹¦¯�Þ.Üß‹½ŸÌÿ9Øâ¹Ç?¶ò¿ý™gÆ %€°˜–&¨¥}M°µzš`#”;fÛ\�ýe¾¯³ÃÓë¿Ëº/êö²›¥uh»;؆Qºýf©G¦vþ8öYêó0]´ýë£ÑÚèãç±³R_+ÆNš�ñøz[ÿ‡q'ˆ�%€ÔQ_l„:Û#e;OZ ¢“¥lèb“°"ûö22ÑvèX‰Œî“çÇì·Ùz³Ñ#ˆÞÍÿÒÊ~x¯2ž:c¹lÿf¿§-&XŒW*¸Ê+ #€ÔÎÓïú™ zÕîí®„;ÛälÆm­,q‹Wo/{y�6¨Û~Ì,¿žwZ÷ühFû¯3=Ù£5Ûß”ôãã)JÚŸÿTÖ>U*x¹àjJ¨«MP©ž&`k¤láö÷Œî祬@M%Ã{[{NHeÑ—iÿT.¾uf£w³DßËÊÉBo�µKwÆëöÆ®_GFht ®Þkí K*’ùÛlæÓŤ’áQÒ}¼t÷6¸Ï2¸—1©tû4­l]ÙèAôղРðÿ¾À#>å?÷”q€æ@j)/ãþNKTâ]ÞÞPž:žþg°}¬4ˆ˜Ö6o|TW?.ÄM�ÚX»»%½÷~M¥ÛgYO6zê›XǽSÒ3öó÷¹7c¬ŒO¦h9@³ uk ßi†µºËÛÊÖÎR`:‚©Ufg¿Yá±Äýœዉõ°ÓÚÛ‘‘Þoô9!•§/nñÒíÓ´²ud£§ zoÅóäÈoùÏN–*\OØ×ߦ<ht ¶ž~ÿA·F @¬G´ëAÞÎP¶ßþg\®B dvV|–žG&ôrëf§5°cmô¦NLY¥tûÇlñÒí³”Ÿ�~{y·ÂcïÇ®CGù8{“�&ˆŒ�ïTma”�߯lì¥@j-‚»ƒmŒˆ(G´ãE´«à9k‘Œ£,î^…¯ü¶ÄçŠ,èÏK–t¿Ï³¸ŸWüþWõné@sšpÐ.q_ZÙºÖF_Îûöë¡4ûû±6ˆ1?>‰àw'ht ž~ÿp>Øþ5øõe–Ö–•^LÊo²”]ú,ÚS“°Fí±ßÿ[áë¾^Ãûø3_×»¸Fß^F�“¬þ€úÙ²Yó)ëúlMûµžµÑ‹÷åøugÿ‡v‹ñ¥Üc?n‹û¾s*€æù‡&XȾ&¨¥–&Ø=O¿ˆ ðÍèÿ¿=yÕ2溓iNÅ^Œý~–žFpµ7Ø> ®RN{š`]ǹ ²‰?ž¿›E |™’Ü·—ï��óÖUöPÞ»nŽW(7~�•Sº}Öõ>²Ñ# }±Á²èYÊ>ßôÞë¯ØžÀ  ,fOÔRKðôû‡~–‚@}´§ü-ýýð4‹½Áö%+/ þzÍï©3ÜÿÃÓ—Kíïíeœ§^æÙìë8ÕÍ×n/®üÒí³Äk-Ý«Š€øáiL™6±ò>KR.Ï ¹”pÊóãúç‘uYÃ�ƒ�qŸ$GFv”çþϰv*¾¬*2»[ùþž/ý ·—Ñ&ϲú”÷Ž@ïÉ’}½Ÿ÷a•F}ÐÙP{½Éö³”‘~‘oÃ~½½<<€f“�”i<�ýþïÌæXog©¼{ܧ5v¿¸­3¸ïñR¯˜²º«Ì莲ô¿e©L÷2Ùè)h�²™¯³ÍVTYµtû¦Ä„‹ÑÚäUŠÉQíàK>¶{yØ.è@™Ö?/ ž‚´7ùv’g›·óûÇÏ~)¯Y�ȾŽu¹/†kœ/#Úçðôy–J“Ÿmà=ôòŒøâRþþ†ÇZõAôôZ]‡9l/t L£ joæ½Òšàݬœ`äцÞkd½_åÙè/—ÊäN�9žÕd¼EûßmdMt`+  å¹½|žX«)©ž^«µáwÝlöåxéŒî޶e£¯»ý.òI EÛ;öëc�F\ìÏõ°íàçñç†ý|û÷£sEL"ù#ÿÙ3 –>ÎÚùu0<®×™¿òŸ&;Q‡ñ:º&Äõà×GŸ·ÆÇko©ÏIÀÖ@ÊUíäíš¼ëXN™ä˯+åà²Ñ×õÞî–.;Ÿ‚û­š�¸ýaIùÛËs_A)ðõ¹„gºŽ«X>z|O1>ßd©2ż±z4ö¸Ñ2¿W¶ÿ‡§ÿ›rK´áÁš^ó*KËFLÓ¼öq‰¯�ª¯3î±ø{�Þ^ë¶|T¿Ï•ŒßÃÓÎà¿¿eE+Àžö³T¡æ}éŸÒÒ"gkm—*ŽÙ*ÞÇüóÿEi×ÔYÇÀíå/^ë^çãu¯àx½ÉÇkÃ×äfŒ±ÃÓÏ6?Øèg¶š:Ðd/j¶?ñåì~ž�Þ[êÒ—´y€"‚]eg£/[º½�ͼmÒ›Áþuk‘-–¸ãÛ?³”íN¶ô8Œ1Úη³<èð{¥“R»G@ ³Â{è ·´ÿ'KW”¨·‹löä‚xÿeN"¸žqÛòKI°©ó['[m"Ukì8ëe)PÛÓ° ‹‰zÏ–ž¤·{ãµ��×ö ãõípKãõDØè@“µk¸O­,2ˆO#Ëûb…lôîX6zY뼿[êËßú•n,ö/²Ž«P?db¶³ª–.¨¿8"�Çk�ÍΦ[vÿ?æ“ã­*ã磘à3»ò@œsž•Ô/û3îq¡DrC¤J×sús™ëw{åkånÙË�]Ë–ÌÿÌRæg§Ñxýj¼Âîø?M4RÊ8­sÀ2²–>ç�‡åÄ´·—/¿ÅÖ_qâñK>ö:«p¸“i^ÕøÛl_³‡/éÏÖÊ��ÎÚΩÎÖ´ÿí,L޶ªWÒ„†w3û-¿W=?Ïê—Þ KIPíµ¶“¥2üûkz…·ùq¶¯±²Ÿ/ÅÀäñçí?³rƒçå~¶A`1¿j‚zúö䕸ì®ý†ìãוƒQ©”tdœ­pZnmö®mÀæÄíϸýlÅÑõÜóõ—Æýu¯ÔÊRP²­Ñòvë&ö”7^?gëŸÐ¶Ÿ ¢ÃÖSÂ`12Šô õÓ¤/.#e¾_.]²8¿OÏó)K�V�Gß,UF;e‘^7¨�£�»k•ô%ýhüÝei]ÔžCr¦«aIô2Jv§ EÑ IôOCdirìhÝöEÅ$€´´Â6X¬”{d¹,Ñ?oç´­ÒíMPæ”'_üœÔ·&÷Òãõ>«1fÿl/ò¿· Œ×¢èØNèÀêR@+Ö þkì¯ýìÇ Ç»’׌|ѰVe£_¬Tº8jŸX÷9Ú|ÙlÏ&”n×®èuÆ«½ÜÒ``w°ý¾à¸Žöè̹ßhMÚÕ2ñS™þ� ŽËxŸò Óž/²8[`ÿÓñpxz·5Á’8—žFÛM=ž"^ä|•úgÖy)Úï|É=.:v>O݇˜ô²¸û5µÿ/5¾¦e‹#£mÞ�µiçÁ4&ŽòqÑš96V=?ì–½üœt°óëq§Ï W Ž×tm›v¯¯øL1 ¢?³&zÃÏy0�:P†ø±3÷^‘-•‚rïJø²q¯¡ít•g£¯x� T ~E�cV†ßÅ’¥Ûçe‘Ö³}ã‹ôõ8²Ï·7“ö¯³ê{ùx‰€dÈÞθo{XÕ`µ6[¤úÂ]~|Ý-pÅ1t3œØ’ž{Þ˜�rîÏ·(Xrœ¿çiçÓ¨žÑ-ð~çMºY¾t{Ñ*Q1`²{#f¶Ûh²Ë<ﺾ¤Û»Ã-e O ¤ßgË.3²ÛF�ã㯋LªºYhŒý8^Û \sF¯oòlk UjeéËó¯yyðU4yíÉvÞ«­aÂÛË(wÁËI_ ÷–Êv_<›«®clÝy~‹”Œq8/�ófé×HÇ˼c¦;<&ŠN¢ˆ ~Z§þbÁsØöôÛì>[4˜ºHÿ\(uÜó&AĘy9<Þ‹¼Óϳ—ÚˆçP{y�|ÂÛ®šWÙ ËRàüåãµ—�×›¹ŸéÒä`‹ «‹/£4c*Ïxðh»È·nöxle«¬­½zð½RÖÒáéÇ<ƒj•ö� y|ÉÛ{tËÉ’ÏxÝàv­bbÅ¿ÖvŒÅ2ÏÝ ã°¬þ™7©#‚çÇ+îÿy6ÀÛ­ê÷QþtGs'û¤sجþY¥t;UI·ó&©¼œ¹,ÂüñvŸ§'_§ÏWu•O|ÛµñÚÊfW=ɲ<ï®8^_f?Nú˜äÌ0„í"€”+ÓÇ·ó|‹/ÌŸe©¼rh¯ð…ok‹Z,‚–��>Ê �vމ Ëe{žFlß@ži½5<ë¬ÀÞ¼2äˈ`IÆí�­ VÍ/åÞʦ«æŸ7”no‚”Í;k"×rK‚Ìw} _HîçˆÝ¯{Ãóðìñz^âX]ä:óÆ…í!€TëÇ@ʲ¨mÍ�Ž Ô×< µJ_£;}}m€.,JºŽ�åv¾íïà¸�¤¼€iªT1«íÞ/5æçCóÖCïlÙ¹y^)÷·SÎM×3Ç�ÒíM1¯ Ê…&Ú¸y{Ú;Té¤SùxMŸ_»3¯ñ»XJ¶”:P­Ý+1ZT+K¥X¯*~Ýël»Jã¯W °FÉühô²‡ò×£ìôÈÔŒ/ð�óûEU€_vl—eV`ï>oëuôq7›¬z±…ý6¿”û�çóóLéöm1«BFoM•%(&ŽÍ—sîs¶ò’0Í0ëü{·Æñ:/0ß6La;üCÐp‚0ÍÓû½§9¦e{¯½üqú²}[¾p¿¯ì•Rý]¶®nS¥I2e~F›(¹)=ûüñóO/OÞÞº¾‹¶<<�Ñ´ <ûàyd•§LËYk+ÝÞ,³Æóïš§6ÇèÝàØ‹‰)³*?\îs·å%ò�62^£M£m§WEyá3lt€Å´5AmÅ?\o4À†žv²0–ôøòöa½ÊQ`æ>[¾Ìó¯;t�û: h­« rZ÷v›J· ØmÞ¬õ°ûKÓ|Yó{ù4ãýì �Ÿm RÅ:ׇ§¿Íø7ЛÁíÝlz�=‡J·Ï;÷–Õ>ý¼ZÂ*û2¯ìt¯á­ýzå¥QêuŒvïçE6½ŒyZýðô`ÍŒ6uì̯ëþ~àÓŒë’îë?çe®/TA(ÃëlR°åðtÖcVY·x×*�œå­—kÖ]oY{  oRš�ñfÆ=z%ïýµ¾Ÿ(<û<ÖZû>lFd¸~�Òö{3n<óÎëåˆcª»âsÌêËû-˜$ÒÙÂñˇìg³¹W[z,îÍ9o¯{¼öf¿-§¶µŸó¹ædݬ�”¡Uðþïd�6ÊF[Ú3¦,®mÊ–ºÛÊl»¦HÁó�ÙìàÆ§%ÆètÕ¬ËÜß¹¾L¨YkýÎêc¥Û›y}™~^¥ŽÇèh=ôY×¼N^!h—Æ«kP t  ‘át�o‘u1¶õƶøÿgÙíå‰&[J­®²ÃÓÏy°r5)Èõ<Ûžµè{†ÈÄò ©4kd%Ï lD©é›%Æü¦õw²_£”{ñc*ÚÊú¿Í¼¶Ð¼c´ŸÍÏ0¿^ ä¹ñZ¼Ýg]Û'4ŸîÀê~Ìíi�µkg)ýx‰€äã¾ëg1ñ!e¶GyÍ&RÞ¥y±Àz¥ÿÎf—~ÌÄ™æ™UÊ}òýU�€*?Ý ÎÕÙìòرúsÇfeúššO:@3E@ëci™e)ÛôÙ`»ih{ô*]§wû2úkg) 3këd‹Ï»KNöèÏé‡*&|ìg»j~)÷qï**©Oùîç\k¨÷qzžÍž¼ØÊÒòÛ¢?ç½®rm�Š*Î,}?i‘¾A Í' ¹nJ]kx´¦êáéQe_›8¹¨øõ®•i�~¡úƒí¯ü÷ûl| áÝ 0FðüxÉ1Ù´ó¬{D c}mšô»@ŒÉ5‡§¯³yåù«?›®¬q{·æçØ×U�ë¡GµˆÖ”ÛÛê")ØÞtý·µV<ßΟãzøÿirác&•löœ•@š¨¯ †�Ùãµ^O#x~4á–N~ۢϿÊuÁg¨t€æéM)+Z¾{žE&q=],µÞh*¿ÞZáuG%\fU¥Ìªñ@~dä[7ÿÛ¶gèÞçïõ o“^IÏûeÆmû�Jé—'•óí̸ÇMÛt]™îHó4DúlÐ�síú¸å㵓Ÿ¿=׿�s®�çûœ¯�ÞÎfWœù´BßÍú,Sæu®5ç3�)á0×·'¯¬oUoÿÖ5px_&¶³ Ý^v5ÈÚ�Túj)ÓýdÐÏ¿g?—8ݤî  ^—´Ó²£û¯Oÿà½kÐëgó³áb\ü‘�Öy_ß慨šqûY¶ž²¹T™õo�/NEl‘üM ”·籎ë{£>#ìϸVïÏ9§6}¼îåçïóŸ«—_Çf�ïã:þ9›=kÑôýˆÏÚ£ÏË^ß˦Ð-ÃcÐæÛ×µÖÒµÁÈ·ùïÝ ^¯—¥ Ù.º(üjdW•‘±ž^÷ùàùÎkÐþwÖ%]´=âóCgØ–Ë?~ä~F[�¼hà8‹ÌúóZìIdåź½Ó3ðÚ%¯g?êã73îÑ/õõ`ób<_ϸ=Öƒ¾±Ö}¤õÐãúßiAáý-¯W3Þß›AtªP×ëÃÓƒlþÁý¹û´úñq7ãZ÷:[}"ÞÑœ×rJ¸eø¯&¨D?+ZJ=»#ðñ5¯°ºX}–­'ëwñº+<~ÕàkÊßÛ†èÚ¼ŸsûuÉëúF eo…ý�fI�¿îŒ{ìe³ìÅž^•v]âqF0ôxËÇëû9ãõãÂ¥ÜS{¬ø¹æ¢„w6«²I±ÒôÅ?ÿ¨ªcЀ2=É6W-fÝïm^úh¼~-4^ÓçŒ"çùûÒ® )¾?ão拾Äqw”ÍžLÐ_y}uØ2è@Æ¿t‹/èFòv6;“³µÂköv¨}S𸘫lv©ñ¯ùª«KÙè/¿ÅÖ_ÃûïfQ2þö²»Ò³¤Šûcû½ìjÍ—�¿„ÞsŠ(eœEÛÎ˦ÜËÇöU¡€I Hü™Í/Ãßµö9[î]6¿:G§ð5$&m¥ìõÏΡ‚èëõ2«¶RL•×…óÆëhrU‘ <Ýlñ*5½’'vΛÄr]è—>û|\ñ5açüC+‹/O§ÝÚÛþúáÿW+wÙß¡¾(Xº=‚�9÷•¼Ž`àq)_þ¦ÀâMŒ|“­–y}Ÿ¥¬ö‹RÊ¢þXÎ~ÕñÓÍ&MàH_äÏ"»kõ1v’g°Î³”ycèÓ°í�£ô<¿eQ–z±É<ÑUdŸ·VXÞb’Þ “EX—rû¸¼¾N×ó˜°õ9›=ù§•_Câ¸zŸ¿öÝ£÷÷?Îæ]“"ˆþlG«Ì¬û3Z”&ÿº¥ï0ÞÛŸsÆë¨ÏY~ ]î�×ý|¼v ¼þQ^½äeIŸ¥ºù~¶¸ÆM{/E®oý•'(nîœW;³“Ѐ²ÄçûÙC�©_Òz�Óüµ#íÚ+´Þrú¸Èµñåj{0)+³6}™ÙÍ÷%žÿ·l~fo–¥ v/Kk…ß”D™\ξ¿Âûëzüíå/¥¬ÍÍHK>góƒèÑæ�l™>ÉgÑóÛAE�½8nÎJ?�P7gkzÞÕû:á)à:/ˆ>¯W%cÙð³ƒàùz¤>=ÉŠ¬ Þœ÷v_`¼Æíoó­Œ1;ÒÎÒ�—%}öMb©â½¼lð9¯—-·f=Ì%€”åþïŸÕd;Ækœí@»Íx�àyÑ`í(½›•ÀH_"¿ËFe·S@½5å¾ë3“J©´…Êmˇ`I%Ÿ« žC]޳"Aô2Ë$]{Ÿ¾ôé¯Y±ìjãuqq=ú:Ü�U×�ÏDÕLx8¶ö9L&€”%¾€kg‹e—õzÛî]¡/6SyÌUÚ¿“=d£÷J7E3·Ë�ÖÜÝßÑñ³½R0ûyÞ¿�5¾R7“Ëîg£ ä´óhâØ:Ä«ÌIÞ—û[:^Ÿe©âL{C{ÁûV)Ÿ1Ò„‡x¾uM5ifø?M°šoO^í¶öãM˰ƒþ[é«¥€Ö6áÞÏbýïEý¼Æ÷²ZY*CzÕø²ã³ƒ«=‡ì¸½Œ2·/³ò'fô³´ží±à9;~Œ¥ £J"åê¶g‚ç•vŠóæýÖ¾¿Û˯'kx�½áx�]2¼¼åpÒû9ϯqe¾—ûüúÖu@Àt2Ðæ³^e½µ5ëôíÉ«8Œ²4þ�=dk´xìøÿƃñeE?Kë6÷â÷§ß?ôµ2[äîï±¥º×»þùH/ÛÆ,ªdñ¬×è¾.ùõcMÍ£|=Ïæ7fÏû‚¢[$+n}ý“HZ+<[œ·Þg 1F`tŒÝ¯I‡§ïóc¬³â3Þ �³j–{áçþ­‡~½Åïñ]¾,M|–y“­ö½NŒÓ‹±ñzœ¯7þø8x·– t\ãO㵯†ŸË–/÷ùõí�ëÌ'€0ß¾&€ÝñíÉ«V–‚ã/òŸ­5�KÎò׋//zYZ‹·÷ôû84×(ˆU­8vÞnakö f0�­é3Kœc=Ï‹< ªþÒd‚yå[{Ø¡ûmÑoà9¨›Eàûð4úþ·| ,r\ôóvøTjæà"Çy5Êê˦}FéÕà}T}®é¯ù‹ç?΃¯�üóòQ�¶ø4üœ°ž výí}ß 1UÅûHç˘ì˜úp�ãhs×Û$>ni‚U‘ßÝåãµ;q¼Fu’ÃÓø :š„÷;Yó{{Gc׸yÁôûGÇÞºçU_'üÛ™µù¥;ñíÉ«ól}ë8PÁÁ§ß?h¨ä|ù?­Ð\ƒså/Z¡±ÇžÏ*Ž/¶ÿ8�/×_géˈֆw§Ÿå_r Æö�Þ�9R°ô?[ö®ÒÚ΋OãÜõ±‚ýŠ/)�k��žÊØ\à\þ²â@)›íü·{ù1v÷÷¸–‰eœ{Gk?·Æ®÷ÃM‰vê÷ÙñqU±~6š8P¤2B Ìÿ6xÌË ½—ÖŒÏ<®o°tÊ €Õ�/ÐL€¯ÑÇžÏ*Ž/¶óØne©œ_‚æÓÄÝÁö»Ìt˜áðôk¶]cÏöN_ÿ™U»äL=³ÑO‹|fû—/•€IþO»äÛ“W�Áö9K§(ûܪñîîåûøu°Ï±uô [)²gRϲ~ߢÖè N̪ ž‡³á¤…‡¬ÞM�Ÿv>‰bÑàyOð˜ÆèÀÖûöäU—:YÊ8o5ômDvíõà½D€èý`ë>ýþAˆfJYÓíìaýÆVþ÷e×fìmQëhÇ·Ùìu¾×}Nú<؇n–2ÒûG1nÎòó{Ÿ„À4èÀVË—aˆÀùÞ–¼¥Ö`»lgƒ÷vñôû‡wz™FHÙʱEÐ|Z¹õXR¡[ø¹cmÕÃÓ~ÖÜ 2#Ý…×ÝLëÍ^Õ`Ÿ;Ã-ÒßW²Îmzïo²â�ókŸS  [)/w™‰­-}‹1!àjð>#ˆtòôû!ê%9ÛƒíE–ãÓDÆyŒß/Ùj™ä¿g‹—ð®£h‡“Û6Žÿëší'K�ô»¼/nJÍJOÙæ1Ž^g«­wßÛH¶<ÐèÀVùöäÕ(+³½#o¹5Ø>Þwoðóøé÷}£€�K¥ÅgeG?ÌËËXîfÍ _(_ïs¿¦ïc?ß®ò`z/ïë»B�ë0�çy‘ŸÏËz¿¿;@€YЀ­‘—k?ÛÑ·ßl_•u§&z�þHý´p‰ò¢"8{xÚËš9y&‚Ë‹·‡§‘…ý¶!ïkL›ïûøØˆÉŒÝ÷Eþs/[ßä€Qµ€©Ðh¼oO^í=ýþá^Kìôˆ`K”3Þßñ¦•u�5¦e£³9iMòn–2�o dV¯*²‹Û l±&—n/j¼Ž*~í*Ç"ÐPÿ§ æú·&¨½}M°»¾=y™�_�ƒ´³”�~¤)ؘÛËãÁÖ­4`¯—²Œ›ä]�¬ü�Yš(ÃrÞk`t€ùZšê'* ¶ÈļÒE�-ÖF×>ìš&I#رÐ=Oϳff××EoX`t q¾=yÕüø<Ø:Zc®·ƒöŠ@º¬UvÅ»íëÉBú‡§QaãL×®Dö9°t QòõΕl/&J¹Dg'¤€t·{ÚËKÎÏ–Ö=ÿ¨cWÒ´õ�f!€4Æ·'¯ÚYÊ<.n8ñ Ÿ€Ûî¢ûx²àýb†–.ÝúñÔ„:Ðßž¼êd‚ç«je)]�êž¶ Ü·=Ø®–~­ÛË~Vï,ôw ­Ç}x罎Á³’þB™þóúáðô«¦€Ý €Ô^<¿Ö¥ˆ ‚èTçðôh°Åä—Ï Üw¨Œû¾Ò—YÇ÷5l‘ûl‘Œè4áàÊZÙÉŠã÷:¿þìç€-'€ÔÚ·'¯býnÁór�‚è-MAbœµ‡?' #P|xz>Øþ“�•^/ýª) ý} Ûã$_§}ž�™Š«ê­´öy ž��Ù³G·ïª¬4‚:P[y–´àùzD`îã �èX·nö� þOeÚãøþ3K�Éñ±Øl/³ÛËã_û]V¯,ô»…ʉ§òõªD¬nùµÏ£rÂÏåóc²GTFØÏ«*üg8~Oÿ”�ÛC¨¥<�Ì÷ö†Û`~@7J‚«ºQ†Õ&N¤lòÖ£¿ždÁóiצ³Ác»ùòÍ�Æ\YÕîÖr oö=5³ 6×Võ{ÿ‹Ol¹+eâVñýÛ_ÃçÝûÁ{¹ÛP{/þ~V›äTdŸÚùg€ù¾Mó£}ù’.é5ê\@éÐæki‚ÚdÝ>�×Af½:ßž¼úòôû‡®¦`-âKèÃÓ^öc;þÿ÷…Jš¯.&ˆü¹ÁX4 kÝór\¬Ä{ýèÿ#¨ãôëýÞ5¨­Ê­Drx::¶#x÷©²Ù:ßS1Y³&u–ßV‡§÷y|;› B¦ÀéY�óôÍöò*[ǯt,öó¾øTáä€"ïç—5ö}œ«£šË›¯«íG?ã9î²4ÁïF0`÷(á0_K@u¾=yuž¥ÕºÎËæÃºŒ¯GÝÍË´w+yåôÅ÷Åßûû¹Ý”%Ù6LVYƒ«°Û?õ_šØµÈgB RûEàêó`\ÿg°Ey{Ÿ§wÇ^þ9.©n¸ÿßÚïT}bÛþïéc~,žç�åívxú6K“æÎV<'ï×d°è@mäÜ3-±1×yù|(_ÊDíçÿ×ÞÀëŸg)+¶jýüµ§+–%Ét1IaËQD†î¤‰]ÇÆÔ½nøA\S:™Ô.õÿy¥¯šÆZÑ ™o¶üXŒëÌ×üš³}brÀáiTr¹ÊÊŸÌ4ÇW;1 t òÀíG-±Q&0°n£,ðÖ†2ý^fÕ8g—nO_Ä;÷•ã¸Â2»£5ÑWîè醩â˜ÿšg‡²{Î}ÿ¹Âàã2Áðý­ .?he©:Dg ß[\K×]Åêm~Sµ `Ë  u�Û–fظ·ßž¼jkÖ$Ö`°_Wþê)¸zRá+öXwÖºçåè–¸Æïͯeâ¯ý=ª Ü銙b¬_ ³ÑÙEí¬Š C)HßYòѯw¤/®·*ˆ™áÕU·iíÐ8ØYèÀÆå[iõ!°Áz¤uÀ»Y*åþiCûÐÍ÷¡ ó²ÏÏ3ëž—á.+wbDTJ˜V© ‚çÇy¸=áq,¦3hïša'µ+(çYÈ{+ŒÍ]™ÔtµË*¤÷På¿#î×�‡2Àv@êàJÔJëÛ“Wçš�µˆ/�o/Ÿå¼óE9Ý´vòçáÚ¦e”×�èú×CïÎÌF¶îyYÒºçirFYc4úí û¹ûÅp‹±øsvk?ŸœÁâöe¢ï¬³5nW]Ë|W&uîmÉu¨Ê÷�®9l=t`£¾=y_RZG°~Þ ú¦¥بT^v¬lg)«ðs^ªuU ]×zèñ¼'3Þ—uÏËs¼–²éñœ·—1Fžçcå_ƒÿ?‹�½ÔKéX}g­'è™&'­ú¹òõŽƒÍý¼·Z¹þe\Xª`7üC›òíÉ«mÉ|ÙF£¾‘eÃf¤/ôdzSã ëV>6ßnÿká,öI"cùð4£Ÿ³ò× ?'#Úºçå8.qÝóiãäq ¤;áºu, ²’(#}3hÃþ^;ú­Ì‰4÷[ÜO³Úª½Äó­é3FÁïÖp×îT•ˆ¾x×Ð}ß/x|^d©BÌýØç�ý| ¿ÈÛbš›•>wÐ(èÀ&EÖ™ R}u¾=yuñôû‡¾¦`Zc¿Ç—Ö/óL³xÇ—ÝQ¸»RéîzžF¦x™e¤ûy¦òdÖ=/Kw#Á­òƸK�©ÒÀîÙâ}þ>å¶Ó/VÛ›š°ý×s8•ÐV©bÈU�Ïu{Ãlñ2Û?M¼ê”ôl¯ó1_·—¿,ðþÛùñÔ*ðÌqì650\äœs11ž&@Ý Û }ΈŸ¼y4–•nØ1J¸° ^h‚æÉ³Ïßh‰ÚS!€:ø4üo –�J£—S¶5?ËüRübê-‡§GŽ©RtóuìW¥Ã—+]ü>K�µç;¶îù_Ã`çäíŸ÷,¿ž�ÆóñJöht€¾=ye]fXÙçÍб:k_‘‰˜¶Ñy¡?v�‡‰R)[qô%øo¥¼~yAôÞÔ€êÏ%éYNYÁó˜ÌÙ²_óÌÙ"ãån¸›)9^o)€ýk 6™T×üþ�sóE�Gü³ä=({ �5´îócpQí¡o µa ¤?ϪX.€Ú@˜MpÖ@öyãȘeÝbÂÚçl|=òœÊ¯Iú)ÿÙ.mÊ ¢O¥IÖ=_]Œ‡“•ŸåÇÉ {ÃßO¯Ç&o°úñÁ¦—u¤á¶B‘@cy“•ÓD˜E�á»…ÇdSÏ éÚ7êÏb“¦²úØ1èÀ&t2�¤&9Ê'=@Zc¿�Ö[ÞË3†×kµ zoÆZ¾‘鬪Íj"(wPR ÝI“âºôy0ÎôSyÇSï ûÚú½¿¡W~½àýŠdÉ—³TÈæôw`Ä}�qމ Sÿló¥<Ú\û‡&6@öy³Œ¾@~§)X“ñ h|¹ÝËï�ý=ʸ�2ûùm­Ò÷$‚臧ñ[Ñrë“ïñå|³0uPNÙöÔ­ã&Æ^d'ž ^Ïù®¨|[àþÑwîßÕ ¿Ë˜”q’—/§Øºáw%½fŒ›ö‚÷~?¬�pxÚ_ðò¦ÁŸö+í‡Íè­ðØ£lTù"]÷ã¹¾ ?k8žvž t Rßž¼jgëz±n&=°>é‹êþßcmT2÷Ç/°÷ÇîÕÈF~¶¦ýéfÅ2Ñ»³.SPçJ¯¤¼àyêÛè§7½÷ºÊ3UÞX½½#8]¤¤wÕŸF�×U7cåA§À}ÿ[ñg”ññøûÂc²Š (eK“·ö ´KSÏ1qN/+ØÇr,[©þlW'„°EЀª½Ö�Ôúöä•Òº¬Ó¨¤n+KåUÛÃu©ô+Ý›D?È ,ü\8_?ëÖ•œ”<èÛûáŒÙeœ#`ö§Ò¾¥øR྿j®K“†ŠL¸»+á5‹”Y¿[¢»µŸ]SÀÿ¬À#>5þZ±†Ï½Yªžñg>¡Êg`€£„;Pµ#MÐXñ²’–¬G*�c¬��—U}ðûö©7ا´F Ú—çï~Ê>žËJ]N¸NòI ëìßóA_õòþmM¸GêÇÃÓ‹á}YV‘ë†c¦‰‚Øgû°W«Y"àýØñßì÷Í‚ŸK�†™È›[ß}ÔÎí9÷ˆÛË/Ý>rÓèñ—®ÕÝl}Ë¥åc ®÷'x€Ý €TæÛ“Wñ”/Ç›+úχ¬ÓË,3Ç£`jo¡gHYb¯Kû’;ÊÈ?ÑZbß&e1GÙvÙjËéÇAUëϦÀËó)ý;r6¸Ï‹|¿îu;jÖzñí%žï¦¤ãiÑìð» ç•ß³Å'v¾©Ág uT5ém|b@9çòã|óÎ_åm>‰áÀµ`û)áTé7MÐhʸ³^©´vÑc�ê(Ý�~_49Ï#Àðv¸viùûõ8Xþþ§/ÑÓº³�¹”È‚|^Yðüçþ�3ù‹]7k½øe¼_y�R©òÖÒ¯w{çœE¡�<Ó~ÛlÏÄÈ´äÇEÇÁG§€í'€À6hi‚ÆP¾½ùÚš€µ‹l¸šGpcÑ,¯‡àù(À±¿†ý:ÏÒºèý,]Þ=Ú‡8Ç]éÀÂFU6›á}{ýÙèwöïXÆa%ŸÓúšk't®*2Û¢ë­Ç±{3u_³·…ŸcO*Ÿ°´þóx\§ŸeÅÖ¸/þYøðôÜa °ÝÐØ-MPyæ² ¾æSE€úù9xÖHAŸ²þìMûp­3 ‹~:ȃכ—‚I1I¢;ö׋­ 2UëE�ûþ¥¹vâ˜_=ë9Ö$_|R߬ÉXE2á϶¨ºµ9ï–ïçÙèÏò±Ö_ë¼q(l7k ̦T1”§­ ô#”nrð<üwm¯™1½±}ˆ×þ˜™$TD´áû<[°ì1ç©n�½V7K�ðûû7ÖÓý2øùëÖ™ªS$k·ê‰ wÙâ%¼ç�gkï²Ö�>+x^8ŸÓ‹œ¿[ÃóK9Ùó›t²çµ´¶û»á–&\ĹèEþYvÕëõÞ°êLZ€-$€0ïÆ@Y^h‚íðíÉ«öÓïzZ‚�û9x>©rŒ¶3ÕPŠˆ¾9΃e�‰(¡ÿvÂç¹·Y*»»xð.–`Õþx[ðótÕô“-†6é¸/g™†4i©ÈÄŒN‰ïãuÅ×—²ûàd'+jŒÓ>?´³Õêñè[J w *mM°5Tç`ó~žG@àýFö%e E©ØžŽ™©Ÿ¥ÚAéÁó¨žF ý·sÎ]Ö¨¯ö=+4>Ö1©‚:÷Çùq_V¦þQ¶¹‰Î�<›¹iÞå}°ÝÁótÞYäº}7Ì¿½ŒkÒ¿†c´ø5Üä`€-&€¬Ý·'¯Z™ŠÛäWMÀFMžüpŸIY¥`=<í¬eŸÒš«ù~X/ûG4‹¬Çgk)w›²Qc<,Ò·�üþT{Œ.âw ·UâX?Î�ûnÉϽéµÈ; ì�· —›~Þ9<ýXø<c4]û]‚î@Zš`«È@gs~ÌÅ—ÞÇùm¿Îx\d'w²@]_yêÏvüü—Ö9O™�÷k£àùþ”×ß›rë9˜ÖvŒžgi ú¢º¯Öæ­·ÿwxl­³,~¬A¾ùójŒïóÊ_õöò—Gm7ŠTÕ¸^Ÿ¶ó¼³7öÙ *Ä’'K\ëO²fN� dè@Úš`« ³³‚çÉäõÏ‚ç!@ëÏO_Úw¯_ä¿Ù±ó`?K�óîÚç©_gÏO†åy‹˜(Þ­¼^äÇÙ2þ]åÛk¯.ëÅ¿©Á>ì 'I­k"Öâ×™8ÇýVàú²?œàr{y¾eç ½ìçŠñ{T�‰‰lúªÈg\ç,€-&€T៚`»DYþ§ß?ôµô8xF_|?mžW»l*Y~“ÿßdÛ�ÙÖË¢ wA¥Ôž³É™¨ÇcûpïP)ÍYŒ*[*ñ¿¯óŒæòŽ�z™·Sš¬qT“½y�Õ£jB\¿f‹O\‰ãøfËÖB¿Ê¦¾cÌD ý*ï¯/ùqz?a|µóëÊ¢þrPl/t  2–·O+“yCÕâ ÿÃÓX£ôõà÷I·Qá�á7<¼ï舒²iŸ^oɹ1Î1Ià}ÅÄ­lzçÈ„îæAöIÙ矇eüSÀ?úåËð§ èM9Yk¥‚Ù:kxΞ.]›75Ú—öð³é@tœ·O/²b•6b�ðç<îÊ“–ŒXä8ŽÏoó-÷¸Í~V¼zÆ�C`{  UØÓúJ‘‚?,~Ì"½¯UðüÇý�}y7ܲ)›Lïg)pðû'$DVd^^O¸µ“—ômÏ9Wµó­3lBwãe°i’N�ûö–|�"�Ôè×àºR´”{\{¢šÄI£GC”ÑOïcÙϱí^½¿eYü<"€ÀVøöäÕþÓï|‰Q_2з³OeÞPWãéŃç)cù¾²Läô:�ƒé‘=ÝÎê7I¥—¥Líú”þ�ÀkÊ$ŸD_´Ìs}&WìžIË0À´ós§Ày1*J¬ð:× Þ»3¬*R�LÜßöýSc—H×Ì« îÁ…ƒ`»ýŸ&`KȆØmí±ß— žÇu$Ö>ýšPªÁôÈ"¼½|9Øþ5øËó,ev³I÷ëÕÏÒ™‹¼ý~£n/ÏkhNÙËó²(ï³Ék¡ žoŽà9E)ßþ~…×¹™r¾˜æmMÎ…ý¬xP÷:¿ö5Oz¿›šÈÙS9`ûÉ@˜íWM�T48‡­ü÷(…ÛÝèÞO*UŸÊÔ·òíßùÏ~,S壟oÑNä?Ók6m]ÜT¾8>³u¦Üãý0øŸ* D¾Èÿ~bÝó�—'‚O’Î}ûÆØò�Õ8ÿžÆø\40OÎkt.,ZÊ=²¸›9™%&áž~ÉßCUb|½tPl?t€Ùd5@3¼û½Xð–×þ©4x«À#ZK¶Q꿬¡Ýâ|×)x>Þ+!H¼®÷S¤ï¢·[êø¾üš_b›5 ô.ßâ=Üšì®_ê°ßž¼Š Ú™îh¬ÞÓï4Tr¾üŸVh®Á¹ò­ÐÈãîs¶xé>6ë`pœõ4ƒk•¹sçš�ÆJ¥^G%ß‹­™Îºúa’è“—…3˜#ôD<àñubLï«’Àc2Ѐݒ‚«ãåj/Ïk+QÒ=Êç.¾Æpd žÆdÿN^ÎƯ®ûLe t`×Dð¼•ÿ~#ÀºA‹•üŽ ³ÃÓ«‚Ï}¯o€¢ÐØ{š€¹O�ÿ=Êÿ/J·+íÝoý÷9¯ °èl‹}MPkJ$êSؼŸK·³”×ÿº-�?SoÂÿw§Ü·=ØþÌ×®(�5Ѐ*Ük} —ÖÅŽŒó¢w ­©½šÈšŽŸ½Áöiø³ŠÀ}•Ò$�ÖØ_îVx�íÁc<çƒß'•m�‰±.ú‰í@ÙÐfki(Ed+·5ÃVék)‚懧½Ê^/eJ�>Otò-Ë÷!‚éÝÆÓOã¼þ[–Jâ·&Ü~1xoçK]#R©ýßæ<îjp¿YU•€� €0[K@)þ« ¶ËÓïúZ�ƪ6ØÚžñ÷Ø"Üü<©}8•À�}Íç­E~6xÌ?ïëd‰kÄÇ÷Jà(•5Ѐ*ô4�þ„õÛ÷édÍX×;öócþsoÁǼ]` ø»žçîÑýâ<ô<»½”}”J:P…¾&П°s2¶÷�µÒ#(ܟ󼓟óö²·æw´ìóG¶ú¬µÊïçÜÙùݼ=cMôß+x¯ÀŽ@Ö.Ê}{ò*‚ {Zc+ü¡ `!í ‹sáM6¹ú(@ürÎó¾Éÿ£ÃÓI÷�¬íÞeÔ绽¼¼F?ûy‰›x?'ÃÀÿáéu6ZçýÁ¯sžyZúE�÷Q†yúylX뤄;P•ž&ØwšòÛÄsa”ϲgƒ­;áö£<Óz–"“‘öó­,7ÿú�5ÿ{ágœV‚ýöò\yv jè@Ud-o‰§ß?ô´,¤=áoŸ†ÿ�Àp ¤÷'ܧÎk¡šð·ôOk¸_M¸ýËÏûs;Ì_; tè@Uzš@?ÂÎHÁäÖÇP¿‚½ùRÚ3¥µÇ'e…ÿ9ؾf?ÿãýÝ,ðÌýG¿Ì] ` ¬�À¶ø·&¨·ÈZ¶úVø¢ `!í »û!(œJµ—“m~{ùËŒçì—üÞ" Þyô·Içö8ç¿\° {?¿ÿÅàþï `SÐØ-Mн,JýÒd7šòÛ”sà¸(w>)ð|7繺ž~žðÿþk‰—+ʸwæÜ§;ØN ¬aþ¾àýÖB¨R]Л«ÿôû‡;Ís¤,ðö„[Þn‹s`?K¿Zîs³@yRн=ãÿÏK~‡½)�óÃûßÃ�n/�[€Z°:Àßž¼ji(�ìeý» =ã¶V~û´ÏïkÿîRp|Òù 7¸­+‹h2t€éZšÊõôû‡iAšáwMký ë÷–|ì´ uoMïñË„¿©04ž:P5AØfR¾u{ùnðß ¶ã,­>/#;n?Ya­òÈüþ×`ûå‡-ËžŸw=&M†je‡§ûÐdÖ@*õôû‡›oO^õ3UšÆÄ("•1ïæÛqXŽ í��ÿâ\øÇð>‹–=Ÿ Þüýó£¿Åsÿ•?æ~ðüý’ß_ð¼wÃ×~ ÂÐxèÀ&D0öL34JWÀ n/#Ø\F‡½)kÏxL/Kõ²Åzí/Û§,eÁ[ûh<t`¢¼±zstŸ~ÿÐ× Àn/»™É5À–±:P¹§ß?ŒJÓ Ê·;A:°)ƒ­£j¯÷ôû‡žf€š¸½Œãñ—ìð´=áÖX�üq‰÷_›Òê @`[ìk‚f‰’àßž¼êf‚èuw¡  †R ý±ž†X�îl‹=MÐH‚³õ&ûØ)èÀÆDz&ˆ^g'šØ%èÓÉh†j¼Ë¬Ï[Gݧß?Üi`— LgMm¨ÀÓï"x~¬%j%úDö9°sЀ�{úýÃÍàGOKÔÆE>±`§ uYè‚¶›×{úýÃ;Íì"t ž~ÿÐÏ” ß4åô€�&€ÔÆÓﺃ]-±1ÇùD€�$€ÔMd¡ßi†Êuóµèv–:[ãÛ“W{Z¡ùž~ÿ0*#n=ôêÄ„åó€�'€À6Ù×Ûáé÷еw5b¢ÂË|âÀN@j)/'.ˆ¾^4?°î9@"€ÔÖÓﺃï´ÄÚœäÙþdè@Í=ýþ!Öæîj‰ÒçÈ  L÷B@=<ýþ!J¹wµDiÏ&@A½4‚çS �‘Ñ�µÄRî3Ás€™Ð€FÉÀD¿× ‹¶:<˜M Y û;ˆ~0ØúZc®»Áö|Ðfwš`6tÊ  ÕñÅ·¾£Z}Mõ•„Ÿ¶žÖ˜ª›¥Ìsç3€Ô%€ÞÓ�ö‡&˜Ëd£fúK8Ö¨·§ß?ܶÈD¿Ð? '±f|´‘æXŒ tÊà 9¨N_è;`:%ªwºïϳ”�n ¤6ˆ¬ówš ˜ZП~ÿÐÓ�æKJ¨ŽlX}‡kÓõ5Án‹ ƒ-‚軚�[/¢ L&XN�2Ð}ÁÓP&@@¥oúŸOpœ1ÿóéùàdzÁv³Co;Þëóü½°¤:Ð×�t£  :ù„Ë&4ϽÉF�=æú™¬æ&ù¢ ?~ÛËÁ¯±>ú6Ÿƒã½E¹ö—ù9 €Ô)€ÞÍ…šè½&€Ê™¸Ò<]MÐh&ù5CLTq¬ñ“˜À4Ø"ˆ¾m�ô¨�Aó“´ÊS›úÓï"x.Û,=_ÖÁF\h‚Fq}k¾nf’Ÿs#�÷(�ÞäÉhñùû _çܤ:€’ýR·úöäÕ×Á�}]S{Hx®L$lì\y>øq¦%áXVìVso?®´Dm�£Pä¸n ~¼lGƒ­Õ€ÏÞq-yïó7ÀzÕ1€¾7øñ9D¯³ø/²^î4lô|y=øÑѵ&x¾]Ç\Ðßj‰Úée©Œµ*¬r|Gý·,Ó÷jô™;2Ì?É4¨Î/uÜ©<ˆ™•¾¤®Ÿ^–B}Mµ8_ÆýHßÓµÒÏÏ•=M±•Ç\Ò[Zcã†Ë# ޳sMAÉÇyLä�cýÅ`kWüòwùçíO®!›ñK�w./«�ô:e‚ìªÈzyï‹<¨å¹r/?OŽ2çØ ™‚»uÜÕ1[uWÄg‘Oƒ­+뜊Ž÷v–ªcýš¥É3í’ž:‚åýÁöG>®ïŒi€Íû¥);šg‚ÄÔmÝV™øRï^Кeì|9úÉúôG›Ê;}Ì�o– )ßýØgËÇP·ã>+p½}¦vÍøöîè¸�#[pß­û¾Ú7_<ŽÀT†"Xª€ DF@*R €%:ÁˆŽÀØß–7‚ÿï$“"b€îžï«š‚ä]Ëâ0ÓݧÏi ÿ#´âóÓ—ûñåÉwÿx¯›x]}÷Ïg{×3Q$ШÊç§/S‚<%ÊGñú%Ìæ£Gü‘‹äú,^ÿ‰×4ý~ïúâF´`X$Ð(ZN˜Äë×0O”w[úO_åë·xM%Ô }èçóÓ—]˜'Í_…yµy ¦ažL¿ÔúÚ$�@1>?}9ó¤ù¨ð¿ê4^ï÷®/&Þ5h‡:;•«ÍßÄkæç™×$µuŸÄë�ªt¨Ÿ:;‘çÇaž8oÁ$^o%Ò ^èlUƒ‰óïM‚D:TI€­ùüôåI˜·k2€÷m¼Îö®/n¼óP tz÷ùéËQ|9�W7°}¯£½ë‹KŸ(Ÿ:½ùüôeª4OíÚŠ”@­Ê&�@/>?}¹_>„áU�ß%%Ï_«F€rýCØ´ÏO_¦ŠóOAò|YªÆÿcs*P&èlLnÙžÎ:?�ºŠ×s-Ý ,èlDNžŒ×¾hM—~�Ö‘þXúõUþõ•5&†B ŒISj‡ø&¬·{øGÒÄæ]¼ÎLr€ç*]|ùÔÃ<…Ç{çx'ÂÅÞ?â‹×_òkêïè±Hª/í‹ß«n§è»�P�Âü »¾'Ois'2—¢”Z´w"Â¥jô÷ªÒYVL½Â -hÙ,Ì=Ò¹ S•ˆk�mÆ¡ÎÎ:³xßÿÙ;ôto´0æ–@¹Ïé´6{ Õ{¦8†Êî=£0Oš�EƒÆ®©*ý̸•’èçn�P¬i˜/x¨ xøØ¦¦³Ï¿÷:Þï'ÞE ‡{£sUaØRU�êt(÷9ýß KL t£–{Î88ÛœrMâõΆ¤á*)�®};Ôa¾¶4ñð¸}\3Š/+þ®â=þ™wØð½1-ÈÿW$€Åx#Ì+|$Ó¡ŒçtJ`}‰&¤ûê a à9Á8Ìç�ˆP�i¼ÞÆûêT(†¥ˆz¼i¦åŸÞ¨Î,H¦Ü6¶i¡õás“`Ã÷ÆQ¨{sÐÉtØýszê<‚Š¿s:%Þc盧ĹNÔ(�QßêØ8¥$ÐGÁB ´ðIÉôw<€�O »ÐÆÆ@U À¦ï�'ñåX$€{,’égOÂVŸÓ§ažÜ¢ ?[Ÿ£�{‹Ä9­I÷V‰ôøG!g\@ýº<ú3�m¯Ã<@šV’Cy3À¦üS€HkD)‘÷ßÔÕ'^B[ûîÑs9v*­ ç ´©À ­“X'¦¥ûëyü|§±ê‰H»JI û€A{“®Å‚ǹ`HÄ0?Ë«*E�M�V‘æ’òå©Í}à9̓�„€]ÉGBHœÓº'ù3þ§Dz›þ!@ÏÒ€)-x,$�� k­åáØ(À¢ýkšW~Ì óÀæ¿g´Cç¶.Q¥5àøËs÷öü\$Ò…Ò t`[º¥IªJ Р7 þLÿ@IÒ\ò|i“¶zx$k4MÒQ€mÞCöÓ·øËÁñ W“žæ1êX8ê'�ìBz€|Ìg¥{˜­LÇ¡ÍÖo,LêÂ|“öâè°NHþbGïò9çéÏOÁ±°›–O(Ý8H¤eJbydpÔ:Ž éÀðtBÀc©:‡^ìç±éi¼lJ)”:P‹q�HÊ›Hvùþ4ªÐ€š�Â×Dºy%Ü!%õT�Cï¾lP±Á³Lè@mÆák"Ýà Øµã¡Ýƒ-6 ™WÀíâ³ñ ='ƒªs؆.¨F/’:P«q˜/xœx°;šP¦{ÏÁôcï>ÐÚ¼R(€Ý«JI¼øò!¨:‡m[T£ï E$Ð�š¥�Üq~°Œ…ØÁÀvˆÊ—€Öæ•q|ó§ö™@Kö®/®D�‡Ê-ÛÓY燢;Ó…y®ãD(vOhåÁržÏ± °%C=ü‰ 5Ðè¼2Í)?Ø,À�äõÔÔ²]å+”á8ç:ŒIwHhIì}tް…Éå8ÌY‡ê�OШ/ç¾Æñž ƒ4/?ï>-Û¡4£<& ÅnH - @¿†~øGg-�uâuš+Tã1Z}·g&Ü'!¥ç�H@ÑcÒ�ò»!�´üpI Ÿ,xž`ŽÂ°«ÏŽ…h\÷9‡’!¸‚æÌ„€»,�w> ¨Â©c†¶Oh]Jž§�S`C$Žçºx_=`ã?›³iÜLšcS·ÊϲԲÝ3 ê’ÖàtGÚ" t`(R›“OÎ 60ÑtùÊYèÀP,6gk¡Isö®/f¢Ðœ?„€ïIžCãÑ� Z¶C’.?`T£ë’0þÖÈÆ$``g£wBAcœƒÞ–™°,'ÜRòÜš(Ô-}‡?ØÔÙ? t`ˆT£ëL6»à|°Û¼``FyN©ú‡–Ì„ )6Dð—ø¼Ç—AòZ’6už C$Ð�¡ê‚jt`5ªÏo7V� ТúçT(h„–ß Ù»¾�@狜<—dƒ6¥5¹Oòý�@†.U£ÌgÜ5áLѱHÜéX€¡Î)óÂe'Tn*ÞKÚ"yƒ°8]~cÃ$Ðæ™OÎ ~ M:íæ¼Û�Ý®€9¥–îÔkïúb* Íø]ÈR$Ïa8cQIô “@ø*µsÿ( ÜBûöK÷M›�€¡�‡RK÷¡ bS!h‚öí—ÏE6G‡á�E?åÎl€:À·Fñú3>hFBä‰gxv"q/› B8ŽãÇ6fS)•Ë Ø»¾¸…áʹÆ"ƒu.‰¾è—:>ª2‰áÞ; о8ÚhR'‰Wï!Ësòc‘€Á;Ï�(x t€»k郟|ŽÂü!x߀/œEIuö®/Rëï™HTí7!¦œ<—0Æ’è�#�ðc£0?;Ä¢ “„ðjºx¿<€/œEI�¦BP5è$yÜ!%Ñ?)\�:Àýº0¯  jš¾û#‘X™–÷ß:wDy'պܻ¾¸†aÉ�ó$Ï�»,º"I¢¯HàaÒ&-zœ  †êóõŒò€¥±¥6šÔ@÷ªiß>0¹cæ‘îáh¡5H ¬æÐ¹è0ˆIh_Æ"±¶WBð7©�æóI*ð^ªs³w}1†áÈÏ’�a^ôpIôI ¬näaÍ Áãâ—7!ð­ƒ �&å›�÷ŒrIžkzä5L`=vlAÛQçx?žø?žOJ|P¤½ë‹Y��­�³ë‡å7NÂ|Ý`]iüéX¡{H <Þy¼mP9½¹Áø¡0ÜI�bí]_\Å—©HTá­ C|^¤Ä¹5 `SãÐsa¸›:ÀfÇA¬Ô?Ç—N$6F+|€“D§d³åS}>ùIk�À&ä�9ÜB`sÆ)‰náªöJ6ê‰c.î%‰N‘ö®/¦Azélr€ü|HÉsÏ `ÓN�Ao'�°Yã`áj��ŽâËH$6N{9€ûI¢Sª×BP¬3ÕçƒqšŸ›ÖG0ÞJ`ó,|@�´ïi ž7'`.Ier‚öL$ŠsTŸBîê6  G:rÞB > ® i_œùÓUèæ’Ô+%jo„¡(G{×Þ“ÆåµŠS‘zÖ9 ýï$Ðú³XøÐb Ê'ÁÛ¯‘{!ÀjsIa 9Q«•{9¦ñ=™à 8÷Ø–_…à[èý’D‡Âåê»,û§E>À sÉ8N=J±w}q_.Ebçlfˆø 8‰/#‘¶5ö‚oI ô/%ç$Ñ¡\‡ÁŽîmçös<|ü$‰NIRâVÛðÝ:ÊçÒÓ°¼†¨S°M#!ø–:ÀvH¢C¹TF‹5@©RýP(AnåþB$vf¢uû`Ø<°cèÛ#‰…‰ßÇqP}¾MãÜ2€‡;ÍãVع½ë‹i|y+[w¯#ah_Þ4eí`Ç$жKÊ¢%Úöï�*¨VwjI)ö®/NâËT$¶æË¹ç¹ ËÇžY§(€:Àö¥ÒU˜°ó‰éA|éDbë^ ÀZóÈ�æ‘$µr¿†íÄzïúB¬‡á<è’ì†MZß‘@Ø�.Xü€]s÷ŽîZ�¬E�bäjè×Á‚{ß^ç¶ù4.oò‰°#6j}G`wRû=‹°›‰éÈÄt§´¤Xy* ” WE?’è}IÉó‰0 †{;°K¿ Á·$ÐvKvCñÝêò&V7Žc©Ca ’è½y+y>ñž~1ìÖ¥|K`÷Rý\`kÓ4)‹ÄΩBXß© ‰”B}ãRòüD†!Õ8bØ¥Y~–³D  qÀ,‰ÛabZ†Q¼ïí ÀÚ>èfF)$Ñ7æµäùà¤ÖíîåÀ.½‚¿“@(Ç8·lz’Ç"Q ›֗ƶ„�RH¢?š3ÏF‡< 3ÏžÛI ”å8ž œ¡?é¬H;»Ë1Î ¬gd#6%ÉIôgñÒ öáÒ†ƒgƒäh3`×TŸßA <çβƒÞ¼‚âX0xäxÊ’’ì]_̼ýR4îõeÃ�³g‡Gõ9PÂ3Èæ­»I ”郳�aã“Ó41íD¢8ÎîØÈÒ˜Šbì]_ÜÄëEPÙö#“x=Ï›É�]{-w“@(SZø8·&§¹ß À£ÇTÎC§8{×'a^�>�¿¤–í/blÒ™ç΋ Õç@Žt?ù1 t€r¥ t °™Éé(¨>/Ù!x´tº�‰gïúbæç¢Ÿ‰Fø‹íí‡Í`—.ãsÈ3ùèeK çÂ&§�{’[ìðÈqo®l„¢ä–îGa¸Õ苪s-ÛNõ9°c©ê\ëö�@(ßXb 59MÝF"Q<›O+wŠ–ªÑãõs˜Ÿ�>”öåégýYÕ9æ¾ÀŽ-6s9>ä$Ðêpž“€Àê´¯CïsÂðhûq\u" ”,Ÿ�ž铆Ìô³¥Äù‰dI¼7§MNæ½À.¤ç�.(+�@¨Ç‡<Ð>9í‚Öh5±Ù`3Žm¦t¹­{j#ÛZ"=ý,)qþZ¢‚ïŒÃ¼SÀ¶ÅgÒ•0<œ:@=º ¬39¥£ÏO_Ž„`#Î…€¤$óR"½ÖÖîéï|$Îù1›Æ�]HÉó‰0¬F .#­øàarÇ“Óú¼€�H­Ü…�ZäDzjwþ¯øÛ”PŸVð×Nçš§„ù¿âu$qÎ]ò‘e�H[6‰Ï¦3aX�:@}ŽUhƒŒƒÖhU¾o¹õ>›™?[Q�T)¯ça^•~¯’ÚÎNóß)U›¿PÕÇÙ,lýy•;¼°†ÿ€*¥óÐÓDíF(àNªÏëuæ'?}¹tȶçH®`3þ!U[´r¾:‚6îo¹?'çª\¶ç•[’’çaØ t€úí~úòDà¯6}#‘h†Í›e6Àv9ÿ؆‰£^7K  ǹ% þ» Méâ½ÍbÀæŒò‘GôLûv`K.÷®/^ ÃfI ´C%CŸ˜¦# Æ"Ñœ7B`îP!íÛ�¾¥óÎ%Ï{ �ЭܺC!h’*)€ÍJ]~ÆÂÐ;Õ€>ÝÄëùÞõÅ�Plž:@[´rg�rõ¹JåvÙµ°Y§y @â=¶ Ú·ý‘<ï™:@{N…€J»º-¶kœØŒ4vÖÁ  ?ªÏ�>½Þ»¾¸†þü¯ð@Óøe|~ßÿ© «ûálj”_¾ûߟäZ�ÚÆïì™P0 ÇB0ˆ÷Ø™Nýšåkñëÿlz^¹Î¿ǵÓ‚sO%Ù]sÊ_¿«û˜Q�7ñs}¦r  ÿ ')y~) ý’@g£JYØk.Ì,䨋ûâ×ÿ _DFÞm —Z¹O,„0ùüÆN$šwßë#÷5€^½�÷Ùa¸s¾; _7Üf¥E¬¥„ü"ù¾˜svÆ6lIúì¥f6)lÞHx¤´þ±\aüûÿ½å<†ÂÁö¤Í�aèŸ:Üâ»ÖÓÛþ?ù¬°ý¥‡Pªjï<�(„…†ÄùØÃ¹¯¥6£'B@#óÎYøš�¿¼eÎ9ÊsÌ_ò„.—ªÒ=´y(Uèøã³,6h§Å9­ÝY–?‡Â°ÕçmšÄ+%ÎÏ„båñæòqB ïw•ãD$Сþ‡ÐbÑ#�1’Ú»k‹Â�¤ŠMƒyª?¿ñ¥ ²Rðè9墵{Z¬›‰ÁQ9ë²æÖ–4.zžÖÞ‡zÆù†Çœ‹‚Àt]ŠÈß¤ÏØ ŸµrH C; ›|.Æâœt7Zîr*TÌÙç,S!››S^æê ·¢1x]>6 €Õü"͘ÄëYÚh(s¦Í›iã¦Îº_¥\NÚ¬1ŠrH C{ ïéð½Ñç§/GÂ@mòçÖg—ïÙT›�S.æ“SÑ0Æ`%�T/%2_«:ßʘs¶tNúÐ+ÒÓçíʧ¢,èÐîh9‘>¾c1„i%Émž¨�€�Ï'g¹ÅæQÐÝl¨öm¼XýÞ)UK ÌT<Š­�;SEz{N‚”<×Ò¾@è0ŒÐëü²‹‰…tnp' Ô"^Ç"ÁŽ…z™OžÅ—gæ’ƒe+À}~úRò¼n‹ä¹1ÏîÆ�Ó¥3Ògù±'6l”K†õJ *X�p¢&º&ð#éœÎa€^æ’³<—tDØðŒm¼x°'BP­”Ä|¦e{1cÏ”ÇH]u[ÏcLrá#…’@‡á=€R�sEH,†P…ø9M“бHp›, ß¹äI˜WY\ؼQd$U’Ä,wì¹Ècœ5øã¥NGÞå²I Ã0>7ù\‘tYü6UèÔà0ØÉÍýFÎé€Þç’Ó0_ÈÔÞt8´q U—’çÅ�=S#%š[:RhæÇÈËN†ýJUèiñc*ƒu�«{¡díðY€ræ‘7¹¥ûD4!•3€{ý*UIÉXÉózÆŸW�O›þî/$Ïë �>iñãyÐ2d¨RòüP(U^¬ëD‚r4lo.ùÚŠ”ϳîD‚9š¶;�œäy¤*®¶™3ЂI>Ö”úÇ 5O{”ÇÌTDøþÁ³héî†>,ªÐ)‘JbÖáh Øþ<2mÆNóHItsF€ÁÑ¥£iœâø™¶Æ ËÇÓ–:�ä®MTF¸ëá“ÚðiÅ7ZòQÚäs?hÆú,ðÀöç�’èmK�Ë„àö{¤Tá­sÏ›‡.ާ-­Eú$çY¨�:ð£Ï$Ôw–ë{#ø<ÒGSÀnæ�’èmû·P©™*àæÇ¡é=NIô·…ü•®$Ïë&�Ü÷àIg‰X†±P‚Ü Áç‘Çrìf)‰Þðœ1ŽÕŸÀßtBP¼·B0˜±èIcÑÅx˜ŠI yèX†'*6)„Ï!›�ªÐGÂæ�l”6î·Ì?… hih&ñ“(§¢Ñ”4`P­É6 =JÏ©Ãx�ìtþhv;Ò¦k t¾ˆßë´†�Ö…:ÑXÙ(¿~‰aŒeJмuf3lŒ:£~ÿYÉ9� c¸œ�lâ“&Í‘hJ—wÞAïòùÔ#‘ gÎê€2æ�óÇ&ìçq<æsiĊ y¾±ïVŠgŒë‡EòxÔ¸Cò“µ?;>?Ã&�lJª"°£¯-’Ml‹ês¶!m  ˜?²1B0l9ynŒÝß÷ë£0À£kk“@6"ïÆzæç�І‘з¼£~,l‰ÍPÎüñµHTïßB0è¹Ü¡¹\ïR§‡a€µY§Ö&�lÌÞõÅ,Xim¢Ö =;¶(U¡�„Š˜?~9çW$ª6Òbz˜òûnsêv[›€í“@6jïúâ2¾œ‰D3´ä£7yÑå�H°eú œùãIÐ^Õœ‘�ãeóÄvã l‘:°q{×γk‡–|ô)-¶YtaÛR¥Ô¾0@1Ž„ j¿ Á Y+ohš:ЭÜÛ %}R Ì®è|…Ø»¾˜]Ìj¦}˜FB°UûÖfÀ½ Ø. t γ3Ø„‰“ÿq|éD‚;GŠ’æŽ7ÂP¥'q\eÎ8¬¹œ÷{7tÑ€-’@zã<»fhF^ ;¦ Ê™;¦ä¹ Øõ Á ¨„ö=ƒjØô¬Kè›Vî&ipÛäÅçŠ]kƒ娻¾HmÜg"Q%›®‡E%4P“N€uH ½Òʽ��¦VÇl˜Ê_J�’ç‡ÂE±»NÎg†þý"à»l�:° * ê76!oÆ8 á((ÈÞõÅ4¾LEœø›TÊæKÏ' 1è@ïœgׄ_…€ 9 ’:lŒ…ŠbîhÎP›ÿ‚bíë¬ ¬CØŠ½ë‹IPIP³‘ðX¹­ãX$(ŒMPÖÜqjîhΤ"°2 t`›TÔË9èl‚ó¦)õþ60wäQœƒ@©ß¬LØš\Ip)Õ �ôF(”*t(oî8 sFŠôO!€¿¹‚¢¥M^û¬Bض#!¨Ö/BÀºò9Ó*R(ÕÈdŠóNªãôa0n†¿»‚â)êV"�lÕÞõÅ,¾LDÂ$™ÁQá‹É4°ÊÜ1u/›‰DUFB½±&�3v<%° t`œgW§‘°Ž8A9ˆ/&)˜L«R…^ >è�ŽnðxŠ;€“@¶Nz½>?}9Ö ²ŸU`æ�æŒÅÛ»¾˜ŠBÆŽoJØUèu2Èd%ym$T4™Vمػ¾¸ ’èµ1ö d§B<„:°¹ }*ÕùEXÑ+! ")y~( P”÷B`ÎP�!¨ÂèóÓ—æýÀ½$Ð�]R…^ŸNx¨|žôX$¨ŒMP�Üw&Õе ª+!¨Æ©VîÀ}$Ð��±R¥‘°‚c! B]œH�…Š¢ ½®±”#q(ݹçð#èÀ®©B¯Œš<ðs’&!"A¥lþ€²L„ *æŒÀý.Õ=«Î…¸‹:°k—ÁAµé„€HçIÙÉKµ÷¹ÏO_Ž„ʰw}1 ZãÖÄ8 €Ĺ¿$:p+ t`§ö®/RòüR$ª¢š€‡pŽ4µS…eÑÆ½? AÓ¬ Àí¦BP¥±$:p t ï„ *¿?’Ï�îD‚Ê�YE™ A5Œ¡Ú¦ÓØîæÚb_66ë5ÎkY‘@vnïú"µâ›‰„É2ÍP¹K+Þ˜7²2 tðÝêsŠz�~úòP€ t ªÐë1î’Ï�îD‚F¤]è>ÏPÇÕ3/�è†H½n§Ú¹ è@),„TDÛ0~@õ9­Q…åø]ªaÎ ÑLªçLtà t {×i€i—f=Tð7¹Êd$48y¶ e˜ A5Ì €!úCšYøh-†M(É{!¨F'ÜB¥.-Jfç @ö®/n‚�×µøI€òŒjÇ(^IÃ%�”d*Õè„€eùœè±HШWBæ�˜3ÜC½-)yž’èBÃ#�cïú" 2g"QÕ|Oõ9-ëâ„y, Pç ×AÅ08ùˆÊ‘hJêJ÷áóÓ—§BÃ"�”f*Uè„€…|&ÔX$hܱ@T÷ÕÁ¹±ÀPM… I‡ŸŸ¾ü”;0 �”æ7!¨‚Å–�}&€T…>Ø-Õ}õpn,0PA³Òs-%Ñ…Ú'�”f*Õ aAûv†B:”Azl²mŒ ¥ð S!hþÙvï‡U£CÛ$Ð�¢ì]_¤J‹!P‰|.´ C1RIEpzŒ›`³Ì½+°w}1…AÅëÏÏO_žä£ �ÆH %’@¯€]–dªÏñ™Ì¹�„lV'Õ˜ Á`¤Nu©­û�P@[$Ð�©&0q£¹}Ÿª†flìœ3Ðëð“emsXºx}ÈmÝGÂm�@J¤šêਃ:PªÿË`BèL|`§þ v)|'ufImÝÿ¯Sç¤CÙ$Ð�RM… x¿ÁðäsŸßˆ|á(€Ï:Q†hïú"uJqÜ·Y ÷gnï>(�:P*íøêì1ïD¢8“[&à©}&4Eiƒ½�W=k�¡qnqÞ Kë�çažL?Ïëƒ0HèÀmœã\¦w+þsvG:ô«``T„6@:Ç+U¤K¦3HèÀ7òùÍ#‘(Î4O´ocò]àDSe·P…NMÒæ�q˜'ÓÓ™é§ñÒƒæI °®+!hÖ+!(Ò�Uæ{×7A½Dc!à»9üÌžJuñ:Œ×'ÉtZ'�”ìŸBP´!hO®˜‹Dqfù¬óy/LÅy¿SÚ,@?FB@ÅŽ‚µ5êÖ…¯ÉôtêÄGK$Ð�’Ù½ÛçÜæ2ݛ߻¾˜Æ—™PeÑæ `¨¦Bpë>%Ï߉�Hëø§ñúS2�VH _äJÙ‘(Òä�ÿ?稕ç�@/t, vgÁFxÚ³œLOç¦�uç£Fè@ÉÛ|ÎP+Ï~üŽ�„6û|‚*èŽÔ–N ¯…€Ië�ãx¥ŠôÿJ¦S* t TCÊ÷!hʱiådøÞõÅU|¹:ß1hÕç§/;Q¨C›Òß=èç^9 Z¹3Lß'ÓOã%/@$Ð�’žÀäÊØN$Š4YóßS…^ž‘Å~ØÏT`¨ów‰¥Fí]_œ›á¶”8Œ×§x¯ûS2�]“@JåáÛ£2¶L“|¦ù:ïIÐ.Ów Ú5` œ´M+w˜ë·ÉôCE l›:Pª_„ x3!¨_ÞÉ9‰"=¶Š|"„Å›ðÀFü$U˜ ÀÃåc/´r‡ouñ:�WJ¤’Lg[$Ð�’Œ”m&Mx#EºÚÀy‘Ú¸—i,`¾·ÑÊ~(-’ééÜôT¨ 3½�@J~=Ê»5Ç"Q¤G'¿ã¤{Tý”è�É<ÚHª0€µ¼ŽeƒûÄë<^ÿÍÉô!a“$Ð�âćÝHª` _?Õçå~·.7ôg½Îâ¤äùX`íù¢ÍÖõø�¬.oˆw:<\Jž§$zJ¦Ÿ/² è@‰<àêÌk'U±\;‰"]Æï×͆¾§“ ò§D6¯€ùâØt °þ|>m¬? XÉb½3�•žÚ¼Ÿ8/�uI %úU w‡yPIyÞnøÏS…^ž.�Ó% `¾Ø8›®aïúâȽÖÖÅë8ÌÏKÿè¼tV%�”h$Å3x¯ß+!(Ò4·jÛ¤‰°I:¬Gz=T <Þs÷Sx´Q˜Ÿ—þ§ï<”:P”üð²¬|îuÏÆa¾ “òl¼Z<'ä/…¶8ûñ»8Xi›æŠ<+áØ¯æ¾¾{°›{iZƒ“D‡Íø¾Åû¡ªtî"�”f$U˜ AÕŽ… H7ùÌò>¼^ßE0_Äœ‘G�`€É’ŽD6ª‹×i¼þ›«Ò�3ù†:PçÙÕá?BP§<ìD¢H“'ÛÓ`³D£ø�ô}óÅ{B?t¨¼á^ú1Ž×GUé,“@Js UÐ6ª^*^Ëõ®ò?ßI0_dAûv臤΀í]_œ…7ß«J·iiÀ$Ð�bÄ’Å�zX ©ó;–�#‘(Òe>«¼O&Ùe«B€�e=3ëñB°y{ׯÍïa+Æa~VzºÆÂ1<è@Iþ-Õ˜ A•Tº–ëý&Ù7&ÙEOÊ€³áº.S!èm~/‰Û“ªÐS5zªJ?ÑÞ}8$Ð�’X©g >…ºäŠ�±Hi¿S—[úo½î"½1€{9ÿ¼²1®ô''Ñuˆ„íIë6©8iÑÞ½’¶I EÈíÛ%ê`p^§±kkIí8Ážú; ó€»ç‹éYiÃu=nlºØŠçæø°ãxýǨã5Ž6I ¥Ð¾½7BP—¼àøF$Š5Ùòï��Éwî&y^É€-ÈGµ¥$ú¥hÀNŒâõÑ9ém’@vN5Au~‚ꤜešì :'M¬m„)Og²w²áº.è[’’èñzœ‰»´8'ýOk;í�@J }{]fBP•­åÚú™äy‡ºÝ龫P®«ô‡l}®ŸÎDŸˆìT$Ò›!�”à•Te&õȃµN$Êü.å3ÉwA÷2í;; þFò¼>*Ы ”-'ÑÏDv® éÕ“@v*>@Òl$U Ƨ¢P­åz·ÃïqZÔô].Ó±€ñlåsF ô6éu܃�âËk‘€"tA"½ZèÀ®Y ©ËLê‘+YU ”k²ãÿþ{oA‘Fñ»Û |ÏvƳՙ ôê'!à>{דø’ÎE¿ (BÓJ¤WFØgÙUI%A]lP)×$ŸE¾ë µÉt™T¡€ñl­~èU'ŠÝ“@vAõy�TŸWÀ•âMJÛˆ’'Ð3oM‘|—ð ¤&¿ @ÙR':-Ý¡him÷<Ÿ�¾/»#�l•ä^Õ,†ÔÁY‘e+õÌqUèeêì:`€sÆQ˜ŸI}¦BP‡ÜÒ=%Òu›„2¥ñð§86>ÕÖ}7$Ð�mS}^/èupVd¹¦ùÌñM¼=Ųé ãYª˜/:ò  .ñ¾=�/?‡òŽš¾Jù”Oùˆ#¶HØšx“ï‚Å�ÚÕ”ýgE–ì}ÁßïY�D/Õ~®Ä€!ŒgÓ3Ïs¯Næ‹íûE =ióS¼^Ä_ÕèPª.^âXùƒjôí‘@¶éTª5‚*¼‚b¥ é¤ð¿£6îå²ù ÏÔîÜX¶z©¥ïL†'Þÿ'a^�> (Ò¢¥»"Ç5I }ø,„´B;¾² AÑÞ×úωhÊ¥ €ªåÍ`Z·ïP±´v¯T‰ž*Òu°„2�Ʊ·®Ok�@6*Ÿ{>‰fL… ØïZZpìD¢X©¥Yíߟ·ÞÆbíkÃ@ÅãØ4†=‰&Øp9°1¨·Iëñz´u‡R�ãü“sÑW#�lL®"Ф—·Ÿ¨e«þ ñÜŽsê­,–ÔJDzvæ‹3aß[à‡–ÚºÛ�åIáR݆¸’@6"ßxU´Eûör¿o£ ÓCÉÒÆ“I#?‹¶œåå >¨i›ÚGZ´3_w/u¥X¹­ûIp>:”¨ ósÑ=G@ØÄÀ=íBþìFnÍTŠõJŠÖL÷†¼{\'Šr©B ¦yã8¾ŒE¢ i|¨};ìŽõ7Š—º”,��>(êò)�Íù tàQ$Ï›u¥_±ß¹.Xx,Ý;?[2v~•ŒaS•˹H4Ãq_=%Ñ%Ò¡,ç’è?&�¬m)y®åG{´m.—³ÏË–6Ÿ\5ö3M¼­E; Ÿ7vyÞˆù"%‘E’Dÿ tà1Ò™ç’çmšAyò¦ƒš²5W­�»QhÑY®7ªÐ(|üú!èXÖ’Ô’w* ¬C"Š“’è'ÂðwèÀZâM5µß‹D“´ã+Wª4µøX®–Ï‚TeT®tO8 œ3êXÖ&ÇûðhéP”ãœïa‰:°2Éóæý&Åz%E›´ºù$þ\icÀÌ[\îDG(Pš7Jž78æ6e)‘þ³g ìÔX%ú·$Ð�•Hž7¯å ÚÚ¿{é{׉DÑZ¯ÆQ…^®Î™U8oÔ!¥=ÝÊèC:>.^¯ã/ÿ¯·Á&~Ø…cëK_I ’ÚïIž‚öí`„ hÓ|VxËμÍEÓ¡€RæŽæ�í²¡€^¥uÉx�Ä+U¤§„úTT`«Î%Ñç$Ð�{-�]çÆÙ> "e~GAõ¹ïN“Ø �ZÉFù^»·Jž·ë*µÙ†Á~·Çl]|îLr{÷ga¾¡è¶C=H ÷O’º0Ož›,µofA¤XªÏËÿîLò³ÚdS¶7BÀŽæ�©cÙ§ yÞ²wB0hO„Ø•½ë‹«ÜÞ=U¥íÝa·¾�N¸S¾A¦EÉóa�+÷{8 ß�B&­SÕ¢ä�o°Íñê¢c™yc»n´aj`�€AÊíÝÏr{÷T™îÙýú8ä$º:p«Ü¢#-‚Øe<�eRQê»SÕGeÓ±€mÎmº6þ€­KüsUú¿‚ªtèKÊ �ç ³ƒ#�üM¼!ž¦c�<’Ë8è4Ð,ï»Øm0}wÊ3 Î+Ùx¨¶>V=óM×�h4-�û΄€©J‡Þ¥�²†øƒK ÉçÖ¥�CÑeR}^¾Á}�&§ñåÒ[_4Ïqúž;ž„ùBšM[í»Ìã?(Ú-UéW¢1ÊE—ƒ"�|‘«þ ÎQ¢Y>ט²¾“i1r,Åw†šH¶é¦loT¡Ð×5^)qîÈ�áx+Ôd©*ýYümº&A'=x¬Ã|ìï`H ÃÀå�´{HõÀpY)ÓØw²xƒM"ÇIhÚÅm'w¹Ò½ã@ØðÜqæç�{Æ ÇÄQ_Ô,­_¤ªôx¥ªôT�>XÛiœì凕@‡Ë7;-Û‡M+æriß^¾ÉÀ~UèeSÀ&çŽ'ÁyçCd³5ÍØ»¾HÃÒ9é?çgÜLT`%©`ã|(]%Ða€–ªÎSõÀ¾ˆ Ú;çÙù‹“¥› ý»“&žA ´’uCk­@/ãÒýx¥y£�YÃï΄€Ö¤ç[¼Nâ•é)¡> Ö7à¡R>iç¡K ÃÀ,µÝSuN*hK¥ú¼|ï…À=¤¯„€5ç�OrÕ¹M×Ã¥ú€æí]_LS‹÷0¯JO¯:uÂýÆq®Ðü±Nè0ñ†–*ÑÒ9çÚî± ¢ Ìïê(X¤,]Ú©<†/´q/Û(ßS`Õñ¨ªssEsE–™£MK]s‹÷ažL?Š×•ÈÀ�šoå.��[ªø3^"Âe²Pé»SÓs_¦"Q4-xèÜ1mºN®mºÆx—ï=‚¢üS ?¹ÅûY¼žÅߦë,8/nœ·üJ CÃ>?}™Ú´§Ä¹„ß»TQPäw¶‹/#‘(Z:K;¯oig_¶ƒ|o€»Æ iÓõiž;‹¢úʧ#lI|&^Åë(Ÿ—žªÓ'Áyé°�֜ƭþpèРtÓŠWZüH‹ v sm—Ëd³KùÒæ¥o'“iò8 ÷ª›7.w+;2Õçp‹½ë‹Kç¥Ãßœ¶ÚÊ]²”8O­3:áSç7ùýMßÙ±HÏæ“Û©B/Û¸õs©Xiܹœ8O›¬<#X8S}?öÝyéÿ ÎKgØšmåþ¿Þ[¨[^OÕo‚…FEA™ÆBP¼Ô¶Ë„èv“ Ê¹ti¬p" ƒž;vùymÜÉmnÌ`5¹Ka:#ýl©8æUPÜÆ°¤Vî£ÖŠöT C¥Ò9^ig�ªV¡ú¼Ìïsúþ¾‰â©>¿{Â8 Z—•î�*t€ÁŽ5Ó‚ÖÇë›»¥6îÂP¬'ùýñ9Ƽ±ËóÆtïïD„{ÌÂ|‘Ø€ÜÁ0]Gy=ÿßy\fý�V¥‚ÏÃøÙofL)�Ðþ… ™h?],Õç|„àÞÉáe|^Í<§Švì³ Ðü¼ñ Ï÷E„¼U}ýHë%ñ%­™å±Ú"™­9ŽŸóI+ãJ t(Tn³ºXü‰à<»2¿ëã áXíÛ§Sa(VÚ <ÖM ©±d$Íyœ©±ô/'Ó3w’×þÇÆp4&}®SñF]p%С KIs»ÐØ´I>£˜ò¼‚âM}~¯ è5Üs&ÂPõœq¯_ƒöìl†�Ö°e9™žZ]Ÿé>Kcãgú] k©è°c’ælA�9û¼Ìïÿ(è0QÕç+LS«¦<ñ£L£tï‰ïÕT(ª/va^•ôk7ªPb“&Æ<Ð/BÐ�œh*Q}º:lX®H×/ùÕC�]RQPö ‚òM„`-é,Ísa0‘ªó*ÎgÒ<æ� þ™ÓÊc²Hv?Ä?oùÿvA¢ƒò\í]_œ ”/ox›¤k© ç�1&…«¾ ]Ö”«Ê‹)¿„¯­ö S­§‹½¤{ÇX$Šw©BwýØÅë4hõXúDæHåð�._›ì’cÓ ”Ç&:¨P^§J›àΜ—Nª.Þ�@‡l©�ÞÂr¥ÁOKÿÛH´¨„E‘r AÞ ÁÚº›ø\½ 6ŠÔp/:”³|¾*P1ç¥S�ƒš‹7$Ðy¨Qü ÿ¿-ý·nò�ÿA/o Üê­ÊÙ2åêó7"Q¼Y:Ë[å�‰[ñÞÄ{Ò™*tÎ7Ì�Úâ¼t •ÖÁ«-Þ�@§Ô/•›;¬oæ­|(ÓAÐV©ªÏ?y»Š·©gzñc®tOš Âkç ]ÎK§@i3ÇI�ñxïšcQ¤lέÃD6⽸'E¸Ôa †#u'�Wê:÷süí³0/¸²f̶uùˆ�êH ´Å¢HÁò`¡‰âMlBÙØdmbrVÅDf, д4{- 0L©K`¼ÒYÔÿŠ¿}ް]Ug*�Ћ" l†ªéÍ2)+ß+!€¦éR|‘ÎK�WZCNÉôôz%*ôl”�¨Š:@;,Š,FÁYÐ5¸ÒÅaãœ'_ÇDÆý Ú”º+] �´/mIëÈ©s`¼R{÷Ôæ]‹wú4®í/,�ІK‹"ÅSáYÉÞÍOÈfñe*ÅÓ!Ú“ÆaGÂÀ<‚²|~úÒ¦`còyéË-Þ­3³iÕ­�K ÔOëöò'¶]¨p—Ý@¿K&ý°1¡|5¶Ó~H—2h—M @/r‹÷”DOUéoÃ|CÛN@\¥½ê¥0ÛRæÔj"½´xŸ‹_fÎI žÒjú\B8‚fB°µAUH-D½ X»òŽ¥u;°eΧî“.‘Η”6î“'*�KiÙþ\Ú—_Êjº‰H„ *Z¼ùx03Å_ÀÎI¤s!ªÐ%Ðb1)‡½Ïc¸ª{s³=3!á…@§¶øZòA"Ý{ ·}á$%Ðâ8­í�iÜÍþÑAþD"„Ÿ…`먲h&Íò½Ì} Ú7³ï9[ßÀƒÔDúÓ´Üjf!"$謑I‘XTŸÇ°È×Õ•0ì„6îîeÀã•j®Sa`Ãö„ Iƒ‘]ªóâÏ’mÿÆ é}Ð%ÐÚR* $Ïc:‚0fBÐíôÝã€/“<F£vŽü6iéÞ»ƒ–ON å…À¤H@7ûGCR}ÅÌ5ÖÜ h–¬*Ž`’ïuªÐ`7Ê8Ñ20uoôÒÒ}&Ý’@à«Jòè¹Ä^X*3ãо½MC1œlݱä90Vù=è8éØÚ«½›ý£½VON`÷VíøBO~ÈOòÇ !\›|l–… 1 µã°ǵ[À‡¾`,òûÐey/‰.5[….�°[«ä¹¤^\ªÏã�¤mw ´ÈW" !€­�<¾ä@€1©ïE’èžg[#�°;’çÁÕêó©H„¹Þ$hÛö³„0­÷>`s$Ï> ‰Þ%-ÜøÈ©äyxS!ã*_oï„¡éAPYà°‰tހ͑<øŒúžt)Ýø¾Õ“@Ø “"ÁÝì•Õq'"Æ…„  =†ÃzŒéß ´$¿/�&�&{¡€˜éôå<ï™×=¶iŸ{cœÁÍKa€µYmïå]ànJ+÷…0„gt–v“"ÝP}‡ªæ êB÷H÷@“Uò|.wS·j´:#�°?Ð%Ïûp³4Í‘ˆqí¹î±à!†½z/7N,Éók¡¸ŸºÑ~èÁÝì -ž—:ÀæiÇו—qü(!? ‘áLàÁJÒü©ä9À£\¤åü;¬•:Àf©(èL]w aÌ„ $ b˜ä{â¡0À½]×qâB(®¶r?‰Ð&-ž”:ÀæHž÷IÅeW&%Ú%«‡£Ð‘îùŽZljÞuÖ v~]ˆDX“OJ`3V’ç¹Ù?*óA$°—vÜ�O™P¾‰†V÷ª€Íò{ÎsÉs` tÆxŸn†¬•:ÀúIž÷Kõy‹| JÀø°/„¾ê4¿Ÿ °&{BðžYÒÍ0ªoZ<) t€õÒŽ¯S7ûGep:‰0TŸW!Yˆôvè>VƆ¥êüR(6C7ÃКìª"�°>Úñõí¥„b‚²ªÐãС>¶HËÖ&s‰ì! ˆ_…€uù�¬Åñ“·¯gÂЧZ}~"aÌ,déF™l~•´'Œà0ß+O]{ð�yZVž{6�½¶¡yÌ6ä�r|wkœ]žS”çV~fÍE‰ÈÊ¢Åü;ÖBàqÊK汊‚î& ¼H´oïgàó.|fIˆöê÷t.�.ó{Ì©0Ђ<®.ã´ù˜|æ)ó^gùŸ[¤åœÊ¥`vUÓð(Z¸<\Ù›W;¾qК8Ž…ÓÝÑÆ=�:»ÕkÉsv®TœçãÏ´œ×šÜáÿ2©ÿìŸ%é^;2B4ë àaæI;¾± 6¦wdІE]YM_ʽÖÀ½}{åžiKFª,°.Éók¡`×ê|ÖOÛ¥e"ý¤nÕeŒG$ó¤*šI‹'%�pÚñ�Ë !e¨°e�:F¦<ûN-° �LžßVé?åß‹úœ³HŒüNã™´xRZ¸Ü]™ y.y>ªÇ�$cî5èÉ÷N{�0&%¡p,y@ Ö˜<¿mÈÇï:þA}'ó^Æ£I Ü�ýÎÇÉ~¾î�ð)eböi#^ -ØPòü¶³üg”Dú�hÓ8Uè<š:À×ÍÒ2yîÁ;®AÇ$¨¢¸¿¡vð€^Íóñ­1"°KÞ¹ùà÷0M›Mž¯”äù›úçtKàóV-Ûµã§3!x°B@§.óøð™1"­Øbò|eµ7ú+ѧQ¿ �%�ði¥’à©–í£x”�ÀT$lZ;y@OÊâêSa ;Hžßö2ÿù?ù€I |ìâÉÛ×%y¾ŠÑz)�¦“½™#´'Ц'ÏW¦u_t÷  +èÿZ¤eÕù¹PŒÞ‰<Ú¡Iz'x¶1BBíÉÏ£ó´ûäùíûÄÏHà�-ž”:ÀÒeZ&ϯ…bô�i²Â`ʽTG<Û`�jÛôÖ:~•$úŸùÜ,ºîkÑâII nÎ)=+ûØåã�p�´X'=èîÙv³4v¡&ϧ�ž^YhV*Ñß;ö�ðXèÀ˜­ªÎçBA„&û¬Ó^íìÝ<Û’E·ì@ãÉóÛÏÉ7Æ�ì˜N<š:0F‹¤êœOS) °~’ ôfª €m ’<¿í§zÎ!I csñäíëoU�ó‰�È�?‘X»Iíð=±@ €­˜<_) ÎJ5úžo‘-„€Ç’@Æbž�’8? >ã…lŒôF:°kîA#8y¾2¤eKw-µÙÖ5ãÞϢœ’@Æpó}þäíëÒ²}!|áÅj*3ÔNÐUèÀ.M„ o$ÏWJòü�Îdlñ÷F,ÿ×âII =»ÈÇÓ'o__ _aâ `ótú 7ªÐØˆŽ’ç+¥�û/ù¿ëܷˆ}/¬ƒ:У’0ÿ»]{>Þ _�”x+`6O’€½Ö©Ãäùmgù¿ïû¢³AæyY t 'ó|”Víϵkç^¦å*X6OÇzsh›zeñ#ìäºë9yþϳ3¿Û� =·<»â™·xRè@ù8®ûœÏ…ƒ{ÒR`{UÐ! ÄèÕD`{F’<¿})Iô—¾yÖHõ9k#�DVÚ³—Äyi×>00™šت’<7A@oUèÞ ^ªÂÙ«DÐUèÜ[ý$ÏѽÉñ8 È6�¬•:�Ä9› BÀ=ÖI€;“<ÿôX1Ç¥T£O„‚{\Kå÷2‰�®[=1 t e‹$qÎf^ªòÇ ;3É÷âCa ÃçÛTøÉó/*óv¿×}áá.,Ò�«ÙœÏ|7@ƒù¸°¿9dÿ]€6îÅWÂ@gÊ®±,° ß AL’çw²—�W9V?äÏã'o_/„„Ï\OeÁÅT$ÂR�pó|<Ï/DßJž³Á—ª‰—*€&Ø+€©B¶eOâ‘<¿ÿ¸1©FçË^ Ahµzbè@ fùxúäíëgùP‰Æ¦©>hÇ ! CeÿV‰-Þ#yþ`«jô7µÚV×TYX1ˆDh*Ð>°HËýÍÿ÷ÉÛ×¥ ϵ�°…—ªòÂ= €fLkgèIy¶©”à’çk1¤e5ú¹…jÔ¹{ŸÇ×ìèèÀ¶• óU›öó|¼¶¨LbyÁh‹/=:1¹OTzÂHžod ù»-ÁFï—dž7¼'o_Ï[=7 t`iYm^’æÏµig‡´ hÏ¡*Ï6Uèôð;Aò|c&ù(-ÝÑÕl”×UÙ÷Ü"¯øš.®”@6i–Þ¯6_ ;°x¡h�½R…0b’ç[q˜�?µuÝue¡Moë+�lâ¦wœþÝÛ\µ9­8÷hØ"‹ÄFJò|ëJ[÷?kÜé÷º:t]uEÅ�î4-[´?ÍÇÌÞæ4ör5$m}Z¶g¢€N�i- 0.’ç»W–¸çøÿiô.¯«×Uwþhùäþãû¨$ÍÎÇ•Öìp&!îÕ3a Ógܱ0ôOò¼ “´Ü}ž?/ž¼}=’ð×UIž¿IËEôcÑòÉ©@îãÃJóKÉs¼`•—æA$š7©íØ 7SUèý“åÅ`ž– ó¹=Ìéøe«L6žˆ@8¥BïÂ; ìÄ|Mÿžß mãÞr½î…ÉõÝvÈÇÉ"ÑÈÊdý â  ’çáMÊ÷—¿Ç²ÀíÇ|ÌŒCw~M�'Ûq{6BÆq#*í0V sæŒEÄì @HeÔ©0Àvå±‚ÊØv¿›2Ž»*Giž–“õ*Óc:K& ¡úNàÓ$Ï»²WŸÏgù{�åÏŸ-vÛúõ4Ôëi"£ßhálÕu=Jû‹¹½Õ9Õçq­ªÐ-üø@çå{dYìð{2É‘*t"±(>Aò¼ï±h�.Ò¿U鯥›»–&õZDcTB¼K CL塽ª*/ókÉrøh 3 €°ÊdíË|œ ÀÇÊDn~ç}ž–Itâ)­bŸ @<’ç£1©ÏëWù;/]€~µWúZ¯£ßRõ?�Qú-ÂIJ CÛJRü]½¡¬’æ×V½ÁW½€.îåçÂðiµý"Ù'2¢ƒ’€1K¾w&Éó1*ßûaÝ+}•L¿–_C¥kè £6�p’è°[«¤xùüãÖ_/òCx!<ð ±ÁK@&’ _u™–“�Ú,ÇS>xÆ‘Ç&Iò|ìÊûÖ4-[¼—yü¿“éi¹•ª‚·Ï_;“·²H~""DÙÊH6ãö  $Äÿúàï«"‡Í±÷9@?$¾ ¶rÿ1©B�ÈB1€ jòüM²`�ý“L¯¿‘’L/]d¯Æý�ŸUµù�Ÿ ·Ì£œ¨:wµª’‹UEø]/rãÐÆËÙ¤¾œЇ’\¢¬NØUèqY(Êu1äãû´LœO|õÜÁ<ʉJ ÐË fÕ: €¾ e⪗I€ ¹ð.ÖY~ÎÍlóÐÉsÖh’Þo÷^>æiÙõ·tÁ ‘T¯×D9¾KËĹÖìÜ×"ÒÜŽ:½x)Ý*­‰�…àÓJûÞ’„M’èíձ̹P´Aòœ-Ò­6ç5©¾ÚF÷·úYþzë[ÇÖßÿªºü¿i¹`ð•±óH'+�@›òRw"Ýšæ{ý…ý¾Hz\'ù9w© `÷$ÏÙ¡UE÷ðÁo²|¬ê©~þUÿ÷í¿W“ôo»õoný¹~÷lد‘NV€zÁè^Y(u* Ÿ¦ =4Uè´jÆDòœÆß÷f‚�W®"�ïÿøÊèÀ™toZ;ŽðyBV©BŸÀnÔ±†ä9Àf\E;áÿ ½ûInã8ÜIef§ò&Ð L�ÀÐBkS¥<�¨�¶¢¶ÞP:¡¸D¯½|3'0²QyþNð}hÎÀ¤$R¦gÝ3ÏS…(±#hôô¿·ßntJŸàÌÂͱC ×:;€{ÔW]Ì•D±ýœ�Á{ xй_J{Ãè”î¹"ÐæðYèåšÉBØ‹óps4í“�}ùôàÙ4¸÷`L&õÉ#Ü£ÎB¿PÅ’…УÕü"žru¤$:3_ÍQ®J{Óè”L&"Àø,löNK:@Oê£ÛÍ/ºõK‰oZ€R'9“Õ3%0:“úîñÝŸ?/V,”D±Î°%ÇŽ§‰Ùçî=èÎÕjnRäéXè”Êa}÷sz¹Žl¶$ø›æ…"èÔ¼Ô7.€@qê#¶ÜO0^ÓU_ Ûàd¡Ïf1€­æq]É€n½/õ�  P"Gl [`3YèåšÊBèÔŠ S—ßýùóe©o^€ š0ûôàÙD1ÜOzñd¡tljVÝzWò›@ (Ÿ<›ÙçTl¨ØLz¹¦õð�¹÷T)0pEЙ«Õë¢ä €@ida°³ÐmªøYèÅ;S�˜(€ÎÌWó�«’?€:Ũ³/LpX‹ÁóŰ‘,ôrMêS¸h×¥"èÌ»Ò?€:%qT/_z®¾­ÎB_*‰b9…  }WŠ «ùGñstŠðéÁ³ÃÕS%ÀdælGº¾€2кñnB€RÈ>à>2ó6øîÏŸçAzÑ}ݧÏð…*‚Æ~S­[Ô§_O€ì}zðl²úc¦$¸GÌÌ›*€�d¡Ü×­^'ŠøÂ¡"hæ»?¾Žq0߸‡:%�}À&²Ð6�…^þ¼H:@«æŠ 5Ë¡dŸGèd­^ š) 6˜®ú 8›ÉB/WœÉBhÏ;E`žqtrˆdY° '–l ½ü¾N:@k}bìm,HwYÏ3C€Ü=WliöéÁ³‰bØH° \1xîÚúªk0oƒ�e©^í  �­OžÍVL”;�…°�,ôâ�Ø0F\�ÃXúÄ«ÕÇJ ±Å�î>_@ g2+ØÕÌѶ[‘…n®@¸¢/B•‰€yÅ5t²ôéÁ³i�}ÀîbðüD1|›,ôâ¹¶ Ý~1?¼P;¹böy$€@®dTÐÔsE°YèæLÜxl.ØÅË¡~0t²óéÁ³x×ÚTIÐÐdÕ—ÌÀ·ÉB/Þ¬ž;ÐN¿ïC�Aô+¥°Ñ›U»9ع„:9z¡H$+`;ïAÑÎŒÚD´ë»?¾\ýq¬$¾i¹z½ò@ +õ=~3%@¢˜…>U ̓L»’Mõw0î1¯"hßwþ||,1@ ^Ö§v –:¹™)Z" `ƒzáKºþ€ÏûǘY9W_YÔ�M€l|zðì 8¾€öLÝ °•$�…^v7U íúîÏŸãQîJà/qÎ0Šk.ÐÈÉlõ:P ´ÈÆ,€ d¡‚»ÐºE—ŠàÚ›ÕÜa9†*€@N9hÛìÓƒgŰ‘,ô²®ú»™b EÿTð×&³ÇA`Q_o1 èd¡^ì™( :`ƒÀ²ÐÁ]è´É58ðy)ˆŒÙhŽn_@ ‚t%f¡»"`3Yèe›ÈBèFD?ÖO#5š£Û×ÐØ»OžMƒÝít'ÏOÀ·ÉBYèÝõ“1=f¢ ¢c2ª£Û×ÐÈ�Eºö\lEzÙbú©bè† :02±­{:Æ.€À^}zðl²úcª$è˜cm¶ }^¸º Ó¾R‹§õü`tÐØ7Ùçèsò" ½l®.�©¯Å£g‚èÀÄ{Ïcýðÿðý°ÇI^\Ø™)‰¢ÅAÔ/«×¥¢`b›õ£v«h1 }:æ À6b–ɪ½ŒYè6•+f¡¿kÆ@Oýå媭�Aô�õ|`(â½ç§c.töIVDÙ^®Ro#sQ,�”+ƒŠ`£8Î{¡¿+Ö: ýTQt§¢?¬çˆ‡J€eé½ç·9€½¨³Ï_(‰b½3f¢;�(]lÏž:ÅH€ý9 ²YJH½Q ŒY}ø\IË.€í¸ ½lq¾å~€~æˆë úBi{Y'ŽŒž:ûb!§\v!µ÷Š X3y›ÉB×ç1j6¼Cƒ~sõŠAô¹Ò ôfÕ†i¿jèôîÓƒg³Õ%Q,‹¨þÊB_*‰bÍÀVd¡—Ïæešpå 4Ÿ+'÷e™¯Ú®SÅpC€}x®еtŒ|Ɔ’rÅ{aeVlPg¡Ï•DÑd¡ôßž®þ8V@.ë�?Ü"€@¯>=x6]ý1UÅ,„ÏÍA±bð|¦ŒGâ\ô«> ùQp’ �¯˜(õX1|M€¾½PE›+¸!+OŸ0’þn©¿+Þ´ÞÌ @¿}h NÅ ºÓü€ÜÄ1þãzm‹/ ЛúØÀ#%Q¬¹Üé½"(ÖdÕ7ÍÀVÜåZ>w¡Ã0¹–(sõF´˜á9W@&âïSk½÷@ OlÊ&HwXM6¡ÚµK™d¡l×ßžn®$Š& †éPÑ�^Õw ¿TÀžÅ ùãú„ î!€@/>=xæ®Ù²-ë !p7wÖëP0`k²ÐËgS3À}÷çÏoƒ{Ñ�ý<ß’:}9QE„o›+‚¢ &lAú Ä,t×jì·?��«‡«×Bi=<ß�:}qDnÙæŠîWßå9)W &LÀVd¡—ïLì¹zÅ{ÑéôAð|GètîÓƒg³ÕJ¢Xó:8|›“Ê& ` ²ÐaRÏÑØ¿º>Ò}©4€Žž7 €@%Êö^ÀfõDÄd¤\3Yè[“…nŽ@»sÉD«4€–-ƒày#ètª¾_o¢$Êd­X Å[“…^¶™"ØLú ÈBç[óø©R€ÞûÖx¤{<Îýi¨²ER]oÎëÓƒg®ØŽ,ôò�é÷òòÝŸ?Ç9åÃzn ÐT š?v-gsèt¦Þµ>UE›+Ø^=1±ÐQ®D˜)€­ú¼¥±â ú½Å�ß¼rõŠ™è²Ñ�&æ«6ä‘àytºô\?Ø2ЂÝÉÈ+›“Sôy£ê÷d¡äéV6ú\iÛŽÏWmDZbH'€@'>=x6 ²øJçøvh ÎÈ[(‰b¹`·>o®$Š& Ê÷oE0è¾öª†=^½–J¸GL‚:^µ§Š¢ètåµ"(ÚåjÀu© ±÷Š h²Ð¶' }ýž,t(ÚD ßwþ¼X½êw�;,Cußù\Q´G€ÖÕ‹/GJ¢h²Ï!A=iqB¹W}ÙT1lÕç-ƒ,ôÒÅùÛ™b(¢ß= Õ±î ¥ÔmÁ#‰Pí@  ñ@ åŠA¿ ÅÉlD)›“T¶'®|³ú.27¯­^ñH÷§Á±î0ê1xl âUŠ¢}ètá¹"(Ú…�´b®Š6HØŽ,ôÁ°y  ¬þ÷âÖ±îÖq`<âóþØ}çÝ@ UŸ<›÷o•N´ &8Í¡l Æ�c"  Ì¹çi¨ŽuŸ+ ¼E|ÞWÏýBQtK€¶ 6>«ƒ~@;Þ+‚¢ $lIºù{퇯V¯ãà~t²—Žlï�:­ùôàÙ4È>/�`´(©ÜIWº™"Øš,ôô{õ¼ŽqS Ü9èú~ôøZ(„ËÕëÑêÙ~«(ú#€@›d+”-îVž+hÝ;EP´Ÿ<;P ›ÉB7¯ ›>yQÒcVúR‰@±Þ¬žå<¿Tý@ Ÿ<; v©—N�º1WE‹Áó™bØš,ôòMe¡ CL”X½â±îéP–uÖù©¢ØtÚòBo® }õÝTž/}ÀXú½¥~od¡ «H‡rÈ:Ï€:É>=x6 ²óJwQ/vÝpÂCÙ&«¾N? ßYèPÐóªØ–@:dm±z=”užtÚ0SÅ³È ªw Û9\¶çŠ`§~o¡$Š' `¸}õí@º¹*ì×rõzºz&Kpʇ:mT(|�¶œ-tÎF•²MëWØŽ»Ð‡Ñ÷MÀpÕ�ôG«ÿú8Øüû3ÇãÚ/E^ÐHòéÁ³£Õ%Q4A=èA\˜Xýq¥$Šæ.t€íû½E°?²ÐFÒoÇì×Õ�Áô¹�ÎÅ€ùõqí«—µ¢  �êGEP<#ð¼±�#E°Yèås ÀˆÄkXV¯x¬ûú؃v-V¯xTûSǵçM€T‚ e›Ûå½râCÙ&Ÿ<;T ”»/i�,ôÁp Àøúðe�û¯àžthÃ2T�óÇ®Ò,ƒ:»<ÜŸ©�o?PẼÕ»‹ÝkU6ÇÌ)†Ê†:º" ]ß@ÙóØÛ÷¤Ï•ì<>^=CÎËò÷Œ*yw’¾#à.²ðÊï·²ƒúgãJÙ~P�ýWdÍœ�NÈB'°ŒÏ÷Џ«O¯�w�Yé/�aãüj8Ÿ+Žò ãûR"”M6ìAH0¾*×T4fÓVÞlpÀ¸ýkNš¾5§½Z½ÞÆÀ`�•wÍ{ÎàïU(òµTƒ³plì•@BÁdá™÷ ul èJ=ît…IÙþ­ ëñ©Mì­�¿••î®tÆ>ŸŠ÷›?8†,èqÇR¤ÍÙoŠ`pï`¿ãßy¬*™Êfõ~iÞ—õ÷£M¢kñ¨×+ÅP,›ÇÀ3 ßK^ݺ+=f¦¿1ög$æ«W š?6§–¿gô^ìDΗï`XdŸC^*ÆØ)s>Æ©ÞDc'Œ Ï_½Në#Þc@ým°‰ŽaY�kÿO`X½œ¼0@9Ðßû:òl<üƒ',ÇŠö¯c $06ïA–~QôÔ÷Åô…’€ñÌ{W¯—«W<âýi¨²uÓ)UÇ>­ï7?­O×f ²  × ˆ&Qù±¨ |‹+Êó²ÎþòŸwÄ1®:oÞ—Ÿ¥;úèÙS}_‘|g¤Î.êl]ÁtŠš/…*Nö°>¦Ýé]#ñ÷ÌÞ�`mf ƒ…`†²Ìµë�¥ÇÁ¢AiÒ¸¾À÷ÁˆÕ™:Çú¾âüGÐâxàv0}}ÌûRÉ�‘yø<Û\ý™¬èõ}¬o}-ÙpÄ/°©Ý–EVР/NLdÙ–Æ‚ z9Žikeü h›ÏøÀ†HöÕèûÊqe#.]Ž êcÞ×w¦Ç¹ÂBɰqn×O×w››+�Xnè±±Œ�£ŒŽý;®74ll/‚…¯Ü žC ¡ $çÏ©YíÔù¸qz®$öêÒø€ ú¾‡ú¾bæ|ÐËø ÎâQÙ«ÿù¯ºŠó†¥Ò¡#·ƒæ1ã|nÓ<Ñßr|SŸ<;Xýq¶zÍ|Eû™ÙY ìØn®þø¸z(�켬ƒ@íilGÏW¯#¥aœ<’:ºúãµ’è�ÍuäÖÄ5 %‘¥7ñØRÅ0ºg2Îï§J¢«çôoJ�´M‡uÛô£6ŠD1hþKüS°œûdݱ®Ä8yŠ‹)2ýX„*Ðb÷7Ðtûaõš(�,ÌCµØµTPd›èçÆÁÙ¸¾¯×ñm�Õ÷i=ï›*�Î-ƒÓÆÈ{>q¦-Ð÷‘Åóxlp+Š:#žGÄׯl1Šs _ŒmØVök�…3[½^A™®Äã�E ¥vûD›½×ÁàºM_*(¾=5Σ]}¿z½µ+½—:?­ë»ÚuU�,QJ[px«-°‘¬—ußçøÒq?‡§A½¨çvõ¼>R h»ÔùÌ"TYæ I£4QÔδU8©¾ïW¯Ãúkw›]Õ¯ÿÔ ÇB±µÙ±�>ªÛ샺ݶÖn{ýê]”‚æ0ø…€õ1u‡J¤3˺��cå “ì½×ùøúw¨6�¨û[Õßå­ñÁU°XÄpÚ‚ïë¶@;О«psÿüºï3§àö³÷QIÃÕ,pw[¶>òý‡z 1Q*ƒ¶¨çAâ^´ÂÑ.PÞB@\Ø% ØöäqZx.ÃM ñú ü5ÖŒãÌÿ)‰bÄëæŠ¶jÛ¦õ=xö!¸V¥Wßýùó¿4në&¡ ¤¯ƒê“ S=G—á&`~é”-úðEÀ-ï‚z)ßÐP}Y|]¬ÿY�©¾ªÿûÖ—­Þ�E¸¹^f)»œ}‘�Àgd¡g/fa>R Ð[›¸¬Oê×÷¡ ªO•ÎÎÖG°/W¯ÿ†*hîj5²"€/‡*Ht¨(²O�ôç»?ŽAßÅ]ÿîVp}ýç?oµ�Ó·Së@ùÿ­ÿ·ŒrJ!€¯ÔA¡³Õk¦4²�š>®ƒy@9íé:À¾²G·í·ÿyηþûoõŸë@yt©}bиWøyª#ÝÝÜ¿ŒŠ�ó÷²7a4íî$T§€Üv×?»Ëwü³u&ø¦¶æò‹æhuFI€­ÔÁôIøú˜bÚƒXÿ©ÿ¼4rò7E�‘'¯&«ÿœ|ñO—áן– ‡;êËôÖÿº\Õ“+…rg9®þóà‹ª¼ímnmó¯?- ê`LAËå¬/i·lc}¨\Q×áÎz3 7kûÃ]Óÿ|Îvµúœ—Ú m Qô»Çm*£Ãzòêãê?¿œè-Vïý±ï'ãÏØuÇøäÕQÝÿP¿ÇMï3¾Ÿ8pùÏêu1êE™jóGø:€þfU.§ 6»ÐÏ‚\÷íC·6=¿_„»þÒ~_Ø{}껼ڭËÓºþ¾~6§[½ç؇Uíí¢×‰RÎýÖvïý�;þÍñê½Í÷üÞ¦ ÿæp š÷WÍÊäîñpü9�{m#Òú·ÍíYóº–ƒÍc÷îÇ9ýÍ;šW9Ìqú}~õs·ÇB»Ž)–õë·ºÍ[ô>.Êw\¿{}|òêÿÝñO竟s¬o]�§·Ö!¶ã/®¿¯ª/zmGòíwò#æ9¹ê¹Þ¤Öõþç�ûXC¼¯Í™†›µÚMßÁå­~~QÄÜñæ3~ë3l|†«ÏzyëùXàûNi+–·Ê¢„~±é<¾Ì~xHëÚéßA»ó×ô²Í"¦û�L¾êÙêõºãFãËFü·½LJ7wJ_š^ÿ»ýv¬}}?ëŽeßOÊgü[åëÂóÕë¨AÃ7½U—ÎW?ëbõç/{JìÇÉ=åËötÏïí,l·˜ÖV»÷[G»îÛ‡nmz~cÛü±áÏ~\·i»Hù}]Úö³ô]^© ØÆþXÿÙÄaý:º~ªg®¯67¯~k7÷½ïØ6ﻯjZu¢¦ýU›eRµ-O^=.dl¸M{ö±à:ñf‹qT÷ãœîÞ{[ßU“ß5Äçwøý\5¦˜ÕcЦõ~R¿¦·~nÿÕ¦èå æýûícgõBÞÓÑ®3|»Oê1íóÐ<·®¿Gõϼº56¾(~~=¼1âÇQ–Wûuýóy`ÿ‰,ݯ!ܬÙÞŒ^ŒîÄÔªîÌêº3IX;ˆ?+–Ý»PmîºÊè3NëÏ7 Í_ë@óíñ̲~>Þµé¼Ý¶"Ô/6m‡Kí‡sÿ6/Æzzžð³âóù¨Å÷vžX'ŽÃþ×Ãßø¬ñTû°zýoÕp�g²ëòEÃ74_~?V¯Ùˆc‡uæÕǺocçÖQ¨éÔÙìcrß³3Q½Z·{¯C|ȩ݃ýL‚â3Pe@Ÿ‡æÁóMmn|ÖNëEunÊd¸{  mâ~ë ú¡¢2éÓ¦õ˜â¡›EÃÃúçþQ�ß' ½•õ†�ÆgwÖã?êúÖf?»÷}¨×#Œ�Ùw]ÿÐQ]¿™»Í>¬Ÿéußôûêu2øç:~¾'¯Îêº×ÖÚøn'·Êñ4ƒÏ7»n§oÖ£Û¬¿ñgŵþßë¾`6â¶âË~q¦_¤5U"Ï"©�oëù¬bP)s£E.É W³®­�u£5ÝË;¸ÙÁvÿ{ïdùvðw6èOZ ˜~ÝíÖšÔô‡QtÎU}ùÖç|=ÒgêàV»÷Q°ŠÑøÜÃç; 7É}ÔÕIÈu�¿ÿ¶bn6ÝÌ´ãeâß?k©-?Ûóçh�úç�ÖÇ=_·ôÿú÷s^ü†5ø¬vúýÞãw¿�`ñùõÆ:e€2­Û½3Á=ífƒÒ>žùu ý÷žòu·ùTYèlóLÙìkÞvºÝô¼É¬S@¦9Éœ8Çzûñ³:sÕ†8ú¨ëµÙ½Íy‡uòeµŽð!ôs¯ó¤®+³ž>Û´^‹> û¹·:~Þó,ú‚ª­8ßc[q�MYP¾êš„yb}‚u݃zÎ{6€:tö“ÌvÞù§jw÷±�__PµëS s) ›$I³·¯þþ‹ÆýU57Ii;ãû~“Sa  ßßá÷µëkÖÑÿw ²!”LJ=XÖ C\tÙ60~(ƒí/“àX\†¤Úhõ{&“Â/ûò±Þ½ùbë2²i�íœ ¢=Œ)fõ˜"·¾é(8м­9ñL=Þ›×6ƒÐr]? Up0·º¾ŽMGð-œÔ'ˆ\‡öÙ/t3Ç©2­?†W“�vòËeý¾³"€Þw‡õ¹m�2½=ÐrŒÇç΋ܽY5(¹tÔ‡;6m×rußîÝ:ƒ4ÊTí/a·îddÏÚ®m³,tvSÛ¥´?¦8eœ·¿ë7Óœò8ñrêñ¡y(#©ëëöf6‚oeZàF×\Þoµ¹«�gcjØyÔ—Ïˤ´£^s쬇P¯^4þì26îê$µ5”£Ô•8±8,ô}ï¢i= ÁøJö�:¦8 åbÆ2¦è§/Âbu¹õxêjFR×CO½”6õy†ï釄gc�SâÜh}½J{ï½Ì�6ëqɩƞ†bVwÊ�âۮ祌ݮB¦Ùç‘zû•¥�Ÿç^Я�³XZíÈÉ齯Fê8Ç;&v,×IHÛà¹R&”ûüçrOYSGlÔ˩ϛ$Lš]]î ¤ô[G…���l‚mEÙw�–_�O\³ÆHêzÝ^š‹I!rkwæ«×Ó„¿Ê8¶ý[óºvN%©ê߬à²x=‚v‚.üúÓrõŸïÛïÓ-ž¯”úù®~ŸY@ßÞAk�}ó£LKxô?É-g�bQ þµj\þvý áa¨îµXìø³.W¯Ç«Ÿ1À÷ÿ:¹þÈBÿº® ‘»jaó6~Bxoô——MèC‡?· qbØf�,Ö«_6.>WƒÄiýŒ´11]/œ<L�l繃º}ë§A݉��¸Q0Ç6,õYž0öIýÝËËå2t{|ÛÒ£Á7´¹!/Öµ‹ë±ê¦6¨�1Åz±}±§²ërürµÇ:±¢gÚ—t]�¯nÕãÅõø¨®ÇÓ~÷¤矪¿Ù鲯¾*¤®Ç:þch/‘§ºëúןN{þ.ßlü�7§=¾N,ïÃ럕o–á$±ÏôU°»úßׯ'¯^Öuöh‹çëiR9UßY›'3Ä÷ò.Tw/·¬//Ëô¶˜l0O(“¶ÛŠù–ã»IÝV¼í¬YMöÔN”ܧlÓ¯Äï3eüœr"fU—ºÛ¢ª jº c$¿+fr’ø|½l¼Q¨'ÿcP5hÇ«ŠòKb£™> ¨‚Zmt “ëŸ5„Ýå7ßÏ»PÒš~?‡×Ù̺�rMµ›mSy.Våð¨.ËÃ{:Žã�e&´50<ºîPò¯CÛïvUÍ[RNY”Ÿ×îÕò&á'´p¼j›GÖ¼Ø÷SMüÓ'·Ãž¨l? ¾Î� åÃAÏovº·ax÷íV‹wo³·ºSõ74¾öÃŽåÛ¸õæùøçóPÛ˜7·6¾ˆ?£ÚØF ìÅŽmEì¯'-µË–ž¡Óº,ví�ÈGõ/Ö町t‹¬þL¿-eðßGZ]ö•áõº�Ÿ¹o¿%üÝIŸï¿ ÷¬>g—ÁíãÁv¶U »í“†˜…~Òî°>�ŸYâß_Ï/[~Þ.¯'À›'DoBêŽî|ÛæIhï>À›þ]ð“t1ˆ~®€/Ž~}ýªS†hÛ›ÄönÝÖ¥Ä$S7cõj¬ôÿKê›ubÓÐnöùíNzh;ø¯©�Ìx 釭OˆƒøáNBÛÎ>ok�•£ß Kêsú´³¶ñë@¾ìÒŽÿÊßëL¿sˆf‚èÀ­yzêbsµÀÙ•¯ƒèëÅÜ…/o¯bý,³z|”\�»›Vcîûž“E�]Ç®c¹¼ëzêÕ”yÏy†ÌK_ˆsŒ?¶Þ`UefÏ[þ©×[.:=¤jç/zùŒéWÊ­7+î£_\ŸJ0ÐÍÜ"%x“[>&>_E�ÂùwµfgM ]€^ ¬Œ‡žíºhiP��ûýzp6¼#Ç÷ý\ 1ÓñŸšo£Ú86Iø óΞ¿žUƒÔ¡ß[Õή÷û ïêöev=éïø ¸Ý·¤é~è&ˆ>]œžCS'mÈJ š¼ë¼^UG\¿ùblüTv;Î5J¨ë‹�vçóÄ5z{“Ü�„jƒU ¤Ÿîá8þþn·oìr|ù]¶]Nm+úßÅvâíýâS×óÐqÝ‹õ.¥/L™?]–¶9ä#­&ÿNø»»ì»9Êô¶¸øf@“Ž]ûb'øäÕ¼¥:qª£3âÑî½Lò™8ÍB·Gö¿Nœôäf@}^ë稠¸PXµÍ¡ÊtXŽà»éú ŒØ6/<´ÔŽÄ úc Y9¬w´§ˆ“ò—Š’æžËÞ€ªvê8ÛRLnÃu µ´»©b�ñ'ð± Þ…ê.¾!w¼]?WUúŽÌ©6ñŽà¹bûüfÒÖÿÑ0í‹Gܤ T–E|Ϊu5^g¥ÿq}/ÛêÅ×õdºÍ>ocb�“óÄú*3�ܤ<ÿ¿)¾Îœ„~®aq:m�›~wT&Œº hêBñq«íójõ˜±˜&üÝ÷=¿×_ö4ßíN5^ž%ü„ÿPǺJN‹ßi ǵÚ\M––4Óo’ÁUbß3ÝPOc_œÒë‹xZJ_kþ©w¯ïÍx2ЫÆó,±£o’…Ùg.~¶Ó‚¿£Ó�šU\ÖñåoêN¿«ÛXçOBu7Ûý£÷¾$mØ•~�â‰Pÿ¯¥Ÿõxëõ–Ô *ý×õEÂßÍocgðý�øSòooª¬Ì˜ðÔåÕdë¬ôXoZ:Þ}²§ºÚÔBóhÿÙáósi��ѨNKI¹RaïJ}¶Ê o·cëǺQž$þ¶ÅŽïm}¬v_^\wò9e‹n÷ý†jÑ=õû)+(RÝûw6Æû&ºÞ©> UÀaö?[uªÏÉDn÷íìÐîÍZ¨[¿Ž+'*tÖ6·ÑÞìÚ6+xZí_«LôãA\ßl#eþ)èÈ}}Iß²ÔcF"ehÑû»­Öü®zž#íêß[¬/ž2ž6má÷-ЍiÕ½Ô‡¡ûc¥ãïˆÙè±\^îqCÓ>NX„”{л³ÔÔ2*¿þtºjƒž‡nO>Y^ÿžB !½ÏËîw½£¯£LoOœâï<é÷S^ /Žâ亟 z4 Õ.Çã–v8îÃëž_n÷íöü\ÉÖ%/iY6 ‡Ó6ËB§+ç­�æ ™þ‡û¬ƒèÇê1Œ^œNÎ{ûX‡š…þ¿æ…mfªŒûi¿+þŽßëÌ÷7 Ëé}Á_RÖŒþ`|^†ôF¾¥èä—¿«[[ì´H[eŸ7=Êtš/ò¿Øã½[û�ø•š-Tí0|ú ì„j‡ãyqu¥ÚÛtðš~Æi.8E†Avmó,4ß­šÒ6¿Pøt$^ò£b¾á¿Š€�sîüûõ˜±øMìÝû¢Þm bWW Ì{ü­1)îcbÒ@)å»è¸¶ãº\WÏ䢨«qkèÛ{¹ãÿ–Ð`ÇAÅ»†7þΣ~?o o¨–!ÞýÂÛ묘•4°H –Ägªi fšxG¢ç ²”MFq|ÕtÙl¤›¹obÖîס"ЗèKZh[K  üúSÌœŒ¯¾²ç«S'‡D7�‡Üös{#€¾�7 î!I ôÅ ›lÙÝbóâ?Eµ»1b }ÙóÀ,ÿ�KÀnº9dYÖß{®vj÷–òãó¼ÚæiHÉ>¯Ú™¦móúêX�¥Ž‡0AŠð½"t_2ɧý·/œ‘ø¡À÷<”yïUñãójÍùQèïŠÃõU C¢»ÇòjçâsÓv"Ý b è›]î|É}êQ¦±bU÷�4�´Mê÷0±œž¬ÁІþv8ÆAËy%“À~W—íEÂ@j6¢,ôÅÎíô×F¦\w! (¯¶y8Ÿ'üŒv¯óE1‚è@÷ô=ÃîKb?2†Ó¸&¾lèTóùgÚ¼7'/‘œQ­ÕÇõç˜ô´èiœ±KÂÓo{©§yJ©o6H2fñdä¶bQW¡ß“–;#€þm—uǸ«6“¿üï}¾‡R\]?Õfƒ!NÚç«ÿ\Ò»l­eùý_®g ?aî¹Ú©Ý{`˜[[m›§!^qÑtR[mj ‰›û†;†D6÷EÍÙ”7ü¾ä´�¾D=f,RÊÓÞßm5ïûÜóx'†~Þ7,ê»Ñû¤ÇúÓGÂÓ>N#™î©-Ød¢©eÄc߸6÷²¥Ÿör(1»¨ßlŒwΦeŸß,&¯;å'¯– Þä: zûç Ë:x~9èZxT˜×îç¡»;îÏBÇí*%p=ÿâ9ž'ü¼˜…þr°›6š¶{°ŸºÚtÁošq[Wš¶6 ®ÿ÷¬áÏŠ×æ¼õuðÅ*Ž�b[ñ1X¼,mŒŸ:¾¿TŒlÙN,WíDÓ¿}p}´êÐç£Û[ ¸/‰ÿí|Àõxâê®�Ößá=�W u=ì¡ÍNY»Bß2¼àùçõqqÝvT ?¯ëïû “z7®o¾C¾¬ &i×*ü_‡eqx½ùź¨ùßxûÚ8ö}žØ.,†Ôþ  ßím}uÏ~ï]™±ñè鳆?/.(1H OG×™U›!.êÁYüng-ÎòÜtQíÚ=jí¹ª.~f¼o÷T»{µ Íè?öÚ7uòUõE)êùW i›gƒ^ ¡éØérU7bvˆ z9.ë¬èsnÙ´?; ìÖíí㶸�Çrç÷%)›Kg½Îos ØëwÆÒf?ï¹Íþ1q¾[ò\ýx‹€ïPúˆêóÆd›j\ð:´ŸÅü¿¸grþnOï‡a�›ÖW2-p‡”¹ÔsÅ7š¾ä¢îK®ÔãdVãâ�õfLè³®÷7¬êwJ"Èo…~?±­|4šàùçýÄÕõ†«_zX÷óúÑ¿ÿ2±�ês>}’XÖ‹ŽÛŠþÊ¢j“âzÑý"µgñkÚŽ/†Ö Wbó¦îäS²ÒÚ_LN»´ï°ß«®ŸÕ�88{Ú»{gšÕç«/~»{Ë­ùÀò y°—÷rQ¢Ôö®Ÿ¾±ºÖ%NvcFP\(<Äœjb7Kø ïïùç)ã�I]ÞpW¿y=Η) |-%H¡ï__’k=µ÷3¿�cáj\×bÀàTŢǺÞçZÎYâß/õ4щ*Ö÷¤Ç„§LÛB¿Q]³Ù"áwÌz যó.zh+úß�ÕÏÍQÝ/žx€ /c?Â=H~iåÈÏôìó÷ßøw¿„æ ÕÓ�ÝÍv²ú<¿ òn÷j ;Η;›ss÷N¬ƒç ƒÖIvßwZpw¾áß5˜ÄÁÞ�&ûç×÷ĺû®t±Nÿw$ÒËÄ#¹b_òK§»"« îùmZ¼Ã¬ô#íR6 ,ïíë£Öæ cžç¡Ï£Ö(­Ý¸ºuœû¡j©ýñÙõõP»œ�öëO§;Ï%bÆnn›�ÇÙ—Ä1hÜ�õ!³¾ä"¤ÝÓþº®ÇÝÍ«`Äíù÷Aý{ã)xÇZ³ƒXOÞ·ø³vy/ê+ëúe‡uý(¤eŸ/ ^›9 Õºæñ j|µÙg¹sü ú_®þþúz֣IJÝToIü±éú(íÔëP¶=Qô"¤mdÙ}|׬_œ}Ñ/žÝê—Ø»±Ð×ǘĆ翡›£R޽Z~3 \ ã{Ÿ4üù/6x^°¯`¯“ëÞ&�tu‡lœÐÿÑx@’׆‹”çj¾¡ ß…æôƒ�Ý·ëJ\z¤[,Úû‘•ö>¤rãY;™UÁó�÷üÛI¨²Ñç¡Ú0UÖ•$ÕŽñYâ÷¶iBÜôçWW׌ñÈ@¶'­ƒè‚@pÓ.Ìúžõ8ÚÝ�ã©3Ëì6dµU�›®Cl?Æ÷u_€?–a̺‹§B¾ù=°¥XÖ�öå"q>rÞa]? i›Y¢w…×�Y�ø4/¾¦W‰N7¦âÚ|“¹fµ~ýôú˜îæîÉ–ÏÆYh ž^ŸÒÕ�ÕF„£žÿmûêEœï îçuTßêã{þãº_Üo[ „!Ðýéo™t¨)ƒ·Ë-îF½ Í賺Ñ]fñýT‹ï)“Íáûª²ˆ “/&�‚èW×»äÒ&Ï9”Í,¤eÄÿw‹çj™ð;â÷¶� AÜôøž‰ÚÇ„ïï°Ó�2´oÒèu[û´Õþqûgqn²ÑK:U%õH±å†¶ùª~5Ï*içJ†ª[=^ÕÃóÄñ;0ïÛƒéu›R×Úżè089#¿¾$¿SMRëqÚ:Ä·çõÛãîùNj¢{ï2­ë©k2·ç¹}¹;@W]·�2ï;«ŸÊ=Y¢j»¾ F¯75ý\ïCzySß”ºÁ$žÖ÷­n«àùëägc·µ›÷!mÓôa½éáxoý¢SZ`ïÜ�ÞŽÔà¨d}ëu´ç÷Øn‡^eħt>‡õ€®\7ƒë“o è›NÆÿ빺þû›ž«IÂÏÏëÎÃj0•ü>©OC€üU§ÔÅ…uÆË´•÷TÝWõ{Ø~Ñd�íó¡—ûÆÒ?_ê}e¡ž(nj›S�¦­}Ÿ ½ 9Žüªö`Ò7_Í®�Y¯úÊ6ûÞÙŽc ú«7Õ†¬\ú’vêqê:Ä—õ÷,ì–�;©ÿYëÏCzö.[ªë´8œ…¶‚ç9œÂP%V¤”ñA¨2ýË{Žã{®‚¦w5¾Þ„?køÓûønß´ð3^×k-–gq‰÷;ÖãØ?/çQkýbUç;ö‹ë5+ý"ì‰zzã7 ed¯Ì²Zœ¯¼©;ôË öÝY·˜¼6Ùõùc»»Ì |f!¿ûØïTæ5‘ŒƒÃ·‰?弈@´79\O‚›×ý¸ðRÝMÚtc×Q=):ɼ¼OB øÏ=lÙo·ÔŽÆÑ4T™ôõ�8&¹YÀOá8ìnû‘«Ì6dµQ�׋õ§�ëã3ðäÕ¡yëIýly`ê½Îëö:§Ÿ†Ôħ´;¨ûWIßNb[ohМöÐ/-[꓎’Ç4Õßý#´“u?ox:I>ýbU·š–çº_œ WÿPÉ^ô^c#}šÑd3Þíþ6¤ TÖ}èUG»Ë½›¡:ú(ÄßmuÌoÕ)&|/Wž«­M²»o7î®T®H`øª{­â„¨�Í,³Pm6‹íl¼‡{ñ;¥Zd‰Áç¡�£;ã³÷½1OKã�þ¯®9èp2»,fœSf;rz}Ÿaz�ŠüŸ¥µ«žŽAüw‡Ÿå²ã1ûøÚ´8ž¯ÆGÉeWÍQãØ$ŽQ.vú®ªÅû¡½MúýlÎsÛ´þ¯Ê „}'W´W�C=¾~]ß­¾_m¨»ÓºþNZ™kWcì õ—{êzêzâ—óÀmëú¤®ëÏC»Ñw™¬Ç­Ëøêúº³*³>e^VÆ}è»1~R¾—[õõ7ý{S‹þ¿/ë~ uSÇí1MµN²iý³jO¬ÿ¤¥oçªñx&Ö½'¯ÚzV__‡Ÿ¯mꟷXë~q†å@?LÎÐÓ:׃PÖ݉/®˜y ÈÆì«>燆˜i¨Ž¥]Öƒ–ßêÄeýó&õ+uP°È¤œJºã/Çûvã$'å¨G÷¡S’·õ­­vã(¬Ÿ¼Z·³_?#ígbç–qðeÛ< eÛæãßúZ–.ÄÅ‚S�z§ãÑyøDß¿.Ÿ¥ÛãÝÇ=|–Y‡sÅÇ�?ÇÚ¦×s©6ú»IݦT¾«ïëÿîøÞÖó¸ï[üÝ7eÝßbáPžÛ”¾äø:P´ÿ¾ä¸åºtÓ–UëË{Ö+Ú¶lý^õwhÞ„vƒtÛÔõ.æ�!Tãòë«� ©Öó¾½ÚqÞ°;¨ÿîYTýO½~pUןu}ù±…1ár‡ï-n~ˆ}Á‡–J)–ÑI¨N‚ ·>ã—eqØÙ³ž¶ù2–E[×áì±_ŒÏÑÛ0& ø>ïŒ÷ïuaí@~÷íV˧Éí›#ó(Aµ)ì8tsDéAXo`úüÕEßÿ2« nå·Í3×Q°c[2oaL ”?¦xÚÁO>¬çp¯ëyÛí×yýÏÛÈû|^—c0f}Éqõ¸«÷0¹glÜ…§*Öõ.æ�WY×÷ª/I9 "÷ûÐ_·ÐŽ­ƒªgu߃µÿ«ÿû‡ÐFð|×ru‚é¼£2;¼ãÙè*x~‘4®ÊîeÏmEíÄqz'€ÞTÕñ¿(ð�¿ÈnÐÒÎBE Á¾îØ|¿ççªË x×îÜ&9‹êœûÐ)Cµ›½äæYkWeŸ—ؼðpРïDí@é§0-ƒ,›}Ö¡8¦ëjsç¶ïá"”±þpŸ—ŽZe‡6»ô ÒÓ®k:»d@-çûÐß0öoºVÇ3%·¥—­=ßUß<×/»@onÊÊ>_‹ï9¿@ó‚}ÕŽ¹EÆuc‘Ag\j°cšeÝ«v /Û‹J�CÆOóIaî õ¯ ­³Œ3È·-‰Ï¤ :Œ»ˆó¶y¡ï¾Úœž÷©6c—î·/©æ‚%Öãù@�¨¥Ûç­Ô6ûxã�Òy”q‰O³zcvnŸmYÀ||žð½Å¾¨ÄÀk5'ks<¯Z)·_,µ�ƒâ  7WrVSž áãöÅç2Ó÷¶ßêUúÈsÕI�à Pb½ýIaûmó4”™}¾œx0hЖÄgóQ(;cHkJ\d½ªÇÚ®|ú’}ÑK«Ç‹ú=ÃÚì㢂bU›–ú|Æ»Â3ülóŒëOÚýßeÑ»[')¯­˜ëa¿Л(÷(ÓµI–»þ*©漃}7»6s –¼Í`×ëëÂ[†Y¦YèëÁr ÷¡SŽr‚èùχÑ6¿�…NödÊÍØÚiޝçIe¨6þžçV‡ÖAôåžëñ¼€ÒŠÇλ÷œÔº^ÂÕÕ]Æ%f”¦šó½½ª?‹ ûö·-|¶’‚èݯ“”Ó/ žCЛyí3tÖ‰ ÿ>ôü޽\½§ýWTžgž«Në\êwì>tJ›Øç¸YimJžWÙçÓÂkÃÁ@úö7.D‡q·q ½ßû¬7»¨ÇK_X¶s±ýžjR-À缃® �º~šy›½¬ÛëyÁ¥œz¯vÎ÷¡?­ûÔ¬7Z\µôl¬ç59×½yèk�¤ês>ºÿ­à9äA}WéÙçq�±hé•2`™ddC°/Ÿ£A×Áü}K ô«ë>uÿãþn®d©>ß:pœÓ¸a=ž9îußÅêÎÛÐEæùÝŸoyÝWÇÖï£-^†*Ózÿ}ÁM¿¸¯ ó) à+è»I=ÊômïémH †å» <¦`_ ªT§ÄÏÛÕ@i]ÖÁ¤ý«Ê)Gë.: H½Iœœd\×܇Î8UÄãzœÚwn; z™U›»}ÛŸïYÂO˜w4ñKi›'YH‰mIî÷ÈýŽ)桟cPãïy\/®.| ƒèKæÔã§¡¿Ñ:«ò¡S�x›½ 7�óÓA_±Q‡zúí#Ï»n'uÿ²÷ú7 Äv¹šßô1¾ˆ¿ãiÝ\dö}_Ö› úº+~q«_¼@–зUëôûëNê¦�µ«[íãþ7ÇUëÑ�[þŒk·×¥g,®’ܺê/ÃMrÛcý"äïoŠ2w³y#Þ ? Õ̓o Ú—u‡üŸPeh/"ÀÖmí¿oµ³‡&>W·ÚÛ<ïoºl7f¡Êšùr\&CŒ)ëùÛ6cŠå­×zL¡ W_òõ=¿þô·=¿¯é­z<¹õÚ46þ­®ËÖ"(åT×iRo&õúAüó‡ }ýzÝ`ÝÏ/ŠX;¨N ½ý|„o¯I_Ýz>ªÏY�i®FÚV¬Çr¿Õår©­¸×ÿ�€ž+i©IEND®B`‚python-pot-0.9.7+dfsg/docs/source/_static/images/sinkhorn.png000066400000000000000000001105241525521106200242550ustar00rootroot00000000000000‰PNG  IHDRèñþ$ësBIT|dˆ IDATxœì�wXTG÷Ç¿»Ko¢€Š ¢ ‰bWŠ‚TŒ±ìÆ’Ø¢±ü¢y5jìFì1F5Æ^bAŒT"vQ4@Xéu—=¿?|÷¾¬» »° ¨óyž}˜9sæÌ½sï¹÷Ι ƒÁ`Ô*ø5mƒÁ`0 y˜ƒf0 £Â4ƒÁ`0µæ  ƒÁ¨…0Í`0 F-„9hƒÁ`0j!ÌA3 ƒQ ašÁ`0ŒZsÐ ƒÁ`ÔBtÔ-�žžŽÒÒRmØÂ`0 ã¿ðÔ]êÓÞÞ/^¼Ð–= ƒÁ`0P‰7è�"##C¶0 ƒÁø/j¿A3 ƒÁÐ>,HŒÁ`0ŒZsÐ ƒÁ`ÔB˜ƒf0 F­¤   Æg åååÕXÝZqÐ………ÚPËøÈËËÃÝ»w‘žžÎ¥¢¸¸¸­b|ì<{ö ÑÑÑ5m£ /^¼ÀìÙ³!‘HæW×½ääɓطoŸFuªŠÆôÚµkabb‚k×®©$ß²eKØÛÛÃÒÒŸ|ò‰¦ÍÑ%%%èß¿?¬­­ñÓO?©U¶¸¸-Z´€½½=,,,Ð¥K-Y©=JJJ�ŸŸ_éòÉÉÉ4hæÍ›‡¨¨(¬]»ãÆ�ÃË—/1qâDÜ¿Ÿ“•H$8zô(žù�5BÿþýË•½víZ·n ;;;xzzVºÎÚÀíÛ·¢_¿~èÝ»7 €ÌÌLÙÞ½{srøùçŸkÈêšgùòåøæ›oÊ•¹yó&Ú´iƒÆ�£]»vÕdYùTæZyú{NN‚‚‚0wî\èêêÊä•w/™0aw/QçZð÷÷Wz- 6 —.]™3g´ßðw! "‹©qãÆ€¬R™‹/R·nÝ5kÖL“æh”óçÏ@|>ŸJJJädΞ=K‹-’K/--¥ .P×®] yxxT‡É!##ƒ6oÞL666\)¹¹¹äììL/^”K4h ˜˜.=""‚P»víªd{VVµlÙ’zõêU¡lNNyyy½WçFÒþ€ŒŒŒ(55•$‰œÜljÏçÓìÙ³)!!A¡ÌÇ@qq1™››Š��/W677—zôèAnnnÕc\”½V�¿¸¸8…åj{9r$9rD.]Ý{‰¦®…‚‚jÛ¶-eddh¨…ª¡Ñ7èS§Nq‹˜9r¯^½ª°Œ··w­}Š+KóæÍaffèСƒÜSðöÉôöíÛré|>>>>ïÍ›sbb"Ú·o8;;ã믿Fjj*D"Q¥ôýüóϰ··‡···Lº‰‰ BBB §§'“ÞªU+øûûcäÈ‘•nÔ©Svvv*ÉšššÂÁÁ¡JõÕø|>>ýôS€�� ¬­­Áãñddˆ¡¡¡8yò$V­Z{{{9™�…³gÏ¢N�:€?þø£\Y4iÒ¤:ÌR‰²×Š¢ó·råJ…åjs¿~ý:îܹ£ðËWE÷’wïËšº 1räH|÷Ýwšh¢ÊhÔAoÚ´ |þ[•b±Û·oW©Üûpc°±±All,ÂÃÃqùòe¹|"‘#GÊÕñ>´¬¬¬°víZ„‡‡C(¢nݺUÒwõêUÔ¯__ažµµµÜ…hee…S§NaÊ”)UªP/秪H$üßÿý÷YïcçàÁƒØ¹s'¨4ÖX›úIy×Jjj*^¾|©´lmjGYÖ­[‡)S¦(´OÝ{IE¨s-Œ3‡FZZšZuT�9ègÏžáܹsX½z5—¶mÛ¶�ÀÓ$vvvèÑ£ttä`Û²e îÞ½[Vi###xzzÂÅÅ…{ઠ‰ÿý·ÒhHooïZ{³øÐ‹Å˜9s&>ûì3¹·��‘‚‚äååÁÇÇ­ZµÂ½{÷ððáÚ6K#lܸQi€Um¥¤¤gΜÁàÁƒæWt/ñññQù^¢îµ`bb‚Ž;b÷îÝ*é×j/õ©Œ­[·¢gÏž˜6mÖ­[‡ääd$%%᯿þB```¥õŠD"ìÛ·7nÜ@RRlmmѺukøúúbãÆ�ˆŒŒD@@.\È•¹sçŽ;† ++ -Z´@ûöí1tèPN.)) +W®D^^rss1a¸»»cÍš5 …ð÷÷ǰaÃÀãñðÝwß!-- yyy077Ç–-[QQQذaöïßÏÕ=iÒ$@ƒ ðÃ?(m[tt4öîÝ‹/^ÀÚÚAAA2ŸûE"fÍš…¼¼<äåå¡U«VX°`®]»†={öàåË—hÒ¤ úõë‡nݺ�ˆ�C‡áÕ«Whذ!ÆŒƒ¶mÛVúøk‚nݺáÌ™3èÛ·/6lØwww™ü1cÆpŸ¹333qâÄ …Bèèè`êÔ©€””™7ú±cÇâÖ­[ˆˆˆ€ŽŽ|||ЦM› mILLD\\ ‘H`mm�-ZÈÉݹsçÏŸ�Ç+W÷Í›7qáÂÁÌÌ ���2ŸãããqñâEdddÀÉÉ ýû÷Çþýû! 1hÐ ØØØh¬mQRR‚3f`üøñå꫊=åé± …°´´Dpp0Nœ8�»wïÂÁÁݺuƒ½½½Ü±SU¾2œ÷îÝÃü�Å‹«­Kzý]¿~úúú �ÇChh(RSS1uêT899©u¬Êë7Ê®•„„üþûïøñÇѲeK=zÀÛÏÚ¾¾¾ m/¯¿—×ŠŠŠàïïÏÍݹsàóùðöö†‡‡‡ZÇðÂ… °±±�¹¹¹Â|uî%å¡êµð.žžž8{ö,fÍš¥r™*¡‰�ìÂÂB²°° ãÇ�Ñ?üÀTõîÝ»Âò .T$öæÍrss#]]]Ú½{7%$$О={HOO�о}ûÈ××—Œ��I,Sii)}óÍ7ÄçóiܸqôàÁ …´oß>233£¶mÛÒãÇ�9ý)))4wî\211!´aÃêÖ­íÝ»—êÔ©Cè÷ß'"¢eË–‘³³³œ�‘‘‘Bööö€Z¶lI!!!B»wï–iϼyó¸ ±mÛ¶Ñ—_~I·oߦ۷osm:}ú4'/‰hþüùœîþýûÓ²eËhܸqtûömº{÷.:~ü8͘1ƒfÍšE±±±IˆÇãQtt´Úç´,€†Z©ò¹¹¹Ô¢E ®O888ÐÈ‘#)44” ddÓÓÓiÅŠdii)ˆ“��@K–,¡ �¯¯/íܹ“öìÙCEEEtçÎ211¡'NÈÕݧO™ ±û÷ï“…… ?~<]¹r…Ë5jyxxÐ�hïÞ½åê‹Åôÿ÷4dÈÊÉÉálìÒ¥‹Ìyôè-Z´ˆêÔ©CãÆ�£… ÒíÛ·ÉÑÑ‘ªÔ¶òÈÌÌ$äääDDoƒ\úôéC”ššZnÙÊØ£Êñxôèýøã�T¯^=j×®�3†Ž=JééétèÐ!jР…„„È»Šä7lØ Öqy—¡C‡Rff&=þœPÓ¦MË ˜›8q¢\�˜H$¢ÀÀ@š0aeddЭ[·¸~^TTD“'O¦C‡©u¬õ›&MšpýæÝkEjstt4íÞ½›\]]©E‹´{÷nÚ½{7w–¢j¯¨?˜ššÒ¡C‡èàÁƒ´wï^*,,¤{÷î‘©©©Â@¯ò˜>}:�5Ji¾:÷)U¹Þåï¿ÿ&ccãj ¨ÔˆƒÞµk5nܘÄb1½|ù’ttt8ñìÙ³rË+sÐÓ¦M#äéé)“îããChÚ´i”˜˜H7nÜ "¢õë×+�hüõ×_ µnÝZ.»gÏž\ýçÎ�“‰F ãäæÏŸ¯4Ú¼cÇŽœU†ÔA›™™ÑÌ™3eòÚ´iChÈ�!råfΜI¨Q£F4mÚ4…õÖ«W�V¬X!“gggGhòäÉJmR…ª:h"¢×¯_Spp0 îâ@VVVtõêU9ùàà`…‘²AAAäàà@;wî”IïÚµ+ùúúÊÉ¿ë ãããÉÏÏ�>|(';jÔ(ª_¿¾Ì9W¦û—_~!ggg‰D2éÉÉɤ££#÷PÔ«W/jÕª]¸p�ˆˆ–,YB¿ýö[•ÚVeoJ¹¹¹4vìXZ°` ÀÀ@•n0êØ£ÎñèÝ»7YYYQbb¢ŒìñãljÇãÑÑ£GeÒË“ '¯*™™™r×›ôz’ÞS¡ÈA‡„„ŸÏçœ=Ñþó211¡7oÞÈȪs¬¤ýæï¿ÿ&"ÅýFz­¼{N½½½Ëí7êôw¢ÿõéKKYù¦M›Òž={dÒ===ÉÛÛ[iýŠ0`ÍŸ?¿\™òî%×®]““×ĵ %66–Èœgm¢‘1èM›6aâĉÜçc[[[|öÙgÒ7tlݺµRz¥óÙ5j$“.ýÿöíÛhܸ1<<< ‰0þ|ŠÇ!ºwïΕy7DùWTT___DEEáüùó6l'§hì¹2äää`úôé2iÒO[ ròRû’““1sæL™úõë'×'6lˆöíÛË�'¼~ý^^^€ `ìØ±k›2 0mÚ4,Z´‹/FÏž=qüøq„††VXVU{Ô=�ŽŽŽhܸ±Œlß¾}ѬY3LŸ>TfuåUåèÑ£rçzøðáÞ‹©£óêÕ«066–ù4ëì쌼¼<Ü»w�KS÷X©Òo”]+ª J/k‹²þðôéSîž/ÅÉÉIí~›––ÆEÔ+£¼{I¯^½”î¶X•kAJ½zõ¯_¿V½QU ÊúÎ�;ˆŽŽ†‡‡îܹÃý¤ vìØQ©Õ]¤c§ïî?-ý¿uëÖ\ÚãÇ�¹…4¤±,e/œ7n(¬ïÓO?å»��ºwï.3f­)LMMå¦ÿHë-/¨ÎÔÔTî&%-×´iS(Ì«M�"fffð÷÷ÇŠ+ðàÁ̘1999ذaƒÊ:lmmadd$“&Ê]HåèÑ£ðññA·nÝʽ¡©¢ûîÝ»ÈÍÍ•;R#×ç+ ºS¥þ+W®àäÉ“2¿S§N)ÔW\\Œ+VÀÎÎ<¿ýöÌÌÌ0}út•öuWçx(›Ò¦ìx¼ �ÇC›6m�˜˜ˆ””” mSWþ]öíÛ‡¿þú “&Mâ~‘‘‘€ýû÷«uÝ4kÖ ¹¹¹ÈÎÎæÒ¤�LÎÎÎ\ZeúŽ*ý¦²¨{-)“¯Ì5©¡PSSS•dÕ½—”w-$&&ªT§Ô�…BÕTEª|Ö7mÚ+++Ìš5 ÁÁÁÜoëÖ­Ü�ÎÈÈÀ¡C‡ÔÖ½`Á888àŸþÁÖ­[ñðáCüúë¯ÜJ8‹-âd+z¢)ÛÁ•=ÕUôä¦)ôôô*µ\^9U#jŠåË—+LçóùX½z5œœœ”>4)BÙ— eo<7oÞÄëׯ1zôh|õÕWHNN®’¤reutt ‰äú™t}y¨Rÿ÷ß�AƒÉü¤o~ïR·n]™i)�5ÂÏ?ÿŒœœŒ7®B'¤Íã¡ (wzPU䥤¥¥ÁÂÂëÖ­ÃÒ¥K¹ßæÍ›Ñ£G$''ãÊ•+*ë›2e lmmàí׸]»vaîܹ°µµåä*s¬Té7•EÝk©<»ÕÑ£ 33³r—Š®è^âèè¨ô^RÞµ0~üx•È TŸ¯¨’ƒÎÎÎÆž={pèÐ!ÄÆÆÊý–.]ÊÉnÞ¼¹RúíííqàÀÜ»wK—.ÅÓ§O†˜˜™ù¹nnnÜß¹¹¹rºrrr¸¿[¶l©°>MOõ1bbbb4ªó}$22²Ü ¾cÇŽ>P„��&NœˆÕ«W£aÆ9rd•¾*H#G˾-•%++KáWMñ÷ߣ¨¨Hæ§ÌEŒ5 }ûöEDD7¡*hòx<þ|>_æzÖ¤¼”ƒ"((–––r¿   àff¨‚H$Âøñã‘‘‘�iÓ¦aÖ¬Y˜5k–/_.s_©É¾sðàAûì3­}–¬*<[·nEݺu1{öì*­yüïxDEEÉå);Ò7‘²äääàêÕ«ð÷÷—†PW¾"Î�;‡ž={*Ìûì³Ï ¯¯�@,«¤ïúõë°´´ÄÂ… ñË/¿`ãÆ�4h�ÜCuöcccqÿgeeieÈN“X[[—û5DÓ÷éµP¯^=Ìž=OŸ>-Wþõë×ÐÕÕ•FMMMEjjªÊõªŠÚ½€ˆPXXˆøøxüòË/ðööFnn®œ#’H$011‘ ²Ú´iÄb±L§‹Å\Y"’ùßÙÙ‰‰‰hܸ1Ú´iƒ¶mÛ¢k×®ðóóÃØ±c±oß>™1Ž;vÀÚÚáááØ¿?÷¤•––†yóæ�ÇãaõêÕܼ×wë“H$2ÿ—E‘�eŸä¤Ëx>xðÅÅÅHIIAJJ Z´hÁÉKÇ—ËÖû® šÌ“Ú'm—:Ÿ›¤çI‘ž²mQ•1cÆàÙ³gré·nÝ£G�ðõ×_ˤË|õ(›^RR¢0]ÑÍô]y777Œ?óçÏDZcÇ*¥[  44W®\‘sJëÖ­ƒžžžÜ†*ÅÅÅr ôWµmåñæÍooÊŠú³­­-¾úê+`èС ÇÔÔ=W¯^Uùx<}úTn÷¨¥K—‚Çã)|“Q&ÏçóÕþ ð×_áúõëJžëÔ©ƒÎ�;#==‡–ËWt\\\\°dÉüøã�ؼy3¶oߎ}ûö!66V.àM�¾£j¿Qt­xxxàéӧܵš˜˜(p[Õk©²zÊÃÃÃCárÉeQ÷^¢Îµ0dÈ�rÇ—oÞ¼ ™�üü|îÁKã[Sªö�””D|>Ÿ ÉÔÔ” H йsçdäV¯^M|>ŸôõõÉØØ˜LMMÉÐÐ�tttÈÁÁ�“ÓÕÕ%}}}211!###tàÀ""’H$Ô«W/™Púw®®®2 ˜geeÑ7ß|CNNNdkkK-[¶¤úõëS�=èŸþ‘±1&&†ÈØ·páB¹vKí”Ê º}û6—_XXHcÆŒ!===²··';;;:xð —'=ÒvêèèPHHÅÅÅŸÏ—±A Љ'Ê-÷äÉ…åŽ9B‘‘‘$äÎQÙEä+ÂÒÒ’tttdΟ‘‘ééé‘@  �ª¬«]»vtöìYš>}:Íœ9“Ž=JQQQôóÏ?S›6mdÎËÍ›7©gÏždiiIÆÆÆäééI—/_¦èèhêÙ³'YXX�‰‰ yyyÑŽ;(&&F&½{÷î´}ûv '???ªS§Õ­[—zöìI—/_¦„„jÒ¤ ‘¹¹9ùùùÑ… ÔÒ-åÎ�;äëëKóæÍ£�;wÒøñãiðàÁôêÕ+N¦¬fffÔ©S'š9s¦ÌÔuÛV·nÝ¢>}ú�‹‹ ™˜˜�±±1}úé§Ô¿™é>Ë—/'[[[®�4iÒ„†^%{T9Do§¾uèÐ�Ö¬YCkÖ¬¡£G�ÒØ±c©oß¾ôâÅ ¹6•'Ÿ””TQ÷ã¸víµjÕŠëË 6¤ððp™#GŽP÷îÝÉÄÄ„LMM¹{Çþýûeú¦‰‰ uëÖ�›¢”——G=zô Æ�“‹‹ 5hЀ›äèè(7ý§¢c¥¬ß”EÙµ"E(’‡‡�3†6lØ@›6mªÔùUUÞËË‹~ýõ×Jõ[)�?&===***R˜_ѽäúõ뜬&®…w1bmܸQ&M,“——uíÚ•›j¬)xD•˜ŸP £_¿~¸~ý:/^ gggðù|Þ¼yƒøøxlÛ¶ IIIX´h‘ÌJbRŠŠŠ�ŸŸÏ’T—ÝoÞ¼Aýúõkýç¤êâúõëhß¾=€·c†QQQÈÌÌ„››:uêT¥i"µ�ÌÌL¼xñ...Ü�ÎÇLEÇ£oß¾ÈÈÈ@tt4„B!’’’àââ"\YùêF$¡]»vøñÇѧO.]"‘ 55ß}÷.^¼ˆ„„¹OÞÚî;D„¨µqLMÓ¹sgÌš5KáPdMÞKŠ‹‹aoo�7nÈMýÕu÷äòåË€F�­TæÜ¹sJ÷`0µé±¶ä«›Ó§OS›6m”æ …B@ÉÉÉÕhÕûËþýû©G�5m†aaaÜ*nÕEíŒbТE XYYáï¿ÿV8-F"‘pcˆ½{÷®nó F%)--U+†A]ùêÆÕÕÏŸ?W:}/::Mš4���M5[ö~2xð`ˆD¢ Ç¢«“ÒÒRlذk×®­Öz‹ÊN&®E¡_¿~xüø1fÍš…{÷îáÞ½{¸víþøã̘1IIIX±b…ÜÊ: £öqëÖ-¬\¹'Nœ@JJ ^¿~ SSS¥^¨+_S˜››s+€åççs3:þý÷_lÙ²‡ÆŸþ©p%†<<]»vÅ—_~‰¡C‡ÖŠ5/^ ôëׯZë­µcÐeÉÎÎÆ‹/�’’‚‚‚Ô¯_ 6ü¨7™g0Þ7„B!^½z}}}ow²´´T:§T]ùš†ˆƒÿý™™™hРZ·n�¦M›Ö´iï%‘‘‘8|ø0~ú駽ϟ?X¶lYµÛñ^8hƒÁ`||ÄÇÇÃÆÆFnãêäñãÇhÖ¬Y�ÔÍ4ƒÁ`0µ�Z$Æ`0 ÆÇ sÐ ƒÁ`ÔB˜ƒf0 £¢öB‹-Â?ü  [ ƒÁ`ü$Æ`0 F-„}âf0 £Â4ƒÁ`0µæ µžòÖbV¶§/ƒÁ`¼ï¨í ãããñé§Ÿ–ûóôôDpp0,X€»wïjÃn­SRR‚þýûÃÚÚZn³ù��ÀÕÕvvv¨[·.¾üòK­Ö—žžŽ+VÀÏÏÐÕÕ…‰‰ ÜÝÝ1}út¤¥¥A$a̘1øþûï+]Onn.‚ƒƒ€'Ožh° ƒ¡ÔÝþª  €bbb(&&†  “'ORLL ýóÏ?tðàAúá‡ÈÌÌŒ����ܦíµ�óçÏsmãóùTRRRÓ&Õ¹¹¹tîÜ9jÔ¨ Q£Fi¥žÒÒRZ±b4h�:uŠI$ÑãÇ�iåÊ•dkkKmÛ¶%4kÖ¬*ÕYTTD¶¶¶Ô±cG µ‚Á`04C•öƒvssãœXRR’\þ�;w¸ü-ZP^^^Uª«VRRR¸ŒN�:Õ´9µ‚€€­9h‰DB£F�âˆöï߯TöÉ“'T·n]�8h"¢Å‹Š��­².ƒÁÐZƒþä“Oàèèxøð!Ο?¯Íê4Š�� bccŽË—/×´9µmîä‚ÐÐPÀ·ß~‹!C†(•uvvÆŽ;4VwPP ,, Äf2ŒZ‚փĤ[Å@BB‚¶«Ó(vvvèÑ£ttÔ^Ï…¡ùùù(»-ù´iÓ*,Ó¯_?ØØØh¤~GGGtêÔ {öìaAg £Ö UÏ“››‹ÄÄD€��úôé£TöÊ•+8uêbccQRR‚fÍš!88mÛ¶•“}ùò%öïß�¸¸8ÁÛÛC‡…©©)V¯^ C$áÛo¿E^^òóó!°gÏ@ZZ–,Y‚üü|äååÁÖÖëׯçêøî»ï�––†¼¼<˜››cË–-€¤¤$¬\¹yyyÈÍÍÅ„ àîîŽ5kÖ@(Âß߯ “yÛT§mÿý7NŸ>�/¾ø­[·®Ôq�‰‰ÁÞ½{‘””„ôôtØÚÚ¢yóæøòË/5æÔîß¿�'NàÞ½{¨S§ÜÜÜðÅ_ÀÒÒRm]ÈÌÌ4iÒ 6¬°Œ@ ÀŒ3P¯^=µëSDPP¦L™‚ÈÈHøøøhD'ƒÁ`T‰ª|/o :%%…ú÷ïOÈÜÜœŽ?®PGII �7ŽÐ°aÃ(!!�ÒÓÓiÆŒÄãñhñâÅ2òëÖ­####233£ 6PLL ­ZµŠ:vìH$>ŸO .$""‘HDóçÏ'{{{@œžôôtš7o™››òðð�©gÙ²eäììL¨Y³f2íš;w.™˜˜Ú°auëÖ�öîÝKuêÔ!ôûï¿+l[|||¹m{ñâéèèª[·.©u>ˆˆîß¿O<�ôôôèÉ“'”——G§N�"KKKÒÓÓ£“øG¡Ë IDAT'Oª­SJŸ>}YYYQ‡èÀ�”˜˜H.\ GGG²°° ¨­÷?ÿù×�j*X+--�ttthüøñ5R?ƒÁ`¼‹Æô§Ÿ~J:t ²°°àÒ-ZDJu,Y²„�««+•––r饥¥\¤î­[·ˆˆèرcœÞ'NÈèÙµk—'uÐRfΜ)ç ¥*tÐDDóçÏ—sÐRzöìÉå�;wŽÄb15nܘPXX˜Úm#"zøð!×]]]ÊÉÉQzÜ”±{÷nâñx€î߿ϥ/[¶Œ;emQ©ƒnÞ¼9Èä]¹r…�¾¾>ÅÅÅ©¥wòäÉ\»ýýý+e›&èÓ§™››Saaa�ÙÀ`0R46}âÄ DGG#&&W®\�ŸŸà—_~Add¤Â2EEEX²d Àßß|þÿÌáóùÜ'ñ#GŽæÍ›hÚ´©Üçò   ˜››+¬GWWW©Ýåå•7ö,-WTT___DEEáüùó6l˜Úm€æÍ›cýúõðõõÅîÝ»ajjª´~e 2!!!8xð ÜÝݹt777€P(ÄóçÏÕÖ[–úõëÃÐÐP&­S§N¨W¯Š‹‹±téRµô•=oYYYU²­²¤¤¤ -- YYY8uêT�ØÀ`0eÑø4�ÇC³fÍðçŸÂÑÑoÞ¼ÁðáÑœœ Ù ¤¤ðäÉlذA&ÿñãÇ€/^ //�=¸ººÊEóù|4hРÚoðŸ~ú)g‹��ìììwîÜQ¹me™>}:¦OŸ^i{ôôôðõ×_#11ÀÝ»w‘˜˜(ã”‹ŠŠ*­_<¶¶¶xóæ nݺ¥VYWWWîïÔÔT•ËÅÇÇ£¸¸X¦|exùò%†ŽÐÐPtìØaaaøüóÏ«¤“Á`0ªŠÖ‚ÄêÖ­‹¦M›"&&EEEÇðáÃedòóó¹¿ëÕ«™|ÁÕÕ™™™ÜeoÊ5A�:u¦«Ó6MòäÉL™2áááh×®fΜ‰É“'ãîÝ»åéiéWu’ §§‡’’$''#;;[éq-˲eËàêêZ¥cøòåK`Û¶mhÞ¼9† ‚]»v!33uëÖ­´^ƒÁ¨*Z�â.ëH}Výä“O¸¿---Ñ·o_¥ºˆæææÈÊÊBrr²Ru)..V»LY”Í V§meIIIÁµk×н{wµD^^ºwïŽäädŒ9;vì€@ ð6êZÛäææ€ÚÑç–––˜:u*Ö®] ±XŒÐÐÐ §ZI$œ9s¦JËŽ&''£{÷îXºt):tè5j¶oߎ`„ •ÖÍ`0UE«ó ­­­¹¿Ë:ˆ[·náÊ•+¨S§ 8wîœÂ9¨¡¡¡øþûïÁãñ0pà@À�7���-#' ¯Ðccc�ÛpA"‘ 66¶-«uÚ&%33Ÿ|ò  „Î�;+Ý B7oÞä^‚‚‚8ç (~@’’ššªÖuEv%&&rÇÿ‹/¾P[ÿâŋѩS'À÷ß�Û·o—+?gÎtèÐíÚµS».àíЂ··7F�-³(J—.]àää„°°° u0 †VQ7ªL"‘�H$"‘HD-Z´à¢oãããI$ÉDoذ�Ë·±±á¢cûôéC»wï&""¡PH...€æÌ™Cb±˜ˆˆŠ‹‹éÈ‘#dkkË-Á˜••ÅM}š4i'›™™I~~~J£¸#""¸¼´´4®+W®¤&Mšª_¿>½~ýšÓ)‰èûï¿'äââB"‘ˆ$ ×~@#FŒ�k·”wÛ&‰”¶�ˆèæÍ›œ�H(ªun²²²¸u¬gÍšE‰„ˆˆ222¨W¯^Ü4°�;wrç"//�Œ��ÉÈȈ233ËÕ/�â611¡ßÿ�ÓŸ››K 4räH.]]ýYYY4dÈ@ÆÆÆ´téRzöì™Ì±}øð! >œ:wîLÙÙÙ2åU­+11‘š4i"g«”~ø�ëÓ ƒQS¨í ãââˆÇã‘®®.‘©©)“¾¾>ñù|Z½z5'+‹éÇä6Yh×®õíÛ—ºwï.³.waa!ýøã�äêêJöööÔ¡C²³³£ÀÀ@züø±Lý9994gβ³³#777ò÷÷§Î�;ÓÉ“'I___¡ƒ&"úé§ŸÈÆÆ†œœœèóÏ?'OOOZ½z5Í�;WÆ)J§oéêê’¾¾>™šš’¡¡! º}û6ÅÄÄ�@ .OGGGa�ê¶M,Ó°aÃÈØØ˜æÌ™£î©!"¢˜˜êÖ­éêêRÛ¶mÉ××—|}})66–Ö­[G666ÜLDHHH@FF4h;;;¥c·"‘YYY°´´¬píg阧t:’��Š‹‹±páB™¥#¥H$dgg£¸¸ 4�ÇCVVŠŠŠ```CCCèééimÍiuÚVUŠ‹‹! aff“ åcbb�’’‚þýû«\Gii)222`ii)ó9]Sú% ’’’  a``gggèééUX®¢ºžÛ·o×ê”"m믩º C›TË´¶IKKCzz:ôôôÀçó!‰ ‰Ð²eËš6í½@:ÿ¾ê¯©º C›|šÁ`0Œ�â÷»([°Dê,ÐÁ`0 FuñA9h"Âúõë¹éWeQ¶*WTT"""´mƒÁ`0jQ-Ó¬ª‹µk×ÂÑÑ­ZµðÖa/]ºÙÙÙ°³³ÃÓ§O1qâD™¹­ÁÁÁ˜6mLMM¹õ˜ ƒÁ¨i>˜1è””Lš4 ÇŽãY¿~=ž´Í_ýoooÌ›7¿üò sÎ X·n¦L™¢µ Â��„††¾ýö[9ç\gggìØ±C%½D„}ûöaذaåÊÂÂÂÊ•3f >Œ´´4•êg0ªóA8hKKK9Gìèèˆzõê)tÐíÚµ“Ó‘‘‘�Æ�kÕNU122‚§§'\\\ÀçWß)""¬[·cÆŒÁÁƒ1qâÄj«û}¢¤¤gΜQ8†« òóó±hÑ"Ð'XL›6­Â2ýúõ“ûФˆ˜˜888T¸ ™££#:uê„={ö”xfbb‚Ž;Ê^2ŒªSk^�D"fÍš…üü|äççC `Ïž=Þ®µ½dÉäçç#//¶¶¶X¿~=WÖÉÉ YYY())áö æñx8p ê-KJJðÕW_!22W¯^…³³sM›¤"ÂéÓ§ñàÁ4h€áÇCOOwîÜAzz:ôõõQ\\ ccctîÜÅÅÅ8þ< AD000@çÎ��‹/B(ÂÒÒÁÁÁ8qâîÞ½ tëÖMáV—.\€���Ö¶*�ˆˆà,›4i‚† VXF `ÆŒN›Ú½{7FŒ¡’AAA˜2e "##áíí­TÎÓÓgÏžÅÌ™3Ù4ECƒÔš7h�333œ?û÷ïÇÙ³g¹<>Ÿ9r@TT”LY}}}´oß7nÜ�I_¹r%®_¿Ž_ýÑÑјԡC¹t‰DBÍš5#{{{’H$\z—.]hÕªUUhMùÌ›7�;�:uÒ˜ÞAƒÑÅ‹U–ùò%µk׎ÐÁƒ•Ê¥¤¤Š‹‹Ó„™ ã¿Ôš1èªÂãñ°mÛ6üðÃøå—_ ¯¯¯R¹„„\¸p¡J«*½ÏlÛ¶ £G�üùçŸðôô„X,Æwß}‡Ž;ÂËË«f ü/wïÞ…X,ÆëׯeâàÙ³gHII‘IûöÛoqõêUôë×_|ñ–.]ªv�<mÚ´Á¾}û�’’Â� …B˜šš*,“™™‰îÝ»«5%ÎÁÁAf‘œæÍ›s«3õ/>>ÅÅÅpuuUh×ýû÷áéé©’®—/_bøðá E‡°gÏ 8P¡¬t,^(ªl+ƒÁ¨˜ÆAoW [²d """ЧO•ÊDFFbÕªUmp‹ƒƒ÷wÇŽ±fÍ̘1¥¥¥6lnß¾­Ö8¨¶�Þüýüü0eÊ™¼)S¦ÈE»óx<¬^½...xþü9ˆ¨RçX:ÿòåKÎA›™™¡°°P¡|ݺuqýúu¥Kq*âÝilÐÓÓCII ’““‘���:uêT¨gÙ²epuuUè 8€Aƒ©4+àåË—À¶mÛмys 2»wïFff¦Â) ’� CuÞ+ýî¢#Ѝ_¿¾Ê΀ʭï²téR\¼xQír“'OVú&R˜6m._¾ŒÃ‡#55_|ñÂÃÃUÞB[ÇEØ÷æÍ•æe‹Åb¬X±‚{XÛ¸q£œcW…çÏŸƒÏçË,ÖQ¿~}dgg+-£««[¥¥?---1uêT¬]»b±¡¡¡Nµ’H$8sæ ¾üòK…ù»wïÆöíÛ+¬;99Ý»wÇÒ¥K¹ÝÝF��;vàÀ�˜0a‚\鱨_¿~…ú †êÔ:-]päÝ7‰D‚ØØØš0I!S¦L)w5&ehëm455ÅÅÅ2oÄ•�ÇãaÇŽ¸{÷.ž={†¿ÿþ‹-Â’%KT*¯­ãâììŒV­ZáôéÓ mÙ¸q#¾þúkîÿE‹aúôéhÙ²%6mÚ„‰'ÂÃÃ�:uR¨_úX–œœ\½zþþþ2 áX[[ãåË—ª6­R,^¼×®]ÃÕ«Wñý÷ßÃÓÓ­[·V*?gÎtèÐAáÿçÏŸC$¡Y³fåÖùâÅ tëÖ ãÆ�“Y¥K—.prrBXX˜BýúõkèêêVzg,ƒ¡„š—ˆˆ.@&--�ˆÞë¬\¹’š4iB¨~ýúôúõk‹Å5l­ö‰Dܯ^½z€Ì¥•m{^^“‘‘eff*Ô'‘H¸²-Z´àŽqDD„œ¾¢¢":xð '€öíÛGùùù•ŽîÖ·nÝ"}}}¹õ¯¯^½JË—/'"¢’’Z¸p¡\´rß¾}©aÆrk޽ 344¤k׮ɤϞ=›êÕ«GIII2é›7oV¨¨i²²²hÈ�!€Œ��iéÒ¥ôìÙ3™sððáC>|8uîÜ™²³³êùá‡hÆ åÖ•˜˜HMš4¡‘#GÊĕՀâããåòBBB¨G� Ë1ŒÊSë4ÑO?ýD666äääDŸþ9yzzÒêÕ«¹oéïøñã5mªÖ°´´$Ò××'ccc255%###ÒÓÓ#�@@ädÅb1uíÚ•œ��éôéÓ õÅÅÅ�Ç#]]]244$SSS266&}}}ôõ×_s²³fÍ"�@ S·�� Ú¼y³ÖÛ^÷ïß'oooš>žôôôhÊ”)ry#FŒ �7ªß8ƒQ.<¢Ú¹º€D"Avv6Š‹‹Ñ AðxŸ/1ª­¥?¢££PÓfT æææZé*­¤6cÆ lݺµZ4ŸÏ‡½½½ÂåG•±k×.Œ9²B¹¦M›V(£h)܃ÂÃÃ9gC Ôš¥>U£¤¤/^¼€““SM›òÞQZZªÖ´(˜ƒf0ªH||–,Y‚Ö­[ãüùóZ«�¡œ6mÚ`ýúõÈÈÈ@vv¶Ú«“1 FYjÝZÜ—.]½{÷ààà€Þ½{#''ÎÎÎÈÈÈPº—ouáïï??¿r7(K«V­àïï¯Õõ±sssñÕW_¡yóæ¨W¯¨A,,,àïïüûï¿5mƒÁxÏ©uúÞ½{þ·0¿™™?~ ‘HÄ­øTSŒ1B-y+++œ:uJKÖ¼ÅÌÌ aaa€‹/jµ.†j°•µ †&¨uŸ¸¥ë¦ðùÿ3�Ïç׸sf0 £:©5oÐÙÙÙ˜;w.·;P\\&Mš7nÚµk¸sçŽ;† ++ -Z´@ûöí1tèPN_RRV®\‰¼¼<äææb„ pwwÇš5k  áïï�aÆ©ô¶…‹/¢G�èСƒJíÉÌÌĉ'  ¡££ƒ©S§RRR¡PˆºuëbìØ±¸uë""" ££´iÓF­c÷±PXXˆmÛ¶AOOcÆŒ���’““¹ãܵkWøøøT›=©©©Ø¿?âââ�žžWWW|õÕWå–Qµÿ2 F­yƒÖÑÑ�»»;4hàíFðîîîpwwG�:u ‘H0sæLxxx )) ‹-Âü�Ž;bòäÉèØ±£Ì¸ŸŽŽLLLpèÐ!>|OŸ>ň#о}{?~_|ñBCC+´ëìÙ³HLLÄ_|�^½z©I^ZZŠÔÔT,[¶ [·nåÒE"’““±zõjìÝ»¡¡¡xôè¦OŸ___x{{ãäÉ“j½�ƒåË—cÀ€ˆŽŽÆŒ3pìØ1ìÛ·ýû÷Ç´iÓ0jÔ(ìÙ³§ZlÙ°a\\\°qãF 8?ýôZµj…~ýú!!!AN^ÝþË`0 ZÆòåË 999ɤ¯_¿žP»víH"‘Èäýú므Z·nM%%%2y½zõ"Ô¬Y3:wî‰Åbjܸ1 °°°rm)--¥o¿ý–ˆˆ._¾L(66V­ö“›››\zPP988ÐÎ�;eÒ»víJ¾¾¾jÕ!åÂ… €víÚU©òµ™ÂÂBš={6�;–ŒŒŒhË–-22}ûö¥(Õñn¿©,ýõ zõê•LžP($@ .äÒ+Û ÆÇK­yƒ.‘H„ùóç|||ä>KwïÞpûömìÛ·O&OWWÀÛí}}}!…óçÏcذaåÖ…ž={<ˆ  yóæjÙnll¬0]GG‰‰‰UË–9sæxÛ¥_|¤Ô«WOîSuUú/ƒÁøxy/ôãÇ�‘ŸŸàí ð]ÌÍ͹¿oܸ¡PǧŸ~ÊÝíììн{÷ Çü¼¼¼Ð£G”””`Ïž=– ^«*¶¶¶022’I\[ÿÃÏÏÞÞÞÈÉÉÁÝ»w1hÐ G—––†„„.Vá]¼¼¼°e˹ôÑ£G#::Ze;òóóñðáC@Æ U*£‰þË`0>>jM�Xy¼~ýºÜü²NSÙÛg�:u*]ÿÉ“'! 1jÔ¨JëP„ŽŽâÃOlP¥DEEA"‘À××W&ýܹs 4H¬qãÆri)))xôè‘Z�eB¡�;?Ò¯3¡‰þË`0Ê[•G IDAT>>Þ íææÆý�››+—Ÿ““ÃýݲeK…:ª27544mÚ´AË–-‘ŸŸ�ââb…oB ísñâEXYY¡E‹2é{÷îE§N�`oo�üü|ÁÂÂééé ƒŽŽ¾þúkðù|­Tו+WˆÏçS\\œLz^^¹ººrç(99Y&Á‚äïï/3½éÕ«W@åóù.¹¹¹4kÖ,rpp /// $///úí·ßHOO�\uú/ƒÁ`ðˆÞψ¤¢¢"äççÃÂÂBëuâÑ£Gpuu…¡¡¡ÖëÓ6111HIIAÿþýkÚ*²K,#!!ÎÎÎryÒsÔ¬Y3™Èx±X üüóÏ\D¾¥¥%þüóO,\¸qqq€øøxnýwuÉËË�Çã¦Ô%''C__044Tøv^�ý—Á`¼ŸÔªOÜê```Pm77CCC´nÝúƒpÎ ww÷š6CŽŠìÒÑÑQèœ�ÿ�£w§­]»v ÙÙÙèׯÖ¬YÃ9Ë›7or»£�8q‚sÔ•ÁÄÄDf¾{£F�`eeSSS¥ŸÎ«³ÿ2Œ÷“÷ÖA3*GII ^¼xGGÇš6E©]ÒqbM!ÿÞºu+\]]¹9Ç^^^xõêvíÚ…/^Àßß_£õ2 FUyo?q3*ǦM›Ð£G4mÚ´¦M‘A›v‰D"ÂÌÌL&½°°¥¥¥*ïïÍ`0Õ sÐEEE000¨i3䨭v1 FMÁ4ƒÁ`0µ6­"ñññʨ²ÐãÃE•>¢-Xßc0><Þ‹¥>k"ÂÏ?ÿÌ­¥(_ºªUTTž>} ??¿ê4‘ñ_ÂÂÂTÚ™ŠÇãaôèѰ··×H½êôMPZZ*·Ñ ë{ Ƈ{ƒ®€µk×¢qãÆhÕª•\Þ»[ãøñãøçŸªÓDÆñðð€���Ì/::®®®ré–––«W�>RY ‡ß~û vvvrkz³¾Ç`|x°1èrHIIÁ¤I“pìØ1…o@/^¼@nn®ÌfB¡AAA8}ú´Fßš•côèÑX´h´¢¿2}¤2ÄÅÅáòå˰±±Aÿþý‘™™)³M% �¾'‰�››«Òæ0êÈ2ŒŠaoÐå°jÕ* 8PéÍ®qãÆr7^ XZZâôéÓÕa"£†©L© Í›7Çĉáââ¢TF}oûöíØ¼y³Æe FÅ0­"ÂñãÇ(——žžŽuëÖ!$$Dáæ���8yòdu˜©”ÀÕÕvvv¨[·.¾üò˵ç]âããááá�: K—.ðöö†§§':uê„víÚaÅŠ2ò—/_FÛ¶mñÿì�wX×÷ÿßË‚ %Á.Å*¢‚Ä Q5(~bW0Æ’(vE‚‰1¢ˆ[À[„hŒ "Š#‰hPQб!(ˆì²ô¶åüþà»ócÙ]Ø…¥¨ózž}”{Ï=sæÎ™93·ººº¢_¿~èÛ·/œœœTÞò±>¨‹�Ôšö½ððpŒ3F%Ùˆˆ•dáåå…aÆÁËË #FŒÀСCñÅ_ ''‡‘[¿~=†Š#F`ĈðððÀñãÇk}.ŠˆŠŠB�=о}û·~'1‰D‚S§Náñãǵ*�^½zÁÜÜnnn¶®ñ¹}û6`aa�Þ½{ËäÕµîê�†_þûí ==�LLLäÒ% Íž=›Š‹‹ÉÊÊŠ®^½*'sëÖ-êÖ­[C˜©”‚‚ºxñ"uèÐ�Д)SÕžªSBB%$$�••³¹Ä®]»(!!�RSSeäsrr(""‚ôôô¨ÿþtâÄ Š��'¡PXíq¦L™BÏŸ?¯—s¨‹�Ô–‡ ó5é{�?&ggg•e]\\TÖ-‹)66–�¾¾>effÊlfBTQ�iiiôÉ'Ÿ���åææªe¿ªêÒ¥ yzzÖYWÕ�\’K—.êÝ»w­uäççS¿~ýÈÑÑQ­r�yÞêPPP@ƒ–»G4Qwõ;Š[ ™™™hÛ¶­\º²- +Ó¶m[•öI®O 1hÐ ØÛÛãåË—×_\\ŒK—.áÎ�;L¿£››úöí+7ÂXÍ›7‡““ó)666Lze^½z…~ø_ý56oÞ¬Öö�õEm|¤¼¼Ïž=«Q·±±±BÝ5¡Iß‹ˆˆ€���ʲÞÞÞ*ëÖÒÒBÏž=TØÜ¦M9‡sssŒ7vvv066VY¿:˜˜˜ÀÜÜ\#ºÖ¯_�hD—ºØÛÛcذaøüóÏk­ÃÈÈ–––xðà�Zåó¼ÕÁÐÐ;vDFF†Lº&ê®>hü§\¥¤¤Df)VVV°²²ÂòåË1dÈtèÐANF___á^Ï��¦ªvíÚ…;wî`üøñX²d ôõõ‘‘‘�›7oböìÙøâ‹/0tèP�3** S¦LApp0fΜ©1½u¥6>’ššŠ°°°uüñÇX°`�Ú6iÊ÷ˆG�ŵk×T–ýóÏ?ë|\Ep¹\•^úê‚&î“ÌÌL¼zõJÖÔŽ–-[âܹsuÖ£n]4öy«‹¢óÓTÝi6@+ÁÌÌ �@ažH$Â�°mÛ6äçç3[Jáñx°°°h(S ‰D???ôîÝ{öì‘qô: C‡5j‚ƒƒñèÑ£Z˜Ê¶nÝŠµk×â÷ßW:ϸ±¨��X[[« k‹¦|/>>�;wFË–-5*û.³}ûöoÐTx_Ï»!x§ô•+WpæÌf�ç²²2X[[Ã××ðæÍ£yóæ‰Dpsscö"¶²²Bnn.ÊËËѬY3½•·0\½z5/^,“Ÿžž^ë½…ë“äädDFF")) ÆÆÆèÖ­&Mš¤òœààà` 2¤Ú¯ãœœ<}úæææ8wî\­›ŒÊËË1{ölÄÅÅáúõëJ·™ÔB¡~~~(**BQQ¸\.~ýõW>„¢¢"¢]»vزe 8N�|¤¶ˆÅb™«¢)ßS·yÛÇǧÁ¦>þW¯^ŸÏ‡™™|||‰{÷îÁÒÒ ¨v�‡«W¯"%%]ºtÁ�!C”ÊæççãСCÈÏχH$‚««+,#“ššŠ 88Ý»wÇ©S§T4»»»3rD„èèhÆŒ===DFF‚ÏçC[[óæÍ“)OD¸téþùçèêêÂÛÛ¿üò 2331oÞ<…ÝuwïÞÅåË—ÁápðÙgŸÁÁÁAíóV†H$ÂÑ£GQTT„¼¼<´iÓ\....r¶$&&"&&�Í›7ÇøñãakkËä«r�”!Ö]FFbbbÀçóñá‡bÚ´i¸sç.]ºmmm¹ú¨k]+¤ñº¿ëŸÄÄDÚ¶méëëêÙ³'EFF2ù�€fÏžMZZZ´hÑ"ºvíšLùaÆÑßÿ-§÷Í›7äèèH?ýôýúë¯rùË—/§]»viþ„j�§§' –-[’‹‹ ;vŒ^¼xA±±±Ô©S'255¥cÇŽÕ¨'--�,XP­ �Ç£)S¦Pvv6 …B?~<•••Õ¨ÛÎÎŽ$KÙÙÙÔ¿@–––”““£òùVeÑ¢E”™™Y­ŒP(¤eË–ÑG}DÈÔÔ”ÉËÎΦ¥K—’‰‰  GGG™ÁLµõuyòä -\¸�LFFF4bÄZ´heeeÉÈiÂ÷ÊÊʨcÇŽT\\¬QÙª@VVVÕÊ…††Rll,ó÷ÿýGÁÁÁÔ¢E êÝ»7M�:•N�:EÙÙÙtâÄ jݺ5…††*ÔuäȲ··§sçÎQII =zôˆfÏžM={ö”$ÆãñÈÓÓ“^¼xADDyyyäááAÓ§O—ñ�7nPxx8ÙÚÚR×®])<<œÂÃÃéÌ™3ŒLQQ�7ŽfΜI¥¥¥$‘HhíÚµÔ·o_ÊÏϯ±®þûï? $cccš>}:­X±‚©cÇŽôùçŸSvv6­[·ŽÌÌÌä@ …Bòòò¢o¾ù†²³³éÎ�;diiI_~ù%•••‘¯¯/�8q‚‘Ÿ2e 9::Ò±cÇèðáÃTZZJwïÞ%CCC™ç§*ç­ ±XL£F�¢;wî0iÏž=#sssŠ‹‹cÒ$ ­\¹’œœœèÉ“'DT1�mÒ¤I£Öu’2sæL™:RVw©©©D­[·&www:xð ýúë¯Jë£j]óx<¹ºž={¶L]×Ä; ¥,X°€-š��-“·jÕ*š4i’ÂrgÏž¥yóæ)Ì+//§¼¼<¹t±XLÎÎÎ*²²²Rû¡’~i€îÒ¥‹ÜCôï¿ÿ&¤««[ã(ÌuëÖÑË—/•æóx<úꫯdê7""‚._¾\£�•ôöíÛ©S§NÌès4bĉD5ê©+‹/– ÐR¼¼¼èÚøH}¡®ï)ãäÉ“4mÚ4�ËV¥¶ZÊСC©e˖̃YÊ™3gˆÃáЩS§dÒ/_¾L‡’““eÒ‹ŠŠÈÄÄD.@ïÛ·�ÐîÝ»™´ÿý—Ptt´œ=ýû÷'www…ç@–––TZZʤ‰ÅbêÚµ+)>q 2„ìíí™ú ¢}ûö1ù>>>r:44”´´´dFþÿøã�dhh¨p6À”)S¨U«VrϘO?ý”ÜÝÝå‚^u筌۷o“���\úÆ�etDDiiiÉ\³¤¤$@&L ‰D¢öuª ¥(ª;""ooo²´´¤ȤKë£2êÖuM¼ó /^ mmmËôÿ#44ß~û­ÂrŸþ9ž>}*7âtttäö€“'ObòäÉøðÃU²mÆ xòä‰Ú?uFÌ@«V­dFK€««+Z´h�²²2¬^½ºÚòYYYhß¾½Â<�???„„„È4—;::"..N-;.\???ܽ{—Y‰��µôÔµójã#õ…º¾§ŒððpLžŸ�}ûöA$aÓ¦MXºtiµ7ûöí„K—.ÕxŒ¸¸8lذá­Zƒ[z£×HKKKåÒ²³³•g b —²·ReT^3»OŸ>Ì—³X,Æ„ �••¥–¾†@©/4å{ÇŽØ1cT êÈ*ÂÐÐzzz23EdeeÉ=kBê�Òé?iii –¡PˆeË–¡k×®HMM…¯¯/UÔU>Ÿ4hæÎ�+ó;}ú´Ú>­n«ÌܹsÑ®];DDD¨¸—:„€€æ½*ÊÖÐD°*^ˆvïÞ� .À××\.Û¶mƒ­­-3[:Ÿ¿êšóUÑÔuªUë£6u]íqÕ7õíÄÎΞžžˆŠŠbJ«V­TZÒ®U«Vðôô¬Q®6M}«W¯ÆÕ«WÕ.çëë«‘¥ ¥Ke*û²�Ò¢E äææ27‹48oÞ¼Yap€7n(å¨óçÏǵk×ðÇ 33“&MBLLL½Ï‹­JM_ǪúH}¡©fæððpìÞ½[㲊ÐÖÖF¯^½�ììl¥Ó´RRR`cc£–îgÏžAKK‹Y]:â·¦—�ÊøûûãçŸFbb¢Lxå‡rNNŽÒÍAŽ?;;;ôèу‘mŒv„B!f̘�‡ùóçC,ÃÏϯޖ6•žwåQÖUù÷ßÁápàááË�3Û·oÇŽ;еkW¨qaŸ%K–`ïÞ½µ¾NšDÓuýÞh@TTÒÒÒ0sæL>|¸Ñ¿vçÎ�«òt–Ê´nÝZ-yEÓr^¼x�çÏŸ&MšTmù±cÇâàÁƒX¸p¡JÁ™ˆpäÈlÞ¼Y-;«Âáp°ÿ~$%%áÉ“'¸rå T'½Š�~íW­+‰D‚û÷ïküxM�çÏŸ£¤¤„y0jJ¶:|||�„„…SòÒÓÓ¡­­­ô«±¸¸X.-??ׯ_ǰaØkjllŒ�"..NáþÜ2ã'„B!:OOO™‡¾@ �9æÚµk±qãFþSù 77\.VVV°··Gtt´B¿Ý¾}{½v‹ýóÏ?033“›z¥)”�wu¤¥¥áòåËLÝ@·nÝðÝwß1_ŸFFFܼy;vìÀ–-[ª}#=räzôèCCC…ùDÄÂÊoºb±"‘‡¹Éõôô°nÝ:f#†Õ«W£[·nðòò‚žž^­›X«òÉ'Ÿ€Ãá 77—ù¢#"„„„0ôôôt¼yófff þ_ßÔfîs]‘nÕ¹hÑ"ôïß_¦K¤°°K–,ÁÚµk•–òä nܸ�>}ú0i«W¯‡ÃÁ®]»ddwîÜ œ$‡C:::Ô¼ys222"ÒÕÕ%.—KsæÌadýüüˆËå’®®.�‘‘ééé—Ë¥�;wªV*²yófjÛ¶-YYYѨQ£ÈÍÍ�6nÜHÌ´/róßv$ uéÒ…^¿~­QYU(..&²··§C‡Q\\mß¾�†N7nÜPZÎÓÓ“\\\($$„BBBèÔ©S4mÚ4>|8¥¥¥),óßÿÑÀ�iùòåtúôiZ¿~=íÚµ‹\\\988Ð… ˆˆ(&&†,--iÑ¢EM!!!tñâE:wî™™™ÑÌ™3éîÝ»Œn>ŸOŽŽŽ4uêT £;vÈ;99™ú÷ïO¾¾¾tèÐ!Z±bmذ�Äbq�uCƒ "cccúàƒÈÕÕ•/^Ìäß¾}›<<<ÈÌÌŒ ÈÍÍ�Yס°°�LdmmM­[·&.—K¨S§NODóš=<<ÈÔÔ” ©_¿~´ÿ~JHH�I8p íÝ»WåóVDtt4¹»»ÓòåËiß¾}tþüyZ³f -X°@f*QÅF,ýúõ£éÓ§SDDÒÚµk™©—ª^§ÊudhhH  ˆˆ¥uW›ú¨©®¯_¿^cÝTæ½ ÐDÝU Jï"‘ˆ^¿~]ëyÅ4oÞ<Úºu«Ü¼¾ÔÔTš3g«ôÐiªˆÅbÊÉÉ‘ÙaI Pff& f¡‰w‰›7oÒСC5.«™™™IaaaK%%%ÕÊK4QÅ<üÄÄD***RéX999ôàÁÆO¨�ºæihXË{Err2~ÿýwðx^¿~Í,›)Ýʳòh`Yˆ HII�@ @ëÖ­Ñ«W/|üñÇ�mÚ;‡&ëš Ð,,ï9qqqèÛ·¯JÓÕÔ‘eaa©ì]¦"Ò=ª#55µþ a©7Þ×kìææ¦rÀUG–……¥n°wZ ¶nݪt1�Ê ýõW£®ÇÌR;ØkÌÂÂÒat lÚ´  w¼Šˆˆ�ÙÉÇÇÇgΜÁÍ›7ÒD–:¢î5ŽŒŒd¯1 K½ÃöAWCFF³¡¢%ÚÒÒÒPPPÀ,ÈT `ñööFttt£¯óÍR3Må …B¨4ÂSY–·ö º6l؀ѣG+}[XXÈ<¸�ŠmîÌÌÌÝ&²Ô‘¦r�÷îÝ‹�;wj\–¥ñ‰Dxøð!Þ¼yÓØ¦°¼¥°Z D„3gÎÀËËK./;;[¶lAhh($‰\¾——Ξ=Ûf¾•<þŽŽŽpqqAß¾}Ñ¿¸¹¹ÁÕÕ½{÷ƺuëdä¯]»†Þ½{ÃÕÕýúõCß¾}áää¤Ö&ŠhJ×8<<œÙ DS²‰‰‰ðòò°aÃàåå…#F`èСøâ‹/ ¹õë×cèС1bFŒ?~¼Ö碈¨¨(ôèÑíÛ·¯·m5ÁíÛ·áàà ôîÝ»Öz¢¢¢0xð`Ü»wÎÎÎ× µûê« IDAT•õOuõ ‘H˜ FXꙺ®?ú®’žžN&&&r鉄fÏžMÅÅÅdeeEW¯^•“¹uëuëÖ­!Ì|+)..¦„„JHH N�:1PìÚµ‹(55UF>''‡"""HOO�ú÷ïO'Nœ øøx …u²£©\ãÇ�“³³³Æe‰*ÖŽ��%¤¯¯/³¾¸‰DBiiiôÉ'Ÿ���åææªe¿ªêÒ¥ yzzÖIÏÇ5d‘b hðàÁµ¾¾�?&JJJ¢ììlâr¹4uêT&¿¾í×ÊêáÒ¥K€z÷îÝH–½?°K}*!33Sá’lÏž=èQ£�žžŽçÏŸÃÊÊJN¦mÛ¶ÈÌÌl3œââb\ºt wîÜaúAÝÜÜзo_•·_lÞ¼9œœœ˜ÿK±±±aÒ+óêÕ+,_¾_ý56oÞ¬±Mï›Ê5®Ïmµ´´Ð³gO6·iÓFN†ÃáÀÜÜãÆ�ƒ��Œ��UÖ¯&&&077¯³ž 6`ÿþý°H1†††èر#222jUþìÙ³033c–Â}öì™ÌþíëׯÇ�4bk}¢¬ìíí1lØ0…ûw³h6@+¡¤¤DfoZ)VVV°²²ÂòåË1dÈ…k®êëë+Ü?ôm†ˆ°k×.ܹsãÇ�Ç’%K ¯¯�ŒŒ ܼy³gÏÆ_|�¡C‡jô¸QQQøòË/ŒY³fiTwS¸ÆD„£G�âÚµk•­ \.·Þ÷¸®ë ºÌÌL¼zõJCÖ(§.v>}úT& [XX0ÿo(û5…¢zhÙ²%Î�;×Ö¼°}ÐJ033“é§«ŒH$Â�0}útäççƒÇãÉäóx<™›òmG"‘`ñâÅ066Æž={0hÐ BKK :tÀèÑ£±k×.ܺu Û¶mÓÈ1‰[¶lÁÔ©Sqüøq�g i\ãøøxtîÜ-[¶Ô¨ì»ÊöíÛ›üD¤4Àoß¾]á˜E¼Ó_ÐB¡K–,Aaa!ŠŠŠÀårñ믿Þ¼yƒ   ¡°°íÚµÃÖ­[™²VVVÈÍÍEyy9š5k&£7>>yyy1bV¯^�Å‹Ëä§§§£cÇŽõ‚*’��€Ã‡#==ÙÙÙh×®ºt邯¿þZ¥�U‚ƒƒ1dÈ�j¿ŽsrrðôéS˜››ãܹsujþ*//ÇìÙ³‡ëׯ£sçÎÕÊ_¹rgΜ�žž ¬¬ ÖÖÖðõõPq­ƒƒƒÑ¼ysˆD"¸¹¹aäÈ‘Mâ×góv]yþü9®^½ >Ÿ333fø½{÷`ii‰T»A�ÇÃÕ«W‘’’‚.]º`È�!JeóóóqèÐ!äççC$ÁÕÕƒfòSSSqàÀ£{÷î8uêÀÈÈîîî2ºˆÑÑÑxðàˆ­[·ÆÄ‰å®qm¨NwVVâããñìÙ3äää06º¸¸ ¬¬Leû!‰pôèQ!//mÚ´—Ë…‹‹‹\Lbb"bbb мysŒ?¶¶¶L~Mu]�‘‘‘àóùÐÖÖÆ¼yóTLWŒ‰‰ŸÏLJ~ˆiÓ¦áÎ�;¸té´µµñÙgŸÁÁÁAa}^ºt ÿüótuuáíí ‡ƒ_~ù™™™˜7ožÂ.¦÷…w:@s8áôéÓxñâLMM™<---"""¹¹¹ptt”)««« gggܺu Ÿ|ò‰Lž­­-lll°{÷nØÚÚÂÄÄD&?66#FŒ¨¿Sƒû÷ïÃÙÙÍš5Ãжm[\»v S¦LApp0þøãxzz*-Ÿžž>Ÿ_mpæóùðóóCHHLLLàããƒÁƒCGGGm{y<ÆŒƒ?ÿü–––2×L-Z´@§N�ðÝwß¡¸¸={ö”yè5kÖ "‘7nÄ‚ ˜ùÃ�}�ËËËqþüylÚ´I£²š¢¼¼™™™Ø´i¬¬¬píÚ5Œ9sçÎŵk×àââ‚åË—cΜ9reûí7¬]»ëÖ­ÃâÅ‹‘––deeÉí…Ëçó1eÊìØ±ÈÏÏÇØ±cqôèQüüóÏàp8ÈÊÊÂÇ [[[ˆÅb€\“|qq1¦N�Š?üÛ¶mC³fͰ~ýz 8ÑÑÑ022ªu}Ô¤[j—tžºÔF‘H¤²ýŠ�H$?~<~øáôêÕ @ÅËSÿþýqøða&€‚‚‚‰ß~û VVV(((À¬Y³ðÕW_aðàÁ*ÕuuˆÅbdff"$$­[·f´P(ÄË—/;;;p¹\èèè`Á‚øï¿ÿðé§ŸâÈ‘#>|8£K$aôèÑhÓ¦ Ö¬Yƒ´´4|òÉ'èׯ~þùg,\¸÷îÝ{¯ô{1Š{ñâÅ€LMMåò¼¼¼9::Êå�={–æÍ›§Pgyy9åååÉ¥‹Åbrvv¦œœ•íó÷÷'+++µ5êgFI'''3ékÖ¬aêD,+-¿nÝ:zùò¥Ò|�G_}õegg3itùòe•ν[·nŒ}Û·o§N�:Q‡˜´#FTk_e,XÀŒV®lѪU«hÒ¤Ireê+âäÉ“4mÚ4�ËVE ²²²ªV.44”bccåÒ‡J-[¶¤/^ȤŸ9s†Ð©S§dÒ/_¾LGÆßˆˆŠŠŠÈÄÄDn÷¾}ûíÞ½›Iû÷ß EGGËÈöïߟÜÝÝ•žC@@YZZRII “&‹©k×®¤´\UfΜ)7zYª»´´´ZÝsæÌ!…zk²_·oß&;;;¹ô�7R\\ówDDiiiÉÔ{RR  &‘zuM¤¸ˆˆ|||¦{{{“¥¥%8p@&ýÓO?•;ïÐÐPÒÒÒ"�@À¤ýøã�dhh(“ö>ó^ôAW÷%W]ÞçŸާOŸ*Í©££ƒ>ø@.ýäÉ“˜üðC•íÛ°až|øpØØØ`Á‚2ë•Ïš5 ÎÎÎ2þT ¬³¶¶–Óß·o_Œ9...Lš�� ttt��� ²�|>›6m€˜­(�ŠÖ2www;vLe] ©»&ôôôðàÁ¬Zµ éééLúÈ‘#™n"B@@€\½[[[ÃßߟéîÑT]+\ ÚÚÚxñâ…Ü~á�;wÆëׯeÒâããa`` Ó:Õ¹sg"))Ie[ÞeÞé&îºÂáp°gϬ\¹?ýô“Ì�©ˆÔÔTÄÆÆ"44´�,¬™fÍšaΜ9xñâŽ;†{÷îáÅ‹2A¹´´Tm½<K–,Á¦M›äš¡?þøcäææª­sÏž=øê«¯¿ÿþ;ÜÜÜ ‰ðý÷ßÃÅÅýû÷¯¶¼……&Nœˆððp„……Áßß8x𠜜œ®µÝX×X  ))If�oMÈÖuF/s8888àÈ‘#ÈÈÈ@ûöí‘™™‰Ç�+Ô§H¿�� N�:…ÒÒRÄÄÄ >>ž øEEE*ÛsïÞ=¦9¹ê`Ee/aµÑ]y¬Š&t×D—.]0cÆ ¬X±+V¬€­­-† ‚€€f Izz:2220`À™²ºººØ°aó·¦êº:Úµk}}}™4.—+§ßÚÚÈËËc¦öI›þkwò¾ð^|A×…öíÛ#((H¥Œâââ°aÆ&µ÷ãÇ�áááKKKlܸvvvX·n–-[¦RyEÁ;;;[ip*ú.•½aW‡¥¥%óÿ>}ú0_Îb±'NDVVV�:¤_É|>ûöíƒH$¦M›°téR¥×¥1®ñ±cÇ0fÌ•¶nTGV†††ÐÓÓC^^^µrYYYrÖš�^éÔ¡´´4PK�P(IJeËеkW¤¦¦Â××���jêâóù€Aƒaîܹ2¿Ó§O«ä?�¡»&8vïÞ� .À××\.Û¶mƒ­­-®]»†?þø™™™˜4ibbbªXcggOOODEE1/­Zµ‚››[µÇªïk\•ˆˆìÚµK%ÙððpìÞ½»ÖÇÒÖÖF¯^½�ììl¥Ó´RRR`cc£–îgÏžAKK‹é2‘Ž®ée 2K–,ÁÞ½{‘˜˜(Ó^ù�ž““£psi×�­­-zôèÁÈr¹\�¾(WÖ­©…rYû•ñï¿ÿ‚ÃáÀÃÀ`̘1ؾ};vìØ�®]»@µÝUàïï�Ÿþ¹Vu­i„B!f̘�‡ùóçC,ÃÏϯI/ÛмZú5Wuþ¤D"Áýû÷Ã$æÎ�[«é3•CPÄíÛ·ñòåK€···L`«éF–2vìX> §À¥§§C[[[aŸ;PÑ�_•üü|\¿~Æ cî1ccc 8qqq çÈŒW …8tè<==e†@ �9æš5k™àŸ››ËøuçÎ�aoo�èèh¬ZµJÎÞíÛ·+q® •u+ò?UuWg¿2ÒÒÒpùòelܸ‘IëÖ­¾ûî;æëÓÈȃÆÝ»wN ŬY³ÔªëúæŸþ�™™3œEž÷¢‰[:…&77ÙÙÙ*IHHóIOOÇ›7oeXZZªý«¼L¦"zöìÉÌ Ž‰‰aÞ’ù|>"##™~ŸÛ·o+í‡îÑ£ž={†›7obÉ’%زeKµSŸŽ9‚=zTÛÄMD‰D‰D2oîb±"‘HæèééaýúõÌuZ½z5~ûí7+]ðáÓO?…««+€Š~éÊS;š* ð“Êjbîó¬Y³àáá�E‹ÉõbÉ’%Õ¾ø$‡C:::Ô¼ys222"ÒÕÕ%.—KsæÌadýüüˆËå’®®.�‘‘ééé—Ë¥�;w*=Æ… 9r¤Z[‰DB]ºt¡ÌÌL•e_¿~­‘c“¿¿?ÙÛÛSxx8ÅÅÅÑöíÛiøðátãÆ ¥å<==ÉÅÅ…BBB($$„N�:EÓ¦M£áÇSzzºÂ2ÿý÷ 8�–/_N§OŸ¦õë×Ó®]»ÈÅÅ…�ƒƒ]¸p�ˆˆbbbÈÒÒ’-ZDÑÑÑB/^¤sçΑ™™Íœ9“îÞ½KDD|>ŸœœœhêÔ©F;vì�;vrr2}öÙgäëëK‡¢+VІ Tš®wûömòðð 333244¤ÈLgLNN¦þýû+Ô}÷î]2dµlÙ’ ÉÑÑ‘¼½½eî>ŸOŽŽŽÕÚ_•èèhrww§åË—Ó¾}ûèüùó´fÍZ°`�Ì”/¢Š�:úõëGÓ§O§ˆˆ ¤µk×2÷¶ªu­¬*§�››]»v�nܸAdjjJ†††Ô¯_?Ú¿?%$$Ȥ8�öîÝKDD………4xð`²°° kkkjݺ5q¹\@�:u¢øøøëæ]ç½ ÐDssrrdvô”™™I�€JKKk .o+¥¥¥ôêÕ+*((¨Uù‚‚š7omݺUnŽbjj*Í™3‡‚ƒƒUž³Üäçç7¹ëyóæM:t¨JvIe5Mff&EFFRXXÅÆÆÊÌV„4@UÌ{OLL¤¢¢"•Ž•““C<`üâÁƒ”’’B|>_ÆW$ ¥¤¤PBBåçç3éùùùr/މ„’““)--­Úc JJJ¢òòr•lU‡ºèVÕ~)ùùùL NMM¥Û·o+œŸ_™ÜÜ\¥ö©S×õEyy9õìٓΞ=+“.‹éåË—4yòd²°°hRϓƀCĶ%°¨Nrr2~ÿýwðx"##qâĉz²š…………å]… Ðÿ‡©©)† KKËZ•ÿóÏ?‘””KKK :­[·FçÎ�abbÂl}È¢*ìRŸU¨ºÁ¼ª$%%:vìøàƒðèÑ#…Bf%&Ua¿ 5„t½—Ê+?iii±Á™…………¥V4É/è¿ÿþçÎ�Ãýû÷Q^^øøøÀÉÉ @ÅN?~~~(,,Daa!ìííñÃ?àÆ�8|ø0ÒÒÒЦMx{{+]Ø?33G�ÅÇ‘�� [[[Ìž=[m[óòò€ÄÄDÀÇ1kÖ,ÀôéÓÑ»woÀÝ»wqúôiŸ�aÆa„ µþÒgaaaay;hR_ÐB¡3fÌÀ§Ÿ~ŠgÏžá§Ÿ~Bxx88œ��™‘Õ|ð®\¹‚cÇŽáÖ­[X»v-öïß�iÓ¦!00@¿~ý-wœ°°0X[[cûöí=z46oÞ {{{Œ1©©©jÙ¬­­ ;;;´nÝPVV;;;ØÙÙÁØØ‰‹/†££#ÒÓÓˆß~û }úô�¯¯/úô郔”}†††8qâþøãŸ´µµ ­X±B-Û×®]KÈÊÊJ&}ëÖ­€z÷î-sNDD?ÿü3 ^½zQyy¹Lž‡‡ ºxñ"‰D"²°° ¡–m,,,,,oMæ º´´”ùB6l˜\_®§§'àäÉ“LºŽŽàåË—X¼x±Œ>éhìª_Äß~û-à³Ï>c¾z¥´hÑB£û� …B,[¶Œ9^Õfé�qäÈ™<鹕––ÂÝÝ\.ýõ._¾Œ &hÌF–¦I“éƒ~ðàÊËË�?FXX˜Lþ£G�iiire�ŒŒ`nn.“& †•ç3áßÿ´oß^sÆ+áÑ£G(**Pü«hæÿ·nÝ—_~)§£gÏžL9sss¹ódaaaay7i2ZÈ€Š`Vu>²¥¥%¼½½akk+W¶Y³f* šâóùÌhkéj}’••Um~åV‚ׯ_+”166Ö¨M,,,,,oM&@÷èуù¿™™†®ñc˜››ÃÈÈ(((иþªtëÖ�ùAAˆHæE"??Ÿù÷îÝê`Gk³°°°¼Ÿ4™>hcccŒ;@ꨉDNæ—_~ÁòåËk} ‡ƒqãÆîܹÃ|MK … �[[Ú´iÃô3ߺuK.ÿöíÛ*ZF�­±ã²°°°°¼ý4™ »v킵µ5’““ñÝwß1ýÇååå8uê¾ÿþ{L˜0D‘HÄäKÿ–H$2ÿ¯š!!!°²²ÂãÇ�±wï^æØEEE˜}˜s‰DÌhðÀÀ@ÍW8 K“¤ÉèÊ…BäææÂÌ̬Þúb Ááp``` bÊ–®®.ôôôмyshkk¶›¾´´EEE055Õ¨^–w‹& YXXXXXÞWšT4 Kl€faaaaai‚°š……………¥ Âh–& YXš ÏŸ?o°c©»Å*KÃÓPþÀúBÓ‚ Ð,,M"ÂÖ­[d)Z)ýõ.]ºÔ`ÇcQ�†öÖšl€faiBlÚ´ °··o°cúøøàÌ™3¸yófƒ“E5ÚX_hZ°ó YXêHqq1tuuë¼—xFFfÍš…Ó§O7ø&)|>ÞÞÞˆŽŽ–;vaa! Ôž·™·ÝX_h:°_Ð,,u -- þþþY+}Æ =zt£ì`fjj 333DGGËå�={GŽip›ÞFÞ`}¡éÀ~A¿£ìÛ·‰‰‰j—Ó××dž êÁ¢Ú…eË–!;;}úôÁ‰'˜<‰D‚3gΠ[·nøøã�Ô®üü|xzzâ×_………E�t¬¬¬pûöm|øá‡2y ¸rå 0`ÀDDD ??‰³fÍ‚žž^�Ž-å÷ßÇÕ«W±cǹ<___Œ9C‡•Iþü9ÆŒmmmæ'ÝàE$aôèÑXºt)#íÚ5,^¼˜Yk^,£¬¬ ±±±022RÉΨ¨(|ÿý÷àñx¬?Ô“?ÔÆXêzO¸pá6¶ ÆÀ� uîÜ™¾þúkÚ°amÙ²…>øà@èÛo¿¥Í›7ÓÂ… ÉÕÕ•�––‰D¢Æ6_�@@]ºt!OOO™ôK—.êÝ»wƒÛôå—_ÒÉ“'5¢+==�LLLäÒÅb1-_¾œRRR¨yóæ´téRÊÈÈ "¢)S¦Ðúõëê{óæ …Bµl¸uëuëÖMa^qq1999�Ç“KOHH „„²²²b|k×®]”��@©©©2ò999AzzzÔ¿:qâÅÇÇ«m+ëòþðÕW_)ô‡†òÍóÞèÀÀ@9rdc›Ñ`ØÙÙÑgŸ}F………2éíÛ·g¢ÉÉÉLºD"!@YYY m®JxxxÈ=�ß¼yCÆ £ÐÐеåæÍ›dooO‰D#úþùçêÒ¥‹\úÿýG×®]£ÈÈHÒÒÒ¢””&ÏËË‹V¬X¡PŸ···Œ,Qyy9Íž=[© ¯^½¢-Z(Íÿé§Ÿè›o¾QšoggÇøVll¬B™ääd²´´¤yóæ©4ªÂúƒ¬š1¨$ÛIDAT?Œ9R¡?4†/°h†÷"@K$êÑ£Ç{ [µjEçÏŸ—KW ‰*¾JtuuåÒ› C† ‘{ 7&L ={öhLߟþINNNJó—.]Jÿûßÿ˜¿Åb1µnÝš¢¢¢T>Æõë×ÉÃÃCi¾@ CCC¥ùdff¦ô®¦}öìYjÑ¢íÚµKe›«ƒõYhÓ¦ �;wN%ýõí ,šá½$¶k×.Ü»w¯±Íh0Äb1x<\]]Õ*gbb;;;deeÕ“eïååå8þ<ÆŽ«1�fffJóÿüóOxxx0_»v ¥¥¥8p Ì"÷ïßÇŽ;°oß>&M$aýúõX¹r%„B!6mÚR0ô„ÇãUÛwjhhˆ>}ú <<\­s#"lÙ²S§NÅñãÇ1sæLµÊ7ušª?4E_`QÍnv\ÒÓÓ±~ýz¢  ß|ó ììì>Ÿ�aÆa„ 2#ÿþûoœ;w÷ïßGyy9lllàãã'''“îÃÂÂpôèQÀÝ»w1kÖ,@ëÖ­±råJûNûùù¡¨¨EEEàr¹øõ×_oÞ¼APPŠŠŠPXXˆvíÚaëÖ­jÙ,‰àçç‡ÂÂBÂÞÞ?üðnܸ�Ç#-- mÚ´�··7ÜÜÜê\—¥¥¥˜>}:>øàµËúøø¨áñx¸zõ*RRRÐ¥K 2DNF  22|>ÚÚÚ˜7ožL~~~>:„üü|ˆD"¸ººbðàÁ �GD¸téþùçèêêÂÛÛ¿üò 2331oÞ�áǃÃá`ûöí áܹs;v,Æ��éÓ§´µµ€ØØX¬Y³}úôQXéééèØ±£Â<)nnn¸páüüüª•“R^^ŽÙ³g#..ׯ_GçÎ�U*W•·Ñ®\¹‚3gÎ0ƒ¶ÊÊÊ`mm ___þŒæÍ›C$ÁÍÍ #GŽP;ðôôdüaãÆ�MÎXjAc~¾W&##ƒÈÐÐ�PXX 0€>LÆÆÆ€8@Dý'Ó§O'4aÂJMM¥ììlZ¸p!q8ZµjÅÅÅQhh(}ôÑG€ºwïN¡¡¡Jááá̱…B!-[¶Œ‘355eò²³³iéÒ¥dbbBÈÑÑQm›«ê9r$­Y³†¾þúkJLL¤ÄÄDjÖ¬P¹‰ª¶T×ÄÝT8räÙÛÛÓ¹s稤¤„=zD³gϦž={Ê4ifggÓºuëÈÌÌLn@ �Ç#OOOzñâåååÑ�!Chúôérý„B¡�¼¼¼è›o¾¡ììlºsçYZZÒ—_~IeeeäëëK'Nœ`ä.\HS¦LQh{m}‰ˆhذaô÷ßË錋‹£N�:ÉØýÛo¿ÑäÉ“Éßߟ?~̤K$  °°0åååÔ¦M*//Wh7ÑòåËkl~¾rå (ìk­ÚÄ���Mýû÷'diiI999ÕêVF}ùƒ‡‡G�þÀãñjô‡ (ô‡ÄÄDÚ¶mêÙ³'EFF2ù�€fÏžMZZZ´hÑ"ºvíšLùºúCcú‹fh2ZЇ‡ ºxñ"‰D"²°° ADDAAA€lmmI,3eÅb1999ºsç“îââÂÆêX¼x±ÜCUŠ———‡ªª6ùùùêС-X°@F‡ƒƒ qãÆ©VQµDSÚßߟ¬¬¬ÔþU®E\¾|™8ŽœmEEEdbb¢°ÏÑÇÇGî�¼oß>@»wïfÒþý÷_@ÑÑÑ2²¡¡¡¤¥¥E�€IûñÇÉÐÐP&MʨQ£hÙ²eÕžGm|éìÙ³4oÞ<9y‘HDré�@nÄ}yy9YXXPnn®Ì„›7o2}Ž‘‘‘r´Äb19;;×Dïß¿OÖKå½}ûvêÔ©uèÐ�I1b„Ìýª �é•ë¢:øâ‹/ªõ‡… Ò××§ììl™¼U«VѤI“–««?4¦/°h†&×­££ ¢™ÖÝÝ\.ýõ._¾Œ & ´´AAA€aÆAKëÿŸ‚––<=='Ož¬õ±k›§Ìæªr/_¾ÄâÅ‹etXZZx{«ß°ažÿüs<}ú2é\.WáêM&&&r«U‰D"˜››ã—_~‘±ÑÌÌ ÚÚÚøùçŸQVVmmÙÞ­“'ObòäÉrsn«Ò¢E ¨qœÂÂ… áç燻wïÂÜ܉���jËUå]ð‡E‹A[[ÅÅÅ cÒ‹‹‹Š€€…åêêMÅXjO“郮JÏž=™þfsssæ&¿{÷.ÊËË�?–qxxô耊}e6WÅÈÈH.OZN,ׯ‘M˜ÌÌL<~ü˜'PuVT²±±Á©S§PZZŠ˜˜ÄÇÇ3ƒaŠŠŠäd�9‚¼¼<æAVXX ûLù|~½ôÓs8ìÙ³+W®ÄO?ý]]]µu4oÞW®\�X,FóæÍ™ôN�:á�?þÉ-d‘ššŠØØX„††Ö¨_´ø|~µr{öìÁW_} bÑ 777ˆD"|ÿý÷èÓ§úõëW㱚‚?HÏ·.þ`aa�‰'"<<aaað÷÷‡��<'''¥ëlWö‡mÛ¶©½ISñ–ÚÓä¾ ¥({#­|3µhÑ–––2?oooDFFâ‡~h(Sjúª’Ò¬Y³FYα©#}©Ò×ׯ³.¡PˆeË–¡k×®HMM…¯¯/åÜÀܹsÑ®];DDD¨h 9tèЮ];9ù>ø%%%u¶QíÛ·GPPP�vjÖ¬™ÌYŠ®®®Â‡|\\6lØ ’O¨Ù×¥-BЧOæËY,c„ *}u½Kþðí·ß¨fûöíƒH$¦M›dVYS„Ô._¾¬ê©ÊÐ|�¥ö4Ù/heÒ£Gæÿfff>|x­ôOž<óçÏGïÞ½U’/++«Qæ} º«W¯ÆÕ«WÕ.çëë‹Ñ£G+̳µµäååÕÅ4À’%K°wï^$&&Ê4…R¥)%999hÑ¢„B!f̘�‡ùóçC,ÃÏÏO©�­Zµª“�5ùR«V­˜®š†`òäÉ*ËJÏ»U«Vjcþüù¸víþøãdffbÒ¤Iˆ‰‰©vC‰Æö>Ÿ¯1°³³ƒ§§'¢¢¢°iÓ&˜šš¢U«V*ÍÚhHh_`Q�& •allŒ±cÇâØ±c¸xñ"$‰L?4üòË/xòä ÓW-í¯©ü`ü÷ßå´—ö]Umf–H$¸ÿ¾ÆÏåmfîܹðññQ»\ëÖ­•æcàÀ�ˆ‹‹ɽðÀÌ̬Æc…B:tžžž2c�@À¼õÀš5k‚þùfffrÓr”ѦM¼zõªZ™wÕ—²²² ££Ãô?ª ‡ÃÁþýûqïÞ=<}úW®\A`` s�*â]ò@TTÒÒÒ0sæL>|ø­~©¯­/°¨N“ ÐD±XÌMé‚ûZZZrXºðHrr2¾ûî;¬Y³\.ååå8wî¾ÿþ{ÄÄÄ0ò}ûöÅåË—ñàÁ”••�Ïç###]»v•ÑûÉ'Ÿrss‘���–-[‚ˆÂÜHéééxóæ LMM¡¥¥¥’ÍÒs“>¬‰ˆ‘ãp82:*çU=ïºÔkå¿¥H74 öñLLLή+;wƒNž<‰Q£F1é§N�ÂãÇ�ѲeK¹2eeeÈÏÏgþÖÒÒ‚¶¶6rsseä.\¸{{{¢¤¤„9_kkk̘1ùùùhÑ¢ttt```€îÝ»£[·nrQìÝ»·ÚóP×—êº5aCqûöm|öÙgŒ½•ý«²o‰ÅbˆD"p8FVOOëׯǘ1cT´ÂtëÖ ^^^ÐÓÓSèoƒ?8::Öèðé§ŸÂÕÕñññ°°°¨uë_S¡ª/°Ô =l\ ÄårIOO�ŒŒŒ¨y󿤭­­t­á’’ &[[[úè£�ÈÅÅ…ÌÍÍÉËË‹=z$';uêTjÖ¬}ôÑGdnnNÇ�W¨wóæÍÔ¶m[²²²¢Q£F‘››mܸ‘˜é"(22Re›KJJHKK‹tuuÉÐÐ�ôõõI[[›BCCéáǤ¥¥%£ƒËåÊÌ—¬ €´µµI÷ÿµwÿ.©…aÀ9˜–HËq‘ ú±En 6´F£­ MÒQTSa@-QIöci¨�¨EJ‰:$*HÃÁ°ïî½B]«Û½ÙyÕïgó�ï{<_xDÎs^‡MMMp»Ýp¹\p:�°Ûí°Ùl˜žžþ’ñ¾ÂÕÕ099‰­­-ÌÍÍaee¥Ô.ç÷û±»»‹³³3 B×u¸\.ôõõ•zIc±ZZZ …�F1??�½½=ììì@×uŒ��áââðøøˆ@ ŸÏ‡ŽŽx<hšA[[Ž��_Ì/™L¢¡¡¦i¾{ŸÉRµƒX^^.½¾¼¼„ÍfƒÝnGccc)[‡š¦a||¼ôÞ‰‰ hš‡Ã—Ë·Û §Ó MÓ‰DÞ³Vòü\´GD°¶¶ö©ï]E¯³@_O™ý? …>l˜7M÷÷÷®ÖT,‘Íf‘N§KŸ™Ëå�N§‘Ëå`š&›ó¿A6›E"‘(õÎ& ¤R)d2™¿ê§}~~F*•B<G>Ÿ/mÏçó¥  twwc{{ûűÅb†a  Âçóýq½{{{ßü‘÷úsj%K¦iÂãñàîîÎ’ñk!¿Ç«–kþ«³P/j¢@ý«h4 ¿ßÿæþL&�a/¶¯¯¯#TzzJY]]ÅÐÐ�ÕÓ¨(æáïÔCT l›Ñwèêê’››1 £ìþ““imm¯×ûbûÈȈ<==ÉùùùwLÓrÅbQ–––daaÁê©Tóð±zÉ‚ l@™¥LˆêÈáá¡ÌÌÌÈðð°ô÷÷‹®ëb†lnnÊéé©D"‘² \__Ëèè¨Äb±²Ovª%ápXš››% Y=•ŠcÞWOY° 4‘ü¼9�K*•’\.'�Gzzz¤½½ýÝ㎎ŽdccC«ºeæ=²¿¿/³³³5{ޝ1åÕc¬ÄMôŸnooÅëõ~úQŒÕ"™LJgg§ÕÓ¨µœfá{±@)ˆ7‰)ˆšˆˆHA,ÐDDD b�&""R 4‘‚X ‰ˆˆÄMDD¤ O¯‡ejjªs!""¢_ø """ñ/n"""±@)ˆšˆˆHA,ÐDDD b�&""R 4‘‚~!pÕ•Á¥2=IEND®B`‚python-pot-0.9.7+dfsg/docs/source/_templates/000077500000000000000000000000001525521106200211535ustar00rootroot00000000000000python-pot-0.9.7+dfsg/docs/source/_templates/module.rst000066400000000000000000000016231525521106200231740ustar00rootroot00000000000000{{ fullname }} {{ underline }} .. automodule:: {{ fullname }} :members: {% block functions %} {% if functions %} Functions --------- {% for item in functions %} .. autofunction:: {{ item }} .. include:: backreferences/{{fullname}}.{{item}}.examples .. raw:: html
{%- endfor %} {% endif %} {% endblock %} {% block classes %} {% if classes %} Classes ------- {% for item in classes %} .. autoclass:: {{ item }} :members: .. include:: backreferences/{{fullname}}.{{item}}.examples .. raw:: html
{%- endfor %} {% endif %} {% endblock %} {% block exceptions %} {% if exceptions %} Exceptions ---------- .. autosummary:: {% for item in exceptions %} {{ item }} {%- endfor %} {% endif %} {% endblock %}python-pot-0.9.7+dfsg/docs/source/_templates/versions.html000066400000000000000000000025731525521106200237200ustar00rootroot00000000000000
Python Optimal Transport {{ version }}
Versions: Release Development Code
python-pot-0.9.7+dfsg/docs/source/all.rst000066400000000000000000000010221525521106200203130ustar00rootroot00000000000000 .. _sphx_glr_api_reference: API and modules =============== .. currentmodule:: ot :py:mod:`ot`: .. autosummary:: :toctree: gen_modules/ :template: module.rst backend batch bregman bsp coot da datasets dr factored gaussian gmm gnn gromov lowrank lp mapping optim partial plot regpath sliced sgot smooth stochastic unbalanced utils weak Main :py:mod:`ot` functions --------------------------- .. automodule:: ot :members: python-pot-0.9.7+dfsg/docs/source/code_of_conduct.rst000066400000000000000000000002031525521106200226600ustar00rootroot00000000000000Code of conduct =============== .. include:: ../../.github/CODE_OF_CONDUCT.md :parser: myst_parser.sphinx_ :start-line: 2 python-pot-0.9.7+dfsg/docs/source/conf.py000066400000000000000000000265311525521106200203240ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # POT documentation build configuration file, created by # sphinx-quickstart on Mon Oct 24 11:10:10 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os import re import datetime # !!!! allow readthedoc compilation try: from unittest.mock import MagicMock except ImportError: from mock import Mock as MagicMock # check whether in the source directory... # #!!! This should be commented when executing sphinx-gallery class Mock(MagicMock): @classmethod def __getattr__(cls, name): return MagicMock() MOCK_MODULES = ["cupy"] # 'autograd.numpy','pymanopt.manifolds','pymanopt.solvers', sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) # !!!! # 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. sys.path.insert(0, os.path.abspath(".")) sys.path.insert(0, os.path.abspath("..")) sys.path.insert(0, os.path.abspath("../..")) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # 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.autosummary", "sphinx.ext.doctest", "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.coverage", "sphinx.ext.mathjax", "sphinx.ext.ifconfig", "sphinx.ext.viewcode", "sphinx.ext.napoleon", "sphinx_gallery.gen_gallery", "myst_parser", "sphinxcontrib.jquery", ] autosummary_generate = True napoleon_numpy_docstring = True # Add any paths that contain templates here, relative to this directory. 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' # The encoding of source files. source_encoding = "utf-8-sig" # The master toctree document. master_doc = "index" # General information about the project. project = "POT Python Optimal Transport" copyright = "2016-{}, POT Contributors".format(datetime.datetime.now().year) author = "Rémi Flamary, POT Contributors" # 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. # __version__ = re.search( r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]', # It excludes inline comment too open("../../ot/__init__.py").read(), ).group(1) # The short X.Y version. version = __version__ # The full version, including alpha/beta/rc tags. release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # 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 = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # 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 # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "default" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- 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 = "sphinx_rtd_theme" # 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 = {"version_selector": True} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = "_static/images/logo_dark.svg" # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # 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_css_files = ["css/custom.css"] # 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 = [] # 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' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is 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 = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # 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', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' # 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'} # 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' # Output file base name for HTML help builder. htmlhelp_basename = "POTdoc" # -- 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', } # 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, "POT.tex", "POT Python Optimal Transport library", author, "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # 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, "pot", "POT Python Optimal Transport", [author], 1)] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "POT", "POT Python Optimal Transport", author, "POT", "Python Optimal Transport", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False autodoc_default_options = {"autosummary": True, "autosummary_imported_members": True} # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "numpy": ("https://numpy.org/doc/stable/", None), "scipy": ("http://docs.scipy.org/doc/scipy/reference/", None), "matplotlib": ("http://matplotlib.org/", None), "torch": ("https://pytorch.org/docs/stable/", None), "jax": ("https://jax.readthedocs.io/en/latest/", None), } sphinx_gallery_conf = { "examples_dirs": ["../../examples"], "gallery_dirs": "auto_examples", "filename_pattern": "plot_", # (?!barycenter_fgw) "nested_sections": False, "backreferences_dir": "gen_modules/backreferences", "inspect_global_variables": True, "doc_module": ("ot", "numpy", "scipy", "pylab"), "matplotlib_animations": True, "reference_url": {"ot": None}, "copyfile_regex": r"index.rst", } python-pot-0.9.7+dfsg/docs/source/contributing.rst000066400000000000000000000002101525521106200222500ustar00rootroot00000000000000Contributing to POT =================== .. include:: ../../.github/CONTRIBUTING.md :parser: myst_parser.sphinx_ :start-line: 2 python-pot-0.9.7+dfsg/docs/source/contributors.rst000066400000000000000000000001621525521106200223040ustar00rootroot00000000000000Contributors ============ .. include:: ../../CONTRIBUTORS.md :parser: myst_parser.sphinx_ :start-line: 2 python-pot-0.9.7+dfsg/docs/source/index.rst000066400000000000000000000012751525521106200206640ustar00rootroot00000000000000.. POT documentation master file, created by sphinx-quickstart on Mon Oct 24 11:10:10 2016. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. POT: Python Optimal Transport ============================= .. image:: _static/images/logo.svg :width: 400 :alt: POT Logo Contents -------- .. toctree:: :maxdepth: 1 self auto_examples/plot_quickstart_guide auto_examples/index user_guide all releases contributors contributing code_of_conduct .. include:: ../../README.md :parser: myst_parser.sphinx_ Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` python-pot-0.9.7+dfsg/docs/source/releases.rst000066400000000000000000000001461525521106200213540ustar00rootroot00000000000000Releases ======== .. include:: ../../RELEASES.md :parser: myst_parser.sphinx_ :start-line: 2 python-pot-0.9.7+dfsg/docs/source/user_guide.rst000066400000000000000000001526701525521106200217160ustar00rootroot00000000000000 User guide ========== In the following we provide some pointers about which functions and classes to use for different problems related to optimal transport (OT) and machine learning. We refer when we can to concrete examples in the documentation that are also available as notebooks on the POT Github. .. note:: For a good introduction to numerical optimal transport we refer the reader to `the book `_ by Peyré and Cuturi [15]_. For more detailed introduction to OT and how it can be used in ML applications we refer the reader to the following `OTML tutorial `_. .. note:: Since version 0.8, POT provides a backend to automatically solve some OT problems independently from the toolbox used by the user (numpy/torch/jax). We provide a discussion about which functions are compatible in section `Backend section <#solving-ot-with-multiple-backends>`_ . Why Optimal Transport ? ----------------------- When to use OT ^^^^^^^^^^^^^^ Optimal Transport (OT) is a mathematical problem introduced by Gaspard Monge in 1781 that aim at finding the most efficient way to move mass between distributions. The cost of moving a unit of mass between two positions is called the ground cost and the objective is to minimize the overall cost of moving one mass distribution onto another one. The optimization problem can be expressed for two distributions :math:`\mu_s` and :math:`\mu_t` as .. math:: \min_{m, m \# \mu_s = \mu_t} \int c(x,m(x))d\mu_s(x) , where :math:`c(\cdot,\cdot)` is the ground cost and the constraint :math:`m \# \mu_s = \mu_t` ensures that :math:`\mu_s` is completely transported to :math:`\mu_t`. This problem is particularly difficult to solve because of this constraint and has been replaced in practice (on discrete distributions) by a linear program easier to solve. It corresponds to the Kantorovitch formulation where the Monge mapping :math:`m` is replaced by a joint distribution (OT matrix expressed in the next section) (see :ref:`kantorovitch_solve`). From the optimization problem above we can see that there are two main aspects to the OT solution that can be used in practical applications: - The optimal value (Wasserstein distance): Measures similarity between distributions. - The optimal mapping (Monge mapping, OT matrix): Finds correspondences between distributions. In the first case, OT can be used to measure similarity between distributions (or datasets), in this case the Wasserstein distance (the optimal value of the problem) is used. In the second case one can be interested in the way the mass is moved between the distributions (the mapping). This mapping can then be used to transfer knowledge between distributions. Wasserstein distance between distributions """""""""""""""""""""""""""""""""""""""""" OT is often used to measure similarity between distributions, especially when they do not share the same support. When the support between the distributions is disjoint OT-based Wasserstein distances compare favorably to popular f-divergences including the popular Kullback-Leibler, Jensen-Shannon divergences, and the Total Variation distance. What is particularly interesting for data science applications is that one can compute meaningful sub-gradients of the Wasserstein distance. For these reasons it became a very efficient tool for machine learning applications that need to measure and optimize similarity between empirical distributions. Numerous contributions make use of this an approach is the machine learning (ML) literature. For example OT was used for training `Generative Adversarial Networks (GANs) `_ in order to overcome the vanishing gradient problem. It has also been used to find `discriminant `_ or `robust `_ subspaces for a dataset. The Wasserstein distance has also been used to measure `similarity between word embeddings of documents `_ or between `signals `_ or `spectra `_. OT for mapping estimation """"""""""""""""""""""""" A very interesting aspect of OT problem is the OT mapping in itself. When computing optimal transport between discrete distributions one output is the OT matrix that will provide you with correspondences between the samples in each distributions. This correspondence is estimated with respect to the OT criterion and is found in a non-supervised way, which makes it very interesting on problems of transfer between datasets. It has been used to perform `color transfer between images `_ or in the context of `domain adaptation `_. More recent applications include the use of extension of OT (Gromov-Wasserstein) to find correspondences between languages in `word embeddings `_. When to use POT ^^^^^^^^^^^^^^^ The main objective of POT is to provide OT solvers for the rapidly growing area of OT in the context of machine learning. To this end we implement a number of solvers that have been proposed in research papers. Doing so we aim to promote reproducible research and foster novel developments. One very important aspect of POT is its ability to be easily extended. For instance we provide a very generic OT solver :any:`ot.optim.cg` that can solve OT problems with any smooth/continuous regularization term making it particularly practical for research purpose. Note that this generic solver has been used to solve both graph Laplacian regularization OT and Gromov Wasserstein [30]_. When not to use POT """"""""""""""""""" While POT has to the best of our knowledge one of the most efficient exact OT solvers, it has not been designed to handle large scale OT problems. For instance the memory cost for an OT problem is always :math:`\mathcal{O}(n^2)` in memory because the cost matrix has to be computed. The exact solver in of time complexity :math:`\mathcal{O}(n^3\log(n))` and the Sinkhorn solver has been proven to be nearly :math:`\mathcal{O}(n^2)` which is still too complex for very large scale solvers. For all the generic solvers we need to compute the cost matrix and the OT matrix of memory size :math:`\mathcal{O}(n^2)` which can be prohibitive for very large scale problems. If you need to solve OT with large number of samples, we provide "lazy" memory efficient implementation of Sinkhorn in pure python and using `GeomLoss `_. This implementation is compatible with Pytorch and can handle large number of samples. Another approach to estimate the Wasserstein distance for very large number of sample is to use the trick from `Wasserstein GAN `_ that solves the problem in the dual with a neural network estimating the dual variable. Note that in this case you are only solving an approximation of the Wasserstein distance because the 1-Lipschitz constraint on the dual cannot be enforced exactly (approximated through filter thresholding or regularization). Finally note that in order to avoid solving large scale OT problems, a number of recent approached minimized the expected Wasserstein distance on minibatches that is different from the Wasserstein but has better computational and `statistical properties `_. Optimal transport and Wasserstein distance ------------------------------------------ .. note:: In POT, most functions that solve OT or regularized OT problems have two versions that return the OT matrix or the value of the optimal solution. For instance :any:`ot.emd` returns the OT matrix and :any:`ot.emd2` returns the Wasserstein distance. This approach has been implemented in practice for all solvers that return an OT matrix (even Gromov-Wasserstein). .. _kantorovitch_solve: Solving optimal transport ^^^^^^^^^^^^^^^^^^^^^^^^^ The optimal transport problem between discrete distributions is often expressed as .. math:: \gamma^* = arg\min_{\gamma \in \mathbb{R}_+^{m\times n}} \quad \sum_{i,j}\gamma_{i,j}M_{i,j} s.t. \gamma 1 = a; \gamma^T 1= b; \gamma\geq 0 where: - :math:`M\in\mathbb{R}_+^{m\times n}` is the metric cost matrix defining the cost to move mass from bin :math:`a_i` to bin :math:`b_j`. - :math:`a` and :math:`b` are histograms on the simplex (positive, sum to 1) that represent the weights of each samples in the source an target distributions. Solving the linear program above can be done using the function :any:`ot.emd` that will return the optimal transport matrix :math:`\gamma^*`: .. code:: python # a and b are 1D histograms (sum to 1 and positive) # M is the ground cost matrix # unified API T = ot.solve(M, a, b).plan # exact linear program # classical API T = ot.emd(a, b, M) # exact linear program The method implemented for solving the OT problem is the network simplex. It is implemented in C from [1]_. It has a complexity of :math:`O(n^3)` but the solver is quite efficient and uses sparsity of the solution. .. minigallery:: ot.emd, ot.solve :add-heading: Examples of use for :any:`ot.emd` :heading-level: " Computing Wasserstein distance ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The value of the OT solution is often more interesting than the OT matrix: .. math:: OT(a,b) = \min_{\gamma \in \mathbb{R}_+^{m\times n}} \quad \sum_{i,j}\gamma_{i,j}M_{i,j} s.t. \gamma 1 = a; \gamma^T 1= b; \gamma\geq 0 It can computed from an already estimated OT matrix with :code:`np.sum(T*M)` or directly with the function :any:`ot.emd2`. .. code:: python # a and b are 1D histograms (sum to 1 and positive) # M is the ground cost matrix # Wasserstein distance / EMD value with unified API W = ot.solve(M, a, b, return_matrix=False).value # with classical API W = ot.emd2(a, b, M) Note that the well known `Wasserstein distance `_ between distributions a and b is defined as .. math:: W_p(a,b)=\left(\min_{\gamma \in \mathbb{R}_+^{m\times n}} \sum_{i,j}\gamma_{i,j}\|x_i-y_j\|^p\right)^{\frac{1}{p}} s.t. \gamma 1 = a; \gamma^T 1= b; \gamma\geq 0 This means that if you want to compute the :math:`W_2` you need to compute the square root of :any:`ot.emd2` when providing :code:`M = ot.dist(xs, xt)`, that uses the squared euclidean distance by default. Computing the :math:`W_1` Wasserstein distance can be done directly with :any:`ot.emd2` when providing :code:`M = ot.dist(xs, xt, metric='euclidean')` to use the Euclidean distance. .. minigallery:: ot.emd2, ot.solve :add-heading: Examples of use for :any:`ot.emd2` :heading-level: " Special cases ^^^^^^^^^^^^^ Note that the OT problem and the corresponding Wasserstein distance can in some special cases be computed very efficiently. For instance when the samples are in 1D, then the OT problem can be solved in :math:`O(n\log(n))` by using a simple sorting. In this case we provide the function :any:`ot.emd_1d` and :any:`ot.emd2_1d` to return respectively the OT matrix and value. Note that since the solution is very sparse the :code:`sparse` parameter of :any:`ot.emd_1d` allows for solving and returning the solution for very large problems. Note that in order to compute directly the :math:`W_p` Wasserstein distance in 1D we provide the function :any:`ot.wasserstein_1d` that takes :code:`p` as a parameter. Another special case for estimating OT and Monge mapping is between Gaussian distributions. In this case there exists a close form solution given in Remark 2.29 in [15]_ and the Monge mapping is an affine function and can be also computed from the covariances and means of the source and target distributions. In the case when the finite sample dataset is supposed Gaussian, we provide :any:`ot.gaussian.bures_wasserstein_mapping` that returns the parameters for the Monge mapping. All those special cases are accessible with the unified API of POT through the function :any:`ot.solve_sample` with the parameter :code:`method` that allows to choose the method used to solve the problem (with :code:`method='1D'` or :code:`method='gaussian'`). Regularized Optimal Transport ----------------------------- Recent developments have shown the interest of regularized OT both in terms of computational and statistical properties. We address in this section the regularized OT problems that can be expressed as .. math:: \gamma^* = arg\min_{\gamma \in \mathbb{R}_+^{m\times n}} \quad \sum_{i,j}\gamma_{i,j}M_{i,j} + \lambda\Omega(\gamma) s.t. \gamma 1 = a; \gamma^T 1= b; \gamma\geq 0 where : - :math:`M\in\mathbb{R}_+^{m\times n}` is the metric cost matrix defining the cost to move mass from bin :math:`a_i` to bin :math:`b_j`. - :math:`a` and :math:`b` are histograms (positive, sum to 1) that represent the weights of each samples in the source an target distributions. - :math:`\Omega` is the regularization term. We discuss in the following specific algorithms that can be used depending on the regularization term. Entropic regularized OT ^^^^^^^^^^^^^^^^^^^^^^^ This is the most common regularization used for optimal transport. It has been proposed in the ML community by Marco Cuturi in his seminal paper [2]_. This regularization has the following expression .. math:: \Omega(\gamma)=\sum_{i,j}\gamma_{i,j}\log(\gamma_{i,j}) The use of the regularization term above in the optimization problem has a very strong impact. First it makes the problem smooth which leads to new optimization procedures such as the well known Sinkhorn algorithm [2]_ or L-BFGS (see :any:`ot.smooth` ). Next it makes the problem strictly convex meaning that there will be a unique solution. Finally the solution of the resulting optimization problem can be expressed as: .. math:: \gamma_\lambda^*=\text{diag}(u)K\text{diag}(v) where :math:`u` and :math:`v` are vectors and :math:`K=\exp(-M/\lambda)` where the :math:`\exp` is taken component-wise. In order to solve the optimization problem, one can use an alternative projection algorithm called Sinkhorn-Knopp that can be very efficient for large values of regularization. The Sinkhorn-Knopp algorithm is implemented in :any:`ot.sinkhorn` and :any:`ot.sinkhorn2` that return respectively the OT matrix and the value of the linear term. Note that the regularization parameter :math:`\lambda` in the equation above is given to those functions with the parameter :code:`reg`. .. code:: python # unified API P = ot.solve(M, a, b, reg=1).plan # OT Sinkhorn matrix loss = ot.solve(M, a, b, reg=1).value # OT Sinkhorn value # classical API P = ot.sinkhorn(a, b, M, reg=1) # OT Sinkhorn matrix loss = ot.sinkhorn2(a, b, M, reg=1) # OT Sinkhorn value More details about the algorithms used are given in the following note. .. note:: The main function to solve entropic regularized OT is :any:`ot.sinkhorn`. This function is a wrapper and the parameter :code:`method` allows you to select the actual algorithm used to solve the problem: + :code:`method='sinkhorn'` calls :any:`ot.bregman.sinkhorn_knopp` the classic algorithm [2]_. + :code:`method='sinkhorn_log'` calls :any:`ot.bregman.sinkhorn_log` the sinkhorn algorithm in log space [2]_ that is more stable but can be slower in numpy since `logsumexp` is not implemented in parallel. It is the recommended solver for applications that requires differentiability with a small number of iterations. + :code:`method='sinkhorn_stabilized'` calls :any:`ot.bregman.sinkhorn_stabilized` the log stabilized version of the algorithm [9]_. + :code:`method='sinkhorn_epsilon_scaling'` calls :any:`ot.bregman.sinkhorn_epsilon_scaling` the epsilon scaling version of the algorithm [9]_. + :code:`method='greenkhorn'` calls :any:`ot.bregman.greenkhorn` the greedy Sinkhorn version of the algorithm [22]_. + :code:`method='screenkhorn'` calls :any:`ot.bregman.screenkhorn` the screening sinkhorn version of the algorithm [26]_. In addition to all those variants of Sinkhorn, we have another implementation solving the problem in the smooth dual or semi-dual in :any:`ot.smooth`. This solver uses the :any:`scipy.optimize.minimize` function to solve the smooth problem with :code:`L-BFGS-B` algorithm. To use this solver, use functions :any:`ot.smooth.smooth_ot_dual` or :any:`ot.smooth.smooth_ot_semi_dual` with parameter :code:`reg_type='kl'` to choose entropic/Kullback-Leibler regularization. **Choosing a Sinkhorn solver** By default and when using a regularization parameter that is not too small the default Sinkhorn solver should be enough. If you need to use a small regularization to get sharper OT matrices, you should use the :any:`ot.bregman.sinkhorn_stabilized` solver that will avoid numerical errors. This last solver can be very slow in practice and might not even converge to a reasonable OT matrix in a finite time. This is why :any:`ot.bregman.sinkhorn_epsilon_scaling` that relies on iterating the value of the regularization (and using warm start) sometimes leads to better solutions. Note that the greedy version of the Sinkhorn :any:`ot.bregman.greenkhorn` can also lead to a speedup and the screening version of the Sinkhorn :any:`ot.bregman.screenkhorn` aim a providing a fast approximation of the Sinkhorn problem. For use of GPU and gradient computation with small number of iterations we strongly recommend the :any:`ot.bregman.sinkhorn_log` solver that will no need to check for numerical problems. Recently Genevay et al. [23]_ introduced the Sinkhorn divergence that build from entropic regularization to compute fast and differentiable geometric divergence between empirical distributions. Note that we provide a function that computes directly (with no need to precompute the :code:`M` matrix) the Sinkhorn divergence for empirical distributions in :any:`ot.bregman.empirical_sinkhorn_divergence`. Similarly one can compute the OT matrix and loss for empirical distributions with respectively :any:`ot.bregman.empirical_sinkhorn` and :any:`ot.bregman.empirical_sinkhorn2`. Finally note that we also provide in :any:`ot.stochastic` several implementation of stochastic solvers for entropic regularized OT [18]_ [19]_. Those pure Python implementations are not optimized for speed but provide a robust implementation of algorithms in [18]_ [19]_. .. minigallery:: ot.sinkhorn ot.sinkhorn2 :add-heading: Examples of use for Sinkhorn algorithm :heading-level: " Other regularizations ^^^^^^^^^^^^^^^^^^^^^ While entropic OT is the most common and favored in practice, there exists other kinds of regularizations. We provide in POT two specific solvers for other regularization terms, namely quadratic regularization and group Lasso regularization. But we also provide in :any:`ot.optim` two generic solvers that allows solving any smooth regularization in practice. Quadratic regularization """""""""""""""""""""""" The first general regularization term we can solve is the quadratic regularization of the form .. math:: \Omega(\gamma)=\sum_{i,j} \gamma_{i,j}^2 This regularization term has an effect similar to entropic regularization by densifying the OT matrix, yet it keeps some sort of sparsity that is lost with entropic regularization as soon as :math:`\lambda>0` [17]_. This problem can be solved with POT using solvers from :any:`ot.smooth`, more specifically functions :any:`ot.smooth.smooth_ot_dual` or :any:`ot.smooth.smooth_ot_semi_dual` with parameter :code:`reg_type='l2'` to choose the quadratic regularization. .. minigallery:: ot.smooth.smooth_ot_dual ot.smooth.smooth_ot_semi_dual ot.optim.cg :add-heading: Examples of use of quadratic regularization :heading-level: " Group Lasso regularization """""""""""""""""""""""""" Another regularization that has been used in recent years [5]_ is the group Lasso regularization .. math:: \Omega(\gamma)=\sum_{j,G\in\mathcal{G}} \|\gamma_{G,j}\|_q^p where :math:`\mathcal{G}` contains non-overlapping groups of lines in the OT matrix. This regularization proposed in [5]_ promotes sparsity at the group level and for instance will force target samples to get mass from a small number of groups. Note that the exact OT solution is already sparse so this regularization does not make sense if it is not combined with entropic regularization. Depending on the choice of :code:`p` and :code:`q`, the problem can be solved with different approaches. When :code:`q=1` and :code:`p<1` the problem is non-convex but can be solved using an efficient majoration minimization approach with :any:`ot.sinkhorn_lpl1_mm`. When :code:`q=2` and :code:`p=1` we recover the convex group lasso and we provide a solver using generalized conditional gradient algorithm [7]_ in function :any:`ot.da.sinkhorn_l1l2_gl`. .. minigallery:: ot.da.SinkhornLpl1Transport ot.da.SinkhornL1l2Transport ot.da.sinkhorn_l1l2_gl ot.da.sinkhorn_lpl1_mm :add-heading: Examples of group Lasso regularization :heading-level: " Generic solvers """"""""""""""" Finally we propose in POT generic solvers that can be used to solve any regularization as long as you can provide a function computing the regularization and a function computing its gradient (or sub-gradient). In order to solve .. math:: \gamma^* = arg\min_\gamma \quad \sum_{i,j}\gamma_{i,j}M_{i,j} + \lambda\Omega(\gamma) s.t. \gamma 1 = a; \gamma^T 1= b; \gamma\geq 0 you can use function :any:`ot.optim.cg` that will use a conditional gradient as proposed in [6]_ . You need to provide the regularization function as parameter ``f`` and its gradient as parameter ``df``. Note that the conditional gradient relies on iterative solving of a linearization of the problem using the exact :any:`ot.emd` so it can be quite slow in practice. However, being an interior point algorithm, it always returns a transport matrix that does not violates the marginals. Another generic solver is proposed to solve the problem: .. math:: \gamma^* = arg\min_\gamma \quad \sum_{i,j}\gamma_{i,j}M_{i,j}+ \lambda_e\Omega_e(\gamma) + \lambda\Omega(\gamma) s.t. \gamma 1 = a; \gamma^T 1= b; \gamma\geq 0 where :math:`\Omega_e` is the entropic regularization. In this case we use a generalized conditional gradient [7]_ implemented in :any:`ot.optim.gcg` that does not linearize the entropic term but relies on :any:`ot.sinkhorn` for its iterations. .. minigallery:: ot.optim.cg ot.optim.gcg :add-heading: Examples of the generic solvers :heading-level: " Wasserstein Barycenters ----------------------- A Wasserstein barycenter is a distribution that minimizes its Wasserstein distance with respect to other distributions [16]_. It corresponds to minimizing the following problem by searching a distribution :math:`\mu` such that .. math:: \min_\mu \quad \sum_{k} w_kW(\mu,\mu_k) In practice we model a distribution with a finite number of support position: .. math:: \mu=\sum_{i=1}^n a_i\delta_{x_i} where :math:`a` is an histogram on the simplex and the :math:`\{x_i\}` are the position of the support. We can clearly see here that optimizing :math:`\mu` can be done by searching for optimal weights :math:`a` or optimal support :math:`\{x_i\}` (optimizing both is also an option). We provide in POT solvers to estimate a discrete Wasserstein barycenter in both cases. Barycenters with fixed support ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When optimizing a barycenter with a fixed support, the optimization problem can be expressed as .. math:: \min_a \quad \sum_{k} w_k W(a,b_k) where :math:`b_k` are also weights in the simplex. In the non-regularized case, the problem above is a classical linear program. In this case we propose a solver :meth:`ot.lp.barycenter` that relies on generic LP solvers. By default the function uses :any:`scipy.optimize.linprog`, but more efficient LP solvers from `cvxopt` can be also used by changing parameter :code:`solver`. Note that this problem requires to solve a very large linear program and can be very slow in practice. Similarly to the OT problem, OT barycenters can be computed in the regularized case. When entropic regularization is used, the problem can be solved with a generalization of the Sinkhorn algorithm based on Bregman projections [3]_. This algorithm is provided in function :any:`ot.bregman.barycenter` also available as :any:`ot.barycenter`. In this case, the algorithm scales better to large distributions and relies only on matrix multiplications that can be performed in parallel. In addition to the speedup brought by regularization, one can also greatly accelerate the estimation of Wasserstein barycenter when the support has a separable structure [21]_. In the case of 2D images for instance one can replace the matrix vector production in the Bregman projections by convolution operators. We provide an implementation of this algorithm in function :any:`ot.bregman.convolutional_barycenter2d`. .. minigallery:: ot.lp.barycenter ot.bregman.barycenter ot.barycenter :add-heading: Examples of Wasserstein and regularized Wasserstein barycenters :heading-level: " .. minigallery:: ot.bregman.convolutional_barycenter2d :add-heading: An example of convolutional barycenter (:any:`ot.bregman.convolutional_barycenter2d`) computation :heading-level: " Barycenters with free support ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Estimating the Wasserstein barycenter with free support but fixed weights corresponds to solving the following optimization problem: .. math:: \min_{\{x_i\}} \quad \sum_{k} w_kW(\mu,\mu_k) s.t. \quad \mu=\sum_{i=1}^n a_i\delta_{x_i} We provide a solver based on [20]_ in :any:`ot.lp.free_support_barycenter`. This function minimize the problem and return a locally optimal support :math:`\{x_i\}` for uniform or given weights :math:`a`. .. minigallery:: ot.lp.free_support_barycenter :add-heading: Examples of free support barycenter estimation :heading-level: " Monge mapping and Domain adaptation ----------------------------------- The original transport problem investigated by Gaspard Monge was seeking for a mapping function that maps (or transports) between a source and target distribution but that minimizes the transport loss. The existence and uniqueness of this optimal mapping is still an open problem in the general case but has been proven for smooth distributions by Brenier in his eponym `theorem `__. We provide in :any:`ot.da` several solvers for smooth Monge mapping estimation and domain adaptation from discrete distributions. Monge Mapping estimation ^^^^^^^^^^^^^^^^^^^^^^^^ We now discuss several approaches that are implemented in POT to estimate or approximate a Monge mapping from finite distributions. First note that when the source and target distributions are supposed to be Gaussian distributions, there exists a close form solution for the mapping and its an affine function [14]_ of the form :math:`T(x)=Ax+b` . In this case we provide the function :any:`ot.gaussian.bures_wasserstein_mapping` that returns the operator :math:`A` and vector :math:`b`. Note that if the number of samples is too small there is a parameter :code:`reg` that provides a regularization for the covariance matrix estimation. For a more general mapping estimation we also provide the barycentric mapping proposed in [6]_. It is implemented in the class :any:`ot.da.EMDTransport` and other transport-based classes in :any:`ot.da` . Those classes are discussed more in the following but follow an interface similar to scikit-learn classes. Finally a method proposed in [8]_ that estimates a continuous mapping approximating the barycentric mapping is provided in :any:`ot.da.joint_OT_mapping_linear` for linear mapping and :any:`ot.da.joint_OT_mapping_kernel` for non-linear mapping. .. minigallery:: ot.da.joint_OT_mapping_linear ot.da.joint_OT_mapping_linear ot.gaussian.bures_wasserstein_mapping :add-heading: Examples of Monge mapping estimation :heading-level: " Domain adaptation classes ^^^^^^^^^^^^^^^^^^^^^^^^^ The use of OT for domain adaptation (OTDA) has been first proposed in [5]_ that also introduced the group Lasso regularization. The main idea of OTDA is to estimate a mapping of the samples between source and target distributions which allows to transport labeled source samples onto the target distribution with no labels. We provide several classes based on :any:`ot.da.BaseTransport` that provide several OT and mapping estimations. The interface of those classes is similar to classifiers in scikit-learn. At initialization, several parameters such as regularization parameter value can be set. Then one needs to estimate the mapping with function :any:`ot.da.BaseTransport.fit`. Finally one can map the samples from source to target with :any:`ot.da.BaseTransport.transform` and from target to source with :any:`ot.da.BaseTransport.inverse_transform`. Here is an example for class :any:`ot.da.EMDTransport`: .. code:: ot_emd = ot.da.EMDTransport() ot_emd.fit(Xs=Xs, Xt=Xt) Xs_mapped = ot_emd.transform(Xs=Xs) A list of the provided implementation is given in the following note. .. note:: Here is a list of the OT mapping classes inheriting from :any:`ot.da.BaseTransport` * :any:`ot.da.EMDTransport`: Barycentric mapping with EMD transport * :any:`ot.da.SinkhornTransport`: Barycentric mapping with Sinkhorn transport * :any:`ot.da.SinkhornL1l2Transport`: Barycentric mapping with Sinkhorn + group Lasso regularization [5]_ * :any:`ot.da.SinkhornLpl1Transport`: Barycentric mapping with Sinkhorn + non convex group Lasso regularization [5]_ * :any:`ot.da.LinearTransport`: Linear mapping estimation between Gaussians [14]_ * :any:`ot.da.MappingTransport`: Nonlinear mapping estimation [8]_ .. minigallery:: ot.da.SinkhornTransport ot.da.LinearTransport :add-heading: Examples of the use of OTDA classes :heading-level: " Unbalanced and partial OT ------------------------- Unbalanced optimal transport ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unbalanced OT is a relaxation of the entropy regularized OT problem where the violation of the constraint on the marginals is added to the objective of the optimization problem. The unbalanced OT metric between two unbalanced histograms a and b is defined as [25]_ [10]_: .. math:: W_u(a, b) = \min_\gamma \quad \sum_{i,j}\gamma_{i,j}M_{i,j} + reg\cdot\Omega(\gamma) + reg_m KL(\gamma 1, a) + reg_m KL(\gamma^T 1, b) s.t. \quad \gamma\geq 0 where KL is the Kullback-Leibler divergence. This formulation allows for computing approximate mapping between distributions that do not have the same amount of mass. Interestingly the problem can be solved with a generalization of the Bregman projections algorithm [10]_. We provide a solver for unbalanced OT in :any:`ot.unbalanced`. Computing the optimal transport plan or the transport cost is similar to the balanced case. The Sinkhorn-Knopp algorithm is implemented in :any:`ot.sinkhorn_unbalanced` and :any:`ot.sinkhorn_unbalanced2` that return respectively the OT matrix and the value of the linear term. .. note:: The main function to solve entropic regularized UOT is :any:`ot.sinkhorn_unbalanced`. This function is a wrapper and the parameter :code:`method` helps you select the actual algorithm used to solve the problem: + :code:`method='sinkhorn'` calls :any:`ot.unbalanced.sinkhorn_knopp_unbalanced` the generalized Sinkhorn algorithm [25]_ [10]_. + :code:`method='sinkhorn_stabilized'` calls :any:`ot.unbalanced.sinkhorn_stabilized_unbalanced` the log stabilized version of the algorithm [10]_. .. minigallery:: ot.sinkhorn_unbalanced ot.sinkhorn_unbalanced2 ot.unbalanced.sinkhorn_unbalanced :add-heading: Examples of Unbalanced OT :heading-level: " Unbalanced Barycenters ^^^^^^^^^^^^^^^^^^^^^^ As with balanced distributions, we can define a barycenter of a set of histograms with different masses as a Fréchet Mean: .. math:: \min_{\mu} \quad \sum_{k} w_kW_u(\mu,\mu_k) where :math:`W_u` is the unbalanced Wasserstein metric defined above. This problem can also be solved using generalized version of Sinkhorn's algorithm and it is implemented the main function :any:`ot.barycenter_unbalanced`. .. note:: The main function to compute UOT barycenters is :any:`ot.barycenter_unbalanced`. This function is a wrapper and the parameter :code:`method` helps you select the actual algorithm used to solve the problem: + :code:`method='sinkhorn'` calls :meth:`ot.unbalanced.barycenter_unbalanced_sinkhorn_unbalanced` the generalized Sinkhorn algorithm [10]_. + :code:`method='sinkhorn_stabilized'` calls :any:`ot.unbalanced.barycenter_unbalanced_stabilized` the log stabilized version of the algorithm [10]_. .. minigallery:: ot.barycenter_unbalanced ot.unbalanced.barycenter_unbalanced :add-heading: Examples of Unbalanced OT barycenters :heading-level: " Partial optimal transport ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Partial OT is a variant of the optimal transport problem when only a fixed amount of mass m is to be transported. The partial OT metric between two histograms a and b is defined as [28]_: .. math:: \gamma = \arg\min_\gamma <\gamma,M>_F s.t. \gamma\geq 0 \\ \gamma 1 \leq a\\ \gamma^T 1 \leq b\\ 1^T \gamma^T 1 = m \leq \min\{\|a\|_1, \|b\|_1\} Interestingly the problem can be casted into a regular OT problem by adding reservoir points in which the surplus mass is sent [29]_. We provide a solver for partial OT in :any:`ot.partial`. The exact resolution of the problem is computed in :any:`ot.partial.partial_wasserstein` and :any:`ot.partial.partial_wasserstein2` that return respectively the OT matrix and the value of the linear term. The entropic solution of the problem is computed in :any:`ot.partial.entropic_partial_wasserstein` (see [3]_). Following the convention of :any:`ot.sinkhorn`, this solver takes a ``method`` parameter: ``method='sinkhorn'`` (default) runs the classical multiplicative-domain iterations, while ``method='sinkhorn_log'`` switches to a numerically stable log-domain solver (:any:`ot.partial.entropic_partial_wasserstein_logscale`) for small regularisation values where the standard solver returns NaN. Both solve exactly the same problem; the log-domain variant is slower because it computes everything in log-space. The partial Gromov-Wasserstein formulation of the problem .. math:: GW = \min_\gamma \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*\gamma_{i,j}*\gamma_{k,l} s.t. \gamma\geq 0 \\ \gamma 1 \leq a\\ \gamma^T 1 \leq b\\ 1^T \gamma^T 1 = m \leq \min\{\|a\|_1, \|b\|_1\} is computed in :any:`ot.partial.partial_gromov_wasserstein` and in :any:`ot.partial.entropic_partial_gromov_wasserstein` when considering the entropic regularization of the problem. .. minigallery:: ot.partial.partial_wasserstein ot.partial.partial_gromov_wasserstein :add-heading: Examples of Partial OT :heading-level: " Gromov Wasserstein and extensions --------------------------------- Gromov Wasserstein(GW) ^^^^^^^^^^^^^^^^^^^^^^ Gromov Wasserstein (GW) is a generalization of OT to distributions that do not lie in the same space [13]_. In this case one cannot compute distance between samples from the two distributions. [13]_ proposed instead to realign the metric spaces by computing a transport between distance matrices. The Gromov Wasserstein alignment between two distributions can be expressed as the one minimizing: .. math:: GW = \min_\gamma \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*\gamma_{i,j}*\gamma_{k,l} s.t. \gamma 1 = a; \gamma^T 1= b; \gamma\geq 0 where :math:`C1` is the distance matrix between samples in the source distribution and :math:`C2` the one between samples in the target, :math:`L(C1_{i,k},C2_{j,l})` is a measure of similarity between :math:`C1_{i,k}` and :math:`C2_{j,l}` often chosen as :math:`L(C1_{i,k},C2_{j,l})=\|C1_{i,k}-C2_{j,l}\|^2`. The optimization problem above is a non-convex quadratic program but we provide a solver that finds a local minimum using conditional gradient in :any:`ot.gromov.gromov_wasserstein`. There also exists an entropic regularized variant of GW that has been proposed in [12]_ and we provide an implementation of their algorithm in :any:`ot.gromov.entropic_gromov_wasserstein`. .. minigallery:: ot.gromov.gromov_wasserstein ot.gromov.entropic_gromov_wasserstein ot.gromov.fused_gromov_wasserstein ot.gromov.gromov_wasserstein2 :add-heading: Examples of computation of GW, regularized G and FGW :heading-level: " Gromov Wasserstein barycenters ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Note that similarly to Wasserstein distance GW allows for the definition of GW barycenters that can be expressed as .. math:: \min_{C\geq 0} \quad \sum_{k} w_k GW(C,Ck) where :math:`Ck` is the distance matrix between samples in distribution :math:`k`. Note that interestingly the barycenter is defined as a symmetric positive matrix. We provide a block coordinate optimization procedure in :any:`ot.gromov.gromov_barycenters` and :any:`ot.gromov.entropic_gromov_barycenters` for non-regularized and regularized barycenters respectively. Finally note that recently a fusion between Wasserstein and GW, coined Fused Gromov-Wasserstein (FGW) has been proposed [24]_. It allows to compute a similarity between objects that are only partly in the same space. As such it can be used to measure similarity between labeled graphs for instance and also provide computable barycenters. The implementations of FGW and FGW barycenter is provided in functions :any:`ot.gromov.fused_gromov_wasserstein` and :any:`ot.gromov.fgw_barycenters`. .. minigallery:: ot.gromov.gromov_barycenters ot.gromov.fgw_barycenters :add-heading: Examples of GW, regularized G and FGW barycenters :heading-level: " Other applications ------------------ We discuss in the following several OT related problems and tools that has been proposed in the OT and machine learning community. Wasserstein Discriminant Analysis ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Wasserstein Discriminant Analysis [11]_ is a generalization of `Fisher Linear Discriminant Analysis `__ that allows discrimination between classes that are not linearly separable. It consists in finding a linear projector optimizing the following criterion .. math:: P = \text{arg}\min_P \frac{\sum_i OT_e(\mu_i\#P,\mu_i\#P)}{\sum_{i,j\neq i} OT_e(\mu_i\#P,\mu_j\#P)} where :math:`\#` is the push-forward operator, :math:`OT_e` is the entropic OT loss and :math:`\mu_i` is the distribution of samples from class :math:`i`. :math:`P` is also constrained to be in the Stiefel manifold. WDA can be solved in POT using function :any:`ot.dr.wda`. It requires to have installed :code:`pymanopt` and :code:`autograd` for manifold optimization and automatic differentiation respectively. Note that we also provide the Fisher discriminant estimator in :any:`ot.dr.fda` for easy comparison. .. warning:: Note that due to the hard dependency on :code:`pymanopt` and :code:`autograd`, :any:`ot.dr` is not imported by default. If you want to use it you have to specifically import it with :code:`import ot.dr` . .. minigallery:: ot.dr.wda :add-heading: Examples of the use of WDA :heading-level: " Solving OT with Multiple backends on CPU/GPU -------------------------------------------- .. _backends_section: Since version 0.8, POT provides a backend that allows to code solvers independently from the type of the input arrays. The idea is to provide the user with a package that works seamlessly and returns a solution for instance as a Pytorch tensors when the function has Pytorch tensors as input. How it works ^^^^^^^^^^^^ The aim of the backend is to use the same function independently of the type of the input arrays. For instance when executing the following code .. code:: python # a and b are 1D histograms (sum to 1 and positive) # M is the ground cost matrix T = ot.emd(a, b, M) # exact linear program w = ot.emd2(a, b, M) # Wasserstein computation the functions :any:`ot.emd` and :any:`ot.emd2` can take inputs of the type :any:`numpy.array`, :any:`torch.tensor` or :any:`jax.numpy.array`. The output of the function will be the same type as the inputs and on the same device. When possible all computations are done on the same device and also when possible the output will be differentiable with respect to the input of the function. GPU acceleration ^^^^^^^^^^^^^^^^ The backends provide automatic computations/compatibility on GPU for most of the POT functions. Note that all solvers relying on the exact OT solver en C++ will need to solve the problem on CPU which can incur some memory copy overhead and be far from optimal when all other computations are done on GPU. They will still work on array on GPU since the copy is done automatically. Some of the functions that rely on the exact C++ solver are: - :any:`ot.emd`, :any:`ot.emd2` - :any:`ot.gromov_wasserstein`, :any:`ot.gromov_wasserstein2` - :any:`ot.optim.cg` List of compatible Backends ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - `Numpy `_ (all functions and solvers) - `Pytorch `_ (all outputs differentiable w.r.t. inputs) - `Jax `_ (Some functions are differentiable some require a wrapper) - `Tensorflow `_ (all outputs differentiable w.r.t. inputs) - `Cupy `_ (no differentiation, GPU only) The library automatically detects which backends are available for use. A backend is instantiated lazily only when necessary to prevent unwarranted GPU memory allocations. You can also disable the import of a specific backend library (e.g., to accelerate loading of `ot` library) using the environment variable `POT_BACKEND_DISABLE_` with in (TORCH,TENSORFLOW,CUPY,JAX). For instance, to disable TensorFlow, set `export POT_BACKEND_DISABLE_TENSORFLOW=1`. It's important to note that the `numpy` backend cannot be disabled. FAQ --- 1. **How to solve a discrete optimal transport problem ?** The solver for discrete OT is the function :py:mod:`ot.emd` that returns the OT transport matrix. If you want to solve a regularized OT you can use :py:mod:`ot.sinkhorn`. Here is a simple use case: .. code:: python # a and b are 1D histograms (sum to 1 and positive) # M is the ground cost matrix T = ot.emd(a, b, M) # exact linear program T_reg = ot.sinkhorn(a, b, M, reg) # entropic regularized OT More detailed examples can be seen on this example: :doc:`auto_examples/plot_OT_2D_samples` 2. **pip install POT fails with error : ImportError: No module named Cython.Build** As discussed shortly in the README file. POT<0.8 requires to have :code:`numpy` and :code:`cython` installed to build. This corner case is not yet handled by :code:`pip` and for now you need to install both library prior to installing POT. Note that this problem do not occur when using conda-forge since the packages there are pre-compiled. See `Issue #59 `__ for more details. 3. **Why is Sinkhorn slower than EMD ?** This might come from the choice of the regularization term. The speed of convergence of Sinkhorn depends directly on this term [22]_. When the regularization gets very small the problem tries to approximate the exact OT which leads to slow convergence in addition to numerical problems. In other words, for large regularization Sinkhorn will be very fast to converge, for small regularization (when you need an OT matrix close to the true OT), it might be quicker to use the EMD solver. Also note that the numpy implementation of Sinkhorn can use parallel computation depending on the configuration of your system, yet very important speedup can be obtained by using a GPU implementation since all operations are matrix/vector products. References ---------- .. [1] Bonneel, N., Van De Panne, M., Paris, S., & Heidrich, W. (2011, December). `Displacement nterpolation using Lagrangian mass transport `__. In ACM Transactions on Graphics (TOG) (Vol. 30, No. 6, p. 158). ACM. .. [2] Cuturi, M. (2013). `Sinkhorn distances: Lightspeed computation of optimal transport `__. In Advances in Neural Information Processing Systems (pp. 2292-2300). .. [3] Benamou, J. D., Carlier, G., Cuturi, M., Nenna, L., & Peyré, G. (2015). `Iterative Bregman projections for regularized transportation problems `__. SIAM Journal on Scientific Computing, 37(2), A1111-A1138. .. [5] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, `Optimal Transport for Domain Adaptation `__, in IEEE Transactions on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 .. [6] Ferradans, S., Papadakis, N., Peyré, G., & Aujol, J. F. (2014). `Regularized discrete optimal transport `__. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. .. [7] Rakotomamonjy, A., Flamary, R., & Courty, N. (2015). `Generalized conditional gradient: analysis of convergence and applications `__. arXiv preprint arXiv:1510.06567. .. [8] M. Perrot, N. Courty, R. Flamary, A. Habrard (2016), `Mapping estimation for discrete optimal transport `__, Neural Information Processing Systems (NIPS). .. [9] Schmitzer, B. (2016). `Stabilized Sparse Scaling Algorithms for Entropy Regularized Transport Problems `__. arXiv preprint arXiv:1610.06519. .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). `Scaling algorithms for unbalanced transport problems `__. arXiv preprint arXiv:1607.05816. .. [11] Flamary, R., Cuturi, M., Courty, N., & Rakotomamonjy, A. (2016). `Wasserstein Discriminant Analysis `__. arXiv preprint arXiv:1608.08063. .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon (2016), `Gromov-Wasserstein averaging of kernel and distance matrices `__ International Conference on Machine Learning (ICML). .. [13] Mémoli, Facundo (2011). `Gromov–Wasserstein distances and the metric approach to object matching `__. Foundations of computational mathematics 11.4 : 417-487. .. [14] Knott, M. and Smith, C. S. (1984). `On the optimal mapping of distributions `__, Journal of Optimization Theory and Applications Vol 43. .. [15] Peyré, G., & Cuturi, M. (2018). `Computational Optimal Transport `__ . .. [16] Agueh, M., & Carlier, G. (2011). `Barycenters in the Wasserstein space `__. SIAM Journal on Mathematical Analysis, 43(2), 904-924. .. [17] Blondel, M., Seguy, V., & Rolet, A. (2018). `Smooth and Sparse Optimal Transport `__. Proceedings of the Twenty-First International Conference on Artificial Intelligence and Statistics (AISTATS). .. [18] Genevay, A., Cuturi, M., Peyré, G. & Bach, F. (2016) `Stochastic Optimization for Large-scale Optimal Transport `__. Advances in Neural Information Processing Systems (2016). .. [19] Seguy, V., Bhushan Damodaran, B., Flamary, R., Courty, N., Rolet, A.& Blondel, M. `Large-scale Optimal Transport and Mapping Estimation `__. International Conference on Learning Representation (2018) .. [20] Cuturi, M. and Doucet, A. (2014) `Fast Computation of Wasserstein Barycenters `__. International Conference in Machine Learning .. [21] Solomon, J., De Goes, F., Peyré, G., Cuturi, M., Butscher, A., Nguyen, A. & Guibas, L. (2015). `Convolutional wasserstein distances: Efficient optimal transportation on geometric domains `__. ACM Transactions on Graphics (TOG), 34(4), 66. .. [22] J. Altschuler, J.Weed, P. Rigollet, (2017) `Near-linear time approximation algorithms for optimal transport via Sinkhorn iteration `__, Advances in Neural Information Processing Systems (NIPS) 31 .. [23] Genevay, A., Peyré, G., Cuturi, M., `Learning Generative Models with Sinkhorn Divergences `__, Proceedings of the Twenty-First International Conference on Artificial Intelligence and Statistics, (AISTATS) 21, 2018 .. [24] Vayer, T., Chapel, L., Flamary, R., Tavenard, R. and Courty, N. (2019). `Optimal Transport for structured data with application on graphs `__ Proceedings of the 36th International Conference on Machine Learning (ICML). .. [25] Frogner C., Zhang C., Mobahi H., Araya-Polo M., Poggio T. : Learning with a Wasserstein Loss, Advances in Neural Information Processing Systems (NIPS) 2015 .. [26] Alaya M. Z., Bérar M., Gasso G., Rakotomamonjy A. (2019). Screening Sinkhorn Algorithm for Regularized Optimal Transport , Advances in Neural Information Processing Systems 33 (NeurIPS). .. [28] Caffarelli, L. A., McCann, R. J. (2020). Free boundaries in optimal transport and Monge-Ampere obstacle problems , Annals of mathematics, 673-730. .. [29] Chapel, L., Alaya, M., Gasso, G. (2019). Partial Gromov-Wasserstein with Applications on Positive-Unlabeled Learning , arXiv preprint arXiv:2002.08276. .. [30] Flamary, Rémi, et al. "Optimal transport with Laplacian regularization: Applications to domain adaptation and shape matching." NIPS Workshop on Optimal Transport and Machine Learning OTML. 2014. .. [31] Bonneel, Nicolas, et al. `Sliced and radon wasserstein barycenters of measures `_\ , Journal of Mathematical Imaging and Vision 51.1 (2015): 22-45 .. [32] Huang, M., Ma S., Lai, L. (2021). `A Riemannian Block Coordinate Descent Method for Computing the Projection Robust Wasserstein Distance `_\ , Proceedings of the 38th International Conference on Machine Learning (ICML). .. [33] Kerdoncuff T., Emonet R., Marc S. `Sampled Gromov Wasserstein `_\ , Machine Learning Journal (MJL), 2021 .. [34] Feydy, J., Séjourné, T., Vialard, F. X., Amari, S. I., Trouvé, A., & Peyré, G. (2019, April). `Interpolating between optimal transport and MMD using Sinkhorn divergences `_. In The 22nd International Conference on Artificial Intelligence and Statistics (pp. 2681-2690). PMLR. .. [35] Deshpande, I., Hu, Y. T., Sun, R., Pyrros, A., Siddiqui, N., Koyejo, S., & Schwing, A. G. (2019). `Max-sliced wasserstein distance and its use for gans `_. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (pp. 10648-10656). .. [36] Liutkus, A., Simsekli, U., Majewski, S., Durmus, A., & Stöter, F. R. (2019, May). `Sliced-Wasserstein flows: Nonparametric generative modeling via optimal transport and diffusions `_. In International Conference on Machine Learning (pp. 4104-4113). PMLR. .. [37] Janati, H., Cuturi, M., Gramfort, A. `Debiased sinkhorn barycenters `_ Proceedings of the 37th International Conference on Machine Learning, PMLR 119:4692-4701, 2020 .. [38] C. Vincent-Cuaz, T. Vayer, R. Flamary, M. Corneli, N. Courty, `Online Graph Dictionary Learning `_\ , International Conference on Machine Learning (ICML), 2021. .. [39] Gozlan, N., Roberto, C., Samson, P. M., & Tetali, P. (2017). `Kantorovich duality for general transport costs and applications `_. Journal of Functional Analysis, 273(11), 3327-3405. .. [40] Forrow, A., Hütter, J. C., Nitzan, M., Rigollet, P., Schiebinger, G., & Weed, J. (2019, April). `Statistical optimal transport via factored couplings `_. In The 22nd International Conference on Artificial Intelligence and Statistics (pp. 2454-2465). PMLR. .. [41] Xu, H., Luo, D., & Carin, L. (2019). `Scalable Gromov-Wasserstein learning for graph partitioning and matching `_\ , Advances in neural information processing systems, 32.python-pot-0.9.7+dfsg/examples/000077500000000000000000000000001525521106200164045ustar00rootroot00000000000000python-pot-0.9.7+dfsg/examples/backends/000077500000000000000000000000001525521106200201565ustar00rootroot00000000000000python-pot-0.9.7+dfsg/examples/backends/index.rst000066400000000000000000000000771525521106200220230ustar00rootroot00000000000000 Differentiable OT with PyTorch ------------------------------python-pot-0.9.7+dfsg/examples/backends/plot_Sinkhorn_gradients.py000066400000000000000000000051621525521106200254250ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ================================================ Different gradient computations for regularized optimal transport ================================================ This example illustrates the differences in terms of computation time between the gradient options for the Sinkhorn solver. .. note:: Example added in release: 0.9.6 """ # Author: Sonia Mazelet # # License: MIT License # sphinx_gallery_thumbnail_number = 1 import matplotlib.pylab as pl import ot from ot.backend import torch ############################################################################## # Time comparison of the Sinkhorn solver for different gradient options # ------------- # %% parameters n_trials = 10 times_autodiff = torch.zeros(n_trials) times_envelope = torch.zeros(n_trials) times_last_step = torch.zeros(n_trials) n_samples_s = 300 n_samples_t = 300 n_features = 5 reg = 0.03 # Time required for the Sinkhorn solver and gradient computations, for different gradient options over multiple Gaussian distributions for i in range(n_trials): x = torch.rand((n_samples_s, n_features)) y = torch.rand((n_samples_t, n_features)) a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) M = ot.dist(x, y) a = torch.tensor(a, requires_grad=True) b = torch.tensor(b, requires_grad=True) M = M.clone().detach().requires_grad_(True) # autodiff provides the gradient for all the outputs (plan, value, value_linear) ot.tic() res_autodiff = ot.solve(M, a, b, reg=reg, grad="autodiff") res_autodiff.value.backward() times_autodiff[i] = ot.toq() a = a.clone().detach().requires_grad_(True) b = b.clone().detach().requires_grad_(True) M = M.clone().detach().requires_grad_(True) # envelope provides the gradient for value ot.tic() res_envelope = ot.solve(M, a, b, reg=reg, grad="envelope") res_envelope.value.backward() times_envelope[i] = ot.toq() a = a.clone().detach().requires_grad_(True) b = b.clone().detach().requires_grad_(True) M = M.clone().detach().requires_grad_(True) # last_step provides the gradient for all the outputs, but only for the last iteration of the Sinkhorn algorithm ot.tic() res_last_step = ot.solve(M, a, b, reg=reg, grad="last_step") res_last_step.value.backward() times_last_step[i] = ot.toq() pl.figure(1, figsize=(5, 3)) pl.ticklabel_format(axis="y", style="sci", scilimits=(0, 0)) pl.boxplot( ([times_autodiff, times_envelope, times_last_step]), tick_labels=["autodiff", "envelope", "last_step"], showfliers=False, ) pl.ylabel("Time (s)") pl.show() python-pot-0.9.7+dfsg/examples/backends/plot_dual_ot_pytorch.py000066400000000000000000000073121525521106200247700ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ====================================================================== Dual OT solvers for entropic and quadratic regularized OT with Pytorch ====================================================================== .. note:: Example added in release: 0.8.2. """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 3 import numpy as np import matplotlib.pyplot as pl import torch import ot import ot.plot # %% # Data generation # --------------- torch.manual_seed(1) n_source_samples = 100 n_target_samples = 100 theta = 2 * np.pi / 20 noise_level = 0.1 Xs, ys = ot.datasets.make_data_classif("gaussrot", n_source_samples, nz=noise_level) Xt, yt = ot.datasets.make_data_classif( "gaussrot", n_target_samples, theta=theta, nz=noise_level ) # one of the target mode changes its variance (no linear mapping) Xt[yt == 2] *= 3 Xt = Xt + 4 # %% # Plot data # --------- pl.figure(1, (10, 5)) pl.clf() pl.scatter(Xs[:, 0], Xs[:, 1], marker="+", label="Source samples") pl.scatter(Xt[:, 0], Xt[:, 1], marker="o", label="Target samples") pl.legend(loc=0) pl.title("Source and target distributions") # %% # Convert data to torch tensors # ----------------------------- xs = torch.tensor(Xs) xt = torch.tensor(Xt) # %% # Estimating dual variables for entropic OT # ----------------------------------------- u = torch.randn(n_source_samples, requires_grad=True) v = torch.randn(n_source_samples, requires_grad=True) reg = 0.5 optimizer = torch.optim.Adam([u, v], lr=1) # number of iteration n_iter = 200 losses = [] for i in range(n_iter): # generate noise samples # minus because we maximize the dual loss loss = -ot.stochastic.loss_dual_entropic(u, v, xs, xt, reg=reg) losses.append(float(loss.detach())) if i % 10 == 0: print("Iter: {:3d}, loss={}".format(i, losses[-1])) loss.backward() optimizer.step() optimizer.zero_grad() pl.figure(2) pl.plot(losses) pl.grid() pl.title("Dual objective (negative)") pl.xlabel("Iterations") Ge = ot.stochastic.plan_dual_entropic(u, v, xs, xt, reg=reg) # %% # Plot the estimated entropic OT plan # ----------------------------------- pl.figure(3, (10, 5)) pl.clf() ot.plot.plot2D_samples_mat(Xs, Xt, Ge.detach().numpy(), alpha=0.1) pl.scatter(Xs[:, 0], Xs[:, 1], marker="+", label="Source samples", zorder=2) pl.scatter(Xt[:, 0], Xt[:, 1], marker="o", label="Target samples", zorder=2) pl.legend(loc=0) pl.title("Source and target distributions") # %% # Estimating dual variables for quadratic OT # ------------------------------------------ u = torch.randn(n_source_samples, requires_grad=True) v = torch.randn(n_source_samples, requires_grad=True) reg = 0.01 optimizer = torch.optim.Adam([u, v], lr=1) # number of iteration n_iter = 200 losses = [] for i in range(n_iter): # generate noise samples # minus because we maximize the dual loss loss = -ot.stochastic.loss_dual_quadratic(u, v, xs, xt, reg=reg) losses.append(float(loss.detach())) if i % 10 == 0: print("Iter: {:3d}, loss={}".format(i, losses[-1])) loss.backward() optimizer.step() optimizer.zero_grad() pl.figure(4) pl.plot(losses) pl.grid() pl.title("Dual objective (negative)") pl.xlabel("Iterations") Gq = ot.stochastic.plan_dual_quadratic(u, v, xs, xt, reg=reg) # %% # Plot the estimated quadratic OT plan # ------------------------------------ pl.figure(5, (10, 5)) pl.clf() ot.plot.plot2D_samples_mat(Xs, Xt, Gq.detach().numpy(), alpha=0.1) pl.scatter(Xs[:, 0], Xs[:, 1], marker="+", label="Source samples", zorder=2) pl.scatter(Xt[:, 0], Xt[:, 1], marker="o", label="Target samples", zorder=2) pl.legend(loc=0) pl.title("OT plan with quadratic regularization") python-pot-0.9.7+dfsg/examples/backends/plot_gradient_descent.py000066400000000000000000000172431525521106200250770ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" =============================================================================== Solve Fused Unbalanced Gromov Wasserstein with Adam =============================================================================== Since the FUGW loss is differentiable, it can be minimized with first-order optimization. We show how to do this with the `loss_fugw_batch` function and compare the results with the dedicated FUGW solver `fused_unbalanced_gromov_wasserstein`. """ # Author: Rémi Flamary # Sonia Mazelet # # License: MIT License # sphinx_gallery_thumbnail_number = 3 import numpy as np import matplotlib.pylab as pl import torch from time import perf_counter import ot from ot.batch._quadratic import loss_quadratic_batch, tensor_batch from ot.gromov import fused_unbalanced_gromov_wasserstein from sklearn.manifold import MDS # %% # Generation of source and target graphs # ---------------- rng = np.random.RandomState(42) def get_sbm(n, nc, ratio, P): nbpc = np.round(n * ratio).astype(int) n = np.sum(nbpc) C = np.zeros((n, n)) for c1 in range(nc): for c2 in range(c1 + 1): if c1 == c2: for i in range(np.sum(nbpc[:c1]), np.sum(nbpc[: c1 + 1])): for j in range(np.sum(nbpc[:c2]), i): if rng.rand() <= P[c1, c2]: C[i, j] = 1 else: for i in range(np.sum(nbpc[:c1]), np.sum(nbpc[: c1 + 1])): for j in range(np.sum(nbpc[:c2]), np.sum(nbpc[: c2 + 1])): if rng.rand() <= P[c1, c2]: C[i, j] = 1 return C + C.T def plot_graph(x, C, color="C0", s=100): for j in range(C.shape[0]): for i in range(j): if C[i, j] > 0: pl.plot([x[i, 0], x[j, 0]], [x[i, 1], x[j, 1]], alpha=0.2, color="k") pl.scatter(x[:, 0], x[:, 1], c=color, s=s, zorder=10, edgecolors="k") def get_sbm_labels(n, ratio): nbpc = np.round(n * ratio).astype(int) return np.concatenate( [np.full(count, label, dtype=int) for label, count in enumerate(nbpc)] ) def get_noisy_one_hot(labels, n_classes, noise_level=0.1): x = np.eye(n_classes)[labels] x += noise_level * rng.randn(*x.shape) return x n1 = 15 n2 = 10 nc1 = 3 nc2 = 2 ratio1 = np.array([0.33, 0.33, 0.33]) ratio2 = np.array([0.5, 0.5]) P1 = np.array([[0.8, 0.03, 0.0], [0.08, 0.8, 0.03], [0.0, 0.08, 0.8]]) P2 = np.array(0.8 * np.eye(2) + 0.01 * np.ones((2, 2))) C1 = get_sbm(n1, nc1, ratio1, P1) C2 = get_sbm(n2, nc2, ratio2, P2) labels1 = get_sbm_labels(n1, ratio1) labels2 = get_sbm_labels(n2, ratio2) # Use noisy one-hot encodings of the SBM classes as node features. feature_dim = max(nc1, nc2) x1 = get_noisy_one_hot(labels1, feature_dim) x2 = get_noisy_one_hot(labels2, feature_dim) all_features = np.vstack([x1, x2]) feature_min = all_features[:, :3].min(axis=0, keepdims=True) feature_max = all_features[:, :3].max(axis=0, keepdims=True) # get 2d positions for visualization pos1 = MDS(dissimilarity="precomputed", random_state=0, n_init=1).fit_transform(1 - C1) pos2 = MDS(dissimilarity="precomputed", random_state=0, n_init=1).fit_transform(1 - C2) colors1 = np.clip( (x1 - feature_min) / np.maximum(feature_max - feature_min, 1e-15), 0.0, 1.0 ) colors2 = np.clip( (x2 - feature_min) / np.maximum(feature_max - feature_min, 1e-15), 0.0, 1.0 ) pl.figure(1, (10, 5)) pl.clf() pl.subplot(1, 2, 1) plot_graph(pos1, C1, color=colors1) pl.title("SBM source graph") pl.axis("off") pl.subplot(1, 2, 2) plot_graph(pos2, C2, color=colors2) pl.title("SBM target graph") _ = pl.axis("off") # %% # Solve FUGW with Adam # ---------------- # Even though `loss_fugw_batch` supports batches of problems, we use a # batch of size 1 here for clarity. a = ot.unif(C1.shape[0]) b = ot.unif(C2.shape[0]) M = ot.dist(x1, x2) M /= M.max() a_torch = torch.tensor(a[None, :]) b_torch = torch.tensor(b[None, :]) C1_torch = torch.tensor(C1[None, :, :]) C2_torch = torch.tensor(C2[None, :, :]) M_torch = torch.tensor(M[None, :, :]) L = tensor_batch(a_torch, b_torch, C1_torch, C2_torch, loss="sqeuclidean") alpha = 0.5 reg_marginals = 0.5 lr = 5e-2 nb_iter_max = 1500 tol = 1e-7 T0_torch = a_torch[:, :, None] * b_torch[:, None, :] T_torch = torch.log(torch.expm1(T0_torch)).clone().requires_grad_(True) optimizer = torch.optim.Adam([T_torch], lr=lr) loss_iter = [] mass_iter = [] previous_plan_torch = None tic = perf_counter() for i in range(nb_iter_max): optimizer.zero_grad() # Positive transport plan parameterized as log(1 + exp(T)). plan_torch = torch.nn.functional.softplus(T_torch) loss = loss_quadratic_batch( a_torch, b_torch, C1_torch, C2_torch, plan_torch, M_torch, alpha=alpha, unbalanced=reg_marginals, unbalanced_type="kl", recompute_const=True, )[0] loss_iter.append(float(loss.detach())) mass_iter.append(float(plan_torch.detach().sum())) if previous_plan_torch is not None: err = float(torch.sum(torch.abs(plan_torch.detach() - previous_plan_torch))) if err < tol: break previous_plan_torch = plan_torch.detach().clone() loss.backward() optimizer.step() time_adam = perf_counter() - tic T_adam = torch.nn.functional.softplus(T_torch).detach().cpu().numpy()[0] # %% # Compare with the dedicated FUGW solver # ------------------------------------- # # The dedicated solver uses a block coordinate descent (BCD) scheme. We compare # the coupling it returns with the one obtained by direct Adam minimization of # `loss_fugw_batch`. def evaluate_batch_fugw_loss(plan): plan_torch = torch.tensor(plan[None, :, :], dtype=M_torch.dtype) loss = loss_quadratic_batch( a_torch, b_torch, C1_torch, C2_torch, plan_torch, M_torch, alpha=alpha, unbalanced=reg_marginals, unbalanced_type="kl", recompute_const=True, )[0] return float(loss.detach()) tic = perf_counter() result = ot.solve_gromov( C1, C2, M, a, b, alpha=alpha, reg=0, unbalanced_type="kl", unbalanced=reg_marginals ) time_bcd = perf_counter() - tic loss_adam_final = evaluate_batch_fugw_loss(T_adam) T_bcd = result.plan loss_bcd_final = evaluate_batch_fugw_loss(T_bcd) mass_bcd = T_bcd.sum() pl.figure(2, (10, 4)) pl.clf() pl.subplot(1, 2, 1) pl.plot(loss_iter, label="Adam") pl.axhline(loss_bcd_final, color="C1", linestyle="--", label="BCD solver") pl.grid() pl.title("FUGW loss along iterations") pl.xlabel("Iterations") pl.legend() pl.subplot(1, 2, 2) pl.plot(mass_iter, label="Adam") pl.axhline(mass_bcd, color="C1", linestyle="--", label="BCD solver") pl.grid() pl.title("Transport mass") pl.xlabel("Iterations") _ = pl.legend() # %% # Visualize the learned couplings # ------------------------------- # We visualize the couplings obtained by both methods to compare them. On this example, both methods recover similar couplings, # but direct minimization reaches a lower `loss_fugw_batch` value at the cost # of a longer runtime. vmin = min(T_adam.min(), T_bcd.min()) vmax = max(T_adam.max(), T_bcd.max()) pl.figure(3, (10, 4)) pl.clf() pl.subplot(1, 2, 1) pl.imshow(T_adam, interpolation="nearest", cmap="Blues", vmin=vmin, vmax=vmax) pl.title( f"Coupling from direct minimization\nloss={loss_adam_final:.3f}, time={time_adam:.2f}s" ) pl.xlabel("Target nodes") pl.ylabel("Source nodes") pl.colorbar() pl.subplot(1, 2, 2) pl.imshow(T_bcd, interpolation="nearest", cmap="Blues", vmin=vmin, vmax=vmax) pl.title(f"Coupling from BCD solver\nloss={loss_bcd_final:.3f}, time={time_bcd:.2f}s") pl.xlabel("Target nodes") pl.ylabel("Source nodes") _ = pl.colorbar() python-pot-0.9.7+dfsg/examples/backends/plot_optim_gromov_pytorch.py000066400000000000000000000171201525521106200260600ustar00rootroot00000000000000r""" ======================================================= Optimizing the Gromov-Wasserstein distance with PyTorch ======================================================= .. note:: Example added in release: 0.8.0. In this example, we use the pytorch backend to optimize the Gromov-Wasserstein (GW) loss between two graphs expressed as empirical distribution. In the first part, we optimize the weights on the node of a simple template graph so that it minimizes the GW with a given Stochastic Block Model graph. We can see that this actually recovers the proportion of classes in the SBM and allows for an accurate clustering of the nodes using the GW optimal plan. In the second part, we optimize simultaneously the weights and the structure of the template graph which allows us to perform graph compression and to recover other properties of the SBM. The backend actually uses the gradients expressed in [38] to optimize the weights. [38] C. Vincent-Cuaz, T. Vayer, R. Flamary, M. Corneli, N. Courty, Online Graph Dictionary Learning, International Conference on Machine Learning (ICML), 2021. """ # Author: Rémi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 3 from sklearn.manifold import MDS import numpy as np import matplotlib.pylab as pl import torch import ot from ot.gromov import gromov_wasserstein2 # %% # Graph generation # ---------------- rng = np.random.RandomState(42) def get_sbm(n, nc, ratio, P): nbpc = np.round(n * ratio).astype(int) n = np.sum(nbpc) C = np.zeros((n, n)) for c1 in range(nc): for c2 in range(c1 + 1): if c1 == c2: for i in range(np.sum(nbpc[:c1]), np.sum(nbpc[: c1 + 1])): for j in range(np.sum(nbpc[:c2]), i): if rng.rand() <= P[c1, c2]: C[i, j] = 1 else: for i in range(np.sum(nbpc[:c1]), np.sum(nbpc[: c1 + 1])): for j in range(np.sum(nbpc[:c2]), np.sum(nbpc[: c2 + 1])): if rng.rand() <= P[c1, c2]: C[i, j] = 1 return C + C.T n = 100 nc = 3 ratio = np.array([0.5, 0.3, 0.2]) P = np.array(0.6 * np.eye(3) + 0.05 * np.ones((3, 3))) C1 = get_sbm(n, nc, ratio, P) # get 2d position for nodes x1 = MDS(dissimilarity="precomputed", random_state=0).fit_transform(1 - C1) def plot_graph(x, C, color="C0", s=None): for j in range(C.shape[0]): for i in range(j): if C[i, j] > 0: pl.plot([x[i, 0], x[j, 0]], [x[i, 1], x[j, 1]], alpha=0.2, color="k") pl.scatter( x[:, 0], x[:, 1], c=color, s=s, zorder=10, edgecolors="k", cmap="tab10", vmax=9 ) pl.figure(1, (10, 5)) pl.clf() pl.subplot(1, 2, 1) plot_graph(x1, C1, color="C0") pl.title("SBM Graph") pl.axis("off") pl.subplot(1, 2, 2) pl.imshow(C1, interpolation="nearest") pl.title("Adjacency matrix") pl.axis("off") # %% # Optimizing GW w.r.t. the weights on a template structure # -------------------------------------------------------- # The adjacency matrix C1 is block diagonal with 3 blocks. We want to # optimize the weights of a simple template C0=eye(3) and see if we can # recover the proportion of classes from the SBM (up to a permutation). C0 = np.eye(3) def min_weight_gw(C1, C2, a2, nb_iter_max=100, lr=1e-2): """solve min_a GW(C1,C2,a, a2) by gradient descent""" # use pyTorch for our data C1_torch = torch.tensor(C1) C2_torch = torch.tensor(C2) a0 = rng.rand(C1.shape[0]) # random_init a0 /= a0.sum() # on simplex a1_torch = torch.tensor(a0).requires_grad_(True) a2_torch = torch.tensor(a2) loss_iter = [] for i in range(nb_iter_max): loss = gromov_wasserstein2(C1_torch, C2_torch, a1_torch, a2_torch) loss_iter.append(loss.clone().detach().cpu().numpy()) loss.backward() # print("{:03d} | {}".format(i, loss_iter[-1])) # performs a step of projected gradient descent with torch.no_grad(): grad = a1_torch.grad a1_torch -= grad * lr # step a1_torch.grad.zero_() a1_torch.data = ot.utils.proj_simplex(a1_torch) a1 = a1_torch.clone().detach().cpu().numpy() return a1, loss_iter a0_est, loss_iter0 = min_weight_gw(C0, C1, ot.unif(n), nb_iter_max=100, lr=1e-2) pl.figure(2) pl.plot(loss_iter0) pl.title("Loss along iterations") print("Estimated weights : ", a0_est) print("True proportions : ", ratio) # %% # It is clear that the optimization has converged and that we recover the # ratio of the different classes in the SBM graph up to a permutation. # %% # Community clustering with uniform and estimated weights # ------------------------------------------------------- # The GW OT plan can be used to perform a clustering of the nodes of a graph # when computing the GW with a simple template like C0 by labeling nodes in # the original graph using by the index of the node in the template receiving # the most mass. # # We show here the result of such a clustering when using uniform weights on # the template C0 and when using the optimal weights previously estimated. T_unif = ot.gromov_wasserstein(C1, C0, ot.unif(n), ot.unif(3)) label_unif = T_unif.argmax(1) T_est = ot.gromov_wasserstein(C1, C0, ot.unif(n), a0_est) label_est = T_est.argmax(1) pl.figure(3, (10, 5)) pl.clf() pl.subplot(1, 2, 1) plot_graph(x1, C1, color=label_unif) pl.title("Graph clustering unif. weights") pl.axis("off") pl.subplot(1, 2, 2) plot_graph(x1, C1, color=label_est) pl.title("Graph clustering est. weights") pl.axis("off") # %% # Graph compression with GW # ------------------------- # Now we optimize both the weights and structure of a small graph that # minimize the GW distance wrt our data graph. This can be seen as graph # compression but can also recover important properties of an SBM such # as its class proportion but also its matrix of probability of links between # classes def graph_compression_gw(nb_nodes, C2, a2, nb_iter_max=100, lr=1e-2): """solve min_a GW(C1,C2,a, a2) by gradient descent""" # use pyTorch for our data C2_torch = torch.tensor(C2) a2_torch = torch.tensor(a2) a0 = rng.rand(nb_nodes) # random_init a0 /= a0.sum() # on simplex a1_torch = torch.tensor(a0).requires_grad_(True) C0 = np.eye(nb_nodes) C1_torch = torch.tensor(C0).requires_grad_(True) loss_iter = [] for i in range(nb_iter_max): loss = gromov_wasserstein2(C1_torch, C2_torch, a1_torch, a2_torch) loss_iter.append(loss.clone().detach().cpu().numpy()) loss.backward() # print("{:03d} | {}".format(i, loss_iter[-1])) # performs a step of projected gradient descent with torch.no_grad(): grad = a1_torch.grad a1_torch -= grad * lr # step a1_torch.grad.zero_() a1_torch.data = ot.utils.proj_simplex(a1_torch) grad = C1_torch.grad C1_torch -= grad * lr # step C1_torch.grad.zero_() C1_torch.data = torch.clamp(C1_torch, 0, 1) a1 = a1_torch.clone().detach().cpu().numpy() C1 = C1_torch.clone().detach().cpu().numpy() return a1, C1, loss_iter nb_nodes = 3 a0_est2, C0_est2, loss_iter2 = graph_compression_gw( nb_nodes, C1, ot.unif(n), nb_iter_max=100, lr=5e-2 ) pl.figure(4) pl.plot(loss_iter2) pl.title("Loss along iterations") print("Estimated weights : ", a0_est2) print("True proportions : ", ratio) pl.figure(6, (10, 3.5)) pl.clf() pl.subplot(1, 2, 1) pl.imshow(P, vmin=0, vmax=1) pl.title("True SBM P matrix") pl.subplot(1, 2, 2) pl.imshow(C0_est2, vmin=0, vmax=1) pl.title("Estimated C0 matrix") pl.colorbar() python-pot-0.9.7+dfsg/examples/backends/plot_ot_batch.py000066400000000000000000000201671525521106200233570ustar00rootroot00000000000000""" ================================================= Solving Many Optimal Transport Problems in Parallel ================================================= In some situations, one may want to solve many OT problems with the same structure (same number of samples, same cost function, etc.) at the same time. In that case using a for loop to solve the problems sequentially is inefficient. This example shows how to use the batch solvers implemented in POT to solve many problems in parallel on CPU or GPU (even more efficient on GPU). """ # Author: Paul Krzakala # Thibaut Germain # License: MIT License # sphinx_gallery_thumbnail_number = 1 ############################################################################# # # Computing the Cost Matrices # --------------------------------------------- # # We want to create a batch of optimal transport problems with # :math:`n` samples in :math:`d` dimensions. # # To do this, we first need to compute the cost matrices for each problem. # # .. note:: # A straightforward approach would be to use a Python loop and # :func:`ot.dist`. # However, this is inefficient when working with batches. # # Instead, you can directly use :func:`ot.batch.dist_batch`, which computes # all cost matrices in parallel. import ot import numpy as np n_problems = 4 # nb problems/batch size n_samples = 8 # nb samples dim = 2 # nb dimensions np.random.seed(0) samples_source = np.random.randn(n_problems, n_samples, dim) samples_target = samples_source + 0.1 * np.random.randn(n_problems, n_samples, dim) # Naive approach M_list = [] for i in range(n_problems): M_list.append( ot.dist(samples_source[i], samples_target[i]) ) # List of cost matrices n_samples x n_samples # Batched approach M_batch = ot.dist_batch( samples_source, samples_target ) # Array of cost matrices n_problems x n_samples x n_samples for i in range(n_problems): assert np.allclose(M_list[i], M_batch[i]) ############################################################################# # # Solving the Problems # --------------------------------------------- # # Once the cost matrices are computed, we can solve the corresponding # optimal transport problems. # # .. note:: # One option is to solve them sequentially with a Python loop using # :func:`ot.solve`. # This is simple but inefficient for large batches. # # Instead, you can use :func:`ot.batch.solve_batch`, which solves all # problems in parallel. Several methods are available: ["sinkhorn", "log_sinkhorn"] # which solve regularized OT problems, and ["proximal"] which # solves regularized and unregularized OT problem using a proximal point scheme. # By default, the method is set to "auto" which automatically selects the appropriate # method based on the value of `reg`. If `reg` is None or 0, the proximal point method # is used. If `reg` is greater than 0, the Sinkhorn algorithm is used. max_iter = 10000 tol = 1e-4 # Classical OT problem ## Naive approach results_values_list = [] for i in range(n_problems): res = ot.solve(M_list[i], max_iter=max_iter, tol=tol) results_values_list.append(res.value_linear) ## Batched approach results_batch = ot.solve_batch(M=M_batch, max_iter=max_iter, tol=tol) results_values_batch = results_batch.value_linear exact_validated = np.allclose( np.array(results_values_list), results_values_batch, atol=tol * 10 ) # Entropic regularized OT problem ## Naive approach reg = 1.0 results_values_list = [] for i in range(n_problems): res = ot.solve(M_list[i], reg=reg, max_iter=max_iter, tol=tol, reg_type="entropy") results_values_list.append(res.value_linear) ## Batched approach results_batch = ot.solve_batch( M=M_batch, reg=reg, max_iter=max_iter, tol=tol, reg_type="entropy" ) results_values_batch = results_batch.value_linear entropic_validated = np.allclose( np.array(results_values_list), results_values_batch, atol=tol * 10 ) print( f"Exact solve vs proximal batch close: {exact_validated} \nSinkhorn solve vs Sinkhorn solve_batch close: {entropic_validated}" ) ############################################################################# # # Comparing Computation Time # --------------------------------------------- # # We now compare the runtime of the two approaches on larger problems. # # .. note:: # The speedup obtained with :mod:`ot.batch` can be even more # significant when computations are performed on a GPU. from time import perf_counter n_problems = 128 n_samples = 8 dim = 2 reg = 10.0 max_iter = 1000 tol = 1e-3 samples_source = np.random.randn(n_problems, n_samples, dim) samples_target = samples_source + 0.1 * np.random.randn(n_problems, n_samples, dim) def benchmark_naive(samples_source, samples_target): start = perf_counter() for i in range(n_problems): M = ot.dist(samples_source[i], samples_target[i]) res = ot.solve(M, reg=reg, max_iter=max_iter, tol=tol, reg_type="entropy") end = perf_counter() return end - start def benchmark_batch(samples_source, samples_target): start = perf_counter() M_batch = ot.dist_batch(samples_source, samples_target) res_batch = ot.solve_batch( M=M_batch, reg=reg, max_iter=max_iter, tol=tol, reg_type="entropy" ) end = perf_counter() return end - start time_naive = benchmark_naive(samples_source, samples_target) time_batch = benchmark_batch(samples_source, samples_target) print(f"Naive approach time: {time_naive:.4f} seconds") print(f"Batched approach time: {time_batch:.4f} seconds") ############################################################################# # # Gromov-Wasserstein # --------------------------------------------- # # The :mod:`ot.batch` module also provides a batched Gromov-Wasserstein solver. # # .. note:: # This solver is **not** equivalent to calling :func:`ot.solve_gromov` # repeatedly in a loop. # # Key differences: # # - :func:`ot.solve_gromov` # Uses the conditional gradient algorithm. Each inner iteration relies on # an exact EMD solver. # # - :func:`ot.batch.solve_gromov_batch` # Uses a proximal variant, where each inner iteration applies entropic # regularization. # # As a result: # # - :func:`ot.solve_gromov` is usually faster on CPU # - :func:`ot.batch.solve_gromov_batch` is slower on CPU, but provides # better objective values. # # .. tip:: # If your data is on a GPU, :func:`ot.batch.solve_gromov_batch` # is significantly faster AND provides better objective values. from ot import solve_gromov, solve_gromov_batch def benchmark_naive_gw(samples_source, samples_target): start = perf_counter() avg_value = 0 for i in range(n_problems): C1 = ot.dist(samples_source[i], samples_source[i]) C2 = ot.dist(samples_target[i], samples_target[i]) res = solve_gromov(C1, C2, max_iter=1000, tol=tol) avg_value += res.value avg_value /= n_problems end = perf_counter() return end - start, avg_value def benchmark_batch_gw(samples_source, samples_target): start = perf_counter() C1_batch = ot.dist_batch(samples_source, samples_source) C2_batch = ot.dist_batch(samples_target, samples_target) res_batch = solve_gromov_batch( C1_batch, C2_batch, reg=1, max_iter=100, max_iter_inner=50, tol=tol ) avg_value = np.mean(res_batch.value) end = perf_counter() return end - start, avg_value time_naive_gw, avg_value_naive_gw = benchmark_naive_gw(samples_source, samples_target) time_batch_gw, avg_value_batch_gw = benchmark_batch_gw(samples_source, samples_target) print(f"{'Method':<20}{'Time (s)':<15}{'Avg Value':<15}") print(f"{'Naive GW':<20}{time_naive_gw:<15.4f}{avg_value_naive_gw:<15.4f}") print(f"{'Batched GW':<20}{time_batch_gw:<15.4f}{avg_value_batch_gw:<15.4f}") ############################################################################# # # In summary: no more for loops! # --------------------------------------------- import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(4, 4)) ax.text(0.5, 0.5, "For", fontsize=160, ha="center", va="center", zorder=0) ax.axis("off") ax.plot([0, 1], [0, 1], color="red", linewidth=10, zorder=1) ax.plot([0, 1], [1, 0], color="red", linewidth=10, zorder=1) plt.show() python-pot-0.9.7+dfsg/examples/backends/plot_sliced_wass_grad_flow_pytorch.py000066400000000000000000000131621525521106200276650ustar00rootroot00000000000000r""" ============================================================ Sliced Wasserstein barycenter and gradient flow with PyTorch ============================================================ .. note:: Example added in release: 0.8.0. In this example we use the pytorch backend to optimize the sliced Wasserstein loss between two empirical distributions [31]. In the first example one we perform a gradient flow on the support of a distribution that minimize the sliced Wasserstein distance as proposed in [36]. In the second example we optimize with a gradient descent the sliced Wasserstein barycenter between two distributions as in [31]. [31] Bonneel, Nicolas, et al. "Sliced and radon wasserstein barycenters of measures." Journal of Mathematical Imaging and Vision 51.1 (2015): 22-45 [36] Liutkus, A., Simsekli, U., Majewski, S., Durmus, A., & Stöter, F. R. (2019, May). Sliced-Wasserstein flows: Nonparametric generative modeling via optimal transport and diffusions. In International Conference on Machine Learning (pp. 4104-4113). PMLR. """ # Author: Rémi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 4 # %% # Loading the data import numpy as np import matplotlib.pylab as pl import torch import ot import matplotlib.animation as animation I1 = pl.imread("../../data/redcross.png").astype(np.float64)[::5, ::5, 2] I2 = pl.imread("../../data/tooth.png").astype(np.float64)[::5, ::5, 2] sz = I2.shape[0] XX, YY = np.meshgrid(np.arange(sz), np.arange(sz)) x1 = np.stack((XX[I1 == 0], YY[I1 == 0]), 1) * 1.0 x2 = np.stack((XX[I2 == 0] + 60, -YY[I2 == 0] + 32), 1) * 1.0 x3 = np.stack((XX[I2 == 0], -YY[I2 == 0] + 32), 1) * 1.0 pl.figure(1, (8, 4)) pl.scatter(x1[:, 0], x1[:, 1], alpha=0.5) pl.scatter(x2[:, 0], x2[:, 1], alpha=0.5) # %% # Sliced Wasserstein gradient flow with Pytorch # --------------------------------------------- device = "cuda" if torch.cuda.is_available() else "cpu" # use pyTorch for our data x1_torch = torch.tensor(x1).to(device=device).requires_grad_(True) x2_torch = torch.tensor(x2).to(device=device) lr = 1e3 nb_iter_max = 50 x_all = np.zeros((nb_iter_max, x1.shape[0], 2)) loss_iter = [] # generator for random permutations gen = torch.Generator(device=device) gen.manual_seed(42) for i in range(nb_iter_max): loss = ot.sliced_wasserstein_distance( x1_torch, x2_torch, n_projections=20, seed=gen ) loss_iter.append(loss.clone().detach().cpu().numpy()) loss.backward() # performs a step of projected gradient descent with torch.no_grad(): grad = x1_torch.grad x1_torch -= grad * lr / (1 + i / 5e1) # step x1_torch.grad.zero_() x_all[i, :, :] = x1_torch.clone().detach().cpu().numpy() xb = x1_torch.clone().detach().cpu().numpy() pl.figure(2, (8, 4)) pl.scatter(x1[:, 0], x1[:, 1], alpha=0.5, label="$\mu^{(0)}$") pl.scatter(x2[:, 0], x2[:, 1], alpha=0.5, label=r"$\nu$") pl.scatter(xb[:, 0], xb[:, 1], alpha=0.5, label="$\mu^{(100)}$") pl.title("Sliced Wasserstein gradient flow") pl.legend() ax = pl.axis() # %% # Animate trajectories of the gradient flow along iteration # --------------------------------------------------------- pl.figure(3, (8, 4)) def _update_plot(i): pl.clf() pl.scatter(x1[:, 0], x1[:, 1], alpha=0.5, label="$\mu^{(0)}$") pl.scatter(x2[:, 0], x2[:, 1], alpha=0.5, label=r"$\nu$") pl.scatter(x_all[i, :, 0], x_all[i, :, 1], alpha=0.5, label="$\mu^{(100)}$") pl.title("Sliced Wasserstein gradient flow Iter. {}".format(i)) pl.axis(ax) return 1 ani = animation.FuncAnimation( pl.gcf(), _update_plot, nb_iter_max, interval=100, repeat_delay=2000 ) # %% # Compute the Sliced Wasserstein Barycenter # ----------------------------------------- x1_torch = torch.tensor(x1).to(device=device) x3_torch = torch.tensor(x3).to(device=device) xbinit = np.random.randn(500, 2) * 10 + 16 xbary_torch = torch.tensor(xbinit).to(device=device).requires_grad_(True) lr = 1e3 nb_iter_max = 50 x_all = np.zeros((nb_iter_max, xbary_torch.shape[0], 2)) loss_iter = [] # generator for random permutations gen = torch.Generator(device=device) gen.manual_seed(42) alpha = 0.5 for i in range(nb_iter_max): loss = alpha * ot.sliced_wasserstein_distance( xbary_torch, x3_torch, n_projections=50, seed=gen ) + (1 - alpha) * ot.sliced_wasserstein_distance( xbary_torch, x1_torch, n_projections=50, seed=gen ) loss_iter.append(loss.clone().detach().cpu().numpy()) loss.backward() # performs a step of projected gradient descent with torch.no_grad(): grad = xbary_torch.grad xbary_torch -= grad * lr # / (1 + i / 5e1) # step xbary_torch.grad.zero_() x_all[i, :, :] = xbary_torch.clone().detach().cpu().numpy() xb = xbary_torch.clone().detach().cpu().numpy() pl.figure(4, (8, 4)) pl.scatter(x1[:, 0], x1[:, 1], alpha=0.5, label="$\mu$") pl.scatter(x2[:, 0], x2[:, 1], alpha=0.5, label=r"$\nu$") pl.scatter(xb[:, 0] + 30, xb[:, 1], alpha=0.5, label="Barycenter") pl.title("Sliced Wasserstein barycenter") pl.legend() ax = pl.axis() # %% # Animate trajectories of the barycenter along gradient descent # ------------------------------------------------------------- pl.figure(5, (8, 4)) def _update_plot(i): pl.clf() pl.scatter(x1[:, 0], x1[:, 1], alpha=0.5, label="$\mu^{(0)}$") pl.scatter(x2[:, 0], x2[:, 1], alpha=0.5, label=r"$\nu$") pl.scatter(x_all[i, :, 0] + 30, x_all[i, :, 1], alpha=0.5, label="$\mu^{(100)}$") pl.title("Sliced Wasserstein barycenter Iter. {}".format(i)) pl.axis(ax) return 1 ani = animation.FuncAnimation( pl.gcf(), _update_plot, nb_iter_max, interval=100, repeat_delay=2000 ) python-pot-0.9.7+dfsg/examples/backends/plot_ssw_unif_torch.py000066400000000000000000000067771525521106200246430ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ================================================ Spherical Sliced-Wasserstein Embedding on Sphere ================================================ Here, we aim at transforming samples into a uniform distribution on the sphere by minimizing SSW: .. math:: \min_{x} SSW_2(\nu, \frac{1}{n}\sum_{i=1}^n \delta_{x_i}) where :math:`\nu=\mathrm{Unif}(S^{d-1})`. """ # Author: Clément Bonet # # License: MIT License # sphinx_gallery_thumbnail_number = 3 import numpy as np import matplotlib.pyplot as pl import matplotlib.animation as animation import torch import torch.nn.functional as F import ot # %% # Data generation # --------------- torch.manual_seed(1) N = 500 x0 = torch.rand(N, 3) x0 = F.normalize(x0, dim=-1) # %% # Plot data # --------- def plot_sphere(ax): # Create a sphere using spherical coordinates phi = np.linspace(0, 2 * np.pi, 100) theta = np.linspace(0, np.pi, 100) phi, theta = np.meshgrid(phi, theta) # Compute the spherical coordinates X = np.sin(theta) * np.cos(phi) Y = np.sin(theta) * np.sin(phi) Z = np.cos(theta) # Plot the wireframe ax.plot_wireframe(X, Y, Z, color="gray", alpha=0.3) # plot the distributions pl.figure(1) ax = pl.axes(projection="3d") plot_sphere(ax) ax.scatter(x0[:, 0], x0[:, 1], x0[:, 2], label="Data samples", alpha=0.5) ax.set_title("Data distribution") ax.legend() # %% # Gradient descent # ---------------- x = x0.clone() x.requires_grad_(True) n_iter = 100 lr = 150 losses = [] xvisu = torch.zeros(n_iter, N, 3) for i in range(n_iter): sw = ot.sliced_wasserstein_sphere_unif(x, n_projections=500) grad_x = torch.autograd.grad(sw, x)[0] x = x - lr * grad_x / np.sqrt(i / 10 + 1) x = F.normalize(x, p=2, dim=1) losses.append(sw.item()) xvisu[i, :, :] = x.detach().clone() if i % 100 == 0: print("Iter: {:3d}, loss={}".format(i, losses[-1])) pl.figure(1) pl.semilogy(losses) pl.grid() pl.title("SSW") pl.xlabel("Iterations") # %% # Plot trajectories of generated samples along iterations # ------------------------------------------------------- ivisu = [0, 10, 20, 30, 40, 50, 60, 70, 80] fig = pl.figure(3, (10, 10)) for i in range(9): # pl.subplot(3, 3, i + 1) # ax = pl.axes(projection='3d') ax = fig.add_subplot(3, 3, i + 1, projection="3d") plot_sphere(ax) ax.scatter( xvisu[ivisu[i], :, 0], xvisu[ivisu[i], :, 1], xvisu[ivisu[i], :, 2], label="Data samples", alpha=0.5, ) ax.set_title("Iter. {}".format(ivisu[i])) # ax.axis("off") if i == 0: ax.legend() # %% # Animate trajectories of generated samples along iteration # --------------------------------------------------------- pl.figure(4, (8, 8)) def _update_plot(i): i = 3 * i pl.clf() ax = pl.axes(projection="3d") plot_sphere(ax) ax.scatter( xvisu[i, :, 0], xvisu[i, :, 1], xvisu[i, :, 2], label="Data samples$", alpha=0.5 ) ax.axis("off") ax.set_xlim((-1.5, 1.5)) ax.set_ylim((-1.5, 1.5)) ax.set_title("Iter. {}".format(i)) return 1 print(xvisu.shape) i = 0 ax = pl.axes(projection="3d") plot_sphere(ax) ax.scatter( xvisu[i, :, 0], xvisu[i, :, 1], xvisu[i, :, 2], label="Data samples from $G\#\mu_n$", alpha=0.5, ) ax.axis("off") ax.set_xlim((-1.5, 1.5)) ax.set_ylim((-1.5, 1.5)) ax.set_title("Iter. {}".format(ivisu[i])) ani = animation.FuncAnimation( pl.gcf(), _update_plot, n_iter // 5, interval=200, repeat_delay=2000 ) # %% python-pot-0.9.7+dfsg/examples/backends/plot_stoch_continuous_ot_pytorch.py000066400000000000000000000121701525521106200274470ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ====================================================================== Continuous OT plan estimation with Pytorch ====================================================================== .. note:: Example added in release: 0.8.2. """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 3 import numpy as np import matplotlib.pyplot as pl import torch from torch import nn import ot import ot.plot # %% # Data generation # --------------- torch.manual_seed(42) np.random.seed(42) n_source_samples = 1000 n_target_samples = 1000 theta = 2 * np.pi / 20 noise_level = 0.1 Xs = np.random.randn(n_source_samples, 2) * 0.5 Xt = np.random.randn(n_target_samples, 2) * 2 # one of the target mode changes its variance (no linear mapping) Xt = Xt + 4 # %% # Plot data # --------- nvisu = 300 pl.figure(1, (5, 5)) pl.clf() pl.scatter(Xs[:nvisu, 0], Xs[:nvisu, 1], marker="+", label="Source samples", alpha=0.5) pl.scatter(Xt[:nvisu, 0], Xt[:nvisu, 1], marker="o", label="Target samples", alpha=0.5) pl.legend(loc=0) ax_bounds = pl.axis() pl.title("Source and target distributions") # %% # Convert data to torch tensors # ----------------------------- xs = torch.tensor(Xs) xt = torch.tensor(Xt) # %% # Estimating deep dual variables for entropic OT # ---------------------------------------------- torch.manual_seed(42) # define the MLP model class Potential(torch.nn.Module): def __init__(self): super(Potential, self).__init__() self.fc1 = nn.Linear(2, 200) self.fc2 = nn.Linear(200, 1) self.relu = torch.nn.ReLU() # instead of Heaviside step fn def forward(self, x): output = self.fc1(x) output = self.relu(output) # instead of Heaviside step fn output = self.fc2(output) return output.ravel() u = Potential().double() v = Potential().double() reg = 1 optimizer = torch.optim.Adam(list(u.parameters()) + list(v.parameters()), lr=0.005) # number of iteration n_iter = 500 n_batch = 500 losses = [] for i in range(n_iter): # generate noise samples iperms = torch.randint(0, n_source_samples, (n_batch,)) ipermt = torch.randint(0, n_target_samples, (n_batch,)) xsi = xs[iperms] xti = xt[ipermt] # minus because we maximize the dual loss loss = -ot.stochastic.loss_dual_entropic(u(xsi), v(xti), xsi, xti, reg=reg) losses.append(float(loss.detach())) if i % 10 == 0: print("Iter: {:3d}, loss={}".format(i, losses[-1])) loss.backward() optimizer.step() optimizer.zero_grad() pl.figure(2) pl.plot(losses) pl.grid() pl.title("Dual objective (negative)") pl.xlabel("Iterations") # %% # Plot the density on target for a given source sample # ---------------------------------------------------- nv = 100 xl = np.linspace(ax_bounds[0], ax_bounds[1], nv) yl = np.linspace(ax_bounds[2], ax_bounds[3], nv) XX, YY = np.meshgrid(xl, yl) xg = np.concatenate((XX.ravel()[:, None], YY.ravel()[:, None]), axis=1) wxg = np.exp(-((xg[:, 0] - 4) ** 2 + (xg[:, 1] - 4) ** 2) / (2 * 2)) wxg = wxg / np.sum(wxg) xg = torch.tensor(xg) wxg = torch.tensor(wxg) pl.figure(4, (12, 4)) pl.clf() pl.subplot(1, 3, 1) iv = 2 Gg = ot.stochastic.plan_dual_entropic( u(xs[iv : iv + 1, :]), v(xg), xs[iv : iv + 1, :], xg, reg=reg, wt=wxg ) Gg = Gg.reshape((nv, nv)).detach().numpy() pl.scatter(Xs[:nvisu, 0], Xs[:nvisu, 1], marker="+", zorder=2, alpha=0.05) pl.scatter(Xt[:nvisu, 0], Xt[:nvisu, 1], marker="o", zorder=2, alpha=0.05) pl.scatter( Xs[iv : iv + 1, 0], Xs[iv : iv + 1, 1], s=100, marker="+", label="Source sample", zorder=2, alpha=1, color="C0", ) pl.pcolormesh(XX, YY, Gg, cmap="Greens", label="Density of transported source sample") pl.legend(loc=0) ax_bounds = pl.axis() pl.title("Density of transported source sample") pl.subplot(1, 3, 2) iv = 3 Gg = ot.stochastic.plan_dual_entropic( u(xs[iv : iv + 1, :]), v(xg), xs[iv : iv + 1, :], xg, reg=reg, wt=wxg ) Gg = Gg.reshape((nv, nv)).detach().numpy() pl.scatter(Xs[:nvisu, 0], Xs[:nvisu, 1], marker="+", zorder=2, alpha=0.05) pl.scatter(Xt[:nvisu, 0], Xt[:nvisu, 1], marker="o", zorder=2, alpha=0.05) pl.scatter( Xs[iv : iv + 1, 0], Xs[iv : iv + 1, 1], s=100, marker="+", label="Source sample", zorder=2, alpha=1, color="C0", ) pl.pcolormesh(XX, YY, Gg, cmap="Greens", label="Density of transported source sample") pl.legend(loc=0) ax_bounds = pl.axis() pl.title("Density of transported source sample") pl.subplot(1, 3, 3) iv = 6 Gg = ot.stochastic.plan_dual_entropic( u(xs[iv : iv + 1, :]), v(xg), xs[iv : iv + 1, :], xg, reg=reg, wt=wxg ) Gg = Gg.reshape((nv, nv)).detach().numpy() pl.scatter(Xs[:nvisu, 0], Xs[:nvisu, 1], marker="+", zorder=2, alpha=0.05) pl.scatter(Xt[:nvisu, 0], Xt[:nvisu, 1], marker="o", zorder=2, alpha=0.05) pl.scatter( Xs[iv : iv + 1, 0], Xs[iv : iv + 1, 1], s=100, marker="+", label="Source sample", zorder=2, alpha=1, color="C0", ) pl.pcolormesh(XX, YY, Gg, cmap="Greens", label="Density of transported source sample") pl.legend(loc=0) ax_bounds = pl.axis() pl.title("Density of transported source sample") python-pot-0.9.7+dfsg/examples/backends/plot_unmix_optim_torch.py000066400000000000000000000102131525521106200253320ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ================================= Wasserstein unmixing with PyTorch ================================= .. note:: Example added in release: 0.8.0. In this example we estimate mixing parameters from distributions that minimize the Wasserstein distance. In other words we suppose that a target distribution :math:`\mu^t` can be expressed as a weighted sum of source distributions :math:`\mu^s_k` with the following model: .. math:: \mu^t = \sum_{k=1}^K w_k\mu^s_k where :math:`\mathbf{w}` is a vector of size :math:`K` and belongs in the distribution simplex :math:`\Delta_K`. In order to estimate this weight vector we propose to optimize the Wasserstein distance between the model and the observed :math:`\mu^t` with respect to the vector. This leads to the following optimization problem: .. math:: \min_{\mathbf{w}\in\Delta_K} \quad W \left(\mu^t,\sum_{k=1}^K w_k\mu^s_k\right) This minimization is done in this example with a simple projected gradient descent in PyTorch. We use the automatic backend of POT that allows us to compute the Wasserstein distance with :any:`ot.emd2` with differentiable losses. """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pylab as pl import ot import torch ############################################################################## # Generate data # ------------- # %% Data nt = 100 nt1 = 10 # ns1 = 50 ns = 2 * ns1 rng = np.random.RandomState(2) xt = rng.randn(nt, 2) * 0.2 xt[:nt1, 0] += 1 xt[nt1:, 1] += 1 xs1 = rng.randn(ns1, 2) * 0.2 xs1[:, 0] += 1 xs2 = rng.randn(ns1, 2) * 0.2 xs2[:, 1] += 1 xs = np.concatenate((xs1, xs2)) # Sample reweighting matrix H H = np.zeros((ns, 2)) H[:ns1, 0] = 1 / ns1 H[ns1:, 1] = 1 / ns1 # each columns sums to 1 and has weights only for samples form the # corresponding source distribution M = ot.dist(xs, xt) ############################################################################## # Plot data # --------- # %% plot the distributions pl.figure(1) pl.scatter(xt[:, 0], xt[:, 1], label="Target $\mu^t$", alpha=0.5) pl.scatter(xs1[:, 0], xs1[:, 1], label="Source $\mu^s_1$", alpha=0.5) pl.scatter(xs2[:, 0], xs2[:, 1], label="Source $\mu^s_2$", alpha=0.5) pl.title("Sources and Target distributions") pl.legend() ############################################################################## # Optimization of the model wrt the Wasserstein distance # ------------------------------------------------------ # %% Weights optimization with gradient descent # convert numpy arrays to torch tensors H2 = torch.tensor(H) M2 = torch.tensor(M) # weights for the source distributions w = torch.tensor(ot.unif(2), requires_grad=True) # uniform weights for target b = torch.tensor(ot.unif(nt)) lr = 2e-3 # learning rate niter = 500 # number of iterations losses = [] # loss along the iterations # loss for the minimal Wasserstein estimator def get_loss(w): a = torch.mv(H2, w) # distribution reweighting return ot.emd2(a, b, M2) # squared Wasserstein 2 for i in range(niter): loss = get_loss(w) losses.append(float(loss)) loss.backward() with torch.no_grad(): w -= lr * w.grad # gradient step w[:] = ot.utils.proj_simplex(w) # projection on the simplex w.grad.zero_() ############################################################################## # Estimated weights and convergence of the objective # -------------------------------------------------- we = w.detach().numpy() print("Estimated mixture:", we) pl.figure(2) pl.semilogy(losses) pl.grid() pl.title("Wasserstein distance") pl.xlabel("Iterations") ############################################################################## # Plotting the reweighted source distribution # ------------------------------------------- pl.figure(3) # compute source weights ws = H.dot(we) pl.scatter(xt[:, 0], xt[:, 1], label="Target $\mu^t$", alpha=0.5) pl.scatter( xs[:, 0], xs[:, 1], color="C3", s=ws * 20 * ns, label="Weighted sources $\sum_{k} w_k\mu^s_k$", alpha=0.5, ) pl.title("Target and reweighted source distributions") pl.legend() python-pot-0.9.7+dfsg/examples/backends/plot_wass1d_torch.py000066400000000000000000000112761525521106200241760ustar00rootroot00000000000000r""" ================================================= Wasserstein 1D (flow and barycenter) with PyTorch ================================================= In this small example, we consider the following minimization problem: .. math:: \mu^* = \min_\mu W(\mu,\nu) where :math:`\nu` is a reference 1D measure. The problem is handled by a projected gradient descent method, where the gradient is computed by pyTorch automatic differentiation. The projection on the simplex ensures that the iterate will remain on the probability simplex. This example illustrates both `wasserstein_1d` function and backend use within the POT framework. """ # Author: Nicolas Courty # Rémi Flamary # # License: MIT License import numpy as np import matplotlib.pylab as pl import matplotlib as mpl import torch from ot.lp import wasserstein_1d from ot.datasets import make_1D_gauss as gauss from ot.utils import proj_simplex red = np.array(mpl.colors.to_rgb("red")) blue = np.array(mpl.colors.to_rgb("blue")) n = 100 # nb bins # bin positions x = np.arange(n, dtype=np.float64) # Gaussian distributions a = gauss(n, m=20, s=5) # m= mean, s= std b = gauss(n, m=60, s=10) # enforce sum to one on the support a = a / a.sum() b = b / b.sum() device = "cuda" if torch.cuda.is_available() else "cpu" # use pyTorch for our data x_torch = torch.tensor(x).to(device=device) a_torch = torch.tensor(a).to(device=device).requires_grad_(True) b_torch = torch.tensor(b).to(device=device) lr = 1e-6 nb_iter_max = 800 loss_iter = [] pl.figure(1, figsize=(8, 4)) pl.plot(x, a, "b", label="Source distribution") pl.plot(x, b, "r", label="Target distribution") for i in range(nb_iter_max): # Compute the Wasserstein 1D with torch backend loss = wasserstein_1d(x_torch, x_torch, a_torch, b_torch, p=2) # record the corresponding loss value loss_iter.append(loss.clone().detach().cpu().numpy()) loss.backward() # performs a step of projected gradient descent with torch.no_grad(): grad = a_torch.grad a_torch -= a_torch.grad * lr # step a_torch.grad.zero_() a_torch.data = proj_simplex(a_torch) # projection onto the simplex # plot one curve every 10 iterations if i % 10 == 0: mix = float(i) / nb_iter_max pl.plot( x, a_torch.clone().detach().cpu().numpy(), c=(1 - mix) * blue + mix * red ) pl.legend() pl.title("Distribution along the iterations of the projected gradient descent") pl.show() pl.figure(2) pl.plot(range(nb_iter_max), loss_iter, lw=3) pl.title("Evolution of the loss along iterations", fontsize=16) pl.show() # %% # Wasserstein barycenter # ---------------------- # In this example, we consider the following Wasserstein barycenter problem # $$ \\eta^* = \\min_\\eta\;\;\; (1-t)W(\\mu,\\eta) + tW(\\eta,\\nu)$$ # where :math:`\\mu` and :math:`\\nu` are reference 1D measures, and :math:`t` # is a parameter :math:`\in [0,1]`. The problem is handled by a project gradient # descent method, where the gradient is computed by pyTorch automatic differentiation. # The projection on the simplex ensures that the iterate will remain on the # probability simplex. # # This example illustrates both `wasserstein_1d` function and backend use within the # POT framework. device = "cuda" if torch.cuda.is_available() else "cpu" # use pyTorch for our data x_torch = torch.tensor(x).to(device=device) a_torch = torch.tensor(a).to(device=device) b_torch = torch.tensor(b).to(device=device) bary_torch = torch.tensor((a + b).copy() / 2).to(device=device).requires_grad_(True) lr = 1e-6 nb_iter_max = 2000 loss_iter = [] # instant of the interpolation t = 0.5 for i in range(nb_iter_max): # Compute the Wasserstein 1D with torch backend loss = (1 - t) * wasserstein_1d( x_torch, x_torch, a_torch.detach(), bary_torch, p=2 ) + t * wasserstein_1d(x_torch, x_torch, b_torch, bary_torch, p=2) # record the corresponding loss value loss_iter.append(loss.clone().detach().cpu().numpy()) loss.backward() # performs a step of projected gradient descent with torch.no_grad(): grad = bary_torch.grad bary_torch -= bary_torch.grad * lr # step bary_torch.grad.zero_() bary_torch.data = proj_simplex(bary_torch) # projection onto the simplex pl.figure(3, figsize=(8, 4)) pl.plot(x, a, "b", label="Source distribution") pl.plot(x, b, "r", label="Target distribution") pl.plot(x, bary_torch.clone().detach().cpu().numpy(), c="green", label="W barycenter") pl.legend() pl.title("Wasserstein barycenter computed by gradient descent") pl.show() pl.figure(4) pl.plot(range(nb_iter_max), loss_iter, lw=3) pl.title("Evolution of the loss along iterations", fontsize=16) pl.show() python-pot-0.9.7+dfsg/examples/backends/plot_wass2_gan_torch.py000066400000000000000000000143071525521106200246560ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ======================================== Wasserstein 2 Minibatch GAN with PyTorch ======================================== .. note:: Example added in release: 0.8.0. In this example we train a Wasserstein GAN using Wasserstein 2 on minibatches as a distribution fitting term. We want to train a generator :math:`G_\theta` that generates realistic data from random noise drawn form a Gaussian :math:`\mu_n` distribution so that the data is indistinguishable from true data in the data distribution :math:`\mu_d`. To this end Wasserstein GAN [Arjovsky2017] aim at optimizing the parameters :math:`\theta` of the generator with the following optimization problem: .. math:: \min_{\theta} W(\mu_d,G_\theta\#\mu_n) In practice we do not have access to the full distribution :math:`\mu_d` but samples and we cannot compute the Wasserstein distance for large dataset. [Arjovsky2017] proposed to approximate the dual potential of Wasserstein 1 with a neural network recovering an optimization problem similar to GAN. In this example we will optimize the expectation of the Wasserstein distance over minibatches at each iterations as proposed in [Genevay2018]. Optimizing the Minibatches of the Wasserstein distance has been studied in [Fatras2019]. [Arjovsky2017] Arjovsky, M., Chintala, S., & Bottou, L. (2017, July). Wasserstein generative adversarial networks. In International conference on machine learning (pp. 214-223). PMLR. [Genevay2018] Genevay, Aude, Gabriel Peyré, and Marco Cuturi. "Learning generative models with sinkhorn divergences." International Conference on Artificial Intelligence and Statistics. PMLR, 2018. [Fatras2019] Fatras, K., Zine, Y., Flamary, R., Gribonval, R., & Courty, N. (2020, June). Learning with minibatch Wasserstein: asymptotic and gradient properties. In the 23nd International Conference on Artificial Intelligence and Statistics (Vol. 108). """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 3 import numpy as np import matplotlib.pyplot as pl import matplotlib.animation as animation import torch from torch import nn import ot # %% # Data generation # --------------- torch.manual_seed(1) sigma = 0.1 n_dims = 2 n_features = 2 def get_data(n_samples): c = torch.rand(size=(n_samples, 1)) angle = c * 2 * np.pi x = torch.cat((torch.cos(angle), torch.sin(angle)), 1) x += torch.randn(n_samples, 2) * sigma return x # %% # Plot data # --------- # plot the distributions x = get_data(500) pl.figure(1) pl.scatter(x[:, 0], x[:, 1], label="Data samples from $\mu_d$", alpha=0.5) pl.title("Data distribution") pl.legend() # %% # Generator Model # --------------- # define the MLP model class Generator(torch.nn.Module): def __init__(self): super(Generator, self).__init__() self.fc1 = nn.Linear(n_features, 200) self.fc2 = nn.Linear(200, 500) self.fc3 = nn.Linear(500, n_dims) self.relu = torch.nn.ReLU() # instead of Heaviside step fn def forward(self, x): output = self.fc1(x) output = self.relu(output) # instead of Heaviside step fn output = self.fc2(output) output = self.relu(output) output = self.fc3(output) return output # %% # Training the model # ------------------ G = Generator() optimizer = torch.optim.RMSprop(G.parameters(), lr=0.00019, eps=1e-5) # number of iteration and size of the batches n_iter = 200 # set to 200 for doc build but 1000 is better ;) size_batch = 500 # generate statis samples to see their trajectory along training n_visu = 100 xnvisu = torch.randn(n_visu, n_features) xvisu = torch.zeros(n_iter, n_visu, n_dims) ab = torch.ones(size_batch) / size_batch losses = [] for i in range(n_iter): # generate noise samples xn = torch.randn(size_batch, n_features) # generate data samples xd = get_data(size_batch) # generate sample along iterations xvisu[i, :, :] = G(xnvisu).detach() # generate samples and compte distance matrix xg = G(xn) M = ot.dist(xg, xd) loss = ot.emd2(ab, ab, M) losses.append(float(loss.detach())) if i % 10 == 0: print("Iter: {:3d}, loss={}".format(i, losses[-1])) loss.backward() optimizer.step() optimizer.zero_grad() del M pl.figure(2) pl.semilogy(losses) pl.grid() pl.title("Wasserstein distance") pl.xlabel("Iterations") # %% # Plot trajectories of generated samples along iterations # ------------------------------------------------------- pl.figure(3, (10, 10)) ivisu = [0, 10, 25, 50, 75, 125, 15, 175, 199] for i in range(9): pl.subplot(3, 3, i + 1) pl.scatter(xd[:, 0], xd[:, 1], label="Data samples from $\mu_d$", alpha=0.1) pl.scatter( xvisu[ivisu[i], :, 0], xvisu[ivisu[i], :, 1], label="Data samples from $G\#\mu_n$", alpha=0.5, ) pl.xticks(()) pl.yticks(()) pl.title("Iter. {}".format(ivisu[i])) if i == 0: pl.legend() # %% # Animate trajectories of generated samples along iteration # --------------------------------------------------------- pl.figure(4, (8, 8)) def _update_plot(i): pl.clf() pl.scatter(xd[:, 0], xd[:, 1], label="Data samples from $\mu_d$", alpha=0.1) pl.scatter( xvisu[i, :, 0], xvisu[i, :, 1], label="Data samples from $G\#\mu_n$", alpha=0.5 ) pl.xticks(()) pl.yticks(()) pl.xlim((-1.5, 1.5)) pl.ylim((-1.5, 1.5)) pl.title("Iter. {}".format(i)) return 1 i = 0 pl.scatter(xd[:, 0], xd[:, 1], label="Data samples from $\mu_d$", alpha=0.1) pl.scatter( xvisu[i, :, 0], xvisu[i, :, 1], label="Data samples from $G\#\mu_n$", alpha=0.5 ) pl.xticks(()) pl.yticks(()) pl.xlim((-1.5, 1.5)) pl.ylim((-1.5, 1.5)) pl.title("Iter. {}".format(ivisu[i])) ani = animation.FuncAnimation( pl.gcf(), _update_plot, n_iter, interval=100, repeat_delay=2000 ) # %% # Generate and visualize data # --------------------------- size_batch = 500 xd = get_data(size_batch) xn = torch.randn(size_batch, 2) x = G(xn).detach().numpy() pl.figure(5) pl.scatter(xd[:, 0], xd[:, 1], label="Data samples from $\mu_d$", alpha=0.5) pl.scatter(x[:, 0], x[:, 1], label="Data samples from $G\#\mu_n$", alpha=0.5) pl.title("Sources and Target distributions") pl.legend() python-pot-0.9.7+dfsg/examples/barycenters/000077500000000000000000000000001525521106200207255ustar00rootroot00000000000000python-pot-0.9.7+dfsg/examples/barycenters/index.rst000066400000000000000000000000611525521106200225630ustar00rootroot00000000000000 Wasserstein barycenters -----------------------python-pot-0.9.7+dfsg/examples/barycenters/plot_barycenter_1D.py000066400000000000000000000065751525521106200250340ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ============================== 1D Wasserstein barycenter demo ============================== This example illustrates the computation of regularized Wasserstein Barycenter as proposed in [3]. [3] Benamou, J. D., Carlier, G., Cuturi, M., Nenna, L., & Peyré, G. (2015). Iterative Bregman projections for regularized transportation problems SIAM Journal on Scientific Computing, 37(2), A1111-A1138. """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 1 import numpy as np import matplotlib.pyplot as plt import ot # necessary for 3d plot even if not used from mpl_toolkits.mplot3d import Axes3D # noqa from matplotlib.collections import PolyCollection ############################################################################## # Generate data # ------------- # %% parameters n = 100 # nb bins # bin positions x = np.arange(n, dtype=np.float64) # Gaussian distributions a1 = ot.datasets.make_1D_gauss(n, m=20, s=5) # m= mean, s= std a2 = ot.datasets.make_1D_gauss(n, m=60, s=8) # creating matrix A containing all distributions A = np.vstack((a1, a2)).T n_distributions = A.shape[1] # loss matrix + normalization M = ot.utils.dist0(n) M /= M.max() ############################################################################## # Barycenter computation # ---------------------- # %% barycenter computation alpha = 0.2 # 0<=alpha<=1 weights = np.array([1 - alpha, alpha]) # l2bary bary_l2 = A.dot(weights) # wasserstein reg = 1e-3 bary_wass = ot.bregman.barycenter(A, M, reg, weights) f, (ax1, ax2) = plt.subplots(2, 1, tight_layout=True, num=1) ax1.plot(x, A, color="black") ax1.set_title("Distributions") ax2.plot(x, bary_l2, "r", label="l2") ax2.plot(x, bary_wass, "g", label="Wasserstein") ax2.set_title("Barycenters") plt.legend() plt.show() ############################################################################## # Barycentric interpolation # ------------------------- # %% barycenter interpolation n_alpha = 11 alpha_list = np.linspace(0, 1, n_alpha) B_l2 = np.zeros((n, n_alpha)) B_wass = np.copy(B_l2) for i in range(n_alpha): alpha = alpha_list[i] weights = np.array([1 - alpha, alpha]) B_l2[:, i] = A.dot(weights) B_wass[:, i] = ot.bregman.barycenter(A, M, reg, weights) # %% plot interpolation plt.figure(2) cmap = plt.get_cmap("viridis") verts = [] zs = alpha_list for i, z in enumerate(zs): ys = B_l2[:, i] verts.append(list(zip(x, ys))) ax = plt.gcf().add_subplot(projection="3d") poly = PolyCollection(verts, facecolors=[cmap(a) for a in alpha_list]) poly.set_alpha(0.7) ax.add_collection3d(poly, zs=zs, zdir="y") ax.set_xlabel("x") ax.set_xlim3d(0, n) ax.set_ylabel("$\\alpha$") ax.set_ylim3d(0, 1) ax.set_zlabel("") ax.set_zlim3d(0, B_l2.max() * 1.01) plt.title("Barycenter interpolation with l2") plt.tight_layout() plt.figure(3) cmap = plt.get_cmap("viridis") verts = [] zs = alpha_list for i, z in enumerate(zs): ys = B_wass[:, i] verts.append(list(zip(x, ys))) ax = plt.gcf().add_subplot(projection="3d") poly = PolyCollection(verts, facecolors=[cmap(a) for a in alpha_list]) poly.set_alpha(0.7) ax.add_collection3d(poly, zs=zs, zdir="y") ax.set_xlabel("x") ax.set_xlim3d(0, n) ax.set_ylabel("$\\alpha$") ax.set_ylim3d(0, 1) ax.set_zlabel("") ax.set_zlim3d(0, B_l2.max() * 1.01) plt.title("Barycenter interpolation with Wasserstein") plt.tight_layout() plt.show() python-pot-0.9.7+dfsg/examples/barycenters/plot_barycenter_lp_vs_entropic.py000066400000000000000000000135041525521106200276040ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ================================================================================= 1D Wasserstein barycenter: exact LP vs entropic regularization ================================================================================= This example illustrates the computation of regularized Wasserstein Barycenter as proposed in [3] and exact LP barycenters using standard LP solver. It reproduces approximately Figure 3.1 and 3.2 from the following paper: Cuturi, M., & Peyré, G. (2016). A smoothed dual approach for variational Wasserstein problems. SIAM Journal on Imaging Sciences, 9(1), 320-343. [3] Benamou, J. D., Carlier, G., Cuturi, M., Nenna, L., & Peyré, G. (2015). Iterative Bregman projections for regularized transportation problems SIAM Journal on Scientific Computing, 37(2), A1111-A1138. """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 4 import numpy as np import matplotlib.pylab as pl import ot # necessary for 3d plot even if not used from mpl_toolkits.mplot3d import Axes3D # noqa from matplotlib.collections import PolyCollection # noqa # import ot.lp.cvx as cvx ############################################################################## # Gaussian Data # ------------- # %% parameters problems = [] n = 100 # nb bins # bin positions x = np.arange(n, dtype=np.float64) # Gaussian distributions # Gaussian distributions a1 = ot.datasets.make_1D_gauss(n, m=20, s=5) # m= mean, s= std a2 = ot.datasets.make_1D_gauss(n, m=60, s=8) # creating matrix A containing all distributions A = np.vstack((a1, a2)).T n_distributions = A.shape[1] # loss matrix + normalization M = ot.utils.dist0(n) M /= M.max() # %% plot the distributions pl.figure(1, figsize=(6.4, 3)) for i in range(n_distributions): pl.plot(x, A[:, i]) pl.title("Distributions") pl.tight_layout() # %% barycenter computation alpha = 0.5 # 0<=alpha<=1 weights = np.array([1 - alpha, alpha]) # l2bary bary_l2 = A.dot(weights) # wasserstein reg = 1e-3 ot.tic() bary_wass = ot.bregman.barycenter(A, M, reg, weights) ot.toc() ot.tic() bary_wass2 = ot.lp.barycenter(A, M, weights) ot.toc() pl.figure(2) pl.clf() pl.subplot(2, 1, 1) for i in range(n_distributions): pl.plot(x, A[:, i]) pl.title("Distributions") pl.subplot(2, 1, 2) pl.plot(x, bary_l2, "r", label="l2") pl.plot(x, bary_wass, "g", label="Reg Wasserstein") pl.plot(x, bary_wass2, "b", label="LP Wasserstein") pl.legend() pl.title("Barycenters") pl.tight_layout() problems.append([A, [bary_l2, bary_wass, bary_wass2]]) ############################################################################## # Stair Data # ---------- # %% parameters a1 = 1.0 * (x > 10) * (x < 50) a2 = 1.0 * (x > 60) * (x < 80) a1 /= a1.sum() a2 /= a2.sum() # creating matrix A containing all distributions A = np.vstack((a1, a2)).T n_distributions = A.shape[1] # loss matrix + normalization M = ot.utils.dist0(n) M /= M.max() # %% plot the distributions pl.figure(1, figsize=(6.4, 3)) for i in range(n_distributions): pl.plot(x, A[:, i]) pl.title("Distributions") pl.tight_layout() # %% barycenter computation alpha = 0.5 # 0<=alpha<=1 weights = np.array([1 - alpha, alpha]) # l2bary bary_l2 = A.dot(weights) # wasserstein reg = 1e-3 ot.tic() bary_wass = ot.bregman.barycenter(A, M, reg, weights) ot.toc() ot.tic() bary_wass2 = ot.lp.barycenter(A, M, weights) ot.toc() problems.append([A, [bary_l2, bary_wass, bary_wass2]]) pl.figure(2) pl.clf() pl.subplot(2, 1, 1) for i in range(n_distributions): pl.plot(x, A[:, i]) pl.title("Distributions") pl.subplot(2, 1, 2) pl.plot(x, bary_l2, "r", label="l2") pl.plot(x, bary_wass, "g", label="Reg Wasserstein") pl.plot(x, bary_wass2, "b", label="LP Wasserstein") pl.legend() pl.title("Barycenters") pl.tight_layout() ############################################################################## # Dirac Data # ---------- # %% parameters a1 = np.zeros(n) a2 = np.zeros(n) a1[10] = 0.25 a1[20] = 0.5 a1[30] = 0.25 a2[80] = 1 a1 /= a1.sum() a2 /= a2.sum() # creating matrix A containing all distributions A = np.vstack((a1, a2)).T n_distributions = A.shape[1] # loss matrix + normalization M = ot.utils.dist0(n) M /= M.max() # %% plot the distributions pl.figure(1, figsize=(6.4, 3)) for i in range(n_distributions): pl.plot(x, A[:, i]) pl.title("Distributions") pl.tight_layout() # %% barycenter computation alpha = 0.5 # 0<=alpha<=1 weights = np.array([1 - alpha, alpha]) # l2bary bary_l2 = A.dot(weights) # wasserstein reg = 1e-3 ot.tic() bary_wass = ot.bregman.barycenter(A, M, reg, weights) ot.toc() ot.tic() bary_wass2 = ot.lp.barycenter(A, M, weights) ot.toc() problems.append([A, [bary_l2, bary_wass, bary_wass2]]) pl.figure(2) pl.clf() pl.subplot(2, 1, 1) for i in range(n_distributions): pl.plot(x, A[:, i]) pl.title("Distributions") pl.subplot(2, 1, 2) pl.plot(x, bary_l2, "r", label="l2") pl.plot(x, bary_wass, "g", label="Reg Wasserstein") pl.plot(x, bary_wass2, "b", label="LP Wasserstein") pl.legend() pl.title("Barycenters") pl.tight_layout() ############################################################################## # Final figure # ------------ # # %% plot nbm = len(problems) nbm2 = nbm // 2 pl.figure(2, (20, 6)) pl.clf() for i in range(nbm): A = problems[i][0] bary_l2 = problems[i][1][0] bary_wass = problems[i][1][1] bary_wass2 = problems[i][1][2] pl.subplot(2, nbm, 1 + i) for j in range(n_distributions): pl.plot(x, A[:, j]) if i == nbm2: pl.title("Distributions") pl.xticks(()) pl.yticks(()) pl.subplot(2, nbm, 1 + i + nbm) pl.plot(x, bary_l2, "r", label="L2 (Euclidean)") pl.plot(x, bary_wass, "g", label="Reg Wasserstein") pl.plot(x, bary_wass2, "b", label="LP Wasserstein") if i == nbm - 1: pl.legend() if i == nbm2: pl.title("Barycenters") pl.xticks(()) pl.yticks(()) python-pot-0.9.7+dfsg/examples/barycenters/plot_convolutional_barycenter.py000066400000000000000000000051151525521106200274510ustar00rootroot00000000000000# %% # -*- coding: utf-8 -*- """ ============================================ Convolutional Wasserstein Barycenter example ============================================ This example is designed to illustrate how the Convolutional Wasserstein Barycenter function of POT works. """ # Author: Nicolas Courty # # License: MIT License import os from pathlib import Path import numpy as np import matplotlib.pyplot as plt import ot ############################################################################## # Data preparation # ---------------- # # The four distributions are constructed from 4 simple images this_file = os.path.realpath("__file__") data_path = os.path.join(Path(this_file).parent.parent.parent, "data") f1 = 1 - plt.imread(os.path.join(data_path, "redcross.png"))[::2, ::2, 2] f2 = 1 - plt.imread(os.path.join(data_path, "tooth.png"))[::2, ::2, 2] f3 = 1 - plt.imread(os.path.join(data_path, "heart.png"))[::2, ::2, 2] f4 = 1 - plt.imread(os.path.join(data_path, "duck.png"))[::2, ::2, 2] f1 = f1 / np.sum(f1) f2 = f2 / np.sum(f2) f3 = f3 / np.sum(f3) f4 = f4 / np.sum(f4) A = np.array([f1, f2, f3, f4]) nb_images = 5 # those are the four corners coordinates that will be interpolated by bilinear # interpolation v1 = np.array((1, 0, 0, 0)) v2 = np.array((0, 1, 0, 0)) v3 = np.array((0, 0, 1, 0)) v4 = np.array((0, 0, 0, 1)) ############################################################################## # Barycenter computation and visualization # ---------------------------------------- # fig, axes = plt.subplots(nb_images, nb_images, figsize=(7, 7)) plt.suptitle("Convolutional Wasserstein Barycenters in POT") cm = "Blues" # regularization parameter reg = 0.004 for i in range(nb_images): for j in range(nb_images): tx = float(i) / (nb_images - 1) ty = float(j) / (nb_images - 1) # weights are constructed by bilinear interpolation tmp1 = (1 - tx) * v1 + tx * v2 tmp2 = (1 - tx) * v3 + tx * v4 weights = (1 - ty) * tmp1 + ty * tmp2 if i == 0 and j == 0: axes[i, j].imshow(f1, cmap=cm) elif i == 0 and j == (nb_images - 1): axes[i, j].imshow(f3, cmap=cm) elif i == (nb_images - 1) and j == 0: axes[i, j].imshow(f2, cmap=cm) elif i == (nb_images - 1) and j == (nb_images - 1): axes[i, j].imshow(f4, cmap=cm) else: # call to barycenter computation axes[i, j].imshow( ot.bregman.convolutional_barycenter2d(A, reg, weights), cmap=cm ) axes[i, j].axis("off") plt.tight_layout() plt.show() python-pot-0.9.7+dfsg/examples/barycenters/plot_debiased_barycenter.py000066400000000000000000000100321525521106200263070ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ================================= Debiased Sinkhorn barycenter demo ================================= .. note:: Example added in release: 0.8.0. This example illustrates the computation of the debiased Sinkhorn barycenter as proposed in [37]_. .. [37] Janati, H., Cuturi, M., Gramfort, A. Proceedings of the 37th International Conference on Machine Learning, PMLR 119:4692-4701, 2020 """ # Author: Hicham Janati # # License: MIT License # sphinx_gallery_thumbnail_number = 3 import os from pathlib import Path import numpy as np import matplotlib.pyplot as plt import ot from ot.bregman import ( barycenter, barycenter_debiased, convolutional_barycenter2d, convolutional_barycenter2d_debiased, ) ############################################################################## # Debiased barycenter of 1D Gaussians # ------------------------------------ # %% parameters n = 100 # nb bins # bin positions x = np.arange(n, dtype=np.float64) # Gaussian distributions a1 = ot.datasets.make_1D_gauss(n, m=20, s=5) # m= mean, s= std a2 = ot.datasets.make_1D_gauss(n, m=60, s=8) # creating matrix A containing all distributions A = np.vstack((a1, a2)).T n_distributions = A.shape[1] # loss matrix + normalization M = ot.utils.dist0(n) M /= M.max() # %% barycenter computation alpha = 0.2 # 0<=alpha<=1 weights = np.array([1 - alpha, alpha]) epsilons = [5e-3, 1e-2, 5e-2] bars = [barycenter(A, M, reg, weights) for reg in epsilons] bars_debiased = [barycenter_debiased(A, M, reg, weights) for reg in epsilons] labels = ["Sinkhorn barycenter", "Debiased barycenter"] colors = ["indianred", "gold"] f, axes = plt.subplots( 1, len(epsilons), tight_layout=True, sharey=True, figsize=(12, 4), num=1 ) for ax, eps, bar, bar_debiased in zip(axes, epsilons, bars, bars_debiased): ax.plot(A[:, 0], color="k", ls="--", label="Input data", alpha=0.3) ax.plot(A[:, 1], color="k", ls="--", alpha=0.3) for data, label, color in zip([bar, bar_debiased], labels, colors): ax.plot(data, color=color, label=label, lw=2) ax.set_title(r"$\varepsilon = %.3f$" % eps) plt.legend() plt.show() ############################################################################## # Debiased barycenter of 2D images # --------------------------------- this_file = os.path.realpath("__file__") data_path = os.path.join(Path(this_file).parent.parent.parent, "data") f1 = 1 - plt.imread(os.path.join(data_path, "heart.png"))[:, :, 2] f2 = 1 - plt.imread(os.path.join(data_path, "duck.png"))[:, :, 2] A = np.asarray([f1, f2]) + 1e-2 A /= A.sum(axis=(1, 2))[:, None, None] ############################################################################## # Display the input images fig, axes = plt.subplots(1, 2, figsize=(7, 4), num=2) for ax, img in zip(axes, A): ax.imshow(img, cmap="Greys") ax.axis("off") fig.tight_layout() plt.show() ############################################################################## # Barycenter computation and visualization # ---------------------------------------- # bars_sinkhorn, bars_debiased = [], [] epsilons = [5e-3, 7e-3, 1e-2] for eps in epsilons: bar = convolutional_barycenter2d(A, eps) bar_debiased, log = convolutional_barycenter2d_debiased(A, eps, log=True) bars_sinkhorn.append(bar) bars_debiased.append(bar_debiased) titles = ["Sinkhorn", "Debiased"] all_bars = [bars_sinkhorn, bars_debiased] fig, axes = plt.subplots(2, 3, figsize=(8, 6), num=3) for jj, (method, ax_row, bars) in enumerate(zip(titles, axes, all_bars)): for ii, (ax, img, eps) in enumerate(zip(ax_row, bars, epsilons)): ax.imshow(img, cmap="Greys") if jj == 0: ax.set_title(r"$\varepsilon = %.3f$" % eps, fontsize=13) ax.set_xticks([]) ax.set_yticks([]) ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) ax.spines["bottom"].set_visible(False) ax.spines["left"].set_visible(False) if ii == 0: ax.set_ylabel(method, fontsize=15) fig.tight_layout() plt.show() python-pot-0.9.7+dfsg/examples/barycenters/plot_free_support_barycenter.py000066400000000000000000000054421525521106200272750ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ======================================================== 2D free support Wasserstein barycenters of distributions ======================================================== Illustration of 2D Wasserstein and Sinkhorn barycenters if distributions are weighted sum of Diracs. """ # Authors: Vivien Seguy # Rémi Flamary # Eduardo Fernandes Montesuma # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pylab as pl import ot # %% # Generate data # ------------- N = 2 d = 2 I1 = pl.imread("../../data/redcross.png").astype(np.float64)[::4, ::4, 2] I2 = pl.imread("../../data/duck.png").astype(np.float64)[::4, ::4, 2] sz = I2.shape[0] XX, YY = np.meshgrid(np.arange(sz), np.arange(sz)) x1 = np.stack((XX[I1 == 0], YY[I1 == 0]), 1) * 1.0 x2 = np.stack((XX[I2 == 0] + 80, -YY[I2 == 0] + 32), 1) * 1.0 x3 = np.stack((XX[I2 == 0], -YY[I2 == 0] + 32), 1) * 1.0 measures_locations = [x1, x2] measures_weights = [ot.unif(x1.shape[0]), ot.unif(x2.shape[0])] pl.figure(1, (12, 4)) pl.scatter(x1[:, 0], x1[:, 1], alpha=0.5) pl.scatter(x2[:, 0], x2[:, 1], alpha=0.5) pl.title("Distributions") # %% # Compute free support Wasserstein barycenter # ------------------------------------------- k = 200 # number of Diracs of the barycenter X_init = np.random.normal(0.0, 1.0, (k, d)) # initial Dirac locations b = ( np.ones((k,)) / k ) # weights of the barycenter (it will not be optimized, only the locations are optimized) X = ot.lp.free_support_barycenter(measures_locations, measures_weights, X_init, b) # %% # Plot the Wasserstein barycenter # ------------------------------- pl.figure(2, (8, 3)) pl.scatter(x1[:, 0], x1[:, 1], alpha=0.5) pl.scatter(x2[:, 0], x2[:, 1], alpha=0.5) pl.scatter(X[:, 0], X[:, 1], s=b * 1000, marker="s", label="2-Wasserstein barycenter") pl.title("Data measures and their barycenter") pl.legend(loc="lower right") pl.show() # %% # Compute free support Sinkhorn barycenter k = 200 # number of Diracs of the barycenter X_init = np.random.normal(0.0, 1.0, (k, d)) # initial Dirac locations b = ( np.ones((k,)) / k ) # weights of the barycenter (it will not be optimized, only the locations are optimized) X = ot.bregman.free_support_sinkhorn_barycenter( measures_locations, measures_weights, X_init, 20, b, numItermax=15 ) # %% # Plot the Wasserstein barycenter # ------------------------------- pl.figure(2, (8, 3)) pl.scatter(x1[:, 0], x1[:, 1], alpha=0.5) pl.scatter(x2[:, 0], x2[:, 1], alpha=0.5) pl.scatter(X[:, 0], X[:, 1], s=b * 1000, marker="s", label="2-Wasserstein barycenter") pl.title("Data measures and their barycenter") pl.legend(loc="lower right") pl.show() python-pot-0.9.7+dfsg/examples/barycenters/plot_free_support_barycenter_generic_cost.py000066400000000000000000000220441525521106200320160ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ===================================== OT Barycenter with Generic Costs Demo ===================================== This example illustrates the computation of an Optimal Transport Barycenter for a ground cost that is not a power of a norm. We take the example of ground costs :math:`c_k(x, y) = \lambda_k\|P_k(x)-y\|_2^2`, where :math:`P_k` is the (non-linear) projection onto a circle k, and :math:`(\lambda_k)` are weights. A barycenter is defined ([77]) as a minimiser of the energy :math:`V(\mu) = \sum_k \mathcal{T}_{c_k}(\mu, \nu_k)` where :math:`\mu` is a candidate barycenter measure, the measures :math:`\nu_k` are the target measures and :math:`\mathcal{T}_{c_k}` is the OT cost for ground cost :math:`c_k`. This is an example of the fixed-point barycenter solver introduced in [77] which generalises [20] and [43]. The ground barycenter function :math:`B(y_1, ..., y_K) = \mathrm{argmin}_{x \in \mathbb{R}^2} \sum_k \lambda_k c_k(x, y_k)` is computed by gradient descent over :math:`x` with Pytorch. We compare two algorithms from [77]: the first ([77], Algorithm 2, 'true_fixed_point' in POT) has convergence guarantees but the iterations may increase in support size and thus require more computational resources. The second ([77], Algorithm 3, 'L2_barycentric_proj' in POT) is a simplified heuristic that imposes a fixed support size for the barycenter and fixed weights. We initialise both algorithms with a support size of 136, computing a barycenter between measures with uniform weights and 50 points. [77] Tanguy, Eloi and Delon, Julie and Gozlan, Nathaël (2024). Computing Barycentres of Measures for Generic Transport Costs. arXiv preprint 2501.04016 (2024) [20] Cuturi, M. and Doucet, A. (2014) Fast Computation of Wasserstein Barycenters. InternationalConference in Machine Learning [43] �lvarez-Esteban, Pedro C., et al. A fixed-point approach to barycenters in Wasserstein space. Journal of Mathematical Analysis and Applications 441.2 (2016): 744-762. """ # Author: Eloi Tanguy # # License: MIT License # sphinx_gallery_thumbnail_number = 1 # %% # Generate data import torch from torch.optim import Adam from ot.utils import dist import numpy as np from ot.lp import free_support_barycenter_generic_costs import matplotlib.pyplot as plt from time import time torch.manual_seed(42) n = 136 # number of points of the barycentre d = 2 # dimensions of the original measure K = 4 # number of measures to barycentre m_list = [49, 50, 51, 51] # number of points of the measures b_list = [torch.ones(m) / m for m in m_list] # weights of the 4 measures weights = torch.ones(K) / K # weights for the barycentre stop_threshold = 1e-20 # stop threshold for B and for fixed-point algo # map R^2 -> R^2 projection onto circle def proj_circle(X, origin, radius): diffs = X - origin[None, :] norms = torch.norm(diffs, dim=1) return origin[None, :] + radius * diffs / norms[:, None] # circles on which to project origin1 = torch.tensor([-1.0, -1.0]) origin2 = torch.tensor([-1.0, 2.0]) origin3 = torch.tensor([2.0, 2.0]) origin4 = torch.tensor([2.0, -1.0]) r = np.sqrt(2) P_list = [ lambda X: proj_circle(X, origin1, r), lambda X: proj_circle(X, origin2, r), lambda X: proj_circle(X, origin3, r), lambda X: proj_circle(X, origin4, r), ] # measures to barycentre are projections of different random circles # onto the K circles Y_list = [] for k in range(K): t = torch.rand(m_list[k]) * 2 * np.pi X_temp = 0.5 * torch.stack([torch.cos(t), torch.sin(t)], axis=1) X_temp = X_temp + torch.tensor([0.5, 0.5])[None, :] Y_list.append(P_list[k](X_temp)) # %% # Define costs and ground barycenter function # cost_list[k] is a function taking x (n, d) and y (n_k, d_k) and returning a # (n, n_k) matrix of costs def c1(x, y): return dist(P_list[0](x), y) def c2(x, y): return dist(P_list[1](x), y) def c3(x, y): return dist(P_list[2](x), y) def c4(x, y): return dist(P_list[3](x), y) cost_list = [c1, c2, c3, c4] # batched total ground cost function for candidate points x (n, d) # for computation of the ground barycenter B with gradient descent def C(x, y): """ Computes the barycenter cost for candidate points x (n, d) and measure supports y: List(n, d_k). """ n = x.shape[0] K = len(y) out = torch.zeros(n) for k in range(K): out += (1 / K) * torch.sum((P_list[k](x) - y[k]) ** 2, axis=1) return out # ground barycenter function def B(y, its=150, lr=1, stop_threshold=stop_threshold): """ Computes the ground barycenter for measure supports y: List(n, d_k). Output: (n, d) array """ x = torch.randn(y[0].shape[0], d) x.requires_grad_(True) opt = Adam([x], lr=lr) for _ in range(its): x_prev = x.data.clone() opt.zero_grad() loss = torch.sum(C(x, y)) loss.backward() opt.step() diff = torch.sum((x.data - x_prev) ** 2) if diff < stop_threshold: break return x # %% # Compute the barycenter measure with the true fixed-point algorithm fixed_point_its = 5 torch.manual_seed(42) X_init = torch.rand(n, d) t0 = time() X_bar, a_bar, log_dict = free_support_barycenter_generic_costs( Y_list, b_list, X_init, cost_list, B, numItermax=fixed_point_its, stopThr=stop_threshold, method="true_fixed_point", log=True, clean_measure=True, ) dt_true_fixed_point = time() - t0 # %% # Compute the barycenter measure with the barycentric (default) algorithm fixed_point_its = 5 torch.manual_seed(42) X_init = torch.rand(n, d) t0 = time() X_bar2, log_dict2 = free_support_barycenter_generic_costs( Y_list, b_list, X_init, cost_list, B, numItermax=fixed_point_its, stopThr=stop_threshold, log=True, ) dt_barycentric = time() - t0 # %% # Plot Barycenters (Iteration 3) alpha = 0.4 s = 80 labels = ["circle 1", "circle 2", "circle 3", "circle 4"] fig, axes = plt.subplots(1, 2, figsize=(12, 6)) # Plot for the true fixed-point algorithm for Y, label in zip(Y_list, labels): axes[0].scatter(*(Y.numpy()).T, alpha=alpha, label=label, s=s) axes[0].scatter( *(X_bar.detach().numpy()).T, label="Barycenter", c="black", alpha=alpha * a_bar.numpy() / np.max(a_bar.numpy()), s=s, ) axes[0].set_title( "True Fixed-Point Algorithm\n" f"Support size: {a_bar.shape[0]}\n" f"Barycenter cost: {log_dict['V_list'][-1].item():.6f}\n" f"Computation time {dt_true_fixed_point:.4f}s" ) axes[0].axis("equal") axes[0].axis("off") axes[0].legend() # Plot for the heuristic algorithm for Y, label in zip(Y_list, labels): axes[1].scatter(*(Y.numpy()).T, alpha=alpha, label=label, s=s) axes[1].scatter( *(X_bar2.detach().numpy()).T, label="Barycenter", c="black", alpha=alpha, s=s ) axes[1].set_title( "Heuristic Barycentric Algorithm\n" f"Support size: {X_bar2.shape[0]}\n" f"Barycenter cost: {log_dict2['V_list'][-1].item():.6f}\n" f"Computation time {dt_barycentric:.4f}s" ) axes[1].axis("equal") axes[1].axis("off") axes[1].legend() plt.tight_layout() # %% # Plot energy convergence and support sizes size = 3 n_plots = 4 fig, axes = plt.subplots(1, n_plots, figsize=(size * n_plots, size)) V_list = [V.item() for V in log_dict["V_list"]] V_list2 = [V.item() for V in log_dict2["V_list"]] diff = np.array(V_list2) - np.array(V_list) # Plot for True Fixed-Point Algorithm axes[0].plot(V_list, lw=5, alpha=0.6) axes[0].scatter(range(len(V_list)), V_list, color="blue", alpha=0.8, s=100) axes[0].set_title("True Fixed-Point Algorithm") axes[0].set_xlabel("Iteration") axes[0].set_ylabel("Barycenter Energy") axes[0].set_yscale("log") axes[0].xaxis.set_major_locator(plt.MaxNLocator(integer=True)) # Plot for Heuristic Barycentric Algorithm axes[1].plot(V_list2, lw=5, alpha=0.6) axes[1].scatter(range(len(V_list2)), V_list2, color="blue", alpha=0.8, s=100) axes[1].set_title("Heuristic Barycentric Algorithm") axes[1].set_xlabel("Iteration") axes[1].set_ylabel("Barycenter Energy") axes[1].set_yscale("log") axes[1].xaxis.set_major_locator(plt.MaxNLocator(integer=True)) # Plot difference between the two axes[2].plot(diff, lw=5, alpha=0.6) axes[2].scatter(range(len(diff)), diff, color="blue", alpha=0.8, s=100) axes[2].set_title("Heuristic Fixed-Point Energy - True") axes[2].set_xlabel("Iteration") axes[2].set_ylabel("$V_{\\mathrm{heuristic}} - V_{\\mathrm{true}}$") axes[2].set_yscale("log") axes[2].xaxis.set_major_locator(plt.MaxNLocator(integer=True)) # plot support sizes support_sizes = [Xi.shape[0] for Xi in log_dict["X_list"]] support_sizes2 = [Xi.shape[0] for Xi in log_dict2["X_list"]] axes[3].plot(support_sizes, color="C0", lw=5, alpha=0.6, label="True FP") axes[3].scatter( range(len(support_sizes)), support_sizes, color="blue", alpha=0.8, s=100 ) axes[3].plot(support_sizes2, color="red", lw=5, alpha=0.6, label="Heur. FP") axes[3].scatter( range(len(support_sizes2)), support_sizes2, color="red", alpha=0.8, s=100 ) axes[3].legend(loc="best") axes[3].set_xlabel("Iteration") axes[3].xaxis.set_major_locator(plt.MaxNLocator(integer=True)) axes[3].set_title("Support Sizes") plt.tight_layout() plt.show() # %% python-pot-0.9.7+dfsg/examples/barycenters/plot_free_support_sinkhorn_barycenter.py000066400000000000000000000110431525521106200312020ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ======================================================== 2D free support Sinkhorn barycenters of distributions ======================================================== .. note:: Example added in release: 0.9.1. Illustration of Sinkhorn barycenter calculation between empirical distributions understood as point clouds """ # Authors: Eduardo Fernandes Montesuma # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pyplot as plt import ot # %% # General Parameters # ------------------ reg = 1e-2 # Entropic Regularization numItermax = 20 # Maximum number of iterations for the Barycenter algorithm numInnerItermax = 50 # Maximum number of sinkhorn iterations n_samples = 200 # %% # Generate Data # ------------- X1 = np.random.randn(200, 2) X2 = 2 * np.concatenate( [ np.concatenate([-np.ones([50, 1]), np.linspace(-1, 1, 50)[:, None]], axis=1), np.concatenate([np.linspace(-1, 1, 50)[:, None], np.ones([50, 1])], axis=1), np.concatenate([np.ones([50, 1]), np.linspace(1, -1, 50)[:, None]], axis=1), np.concatenate([np.linspace(1, -1, 50)[:, None], -np.ones([50, 1])], axis=1), ], axis=0, ) X3 = np.random.randn(200, 2) X3 = 2 * (X3 / np.linalg.norm(X3, axis=1)[:, None]) X4 = np.random.multivariate_normal( np.array([0, 0]), np.array([[1.0, 0.5], [0.5, 1.0]]), size=200 ) a1, a2, a3, a4 = ot.unif(len(X1)), ot.unif(len(X1)), ot.unif(len(X1)), ot.unif(len(X1)) # %% # Inspect generated distributions # ------------------------------- fig, axes = plt.subplots(1, 4, figsize=(16, 4)) axes[0].scatter(x=X1[:, 0], y=X1[:, 1], c="steelblue", edgecolor="k") axes[1].scatter(x=X2[:, 0], y=X2[:, 1], c="steelblue", edgecolor="k") axes[2].scatter(x=X3[:, 0], y=X3[:, 1], c="steelblue", edgecolor="k") axes[3].scatter(x=X4[:, 0], y=X4[:, 1], c="steelblue", edgecolor="k") axes[0].set_xlim([-3, 3]) axes[0].set_ylim([-3, 3]) axes[0].set_title("Distribution 1") axes[1].set_xlim([-3, 3]) axes[1].set_ylim([-3, 3]) axes[1].set_title("Distribution 2") axes[2].set_xlim([-3, 3]) axes[2].set_ylim([-3, 3]) axes[2].set_title("Distribution 3") axes[3].set_xlim([-3, 3]) axes[3].set_ylim([-3, 3]) axes[3].set_title("Distribution 4") plt.tight_layout() plt.show() # %% # Interpolating Empirical Distributions # ------------------------------------- fig = plt.figure(figsize=(10, 10)) weights = np.array( [ [3 / 3, 0 / 3], [2 / 3, 1 / 3], [1 / 3, 2 / 3], [0 / 3, 3 / 3], ] ).astype(np.float32) for k in range(4): XB_init = np.random.randn(n_samples, 2) XB = ot.bregman.free_support_sinkhorn_barycenter( measures_locations=[X1, X2], measures_weights=[a1, a2], weights=weights[k], X_init=XB_init, reg=reg, numItermax=numItermax, numInnerItermax=numInnerItermax, ) ax = plt.subplot2grid((4, 4), (0, k)) ax.scatter(XB[:, 0], XB[:, 1], color="steelblue", edgecolor="k") ax.set_xlim([-3, 3]) ax.set_ylim([-3, 3]) for k in range(1, 4, 1): XB_init = np.random.randn(n_samples, 2) XB = ot.bregman.free_support_sinkhorn_barycenter( measures_locations=[X1, X3], measures_weights=[a1, a2], weights=weights[k], X_init=XB_init, reg=reg, numItermax=numItermax, numInnerItermax=numInnerItermax, ) ax = plt.subplot2grid((4, 4), (k, 0)) ax.scatter(XB[:, 0], XB[:, 1], color="steelblue", edgecolor="k") ax.set_xlim([-3, 3]) ax.set_ylim([-3, 3]) for k in range(1, 4, 1): XB_init = np.random.randn(n_samples, 2) XB = ot.bregman.free_support_sinkhorn_barycenter( measures_locations=[X3, X4], measures_weights=[a1, a2], weights=weights[k], X_init=XB_init, reg=reg, numItermax=numItermax, numInnerItermax=numInnerItermax, ) ax = plt.subplot2grid((4, 4), (3, k)) ax.scatter(XB[:, 0], XB[:, 1], color="steelblue", edgecolor="k") ax.set_xlim([-3, 3]) ax.set_ylim([-3, 3]) for k in range(1, 3, 1): XB_init = np.random.randn(n_samples, 2) XB = ot.bregman.free_support_sinkhorn_barycenter( measures_locations=[X2, X4], measures_weights=[a1, a2], weights=weights[k], X_init=XB_init, reg=reg, numItermax=numItermax, numInnerItermax=numInnerItermax, ) ax = plt.subplot2grid((4, 4), (k, 3)) ax.scatter(XB[:, 0], XB[:, 1], color="steelblue", edgecolor="k") ax.set_xlim([-3, 3]) ax.set_ylim([-3, 3]) plt.show() python-pot-0.9.7+dfsg/examples/barycenters/plot_gaussian_barycenter.py000066400000000000000000000057671525521106200264040ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ======================================================== Gaussian Bures-Wasserstein barycenters ======================================================== .. note:: Example added in release: 0.9.2. Illustration of Gaussian Bures-Wasserstein barycenters. """ # Authors: Rémi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 2 # %% from matplotlib import colors from matplotlib.patches import Ellipse import numpy as np import matplotlib.pylab as pl import ot # %% # Define Gaussian Covariances and distributions # --------------------------------------------- C1 = np.array([[0.5, -0.4], [-0.4, 0.5]]) C2 = np.array([[1, 0.3], [0.3, 1]]) C3 = np.array([[1.5, 0], [0, 0.5]]) C4 = np.array([[0.5, 0], [0, 1.5]]) C = np.stack((C1, C2, C3, C4)) m1 = np.array([0, 0]) m2 = np.array([0, 4]) m3 = np.array([4, 0]) m4 = np.array([4, 4]) m = np.stack((m1, m2, m3, m4)) # %% # Plot the distributions # ---------------------- def draw_cov(mu, C, color=None, label=None, nstd=1): def eigsorted(cov): vals, vecs = np.linalg.eigh(cov) order = vals.argsort()[::-1] return vals[order], vecs[:, order] vals, vecs = eigsorted(C) theta = np.degrees(np.arctan2(*vecs[:, 0][::-1])) w, h = 2 * nstd * np.sqrt(vals) ell = Ellipse( xy=(mu[0], mu[1]), width=w, height=h, alpha=0.5, angle=theta, facecolor=color, edgecolor=color, label=label, fill=True, ) pl.gca().add_artist(ell) # pl.scatter(mu[0],mu[1],color=color, marker='x') axis = [-1.5, 5.5, -1.5, 5.5] pl.figure(1, (8, 2)) pl.clf() pl.subplot(1, 4, 1) draw_cov(m1, C1, color="C0") pl.axis(axis) pl.title("$\mathcal{N}(m_1,\Sigma_1)$") pl.subplot(1, 4, 2) draw_cov(m2, C2, color="C1") pl.axis(axis) pl.title("$\mathcal{N}(m_2,\Sigma_2)$") pl.subplot(1, 4, 3) draw_cov(m3, C3, color="C2") pl.axis(axis) pl.title("$\mathcal{N}(m_3,\Sigma_3)$") pl.subplot(1, 4, 4) draw_cov(m4, C4, color="C3") pl.axis(axis) pl.title("$\mathcal{N}(m_4,\Sigma_4)$") # %% # Compute Bures-Wasserstein barycenters and plot them # ------------------------------------------- # basis for bilinear interpolation v1 = np.array((1, 0, 0, 0)) v2 = np.array((0, 1, 0, 0)) v3 = np.array((0, 0, 1, 0)) v4 = np.array((0, 0, 0, 1)) colors = np.stack( (colors.to_rgb("C0"), colors.to_rgb("C1"), colors.to_rgb("C2"), colors.to_rgb("C3")) ) pl.figure(2, (8, 8)) nb_interp = 6 for i in range(nb_interp): for j in range(nb_interp): tx = float(i) / (nb_interp - 1) ty = float(j) / (nb_interp - 1) # weights are constructed by bilinear interpolation tmp1 = (1 - tx) * v1 + tx * v2 tmp2 = (1 - tx) * v3 + tx * v4 weights = (1 - ty) * tmp1 + ty * tmp2 color = np.dot(colors.T, weights) mb, Cb = ot.gaussian.bures_wasserstein_barycenter(m, C, weights) draw_cov(mb, Cb, color=color, label=None, nstd=0.3) pl.axis(axis) pl.axis("off") pl.tight_layout() python-pot-0.9.7+dfsg/examples/barycenters/plot_generalized_free_support_barycenter.py000066400000000000000000000105521525521106200316440ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ======================================= Generalized Wasserstein Barycenter Demo ======================================= .. note:: Example added in release: 0.9.1. This example illustrates the computation of Generalized Wasserstein Barycenter as proposed in [42]. [42] Delon, J., Gozlan, N., and Saint-Dizier, A.. Generalized Wasserstein barycenters between probability measures living on different subspaces. arXiv preprint arXiv:2105.09755, 2021. """ # Author: Eloi Tanguy # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pyplot as plt import matplotlib.pylab as pl import ot import matplotlib.animation as animation ######################## # Generate and plot data # ---------------------- # Input measures sub_sample_factor = 8 I1 = pl.imread("../../data/redcross.png").astype(np.float64)[ ::sub_sample_factor, ::sub_sample_factor, 2 ] I2 = pl.imread("../../data/tooth.png").astype(np.float64)[ ::-sub_sample_factor, ::sub_sample_factor, 2 ] I3 = pl.imread("../../data/heart.png").astype(np.float64)[ ::-sub_sample_factor, ::sub_sample_factor, 2 ] sz = I1.shape[0] UU, VV = np.meshgrid(np.arange(sz), np.arange(sz)) # Input measure locations in their respective 2D spaces X_list = [np.stack((UU[im == 0], VV[im == 0]), 1) * 1.0 for im in [I1, I2, I3]] # Input measure weights a_list = [ot.unif(x.shape[0]) for x in X_list] # Projections 3D -> 2D P1 = np.array([[1, 0, 0], [0, 1, 0]]) P2 = np.array([[0, 1, 0], [0, 0, 1]]) P3 = np.array([[1, 0, 0], [0, 0, 1]]) P_list = [P1, P2, P3] # Barycenter weights weights = np.array([1 / 3, 1 / 3, 1 / 3]) # Number of barycenter points to compute n_samples_bary = 150 # Send the input measures into 3D space for visualization X_visu = [Xi @ Pi for (Xi, Pi) in zip(X_list, P_list)] # Plot the input data fig = plt.figure(figsize=(3, 3)) axis = fig.add_subplot(1, 1, 1, projection="3d") for Xi in X_visu: axis.scatter(Xi[:, 0], Xi[:, 1], Xi[:, 2], marker="o", alpha=0.6) axis.view_init(azim=45) axis.set_xticks([]) axis.set_yticks([]) axis.set_zticks([]) plt.show() ################################# # Barycenter computation and plot # ------------------------------- Y = ot.lp.generalized_free_support_barycenter(X_list, a_list, P_list, n_samples_bary) fig = plt.figure(figsize=(3, 3)) axis = fig.add_subplot(1, 1, 1, projection="3d") for Xi in X_visu: axis.scatter(Xi[:, 0], Xi[:, 1], Xi[:, 2], marker="o", alpha=0.6) axis.scatter(Y[:, 0], Y[:, 1], Y[:, 2], marker="o", alpha=0.6) axis.view_init(azim=45) axis.set_xticks([]) axis.set_yticks([]) axis.set_zticks([]) plt.show() ############################# # Plotting projection matches # --------------------------- fig = plt.figure(figsize=(9, 3)) ax = fig.add_subplot(1, 3, 1, projection="3d") for Xi in X_visu: ax.scatter(Xi[:, 0], Xi[:, 1], Xi[:, 2], marker="o", alpha=0.6) ax.scatter(Y[:, 0], Y[:, 1], Y[:, 2], marker="o", alpha=0.6) ax.view_init(elev=0, azim=0) ax.set_xticks([]) ax.set_yticks([]) ax.set_zticks([]) ax = fig.add_subplot(1, 3, 2, projection="3d") for Xi in X_visu: ax.scatter(Xi[:, 0], Xi[:, 1], Xi[:, 2], marker="o", alpha=0.6) ax.scatter(Y[:, 0], Y[:, 1], Y[:, 2], marker="o", alpha=0.6) ax.view_init(elev=0, azim=90) ax.set_xticks([]) ax.set_yticks([]) ax.set_zticks([]) ax = fig.add_subplot(1, 3, 3, projection="3d") for Xi in X_visu: ax.scatter(Xi[:, 0], Xi[:, 1], Xi[:, 2], marker="o", alpha=0.6) ax.scatter(Y[:, 0], Y[:, 1], Y[:, 2], marker="o", alpha=0.6) ax.view_init(elev=90, azim=0) ax.set_xticks([]) ax.set_yticks([]) ax.set_zticks([]) plt.tight_layout() plt.show() ############################################## # Rotation animation # -------------------------------------------- fig = plt.figure(figsize=(7, 7)) ax = fig.add_subplot(1, 1, 1, projection="3d") def _init(): for Xi in X_visu: ax.scatter(Xi[:, 0], Xi[:, 1], Xi[:, 2], marker="o", alpha=0.6) ax.scatter(Y[:, 0], Y[:, 1], Y[:, 2], marker="o", alpha=0.6) ax.view_init(elev=0, azim=0) ax.set_xticks([]) ax.set_yticks([]) ax.set_zticks([]) return (fig,) def _update_plot(i): if i < 45: ax.view_init(elev=0, azim=4 * i) else: ax.view_init(elev=i - 45, azim=4 * i) return (fig,) ani = animation.FuncAnimation( fig, _update_plot, init_func=_init, frames=136, interval=50, blit=True, repeat_delay=2000, ) python-pot-0.9.7+dfsg/examples/barycenters/plot_gmm_barycenter.py000066400000000000000000000106371525521106200253420ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ===================================== Gaussian Mixture Model OT Barycenters ===================================== This example illustrates the computation of a barycenter between Gaussian Mixtures in the sense of GMM-OT [69]. This computation is done using the fixed-point method for OT barycenters with generic costs [77], for which POT provides a general solver, and a specific GMM solver. Note that this is a 'free-support' method, implying that the number of components of the barycenter GMM and their weights are fixed. The idea behind GMM-OT barycenters is to see the GMMs as discrete measures over the space of Gaussian distributions :math:`\mathcal{N}` (or equivalently the Bures-Wasserstein manifold), and to compute barycenters with respect to the 2-Wasserstein distance between measures in :math:`\mathcal{P}(\mathcal{N})`: a gaussian mixture is a finite combination of Diracs on specific gaussians, and two mixtures are compared with the 2-Wasserstein distance on this space, where ground cost the squared Bures distance between gaussians. [69] Delon, J., & Desolneux, A. (2020). A Wasserstein-type distance in the space of Gaussian mixture models. SIAM Journal on Imaging Sciences, 13(2), 936-970. [77] Tanguy, Eloi and Delon, Julie and Gozlan, Nathaël (2024). Computing Barycentres of Measures for Generic Transport Costs. arXiv preprint 2501.04016 (2024) """ # Author: Eloi Tanguy # # License: MIT License # sphinx_gallery_thumbnail_number = 1 # %% # Generate data import numpy as np import matplotlib.pyplot as plt from matplotlib.patches import Ellipse import ot from ot.gmm import gmm_barycenter_fixed_point K = 3 # number of GMMs d = 2 # dimension n = 6 # number of components of the desired barycenter def get_random_gmm(K, d, seed=0, min_cov_eig=1, cov_scale=1e-2): rng = np.random.RandomState(seed=seed) means = rng.randn(K, d) P = rng.randn(K, d, d) * cov_scale # C[k] = P[k] @ P[k]^T + min_cov_eig * I covariances = np.einsum("kab,kcb->kac", P, P) covariances += min_cov_eig * np.array([np.eye(d) for _ in range(K)]) weights = rng.random(K) weights /= np.sum(weights) return means, covariances, weights m_list = [5, 6, 7] # number of components in each GMM offsets = [np.array([-3, 0]), np.array([2, 0]), np.array([0, 4])] means_list = [] # list of means for each GMM covs_list = [] # list of covariances for each GMM w_list = [] # list of weights for each GMM # generate GMMs for k in range(K): means, covs, b = get_random_gmm( m_list[k], d, seed=k, min_cov_eig=0.25, cov_scale=0.5 ) means = means / 2 + offsets[k][None, :] means_list.append(means) covs_list.append(covs) w_list.append(b) # %% # Compute the barycenter using the fixed-point method init_means, init_covs, _ = get_random_gmm(n, d, seed=0) weights = ot.unif(K) # barycenter coefficients means_bar, covs_bar, log = gmm_barycenter_fixed_point( means_list, covs_list, w_list, init_means, init_covs, weights, iterations=3, log=True, ) # %% # Define plotting functions # draw a covariance ellipse def draw_cov(mu, C, color=None, label=None, nstd=1, alpha=0.5, ax=None): def eigsorted(cov): vals, vecs = np.linalg.eigh(cov) order = vals.argsort()[::-1].copy() return vals[order], vecs[:, order] vals, vecs = eigsorted(C) theta = np.degrees(np.arctan2(*vecs[:, 0][::-1])) w, h = 2 * nstd * np.sqrt(vals) ell = Ellipse( xy=(mu[0], mu[1]), width=w, height=h, alpha=alpha, angle=theta, facecolor=color, edgecolor=color, label=label, fill=True, ) if ax is None: ax = plt.gca() ax.add_artist(ell) # draw a gmm as a set of ellipses with weights shown in alpha value def draw_gmm(ms, Cs, ws, color=None, nstd=0.5, alpha=1, label=None, ax=None): for k in range(ms.shape[0]): draw_cov( ms[k], Cs[k], color, label if k == 0 else None, nstd, alpha * ws[k], ax=ax ) # %% # Plot the results c_list = ["#7ED321", "#4A90E2", "#9013FE", "#F5A623"] c_bar = "#D0021B" fig, ax = plt.subplots(figsize=(6, 6)) axis = [-4, 4, -2, 6] ax.set_title("Fixed Point Barycenter (3 Iterations)", fontsize=16) for k in range(K): draw_gmm(means_list[k], covs_list[k], w_list[k], color=c_list[k], ax=ax) draw_gmm(means_bar, covs_bar, ot.unif(n), color=c_bar, ax=ax) ax.axis(axis) ax.axis("off") # %% python-pot-0.9.7+dfsg/examples/barycenters/plot_solve_barycenter_variants.py000066400000000000000000000170051525521106200276150ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ====================================== Optimal Transport Barycenter solvers comparison ====================================== This example illustrates solutions returned for different variants of exact, regularized and unbalanced OT barycenter problems with free support using our wrapper `ot.solve_bary_sample`. """ # Author: Cédric Vincent-Cuaz # # License: MIT License # sphinx_gallery_thumbnail_number = 2 # %% import numpy as np import matplotlib.pylab as pl import ot from ot.plot import plot2D_samples_mat # %% # 2D data example # --------------- # # We first generate two sets of samples in 2D of 8 and 16 # points uniformly separated on circles. The weights of the samples are uniform. # Problem size n1, n2 = 8, 16 nbary = 12 # Generate random data np.random.seed(0) r1, r2 = 1, 3 x1 = r1 * np.array( [(np.cos(2 * i * np.pi / n1), np.sin(2 * i * np.pi / n1)) for i in range(n1)] ) x2 = r2 * np.array( [(np.cos(2 * i * np.pi / n2), np.sin(2 * i * np.pi / n2)) for i in range(n2)] ) style = {"markeredgecolor": "k"} pl.figure(1, (4, 4)) pl.plot(x1[:, 0], x1[:, 1], "ob", **style) pl.plot(x2[:, 0], x2[:, 1], "or", **style) pl.title("Source distributions") pl.show() # %% # Set up parameters for balanced OT barycenter solvers and solve # --------------------------------------- # balanced OT lst_balanced_solvers = [ # name, param for ot.solve function ("Exact OT", dict()), ("Entropic Reg. OT", dict(reg=1.0)), ] lst_balanced_res = [] for name, param in lst_balanced_solvers: print(f"-- name = {name} / param = {param}") res = ot.solve_bary_sample(X_a_list=[x1, x2], n=nbary, **param) lst_balanced_res.append(res) list_P = [res.list_res[k].plan for k in range(2)] print("X:", res.X) print("loss:", res.value) print("loss:", res.log) print( "marginals OT 1:", res.list_res[0].plan.sum(axis=1), res.list_res[0].plan.sum(axis=0), ) print( "marginals OT 2:", res.list_res[1].plan.sum(axis=1), res.list_res[1].plan.sum(axis=0), ) ############################################################################## # Plot distributions and plans for balanced OT barycenter solvers # ---------- def plot_list_res( lst_res, lst_solvers, fig_num=1, n_cols=2, show_masses=False, show_legend=True, s=100, fig_width=None, fig_height=None, ): n_plots = len(lst_res) n_rows = int(np.ceil(n_plots / n_cols)) if fig_width is None: fig_width = 8 * n_cols if fig_height is None: fig_height = 4 * n_rows fig, axes = pl.subplots( n_rows, n_cols, figsize=(fig_width, fig_height), squeeze=False, num=fig_num, ) axes = axes.ravel() legend_handles = None for i, (name, param) in enumerate(lst_solvers): ax = axes[i] pl.sca(ax) X = lst_res[i].X list_P = [lst_res[i].list_res[k].plan for k in range(2)] loss = lst_res[i].value plot2D_samples_mat(x1, X, list_P[0]) plot2D_samples_mat(x2, X, list_P[1]) if show_masses: # Marginals induced by transport plans a1 = list_P[0].sum(axis=1) * list_P[0].shape[0] a2 = list_P[1].sum(axis=1) * list_P[1].shape[0] # weighted average barycenter masses b = ( 0.5 * (list_P[0].sum(axis=0) + list_P[1].sum(axis=0)) * list_P[0].shape[1] ) # background uniform distribution ax.scatter(x1[:, 0], x1[:, 1], s=s, color="blue", marker="o", alpha=0.25) ax.scatter(x2[:, 0], x2[:, 1], s=s, color="red", marker="o", alpha=0.25) ax.scatter(X[:, 0], X[:, 1], s=s, color="green", marker="o", alpha=0.25) list_size_1 = s * a1 list_size_2 = s * a2 list_size_b = s * b else: list_size_1 = s list_size_2 = s list_size_b = s if i == 0: # add labels h1 = ax.scatter( x1[:, 0], x1[:, 1], s=list_size_1, color="blue", marker="o", alpha=1, label="Source distribution 1", ) h2 = ax.scatter( x2[:, 0], x2[:, 1], s=list_size_2, color="red", marker="o", alpha=1, label="Source distribution 2", ) h3 = ax.scatter( X[:, 0], X[:, 1], s=list_size_b, color="green", marker="o", alpha=1, label="Barycenter distribution", ) else: h1 = ax.scatter( x1[:, 0], x1[:, 1], s=list_size_1, color="blue", marker="o", alpha=1 ) h2 = ax.scatter( x2[:, 0], x2[:, 1], s=list_size_2, color="red", marker="o", alpha=1 ) h3 = ax.scatter( X[:, 0], X[:, 1], s=list_size_b, color="green", marker="o", alpha=1 ) if legend_handles is None: legend_handles = [h1, h2, h3] ax.set_title(name) ############################################################ # remove unused axes for j in range(i + 1, len(axes)): fig.delaxes(axes[j]) ############################################################ # Single legend above all subplots if show_legend: labels = [h.get_label() for h in legend_handles] fig.legend( legend_handles, labels, loc="upper center", bbox_to_anchor=(0.5, 1.02), ncol=3, frameon=False, ) fig.tight_layout() pl.show() plot_list_res( lst_balanced_res, lst_balanced_solvers, fig_num=2, n_cols=2, show_masses=False, fig_width=8, fig_height=4, show_legend=True, ) # %% # Set up parameters for unbalanced OT barycenter solvers and solve # --------------------------------------- lambda_unbalanced_vals = [1, 2.5, 10] # unbalanced OT KL lst_unbalanced_solvers = [ ( "Unbalanced KL No Reg \n" + r"$\lambda_u$=%s" % lambda_val, dict(unbalanced=lambda_val), ) for lambda_val in lambda_unbalanced_vals ] + [ ( "Unbalanced KL with KL Reg \n" + r"$\lambda_u$=%s, $\lambda_{ent}$=%s" % (lambda_val, 0.1), dict(reg=0.1, unbalanced=lambda_val, unbalanced_type="kl", reg_type="kl"), ) for lambda_val in lambda_unbalanced_vals ] lst_unbalanced_res = [] for name, param in lst_unbalanced_solvers: print(f"-- name = {name} / param = {param}") res = ot.solve_bary_sample(X_a_list=[x1, x2], n=nbary, **param) lst_unbalanced_res.append(res) list_P = [res.list_res[k].plan for k in range(2)] print("X:", res.X) print("loss:", res.value) print("loss:", res.log) print( "marginals OT 1:", res.list_res[0].plan.sum(axis=1), res.list_res[0].plan.sum(axis=0), ) print( "marginals OT 2:", res.list_res[1].plan.sum(axis=1), res.list_res[1].plan.sum(axis=0), ) ############################################################################## # Plot distributions and plans for unbalanced OT barycenter solvers # ---------- plot_list_res( lst_unbalanced_res, lst_unbalanced_solvers, fig_num=3, n_cols=3, show_masses=True, fig_width=12, fig_height=8.5, show_legend=False, ) python-pot-0.9.7+dfsg/examples/domain-adaptation/000077500000000000000000000000001525521106200217755ustar00rootroot00000000000000python-pot-0.9.7+dfsg/examples/domain-adaptation/index.rst000066400000000000000000000000671525521106200236410ustar00rootroot00000000000000 Domain adaptation with OT --------------------------python-pot-0.9.7+dfsg/examples/domain-adaptation/plot_otda_classes.py000066400000000000000000000104351525521106200260540ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ======================== OT for domain adaptation ======================== .. note:: Example added in release: 0.1.9. This example introduces a domain adaptation in a 2D setting and the 4 OTDA approaches currently supported in POT. """ # Authors: Remi Flamary # Stanislas Chambon # # License: MIT License import matplotlib.pylab as pl import ot ############################################################################## # Generate data # ------------- n_source_samples = 150 n_target_samples = 150 Xs, ys = ot.datasets.make_data_classif("3gauss", n_source_samples) Xt, yt = ot.datasets.make_data_classif("3gauss2", n_target_samples) ############################################################################## # Instantiate the different transport algorithms and fit them # ----------------------------------------------------------- # EMD Transport ot_emd = ot.da.EMDTransport() ot_emd.fit(Xs=Xs, Xt=Xt) # Sinkhorn Transport ot_sinkhorn = ot.da.SinkhornTransport(reg_e=1e-1) ot_sinkhorn.fit(Xs=Xs, Xt=Xt) # Sinkhorn Transport with Group lasso regularization ot_lpl1 = ot.da.SinkhornLpl1Transport(reg_e=1e-1, reg_cl=1e0) ot_lpl1.fit(Xs=Xs, ys=ys, Xt=Xt) # Sinkhorn Transport with Group lasso regularization l1l2 ot_l1l2 = ot.da.SinkhornL1l2Transport(reg_e=1e-1, reg_cl=2e0, max_iter=20, verbose=True) ot_l1l2.fit(Xs=Xs, ys=ys, Xt=Xt) # transport source samples onto target samples transp_Xs_emd = ot_emd.transform(Xs=Xs) transp_Xs_sinkhorn = ot_sinkhorn.transform(Xs=Xs) transp_Xs_lpl1 = ot_lpl1.transform(Xs=Xs) transp_Xs_l1l2 = ot_l1l2.transform(Xs=Xs) ############################################################################## # Fig 1 : plots source and target samples # --------------------------------------- pl.figure(1, figsize=(10, 5)) pl.subplot(1, 2, 1) pl.scatter(Xs[:, 0], Xs[:, 1], c=ys, marker="+", label="Source samples") pl.xticks([]) pl.yticks([]) pl.legend(loc=0) pl.title("Source samples") pl.subplot(1, 2, 2) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples") pl.xticks([]) pl.yticks([]) pl.legend(loc=0) pl.title("Target samples") pl.tight_layout() ############################################################################## # Fig 2 : plot optimal couplings and transported samples # ------------------------------------------------------ param_img = {"interpolation": "nearest", "cmap": "gray_r"} pl.figure(2, figsize=(15, 8)) pl.subplot(2, 4, 1) pl.imshow(ot_emd.coupling_, **param_img) pl.xticks([]) pl.yticks([]) pl.title("Optimal coupling\nEMDTransport") pl.subplot(2, 4, 2) pl.imshow(ot_sinkhorn.coupling_, **param_img) pl.xticks([]) pl.yticks([]) pl.title("Optimal coupling\nSinkhornTransport") pl.subplot(2, 4, 3) pl.imshow(ot_lpl1.coupling_, **param_img) pl.xticks([]) pl.yticks([]) pl.title("Optimal coupling\nSinkhornLpl1Transport") pl.subplot(2, 4, 4) pl.imshow(ot_l1l2.coupling_, **param_img) pl.xticks([]) pl.yticks([]) pl.title("Optimal coupling\nSinkhornL1l2Transport") pl.subplot(2, 4, 5) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.3) pl.scatter( transp_Xs_emd[:, 0], transp_Xs_emd[:, 1], c=ys, marker="+", label="Transp samples", s=30, ) pl.xticks([]) pl.yticks([]) pl.title("Transported samples\nEmdTransport") pl.legend(loc="lower left") pl.subplot(2, 4, 6) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.3) pl.scatter( transp_Xs_sinkhorn[:, 0], transp_Xs_sinkhorn[:, 1], c=ys, marker="+", label="Transp samples", s=30, ) pl.xticks([]) pl.yticks([]) pl.title("Transported samples\nSinkhornTransport") pl.subplot(2, 4, 7) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.3) pl.scatter( transp_Xs_lpl1[:, 0], transp_Xs_lpl1[:, 1], c=ys, marker="+", label="Transp samples", s=30, ) pl.xticks([]) pl.yticks([]) pl.title("Transported samples\nSinkhornLpl1Transport") pl.subplot(2, 4, 8) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.3) pl.scatter( transp_Xs_l1l2[:, 0], transp_Xs_l1l2[:, 1], c=ys, marker="+", label="Transp samples", s=30, ) pl.xticks([]) pl.yticks([]) pl.title("Transported samples\nSinkhornL1l2Transport") pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/domain-adaptation/plot_otda_color_images.py000066400000000000000000000075171525521106200270710ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ============================= OT for image color adaptation ============================= .. note:: Example added in release: 0.1.9. This example presents a way of transferring colors between two images with Optimal Transport as introduced in [6] [6] Ferradans, S., Papadakis, N., Peyre, G., & Aujol, J. F. (2014). Regularized discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. """ # Authors: Remi Flamary # Stanislas Chambon # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import os from pathlib import Path import numpy as np from matplotlib import pyplot as plt import ot rng = np.random.RandomState(42) def im2mat(img): """Converts an image to matrix (one pixel per line)""" return img.reshape((img.shape[0] * img.shape[1], img.shape[2])) def mat2im(X, shape): """Converts back a matrix to an image""" return X.reshape(shape) def minmax(img): return np.clip(img, 0, 1) ############################################################################## # Generate data # ------------- # Loading images this_file = os.path.realpath("__file__") data_path = os.path.join(Path(this_file).parent.parent.parent, "data") I1 = plt.imread(os.path.join(data_path, "ocean_day.jpg")).astype(np.float64) / 256 I2 = plt.imread(os.path.join(data_path, "ocean_sunset.jpg")).astype(np.float64) / 256 X1 = im2mat(I1) X2 = im2mat(I2) # training samples nb = 500 idx1 = rng.randint(X1.shape[0], size=(nb,)) idx2 = rng.randint(X2.shape[0], size=(nb,)) Xs = X1[idx1, :] Xt = X2[idx2, :] ############################################################################## # Plot original image # ------------------- plt.figure(1, figsize=(6.4, 3)) plt.subplot(1, 2, 1) plt.imshow(I1) plt.axis("off") plt.title("Image 1") plt.subplot(1, 2, 2) plt.imshow(I2) plt.axis("off") plt.title("Image 2") ############################################################################## # Scatter plot of colors # ---------------------- plt.figure(2, figsize=(6.4, 3)) plt.subplot(1, 2, 1) plt.scatter(Xs[:, 0], Xs[:, 2], c=Xs) plt.axis([0, 1, 0, 1]) plt.xlabel("Red") plt.ylabel("Blue") plt.title("Image 1") plt.subplot(1, 2, 2) plt.scatter(Xt[:, 0], Xt[:, 2], c=Xt) plt.axis([0, 1, 0, 1]) plt.xlabel("Red") plt.ylabel("Blue") plt.title("Image 2") plt.tight_layout() ############################################################################## # Instantiate the different transport algorithms and fit them # ----------------------------------------------------------- # EMDTransport ot_emd = ot.da.EMDTransport() ot_emd.fit(Xs=Xs, Xt=Xt) # SinkhornTransport ot_sinkhorn = ot.da.SinkhornTransport(reg_e=1e-1) ot_sinkhorn.fit(Xs=Xs, Xt=Xt) # prediction between images (using out of sample prediction as in [6]) transp_Xs_emd = ot_emd.transform(Xs=X1) transp_Xt_emd = ot_emd.inverse_transform(Xt=X2) transp_Xs_sinkhorn = ot_sinkhorn.transform(Xs=X1) transp_Xt_sinkhorn = ot_sinkhorn.inverse_transform(Xt=X2) I1t = minmax(mat2im(transp_Xs_emd, I1.shape)) I2t = minmax(mat2im(transp_Xt_emd, I2.shape)) I1te = minmax(mat2im(transp_Xs_sinkhorn, I1.shape)) I2te = minmax(mat2im(transp_Xt_sinkhorn, I2.shape)) ############################################################################## # Plot new images # --------------- plt.figure(3, figsize=(8, 4)) plt.subplot(2, 3, 1) plt.imshow(I1) plt.axis("off") plt.title("Image 1") plt.subplot(2, 3, 2) plt.imshow(I1t) plt.axis("off") plt.title("Image 1 Adapt") plt.subplot(2, 3, 3) plt.imshow(I1te) plt.axis("off") plt.title("Image 1 Adapt (reg)") plt.subplot(2, 3, 4) plt.imshow(I2) plt.axis("off") plt.title("Image 2") plt.subplot(2, 3, 5) plt.imshow(I2t) plt.axis("off") plt.title("Image 2 Adapt") plt.subplot(2, 3, 6) plt.imshow(I2te) plt.axis("off") plt.title("Image 2 Adapt (reg)") plt.tight_layout() plt.show() python-pot-0.9.7+dfsg/examples/domain-adaptation/plot_otda_d2.py000066400000000000000000000126441525521106200247300ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ =================================================== OT for domain adaptation on empirical distributions =================================================== .. note:: Example added in release: 0.1.9. This example introduces a domain adaptation in a 2D setting. It explicit the problem of domain adaptation and introduces some optimal transport approaches to solve it. Quantities such as optimal couplings, greater coupling coefficients and transported samples are represented in order to give a visual understanding of what the transport methods are doing. """ # Authors: Remi Flamary # Stanislas Chambon # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import matplotlib.pylab as pl import ot import ot.plot ############################################################################## # Generate data # ------------- n_samples_source = 150 n_samples_target = 150 Xs, ys = ot.datasets.make_data_classif("3gauss", n_samples_source) Xt, yt = ot.datasets.make_data_classif("3gauss2", n_samples_target) # Cost matrix M = ot.dist(Xs, Xt, metric="sqeuclidean") ############################################################################## # Instantiate the different transport algorithms and fit them # ----------------------------------------------------------- # EMD Transport ot_emd = ot.da.EMDTransport() ot_emd.fit(Xs=Xs, Xt=Xt) # Sinkhorn Transport ot_sinkhorn = ot.da.SinkhornTransport(reg_e=1e-1) ot_sinkhorn.fit(Xs=Xs, Xt=Xt) # Sinkhorn Transport with Group lasso regularization ot_lpl1 = ot.da.SinkhornLpl1Transport(reg_e=1e-1, reg_cl=1e0) ot_lpl1.fit(Xs=Xs, ys=ys, Xt=Xt) # transport source samples onto target samples transp_Xs_emd = ot_emd.transform(Xs=Xs) transp_Xs_sinkhorn = ot_sinkhorn.transform(Xs=Xs) transp_Xs_lpl1 = ot_lpl1.transform(Xs=Xs) ############################################################################## # Fig 1 : plots source and target samples + matrix of pairwise distance # --------------------------------------------------------------------- pl.figure(1, figsize=(10, 10)) pl.subplot(2, 2, 1) pl.scatter(Xs[:, 0], Xs[:, 1], c=ys, marker="+", label="Source samples") pl.xticks([]) pl.yticks([]) pl.legend(loc=0) pl.title("Source samples") pl.subplot(2, 2, 2) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples") pl.xticks([]) pl.yticks([]) pl.legend(loc=0) pl.title("Target samples") pl.subplot(2, 2, 3) pl.imshow(M, interpolation="nearest") pl.xticks([]) pl.yticks([]) pl.title("Matrix of pairwise distances") pl.tight_layout() ############################################################################## # Fig 2 : plots optimal couplings for the different methods # --------------------------------------------------------- pl.figure(2, figsize=(10, 6)) pl.subplot(2, 3, 1) pl.imshow(ot_emd.coupling_, interpolation="nearest", cmap="gray_r") pl.xticks([]) pl.yticks([]) pl.title("Optimal coupling\nEMDTransport") pl.subplot(2, 3, 2) pl.imshow(ot_sinkhorn.coupling_, interpolation="nearest", cmap="gray_r") pl.xticks([]) pl.yticks([]) pl.title("Optimal coupling\nSinkhornTransport") pl.subplot(2, 3, 3) pl.imshow(ot_lpl1.coupling_, interpolation="nearest", cmap="gray_r") pl.xticks([]) pl.yticks([]) pl.title("Optimal coupling\nSinkhornLpl1Transport") pl.subplot(2, 3, 4) ot.plot.plot2D_samples_mat(Xs, Xt, ot_emd.coupling_, c=[0.5, 0.5, 1]) pl.scatter(Xs[:, 0], Xs[:, 1], c=ys, marker="+", label="Source samples") pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples") pl.xticks([]) pl.yticks([]) pl.title("Main coupling coefficients\nEMDTransport") pl.subplot(2, 3, 5) ot.plot.plot2D_samples_mat(Xs, Xt, ot_sinkhorn.coupling_, c=[0.5, 0.5, 1]) pl.scatter(Xs[:, 0], Xs[:, 1], c=ys, marker="+", label="Source samples") pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples") pl.xticks([]) pl.yticks([]) pl.title("Main coupling coefficients\nSinkhornTransport") pl.subplot(2, 3, 6) ot.plot.plot2D_samples_mat(Xs, Xt, ot_lpl1.coupling_, c=[0.5, 0.5, 1]) pl.scatter(Xs[:, 0], Xs[:, 1], c=ys, marker="+", label="Source samples") pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples") pl.xticks([]) pl.yticks([]) pl.title("Main coupling coefficients\nSinkhornLpl1Transport") pl.tight_layout() ############################################################################## # Fig 3 : plot transported samples # -------------------------------- # display transported samples pl.figure(4, figsize=(10, 4)) pl.subplot(1, 3, 1) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.5) pl.scatter( transp_Xs_emd[:, 0], transp_Xs_emd[:, 1], c=ys, marker="+", label="Transp samples", s=30, ) pl.title("Transported samples\nEmdTransport") pl.legend(loc=0) pl.xticks([]) pl.yticks([]) pl.subplot(1, 3, 2) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.5) pl.scatter( transp_Xs_sinkhorn[:, 0], transp_Xs_sinkhorn[:, 1], c=ys, marker="+", label="Transp samples", s=30, ) pl.title("Transported samples\nSinkhornTransport") pl.xticks([]) pl.yticks([]) pl.subplot(1, 3, 3) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.5) pl.scatter( transp_Xs_lpl1[:, 0], transp_Xs_lpl1[:, 1], c=ys, marker="+", label="Transp samples", s=30, ) pl.title("Transported samples\nSinkhornLpl1Transport") pl.xticks([]) pl.yticks([]) pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/domain-adaptation/plot_otda_jcpot.py000066400000000000000000000133011525521106200255310ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ================================ OT for multi-source target shift ================================ .. note:: Example added in release: 0.7.0. This example introduces a target shift problem with two 2D source and 1 target domain. """ # Authors: Remi Flamary # Ievgen Redko # # License: MIT License import pylab as pl import numpy as np import ot from ot.datasets import make_data_classif ############################################################################## # Generate data # ------------- n = 50 sigma = 0.3 np.random.seed(1985) p1 = 0.2 dec1 = [0, 2] p2 = 0.9 dec2 = [0, -2] pt = 0.4 dect = [4, 0] xs1, ys1 = make_data_classif("2gauss_prop", n, nz=sigma, p=p1, bias=dec1) xs2, ys2 = make_data_classif("2gauss_prop", n + 1, nz=sigma, p=p2, bias=dec2) xt, yt = make_data_classif("2gauss_prop", n, nz=sigma, p=pt, bias=dect) all_Xr = [xs1, xs2] all_Yr = [ys1, ys2] # %% da = 1.5 def plot_ax(dec, name): pl.plot([dec[0], dec[0]], [dec[1] - da, dec[1] + da], "k", alpha=0.5) pl.plot([dec[0] - da, dec[0] + da], [dec[1], dec[1]], "k", alpha=0.5) pl.text(dec[0] - 0.5, dec[1] + 2, name) ############################################################################## # Fig 1 : plots source and target samples # --------------------------------------- pl.figure(1) pl.clf() plot_ax(dec1, "Source 1") plot_ax(dec2, "Source 2") plot_ax(dect, "Target") pl.scatter( xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker="x", cmap="Set1", vmax=9, label="Source 1 ({:1.2f}, {:1.2f})".format(1 - p1, p1), ) pl.scatter( xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker="+", cmap="Set1", vmax=9, label="Source 2 ({:1.2f}, {:1.2f})".format(1 - p2, p2), ) pl.scatter( xt[:, 0], xt[:, 1], c=yt, s=35, marker="o", cmap="Set1", vmax=9, label="Target ({:1.2f}, {:1.2f})".format(1 - pt, pt), ) pl.title("Data") pl.legend() pl.axis("equal") pl.axis("off") ############################################################################## # Instantiate Sinkhorn transport algorithm and fit them for all source domains # ---------------------------------------------------------------------------- ot_sinkhorn = ot.da.SinkhornTransport(reg_e=1e-1, metric="sqeuclidean") def print_G(G, xs, ys, xt): for i in range(G.shape[0]): for j in range(G.shape[1]): if G[i, j] > 5e-4: if ys[i]: c = "b" else: c = "r" pl.plot([xs[i, 0], xt[j, 0]], [xs[i, 1], xt[j, 1]], c, alpha=0.2) ############################################################################## # Fig 2 : plot optimal couplings and transported samples # ------------------------------------------------------ pl.figure(2) pl.clf() plot_ax(dec1, "Source 1") plot_ax(dec2, "Source 2") plot_ax(dect, "Target") print_G(ot_sinkhorn.fit(Xs=xs1, Xt=xt).coupling_, xs1, ys1, xt) print_G(ot_sinkhorn.fit(Xs=xs2, Xt=xt).coupling_, xs2, ys2, xt) pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker="x", cmap="Set1", vmax=9) pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker="+", cmap="Set1", vmax=9) pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker="o", cmap="Set1", vmax=9) pl.plot([], [], "r", alpha=0.2, label="Mass from Class 1") pl.plot([], [], "b", alpha=0.2, label="Mass from Class 2") pl.title("Independent OT") pl.legend() pl.axis("equal") pl.axis("off") ############################################################################## # Instantiate JCPOT adaptation algorithm and fit it # ---------------------------------------------------------------------------- otda = ot.da.JCPOTTransport( reg_e=1, max_iter=1000, metric="sqeuclidean", tol=1e-9, verbose=True, log=True ) otda.fit(all_Xr, all_Yr, xt) ws1 = otda.proportions_.dot(otda.log_["D2"][0]) ws2 = otda.proportions_.dot(otda.log_["D2"][1]) pl.figure(3) pl.clf() plot_ax(dec1, "Source 1") plot_ax(dec2, "Source 2") plot_ax(dect, "Target") print_G(ot.bregman.sinkhorn(ws1, [], otda.log_["M"][0], reg=1e-1), xs1, ys1, xt) print_G(ot.bregman.sinkhorn(ws2, [], otda.log_["M"][1], reg=1e-1), xs2, ys2, xt) pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker="x", cmap="Set1", vmax=9) pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker="+", cmap="Set1", vmax=9) pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker="o", cmap="Set1", vmax=9) pl.plot([], [], "r", alpha=0.2, label="Mass from Class 1") pl.plot([], [], "b", alpha=0.2, label="Mass from Class 2") pl.title( "OT with prop estimation ({:1.3f},{:1.3f})".format( otda.proportions_[0], otda.proportions_[1] ) ) pl.legend() pl.axis("equal") pl.axis("off") ############################################################################## # Run oracle transport algorithm with known proportions # ---------------------------------------------------------------------------- h_res = np.array([1 - pt, pt]) ws1 = h_res.dot(otda.log_["D2"][0]) ws2 = h_res.dot(otda.log_["D2"][1]) pl.figure(4) pl.clf() plot_ax(dec1, "Source 1") plot_ax(dec2, "Source 2") plot_ax(dect, "Target") print_G(ot.bregman.sinkhorn(ws1, [], otda.log_["M"][0], reg=1e-1), xs1, ys1, xt) print_G(ot.bregman.sinkhorn(ws2, [], otda.log_["M"][1], reg=1e-1), xs2, ys2, xt) pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker="x", cmap="Set1", vmax=9) pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker="+", cmap="Set1", vmax=9) pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker="o", cmap="Set1", vmax=9) pl.plot([], [], "r", alpha=0.2, label="Mass from Class 1") pl.plot([], [], "b", alpha=0.2, label="Mass from Class 2") pl.title("OT with known proportion ({:1.1f},{:1.1f})".format(h_res[0], h_res[1])) pl.legend() pl.axis("equal") pl.axis("off") pl.show() python-pot-0.9.7+dfsg/examples/domain-adaptation/plot_otda_laplacian.py000066400000000000000000000073231525521106200263450ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ====================================================== OT with Laplacian regularization for domain adaptation ====================================================== .. note:: Example added in release: 0.7.0. This example introduces a domain adaptation in a 2D setting and OTDA approach with Laplacian regularization. """ # Authors: Ievgen Redko # License: MIT License import matplotlib.pylab as pl import ot ############################################################################## # Generate data # ------------- n_source_samples = 150 n_target_samples = 150 Xs, ys = ot.datasets.make_data_classif("3gauss", n_source_samples) Xt, yt = ot.datasets.make_data_classif("3gauss2", n_target_samples) ############################################################################## # Instantiate the different transport algorithms and fit them # ----------------------------------------------------------- # EMD Transport ot_emd = ot.da.EMDTransport() ot_emd.fit(Xs=Xs, Xt=Xt) # Sinkhorn Transport ot_sinkhorn = ot.da.SinkhornTransport(reg_e=0.01) ot_sinkhorn.fit(Xs=Xs, Xt=Xt) # EMD Transport with Laplacian regularization ot_emd_laplace = ot.da.EMDLaplaceTransport(reg_lap=100, reg_src=1) ot_emd_laplace.fit(Xs=Xs, Xt=Xt) # transport source samples onto target samples transp_Xs_emd = ot_emd.transform(Xs=Xs) transp_Xs_sinkhorn = ot_sinkhorn.transform(Xs=Xs) transp_Xs_emd_laplace = ot_emd_laplace.transform(Xs=Xs) ############################################################################## # Fig 1 : plots source and target samples # --------------------------------------- pl.figure(1, figsize=(10, 5)) pl.subplot(1, 2, 1) pl.scatter(Xs[:, 0], Xs[:, 1], c=ys, marker="+", label="Source samples") pl.xticks([]) pl.yticks([]) pl.legend(loc=0) pl.title("Source samples") pl.subplot(1, 2, 2) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples") pl.xticks([]) pl.yticks([]) pl.legend(loc=0) pl.title("Target samples") pl.tight_layout() ############################################################################## # Fig 2 : plot optimal couplings and transported samples # ------------------------------------------------------ param_img = {"interpolation": "nearest", "cmap": "gray_r"} pl.figure(2, figsize=(15, 8)) pl.subplot(2, 3, 1) pl.imshow(ot_emd.coupling_, **param_img) pl.xticks([]) pl.yticks([]) pl.title("Optimal coupling\nEMDTransport") pl.figure(2, figsize=(15, 8)) pl.subplot(2, 3, 2) pl.imshow(ot_sinkhorn.coupling_, **param_img) pl.xticks([]) pl.yticks([]) pl.title("Optimal coupling\nSinkhornTransport") pl.subplot(2, 3, 3) pl.imshow(ot_emd_laplace.coupling_, **param_img) pl.xticks([]) pl.yticks([]) pl.title("Optimal coupling\nEMDLaplaceTransport") pl.subplot(2, 3, 4) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.3) pl.scatter( transp_Xs_emd[:, 0], transp_Xs_emd[:, 1], c=ys, marker="+", label="Transp samples", s=30, ) pl.xticks([]) pl.yticks([]) pl.title("Transported samples\nEmdTransport") pl.legend(loc="lower left") pl.subplot(2, 3, 5) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.3) pl.scatter( transp_Xs_sinkhorn[:, 0], transp_Xs_sinkhorn[:, 1], c=ys, marker="+", label="Transp samples", s=30, ) pl.xticks([]) pl.yticks([]) pl.title("Transported samples\nSinkhornTransport") pl.subplot(2, 3, 6) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.3) pl.scatter( transp_Xs_emd_laplace[:, 0], transp_Xs_emd_laplace[:, 1], c=ys, marker="+", label="Transp samples", s=30, ) pl.xticks([]) pl.yticks([]) pl.title("Transported samples\nEMDLaplaceTransport") pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/domain-adaptation/plot_otda_linear_mapping.py000066400000000000000000000104641525521106200274060ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ ============================ Linear OT mapping estimation ============================ .. note:: Example updated in release: 0.9.1. """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 2 # %% import os from pathlib import Path import numpy as np from matplotlib import pyplot as plt import ot ############################################################################## # Generate data # ------------- n = 1000 d = 2 sigma = 0.1 rng = np.random.RandomState(42) # source samples angles = rng.rand(n, 1) * 2 * np.pi xs = np.concatenate((np.sin(angles), np.cos(angles)), axis=1) + sigma * rng.randn(n, 2) xs[: n // 2, 1] += 2 # target samples anglet = rng.rand(n, 1) * 2 * np.pi xt = np.concatenate((np.sin(anglet), np.cos(anglet)), axis=1) + sigma * rng.randn(n, 2) xt[: n // 2, 1] += 2 A = np.array([[1.5, 0.7], [0.7, 1.5]]) b = np.array([[4, 2]]) xt = xt.dot(A) + b ############################################################################## # Plot data # --------- plt.figure(1, (5, 5)) plt.plot(xs[:, 0], xs[:, 1], "+") plt.plot(xt[:, 0], xt[:, 1], "o") plt.legend(("Source", "Target")) plt.title("Source and target distributions") plt.show() ############################################################################## # Estimate linear mapping and transport # ------------------------------------- # Gaussian (linear) Monge mapping estimation Ae, be = ot.gaussian.empirical_bures_wasserstein_mapping(xs, xt) xst = xs.dot(Ae) + be # Gaussian (linear) GW mapping estimation Agw, bgw = ot.gaussian.empirical_gaussian_gromov_wasserstein_mapping(xs, xt) xstgw = xs.dot(Agw) + bgw ############################################################################## # Plot transported samples # ------------------------ plt.figure(2, (10, 5)) plt.clf() plt.subplot(1, 2, 1) plt.plot(xs[:, 0], xs[:, 1], "+") plt.plot(xt[:, 0], xt[:, 1], "o") plt.plot(xst[:, 0], xst[:, 1], "+") plt.legend(("Source", "Target", "Transp. Monge"), loc=0) plt.title("Transported samples with Monge") plt.subplot(1, 2, 2) plt.plot(xs[:, 0], xs[:, 1], "+") plt.plot(xt[:, 0], xt[:, 1], "o") plt.plot(xstgw[:, 0], xstgw[:, 1], "+") plt.legend(("Source", "Target", "Transp. GW"), loc=0) plt.title("Transported samples with Gaussian GW") plt.show() ############################################################################## # Load image data # --------------- def im2mat(img): """Converts and image to matrix (one pixel per line)""" return img.reshape((img.shape[0] * img.shape[1], img.shape[2])) def mat2im(X, shape): """Converts back a matrix to an image""" return X.reshape(shape) def minmax(img): return np.clip(img, 0, 1) # Loading images this_file = os.path.realpath("__file__") data_path = os.path.join(Path(this_file).parent.parent.parent, "data") I1 = plt.imread(os.path.join(data_path, "ocean_day.jpg")).astype(np.float64) / 256 I2 = plt.imread(os.path.join(data_path, "ocean_sunset.jpg")).astype(np.float64) / 256 X1 = im2mat(I1) X2 = im2mat(I2) ############################################################################## # Estimate mapping and adapt # ---------------------------- # Monge mapping mapping = ot.da.LinearTransport() mapping.fit(Xs=X1, Xt=X2) xst = mapping.transform(Xs=X1) xts = mapping.inverse_transform(Xt=X2) I1t = minmax(mat2im(xst, I1.shape)) I2t = minmax(mat2im(xts, I2.shape)) # gaussian GW mapping mapping = ot.da.LinearGWTransport() mapping.fit(Xs=X1, Xt=X2) xstgw = mapping.transform(Xs=X1) xtsgw = mapping.inverse_transform(Xt=X2) I1tgw = minmax(mat2im(xstgw, I1.shape)) I2tgw = minmax(mat2im(xtsgw, I2.shape)) # %% ############################################################################## # Plot transformed images # ----------------------- plt.figure(3, figsize=(14, 7)) plt.subplot(2, 3, 1) plt.imshow(I1) plt.axis("off") plt.title("Im. 1") plt.subplot(2, 3, 4) plt.imshow(I2) plt.axis("off") plt.title("Im. 2") plt.subplot(2, 3, 2) plt.imshow(I1t) plt.axis("off") plt.title("Monge mapping Im. 1") plt.subplot(2, 3, 5) plt.imshow(I2t) plt.axis("off") plt.title("Inverse Monge mapping Im. 2") plt.subplot(2, 3, 3) plt.imshow(I1tgw) plt.axis("off") plt.title("Gaussian GW mapping Im. 1") plt.subplot(2, 3, 6) plt.imshow(I2tgw) plt.axis("off") plt.title("Inverse Gaussian GW mapping Im. 2") python-pot-0.9.7+dfsg/examples/domain-adaptation/plot_otda_mapping.py000066400000000000000000000100741525521106200260510ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ =========================================== OT mapping estimation for domain adaptation =========================================== .. note:: Example added in release: 0.1.9. This example presents how to use MappingTransport to estimate at the same time both the coupling transport and approximate the transport map with either a linear or a kernelized mapping as introduced in [8]. [8] M. Perrot, N. Courty, R. Flamary, A. Habrard, "Mapping estimation for discrete optimal transport", Neural Information Processing Systems (NIPS), 2016. """ # Authors: Remi Flamary # Stanislas Chambon # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pylab as pl import ot ############################################################################## # Generate data # ------------- n_source_samples = 100 n_target_samples = 100 theta = 2 * np.pi / 20 noise_level = 0.1 Xs, ys = ot.datasets.make_data_classif("gaussrot", n_source_samples, nz=noise_level) Xs_new, _ = ot.datasets.make_data_classif("gaussrot", n_source_samples, nz=noise_level) Xt, yt = ot.datasets.make_data_classif( "gaussrot", n_target_samples, theta=theta, nz=noise_level ) # one of the target mode changes its variance (no linear mapping) Xt[yt == 2] *= 3 Xt = Xt + 4 ############################################################################## # Plot data # --------- pl.figure(1, (10, 5)) pl.clf() pl.scatter(Xs[:, 0], Xs[:, 1], c=ys, marker="+", label="Source samples") pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples") pl.legend(loc=0) pl.title("Source and target distributions") ############################################################################## # Instantiate the different transport algorithms and fit them # ----------------------------------------------------------- # MappingTransport with linear kernel ot_mapping_linear = ot.da.MappingTransport( kernel="linear", mu=1e0, eta=1e-8, bias=True, max_iter=20, verbose=True ) ot_mapping_linear.fit(Xs=Xs, Xt=Xt) # for original source samples, transform applies barycentric mapping transp_Xs_linear = ot_mapping_linear.transform(Xs=Xs) # for out of source samples, transform applies the linear mapping transp_Xs_linear_new = ot_mapping_linear.transform(Xs=Xs_new) # MappingTransport with gaussian kernel ot_mapping_gaussian = ot.da.MappingTransport( kernel="gaussian", eta=1e-5, mu=1e-1, bias=True, sigma=1, max_iter=10, verbose=True ) ot_mapping_gaussian.fit(Xs=Xs, Xt=Xt) # for original source samples, transform applies barycentric mapping transp_Xs_gaussian = ot_mapping_gaussian.transform(Xs=Xs) # for out of source samples, transform applies the gaussian mapping transp_Xs_gaussian_new = ot_mapping_gaussian.transform(Xs=Xs_new) ############################################################################## # Plot transported samples # ------------------------ pl.figure(2) pl.clf() pl.subplot(2, 2, 1) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.2) pl.scatter( transp_Xs_linear[:, 0], transp_Xs_linear[:, 1], c=ys, marker="+", label="Mapped source samples", ) pl.title("Bary. mapping (linear)") pl.legend(loc=0) pl.subplot(2, 2, 2) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.2) pl.scatter( transp_Xs_linear_new[:, 0], transp_Xs_linear_new[:, 1], c=ys, marker="+", label="Learned mapping", ) pl.title("Estim. mapping (linear)") pl.subplot(2, 2, 3) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.2) pl.scatter( transp_Xs_gaussian[:, 0], transp_Xs_gaussian[:, 1], c=ys, marker="+", label="barycentric mapping", ) pl.title("Bary. mapping (kernel)") pl.subplot(2, 2, 4) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.2) pl.scatter( transp_Xs_gaussian_new[:, 0], transp_Xs_gaussian_new[:, 1], c=ys, marker="+", label="Learned mapping", ) pl.title("Estim. mapping (kernel)") pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/domain-adaptation/plot_otda_mapping_colors_images.py000066400000000000000000000105761525521106200307660ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ===================================================== OT for image color adaptation with mapping estimation ===================================================== .. note:: Example added in release: 0.1.9. OT for domain adaptation with image color adaptation [6] with mapping estimation [8]. [6] Ferradans, S., Papadakis, N., Peyre, G., & Aujol, J. F. (2014). Regularized discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. [8] M. Perrot, N. Courty, R. Flamary, A. Habrard, "Mapping estimation for discrete optimal transport", Neural Information Processing Systems (NIPS), 2016. """ # Authors: Remi Flamary # Stanislas Chambon # # License: MIT License # sphinx_gallery_thumbnail_number = 3 import os from pathlib import Path import numpy as np from matplotlib import pyplot as plt import ot rng = np.random.RandomState(42) def im2mat(img): """Converts and image to matrix (one pixel per line)""" return img.reshape((img.shape[0] * img.shape[1], img.shape[2])) def mat2im(X, shape): """Converts back a matrix to an image""" return X.reshape(shape) def minmax(img): return np.clip(img, 0, 1) ############################################################################## # Generate data # ------------- # Loading images this_file = os.path.realpath("__file__") data_path = os.path.join(Path(this_file).parent.parent.parent, "data") I1 = plt.imread(os.path.join(data_path, "ocean_day.jpg")).astype(np.float64) / 256 I2 = plt.imread(os.path.join(data_path, "ocean_sunset.jpg")).astype(np.float64) / 256 X1 = im2mat(I1) X2 = im2mat(I2) # training samples nb = 500 idx1 = rng.randint(X1.shape[0], size=(nb,)) idx2 = rng.randint(X2.shape[0], size=(nb,)) Xs = X1[idx1, :] Xt = X2[idx2, :] ############################################################################## # Domain adaptation for pixel distribution transfer # ------------------------------------------------- # EMDTransport ot_emd = ot.da.EMDTransport() ot_emd.fit(Xs=Xs, Xt=Xt) transp_Xs_emd = ot_emd.transform(Xs=X1) Image_emd = minmax(mat2im(transp_Xs_emd, I1.shape)) # SinkhornTransport ot_sinkhorn = ot.da.SinkhornTransport(reg_e=1e-1) ot_sinkhorn.fit(Xs=Xs, Xt=Xt) transp_Xs_sinkhorn = ot_sinkhorn.transform(Xs=X1) Image_sinkhorn = minmax(mat2im(transp_Xs_sinkhorn, I1.shape)) ot_mapping_linear = ot.da.MappingTransport( mu=1e0, eta=1e-8, bias=True, max_iter=20, verbose=True ) ot_mapping_linear.fit(Xs=Xs, Xt=Xt) X1tl = ot_mapping_linear.transform(Xs=X1) Image_mapping_linear = minmax(mat2im(X1tl, I1.shape)) ot_mapping_gaussian = ot.da.MappingTransport( mu=1e0, eta=1e-2, sigma=1, bias=False, max_iter=10, verbose=True ) ot_mapping_gaussian.fit(Xs=Xs, Xt=Xt) X1tn = ot_mapping_gaussian.transform(Xs=X1) # use the estimated mapping Image_mapping_gaussian = minmax(mat2im(X1tn, I1.shape)) ############################################################################## # Plot original images # -------------------- plt.figure(1, figsize=(6.4, 3)) plt.subplot(1, 2, 1) plt.imshow(I1) plt.axis("off") plt.title("Image 1") plt.subplot(1, 2, 2) plt.imshow(I2) plt.axis("off") plt.title("Image 2") plt.tight_layout() ############################################################################## # Plot pixel values distribution # ------------------------------ plt.figure(2, figsize=(6.4, 5)) plt.subplot(1, 2, 1) plt.scatter(Xs[:, 0], Xs[:, 2], c=Xs) plt.axis([0, 1, 0, 1]) plt.xlabel("Red") plt.ylabel("Blue") plt.title("Image 1") plt.subplot(1, 2, 2) plt.scatter(Xt[:, 0], Xt[:, 2], c=Xt) plt.axis([0, 1, 0, 1]) plt.xlabel("Red") plt.ylabel("Blue") plt.title("Image 2") plt.tight_layout() ############################################################################## # Plot transformed images # ----------------------- plt.figure(2, figsize=(10, 5)) plt.subplot(2, 3, 1) plt.imshow(I1) plt.axis("off") plt.title("Im. 1") plt.subplot(2, 3, 4) plt.imshow(I2) plt.axis("off") plt.title("Im. 2") plt.subplot(2, 3, 2) plt.imshow(Image_emd) plt.axis("off") plt.title("EmdTransport") plt.subplot(2, 3, 5) plt.imshow(Image_sinkhorn) plt.axis("off") plt.title("SinkhornTransport") plt.subplot(2, 3, 3) plt.imshow(Image_mapping_linear) plt.axis("off") plt.title("MappingTransport (linear)") plt.subplot(2, 3, 6) plt.imshow(Image_mapping_gaussian) plt.axis("off") plt.title("MappingTransport (gaussian)") plt.tight_layout() plt.show() python-pot-0.9.7+dfsg/examples/domain-adaptation/plot_otda_semi_supervised.py000066400000000000000000000112421525521106200276220ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ============================================ OTDA unsupervised vs semi-supervised setting ============================================ .. note:: Example added in release: 0.1.9. This example introduces a semi supervised domain adaptation in a 2D setting. It explicit the problem of semi supervised domain adaptation and introduces some optimal transport approaches to solve it. Quantities such as optimal couplings, greater coupling coefficients and transported samples are represented in order to give a visual understanding of what the transport methods are doing. """ # Authors: Remi Flamary # Stanislas Chambon # # License: MIT License # sphinx_gallery_thumbnail_number = 3 import matplotlib.pylab as pl import ot ############################################################################## # Generate data # ------------- n_samples_source = 150 n_samples_target = 150 Xs, ys = ot.datasets.make_data_classif("3gauss", n_samples_source) Xt, yt = ot.datasets.make_data_classif("3gauss2", n_samples_target) ############################################################################## # Transport source samples onto target samples # -------------------------------------------- # unsupervised domain adaptation ot_sinkhorn_un = ot.da.SinkhornTransport(reg_e=1e-1) ot_sinkhorn_un.fit(Xs=Xs, Xt=Xt) transp_Xs_sinkhorn_un = ot_sinkhorn_un.transform(Xs=Xs) # semi-supervised domain adaptation ot_sinkhorn_semi = ot.da.SinkhornTransport(reg_e=1e-1) ot_sinkhorn_semi.fit(Xs=Xs, Xt=Xt, ys=ys, yt=yt) transp_Xs_sinkhorn_semi = ot_sinkhorn_semi.transform(Xs=Xs) # semi supervised DA uses available labeled target samples to modify the cost # matrix involved in the OT problem. The cost of transporting a source sample # of class A onto a target sample of class B != A is set to infinite, or a # very large value # note that in the present case we consider that all the target samples are # labeled. For daily applications, some target sample might not have labels, # in this case the element of yt corresponding to these samples should be # filled with -1. # Warning: we recall that -1 cannot be used as a class label ############################################################################## # Fig 1 : plots source and target samples + matrix of pairwise distance # --------------------------------------------------------------------- pl.figure(1, figsize=(10, 10)) pl.subplot(2, 2, 1) pl.scatter(Xs[:, 0], Xs[:, 1], c=ys, marker="+", label="Source samples") pl.xticks([]) pl.yticks([]) pl.legend(loc=0) pl.title("Source samples") pl.subplot(2, 2, 2) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples") pl.xticks([]) pl.yticks([]) pl.legend(loc=0) pl.title("Target samples") pl.subplot(2, 2, 3) pl.imshow(ot_sinkhorn_un.cost_, interpolation="nearest") pl.xticks([]) pl.yticks([]) pl.title("Cost matrix - unsupervised DA") pl.subplot(2, 2, 4) pl.imshow(ot_sinkhorn_semi.cost_, interpolation="nearest") pl.xticks([]) pl.yticks([]) pl.title("Cost matrix - semi-supervised DA") pl.tight_layout() # the optimal coupling in the semi-supervised DA case will exhibit " shape # similar" to the cost matrix, (block diagonal matrix) ############################################################################## # Fig 2 : plots optimal couplings for the different methods # --------------------------------------------------------- pl.figure(2, figsize=(8, 4)) pl.subplot(1, 2, 1) pl.imshow(ot_sinkhorn_un.coupling_, interpolation="nearest", cmap="gray_r") pl.xticks([]) pl.yticks([]) pl.title("Optimal coupling\nUnsupervised DA") pl.subplot(1, 2, 2) pl.imshow(ot_sinkhorn_semi.coupling_, interpolation="nearest", cmap="gray_r") pl.xticks([]) pl.yticks([]) pl.title("Optimal coupling\nSemi-supervised DA") pl.tight_layout() ############################################################################## # Fig 3 : plot transported samples # -------------------------------- # display transported samples pl.figure(4, figsize=(8, 4)) pl.subplot(1, 2, 1) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.5) pl.scatter( transp_Xs_sinkhorn_un[:, 0], transp_Xs_sinkhorn_un[:, 1], c=ys, marker="+", label="Transp samples", s=30, ) pl.title("Transported samples\nEmdTransport") pl.legend(loc=0) pl.xticks([]) pl.yticks([]) pl.subplot(1, 2, 2) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker="o", label="Target samples", alpha=0.5) pl.scatter( transp_Xs_sinkhorn_semi[:, 0], transp_Xs_sinkhorn_semi[:, 1], c=ys, marker="+", label="Transp samples", s=30, ) pl.title("Transported samples\nSinkhornTransport") pl.xticks([]) pl.yticks([]) pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/gaussian_gmm/000077500000000000000000000000001525521106200210565ustar00rootroot00000000000000python-pot-0.9.7+dfsg/examples/gaussian_gmm/index.rst000066400000000000000000000001271525521106200227170ustar00rootroot00000000000000 OT on Gaussian and Gaussian Mixture Models -------------------------------------------python-pot-0.9.7+dfsg/examples/gaussian_gmm/plot_GMMOT_plan.py000066400000000000000000000055551525521106200243750ustar00rootroot00000000000000# %% # -*- coding: utf-8 -*- r""" ==================================================== OT between GMM : plan and maps in 1D ==================================================== Illustration of the GMM plan for the Mixture Wasserstein between two GMM in 1D, as well as the two maps T_mean and T_rand. T_mean is the barycentric projection of the GMM coupling, and T_rand takes a random gaussian image between two components, according to the coupling and the GMMs. See [69] for details. .. [69] Delon, J., & Desolneux, A. (2020). A Wasserstein-type distance in the space of Gaussian mixture models. SIAM Journal on Imaging Sciences, 13(2), 936-970. """ # Author: Eloi Tanguy # Remi Flamary # Julie Delon # # License: MIT License # sphinx_gallery_thumbnail_number = 1 import numpy as np from ot.plot import plot1D_mat, rescale_for_imshow_plot from ot.gmm import gmm_ot_plan_density, gmm_pdf, gmm_ot_apply_map import matplotlib.pyplot as plt ############################################################################## # Generate GMMOT plan plot it # --------------------------- ks = 2 kt = 3 d = 1 eps = 0.1 m_s = np.array([[1], [2]]) m_t = np.array([[3], [4.2], [5]]) C_s = np.array([[[0.05]], [[0.06]]]) C_t = np.array([[[0.03]], [[0.07]], [[0.04]]]) w_s = np.array([0.4, 0.6]) w_t = np.array([0.4, 0.2, 0.4]) n = 500 a_x, b_x = 0, 3 x = np.linspace(a_x, b_x, n) a_y, b_y = 2, 6 y = np.linspace(a_y, b_y, n) plan_density = gmm_ot_plan_density( x[:, None], y[:, None], m_s, m_t, C_s, C_t, w_s, w_t, plan=None, atol=2e-2 ) a = gmm_pdf(x[:, None], m_s, C_s, w_s) b = gmm_pdf(y[:, None], m_t, C_t, w_t) plt.figure(figsize=(8, 8)) plot1D_mat( a, b, plan_density, title="GMM OT plan", plot_style="xy", a_label="Source distribution", b_label="Target distribution", ) ############################################################################## # Generate GMMOT maps and plot them over plan # ------------------------------------------- plt.figure(figsize=(8, 8)) ax_s, ax_t, ax_M = plot1D_mat( a, b, plan_density, plot_style="xy", title="GMM OT plan with T_mean and T_rand maps", a_label="Source distribution", b_label="Target distribution", ) T_mean = gmm_ot_apply_map(x[:, None], m_s, m_t, C_s, C_t, w_s, w_t, method="bary")[:, 0] x_rescaled, T_mean_rescaled = rescale_for_imshow_plot(x, T_mean, n, a_y=a_y, b_y=b_y) ax_M.plot( x_rescaled, T_mean_rescaled, label="T_mean", alpha=0.5, linewidth=5, color="aqua" ) T_rand = gmm_ot_apply_map( x[:, None], m_s, m_t, C_s, C_t, w_s, w_t, method="rand", seed=0 )[:, 0] x_rescaled, T_rand_rescaled = rescale_for_imshow_plot(x, T_rand, n, a_y=a_y, b_y=b_y) ax_M.scatter( x_rescaled, T_rand_rescaled, label="T_rand", alpha=0.5, s=20, color="orange" ) ax_M.legend(loc="upper left", fontsize=13) # %% python-pot-0.9.7+dfsg/examples/gaussian_gmm/plot_GMM_flow.py000066400000000000000000000124551525521106200241440ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ==================================================== Gradient Flow for GMM-OT distance ==================================================== Illustration of the flow of a Gaussian Mixture with respect to its GMM-OT distance with respect to a fixed GMM. """ # Author: Eloi Tanguy # Remi Flamary # Julie Delon # # License: MIT License # sphinx_gallery_thumbnail_number = 4 import numpy as np import matplotlib.pylab as pl from matplotlib import colormaps as cm import ot import ot.plot from ot.utils import proj_SDP, proj_simplex from ot.gmm import gmm_ot_loss import torch from torch.optim import Adam from matplotlib.patches import Ellipse ############################################################################## # Generate data and plot it # ------------------------- torch.manual_seed(3) ks = 3 kt = 2 d = 2 eps = 0.1 m_s = torch.randn(ks, d) m_s.requires_grad_() m_t = torch.randn(kt, d) C_s = torch.randn(ks, d, d) C_s = torch.matmul(C_s, torch.transpose(C_s, 2, 1)) C_s += eps * torch.eye(d)[None, :, :] * torch.ones(ks, 1, 1) C_s.requires_grad_() C_t = torch.randn(kt, d, d) C_t = torch.matmul(C_t, torch.transpose(C_t, 2, 1)) C_t += eps * torch.eye(d)[None, :, :] * torch.ones(kt, 1, 1) w_s = torch.randn(ks) w_s = proj_simplex(w_s) w_s.requires_grad_() w_t = torch.tensor(ot.unif(kt)) def draw_cov(mu, C, color=None, label=None, nstd=1, alpha=0.5): def eigsorted(cov): if torch.is_tensor(cov): cov = cov.detach().numpy() vals, vecs = np.linalg.eigh(cov) order = vals.argsort()[::-1].copy() return vals[order], vecs[:, order] vals, vecs = eigsorted(C) theta = np.degrees(np.arctan2(*vecs[:, 0][::-1])) w, h = 2 * nstd * np.sqrt(vals) ell = Ellipse( xy=(mu[0], mu[1]), width=w, height=h, alpha=alpha, angle=theta, facecolor=color, edgecolor=color, label=label, fill=True, ) pl.gca().add_artist(ell) def draw_gmm(ms, Cs, ws, color=None, nstd=0.5, alpha=1): for k in range(ms.shape[0]): draw_cov(ms[k], Cs[k], color, None, nstd, alpha * ws[k]) axis = [-3, 3, -3, 3] pl.figure(1, (20, 10)) pl.clf() pl.subplot(1, 2, 1) pl.scatter(m_s[:, 0].detach(), m_s[:, 1].detach(), color="C0") draw_gmm(m_s.detach(), C_s.detach(), torch.softmax(w_s, 0).detach().numpy(), color="C0") pl.axis(axis) pl.title("Source GMM") pl.subplot(1, 2, 2) pl.scatter(m_t[:, 0].detach(), m_t[:, 1].detach(), color="C1") draw_gmm(m_t.detach(), C_t.detach(), w_t.numpy(), color="C1") pl.axis(axis) pl.title("Target GMM") ############################################################################## # Gradient descent loop # ------------------------ n_gd_its = 100 lr = 3e-2 opt = Adam( [ {"params": m_s, "lr": 2 * lr}, {"params": C_s, "lr": lr}, {"params": w_s, "lr": lr}, ] ) m_list = [m_s.data.numpy().copy()] C_list = [C_s.data.numpy().copy()] w_list = [torch.softmax(w_s, 0).data.numpy().copy()] loss_list = [] for _ in range(n_gd_its): opt.zero_grad() loss = gmm_ot_loss(m_s, m_t, C_s, C_t, torch.softmax(w_s, 0), w_t) loss.backward() opt.step() with torch.no_grad(): C_s.data = proj_SDP(C_s.data, vmin=1e-6) m_list.append(m_s.data.numpy().copy()) C_list.append(C_s.data.numpy().copy()) w_list.append(torch.softmax(w_s, 0).data.numpy().copy()) loss_list.append(loss.item()) pl.figure(2) pl.clf() pl.plot(loss_list) pl.title("Loss") pl.xlabel("its") pl.ylabel("loss") ############################################################################## # Last step visualisation # ------------------------ axis = [-3, 3, -3, 3] pl.figure(3, (10, 10)) pl.clf() pl.title("GMM flow, last step") pl.scatter(m_list[0][:, 0], m_list[0][:, 1], color="C0", label="Source") draw_gmm(m_list[0], C_list[0], w_list[0], color="C0") pl.axis(axis) pl.scatter(m_t[:, 0].detach(), m_t[:, 1].detach(), color="C1", label="Target") draw_gmm(m_t.detach(), C_t.detach(), w_t.numpy(), color="C1") pl.axis(axis) k = -1 pl.scatter(m_list[k][:, 0], m_list[k][:, 1], color="C2", alpha=1, label="Last step") draw_gmm(m_list[k], C_list[k], w_list[0], color="C2", alpha=1) pl.axis(axis) pl.legend(fontsize=15) ############################################################################## # Steps visualisation # ------------------------ def index_to_color(i): return int(i**0.5) n_steps_visu = 100 pl.figure(3, (10, 10)) pl.clf() pl.title("GMM flow, all steps") its_to_show = [int(x) for x in np.linspace(1, n_gd_its - 1, n_steps_visu)] cmp = cm["plasma"].resampled(index_to_color(n_steps_visu)) pl.scatter( m_list[0][:, 0], m_list[0][:, 1], color=cmp(index_to_color(0)), label="Source" ) draw_gmm(m_list[0], C_list[0], w_list[0], color=cmp(index_to_color(0))) pl.scatter( m_t[:, 0].detach(), m_t[:, 1].detach(), color=cmp(index_to_color(n_steps_visu - 1)), label="Target", ) draw_gmm( m_t.detach(), C_t.detach(), w_t.numpy(), color=cmp(index_to_color(n_steps_visu - 1)) ) for k in its_to_show: pl.scatter( m_list[k][:, 0], m_list[k][:, 1], color=cmp(index_to_color(k)), alpha=0.8 ) draw_gmm(m_list[k], C_list[k], w_list[0], color=cmp(index_to_color(k)), alpha=0.04) pl.axis(axis) pl.legend(fontsize=15) # %% python-pot-0.9.7+dfsg/examples/gromov/000077500000000000000000000000001525521106200177155ustar00rootroot00000000000000python-pot-0.9.7+dfsg/examples/gromov/index.rst000066400000000000000000000001131525521106200215510ustar00rootroot00000000000000 Gromov-Wasserstein (GW) and Fused GW ------------------------------------python-pot-0.9.7+dfsg/examples/gromov/plot_barycenter_fgw.py000066400000000000000000000145441525521106200243360ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ================================= Barycenter of labeled graphs with FGW ================================= This example illustrates the computation barycenter of labeled graphs using FGW [18]. Requires networkx >=2 [18] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs" International Conference on Machine Learning (ICML). 2019. """ # Author: Titouan Vayer # # License: MIT License # %% load libraries import numpy as np import matplotlib.pyplot as plt import networkx as nx import math from scipy.sparse.csgraph import shortest_path import matplotlib.colors as mcol from matplotlib import cm from ot.gromov import fgw_barycenters # %% Graph functions def find_thresh(C, inf=0.5, sup=3, step=10): """Trick to find the adequate thresholds from where value of the C matrix are considered close enough to say that nodes are connected The threshold is found by a linesearch between values "inf" and "sup" with "step" thresholds tested. The optimal threshold is the one which minimizes the reconstruction error between the shortest_path matrix coming from the thresholded adjacency matrix and the original matrix. Parameters ---------- C : ndarray, shape (n_nodes,n_nodes) The structure matrix to threshold inf : float The beginning of the linesearch sup : float The end of the linesearch step : integer Number of thresholds tested """ dist = [] search = np.linspace(inf, sup, step) for thresh in search: Cprime = sp_to_adjacency(C, 0, thresh) SC = shortest_path(Cprime, method="D") SC[SC == float("inf")] = 100 dist.append(np.linalg.norm(SC - C)) return search[np.argmin(dist)], dist def sp_to_adjacency(C, threshinf=0.2, threshsup=1.8): """Thresholds the structure matrix in order to compute an adjacency matrix. All values between threshinf and threshsup are considered representing connected nodes and set to 1. Else are set to 0 Parameters ---------- C : ndarray, shape (n_nodes,n_nodes) The structure matrix to threshold threshinf : float The minimum value of distance from which the new value is set to 1 threshsup : float The maximum value of distance from which the new value is set to 1 Returns ------- C : ndarray, shape (n_nodes,n_nodes) The threshold matrix. Each element is in {0,1} """ H = np.zeros_like(C) np.fill_diagonal(H, np.diagonal(C)) C = C - H C = np.minimum(np.maximum(C, threshinf), threshsup) C[C == threshsup] = 0 C[C != 0] = 1 return C def build_noisy_circular_graph( N=20, mu=0, sigma=0.3, with_noise=False, structure_noise=False, p=None ): """Create a noisy circular graph""" g = nx.Graph() g.add_nodes_from(list(range(N))) for i in range(N): noise = np.random.normal(mu, sigma, 1)[0] if with_noise: g.add_node(i, attr_name=math.sin((2 * i * math.pi / N)) + noise) else: g.add_node(i, attr_name=math.sin(2 * i * math.pi / N)) g.add_edge(i, i + 1) if structure_noise: randomint = np.random.randint(0, p) if randomint == 0: if i <= N - 3: g.add_edge(i, i + 2) if i == N - 2: g.add_edge(i, 0) if i == N - 1: g.add_edge(i, 1) g.add_edge(N, 0) noise = np.random.normal(mu, sigma, 1)[0] if with_noise: g.add_node(N, attr_name=math.sin((2 * N * math.pi / N)) + noise) else: g.add_node(N, attr_name=math.sin(2 * N * math.pi / N)) return g def graph_colors(nx_graph, vmin=0, vmax=7): cnorm = mcol.Normalize(vmin=vmin, vmax=vmax) cpick = cm.ScalarMappable(norm=cnorm, cmap="viridis") cpick.set_array([]) val_map = {} for k, v in nx.get_node_attributes(nx_graph, "attr_name").items(): val_map[k] = cpick.to_rgba(v) colors = [] for node in nx_graph.nodes(): colors.append(val_map[node]) return colors ############################################################################## # Generate data # ------------- # %% circular dataset # We build a dataset of noisy circular graphs. # Noise is added on the structures by random connections and on the features by gaussian noise. np.random.seed(30) X0 = [] for k in range(9): X0.append( build_noisy_circular_graph( np.random.randint(15, 25), with_noise=True, structure_noise=True, p=3 ) ) ############################################################################## # Plot data # --------- # %% Plot graphs plt.figure(figsize=(8, 10)) for i in range(len(X0)): plt.subplot(3, 3, i + 1) g = X0[i] pos = nx.kamada_kawai_layout(g) nx.draw_networkx( g, pos=pos, node_color=graph_colors(g, vmin=-1, vmax=1), with_labels=False, node_size=100, ) plt.suptitle("Dataset of noisy graphs. Color indicates the label", fontsize=20) plt.show() ############################################################################## # Barycenter computation # ---------------------- # %% We compute the barycenter using FGW. Structure matrices are computed using the shortest_path distance in the graph # Features distances are the euclidean distances Cs = [shortest_path(nx.adjacency_matrix(x).toarray()) for x in X0] ps = [np.ones(len(x.nodes())) / len(x.nodes()) for x in X0] Ys = [ np.array([v for (k, v) in nx.get_node_attributes(x, "attr_name").items()]).reshape( -1, 1 ) for x in X0 ] lambdas = np.array([np.ones(len(Ys)) / len(Ys)]).ravel() sizebary = 15 # we choose a barycenter with 15 nodes A, C, log = fgw_barycenters(sizebary, Ys, Cs, ps, lambdas, alpha=0.95, log=True) ############################################################################## # Plot Barycenter # ------------------------- # %% Create the barycenter bary = nx.from_numpy_array( sp_to_adjacency(C, threshinf=0, threshsup=find_thresh(C, sup=100, step=100)[0]) ) for i, v in enumerate(A.ravel()): bary.add_node(i, attr_name=v) # %% pos = nx.kamada_kawai_layout(bary) nx.draw_networkx( bary, pos=pos, node_color=graph_colors(bary, vmin=-1, vmax=1), with_labels=False ) plt.suptitle("Barycenter", fontsize=20) plt.show() python-pot-0.9.7+dfsg/examples/gromov/plot_entropic_semirelaxed_fgw.py000066400000000000000000000265741525521106200264130ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ========================== Entropic-regularized semi-relaxed (Fused) Gromov-Wasserstein example ========================== This example is designed to show how to use the entropic semi-relaxed Gromov-Wasserstein and the entropic semi-relaxed Fused Gromov-Wasserstein divergences. Entropic-regularized sr(F)GW between two graphs G1 and G2 searches for a reweighing of the nodes of G2 at a minimal entropic-regularized (F)GW distance from G1. First, we generate two graphs following Stochastic Block Models, then show how to compute their srGW matchings and illustrate them. These graphs are then endowed with node features and we follow the same process with srFGW. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2021. """ # Author: Cédric Vincent-Cuaz # # License: MIT License # sphinx_gallery_thumbnail_number = 1 import numpy as np import matplotlib.pylab as pl from ot.gromov import ( entropic_semirelaxed_gromov_wasserstein, entropic_semirelaxed_fused_gromov_wasserstein, gromov_wasserstein, fused_gromov_wasserstein, ) import networkx from networkx.generators.community import stochastic_block_model as sbm ############################################################################# # # Generate two graphs following Stochastic Block models of 2 and 3 clusters. # --------------------------------------------- N2 = 20 # 2 communities N3 = 30 # 3 communities p2 = [[1.0, 0.1], [0.1, 0.9]] p3 = [[1.0, 0.1, 0.0], [0.1, 0.95, 0.1], [0.0, 0.1, 0.9]] G2 = sbm(seed=0, sizes=[N2 // 2, N2 // 2], p=p2) G3 = sbm(seed=0, sizes=[N3 // 3, N3 // 3, N3 // 3], p=p3) C2 = networkx.to_numpy_array(G2) C3 = networkx.to_numpy_array(G3) h2 = np.ones(C2.shape[0]) / C2.shape[0] h3 = np.ones(C3.shape[0]) / C3.shape[0] # Add weights on the edges for visualization later on weight_intra_G2 = 5 weight_inter_G2 = 0.5 weight_intra_G3 = 1.0 weight_inter_G3 = 1.5 weightedG2 = networkx.Graph() part_G2 = [G2.nodes[i]["block"] for i in range(N2)] for node in G2.nodes(): weightedG2.add_node(node) for i, j in G2.edges(): if part_G2[i] == part_G2[j]: weightedG2.add_edge(i, j, weight=weight_intra_G2) else: weightedG2.add_edge(i, j, weight=weight_inter_G2) weightedG3 = networkx.Graph() part_G3 = [G3.nodes[i]["block"] for i in range(N3)] for node in G3.nodes(): weightedG3.add_node(node) for i, j in G3.edges(): if part_G3[i] == part_G3[j]: weightedG3.add_edge(i, j, weight=weight_intra_G3) else: weightedG3.add_edge(i, j, weight=weight_inter_G3) ############################################################################# # # Compute their entropic-regularized semi-relaxed Gromov-Wasserstein divergences # --------------------------------------------- # 0) GW(C2, h2, C3, h3) for reference OT, log = gromov_wasserstein(C2, C3, h2, h3, symmetric=True, log=True) gw = log["gw_dist"] # 1) srGW_e(C2, h2, C3) OT_23, log_23 = entropic_semirelaxed_gromov_wasserstein( C2, C3, h2, symmetric=True, epsilon=1.0, G0=None, log=True ) srgw_23 = log_23["srgw_dist"] # 2) srGW_e(C3, h3, C2) OT_32, log_32 = entropic_semirelaxed_gromov_wasserstein( C3, C2, h3, symmetric=None, epsilon=1.0, G0=None, log=True ) srgw_32 = log_32["srgw_dist"] print("GW(C2, C3) = ", gw) print("srGW_e(C2, h2, C3) = ", srgw_23) print("srGW_e(C3, h3, C2) = ", srgw_32) ############################################################################# # # Visualization of the entropic-regularized semi-relaxed Gromov-Wasserstein matchings # --------------------------------------------- # # We color nodes of the graph on the right - then project its node colors # based on the optimal transport plan from the entropic srGW matching. # We adjust the intensity of links across domains proportionaly to the mass # sent, adding a minimal intensity of 0.1 if mass sent is not zero. def draw_graph( G, C, nodes_color_part, Gweights=None, pos=None, edge_color="black", node_size=None, shiftx=0, seed=0, ): if pos is None: pos = networkx.spring_layout(G, scale=1.0, seed=seed) if shiftx != 0: for k, v in pos.items(): v[0] = v[0] + shiftx alpha_edge = 0.7 width_edge = 1.8 if Gweights is None: networkx.draw_networkx_edges( G, pos, width=width_edge, alpha=alpha_edge, edge_color=edge_color ) else: # We make more visible connections between activated nodes n = len(Gweights) edgelist_activated = [] edgelist_deactivated = [] for i in range(n): for j in range(n): if Gweights[i] * Gweights[j] * C[i, j] > 0: edgelist_activated.append((i, j)) elif C[i, j] > 0: edgelist_deactivated.append((i, j)) networkx.draw_networkx_edges( G, pos, edgelist=edgelist_activated, width=width_edge, alpha=alpha_edge, edge_color=edge_color, ) networkx.draw_networkx_edges( G, pos, edgelist=edgelist_deactivated, width=width_edge, alpha=0.1, edge_color=edge_color, ) if Gweights is None: for node, node_color in enumerate(nodes_color_part): networkx.draw_networkx_nodes( G, pos, nodelist=[node], node_size=node_size, alpha=1, node_color=node_color, ) else: scaled_Gweights = Gweights / (0.5 * Gweights.max()) nodes_size = node_size * scaled_Gweights for node, node_color in enumerate(nodes_color_part): networkx.draw_networkx_nodes( G, pos, nodelist=[node], node_size=nodes_size[node], alpha=1, node_color=node_color, ) return pos def draw_transp_colored_srGW( G1, C1, G2, C2, part_G1, p1, p2, T, pos1=None, pos2=None, shiftx=4, switchx=False, node_size=70, seed_G1=0, seed_G2=0, ): starting_color = 0 # get graphs partition and their coloring part1 = part_G1.copy() unique_colors = ["C%s" % (starting_color + i) for i in np.unique(part1)] nodes_color_part1 = [] for cluster in part1: nodes_color_part1.append(unique_colors[cluster]) nodes_color_part2 = [] # T: getting colors assignment from argmin of columns for i in range(len(G2.nodes())): j = np.argmax(T[:, i]) nodes_color_part2.append(nodes_color_part1[j]) pos1 = draw_graph( G1, C1, nodes_color_part1, Gweights=p1, pos=pos1, node_size=node_size, shiftx=0, seed=seed_G1, ) pos2 = draw_graph( G2, C2, nodes_color_part2, Gweights=p2, pos=pos2, node_size=node_size, shiftx=shiftx, seed=seed_G2, ) for k1, v1 in pos1.items(): max_Tk1 = np.max(T[k1, :]) for k2, v2 in pos2.items(): if T[k1, k2] > 0: pl.plot( [pos1[k1][0], pos2[k2][0]], [pos1[k1][1], pos2[k2][1]], "-", lw=0.6, alpha=min(T[k1, k2] / max_Tk1 + 0.1, 1.0), color=nodes_color_part1[k1], ) return pos1, pos2 node_size = 40 fontsize = 10 seed_G2 = 0 seed_G3 = 4 pl.figure(1, figsize=(8, 2.5)) pl.clf() pl.subplot(121) pl.axis("off") pl.axis pl.title( r"$srGW_e(\mathbf{C_2},\mathbf{h_2},\mathbf{C_3}) =%s$" % (np.round(srgw_23, 3)), fontsize=fontsize, ) hbar2 = OT_23.sum(axis=0) pos1, pos2 = draw_transp_colored_srGW( weightedG2, C2, weightedG3, C3, part_G2, p1=None, p2=hbar2, T=OT_23, shiftx=1.5, node_size=node_size, seed_G1=seed_G2, seed_G2=seed_G3, ) pl.subplot(122) pl.axis("off") hbar3 = OT_32.sum(axis=0) pl.title( r"$srGW_e(\mathbf{C_3}, \mathbf{h_3},\mathbf{C_2}) =%s$" % (np.round(srgw_32, 3)), fontsize=fontsize, ) pos1, pos2 = draw_transp_colored_srGW( weightedG3, C3, weightedG2, C2, part_G3, p1=None, p2=hbar3, T=OT_32, pos1=pos2, pos2=pos1, shiftx=3.0, node_size=node_size, seed_G1=0, seed_G2=0, ) pl.tight_layout() pl.show() ############################################################################# # # Add node features # --------------------------------------------- # We add node features with given mean - by clusters # and inversely proportional to clusters' intra-connectivity F2 = np.zeros((N2, 1)) for i, c in enumerate(part_G2): F2[i, 0] = np.random.normal(loc=c, scale=0.01) F3 = np.zeros((N3, 1)) for i, c in enumerate(part_G3): F3[i, 0] = np.random.normal(loc=2.0 - c, scale=0.01) ############################################################################# # # Compute their semi-relaxed Fused Gromov-Wasserstein divergences # --------------------------------------------- alpha = 0.5 # Compute pairwise euclidean distance between node features M = (F2**2).dot(np.ones((1, N3))) + np.ones((N2, 1)).dot((F3**2).T) - 2 * F2.dot(F3.T) # 0) FGW_alpha(C2, F2, h2, C3, F3, h3) for reference OT, log = fused_gromov_wasserstein( M, C2, C3, h2, h3, symmetric=True, alpha=alpha, log=True ) fgw = log["fgw_dist"] # 1) srFGW_e(C2, F2, h2, C3, F3) OT_23, log_23 = entropic_semirelaxed_fused_gromov_wasserstein( M, C2, C3, h2, symmetric=True, epsilon=1.0, alpha=0.5, log=True, G0=None ) srfgw_23 = log_23["srfgw_dist"] # 2) srFGW(C3, F3, h3, C2, F2) OT_32, log_32 = entropic_semirelaxed_fused_gromov_wasserstein( M.T, C3, C2, h3, symmetric=None, epsilon=1.0, alpha=alpha, log=True, G0=None ) srfgw_32 = log_32["srfgw_dist"] print("FGW(C2, F2, C3, F3) = ", fgw) print(r"$srGW_e$(C2, F2, h2, C3, F3) = ", srfgw_23) print(r"$srGW_e$(C3, F3, h3, C2, F2) = ", srfgw_32) ############################################################################# # # Visualization of the entropic semi-relaxed Fused Gromov-Wasserstein matchings # --------------------------------------------- # # We color nodes of the graph on the right - then project its node colors # based on the optimal transport plan from the srFGW matching # NB: colors refer to clusters - not to node features pl.figure(2, figsize=(8, 2.5)) pl.clf() pl.subplot(121) pl.axis("off") pl.axis pl.title( r"$srFGW_e(\mathbf{C_2},\mathbf{F_2},\mathbf{h_2},\mathbf{C_3},\mathbf{F_3}) =%s$" % (np.round(srfgw_23, 3)), fontsize=fontsize, ) hbar2 = OT_23.sum(axis=0) pos1, pos2 = draw_transp_colored_srGW( weightedG2, C2, weightedG3, C3, part_G2, p1=None, p2=hbar2, T=OT_23, shiftx=1.5, node_size=node_size, seed_G1=seed_G2, seed_G2=seed_G3, ) pl.subplot(122) pl.axis("off") hbar3 = OT_32.sum(axis=0) pl.title( r"$srFGW_e(\mathbf{C_3}, \mathbf{F_3}, \mathbf{h_3}, \mathbf{C_2}, \mathbf{F_2}) =%s$" % (np.round(srfgw_32, 3)), fontsize=fontsize, ) pos1, pos2 = draw_transp_colored_srGW( weightedG3, C3, weightedG2, C2, part_G3, p1=None, p2=hbar3, T=OT_32, pos1=pos2, pos2=pos1, shiftx=3.0, node_size=node_size, seed_G1=0, seed_G2=0, ) pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/gromov/plot_fgw.py000066400000000000000000000105111525521106200221060ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ============================== Plot Fused-Gromov-Wasserstein ============================== This example first illustrates the computation of FGW for 1D measures estimated using a Conditional Gradient solver [24]. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs" International Conference on Machine Learning (ICML). 2019. """ # Author: Titouan Vayer # # License: MIT License # sphinx_gallery_thumbnail_number = 3 import matplotlib.pyplot as pl import numpy as np import ot from ot.gromov import gromov_wasserstein, fused_gromov_wasserstein ############################################################################## # Generate data # ------------- # parameters # We create two 1D random measures n = 20 # number of points in the first distribution n2 = 30 # number of points in the second distribution sig = 1 # std of first distribution sig2 = 0.1 # std of second distribution np.random.seed(0) phi = np.arange(n)[:, None] xs = phi + sig * np.random.randn(n, 1) ys = np.vstack( (np.ones((n // 2, 1)), 0 * np.ones((n // 2, 1))) ) + sig2 * np.random.randn(n, 1) phi2 = np.arange(n2)[:, None] xt = phi2 + sig * np.random.randn(n2, 1) yt = np.vstack( (np.ones((n2 // 2, 1)), 0 * np.ones((n2 // 2, 1))) ) + sig2 * np.random.randn(n2, 1) yt = yt[::-1, :] p = ot.unif(n) q = ot.unif(n2) ############################################################################## # Plot data # --------- # plot the distributions pl.figure(1, (7, 7)) pl.subplot(2, 1, 1) pl.scatter(ys, xs, c=phi, s=70) pl.ylabel("Feature value a", fontsize=20) pl.title("$\mu=\sum_i \delta_{x_i,a_i}$", fontsize=25, y=1) pl.xticks(()) pl.yticks(()) pl.subplot(2, 1, 2) pl.scatter(yt, xt, c=phi2, s=70) pl.xlabel("coordinates x/y", fontsize=25) pl.ylabel("Feature value b", fontsize=20) pl.title("$\\nu=\sum_j \delta_{y_j,b_j}$", fontsize=25, y=1) pl.yticks(()) pl.tight_layout() pl.show() ############################################################################## # Create structure matrices and across-feature distance matrix # ------------------------------------------------------------ # Structure matrices and across-features distance matrix C1 = ot.dist(xs) C2 = ot.dist(xt) M = ot.dist(ys, yt) w1 = ot.unif(C1.shape[0]) w2 = ot.unif(C2.shape[0]) Got = ot.emd([], [], M) ############################################################################## # Plot matrices # ------------- cmap = "Reds" pl.figure(2, (5, 5)) fs = 15 l_x = [0, 5, 10, 15] l_y = [0, 5, 10, 15, 20, 25] gs = pl.GridSpec(5, 5) ax1 = pl.subplot(gs[3:, :2]) pl.imshow(C1, cmap=cmap, interpolation="nearest") pl.title("$C_1$", fontsize=fs) pl.xlabel("$k$", fontsize=fs) pl.ylabel("$i$", fontsize=fs) pl.xticks(l_x) pl.yticks(l_x) ax2 = pl.subplot(gs[:3, 2:]) pl.imshow(C2, cmap=cmap, interpolation="nearest") pl.title("$C_2$", fontsize=fs) pl.ylabel("$l$", fontsize=fs) pl.xticks(()) pl.yticks(l_y) ax2.set_aspect("auto") ax3 = pl.subplot(gs[3:, 2:], sharex=ax2, sharey=ax1) pl.imshow(M, cmap=cmap, interpolation="nearest") pl.yticks(l_x) pl.xticks(l_y) pl.ylabel("$i$", fontsize=fs) pl.title("$M_{AB}$", fontsize=fs) pl.xlabel("$j$", fontsize=fs) pl.tight_layout() ax3.set_aspect("auto") pl.show() ############################################################################## # Compute FGW/GW # -------------- # Computing FGW and GW alpha = 1e-3 ot.tic() Gwg, logw = fused_gromov_wasserstein( M, C1, C2, p, q, loss_fun="square_loss", alpha=alpha, verbose=True, log=True ) ot.toc() # reload_ext WGW Gg, log = gromov_wasserstein( C1, C2, p, q, loss_fun="square_loss", verbose=True, log=True ) ############################################################################## # Visualize transport matrices # ---------------------------- # visu OT matrix cmap = "Blues" fs = 15 pl.figure(3, (13, 5)) pl.clf() pl.subplot(1, 3, 1) pl.imshow(Got, cmap=cmap, interpolation="nearest") pl.ylabel("$i$", fontsize=fs) pl.xticks(()) pl.title("Wasserstein ($M$ only)") pl.subplot(1, 3, 2) pl.imshow(Gg, cmap=cmap, interpolation="nearest") pl.title("Gromov ($C_1,C_2$ only)") pl.xticks(()) pl.subplot(1, 3, 3) pl.imshow(Gwg, cmap=cmap, interpolation="nearest") pl.title("FGW ($M+C_1,C_2$)") pl.xlabel("$j$", fontsize=fs) pl.ylabel("$i$", fontsize=fs) pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/gromov/plot_fgw_solvers.py000066400000000000000000000327661525521106200237030ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ============================== Comparison of Fused Gromov-Wasserstein solvers ============================== .. note:: Example added in release: 0.9.1. This example illustrates the computation of FGW for attributed graphs using 4 different solvers to estimate the distance based on Conditional Gradient [24], Sinkhorn projections [12, 51] and alternated Bregman projections [63, 64]. We generate two graphs following Stochastic Block Models further endowed with node features and compute their FGW matchings. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon (2016), "Gromov-Wasserstein averaging of kernel and distance matrices". International Conference on Machine Learning (ICML). [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs" International Conference on Machine Learning (ICML). 2019. [51] Xu, H., Luo, D., Zha, H., & Carin, L. (2019). "Gromov-wasserstein learning for graph matching and node embedding". In International Conference on Machine Learning (ICML), 2019. [63] Li, J., Tang, J., Kong, L., Liu, H., Li, J., So, A. M. C., & Blanchet, J. "A Convergent Single-Loop Algorithm for Relaxation of Gromov-Wasserstein in Graph Data". International Conference on Learning Representations (ICLR), 2023. [64] Ma, X., Chu, X., Wang, Y., Lin, Y., Zhao, J., Ma, L., & Zhu, W. "Fused Gromov-Wasserstein Graph Mixup for Graph-level Classifications". In Thirty-seventh Conference on Neural Information Processing Systems (NeurIPS), 2023. """ # Author: Cédric Vincent-Cuaz # # License: MIT License # sphinx_gallery_thumbnail_number = 1 import numpy as np import matplotlib.pylab as pl from ot.gromov import ( fused_gromov_wasserstein, entropic_fused_gromov_wasserstein, BAPG_fused_gromov_wasserstein, ) import networkx from networkx.generators.community import stochastic_block_model as sbm from time import time ############################################################################# # # Generate two graphs following Stochastic Block models of 2 and 3 clusters. # --------------------------------------------- np.random.seed(0) N2 = 20 # 2 communities N3 = 30 # 3 communities p2 = [[1.0, 0.1], [0.1, 0.9]] p3 = [[1.0, 0.1, 0.0], [0.1, 0.95, 0.1], [0.0, 0.1, 0.9]] G2 = sbm(seed=0, sizes=[N2 // 2, N2 // 2], p=p2) G3 = sbm(seed=0, sizes=[N3 // 3, N3 // 3, N3 // 3], p=p3) part_G2 = [G2.nodes[i]["block"] for i in range(N2)] part_G3 = [G3.nodes[i]["block"] for i in range(N3)] C2 = networkx.to_numpy_array(G2) C3 = networkx.to_numpy_array(G3) # We add node features with given mean - by clusters # and inversely proportional to clusters' intra-connectivity F2 = np.zeros((N2, 1)) for i, c in enumerate(part_G2): F2[i, 0] = np.random.normal(loc=c, scale=0.01) F3 = np.zeros((N3, 1)) for i, c in enumerate(part_G3): F3[i, 0] = np.random.normal(loc=2.0 - c, scale=0.01) # Compute pairwise euclidean distance between node features M = (F2**2).dot(np.ones((1, N3))) + np.ones((N2, 1)).dot((F3**2).T) - 2 * F2.dot(F3.T) h2 = np.ones(C2.shape[0]) / C2.shape[0] h3 = np.ones(C3.shape[0]) / C3.shape[0] ############################################################################# # # Compute their Fused Gromov-Wasserstein distances # --------------------------------------------- alpha = 0.5 # Conditional Gradient algorithm print("Conditional Gradient \n") start_cg = time() T_cg, log_cg = fused_gromov_wasserstein( M, C2, C3, h2, h3, "square_loss", alpha=alpha, tol_rel=1e-9, verbose=True, log=True ) end_cg = time() time_cg = 1000 * (end_cg - start_cg) # Proximal Point algorithm with Kullback-Leibler as proximal operator print("Proximal Point Algorithm \n") start_ppa = time() T_ppa, log_ppa = entropic_fused_gromov_wasserstein( M, C2, C3, h2, h3, "square_loss", alpha=alpha, epsilon=1.0, solver="PPA", tol=1e-9, log=True, verbose=True, warmstart=False, numItermax=10, ) end_ppa = time() time_ppa = 1000 * (end_ppa - start_ppa) # Projected Gradient algorithm with entropic regularization print("Projected Gradient Descent \n") start_pgd = time() T_pgd, log_pgd = entropic_fused_gromov_wasserstein( M, C2, C3, h2, h3, "square_loss", alpha=alpha, epsilon=0.01, solver="PGD", tol=1e-9, log=True, verbose=True, warmstart=False, numItermax=10, ) end_pgd = time() time_pgd = 1000 * (end_pgd - start_pgd) # Alternated Bregman Projected Gradient algorithm with Kullback-Leibler as proximal operator print("Bregman Alternated Projected Gradient \n") start_bapg = time() T_bapg, log_bapg = BAPG_fused_gromov_wasserstein( M, C2, C3, h2, h3, "square_loss", alpha=alpha, epsilon=1.0, tol=1e-9, marginal_loss=True, verbose=True, log=True, ) end_bapg = time() time_bapg = 1000 * (end_bapg - start_bapg) print( "Fused Gromov-Wasserstein distance estimated with Conditional Gradient solver: " + str(log_cg["fgw_dist"]) ) print( "Fused Gromov-Wasserstein distance estimated with Proximal Point solver: " + str(log_ppa["fgw_dist"]) ) print( "Entropic Fused Gromov-Wasserstein distance estimated with Projected Gradient solver: " + str(log_pgd["fgw_dist"]) ) print( "Fused Gromov-Wasserstein distance estimated with Projected Gradient solver: " + str(log_bapg["fgw_dist"]) ) # compute OT sparsity level T_cg_sparsity = 100 * (T_cg == 0.0).astype(np.float64).sum() / (N2 * N3) T_ppa_sparsity = 100 * (T_ppa == 0.0).astype(np.float64).sum() / (N2 * N3) T_pgd_sparsity = 100 * (T_pgd == 0.0).astype(np.float64).sum() / (N2 * N3) T_bapg_sparsity = 100 * (T_bapg == 0.0).astype(np.float64).sum() / (N2 * N3) # Methods using Sinkhorn/Bregman projections tend to produce feasibility errors on the # marginal constraints err_cg = np.linalg.norm(T_cg.sum(1) - h2) + np.linalg.norm(T_cg.sum(0) - h3) err_ppa = np.linalg.norm(T_ppa.sum(1) - h2) + np.linalg.norm(T_ppa.sum(0) - h3) err_pgd = np.linalg.norm(T_pgd.sum(1) - h2) + np.linalg.norm(T_pgd.sum(0) - h3) err_bapg = np.linalg.norm(T_bapg.sum(1) - h2) + np.linalg.norm(T_bapg.sum(0) - h3) ############################################################################# # # Visualization of the Fused Gromov-Wasserstein matchings # --------------------------------------------- # # We color nodes of the graph on the right - then project its node colors # based on the optimal transport plan from the FGW matchings # We adjust the intensity of links across domains proportionaly to the mass # sent, adding a minimal intensity of 0.1 if mass sent is not zero. # For each matching, all node sizes are proportionnal to their mass computed # from marginals of the OT plan to illustrate potential feasibility errors. # NB: colors refer to clusters - not to node features # Add weights on the edges for visualization later on weight_intra_G2 = 5 weight_inter_G2 = 0.5 weight_intra_G3 = 1.0 weight_inter_G3 = 1.5 weightedG2 = networkx.Graph() part_G2 = [G2.nodes[i]["block"] for i in range(N2)] for node in G2.nodes(): weightedG2.add_node(node) for i, j in G2.edges(): if part_G2[i] == part_G2[j]: weightedG2.add_edge(i, j, weight=weight_intra_G2) else: weightedG2.add_edge(i, j, weight=weight_inter_G2) weightedG3 = networkx.Graph() part_G3 = [G3.nodes[i]["block"] for i in range(N3)] for node in G3.nodes(): weightedG3.add_node(node) for i, j in G3.edges(): if part_G3[i] == part_G3[j]: weightedG3.add_edge(i, j, weight=weight_intra_G3) else: weightedG3.add_edge(i, j, weight=weight_inter_G3) def draw_graph( G, C, nodes_color_part, Gweights=None, pos=None, edge_color="black", node_size=None, shiftx=0, seed=0, ): if pos is None: pos = networkx.spring_layout(G, scale=1.0, seed=seed) if shiftx != 0: for k, v in pos.items(): v[0] = v[0] + shiftx alpha_edge = 0.7 width_edge = 1.8 if Gweights is None: networkx.draw_networkx_edges( G, pos, width=width_edge, alpha=alpha_edge, edge_color=edge_color ) else: # We make more visible connections between activated nodes n = len(Gweights) edgelist_activated = [] edgelist_deactivated = [] for i in range(n): for j in range(n): if Gweights[i] * Gweights[j] * C[i, j] > 0: edgelist_activated.append((i, j)) elif C[i, j] > 0: edgelist_deactivated.append((i, j)) networkx.draw_networkx_edges( G, pos, edgelist=edgelist_activated, width=width_edge, alpha=alpha_edge, edge_color=edge_color, ) networkx.draw_networkx_edges( G, pos, edgelist=edgelist_deactivated, width=width_edge, alpha=0.1, edge_color=edge_color, ) if Gweights is None: for node, node_color in enumerate(nodes_color_part): networkx.draw_networkx_nodes( G, pos, nodelist=[node], node_size=node_size, alpha=1, node_color=node_color, ) else: scaled_Gweights = Gweights / (0.5 * Gweights.max()) nodes_size = node_size * scaled_Gweights for node, node_color in enumerate(nodes_color_part): networkx.draw_networkx_nodes( G, pos, nodelist=[node], node_size=nodes_size[node], alpha=1, node_color=node_color, ) return pos def draw_transp_colored_GW( G1, C1, G2, C2, part_G1, p1, p2, T, pos1=None, pos2=None, shiftx=4, switchx=False, node_size=70, seed_G1=0, seed_G2=0, ): starting_color = 0 # get graphs partition and their coloring part1 = part_G1.copy() unique_colors = ["C%s" % (starting_color + i) for i in np.unique(part1)] nodes_color_part1 = [] for cluster in part1: nodes_color_part1.append(unique_colors[cluster]) nodes_color_part2 = [] # T: getting colors assignment from argmin of columns for i in range(len(G2.nodes())): j = np.argmax(T[:, i]) nodes_color_part2.append(nodes_color_part1[j]) pos1 = draw_graph( G1, C1, nodes_color_part1, Gweights=p1, pos=pos1, node_size=node_size, shiftx=0, seed=seed_G1, ) pos2 = draw_graph( G2, C2, nodes_color_part2, Gweights=p2, pos=pos2, node_size=node_size, shiftx=shiftx, seed=seed_G2, ) for k1, v1 in pos1.items(): max_Tk1 = np.max(T[k1, :]) for k2, v2 in pos2.items(): if T[k1, k2] > 0: pl.plot( [pos1[k1][0], pos2[k2][0]], [pos1[k1][1], pos2[k2][1]], "-", lw=0.7, alpha=min(T[k1, k2] / max_Tk1 + 0.1, 1.0), color=nodes_color_part1[k1], ) return pos1, pos2 node_size = 40 fontsize = 13 seed_G2 = 0 seed_G3 = 4 pl.figure(2, figsize=(15, 3.5)) pl.clf() pl.subplot(141) pl.axis("off") pl.title( "(CG) FGW=%s\n \n OT sparsity = %s \n marg. error = %s \n runtime = %s" % ( np.round(log_cg["fgw_dist"], 3), str(np.round(T_cg_sparsity, 2)) + " %", np.round(err_cg, 4), str(np.round(time_cg, 2)) + " ms", ), fontsize=fontsize, ) pos1, pos2 = draw_transp_colored_GW( weightedG2, C2, weightedG3, C3, part_G2, p1=T_cg.sum(1), p2=T_cg.sum(0), T=T_cg, shiftx=1.5, node_size=node_size, seed_G1=seed_G2, seed_G2=seed_G3, ) pl.subplot(142) pl.axis("off") pl.title( "(PPA) FGW=%s\n \n OT sparsity = %s \n marg. error = %s \n runtime = %s" % ( np.round(log_ppa["fgw_dist"], 3), str(np.round(T_ppa_sparsity, 2)) + " %", np.round(err_ppa, 4), str(np.round(time_ppa, 2)) + " ms", ), fontsize=fontsize, ) pos1, pos2 = draw_transp_colored_GW( weightedG2, C2, weightedG3, C3, part_G2, p1=T_ppa.sum(1), p2=T_ppa.sum(0), T=T_ppa, pos1=pos1, pos2=pos2, shiftx=0.0, node_size=node_size, seed_G1=0, seed_G2=0, ) pl.subplot(143) pl.axis("off") pl.title( "(PGD) Entropic FGW=%s\n \n OT sparsity = %s \n marg. error = %s \n runtime = %s" % ( np.round(log_pgd["fgw_dist"], 3), str(np.round(T_pgd_sparsity, 2)) + " %", np.round(err_pgd, 4), str(np.round(time_pgd, 2)) + " ms", ), fontsize=fontsize, ) pos1, pos2 = draw_transp_colored_GW( weightedG2, C2, weightedG3, C3, part_G2, p1=T_pgd.sum(1), p2=T_pgd.sum(0), T=T_pgd, pos1=pos1, pos2=pos2, shiftx=0.0, node_size=node_size, seed_G1=0, seed_G2=0, ) pl.subplot(144) pl.axis("off") pl.title( "(BAPG) FGW=%s\n \n OT sparsity = %s \n marg. error = %s \n runtime = %s" % ( np.round(log_bapg["fgw_dist"], 3), str(np.round(T_bapg_sparsity, 2)) + " %", np.round(err_bapg, 4), str(np.round(time_bapg, 2)) + " ms", ), fontsize=fontsize, ) pos1, pos2 = draw_transp_colored_GW( weightedG2, C2, weightedG3, C3, part_G2, p1=T_bapg.sum(1), p2=T_bapg.sum(0), T=T_bapg, pos1=pos1, pos2=pos2, shiftx=0.0, node_size=node_size, seed_G1=0, seed_G2=0, ) pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/gromov/plot_gnn_TFGW.py000066400000000000000000000171101525521106200227360ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ============================== Graph classification with Template Based Fused Gromov Wasserstein ============================== .. note:: Example added in release: 0.9.1. This example first illustrates how to train a graph classification gnn based on the Template Fused Gromov Wasserstein layer as proposed in [52] . [53] C. Vincent-Cuaz, R. Flamary, M. Corneli, T. Vayer, N. Courty (2022).Template based graph neural network with optimal transport distances. Advances in Neural Information Processing Systems, 35. """ # Author: Sonia Mazelet # Rémi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 1 # %% import matplotlib.pyplot as pl import torch import networkx as nx from torch.utils.data import random_split from torch_geometric.loader import DataLoader from torch_geometric.utils import to_networkx, one_hot from torch_geometric.utils import stochastic_blockmodel_graph as sbm from torch_geometric.data import Data as GraphData import torch.nn as nn from torch_geometric.nn import Linear, GCNConv from ot.gnn import TFGWPooling from sklearn.manifold import TSNE ############################################################################## # Generate data # ------------- # parameters # We create 2 classes of stochastic block models (SBM) graphs with 1 block and 2 blocks respectively. torch.manual_seed(0) n_graphs = 50 n_nodes = 10 n_node_classes = 2 # edge probabilities for the SBMs P1 = [[0.8]] P2 = [[0.9, 0.1], [0.1, 0.9]] # block sizes block_sizes1 = [n_nodes] block_sizes2 = [n_nodes // 2, n_nodes // 2] # node features x1 = torch.tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) x1 = one_hot(x1, num_classes=n_node_classes) x1 = torch.reshape(x1, (n_nodes, n_node_classes)) x2 = torch.tensor([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) x2 = one_hot(x2, num_classes=n_node_classes) x2 = torch.reshape(x2, (n_nodes, n_node_classes)) graphs1 = [ GraphData(x=x1, edge_index=sbm(block_sizes1, P1), y=torch.tensor([0])) for i in range(n_graphs) ] graphs2 = [ GraphData(x=x2, edge_index=sbm(block_sizes2, P2), y=torch.tensor([1])) for i in range(n_graphs) ] graphs = graphs1 + graphs2 # split the data into train and test sets train_graphs, test_graphs = random_split(graphs, [n_graphs, n_graphs]) train_loader = DataLoader(train_graphs, batch_size=10, shuffle=True) test_loader = DataLoader(test_graphs, batch_size=10, shuffle=False) # %% ############################################################################## # Plot data # --------- # plot one graph of each class fontsize = 10 pl.figure(0, figsize=(8, 2.5)) pl.clf() pl.subplot(121) pl.axis("off") pl.title("Graph of class 1", fontsize=fontsize) G = to_networkx(graphs1[0], to_undirected=True) pos = nx.spring_layout(G, seed=0) nx.draw_networkx(G, pos, with_labels=False, node_color="tab:blue") pl.subplot(122) pl.axis("off") pl.title("Graph of class 2", fontsize=fontsize) G = to_networkx(graphs2[0], to_undirected=True) pos = nx.spring_layout(G, seed=0) nx.draw_networkx( G, pos, with_labels=False, nodelist=[0, 1, 2, 3, 4], node_color="tab:blue" ) nx.draw_networkx( G, pos, with_labels=False, nodelist=[5, 6, 7, 8, 9], node_color="tab:red" ) pl.tight_layout() pl.show() # %% ############################################################################## # Pooling architecture using the TFGW layer # --------- class pooling_TFGW(nn.Module): """ Pooling architecture using the TFGW layer. """ def __init__( self, n_features, n_templates, n_template_nodes, n_classes, n_hidden_layers, feature_init_mean=0.0, feature_init_std=1.0, ): """ Pooling architecture using the TFGW layer. """ super().__init__() self.n_templates = n_templates self.n_template_nodes = n_template_nodes self.n_hidden_layers = n_hidden_layers self.n_features = n_features self.conv = GCNConv(self.n_features, self.n_hidden_layers) self.TFGW = TFGWPooling( self.n_hidden_layers, self.n_templates, self.n_template_nodes, feature_init_mean=feature_init_mean, feature_init_std=feature_init_std, ) self.linear = Linear(self.n_templates, n_classes) def forward(self, x, edge_index, batch=None): x = self.conv(x, edge_index) x = self.TFGW(x, edge_index, batch) x_latent = x # save latent embeddings for visualization x = self.linear(x) return x, x_latent ############################################################################## # Graph classification training # --------- n_epochs = 25 # store latent embeddings and classes for TSNE visualization embeddings_for_TSNE = [] classes = [] model = pooling_TFGW( n_features=2, n_templates=2, n_template_nodes=2, n_classes=2, n_hidden_layers=2, feature_init_mean=0.5, feature_init_std=0.5, ) optimizer = torch.optim.Adam(model.parameters(), lr=0.005, weight_decay=0.0005) criterion = torch.nn.CrossEntropyLoss() all_accuracy = [] all_loss = [] for epoch in range(n_epochs): losses = [] accs = [] for data in train_loader: out, latent_embedding = model(data.x, data.edge_index, data.batch) loss = criterion(out, data.y) loss.backward() optimizer.step() pred = out.argmax(dim=1) train_correct = pred == data.y train_acc = int(train_correct.sum()) / len(data) accs.append(train_acc) losses.append(loss.item()) # store last classes and embeddings for TSNE visualization if epoch == n_epochs - 1: embeddings_for_TSNE.append(latent_embedding) classes.append(data.y) print( f"Epoch: {epoch:03d}, Loss: {torch.mean(torch.tensor(losses)):.4f},Train Accuracy: {torch.mean(torch.tensor(accs)):.4f}" ) all_accuracy.append(torch.mean(torch.tensor(accs))) all_loss.append(torch.mean(torch.tensor(losses))) pl.figure(1, figsize=(8, 2.5)) pl.clf() pl.subplot(121) pl.plot(all_loss) pl.xlabel("epochs") pl.title("Loss") pl.subplot(122) pl.plot(all_accuracy) pl.xlabel("epochs") pl.title("Accuracy") pl.tight_layout() pl.show() # Test test_accs = [] for data in test_loader: out, latent_embedding = model(data.x, data.edge_index, data.batch) pred = out.argmax(dim=1) test_correct = pred == data.y test_acc = int(test_correct.sum()) / len(data) test_accs.append(test_acc) embeddings_for_TSNE.append(latent_embedding) classes.append(data.y) classes = torch.hstack(classes) print(f"Test Accuracy: {torch.mean(torch.tensor(test_acc)):.4f}") # %% ############################################################################## # TSNE visualization of graph classification # --------- indices = torch.randint( 2 * n_graphs, (60,) ) # select a subset of embeddings for TSNE visualization latent_embeddings = torch.vstack(embeddings_for_TSNE).detach().numpy()[indices, :] TSNE_embeddings = TSNE(n_components=2, perplexity=20, random_state=1).fit_transform( latent_embeddings ) class_0 = classes[indices] == 0 class_1 = classes[indices] == 1 TSNE_embeddings_0 = TSNE_embeddings[class_0, :] TSNE_embeddings_1 = TSNE_embeddings[class_1, :] pl.figure(2, figsize=(6, 2.5)) pl.scatter( TSNE_embeddings_0[:, 0], TSNE_embeddings_0[:, 1], alpha=0.5, marker="o", label="class 1", ) pl.scatter( TSNE_embeddings_1[:, 0], TSNE_embeddings_1[:, 1], alpha=0.5, marker="o", label="class 2", ) pl.legend() pl.title("TSNE in the latent space after training") pl.show() # %% python-pot-0.9.7+dfsg/examples/gromov/plot_gromov.py000066400000000000000000000153321525521106200226420ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ========================== Gromov-Wasserstein example ========================== .. note:: Example added in release: 0.8.0. This example is designed to show how to use the Gromov-Wasserstein distance computation in POT. We first compare 3 solvers to estimate the distance based on Conditional Gradient [24] or Sinkhorn projections [12, 51]. Then we compare 2 stochastic solvers to estimate the distance with a lower numerical cost [33]. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon (2016), "Gromov-Wasserstein averaging of kernel and distance matrices". International Conference on Machine Learning (ICML). [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs" International Conference on Machine Learning (ICML). 2019. [33] Kerdoncuff T., Emonet R., Marc S. "Sampled Gromov Wasserstein", Machine Learning Journal (MJL), 2021. [51] Xu, H., Luo, D., Zha, H., & Carin, L. (2019). "Gromov-wasserstein learning for graph matching and node embedding". In International Conference on Machine Learning (ICML), 2019. """ # Author: Erwan Vautier # Nicolas Courty # Cédric Vincent-Cuaz # Tanguy Kerdoncuff # # License: MIT License # sphinx_gallery_thumbnail_number = 1 import scipy as sp import numpy as np import matplotlib.pylab as pl from mpl_toolkits.mplot3d import Axes3D # noqa import ot ############################################################################# # # Sample two Gaussian distributions (2D and 3D) # --------------------------------------------- # # The Gromov-Wasserstein distance allows to compute distances with samples that # do not belong to the same metric space. For demonstration purpose, we sample # two Gaussian distributions in 2- and 3-dimensional spaces. n_samples = 30 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([4, 4, 4]) cov_t = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) np.random.seed(0) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s) P = sp.linalg.sqrtm(cov_t) xt = np.random.randn(n_samples, 3).dot(P) + mu_t ############################################################################# # # Plotting the distributions # -------------------------- fig = pl.figure(1) ax1 = fig.add_subplot(121) ax1.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") ax2 = fig.add_subplot(122, projection="3d") ax2.scatter(xt[:, 0], xt[:, 1], xt[:, 2], color="r") pl.show() ############################################################################# # # Compute distance kernels, normalize them and then display # --------------------------------------------------------- C1 = sp.spatial.distance.cdist(xs, xs) C2 = sp.spatial.distance.cdist(xt, xt) C1 /= C1.max() C2 /= C2.max() pl.figure(2) pl.subplot(121) pl.imshow(C1) pl.title("C1") pl.subplot(122) pl.imshow(C2) pl.title("C2") pl.show() ############################################################################# # # Compute Gromov-Wasserstein plans and distance # --------------------------------------------- p = ot.unif(n_samples) q = ot.unif(n_samples) # Conditional Gradient algorithm gw0, log0 = ot.gromov.gromov_wasserstein( C1, C2, p, q, "square_loss", verbose=True, log=True ) # Proximal Point algorithm with Kullback-Leibler as proximal operator gw, log = ot.gromov.entropic_gromov_wasserstein( C1, C2, p, q, "square_loss", epsilon=5e-4, solver="PPA", log=True, verbose=True ) # Projected Gradient algorithm with entropic regularization gwe, loge = ot.gromov.entropic_gromov_wasserstein( C1, C2, p, q, "square_loss", epsilon=5e-4, solver="PGD", log=True, verbose=True ) print( "Gromov-Wasserstein distance estimated with Conditional Gradient solver: " + str(log0["gw_dist"]) ) print( "Gromov-Wasserstein distance estimated with Proximal Point solver: " + str(log["gw_dist"]) ) print( "Entropic Gromov-Wasserstein distance estimated with Projected Gradient solver: " + str(loge["gw_dist"]) ) # compute OT sparsity level gw0_sparsity = 100 * (gw0 == 0.0).astype(np.float64).sum() / (n_samples**2) gw_sparsity = 100 * (gw == 0.0).astype(np.float64).sum() / (n_samples**2) gwe_sparsity = 100 * (gwe == 0.0).astype(np.float64).sum() / (n_samples**2) # Methods using Sinkhorn projections tend to produce feasibility errors on the # marginal constraints err0 = np.linalg.norm(gw0.sum(1) - p) + np.linalg.norm(gw0.sum(0) - q) err = np.linalg.norm(gw.sum(1) - p) + np.linalg.norm(gw.sum(0) - q) erre = np.linalg.norm(gwe.sum(1) - p) + np.linalg.norm(gwe.sum(0) - q) pl.figure(3, (10, 6)) cmap = "Blues" fontsize = 12 pl.subplot(131) pl.imshow(gw0, cmap=cmap) pl.title( "(CG algo) GW=%s \n \n OT sparsity=%s \n feasibility error=%s" % ( np.round(log0["gw_dist"], 4), str(np.round(gw0_sparsity, 2)) + " %", np.round(np.round(err0, 4)), ), fontsize=fontsize, ) pl.subplot(132) pl.imshow(gw, cmap=cmap) pl.title( "(PP algo) GW=%s \n \n OT sparsity=%s \nfeasibility error=%s" % ( np.round(log["gw_dist"], 4), str(np.round(gw_sparsity, 2)) + " %", np.round(err, 4), ), fontsize=fontsize, ) pl.subplot(133) pl.imshow(gwe, cmap=cmap) pl.title( "Entropic GW=%s \n \n OT sparsity=%s \nfeasibility error=%s" % ( np.round(loge["gw_dist"], 4), str(np.round(gwe_sparsity, 2)) + " %", np.round(erre, 4), ), fontsize=fontsize, ) pl.tight_layout() pl.show() ############################################################################# # # Compute GW with scalable stochastic methods with any loss function # ---------------------------------------------------------------------- def loss(x, y): return np.abs(x - y) pgw, plog = ot.gromov.pointwise_gromov_wasserstein( C1, C2, p, q, loss, max_iter=100, log=True ) sgw, slog = ot.gromov.sampled_gromov_wasserstein( C1, C2, p, q, loss, epsilon=0.1, max_iter=100, log=True ) print( "Pointwise Gromov-Wasserstein distance estimated: " + str(plog["gw_dist_estimated"]) ) print("Variance estimated: " + str(plog["gw_dist_std"])) print("Sampled Gromov-Wasserstein distance: " + str(slog["gw_dist_estimated"])) print("Variance estimated: " + str(slog["gw_dist_std"])) pl.figure(4, (10, 5)) pl.subplot(121) pl.imshow(pgw.toarray(), cmap=cmap) pl.title("Pointwise Gromov Wasserstein") pl.subplot(122) pl.imshow(sgw, cmap=cmap) pl.title("Sampled Gromov Wasserstein") pl.show() python-pot-0.9.7+dfsg/examples/gromov/plot_gromov_barycenter.py000077500000000000000000000161261525521106200250650ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ===================================== Gromov-Wasserstein Barycenter example ===================================== This example is designed to show how to use the Gromov-Wasserstein distance computation in POT. """ # Author: Erwan Vautier # Nicolas Courty # # License: MIT License import os from pathlib import Path import numpy as np import scipy as sp from matplotlib import pyplot as plt from sklearn import manifold from sklearn.decomposition import PCA import ot ############################################################################## # Smacof MDS # ---------- # # This function allows to find an embedding of points given a dissimilarity matrix # that will be given by the output of the algorithm def smacof_mds(C, dim, max_iter=3000, eps=1e-9): """ Returns an interpolated point cloud following the dissimilarity matrix C using SMACOF multidimensional scaling (MDS) in specific dimensioned target space Parameters ---------- C : ndarray, shape (ns, ns) dissimilarity matrix dim : int dimension of the targeted space max_iter : int Maximum number of iterations of the SMACOF algorithm for a single run eps : float relative tolerance w.r.t stress to declare converge Returns ------- npos : ndarray, shape (R, dim) Embedded coordinates of the interpolated point cloud (defined with one isometry) """ rng = np.random.RandomState(seed=3) mds = manifold.MDS( dim, max_iter=max_iter, eps=1e-9, dissimilarity="precomputed", n_init=1 ) pos = mds.fit(C).embedding_ nmds = manifold.MDS( 2, max_iter=max_iter, eps=1e-9, dissimilarity="precomputed", random_state=rng, n_init=1, ) npos = nmds.fit_transform(C, init=pos) return npos ############################################################################## # Data preparation # ---------------- # # The four distributions are constructed from 4 simple images def im2mat(img): """Converts and image to matrix (one pixel per line)""" return img.reshape((img.shape[0] * img.shape[1], img.shape[2])) this_file = os.path.realpath("__file__") data_path = os.path.join(Path(this_file).parent.parent.parent, "data") square = plt.imread(os.path.join(data_path, "square.png")).astype(np.float64)[:, :, 2] cross = plt.imread(os.path.join(data_path, "cross.png")).astype(np.float64)[:, :, 2] triangle = plt.imread(os.path.join(data_path, "triangle.png")).astype(np.float64)[ :, :, 2 ] star = plt.imread(os.path.join(data_path, "star.png")).astype(np.float64)[:, :, 2] shapes = [square, cross, triangle, star] S = 4 xs = [[] for i in range(S)] for nb in range(4): for i in range(8): for j in range(8): if shapes[nb][i, j] < 0.95: xs[nb].append([j, 8 - i]) xs = [np.array(xs[s]) for s in range(S)] ############################################################################## # Barycenter computation # ---------------------- ns = [len(xs[s]) for s in range(S)] n_samples = 30 """Compute all distances matrices for the four shapes""" Cs = [sp.spatial.distance.cdist(xs[s], xs[s]) for s in range(S)] Cs = [cs / cs.max() for cs in Cs] ps = [ot.unif(ns[s]) for s in range(S)] p = ot.unif(n_samples) lambdast = [[float(i) / 3, float(3 - i) / 3] for i in [1, 2]] Ct01 = [0 for i in range(2)] for i in range(2): Ct01[i] = ot.gromov.gromov_barycenters( n_samples, [Cs[0], Cs[1]], [ps[0], ps[1]], p, lambdast[i], "square_loss", # 5e-4, max_iter=100, tol=1e-3, ) Ct02 = [0 for i in range(2)] for i in range(2): Ct02[i] = ot.gromov.gromov_barycenters( n_samples, [Cs[0], Cs[2]], [ps[0], ps[2]], p, lambdast[i], "square_loss", # 5e-4, max_iter=100, tol=1e-3, ) Ct13 = [0 for i in range(2)] for i in range(2): Ct13[i] = ot.gromov.gromov_barycenters( n_samples, [Cs[1], Cs[3]], [ps[1], ps[3]], p, lambdast[i], "square_loss", # 5e-4, max_iter=100, tol=1e-3, ) Ct23 = [0 for i in range(2)] for i in range(2): Ct23[i] = ot.gromov.gromov_barycenters( n_samples, [Cs[2], Cs[3]], [ps[2], ps[3]], p, lambdast[i], "square_loss", # 5e-4, max_iter=100, tol=1e-3, ) ############################################################################## # Visualization # ------------- # # The PCA helps in getting consistency between the rotations clf = PCA(n_components=2) npos = [0, 0, 0, 0] npos = [smacof_mds(Cs[s], 2) for s in range(S)] npost01 = [0, 0] npost01 = [smacof_mds(Ct01[s], 2) for s in range(2)] npost01 = [clf.fit_transform(npost01[s]) for s in range(2)] npost02 = [0, 0] npost02 = [smacof_mds(Ct02[s], 2) for s in range(2)] npost02 = [clf.fit_transform(npost02[s]) for s in range(2)] npost13 = [0, 0] npost13 = [smacof_mds(Ct13[s], 2) for s in range(2)] npost13 = [clf.fit_transform(npost13[s]) for s in range(2)] npost23 = [0, 0] npost23 = [smacof_mds(Ct23[s], 2) for s in range(2)] npost23 = [clf.fit_transform(npost23[s]) for s in range(2)] fig = plt.figure(figsize=(10, 10)) ax1 = plt.subplot2grid((4, 4), (0, 0)) plt.xlim((-1, 1)) plt.ylim((-1, 1)) ax1.scatter(npos[0][:, 0], npos[0][:, 1], color="r") ax2 = plt.subplot2grid((4, 4), (0, 1)) plt.xlim((-1, 1)) plt.ylim((-1, 1)) ax2.scatter(npost01[1][:, 0], npost01[1][:, 1], color="b") ax3 = plt.subplot2grid((4, 4), (0, 2)) plt.xlim((-1, 1)) plt.ylim((-1, 1)) ax3.scatter(npost01[0][:, 0], npost01[0][:, 1], color="b") ax4 = plt.subplot2grid((4, 4), (0, 3)) plt.xlim((-1, 1)) plt.ylim((-1, 1)) ax4.scatter(npos[1][:, 0], npos[1][:, 1], color="r") ax5 = plt.subplot2grid((4, 4), (1, 0)) plt.xlim((-1, 1)) plt.ylim((-1, 1)) ax5.scatter(npost02[1][:, 0], npost02[1][:, 1], color="b") ax6 = plt.subplot2grid((4, 4), (1, 3)) plt.xlim((-1, 1)) plt.ylim((-1, 1)) ax6.scatter(npost13[1][:, 0], npost13[1][:, 1], color="b") ax7 = plt.subplot2grid((4, 4), (2, 0)) plt.xlim((-1, 1)) plt.ylim((-1, 1)) ax7.scatter(npost02[0][:, 0], npost02[0][:, 1], color="b") ax8 = plt.subplot2grid((4, 4), (2, 3)) plt.xlim((-1, 1)) plt.ylim((-1, 1)) ax8.scatter(npost13[0][:, 0], npost13[0][:, 1], color="b") ax9 = plt.subplot2grid((4, 4), (3, 0)) plt.xlim((-1, 1)) plt.ylim((-1, 1)) ax9.scatter(npos[2][:, 0], npos[2][:, 1], color="r") ax10 = plt.subplot2grid((4, 4), (3, 1)) plt.xlim((-1, 1)) plt.ylim((-1, 1)) ax10.scatter(npost23[1][:, 0], npost23[1][:, 1], color="b") ax11 = plt.subplot2grid((4, 4), (3, 2)) plt.xlim((-1, 1)) plt.ylim((-1, 1)) ax11.scatter(npost23[0][:, 0], npost23[0][:, 1], color="b") ax12 = plt.subplot2grid((4, 4), (3, 3)) plt.xlim((-1, 1)) plt.ylim((-1, 1)) ax12.scatter(npos[3][:, 0], npos[3][:, 1], color="r") python-pot-0.9.7+dfsg/examples/gromov/plot_gromov_wasserstein_dictionary_learning.py000077500000000000000000000344061525521106200314030ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ===================================================== (Fused) Gromov-Wasserstein Linear Dictionary Learning ===================================================== .. note:: Example added in release: 0.8.2. In this example, we illustrate how to learn a Gromov-Wasserstein dictionary on a dataset of structured data such as graphs, denoted :math:`\{ \mathbf{C_s} \}_{s \in [S]}` where every nodes have uniform weights. Given a dictionary :math:`\mathbf{C_{dict}}` composed of D structures of a fixed size nt, each graph :math:`(\mathbf{C_s}, \mathbf{p_s})` is modeled as a convex combination :math:`\mathbf{w_s} \in \Sigma_D` of these dictionary atoms as :math:`\sum_d w_{s,d} \mathbf{C_{dict}[d]}`. First, we consider a dataset composed of graphs generated by Stochastic Block models with variable sizes taken in :math:`\{30, ... , 50\}` and quantities of clusters varying in :math:`\{ 1, 2, 3\}`. We learn a dictionary of 3 atoms, by minimizing the Gromov-Wasserstein distance from all samples to its model in the dictionary with respect to the dictionary atoms. Second, we illustrate the extension of this dictionary learning framework to structured data endowed with node features by using the Fused Gromov-Wasserstein distance. Starting from the aforementioned dataset of unattributed graphs, we add discrete labels uniformly depending on the number of clusters. Then we learn and visualize attributed graph atoms where each sample is modeled as a joint convex combination between atom structures and features. [38] C. Vincent-Cuaz, T. Vayer, R. Flamary, M. Corneli, N. Courty, Online Graph Dictionary Learning, International Conference on Machine Learning (ICML), 2021. """ # Author: Cédric Vincent-Cuaz # # License: MIT License # sphinx_gallery_thumbnail_number = 4 import numpy as np import matplotlib.pylab as pl from sklearn.manifold import MDS from ot.gromov import ( gromov_wasserstein_linear_unmixing, gromov_wasserstein_dictionary_learning, fused_gromov_wasserstein_linear_unmixing, fused_gromov_wasserstein_dictionary_learning, ) import ot import networkx from networkx.generators.community import stochastic_block_model as sbm ############################################################################# # # Generate a dataset composed of graphs following Stochastic Block models of 1, 2 and 3 clusters. # ----------------------------------------------------------------------------------------------- np.random.seed(42) N = 60 # number of graphs in the dataset # For every number of clusters, we generate SBM with fixed inter/intra-clusters probability. clusters = [1, 2, 3] Nc = N // len(clusters) # number of graphs by cluster nlabels = len(clusters) dataset = [] labels = [] p_inter = 0.1 p_intra = 0.9 for n_cluster in clusters: for i in range(Nc): n_nodes = int(np.random.uniform(low=30, high=50)) if n_cluster > 1: P = p_inter * np.ones((n_cluster, n_cluster)) np.fill_diagonal(P, p_intra) else: P = p_intra * np.eye(1) sizes = np.round(n_nodes * np.ones(n_cluster) / n_cluster).astype(np.int32) G = sbm(sizes, P, seed=i, directed=False) C = networkx.to_numpy_array(G) dataset.append(C) labels.append(n_cluster) # Visualize samples def plot_graph(x, C, binary=True, color="C0", s=None): for j in range(C.shape[0]): for i in range(j): if binary: if C[i, j] > 0: pl.plot( [x[i, 0], x[j, 0]], [x[i, 1], x[j, 1]], alpha=0.2, color="k" ) else: # connection intensity proportional to C[i,j] pl.plot( [x[i, 0], x[j, 0]], [x[i, 1], x[j, 1]], alpha=C[i, j], color="k" ) pl.scatter( x[:, 0], x[:, 1], c=color, s=s, zorder=10, edgecolors="k", cmap="tab10", vmax=9 ) pl.figure(1, (12, 8)) pl.clf() for idx_c, c in enumerate(clusters): C = dataset[(c - 1) * Nc] # sample with c clusters # get 2d position for nodes x = MDS(dissimilarity="precomputed", random_state=0).fit_transform(1 - C) pl.subplot(2, nlabels, c) pl.title("(graph) sample from label " + str(c), fontsize=14) plot_graph(x, C, binary=True, color="C0", s=50.0) pl.axis("off") pl.subplot(2, nlabels, nlabels + c) pl.title("(matrix) sample from label %s \n" % c, fontsize=14) pl.imshow(C, interpolation="nearest") pl.axis("off") pl.tight_layout() pl.show() ############################################################################# # # Estimate the Gromov-Wasserstein dictionary from the dataset # ----------------------------------------------------------- np.random.seed(0) ps = [ot.unif(C.shape[0]) for C in dataset] D = 3 # 3 atoms in the dictionary nt = 6 # of 6 nodes each q = ot.unif(nt) reg = 0.0 # regularization coefficient to promote sparsity of unmixings {w_s} Cdict_GW, log = gromov_wasserstein_dictionary_learning( Cs=dataset, D=D, nt=nt, ps=ps, q=q, epochs=10, batch_size=16, learning_rate=0.1, reg=reg, projection="nonnegative_symmetric", tol_outer=10 ** (-5), tol_inner=10 ** (-5), max_iter_outer=30, max_iter_inner=300, use_log=True, use_adam_optimizer=True, verbose=True, ) # visualize loss evolution over epochs pl.figure(2, (4, 3)) pl.clf() pl.title("loss evolution by epoch", fontsize=14) pl.plot(log["loss_epochs"]) pl.xlabel("epochs", fontsize=12) pl.ylabel("loss", fontsize=12) pl.tight_layout() pl.show() ############################################################################# # # Visualization of the estimated dictionary atoms # ----------------------------------------------- # Continuous connections between nodes of the atoms are colored in shades of grey (1: dark / 2: white) pl.figure(3, (12, 8)) pl.clf() for idx_atom, atom in enumerate(Cdict_GW): scaled_atom = (atom - atom.min()) / (atom.max() - atom.min()) x = MDS(dissimilarity="precomputed", random_state=0).fit_transform(1 - scaled_atom) pl.subplot(2, D, idx_atom + 1) pl.title("(graph) atom " + str(idx_atom + 1), fontsize=14) plot_graph(x, atom / atom.max(), binary=False, color="C0", s=100.0) pl.axis("off") pl.subplot(2, D, D + idx_atom + 1) pl.title("(matrix) atom %s \n" % (idx_atom + 1), fontsize=14) pl.imshow(scaled_atom, interpolation="nearest") pl.colorbar() pl.axis("off") pl.tight_layout() pl.show() ############################################################################# # # Visualization of the embedding space # ------------------------------------ unmixings = [] reconstruction_errors = [] for C in dataset: p = ot.unif(C.shape[0]) unmixing, Cembedded, OT, reconstruction_error = gromov_wasserstein_linear_unmixing( C, Cdict_GW, p=p, q=q, reg=reg, tol_outer=10 ** (-5), tol_inner=10 ** (-5), max_iter_outer=30, max_iter_inner=300, ) unmixings.append(unmixing) reconstruction_errors.append(reconstruction_error) unmixings = np.array(unmixings) print("cumulated reconstruction error:", np.array(reconstruction_errors).sum()) # Compute the 2D representation of the unmixing living in the 2-simplex of probability unmixings2D = np.zeros(shape=(N, 2)) for i, w in enumerate(unmixings): unmixings2D[i, 0] = (2.0 * w[1] + w[2]) / 2.0 unmixings2D[i, 1] = (np.sqrt(3.0) * w[2]) / 2.0 x = [0.0, 0.0] y = [1.0, 0.0] z = [0.5, np.sqrt(3) / 2.0] extremities = np.stack([x, y, z]) pl.figure(4, (4, 4)) pl.clf() pl.title("Embedding space", fontsize=14) for cluster in range(nlabels): start, end = Nc * cluster, Nc * (cluster + 1) if cluster == 0: pl.scatter( unmixings2D[start:end, 0], unmixings2D[start:end, 1], c="C" + str(cluster), marker="o", s=40.0, label="1 cluster", ) else: pl.scatter( unmixings2D[start:end, 0], unmixings2D[start:end, 1], c="C" + str(cluster), marker="o", s=40.0, label="%s clusters" % (cluster + 1), ) pl.scatter( extremities[:, 0], extremities[:, 1], c="black", marker="x", s=80.0, label="atoms" ) pl.plot([x[0], y[0]], [x[1], y[1]], color="black", linewidth=2.0) pl.plot([x[0], z[0]], [x[1], z[1]], color="black", linewidth=2.0) pl.plot([y[0], z[0]], [y[1], z[1]], color="black", linewidth=2.0) pl.axis("off") pl.legend(fontsize=11) pl.tight_layout() pl.show() ############################################################################# # # Endow the dataset with node features # ------------------------------------ # We follow this feature assignment on all nodes of a graph depending on its label/number of clusters # 1 cluster --> 0 as nodes feature # 2 clusters --> 1 as nodes feature # 3 clusters --> 2 as nodes feature # features are one-hot encoded following these assignments dataset_features = [] for i in range(len(dataset)): n = dataset[i].shape[0] F = np.zeros((n, 3)) if i < Nc: # graph with 1 cluster F[:, 0] = 1.0 elif i < 2 * Nc: # graph with 2 clusters F[:, 1] = 1.0 else: # graph with 3 clusters F[:, 2] = 1.0 dataset_features.append(F) pl.figure(5, (12, 8)) pl.clf() for idx_c, c in enumerate(clusters): C = dataset[(c - 1) * Nc] # sample with c clusters F = dataset_features[(c - 1) * Nc] colors = ["C" + str(np.argmax(F[i])) for i in range(F.shape[0])] # get 2d position for nodes x = MDS(dissimilarity="precomputed", random_state=0).fit_transform(1 - C) pl.subplot(2, nlabels, c) pl.title("(graph) sample from label " + str(c), fontsize=14) plot_graph(x, C, binary=True, color=colors, s=50) pl.axis("off") pl.subplot(2, nlabels, nlabels + c) pl.title("(matrix) sample from label %s \n" % c, fontsize=14) pl.imshow(C, interpolation="nearest") pl.axis("off") pl.tight_layout() pl.show() ############################################################################# # # Estimate a Fused Gromov-Wasserstein dictionary from the dataset of attributed graphs # ------------------------------------------------------------------------------------ np.random.seed(0) ps = [ot.unif(C.shape[0]) for C in dataset] D = 3 # 6 atoms instead of 3 nt = 6 q = ot.unif(nt) reg = 0.001 alpha = 0.5 # trade-off parameter between structure and feature information of Fused Gromov-Wasserstein Cdict_FGW, Ydict_FGW, log = fused_gromov_wasserstein_dictionary_learning( Cs=dataset, Ys=dataset_features, D=D, nt=nt, ps=ps, q=q, alpha=alpha, epochs=10, batch_size=16, learning_rate_C=0.1, learning_rate_Y=0.1, reg=reg, tol_outer=10 ** (-5), tol_inner=10 ** (-5), max_iter_outer=30, max_iter_inner=300, projection="nonnegative_symmetric", use_log=True, use_adam_optimizer=True, verbose=True, ) # visualize loss evolution pl.figure(6, (4, 3)) pl.clf() pl.title("loss evolution by epoch", fontsize=14) pl.plot(log["loss_epochs"]) pl.xlabel("epochs", fontsize=12) pl.ylabel("loss", fontsize=12) pl.tight_layout() pl.show() ############################################################################# # # Visualization of the estimated dictionary atoms # ----------------------------------------------- pl.figure(7, (12, 8)) pl.clf() max_features = Ydict_FGW.max() min_features = Ydict_FGW.min() for idx_atom, (Catom, Fatom) in enumerate(zip(Cdict_FGW, Ydict_FGW)): scaled_atom = (Catom - Catom.min()) / (Catom.max() - Catom.min()) # scaled_F = 2 * (Fatom - min_features) / (max_features - min_features) colors = ["C%s" % np.argmax(Fatom[i]) for i in range(Fatom.shape[0])] x = MDS(dissimilarity="precomputed", random_state=0).fit_transform(1 - scaled_atom) pl.subplot(2, D, idx_atom + 1) pl.title("(attributed graph) atom " + str(idx_atom + 1), fontsize=14) plot_graph(x, Catom / Catom.max(), binary=False, color=colors, s=100) pl.axis("off") pl.subplot(2, D, D + idx_atom + 1) pl.title("(matrix) atom %s \n" % (idx_atom + 1), fontsize=14) pl.imshow(scaled_atom, interpolation="nearest") pl.colorbar() pl.axis("off") pl.tight_layout() pl.show() ############################################################################# # # Visualization of the embedding space # ------------------------------------ unmixings = [] reconstruction_errors = [] for i in range(len(dataset)): C = dataset[i] Y = dataset_features[i] p = ot.unif(C.shape[0]) unmixing, Cembedded, Yembedded, OT, reconstruction_error = ( fused_gromov_wasserstein_linear_unmixing( C, Y, Cdict_FGW, Ydict_FGW, p=p, q=q, alpha=alpha, reg=reg, tol_outer=10 ** (-6), tol_inner=10 ** (-6), max_iter_outer=30, max_iter_inner=300, ) ) unmixings.append(unmixing) reconstruction_errors.append(reconstruction_error) unmixings = np.array(unmixings) print("cumulated reconstruction error:", np.array(reconstruction_errors).sum()) # Visualize unmixings in the 2-simplex of probability unmixings2D = np.zeros(shape=(N, 2)) for i, w in enumerate(unmixings): unmixings2D[i, 0] = (2.0 * w[1] + w[2]) / 2.0 unmixings2D[i, 1] = (np.sqrt(3.0) * w[2]) / 2.0 x = [0.0, 0.0] y = [1.0, 0.0] z = [0.5, np.sqrt(3) / 2.0] extremities = np.stack([x, y, z]) pl.figure(8, (4, 4)) pl.clf() pl.title("Embedding space", fontsize=14) for cluster in range(nlabels): start, end = Nc * cluster, Nc * (cluster + 1) if cluster == 0: pl.scatter( unmixings2D[start:end, 0], unmixings2D[start:end, 1], c="C" + str(cluster), marker="o", s=40.0, label="1 cluster", ) else: pl.scatter( unmixings2D[start:end, 0], unmixings2D[start:end, 1], c="C" + str(cluster), marker="o", s=40.0, label="%s clusters" % (cluster + 1), ) pl.scatter( extremities[:, 0], extremities[:, 1], c="black", marker="x", s=80.0, label="atoms" ) pl.plot([x[0], y[0]], [x[1], y[1]], color="black", linewidth=2.0) pl.plot([x[0], z[0]], [x[1], z[1]], color="black", linewidth=2.0) pl.plot([y[0], z[0]], [y[1], z[1]], color="black", linewidth=2.0) pl.axis("off") pl.legend(fontsize=11) pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/gromov/plot_partial_fgw.py000066400000000000000000000255511525521106200236340ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ================================= Plot partial FGW for subgraph matching ================================= .. note:: Example added in release: 0.9.6. This example illustrates the computation of partial (Fused) Gromov-Wasserstein divergences for subgraph matching tasks, using the exact formulation $p(F)GW$ and the entropically regularized one $p(F)GW_e$ [18, 29]. We first create a clean circular graph of 15 nodes with node features correlated with node positions on the unit circle, and a noisy version where 5 nodes out of the circle are added. Then knowing the proportion of clean samples in the target graph $m=3/4$, we show how to identify them using : - The partial GW matching and its entropic counterpart, omitting node features. - The partial Fused GW matching and its entropic counterpart. [18] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs" International Conference on Machine Learning (ICML). 2019. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. """ # Author: Cédric Vincent-Cuaz # # License: MIT License # sphinx_gallery_thumbnail_number = 3 # %% load libraries import numpy as np import pylab as pl import networkx as nx import math from scipy.sparse.csgraph import shortest_path import matplotlib.colors as mcol from matplotlib import cm from ot.gromov import ( partial_gromov_wasserstein, entropic_partial_gromov_wasserstein, partial_fused_gromov_wasserstein, entropic_partial_fused_gromov_wasserstein, ) from ot import unif, dist ############################################################################## # Utils for generation and visualization # ------------- def build_noisy_circular_graph(n_clean=15, n_noise=5, random_seed=0): """Create a noisy circular graph""" # create clean circle np.random.seed(random_seed) g = nx.Graph() g.add_nodes_from(np.arange(n_clean + n_noise)) for i in range(n_clean): g.add_node(i, weight=math.sin(2 * i * math.pi / n_clean)) if i == (n_clean - 1): g.add_edge(i, 0) else: g.add_edge(i, i + 1) # add nodes out of the circle as structure noise if n_noise > 0: noisy_nodes = np.random.choice(np.arange(n_clean), n_noise) for i, j in enumerate(noisy_nodes): g.add_node(i + n_clean, weight=math.sin(2 * j * math.pi / n_clean)) g.add_edge(i + n_clean, j) g.add_edge(i + n_clean, (j + 1) % n_clean) return g def graph_colors(nx_graph, vmin=0, vmax=7): cnorm = mcol.Normalize(vmin=vmin, vmax=vmax) cpick = cm.ScalarMappable(norm=cnorm, cmap="viridis") cpick.set_array([]) val_map = {} for k, v in nx.get_node_attributes(nx_graph, "weight").items(): val_map[k] = cpick.to_rgba(v) colors = [] for node in nx_graph.nodes(): colors.append(val_map[node]) return colors def draw_graph( G, C, nodes_color_part, Gweights=None, pos=None, edge_color="black", node_size=None, shiftx=0, ): if pos is None: pos = nx.kamada_kawai_layout(G) if shiftx != 0: for k, v in pos.items(): v[0] = v[0] + shiftx alpha_edge = 0.7 width_edge = 1.8 if Gweights is None: nx.draw_networkx_edges( G, pos, width=width_edge, alpha=alpha_edge, edge_color=edge_color ) else: # We make more visible connections between activated nodes n = len(Gweights) edgelist_activated = [] edgelist_deactivated = [] for i in range(n): for j in range(n): if Gweights[i] * Gweights[j] * C[i, j] > 0: edgelist_activated.append((i, j)) elif C[i, j] > 0: edgelist_deactivated.append((i, j)) nx.draw_networkx_edges( G, pos, edgelist=edgelist_activated, width=width_edge, alpha=alpha_edge, edge_color=edge_color, ) nx.draw_networkx_edges( G, pos, edgelist=edgelist_deactivated, width=width_edge, alpha=0.1, edge_color=edge_color, ) if Gweights is None: for node, node_color in enumerate(nodes_color_part): nx.draw_networkx_nodes( G, pos, nodelist=[node], node_size=node_size, alpha=1, node_color=node_color, ) else: scaled_Gweights = Gweights / (0.5 * Gweights.max()) nodes_size = node_size * scaled_Gweights for node, node_color in enumerate(nodes_color_part): if nodes_size[node] == 0: local_node_size = 0 else: local_node_size = max(0.1 * node_size, nodes_size[node]) nx.draw_networkx_nodes( G, pos, nodelist=[node], node_size=local_node_size, alpha=1, node_color=node_color, ) return pos def draw_transp_colored( G1, C1, G2, C2, p1, p2, T, pos1=None, pos2=None, shiftx=4, switchx=False, node_size=70, color_features=False, ): if color_features: nodes_color_part1 = graph_colors(G1, vmin=-1, vmax=1) nodes_color_part2 = graph_colors(G2, vmin=-1, vmax=1) else: nodes_color_part1 = C1.shape[0] * ["C0"] nodes_color_part2 = C2.shape[0] * ["C0"] pos1 = draw_graph( G1, C1, nodes_color_part1, Gweights=p1, pos=pos1, node_size=node_size, shiftx=0, ) pos2 = draw_graph( G2, C2, nodes_color_part2, Gweights=p2, pos=pos2, node_size=node_size, shiftx=shiftx, ) T_max = T.max() for k1, v1 in pos1.items(): for k2, v2 in pos2.items(): if T[k1, k2] > 0: pl.plot( [pos1[k1][0], pos2[k2][0]], [pos1[k1][1], pos2[k2][1]], "-", lw=0.8, alpha=max(0.05, 0.8 * T[k1, k2] / T_max), color=nodes_color_part1[k1], ) return pos1, pos2 ############################################################################## # Generate and visualize data # ------------- # We build a clean circular graph that will be matched to a noisy circular graph. clean_graph = build_noisy_circular_graph(n_clean=15, n_noise=0) noisy_graph = build_noisy_circular_graph(n_clean=15, n_noise=5) graphs = [clean_graph, noisy_graph] list_pos = [] pl.figure(figsize=(6, 3)) for i in range(2): pl.subplot(1, 2, i + 1) g = graphs[i] if i == 0: pl.title("clean graph", fontsize=16) else: pl.title("noisy graph", fontsize=16) pos = nx.kamada_kawai_layout(g) list_pos.append(pos) nx.draw_networkx( g, pos=pos, node_color=graph_colors(g, vmin=-1, vmax=1), with_labels=False, node_size=100, ) pl.show() ############################################################################## # Partial (Entropic) Gromov-Wasserstein computation and visualization # ---------------------- # Adjacency matrices are compared using both exact and entropic partial GW # discarding for now node features. # Then for illustration, the node sizes are proportional to their optimized masses # and the intensity of the link between two nodes across graphs is set proportionally # to the corresponding transported mass. Cs = [nx.adjacency_matrix(G).toarray().astype(np.float64) for G in graphs] ps = [unif(C.shape[0]) for C in Cs] # provide an informative initialization for better visualization m = 3.0 / 4.0 partial_id = np.zeros((15, 20)) partial_id[:15, :15] = np.eye(15) / 15.0 G0 = (np.outer(ps[0], ps[1]) + partial_id) * m / 2 # compute exact partial GW T, log = partial_gromov_wasserstein( Cs[0], Cs[1], ps[0], ps[1], m=m, G0=G0, symmetric=True, log=True ) # compute entropic partial GW leading to dense transport plans Tent, logent = entropic_partial_gromov_wasserstein( Cs[0], Cs[1], ps[0], ps[1], reg=0.01, m=m, G0=G0, symmetric=True, log=True ) # Plot matchings list_T = [T, Tent] list_dist = [ np.round(log["partial_gw_dist"], 3), np.round(logent["partial_gw_dist"], 3), ] list_dist_str = ["pGW", "pGW_e"] pl.figure(2, figsize=(10, 3)) pl.clf() for i in range(2): pl.subplot(1, 2, i + 1) pl.axis("off") pl.title( r"$%s(\mathbf{C_1},\mathbf{p_1}^\star,\mathbf{C_2},\mathbf{p_2}^\star) =%s$" % (list_dist_str[i], list_dist[i]), fontsize=14, ) p2 = list_T[i].sum(0) pos1, pos2 = draw_transp_colored( clean_graph, Cs[0], noisy_graph, Cs[1], p1=None, p2=p2, T=list_T[i], shiftx=3, node_size=50, ) pl.tight_layout() pl.show() ############################################################################## # Partial (Entropic) Fused Gromov-Wasserstein computation and visualization # ---------------------- # We add now node features compared using pairwise euclidean distance # to illustrate partial FGW computation with trade-off parameter alpha=0.5 Ys = [ np.array([v for (k, v) in nx.get_node_attributes(G, "weight").items()]).reshape( -1, 1 ) for G in graphs ] M = dist(Ys[0], Ys[1]) # provide an informative initialization for better visualization m = 3.0 / 4.0 partial_id = np.zeros((15, 20)) partial_id[:15, :15] = np.eye(15) / 15.0 G0 = (np.outer(ps[0], ps[1]) + partial_id) * m / 2 # compute exact partial GW T, log = partial_fused_gromov_wasserstein( M, Cs[0], Cs[1], ps[0], ps[1], alpha=0.5, m=m, G0=G0, symmetric=True, log=True, ) # compute entropic partial GW leading to dense transport plans Tent, logent = entropic_partial_fused_gromov_wasserstein( M, Cs[0], Cs[1], ps[0], ps[1], reg=0.01, alpha=0.5, m=m, G0=G0, symmetric=True, log=True, ) # Plot matchings list_T = [T, Tent] list_dist = [ np.round(log["partial_fgw_dist"], 3), np.round(logent["partial_fgw_dist"], 3), ] list_dist_str = ["pFGW", "pFGW_e"] pl.figure(3, figsize=(10, 3)) pl.clf() for i in range(2): pl.subplot(1, 2, i + 1) pl.axis("off") pl.title( r"$%s(\mathbf{C_1},\mathbf{p_1}^\star,\mathbf{C_2}, \mathbf{p_2}^\star) =%s$" % (list_dist_str[i], list_dist[i]), fontsize=14, ) p2 = list_T[i].sum(0) pos1, pos2 = draw_transp_colored( clean_graph, Cs[0], noisy_graph, Cs[1], p1=None, p2=p2, T=list_T[i], shiftx=3, node_size=50, color_features=True, ) pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/gromov/plot_quantized_gromov_wasserstein.py000066400000000000000000000460761525521106200273660ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ =============================================== Quantized Fused Gromov-Wasserstein examples =============================================== .. note:: Examples added in release: 0.9.4. These examples show how to use the quantized (Fused) Gromov-Wasserstein solvers (qFGW) [68]. POT provides a generic solver `quantized_fused_gromov_wasserstein_partitioned` that takes as inputs partitioned graphs potentially endowed with node features, which have to be built by the user. On top of that, POT provides two wrappers: i) `quantized_fused_gromov_wasserstein` operating over generic graphs, whose partitioning is performed via `get_graph_partition` using e.g the Louvain algorithm, and representant for each partition can be selected via `get_graph_representants` using e.g the PageRank algorithm. ii) `quantized_fused_gromov_wasserstein_samples` operating over point clouds, e.g :math:`X_1 \in R^{n_1 * d_1}` and :math:`X_2 \in R^{n_2 * d_2}` endowed with their respective euclidean geometry, whose partitioning and representant selection is performed jointly using e.g the K-means algorithm via the function `get_partition_and_representants_samples`. We illustrate next how to compute the qGW distance on both types of data by: i) Generating two graphs following Stochastic Block Models encoded as shortest path matrices as qGW solvers tends to require dense structure to achieve a good approximation of the GW distance (as qGW is an upper-bound of GW). In the meantime, we illustrate an optional feature of our solvers, namely the use of auxiliary structures e.g adjacency matrices to perform the graph partitioning. ii) Generating two point clouds representing curves in 2D and 3D respectively. We augment these point clouds by considering additional features of the same dimensionaly :math:`F_1 \in R^{n_1 * d}` and :math:`F_2 \in R^{n_2 * d}`, representing the color intensity associated to each sample of both distributions. Then we compute the qFGW distance between these attributed point clouds. [68] Chowdhury, S., Miller, D., & Needham, T. (2021). Quantized gromov-wasserstein. ECML PKDD 2021. Springer International Publishing. """ # Author: Cédric Vincent-Cuaz # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pylab as pl import matplotlib.pyplot as plt import networkx from networkx.generators.community import stochastic_block_model as sbm from scipy.sparse.csgraph import shortest_path from ot.gromov import ( quantized_fused_gromov_wasserstein_partitioned, quantized_fused_gromov_wasserstein, get_graph_partition, get_graph_representants, format_partitioned_graph, quantized_fused_gromov_wasserstein_samples, get_partition_and_representants_samples, ) ############################################################################# # # Generate graphs # -------------------------------------------------------------------------- # # Create two graphs following Stochastic Block models of 2 and 3 clusters. N1 = 30 # 2 communities N2 = 45 # 3 communities p1 = [[0.8, 0.1], [0.1, 0.7]] p2 = [[0.8, 0.1, 0.0], [0.1, 0.75, 0.1], [0.0, 0.1, 0.7]] G1 = sbm(seed=0, sizes=[N1 // 2, N1 // 2], p=p1) G2 = sbm(seed=0, sizes=[N2 // 3, N2 // 3, N2 // 3], p=p2) C1 = networkx.to_numpy_array(G1) C2 = networkx.to_numpy_array(G2) spC1 = shortest_path(C1) spC2 = shortest_path(C2) h1 = np.ones(C1.shape[0]) / C1.shape[0] h2 = np.ones(C2.shape[0]) / C2.shape[0] # Add weights on the edges for visualization later on weight_intra_G1 = 5 weight_inter_G1 = 0.5 weight_intra_G2 = 1.0 weight_inter_G2 = 1.5 weightedG1 = networkx.Graph() part_G1 = [G1.nodes[i]["block"] for i in range(N1)] for node in G1.nodes(): weightedG1.add_node(node) for i, j in G1.edges(): if part_G1[i] == part_G1[j]: weightedG1.add_edge(i, j, weight=weight_intra_G1) else: weightedG1.add_edge(i, j, weight=weight_inter_G1) weightedG2 = networkx.Graph() part_G2 = [G2.nodes[i]["block"] for i in range(N2)] for node in G2.nodes(): weightedG2.add_node(node) for i, j in G2.edges(): if part_G2[i] == part_G2[j]: weightedG2.add_edge(i, j, weight=weight_intra_G2) else: weightedG2.add_edge(i, j, weight=weight_inter_G2) # setup for graph visualization def node_coloring(part, starting_color=0): # get graphs partition and their coloring unique_colors = ["C%s" % (starting_color + i) for i in np.unique(part)] nodes_color_part = [] for cluster in part: nodes_color_part.append(unique_colors[cluster]) return nodes_color_part def draw_graph( G, C, nodes_color_part, rep_indices, node_alphas=None, pos=None, edge_color="black", alpha_edge=0.7, node_size=None, shiftx=0, seed=0, highlight_rep=False, ): if pos is None: pos = networkx.spring_layout(G, scale=1.0, seed=seed) if shiftx != 0: for k, v in pos.items(): v[0] = v[0] + shiftx width_edge = 1.5 if not highlight_rep: networkx.draw_networkx_edges( G, pos, width=width_edge, alpha=alpha_edge, edge_color=edge_color ) else: for edge in G.edges: if (edge[0] in rep_indices) and (edge[1] in rep_indices): networkx.draw_networkx_edges( G, pos, edgelist=[edge], width=width_edge, alpha=alpha_edge, edge_color=edge_color, ) else: networkx.draw_networkx_edges( G, pos, edgelist=[edge], width=width_edge, alpha=0.2, edge_color=edge_color, ) for node, node_color in enumerate(nodes_color_part): local_node_shape, local_node_size = "o", node_size if highlight_rep: if node in rep_indices: local_node_shape, local_node_size = "*", 6 * node_size if node_alphas is None: alpha = 0.9 if highlight_rep: alpha = 0.9 if node in rep_indices else 0.1 else: alpha = node_alphas[node] networkx.draw_networkx_nodes( G, pos, nodelist=[node], alpha=alpha, node_shape=local_node_shape, node_size=local_node_size, node_color=node_color, ) return pos ############################################################################# # # Compute their quantized Gromov-Wasserstein distance without using the wrapper # --------------------------------------------------------- # # We detail next the steps implemented within the wrapper that preprocess graphs # to form partitioned graphs, which are then passed as input to the generic qFGW solver. # 1-a) Partition C1 and C2 in 2 and 3 clusters respectively using Louvain # algorithm from Networkx. Then encode these partitions via vectors of assignments. part_method = "louvain" rep_method = "pagerank" npart_1 = 2 # 2 clusters used to describe C1 npart_2 = 3 # 3 clusters used to describe C2 part1 = get_graph_partition( C1, npart=npart_1, part_method=part_method, F=None, alpha=1.0 ) part2 = get_graph_partition( C2, npart=npart_2, part_method=part_method, F=None, alpha=1.0 ) # 1-b) Select representant in each partition using the Pagerank algorithm # implementation from networkx. rep_indices1 = get_graph_representants(C1, part1, rep_method=rep_method) rep_indices2 = get_graph_representants(C2, part2, rep_method=rep_method) # 1-c) Format partitions such that: # CR contains relations between representants in each space. # list_R contains relations between samples and representants within each partition. # list_h contains samples relative importance within each partition. CR1, list_R1, list_h1 = format_partitioned_graph( spC1, h1, part1, rep_indices1, F=None, M=None, alpha=1.0 ) CR2, list_R2, list_h2 = format_partitioned_graph( spC2, h2, part2, rep_indices2, F=None, M=None, alpha=1.0 ) # 1-d) call to partitioned quantized gromov-wasserstein solver OT_global_, OTs_local_, OT_, log_ = quantized_fused_gromov_wasserstein_partitioned( CR1, CR2, list_R1, list_R2, list_h1, list_h2, MR=None, alpha=1.0, build_OT=True, log=True, ) # Visualization of the graph pre-processing node_size = 40 fontsize = 10 seed_G1 = 0 seed_G2 = 3 part1_ = part1.astype(np.int32) part2_ = part2.astype(np.int32) nodes_color_part1 = node_coloring(part1_, starting_color=0) nodes_color_part2 = node_coloring( part2_, starting_color=np.unique(nodes_color_part1).shape[0] ) pl.figure(1, figsize=(6, 5)) pl.clf() pl.axis("off") pl.subplot(2, 3, 1) pl.title(r"Input graph: $\mathbf{spC_1}$", fontsize=fontsize) pos1 = draw_graph( G1, C1, ["C0" for _ in part1_], rep_indices1, node_size=node_size, seed=seed_G1 ) pl.subplot(2, 3, 2) pl.title("Partitioning", fontsize=fontsize) _ = draw_graph( G1, C1, nodes_color_part1, rep_indices1, pos=pos1, node_size=node_size, seed=seed_G1 ) pl.subplot(2, 3, 3) pl.title("Representant selection", fontsize=fontsize) _ = draw_graph( G1, C1, nodes_color_part1, rep_indices1, pos=pos1, node_size=node_size, seed=seed_G1, highlight_rep=True, ) pl.subplot(2, 3, 4) pl.title(r"Input graph: $\mathbf{spC_2}$", fontsize=fontsize) pos2 = draw_graph( G2, C2, ["C0" for _ in part2_], rep_indices2, node_size=node_size, seed=seed_G2 ) pl.subplot(2, 3, 5) pl.title(r"Partitioning", fontsize=fontsize) _ = draw_graph( G2, C2, nodes_color_part2, rep_indices2, pos=pos2, node_size=node_size, seed=seed_G2 ) pl.subplot(2, 3, 6) pl.title(r"Representant selection", fontsize=fontsize) _ = draw_graph( G2, C2, nodes_color_part2, rep_indices2, pos=pos2, node_size=node_size, seed=seed_G2, highlight_rep=True, ) pl.tight_layout() ############################################################################# # # Compute the quantized Gromov-Wasserstein distance using the wrapper # --------------------------------------------------------- # # Compute qGW(spC1, h1, spC2, h2). We also illustrate the use of auxiliary matrices # such that the adjacency matrices `C1_aux=C1` and `C2_aux=C2` to partition the graph using # Louvain algorithm, and the Pagerank algorithm for selecting representant within # each partition. Notice that `C1_aux` and `C2_aux` are optional, if they are not # specified these pre-processing algorithms will be applied to spC2 and spC3. # no node features are considered on this synthetic dataset. Hence we simply # let F1, F2 = None and set alpha = 1. OT_global, OTs_local, OT, log = quantized_fused_gromov_wasserstein( spC1, spC2, npart_1, npart_2, h1, h2, C1_aux=C1, C2_aux=C2, F1=None, F2=None, alpha=1.0, part_method=part_method, rep_method=rep_method, log=True, ) qGW_dist = log["qFGW_dist"] ############################################################################# # # Visualization of the quantized Gromov-Wasserstein matching # -------------------------------------------------------------- # # We color nodes of the graph based on the respective partition of each graph. # On the first plot we illustrate the qGW matching between both shortest path matrices. # While the GW matching across representants of each space is illustrated on the right. def draw_transp_colored_qGW( G1, C1, G2, C2, part1, part2, rep_indices1, rep_indices2, T, pos1=None, pos2=None, shiftx=4, switchx=False, node_size=70, seed_G1=0, seed_G2=0, highlight_rep=False, ): starting_color = 0 # get graphs partition and their coloring unique_colors1 = ["C%s" % (starting_color + i) for i in np.unique(part1)] nodes_color_part1 = [] for cluster in part1: nodes_color_part1.append(unique_colors1[cluster]) starting_color = len(unique_colors1) + 1 unique_colors2 = ["C%s" % (starting_color + i) for i in np.unique(part2)] nodes_color_part2 = [] for cluster in part2: nodes_color_part2.append(unique_colors2[cluster]) pos1 = draw_graph( G1, C1, nodes_color_part1, rep_indices1, pos=pos1, node_size=node_size, shiftx=0, seed=seed_G1, highlight_rep=highlight_rep, ) pos2 = draw_graph( G2, C2, nodes_color_part2, rep_indices2, pos=pos2, node_size=node_size, shiftx=shiftx, seed=seed_G1, highlight_rep=highlight_rep, ) if not highlight_rep: for k1, v1 in pos1.items(): max_Tk1 = np.max(T[k1, :]) for k2, v2 in pos2.items(): if T[k1, k2] > 0: pl.plot( [pos1[k1][0], pos2[k2][0]], [pos1[k1][1], pos2[k2][1]], "-", lw=0.7, alpha=T[k1, k2] / max_Tk1, color=nodes_color_part1[k1], ) else: # OT is only between representants for id1, node_id1 in enumerate(rep_indices1): max_Tk1 = np.max(T[id1, :]) for id2, node_id2 in enumerate(rep_indices2): if T[id1, id2] > 0: pl.plot( [pos1[node_id1][0], pos2[node_id2][0]], [pos1[node_id1][1], pos2[node_id2][1]], "-", lw=0.8, alpha=T[id1, id2] / max_Tk1, color=nodes_color_part1[node_id1], ) return pos1, pos2 pl.figure(2, figsize=(5, 2.5)) pl.clf() pl.axis("off") pl.subplot(1, 2, 1) pl.title( r"qGW$(\mathbf{spC_1}, \mathbf{spC_1}) =%s$" % (np.round(qGW_dist, 3)), fontsize=fontsize, ) pos1, pos2 = draw_transp_colored_qGW( weightedG1, C1, weightedG2, C2, part1_, part2_, rep_indices1, rep_indices2, T=OT_, shiftx=1.5, node_size=node_size, seed_G1=seed_G1, seed_G2=seed_G2, ) pl.tight_layout() pl.subplot(1, 2, 2) pl.title( r" GW$(\mathbf{CR_1}, \mathbf{CR_2}) =%s$" % (np.round(log_["global dist"], 3)), fontsize=fontsize, ) pos1, pos2 = draw_transp_colored_qGW( weightedG1, C1, weightedG2, C2, part1_, part2_, rep_indices1, rep_indices2, T=OT_global, shiftx=1.5, node_size=node_size, seed_G1=seed_G1, seed_G2=seed_G2, highlight_rep=True, ) pl.tight_layout() pl.show() ############################################################################# # # Generate attributed point clouds # -------------------------------------------------------------------------- # # Create two attributed point clouds representing curves in 2D and 3D respectively, # whose samples are further associated to various color intensities. n_samples = 100 # Generate 2D and 3D curves theta = np.linspace(-4 * np.pi, 4 * np.pi, n_samples) z = np.linspace(1, 2, n_samples) r = z**2 + 1 x = r * np.sin(theta) y = r * np.cos(theta) # Source and target distribution across spaces encoded respectively via their # squared euclidean distance matrices. X = np.concatenate([x.reshape(-1, 1), z.reshape(-1, 1)], axis=1) Y = np.concatenate([x.reshape(-1, 1), y.reshape(-1, 1), z.reshape(-1, 1)], axis=1) # Further associated to color intensity features derived from z FX = z - z.min() / (z.max() - z.min()) FX = np.clip(0.8 * FX + 0.2, a_min=0.2, a_max=1.0) # for numerical issues FY = FX ############################################################################# # # Visualize partitioned attributed point clouds # -------------------------------------------------------------------------- # # Compute the partitioning and representant selection further used within # qFGW wrapper, both provided by a K-means algorithm. Then visualize partitioned spaces. part1, rep_indices1 = get_partition_and_representants_samples(X, 4, "kmeans", 0) part2, rep_indices2 = get_partition_and_representants_samples(Y, 4, "kmeans", 0) upart1 = np.unique(part1) upart2 = np.unique(part2) # Plot the source and target samples as distributions s = 20 fig = plt.figure(3, figsize=(6, 3)) ax1 = fig.add_subplot(1, 3, 1) ax1.set_title("2D curve") ax1.scatter(X[:, 0], X[:, 1], color="C0", alpha=FX, s=s) plt.axis("off") ax2 = fig.add_subplot(1, 3, 2) ax2.set_title("Partitioning") for i, elem in enumerate(upart1): idx = np.argwhere(part1 == elem)[:, 0] ax2.scatter(X[idx, 0], X[idx, 1], color="C%s" % i, alpha=FX[idx], s=s) plt.axis("off") ax3 = fig.add_subplot(1, 3, 3) ax3.set_title("Representant selection") for i, elem in enumerate(upart1): idx = np.argwhere(part1 == elem)[:, 0] ax3.scatter(X[idx, 0], X[idx, 1], color="C%s" % i, alpha=FX[idx], s=10) rep_idx = rep_indices1[i] ax3.scatter( [X[rep_idx, 0]], [X[rep_idx, 1]], color="C%s" % i, alpha=1, s=6 * s, marker="*" ) plt.axis("off") plt.tight_layout() plt.show() start_color = upart1.shape[0] + 1 fig = plt.figure(4, figsize=(6, 5)) ax4 = fig.add_subplot(1, 3, 1, projection="3d") ax4.set_title("3D curve") ax4.scatter(Y[:, 0], Y[:, 1], Y[:, 2], c="C0", alpha=FY, s=s) plt.axis("off") ax5 = fig.add_subplot(1, 3, 2, projection="3d") ax5.set_title("Partitioning") for i, elem in enumerate(upart2): idx = np.argwhere(part2 == elem)[:, 0] color = "C%s" % (start_color + i) ax5.scatter(Y[idx, 0], Y[idx, 1], Y[idx, 2], c=color, alpha=FY[idx], s=s) plt.axis("off") ax6 = fig.add_subplot(1, 3, 3, projection="3d") ax6.set_title("Representant selection") for i, elem in enumerate(upart2): idx = np.argwhere(part2 == elem)[:, 0] color = "C%s" % (start_color + i) rep_idx = rep_indices2[i] ax6.scatter(Y[idx, 0], Y[idx, 1], Y[idx, 2], c=color, alpha=FY[idx], s=s) ax6.scatter( [Y[rep_idx, 0]], [Y[rep_idx, 1]], [Y[rep_idx, 2]], c=color, alpha=1, s=6 * s, marker="*", ) plt.axis("off") plt.tight_layout() plt.show() ############################################################################# # # Compute the quantized Fused Gromov-Wasserstein distance between samples using the wrapper # --------------------------------------------------------- # # Compute qFGW(X, FX, hX, Y, FY, HY), setting the trade-off parameter between # structures and features `alpha=0.5`. This solver considers a squared euclidean structure # for each distribution X and Y, and partition each of them into 4 clusters using # the K-means algorithm before computing qFGW. T_global, Ts_local, T, log = quantized_fused_gromov_wasserstein_samples( X, Y, 4, 4, p=None, q=None, F1=FX[:, None], F2=FY[:, None], alpha=0.5, method="kmeans", log=True, ) # Plot low rank GW with different ranks pl.figure(5, figsize=(6, 3)) pl.subplot(1, 2, 1) pl.title("OT between distributions") pl.imshow(T, interpolation="nearest", aspect="auto") pl.colorbar() pl.axis("off") pl.subplot(1, 2, 2) pl.title("OT between representants") pl.imshow(T_global, interpolation="nearest", aspect="auto") pl.axis("off") pl.colorbar() pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/gromov/plot_semirelaxed_fgw.py000066400000000000000000000260061525521106200244760ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ =============================================== Semi-relaxed (Fused) Gromov-Wasserstein example =============================================== This example is designed to show how to use the semi-relaxed Gromov-Wasserstein and the semi-relaxed Fused Gromov-Wasserstein divergences. sr(F)GW between two graphs G1 and G2 searches for a reweighing of the nodes of G2 at a minimal (F)GW distance from G1. First, we generate two graphs following Stochastic Block Models, then show how to compute their srGW matchings and illustrate them. These graphs are then endowed with node features and we follow the same process with srFGW. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2021. """ # Author: Cédric Vincent-Cuaz # # License: MIT License # sphinx_gallery_thumbnail_number = 1 import numpy as np import matplotlib.pylab as pl from ot.gromov import ( semirelaxed_gromov_wasserstein, semirelaxed_fused_gromov_wasserstein, gromov_wasserstein, fused_gromov_wasserstein, ) import networkx from networkx.generators.community import stochastic_block_model as sbm ############################################################################# # # Generate two graphs following Stochastic Block models of 2 and 3 clusters. # -------------------------------------------------------------------------- N2 = 20 # 2 communities N3 = 30 # 3 communities p2 = [[1.0, 0.1], [0.1, 0.9]] p3 = [[1.0, 0.1, 0.0], [0.1, 0.95, 0.1], [0.0, 0.1, 0.9]] G2 = sbm(seed=0, sizes=[N2 // 2, N2 // 2], p=p2) G3 = sbm(seed=0, sizes=[N3 // 3, N3 // 3, N3 // 3], p=p3) C2 = networkx.to_numpy_array(G2) C3 = networkx.to_numpy_array(G3) h2 = np.ones(C2.shape[0]) / C2.shape[0] h3 = np.ones(C3.shape[0]) / C3.shape[0] # Add weights on the edges for visualization later on weight_intra_G2 = 5 weight_inter_G2 = 0.5 weight_intra_G3 = 1.0 weight_inter_G3 = 1.5 weightedG2 = networkx.Graph() part_G2 = [G2.nodes[i]["block"] for i in range(N2)] for node in G2.nodes(): weightedG2.add_node(node) for i, j in G2.edges(): if part_G2[i] == part_G2[j]: weightedG2.add_edge(i, j, weight=weight_intra_G2) else: weightedG2.add_edge(i, j, weight=weight_inter_G2) weightedG3 = networkx.Graph() part_G3 = [G3.nodes[i]["block"] for i in range(N3)] for node in G3.nodes(): weightedG3.add_node(node) for i, j in G3.edges(): if part_G3[i] == part_G3[j]: weightedG3.add_edge(i, j, weight=weight_intra_G3) else: weightedG3.add_edge(i, j, weight=weight_inter_G3) ############################################################################# # # Compute their semi-relaxed Gromov-Wasserstein divergences # --------------------------------------------------------- # 0) GW(C2, h2, C3, h3) for reference OT, log = gromov_wasserstein(C2, C3, h2, h3, symmetric=True, log=True) gw = log["gw_dist"] # 1) srGW(C2, h2, C3) OT_23, log_23 = semirelaxed_gromov_wasserstein( C2, C3, h2, symmetric=True, log=True, G0=None ) srgw_23 = log_23["srgw_dist"] # 2) srGW(C3, h3, C2) OT_32, log_32 = semirelaxed_gromov_wasserstein( C3, C2, h3, symmetric=None, log=True, G0=OT.T ) srgw_32 = log_32["srgw_dist"] print("GW(C2, C3) = ", gw) print("srGW(C2, h2, C3) = ", srgw_23) print("srGW(C3, h3, C2) = ", srgw_32) ############################################################################# # # Visualization of the semi-relaxed Gromov-Wasserstein matchings # -------------------------------------------------------------- # # We color nodes of the graph on the right - then project its node colors # based on the optimal transport plan from the srGW matching def draw_graph( G, C, nodes_color_part, Gweights=None, pos=None, edge_color="black", node_size=None, shiftx=0, seed=0, ): if pos is None: pos = networkx.spring_layout(G, scale=1.0, seed=seed) if shiftx != 0: for k, v in pos.items(): v[0] = v[0] + shiftx alpha_edge = 0.7 width_edge = 1.8 if Gweights is None: networkx.draw_networkx_edges( G, pos, width=width_edge, alpha=alpha_edge, edge_color=edge_color ) else: # We make more visible connections between activated nodes n = len(Gweights) edgelist_activated = [] edgelist_deactivated = [] for i in range(n): for j in range(n): if Gweights[i] * Gweights[j] * C[i, j] > 0: edgelist_activated.append((i, j)) elif C[i, j] > 0: edgelist_deactivated.append((i, j)) networkx.draw_networkx_edges( G, pos, edgelist=edgelist_activated, width=width_edge, alpha=alpha_edge, edge_color=edge_color, ) networkx.draw_networkx_edges( G, pos, edgelist=edgelist_deactivated, width=width_edge, alpha=0.1, edge_color=edge_color, ) if Gweights is None: for node, node_color in enumerate(nodes_color_part): networkx.draw_networkx_nodes( G, pos, nodelist=[node], node_size=node_size, alpha=1, node_color=node_color, ) else: scaled_Gweights = Gweights / (0.5 * Gweights.max()) nodes_size = node_size * scaled_Gweights for node, node_color in enumerate(nodes_color_part): networkx.draw_networkx_nodes( G, pos, nodelist=[node], node_size=nodes_size[node], alpha=1, node_color=node_color, ) return pos def draw_transp_colored_srGW( G1, C1, G2, C2, part_G1, p1, p2, T, pos1=None, pos2=None, shiftx=4, switchx=False, node_size=70, seed_G1=0, seed_G2=0, ): starting_color = 0 # get graphs partition and their coloring part1 = part_G1.copy() unique_colors = ["C%s" % (starting_color + i) for i in np.unique(part1)] nodes_color_part1 = [] for cluster in part1: nodes_color_part1.append(unique_colors[cluster]) nodes_color_part2 = [] # T: getting colors assignment from argmin of columns for i in range(len(G2.nodes())): j = np.argmax(T[:, i]) nodes_color_part2.append(nodes_color_part1[j]) pos1 = draw_graph( G1, C1, nodes_color_part1, Gweights=p1, pos=pos1, node_size=node_size, shiftx=0, seed=seed_G1, ) pos2 = draw_graph( G2, C2, nodes_color_part2, Gweights=p2, pos=pos2, node_size=node_size, shiftx=shiftx, seed=seed_G2, ) for k1, v1 in pos1.items(): for k2, v2 in pos2.items(): if T[k1, k2] > 0: pl.plot( [pos1[k1][0], pos2[k2][0]], [pos1[k1][1], pos2[k2][1]], "-", lw=0.8, alpha=0.5, color=nodes_color_part1[k1], ) return pos1, pos2 node_size = 40 fontsize = 10 seed_G2 = 0 seed_G3 = 4 pl.figure(1, figsize=(8, 2.5)) pl.clf() pl.subplot(121) pl.axis("off") pl.axis pl.title( r"srGW$(\mathbf{C_2},\mathbf{h_2},\mathbf{C_3}) =%s$" % (np.round(srgw_23, 3)), fontsize=fontsize, ) hbar2 = OT_23.sum(axis=0) pos1, pos2 = draw_transp_colored_srGW( weightedG2, C2, weightedG3, C3, part_G2, p1=None, p2=hbar2, T=OT_23, shiftx=1.5, node_size=node_size, seed_G1=seed_G2, seed_G2=seed_G3, ) pl.subplot(122) pl.axis("off") hbar3 = OT_32.sum(axis=0) pl.title( r"srGW$(\mathbf{C_3}, \mathbf{h_3},\mathbf{C_2}) =%s$" % (np.round(srgw_32, 3)), fontsize=fontsize, ) pos1, pos2 = draw_transp_colored_srGW( weightedG3, C3, weightedG2, C2, part_G3, p1=None, p2=hbar3, T=OT_32, pos1=pos2, pos2=pos1, shiftx=3.0, node_size=node_size, seed_G1=0, seed_G2=0, ) pl.tight_layout() pl.show() ############################################################################# # # Add node features # ----------------- # We add node features with given mean - by clusters # and inversely proportional to clusters' intra-connectivity F2 = np.zeros((N2, 1)) for i, c in enumerate(part_G2): F2[i, 0] = np.random.normal(loc=c, scale=0.01) F3 = np.zeros((N3, 1)) for i, c in enumerate(part_G3): F3[i, 0] = np.random.normal(loc=2.0 - c, scale=0.01) ############################################################################# # # Compute their semi-relaxed Fused Gromov-Wasserstein divergences # --------------------------------------------------------------- alpha = 0.5 # Compute pairwise euclidean distance between node features M = (F2**2).dot(np.ones((1, N3))) + np.ones((N2, 1)).dot((F3**2).T) - 2 * F2.dot(F3.T) # 0) FGW_alpha(C2, F2, h2, C3, F3, h3) for reference OT, log = fused_gromov_wasserstein( M, C2, C3, h2, h3, symmetric=True, alpha=alpha, log=True ) fgw = log["fgw_dist"] # 1) srFGW(C2, F2, h2, C3, F3) OT_23, log_23 = semirelaxed_fused_gromov_wasserstein( M, C2, C3, h2, symmetric=True, alpha=0.5, log=True, G0=None ) srfgw_23 = log_23["srfgw_dist"] # 2) srFGW(C3, F3, h3, C2, F2) OT_32, log_32 = semirelaxed_fused_gromov_wasserstein( M.T, C3, C2, h3, symmetric=None, alpha=alpha, log=True, G0=None ) srfgw_32 = log_32["srfgw_dist"] print("FGW(C2, F2, C3, F3) = ", fgw) print("srGW(C2, F2, h2, C3, F3) = ", srfgw_23) print("srGW(C3, F3, h3, C2, F2) = ", srfgw_32) ############################################################################# # # Visualization of the semi-relaxed Fused Gromov-Wasserstein matchings # -------------------------------------------------------------------- # # We color nodes of the graph on the right - then project its node colors # based on the optimal transport plan from the srFGW matching # NB: colors refer to clusters - not to node features pl.figure(2, figsize=(8, 2.5)) pl.clf() pl.subplot(121) pl.axis("off") pl.axis pl.title( r"srFGW$(\mathbf{C_2},\mathbf{F_2},\mathbf{h_2},\mathbf{C_3},\mathbf{F_3}) =%s$" % (np.round(srfgw_23, 3)), fontsize=fontsize, ) hbar2 = OT_23.sum(axis=0) pos1, pos2 = draw_transp_colored_srGW( weightedG2, C2, weightedG3, C3, part_G2, p1=None, p2=hbar2, T=OT_23, shiftx=1.5, node_size=node_size, seed_G1=seed_G2, seed_G2=seed_G3, ) pl.subplot(122) pl.axis("off") hbar3 = OT_32.sum(axis=0) pl.title( r"srFGW$(\mathbf{C_3}, \mathbf{F_3}, \mathbf{h_3}, \mathbf{C_2}, \mathbf{F_2}) =%s$" % (np.round(srfgw_32, 3)), fontsize=fontsize, ) pos1, pos2 = draw_transp_colored_srGW( weightedG3, C3, weightedG2, C2, part_G3, p1=None, p2=hbar3, T=OT_32, pos1=pos2, pos2=pos1, shiftx=3.0, node_size=node_size, seed_G1=0, seed_G2=0, ) pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/gromov/plot_semirelaxed_gromov_wasserstein_barycenter.py000066400000000000000000000264141525521106200320740ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ===================================================== Semi-relaxed (Fused) Gromov-Wasserstein Barycenter as Dictionary Learning ===================================================== .. note:: Example added in release: 0.9.5. In this example, we illustrate how to learn a semi-relaxed Gromov-Wasserstein (srGW) barycenter using a Block-Coordinate Descent algorithm, on a dataset of structured data such as graphs, denoted :math:`\{ \mathbf{C_s} \}_{s \in [S]}` where every nodes have uniform weights :math:`\{ \mathbf{p_s} \}_{s \in [S]}`. Given a barycenter structure matrix :math:`\mathbf{C}` with N nodes, each graph :math:`(\mathbf{C_s}, \mathbf{p_s})` is modeled as a reweighed subgraph with structure :math:`\mathbf{C}` and weights :math:`\mathbf{w_s} \in \Sigma_N` where each :math:`\mathbf{w_s}` corresponds to the second marginal of the OT :math:`\mathbf{T_s}` (s.t :math:`\mathbf{w_s} = \mathbf{T_s}^\top \mathbf{1}`) minimizing the srGW loss between the s^{th} input and the barycenter. First, we consider a dataset composed of graphs generated by Stochastic Block models with variable sizes taken in :math:`\{30, ... , 50\}` and number of clusters varying in :math:`\{ 1, 2, 3\}` with random proportions. We learn a srGW barycenter with 3 nodes and visualize the learned structure and the embeddings for some inputs. Second, we illustrate the extension of this framework to graphs endowed with node features by using the semi-relaxed Fused Gromov-Wasserstein divergence (srFGW). Starting from the aforementioned dataset of unattributed graphs, we add discrete labels uniformly depending on the number of clusters. Then conduct the analog analysis. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs". International Conference on Learning Representations (ICLR), 2022. """ # Author: Cédric Vincent-Cuaz # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pylab as pl from sklearn.manifold import MDS from ot.gromov import semirelaxed_gromov_barycenters, semirelaxed_fgw_barycenters import ot import networkx from networkx.generators.community import stochastic_block_model as sbm ############################################################################# # # Generate a dataset composed of graphs following Stochastic Block models of 1, 2 and 3 clusters. # ----------------------------------------------------------------------------------------------- np.random.seed(42) n_samples = 60 # number of graphs in the dataset # For every number of clusters, we generate SBM with fixed inter/intra-clusters probability, # and variable cluster proportions. clusters = [1, 2, 3] Nc = n_samples // len(clusters) # number of graphs by cluster nlabels = len(clusters) dataset = [] node_labels = [] labels = [] p_inter = 0.1 p_intra = 0.9 for n_cluster in clusters: for i in range(Nc): n_nodes = int(np.random.uniform(low=30, high=50)) if n_cluster > 1: P = p_inter * np.ones((n_cluster, n_cluster)) np.fill_diagonal(P, p_intra) props = np.random.uniform(0.2, 1, size=(n_cluster,)) props /= props.sum() sizes = np.round(n_nodes * props).astype(np.int32) else: P = p_intra * np.eye(1) sizes = [n_nodes] G = sbm(sizes, P, seed=i, directed=False) part = np.array([G.nodes[i]["block"] for i in range(np.sum(sizes))]) C = networkx.to_numpy_array(G) dataset.append(C) node_labels.append(part) labels.append(n_cluster) # Visualize samples def plot_graph(x, C, binary=True, color="C0", s=None): for j in range(C.shape[0]): for i in range(j): if binary: if C[i, j] > 0: pl.plot( [x[i, 0], x[j, 0]], [x[i, 1], x[j, 1]], alpha=0.2, color="k" ) else: # connection intensity proportional to C[i,j] pl.plot( [x[i, 0], x[j, 0]], [x[i, 1], x[j, 1]], alpha=C[i, j], color="k" ) pl.scatter( x[:, 0], x[:, 1], c=color, s=s, zorder=10, edgecolors="k", cmap="tab10", vmax=9 ) pl.figure(1, (12, 8)) pl.clf() for idx_c, c in enumerate(clusters): C = dataset[(c - 1) * Nc] # sample with c clusters # get 2d position for nodes x = MDS(dissimilarity="precomputed", random_state=0).fit_transform(1 - C) pl.subplot(2, nlabels, c) pl.title("(graph) sample from label " + str(c), fontsize=14) plot_graph(x, C, binary=True, color="C0", s=50.0) pl.axis("off") pl.subplot(2, nlabels, nlabels + c) pl.title("(matrix) sample from label %s \n" % c, fontsize=14) pl.imshow(C, interpolation="nearest") pl.axis("off") pl.tight_layout() pl.show() ############################################################################# # # Estimate the srGW barycenter from the dataset and visualize embeddings # ----------------------------------------------------------- np.random.seed(0) ps = [ot.unif(C.shape[0]) for C in dataset] # uniform weights on input nodes lambdas = [1.0 / n_samples for _ in range(n_samples)] # uniform barycenter N = 3 # 3 nodes in the barycenter # Here we use the Fluid partitioning method to deduce initial transport plans # for the barycenter problem. An initlal structure is also deduced from these # initial transport plans. Then a warmstart strategy is used iteratively to # init each individual srGW problem within the BCD algorithm. init_plan = "fluid" # notice that several init options are implemented in `ot.gromov.semirelaxed_init_plan` warmstartT = True C, log = semirelaxed_gromov_barycenters( N=N, Cs=dataset, ps=ps, lambdas=lambdas, loss_fun="square_loss", tol=1e-6, stop_criterion="loss", warmstartT=warmstartT, log=True, G0=init_plan, verbose=False, ) print("barycenter structure:", C) unmixings = log["p"] # Compute the 2D representation of the embeddings living in the 2-simplex of probability unmixings2D = np.zeros(shape=(n_samples, 2)) for i, w in enumerate(unmixings): unmixings2D[i, 0] = (2.0 * w[1] + w[2]) / 2.0 unmixings2D[i, 1] = (np.sqrt(3.0) * w[2]) / 2.0 x = [0.0, 0.0] y = [1.0, 0.0] z = [0.5, np.sqrt(3) / 2.0] extremities = np.stack([x, y, z]) pl.figure(2, (4, 4)) pl.clf() pl.title("Embedding space", fontsize=14) for cluster in range(nlabels): start, end = Nc * cluster, Nc * (cluster + 1) if cluster == 0: pl.scatter( unmixings2D[start:end, 0], unmixings2D[start:end, 1], c="C" + str(cluster), marker="o", s=80.0, label="1 cluster", ) else: pl.scatter( unmixings2D[start:end, 0], unmixings2D[start:end, 1], c="C" + str(cluster), marker="o", s=80.0, label="%s clusters" % (cluster + 1), ) pl.scatter( extremities[:, 0], extremities[:, 1], c="black", marker="x", s=100.0, label="bary. nodes", ) pl.plot([x[0], y[0]], [x[1], y[1]], color="black", linewidth=2.0) pl.plot([x[0], z[0]], [x[1], z[1]], color="black", linewidth=2.0) pl.plot([y[0], z[0]], [y[1], z[1]], color="black", linewidth=2.0) pl.axis("off") pl.legend(fontsize=11) pl.tight_layout() pl.show() ############################################################################# # # Endow the dataset with node features # ------------------------------------ # node labels, corresponding to the true SBM cluster assignments, # are set for each graph as one-hot encoded node features. dataset_features = [] for i in range(len(dataset)): n = dataset[i].shape[0] F = np.zeros((n, 3)) F[np.arange(n), node_labels[i]] = 1.0 dataset_features.append(F) pl.figure(3, (12, 8)) pl.clf() for idx_c, c in enumerate(clusters): C = dataset[(c - 1) * Nc] # sample with c clusters F = dataset_features[(c - 1) * Nc] colors = [f"C{labels[i]}" for i in range(F.shape[0])] # get 2d position for nodes x = MDS(dissimilarity="precomputed", random_state=0).fit_transform(1 - C) pl.subplot(2, nlabels, c) pl.title("(graph) sample from label " + str(c), fontsize=14) plot_graph(x, C, binary=True, color=colors, s=50) pl.axis("off") pl.subplot(2, nlabels, nlabels + c) pl.title("(matrix) sample from label %s \n" % c, fontsize=14) pl.imshow(C, interpolation="nearest") pl.axis("off") pl.tight_layout() pl.show() ############################################################################# # # Estimate the srFGW barycenter from the attributed graphs and visualize embeddings # ----------------------------------------------------------- # We emphasize the dependence to the trade-off parameter alpha that weights the # relative importance between structures (alpha=1) and features (alpha=0), # knowing that embeddings that perfectly cluster graphs w.r.t their features # should ease the identification of the number of clusters in the graphs. list_alphas = [0.0001, 0.5, 0.9999] list_unmixings2D = [] for ialpha, alpha in enumerate(list_alphas): print("--- alpha:", alpha) C, F, log = semirelaxed_fgw_barycenters( N=N, Ys=dataset_features, Cs=dataset, ps=ps, lambdas=lambdas, alpha=alpha, loss_fun="square_loss", tol=1e-6, stop_criterion="loss", warmstartT=warmstartT, log=True, G0=init_plan, ) print("barycenter structure:", C) print("barycenter features:", F) unmixings = log["p"] # Compute the 2D representation of the embeddings living in the 2-simplex of probability unmixings2D = np.zeros(shape=(n_samples, 2)) for i, w in enumerate(unmixings): unmixings2D[i, 0] = (2.0 * w[1] + w[2]) / 2.0 unmixings2D[i, 1] = (np.sqrt(3.0) * w[2]) / 2.0 list_unmixings2D.append(unmixings2D.copy()) x = [0.0, 0.0] y = [1.0, 0.0] z = [0.5, np.sqrt(3) / 2.0] extremities = np.stack([x, y, z]) pl.figure(4, (12, 4)) pl.clf() pl.suptitle("Embedding spaces", fontsize=14) for ialpha, alpha in enumerate(list_alphas): pl.subplot(1, len(list_alphas), ialpha + 1) pl.title(f"alpha = {alpha}", fontsize=14) for cluster in range(nlabels): start, end = Nc * cluster, Nc * (cluster + 1) if cluster == 0: pl.scatter( list_unmixings2D[ialpha][start:end, 0], list_unmixings2D[ialpha][start:end, 1], c="C" + str(cluster), marker="o", s=80.0, label="1 cluster", ) else: pl.scatter( list_unmixings2D[ialpha][start:end, 0], list_unmixings2D[ialpha][start:end, 1], c="C" + str(cluster), marker="o", s=80.0, label="%s clusters" % (cluster + 1), ) pl.scatter( extremities[:, 0], extremities[:, 1], c="black", marker="x", s=100.0, label="bary. nodes", ) pl.plot([x[0], y[0]], [x[1], y[1]], color="black", linewidth=2.0) pl.plot([x[0], z[0]], [x[1], z[1]], color="black", linewidth=2.0) pl.plot([y[0], z[0]], [y[1], z[1]], color="black", linewidth=2.0) pl.axis("off") pl.legend(fontsize=11) pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/index.rst000066400000000000000000000057321525521106200202540ustar00rootroot00000000000000Examples gallery ================ This is a gallery of all the POT example files. OT and regularized OT --------------------- .. toctree:: :maxdepth: 2 :glob: :hidden: plot* .. minigallery:: ../../examples/plot_*.py Differentiable OT with PyTorch ------------------------------ .. toctree:: :maxdepth: 3 :glob: :hidden: backends/plot_* .. minigallery:: ../../examples/backends/plot_*.py ../../examples/gaussian_gmm/plot_GMM_flow.py ../../examples/gromov/plot_gnn_TFGW.py Gromov-Wasserstein (GW) and Fused GW ------------------------------------ .. toctree:: :maxdepth: 3 :glob: :hidden: gromov/plot_* .. minigallery:: ../../examples/gromov/plot_*.py Unbalanced and Partial OT ------------------------- .. toctree:: :maxdepth: 2 :glob: :hidden: unbalanced-partial/plot_* .. minigallery:: ../../examples/unbalanced-partial/plot_*.py ../../examples/gromov/plot_semirelaxed_fgw.py ../../examples/gromov/plot_semirelaxed_gromov_wasserstein_barycenter.py ../../examples/gromov/plot_entropic_semirelaxed_fgw.py ../../examples/others/plot_outlier_detection_with_COOT_and_unbalanced_COOT.py OT in 1D and Sliced Wasserstein ------------------------------- .. toctree:: :maxdepth: 2 :glob: :hidden: sliced-wasserstein/plot_* .. minigallery:: ../../examples/sliced-wasserstein/plot_*.py ../../examples/plot_compute_emd.py ../../examples/unbalanced-partial/plot_partial_1d.py ../../examples/unbalanced-partial/plot_UOT_1D.py ../../examples/unbalanced-partial/plot_UOT_sliced.py OT on Gaussian and Gaussian Mixture Models ------------------------------------------- .. toctree:: :maxdepth: 2 :glob: :hidden: gaussian_gmm/plot_* .. minigallery:: ../../examples/gaussian_gmm/plot_*.py ../../examples/domain-adaptation/plot_otda_linear_mapping.py ../../examples/barycenters/plot_gaussian_barycenter.py ../../examples/barycenters/plot_gmm_barycenter.py Factored an Low-Rank OT ------------------------------------ .. toctree:: :maxdepth: 2 :glob: :hidden: lowrank/plot_* .. minigallery:: ../../examples/lowrank/plot_*.py Wasserstein and (F)GW barycenters ------------------------------ .. toctree:: :maxdepth: 2 :glob: :hidden: barycenters/plot_* .. minigallery:: ../../examples/barycenters/plot_*.py ../../examples/gromov/plot_barycenter_fgw.py ../../examples/gromov/plot_semirelaxed_gromov_wasserstein_barycenter.py ../../examples/gromov/plot_gromov_barycenter.py ../../examplesbackends/plot_sliced_wass_grad_flow_pytorch.py Domain adaptation with OT -------------------------- .. toctree:: :maxdepth: 2 :glob: :hidden: domain-adaptation/plot_* .. minigallery:: ../../examples/domain-adaptation/plot_*.py Other OT problems ----------------- .. toctree:: :maxdepth: 2 :glob: :hidden: others/plot_* .. minigallery:: ../../examples/others/plot_*.py python-pot-0.9.7+dfsg/examples/lowrank/000077500000000000000000000000001525521106200200615ustar00rootroot00000000000000python-pot-0.9.7+dfsg/examples/lowrank/index.rst000066400000000000000000000000751525521106200217240ustar00rootroot00000000000000 Factored an Low-Rank OT ------------------------------------python-pot-0.9.7+dfsg/examples/lowrank/plot_factored_coupling.py000066400000000000000000000042361525521106200251650ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ========================================== Optimal transport with factored couplings ========================================== .. note:: Example added in release: 0.8.2. Illustration of the factored coupling OT between 2D empirical distributions """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pylab as pl import ot import ot.plot # %% # Generate data an plot it # ------------------------ # parameters and data generation np.random.seed(42) n = 100 # nb samples xs = np.random.rand(n, 2) - 0.5 xs = xs + np.sign(xs) xt = np.random.rand(n, 2) - 0.5 a, b = ot.unif(n), ot.unif(n) # uniform distribution on samples # %% plot samples pl.figure(1) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.legend(loc=0) pl.title("Source and target distributions") # %% # Compute Factored OT and exact OT solutions # ------------------------------------------ # %% EMD M = ot.dist(xs, xt) G0 = ot.emd(a, b, M) # %% factored OT OT Ga, Gb, xb = ot.factored_optimal_transport(xs, xt, a, b, r=4) # %% # Plot factored OT and exact OT solutions # --------------------------------------- pl.figure(2, (14, 4)) pl.subplot(1, 3, 1) ot.plot.plot2D_samples_mat(xs, xt, G0, c=[0.2, 0.2, 0.2], alpha=0.1) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.title("Exact OT with samples") pl.subplot(1, 3, 2) ot.plot.plot2D_samples_mat(xs, xb, Ga, c=[0.6, 0.6, 0.9], alpha=0.5) ot.plot.plot2D_samples_mat(xb, xt, Gb, c=[0.9, 0.6, 0.6], alpha=0.5) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.plot(xb[:, 0], xb[:, 1], "og", label="Template samples") pl.title("Factored OT with template samples") pl.subplot(1, 3, 3) ot.plot.plot2D_samples_mat(xs, xt, Ga.dot(Gb), c=[0.2, 0.2, 0.2], alpha=0.1) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.title("Factored OT low rank OT plan") python-pot-0.9.7+dfsg/examples/lowrank/plot_lowrank_GW.py000066400000000000000000000112041525521106200235410ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ======================================== Low rank Gromov-Wasterstein between samples ======================================== .. note:: Example added in release: 0.9.4. Comparison between entropic Gromov-Wasserstein and Low Rank Gromov Wasserstein [67] on two curves in 2D and 3D, both sampled with 200 points. The squared Euclidean distance is considered as the ground cost for both samples. [67] Scetbon, M., Peyré, G. & Cuturi, M. (2022). "Linear-Time GromovWasserstein Distances using Low Rank Couplings and Costs". In International Conference on Machine Learning (ICML), 2022. """ # Author: Laurène David # # License: MIT License # # sphinx_gallery_thumbnail_number = 3 # %% import numpy as np import matplotlib.pylab as pl import ot.plot import time ############################################################################## # Generate data # ------------- # %% parameters n_samples = 200 # Generate 2D and 3D curves theta = np.linspace(-4 * np.pi, 4 * np.pi, n_samples) z = np.linspace(1, 2, n_samples) r = z**2 + 1 x = r * np.sin(theta) y = r * np.cos(theta) # Source and target distribution X = np.concatenate([x.reshape(-1, 1), z.reshape(-1, 1)], axis=1) Y = np.concatenate([x.reshape(-1, 1), y.reshape(-1, 1), z.reshape(-1, 1)], axis=1) ############################################################################## # Plot data # ------------ # %% # Plot the source and target samples fig = pl.figure(1, figsize=(10, 4)) ax = fig.add_subplot(121) ax.plot(X[:, 0], X[:, 1], color="blue", linewidth=6) ax.tick_params( left=False, right=False, labelleft=False, labelbottom=False, bottom=False ) ax.set_title("2D curve (source)") ax2 = fig.add_subplot(122, projection="3d") ax2.plot(Y[:, 0], Y[:, 1], Y[:, 2], c="red", linewidth=6) ax2.tick_params( left=False, right=False, labelleft=False, labelbottom=False, bottom=False ) ax2.view_init(15, -50) ax2.set_title("3D curve (target)") pl.tight_layout() pl.show() ############################################################################## # Entropic Gromov-Wasserstein # ------------ # %% # Compute cost matrices C1 = ot.dist(X, X, metric="sqeuclidean") C2 = ot.dist(Y, Y, metric="sqeuclidean") # Scale cost matrices r1 = C1.max() r2 = C2.max() C1 = C1 / r1 C2 = C2 / r2 # Solve entropic gw reg = 5 * 1e-3 start = time.time() gw, log = ot.gromov.entropic_gromov_wasserstein( C1, C2, tol=1e-3, epsilon=reg, log=True, verbose=False ) end = time.time() time_entropic = end - start entropic_gw_loss = np.round(log["gw_dist"], 3) # Plot entropic gw pl.figure(2) pl.imshow(gw, interpolation="nearest", aspect="auto", cmap="gray_r") pl.title("Entropic Gromov-Wasserstein (loss={})".format(entropic_gw_loss)) pl.show() ############################################################################## # Low rank squared euclidean cost matrices # ------------ # %% # Compute the low rank sqeuclidean cost decompositions A1, A2 = ot.lowrank.compute_lr_sqeuclidean_matrix(X, X, rescale_cost=False) B1, B2 = ot.lowrank.compute_lr_sqeuclidean_matrix(Y, Y, rescale_cost=False) # Scale the low rank cost matrices A1, A2 = A1 / np.sqrt(r1), A2 / np.sqrt(r1) B1, B2 = B1 / np.sqrt(r2), B2 / np.sqrt(r2) ############################################################################## # Low rank Gromov-Wasserstein # ------------ # %% # Solve low rank gromov-wasserstein with different ranks list_rank = [10, 50] list_P_GW = [] list_loss_GW = [] list_time_GW = [] for rank in list_rank: start = time.time() Q, R, g, log = ot.lowrank_gromov_wasserstein_samples( X, Y, reg=0, rank=rank, rescale_cost=False, cost_factorized_Xs=(A1, A2), cost_factorized_Xt=(B1, B2), seed_init=49, numItermax=1000, log=True, stopThr=1e-6, ) end = time.time() P = log["lazy_plan"][:] loss = log["value"] list_P_GW.append(P) list_loss_GW.append(np.round(loss, 3)) list_time_GW.append(end - start) # %% # Plot low rank GW with different ranks pl.figure(3, figsize=(10, 4)) pl.subplot(1, 2, 1) pl.imshow(list_P_GW[0], interpolation="nearest", aspect="auto", cmap="gray_r") pl.title("Low rank GW (rank=10, loss={})".format(list_loss_GW[0])) pl.subplot(1, 2, 2) pl.imshow(list_P_GW[1], interpolation="nearest", aspect="auto", cmap="gray_r") pl.title("Low rank GW (rank=50, loss={})".format(list_loss_GW[1])) pl.tight_layout() pl.show() # %% # Compare computation time between entropic GW and low rank GW print("Entropic GW: {:.2f}s".format(time_entropic)) print("Low rank GW (rank=10): {:.2f}s".format(list_time_GW[0])) print("Low rank GW (rank=50): {:.2f}s".format(list_time_GW[1])) python-pot-0.9.7+dfsg/examples/lowrank/plot_lowrank_sinkhorn.py000066400000000000000000000062371525521106200250710ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ======================================== Low rank Sinkhorn ======================================== .. note:: Example added in release: 0.9.2. This example illustrates the computation of Low Rank Sinkhorn [26]. [65] Scetbon, M., Cuturi, M., & Peyré, G. (2021). "Low-rank Sinkhorn factorization". In International Conference on Machine Learning. """ # Author: Laurène David # # License: MIT License # # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pylab as pl import ot.plot from ot.datasets import make_1D_gauss as gauss ############################################################################## # Generate data # ------------- # %% parameters n = 100 m = 120 # Gaussian distribution a = gauss(n, m=int(n / 3), s=25 / np.sqrt(2)) + 1.5 * gauss( n, m=int(5 * n / 6), s=15 / np.sqrt(2) ) a = a / np.sum(a) b = 2 * gauss(m, m=int(m / 5), s=30 / np.sqrt(2)) + gauss( m, m=int(m / 2), s=35 / np.sqrt(2) ) b = b / np.sum(b) # Source and target distribution X = np.arange(n).reshape(-1, 1) Y = np.arange(m).reshape(-1, 1) ############################################################################## # Solve Low rank sinkhorn # ------------ # %% # Solve low rank sinkhorn Q, R, g, log = ot.lowrank_sinkhorn( X, Y, a, b, rank=10, init="random", gamma_init="rescale", rescale_cost=True, warn=False, log=True, ) P = log["lazy_plan"][:] ot.plot.plot1D_mat(a, b, P, "OT matrix Low rank") ############################################################################## # Sinkhorn vs Low Rank Sinkhorn # ----------------------- # Compare Sinkhorn and Low rank sinkhorn with different regularizations and ranks. # %% Sinkhorn # Compute cost matrix for sinkhorn OT M = ot.dist(X, Y) M = M / np.max(M) # Solve sinkhorn with different regularizations using ot.solve list_reg = [0.05, 0.005, 0.001] list_P_Sin = [] for reg in list_reg: P = ot.solve(M, a, b, reg=reg, max_iter=2000, tol=1e-8).plan list_P_Sin.append(P) # %% Low rank sinkhorn # Solve low rank sinkhorn with different ranks using ot.solve_sample list_rank = [3, 10, 50] list_P_LR = [] for rank in list_rank: P = ot.solve_sample(X, Y, a, b, method="lowrank", rank=rank).plan P = P[:] list_P_LR.append(P) # %% # Plot sinkhorn vs low rank sinkhorn pl.figure(1, figsize=(10, 8)) pl.subplot(2, 3, 1) pl.imshow(list_P_Sin[0], interpolation="nearest", cmap="gray_r") pl.axis("off") pl.title("Sinkhorn (reg=0.05)") pl.subplot(2, 3, 2) pl.imshow(list_P_Sin[1], interpolation="nearest", cmap="gray_r") pl.axis("off") pl.title("Sinkhorn (reg=0.005)") pl.subplot(2, 3, 3) pl.imshow(list_P_Sin[2], interpolation="nearest", cmap="gray_r") pl.axis("off") pl.title("Sinkhorn (reg=0.001)") pl.show() pl.subplot(2, 3, 4) pl.imshow(list_P_LR[0], interpolation="nearest", cmap="gray_r") pl.axis("off") pl.title("Low rank (rank=3)") pl.subplot(2, 3, 5) pl.imshow(list_P_LR[1], interpolation="nearest", cmap="gray_r") pl.axis("off") pl.title("Low rank (rank=10)") pl.subplot(2, 3, 6) pl.imshow(list_P_LR[2], interpolation="nearest", cmap="gray_r") pl.axis("off") pl.title("Low rank (rank=50)") pl.tight_layout() python-pot-0.9.7+dfsg/examples/lowrank/plot_nystroem_approximation.py000066400000000000000000000102501525521106200263210ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ============================ Nyström approximation for OT ============================ Shows how to use Nyström kernel approximation for approximating the Sinkhorn algorithm in linear time. """ # Author: Titouan Vayer # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np from ot.lowrank import kernel_nystroem, sinkhorn_low_rank_kernel from ot.bregman import empirical_sinkhorn_nystroem import math import ot import matplotlib.pyplot as plt from matplotlib.colors import LogNorm ############################################################################## # Generate data # ------------- # %% offset = 1 n_samples_per_blob = 500 # We use 2D ''blobs'' data random_state = 42 std = 0.2 # standard deviation np.random.seed(random_state) centers = np.array( [ [-offset, -offset], # Class 0 - blob 1 [-offset, offset], # Class 0 - blob 2 [offset, -offset], # Class 1 - blob 1 [offset, offset], # Class 1 - blob 2 ] ) X_list = [] y_list = [] for i, center in enumerate(centers): blob_points = np.random.randn(n_samples_per_blob, 2) * std + center label = 0 if i < 2 else 1 X_list.append(blob_points) y_list.append(np.full(n_samples_per_blob, label)) X = np.vstack(X_list) y = np.concatenate(y_list) Xs = X[y == 0] # source data Xt = X[y == 1] # target data ############################################################################## # Plot data # --------- # %% plt.scatter(Xs[:, 0], Xs[:, 1], label="Source") plt.scatter(Xt[:, 0], Xt[:, 1], label="Target") plt.legend() ############################################################################## # Compute the Nyström approximation of the Gaussian kernel # -------------------------------------------------------- # %% reg = 5.0 # proportional to the std of the Gaussian kernel anchors = 10 # number of anchor points for the Nyström approximation ot.tic() left_factor, right_factor = kernel_nystroem( Xs, Xt, anchors=anchors, sigma=math.sqrt(reg / 2.0), random_state=random_state ) ot.toc() ############################################################################## # Use this approximation in a Sinkhorn algorithm with low rank kernel. # Each matrix/vector product in the Sinkhorn is accelerated # since :math:`Kv = K_1 (K_2^\top v)` can be computed in :math:`O(nr)` time # instead of :math:`O(n^2)` # %% numItermax = 1000 stopThr = 1e-7 verbose = True a, b = None, None warn = True warmstart = None ot.tic() u, v, dict_log = sinkhorn_low_rank_kernel( K1=left_factor, K2=right_factor, a=a, b=b, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=True, warn=warn, warmstart=warmstart, ) ot.toc() ############################################################################## # Compare with Sinkhorn # --------------------- # %% M = ot.dist(Xs, Xt) ot.tic() G, log_ = ot.sinkhorn( a=[], b=[], M=M, reg=reg, numItermax=numItermax, verbose=verbose, log=True, warn=warn, warmstart=warmstart, ) ot.toc() ############################################################################## # Use directly ot.bregman.empirical_sinkhorn_nystroem # -------------------------------------------------- # %% ot.tic() G_nys = empirical_sinkhorn_nystroem( Xs, Xt, anchors=anchors, reg=reg, numItermax=numItermax, verbose=True, random_state=random_state, )[:] ot.toc() # %% ot.tic() G_sinkh = ot.bregman.empirical_sinkhorn( Xs, Xt, reg=reg, numIterMax=numItermax, verbose=True ) ot.toc() ############################################################################## # Compare OT plans # ---------------- fig, ax = plt.subplots(1, 2, figsize=(10, 4), constrained_layout=True) vmin = min(G_sinkh.min(), G_nys.min()) vmax = max(G_sinkh.max(), G_nys.max()) norm = LogNorm(vmin=vmin, vmax=vmax) im0 = ax[0].imshow(G_sinkh, norm=norm, cmap="coolwarm") im1 = ax[1].imshow(G_nys, norm=norm, cmap="coolwarm") cbar = fig.colorbar(im1, ax=ax, orientation="vertical", fraction=0.046, pad=0.04) ax[0].set_title("OT plan Sinkhorn") ax[1].set_title("OT plan Nyström Sinkhorn") for a in ax: a.set_xticks([]) a.set_yticks([]) plt.show() python-pot-0.9.7+dfsg/examples/others/000077500000000000000000000000001525521106200177105ustar00rootroot00000000000000python-pot-0.9.7+dfsg/examples/others/index.rst000066400000000000000000000000461525521106200215510ustar00rootroot00000000000000 Other OT problems -----------------python-pot-0.9.7+dfsg/examples/others/plot_COOT.py000066400000000000000000000054231525521106200220700ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" =================================================== Row and column alignments with CO-Optimal Transport =================================================== This example is designed to show how to use the CO-Optimal Transport [47]_ in POT. CO-Optimal Transport allows to calculate the distance between two **arbitrary-size** matrices, and to align their rows and columns. In this example, we consider two random matrices :math:`X_1` and :math:`X_2` defined by :math:`(X_1)_{i,j} = \cos(\frac{i}{n_1} \pi) + \cos(\frac{j}{d_1} \pi) + \sigma \mathcal N(0,1)` and :math:`(X_2)_{i,j} = \cos(\frac{i}{n_2} \pi) + \cos(\frac{j}{d_2} \pi) + \sigma \mathcal N(0,1)`. .. [49] Redko, I., Vayer, T., Flamary, R., and Courty, N. (2020). `CO-Optimal Transport `_. Advances in Neural Information Processing Systems, 33. """ # Author: Remi Flamary # Quang Huy Tran # License: MIT License # sphinx_gallery_thumbnail_number = 2 from matplotlib.patches import ConnectionPatch import matplotlib.pylab as pl import numpy as np from ot.coot import co_optimal_transport as coot from ot.coot import co_optimal_transport2 as coot2 # %% # Generating two random matrices n1 = 20 n2 = 10 d1 = 16 d2 = 8 sigma = 0.2 X1 = ( np.cos(np.arange(n1) * np.pi / n1)[:, None] + np.cos(np.arange(d1) * np.pi / d1)[None, :] + sigma * np.random.randn(n1, d1) ) X2 = ( np.cos(np.arange(n2) * np.pi / n2)[:, None] + np.cos(np.arange(d2) * np.pi / d2)[None, :] + sigma * np.random.randn(n2, d2) ) # %% # Visualizing the matrices pl.figure(1, (8, 5)) pl.subplot(1, 2, 1) pl.imshow(X1) pl.title("$X_1$") pl.subplot(1, 2, 2) pl.imshow(X2) pl.title("$X_2$") pl.tight_layout() # %% # Visualizing the alignments of rows and columns, and calculating the CO-Optimal Transport distance pi_sample, pi_feature, log = coot(X1, X2, log=True, verbose=True) coot_distance = coot2(X1, X2) print("CO-Optimal Transport distance = {:.5f}".format(coot_distance)) fig = pl.figure(4, (9, 7)) pl.clf() ax1 = pl.subplot(2, 2, 3) pl.imshow(X1) pl.xlabel("$X_1$") ax2 = pl.subplot(2, 2, 2) ax2.yaxis.tick_right() pl.imshow(np.transpose(X2)) pl.title("Transpose($X_2$)") ax2.xaxis.tick_top() for i in range(n1): j = np.argmax(pi_sample[i, :]) xyA = (d1 - 0.5, i) xyB = (j, d2 - 0.5) con = ConnectionPatch( xyA=xyA, xyB=xyB, coordsA=ax1.transData, coordsB=ax2.transData, color="black" ) fig.add_artist(con) for i in range(d1): j = np.argmax(pi_feature[i, :]) xyA = (i, -0.5) xyB = (-0.5, j) con = ConnectionPatch( xyA=xyA, xyB=xyB, coordsA=ax1.transData, coordsB=ax2.transData, color="blue" ) fig.add_artist(con) python-pot-0.9.7+dfsg/examples/others/plot_EWCA.py000066400000000000000000000110731525521106200220410ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ======================================= Entropic Wasserstein Component Analysis ======================================= .. note:: Example added in release: 0.9.1. This example illustrates the use of EWCA as proposed in [52]. [52] Collas, A., Vayer, T., Flamary, F., & Breloy, A. (2023). Entropic Wasserstein Component Analysis. """ # Author: Antoine Collas # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pylab as pl from ot.dr import ewca from sklearn.datasets import make_blobs from matplotlib import ticker as mticker import matplotlib.patches as patches import matplotlib ############################################################################## # Generate data # ------------- n_samples = 20 esp = 0.8 centers = np.array([[esp, esp], [-esp, -esp]]) cluster_std = 0.4 rng = np.random.RandomState(42) X, y = make_blobs( n_samples=n_samples, n_features=2, centers=centers, cluster_std=cluster_std, shuffle=False, random_state=rng, ) X = X - X.mean(0) ############################################################################## # Plot data # ------------- fig = pl.figure(figsize=(4, 4)) cmap = matplotlib.colormaps.get_cmap("tab10") pl.scatter( X[: n_samples // 2, 0], X[: n_samples // 2, 1], color=[cmap(y[i] + 1) for i in range(n_samples // 2)], alpha=0.4, label="Class 1", zorder=30, s=50, ) pl.scatter( X[n_samples // 2 :, 0], X[n_samples // 2 :, 1], color=[cmap(y[i] + 1) for i in range(n_samples // 2, n_samples)], alpha=0.4, label="Class 2", zorder=30, s=50, ) x_y_lim = 2.5 fs = 15 pl.xlim(-x_y_lim, x_y_lim) pl.xticks([]) pl.ylim(-x_y_lim, x_y_lim) pl.yticks([]) pl.legend(fontsize=fs) pl.title("Data", fontsize=fs) pl.tight_layout() ############################################################################## # Compute EWCA # ------------- pi, U = ewca(X, k=2, reg=0.5) ############################################################################## # Plot data, first component, and projected data # ------------- fig = pl.figure(figsize=(4, 4)) scale = 3 u = U[:, 0] pl.plot( [scale * u[0], -scale * u[0]], [scale * u[1], -scale * u[1]], color="grey", linestyle="--", lw=3, alpha=0.3, label=r"$\mathbf{U}$", ) X1 = X @ u[:, None] @ u[:, None].T for i in range(n_samples): for j in range(n_samples): v = pi[i, j] / pi.max() if v >= 0.15 or (i, j) == (n_samples - 1, n_samples - 1): pl.plot( [X[i, 0], X1[j, 0]], [X[i, 1], X1[j, 1]], alpha=v, linestyle="-", c="C0", label=r"$\pi_{ij}$" if (i, j) == (n_samples - 1, n_samples - 1) else None, ) pl.scatter( X[:, 0], X[:, 1], color=[cmap(y[i] + 1) for i in range(n_samples)], alpha=0.4, label=r"$\mathbf{x}_i$", zorder=30, s=50, ) pl.scatter( X1[:, 0], X1[:, 1], color=[cmap(y[i] + 1) for i in range(n_samples)], alpha=0.9, s=50, marker="+", label=r"$\mathbf{U}\mathbf{U}^{\top}\mathbf{x}_i$", zorder=30, ) pl.title("Data and projections", fontsize=fs) pl.xlim(-x_y_lim, x_y_lim) pl.xticks([]) pl.ylim(-x_y_lim, x_y_lim) pl.yticks([]) pl.legend(fontsize=fs, loc="upper left") pl.tight_layout() ############################################################################## # Plot transport plan # ------------- fig = pl.figure(figsize=(5, 5)) norm = matplotlib.colors.PowerNorm(0.5, vmin=0, vmax=100) im = pl.imshow(n_samples * pi * 100, cmap=pl.cm.Blues, norm=norm, aspect="auto") cb = fig.colorbar(im, orientation="vertical", shrink=0.8) ticks_loc = cb.ax.get_yticks().tolist() cb.ax.yaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) cb.ax.set_yticklabels([f"{int(i)}%" for i in cb.get_ticks()]) cb.ax.tick_params(labelsize=fs) for i, class_ in enumerate(np.sort(np.unique(y))): indices = y == class_ idx_min = np.min(np.arange(len(y))[indices]) idx_max = np.max(np.arange(len(y))[indices]) width = idx_max - idx_min + 1 rect = patches.Rectangle( (idx_min - 0.5, idx_min - 0.5), width, width, linewidth=1, edgecolor="r", facecolor="none", ) pl.gca().add_patch(rect) pl.title("OT plan", fontsize=fs) pl.ylabel(r"($\mathbf{x}_1, \cdots, \mathbf{x}_n$)") x_label = r"($\mathbf{U}\mathbf{U}^{\top}\mathbf{x}_1, \cdots," x_label += r"\mathbf{U}\mathbf{U}^{\top}\mathbf{x}_n$)" pl.xlabel(x_label) pl.tight_layout() pl.axis("scaled") pl.show() python-pot-0.9.7+dfsg/examples/others/plot_SSNB.py000066400000000000000000000117511525521106200220720ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ===================================================== Smooth and Strongly Convex Nearest Brenier Potentials ===================================================== .. note:: Example added in release: 0.9.2. This example is designed to show how to use SSNB [58] in POT. SSNB computes an l-strongly convex potential :math:`\varphi` with an L-Lipschitz gradient such that :math:`\nabla \varphi \# \mu \approx \nu`. This regularity can be enforced only on the components of a partition of the ambient space, which is a relaxation compared to imposing global regularity. In this example, we consider a source measure :math:`\mu_s` which is the uniform measure on the unit square in :math:`\mathbb{R}^2`, and the target measure :math:`\mu_t` which is the image of :math:`\mu_x` by :math:`T(x_1, x_2) = (x_1 + 2\mathrm{sign}(x_2), 2 * x_2)`. The map :math:`T` is non-smooth, and we wish to approximate it using a "Brenier-style" map :math:`\nabla \varphi` which is regular on the partition :math:`\lbrace x_1 <=0, x_1>0\rbrace`, which is well adapted to this particular dataset. We represent the gradients of the "bounding potentials" :math:`\varphi_l, \varphi_u` (from [59], Theorem 3.14), which bound any SSNB potential which is optimal in the sense of [58], Definition 1: .. math:: \varphi \in \mathrm{argmin}_{\varphi \in \mathcal{F}}\ \mathrm{W}_2(\nabla \varphi \#\mu_s, \mu_t), where :math:`\mathcal{F}` is the space functions that are on every set :math:`E_k` l-strongly convex with an L-Lipschitz gradient, given :math:`(E_k)_{k \in [K]}` a partition of the ambient source space. We perform the optimisation on a low amount of fitting samples and with few iterations, since solving the SSNB problem is quite computationally expensive. THIS EXAMPLE REQUIRES CVXPY .. [58] François-Pierre Paty, Alexandre d’Aspremont, and Marco Cuturi. Regularity as regularization: Smooth and strongly convex brenier potentials in optimal transport. In International Conference on Artificial Intelligence and Statistics, pages 1222–1232. PMLR, 2020. .. [59] Adrien B Taylor. Convex interpolation and performance estimation of first-order methods for convex optimization. PhD thesis, Catholic University of Louvain, Louvain-la-Neuve, Belgium, 2017. """ # Author: Eloi Tanguy # License: MIT License # sphinx_gallery_thumbnail_number = 3 import matplotlib.pyplot as plt import numpy as np import ot # %% # Generating the fitting data n_fitting_samples = 30 rng = np.random.RandomState(seed=0) Xs = rng.uniform(-1, 1, size=(n_fitting_samples, 2)) Xs_classes = (Xs[:, 0] < 0).astype(int) Xt = np.stack([Xs[:, 0] + 2 * np.sign(Xs[:, 0]), 2 * Xs[:, 1]], axis=-1) plt.scatter( Xs[Xs_classes == 0, 0], Xs[Xs_classes == 0, 1], c="blue", label="source class 0" ) plt.scatter( Xs[Xs_classes == 1, 0], Xs[Xs_classes == 1, 1], c="dodgerblue", label="source class 1", ) plt.scatter(Xt[:, 0], Xt[:, 1], c="red", label="target") plt.axis("equal") plt.title("Splitting sphere dataset") plt.legend(loc="upper right") plt.show() # %% # Fitting the Nearest Brenier Potential L = 3 # need L > 2 to allow the 2*y term, default is 1.4 phi, G = ot.mapping.nearest_brenier_potential_fit( Xs, Xt, Xs_classes, its=10, init_method="barycentric", gradient_lipschitz_constant=L ) # %% # Plotting the images of the source data plt.clf() plt.scatter(Xs[:, 0], Xs[:, 1], c="dodgerblue", label="source") plt.scatter(Xt[:, 0], Xt[:, 1], c="red", label="target") for i in range(n_fitting_samples): plt.plot([Xs[i, 0], G[i, 0]], [Xs[i, 1], G[i, 1]], color="black", alpha=0.5) plt.title("Images of in-data source samples by the fitted SSNB") plt.legend(loc="upper right") plt.axis("equal") plt.show() # %% # Computing the predictions (images by nabla phi) for random samples of the source distribution n_predict_samples = 50 Ys = rng.uniform(-1, 1, size=(n_predict_samples, 2)) Ys_classes = (Ys[:, 0] < 0).astype(int) phi_lu, G_lu = ot.mapping.nearest_brenier_potential_predict_bounds( Xs, phi, G, Ys, Xs_classes, Ys_classes, gradient_lipschitz_constant=L ) # %% # Plot predictions for the gradient of the lower-bounding potential plt.clf() plt.scatter(Xs[:, 0], Xs[:, 1], c="dodgerblue", label="source") plt.scatter(Xt[:, 0], Xt[:, 1], c="red", label="target") for i in range(n_predict_samples): plt.plot( [Ys[i, 0], G_lu[0, i, 0]], [Ys[i, 1], G_lu[0, i, 1]], color="black", alpha=0.5 ) plt.title("Images of new source samples by $\\nabla \\varphi_l$") plt.legend(loc="upper right") plt.axis("equal") plt.show() # %% # Plot predictions for the gradient of the upper-bounding potential plt.clf() plt.scatter(Xs[:, 0], Xs[:, 1], c="dodgerblue", label="source") plt.scatter(Xt[:, 0], Xt[:, 1], c="red", label="target") for i in range(n_predict_samples): plt.plot( [Ys[i, 0], G_lu[1, i, 0]], [Ys[i, 1], G_lu[1, i, 1]], color="black", alpha=0.5 ) plt.title("Images of new source samples by $\\nabla \\varphi_u$") plt.legend(loc="upper right") plt.axis("equal") plt.show() python-pot-0.9.7+dfsg/examples/others/plot_WDA.py000066400000000000000000000063161525521106200217410ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ================================= Wasserstein Discriminant Analysis ================================= .. note:: Example added in release: 0.3.0. This example illustrate the use of WDA as proposed in [11]. [11] Flamary, R., Cuturi, M., Courty, N., & Rakotomamonjy, A. (2016). Wasserstein Discriminant Analysis. """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pylab as pl from ot.dr import wda, fda ############################################################################## # Generate data # ------------- # %% parameters n = 1000 # nb samples in source and target datasets nz = 0.2 np.random.seed(1) # generate circle dataset t = np.random.rand(n) * 2 * np.pi ys = np.floor((np.arange(n) * 1.0 / n * 3)) + 1 xs = np.concatenate((np.cos(t).reshape((-1, 1)), np.sin(t).reshape((-1, 1))), 1) xs = xs * ys.reshape(-1, 1) + nz * np.random.randn(n, 2) t = np.random.rand(n) * 2 * np.pi yt = np.floor((np.arange(n) * 1.0 / n * 3)) + 1 xt = np.concatenate((np.cos(t).reshape((-1, 1)), np.sin(t).reshape((-1, 1))), 1) xt = xt * yt.reshape(-1, 1) + nz * np.random.randn(n, 2) nbnoise = 8 xs = np.hstack((xs, np.random.randn(n, nbnoise))) xt = np.hstack((xt, np.random.randn(n, nbnoise))) ############################################################################## # Plot data # --------- # %% plot samples pl.figure(1, figsize=(6.4, 3.5)) pl.subplot(1, 2, 1) pl.scatter(xt[:, 0], xt[:, 1], c=ys, marker="+", label="Source samples") pl.legend(loc=0) pl.title("Discriminant dimensions") pl.subplot(1, 2, 2) pl.scatter(xt[:, 2], xt[:, 3], c=ys, marker="+", label="Source samples") pl.legend(loc=0) pl.title("Other dimensions") pl.tight_layout() ############################################################################## # Compute Fisher Discriminant Analysis # ------------------------------------ # %% Compute FDA p = 2 Pfda, projfda = fda(xs, ys, p) ############################################################################## # Compute Wasserstein Discriminant Analysis # ----------------------------------------- # %% Compute WDA p = 2 reg = 1e0 k = 10 maxiter = 100 P0 = np.random.randn(xs.shape[1], p) P0 /= np.sqrt(np.sum(P0**2, 0, keepdims=True)) Pwda, projwda = wda(xs, ys, p, reg, k, maxiter=maxiter, P0=P0) ############################################################################## # Plot 2D projections # ------------------- # %% plot samples xsp = projfda(xs) xtp = projfda(xt) xspw = projwda(xs) xtpw = projwda(xt) pl.figure(2) pl.subplot(2, 2, 1) pl.scatter(xsp[:, 0], xsp[:, 1], c=ys, marker="+", label="Projected samples") pl.legend(loc=0) pl.title("Projected training samples FDA") pl.subplot(2, 2, 2) pl.scatter(xtp[:, 0], xtp[:, 1], c=ys, marker="+", label="Projected samples") pl.legend(loc=0) pl.title("Projected test samples FDA") pl.subplot(2, 2, 3) pl.scatter(xspw[:, 0], xspw[:, 1], c=ys, marker="+", label="Projected samples") pl.legend(loc=0) pl.title("Projected training samples WDA") pl.subplot(2, 2, 4) pl.scatter(xtpw[:, 0], xtpw[:, 1], c=ys, marker="+", label="Projected samples") pl.legend(loc=0) pl.title("Projected test samples WDA") pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/others/plot_WeakOT_VS_OT.py000066400000000000000000000047031525521106200234700ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ==================================================== Weak Optimal Transport VS exact Optimal Transport ==================================================== .. note:: Example added in release: 0.8.2. Illustration of 2D optimal transport between distributions that are weighted sum of Diracs. The OT matrix is plotted with the samples. """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 4 import numpy as np import matplotlib.pylab as pl import ot import ot.plot ############################################################################## # Generate data an plot it # ------------------------ # %% parameters and data generation n = 50 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([4, 4]) cov_t = np.array([[1, -0.8], [-0.8, 1]]) xs = ot.datasets.make_2D_samples_gauss(n, mu_s, cov_s) xt = ot.datasets.make_2D_samples_gauss(n, mu_t, cov_t) a, b = ot.unif(n), ot.unif(n) # uniform distribution on samples # loss matrix M = ot.dist(xs, xt) M /= M.max() # %% plot samples pl.figure(1) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.legend(loc=0) pl.title("Source and target distributions") pl.figure(2) pl.imshow(M, interpolation="nearest") pl.title("Cost matrix M") ############################################################################## # Compute Weak OT and exact OT solutions # -------------------------------------- # %% EMD G0 = ot.emd(a, b, M) # %% Weak OT Gweak = ot.weak_optimal_transport(xs, xt, a, b) ############################################################################## # Plot weak OT and exact OT solutions # -------------------------------------- pl.figure(3, (8, 5)) pl.subplot(1, 2, 1) pl.imshow(G0, interpolation="nearest") pl.title("OT matrix") pl.subplot(1, 2, 2) pl.imshow(Gweak, interpolation="nearest") pl.title("Weak OT matrix") pl.figure(4, (8, 5)) pl.subplot(1, 2, 1) ot.plot.plot2D_samples_mat(xs, xt, G0, c=[0.5, 0.5, 1]) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.title("OT matrix with samples") pl.subplot(1, 2, 2) ot.plot.plot2D_samples_mat(xs, xt, Gweak, c=[0.5, 0.5, 1]) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.title("Weak OT matrix with samples") python-pot-0.9.7+dfsg/examples/others/plot_dmmot.py000066400000000000000000000106601525521106200224430ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" =============================================================================== Computing 1-dimensional Barycenters via d-MMOT =============================================================================== .. note:: Example added in release: 0.9.1. When the cost is discretized (Monge), the d-MMOT solver can more quickly compute and minimize the distance between many distributions without the need for intermediate barycenter computations. This example compares the time to identify, and the quality of, solutions for the d-MMOT problem using a primal/dual algorithm and classical LP barycenter approaches. """ # Author: Ronak Mehta # Xizheng Yu # # License: MIT License # sphinx_gallery_thumbnail_number = 2 # %% # Generating 2 distributions # ----- import numpy as np import matplotlib.pyplot as pl import ot np.random.seed(0) n = 100 d = 2 # Gaussian distributions a1 = ot.datasets.make_1D_gauss(n, m=20, s=5) # m=mean, s=std a2 = ot.datasets.make_1D_gauss(n, m=60, s=8) A = np.vstack((a1, a2)).T x = np.arange(n, dtype=np.float64) M = ot.utils.dist(x.reshape((n, 1)), metric="minkowski") pl.figure(1, figsize=(6.4, 3)) pl.plot(x, a1, "b", label="Source distribution") pl.plot(x, a2, "r", label="Target distribution") pl.legend() # %% # Minimize the distances among distributions, identify the Barycenter # ----- # The objective being minimized is different for both methods, so the objective # values cannot be compared. # L2 Iteration weights = np.ones(d) / d l2_bary = A.dot(weights) print("LP Iterations:") weights = np.ones(d) / d lp_bary, lp_log = ot.lp.barycenter( A, M, weights, solver="interior-point", verbose=False, log=True ) print("Time\t: ", ot.toc("")) print("Obj\t: ", lp_log["fun"]) print("") print("Discrete MMOT Algorithm:") ot.tic() barys, log = ot.lp.dmmot_monge_1dgrid_optimize( A, niters=4000, lr_init=1e-5, lr_decay=0.997, log=True ) dmmot_obj = log["primal objective"] print("Time\t: ", ot.toc("")) print("Obj\t: ", dmmot_obj) # %% # Compare Barycenters in both methods # ----- pl.figure(1, figsize=(6.4, 3)) for i in range(len(barys)): if i == 0: pl.plot(x, barys[i], "g-*", label="Discrete MMOT") else: continue # pl.plot(x, barys[i], 'g-*') pl.plot(x, lp_bary, label="LP Barycenter") pl.plot(x, l2_bary, label="L2 Barycenter") pl.plot(x, a1, "b", label="Source distribution") pl.plot(x, a2, "r", label="Target distribution") pl.title("Monge Cost: Barycenters from LP Solver and dmmot solver") pl.legend() # %% # More than 2 distributions # -------------------------------------------------- # Generate 7 pseudorandom gaussian distributions with 50 bins. n = 50 # nb bins d = 7 vecsize = n * d data = [] for i in range(d): m = n * (0.5 * np.random.rand(1)) * float(np.random.randint(2) + 1) a = ot.datasets.make_1D_gauss(n, m=m, s=5) data.append(a) x = np.arange(n, dtype=np.float64) M = ot.utils.dist(x.reshape((n, 1)), metric="minkowski") A = np.vstack(data).T pl.figure(1, figsize=(6.4, 3)) for i in range(len(data)): pl.plot(x, data[i]) pl.title("Distributions") pl.legend() # %% # Minimizing Distances Among Many Distributions # --------------- # The objective being minimized is different for both methods, so the objective # values cannot be compared. # Perform gradient descent optimization using the d-MMOT method. barys = ot.lp.dmmot_monge_1dgrid_optimize(A, niters=3000, lr_init=1e-4, lr_decay=0.997) # after minimization, any distribution can be used as a estimate of barycenter. bary = barys[0] # Compute 1D Wasserstein barycenter using the L2/LP method weights = ot.unif(d) l2_bary = A.dot(weights) lp_bary, bary_log = ot.lp.barycenter( A, M, weights, solver="interior-point", verbose=False, log=True ) # %% # Compare Barycenters in both methods # --------- pl.figure(1, figsize=(6.4, 3)) pl.plot(x, bary, "g-*", label="Discrete MMOT") pl.plot(x, l2_bary, "k", label="L2 Barycenter") pl.plot(x, lp_bary, "k-", label="LP Wasserstein") pl.title("Barycenters") pl.legend() # %% # Compare with original distributions # --------- pl.figure(1, figsize=(6.4, 3)) for i in range(len(data)): pl.plot(x, data[i]) for i in range(len(barys)): if i == 0: pl.plot(x, barys[i], "g-*", label="Discrete MMOT") else: continue # pl.plot(x, barys[i], 'g') pl.plot(x, l2_bary, "k^", label="L2") pl.plot(x, lp_bary, "o", color="grey", label="LP") pl.title("Barycenters") pl.legend() pl.show() # %% python-pot-0.9.7+dfsg/examples/others/plot_logo.py000066400000000000000000000067101525521106200222640ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ======================= Logo of the POT toolbox ======================= .. note:: Example added in release: 0.8.2. In this example we plot the logo of the POT toolbox. This logo is that it is done 100% in Python and generated using matplotlib and plotting the solution of the EMD solver from POT. """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 1 # %% Load modules import numpy as np import matplotlib.pyplot as pl import ot # %% # Data for logo # ------------- # Letter P p1 = np.array( [ [0, 6.0], [0, 5], [0, 4], [0, 3], [0, 2], [0, 1], ] ) p2 = np.array( [ [1.5, 6], [2, 4], [2, 5], [1.5, 3], [0.5, 2], [0.5, 1], ] ) # Letter O o1 = np.array( [ [0, 6.0], [-1, 5], [-1.5, 4], [-1.5, 3], [-1, 2], [0, 1], ] ) o2 = np.array( [ [1, 6.0], [2, 5], [2.5, 4], [2.5, 3], [2, 2], [1, 1], ] ) # Scaling and translation for letter O o1[:, 0] += 6.4 o2[:, 0] += 6.4 o1[:, 0] *= 0.6 o2[:, 0] *= 0.6 # Letter T t1 = np.array( [ [-1, 6.0], [-1, 5], [0, 4], [0, 3], [0, 2], [0, 1], ] ) t2 = np.array( [ [1.5, 6.0], [1.5, 5], [0.5, 4], [0.5, 3], [0.5, 2], [0.5, 1], ] ) # Translating the T t1[:, 0] += 7.1 t2[:, 0] += 7.1 # Concatenate all letters x1 = np.concatenate((p1, o1, t1), axis=0) x2 = np.concatenate((p2, o2, t2), axis=0) # Horizontal and vertical scaling sx = 1.0 sy = 0.5 x1[:, 0] *= sx x1[:, 1] *= sy x2[:, 0] *= sx x2[:, 1] *= sy # %% # Plot the logo (clear background) # -------------------------------- # Solve OT problem between the points M = ot.dist(x1, x2, metric="euclidean") T = ot.emd([], [], M) pl.figure(1, (3.5, 1.1)) pl.clf() # plot the OT plan for i in range(M.shape[0]): for j in range(M.shape[1]): if T[i, j] > 1e-8: pl.plot( [x1[i, 0], x2[j, 0]], [x1[i, 1], x2[j, 1]], color="k", alpha=0.6, linewidth=3, zorder=1, ) # plot the samples pl.plot(x1[:, 0], x1[:, 1], "o", markerfacecolor="C3", markeredgecolor="k") pl.plot(x2[:, 0], x2[:, 1], "o", markerfacecolor="b", markeredgecolor="k") pl.axis("equal") pl.axis("off") # Save logo file # pl.savefig('logo.svg', dpi=150, transparent=True, bbox_inches='tight') # pl.savefig('logo.png', dpi=150, transparent=True, bbox_inches='tight') # %% # Plot the logo (dark background) # -------------------------------- pl.figure(2, (3.5, 1.1), facecolor="darkgray") pl.clf() # plot the OT plan for i in range(M.shape[0]): for j in range(M.shape[1]): if T[i, j] > 1e-8: pl.plot( [x1[i, 0], x2[j, 0]], [x1[i, 1], x2[j, 1]], color="w", alpha=0.8, linewidth=3, zorder=1, ) # plot the samples pl.plot(x1[:, 0], x1[:, 1], "o", markerfacecolor="w", markeredgecolor="w") pl.plot(x2[:, 0], x2[:, 1], "o", markerfacecolor="w", markeredgecolor="w") pl.axis("equal") pl.axis("off") # Save logo file # pl.savefig('logo_dark.svg', dpi=150, transparent=True, bbox_inches='tight') # pl.savefig('logo_dark.png', dpi=150, transparent=True, bbox_inches='tight') python-pot-0.9.7+dfsg/examples/others/plot_outlier_detection_with_COOT_and_unbalanced_COOT.py000066400000000000000000000164321525521106200325500ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ====================================================================================================================================== Detecting outliers by learning sample marginal distribution with CO-Optimal Transport and by using unbalanced Co-Optimal Transport ====================================================================================================================================== .. note:: Example added in release: 0.9.5. In this example, we consider two point clouds living in different Euclidean spaces, where the outliers are artificially injected into the target data. We illustrate two methods which allow to filter out these outliers. The first method requires learning the sample marginal distribution which minimizes the CO-Optimal Transport distance [49] between two input spaces. More precisely, given a source data :math:`(X, \mu_x^{(s)}, \mu_x^{(f)})` and a target matrix :math:`Y` associated with a fixed histogram on features :math:`\mu_y^{(f)}`, we want to solve the following problem .. math:: \min_{\mu_y^{(s)} \in \Delta} \text{COOT}\left( (X, \mu_x^{(s)}, \mu_x^{(f)}), (Y, \mu_y^{(s)}, \mu_y^{(f)}) \right) where :math:`\Delta` is the probability simplex. This minimization is done with a simple projected gradient descent in PyTorch. We use the automatic backend of POT that allows us to compute the CO-Optimal Transport distance with :func:`ot.coot.co_optimal_transport2` with differentiable losses. The second method simply requires direct application of unbalanced Co-Optimal Transport [71]. More precisely, it is enough to use the sample and feature coupling from solving .. math:: \text{UCOOT}\left( (X, \mu_x^{(s)}, \mu_x^{(f)}), (Y, \mu_y^{(s)}, \mu_y^{(f)}) \right) where all the marginal distributions are uniform. .. [49] Redko, I., Vayer, T., Flamary, R., and Courty, N. (2020). `CO-Optimal Transport `_. Advances in Neural Information Processing Systems, 33. .. [71] H. Tran, H. Janati, N. Courty, R. Flamary, I. Redko, P. Demetci & R. Singh (2023). [Unbalanced Co-Optimal Transport](https://dl.acm.org/doi/10.1609/aaai.v37i8.26193). AAAI Conference on Artificial Intelligence. """ # Author: Remi Flamary # Quang Huy Tran # License: MIT License # sphinx_gallery_thumbnail_number = 4 from matplotlib.patches import ConnectionPatch import torch import numpy as np import matplotlib.pyplot as pl import ot from ot.coot import co_optimal_transport as coot from ot.coot import co_optimal_transport2 as coot2 from ot.gromov._unbalanced import unbalanced_co_optimal_transport # %% # Generate data # ------------- # The source and clean target matrices are generated by # :math:`X_{i,j} = \cos(\frac{i}{n_1} \pi) + \cos(\frac{j}{d_1} \pi)` and # :math:`Y_{i,j} = \cos(\frac{i}{n_2} \pi) + \cos(\frac{j}{d_2} \pi)`. # The target matrix is then contaminated by adding 5 row outliers. # Intuitively, we expect that the estimated sample distribution should ignore these outliers, # i.e. their weights should be zero. np.random.seed(182) n1, d1 = 20, 16 n2, d2 = 10, 8 n = 15 X = ( torch.cos(torch.arange(n1) * torch.pi / n1)[:, None] + torch.cos(torch.arange(d1) * torch.pi / d1)[None, :] ) # Generate clean target data mixed with outliers Y_noisy = torch.randn((n, d2)) * 10.0 Y_noisy[:n2, :] = ( torch.cos(torch.arange(n2) * torch.pi / n2)[:, None] + torch.cos(torch.arange(d2) * torch.pi / d2)[None, :] ) Y = Y_noisy[:n2, :] X, Y_noisy, Y = X.double(), Y_noisy.double(), Y.double() fig, axes = pl.subplots(nrows=1, ncols=3, figsize=(12, 5)) axes[0].imshow(X, vmin=-2, vmax=2) axes[0].set_title("$X$") axes[1].imshow(Y, vmin=-2, vmax=2) axes[1].set_title("Clean $Y$") axes[2].imshow(Y_noisy, vmin=-2, vmax=2) axes[2].set_title("Noisy $Y$") pl.tight_layout() # %% # Optimize the COOT distance with respect to the sample marginal distribution # --------------------------------------------------------------------------- losses = [] lr = 1e-3 niter = 1000 b = torch.tensor(ot.unif(n), requires_grad=True) for i in range(niter): loss = coot2(X, Y_noisy, wy_samp=b, log=False, verbose=False) losses.append(float(loss)) loss.backward() with torch.no_grad(): b -= lr * b.grad # gradient step b[:] = ot.utils.proj_simplex(b) # projection on the simplex b.grad.zero_() # Estimated sample marginal distribution and training loss curve pl.plot(losses[10:]) pl.title("CO-Optimal Transport distance") print(f"Marginal distribution = {b.detach().numpy()}") # %% # Visualizing the row and column alignments with the estimated sample marginal distribution # ----------------------------------------------------------------------------------------- # # Clearly, the learned marginal distribution completely and successfully ignores the 5 outliers. X, Y_noisy = X.numpy(), Y_noisy.numpy() b = b.detach().numpy() pi_sample, pi_feature = coot(X, Y_noisy, wy_samp=b, log=False, verbose=True) fig = pl.figure(4, (9, 7)) pl.clf() ax1 = pl.subplot(2, 2, 3) pl.imshow(X, vmin=-2, vmax=2) pl.xlabel("$X$") ax2 = pl.subplot(2, 2, 2) ax2.yaxis.tick_right() pl.imshow(np.transpose(Y_noisy), vmin=-2, vmax=2) pl.title("Transpose(Noisy $Y$)") ax2.xaxis.tick_top() for i in range(n1): j = np.argmax(pi_sample[i, :]) xyA = (d1 - 0.5, i) xyB = (j, d2 - 0.5) con = ConnectionPatch( xyA=xyA, xyB=xyB, coordsA=ax1.transData, coordsB=ax2.transData, color="black" ) fig.add_artist(con) for i in range(d1): j = np.argmax(pi_feature[i, :]) xyA = (i, -0.5) xyB = (-0.5, j) con = ConnectionPatch( xyA=xyA, xyB=xyB, coordsA=ax1.transData, coordsB=ax2.transData, color="blue" ) fig.add_artist(con) # %% # Now, let see if we can use unbalanced Co-Optimal Transport to recover the clean OT plans, # without the need of learning the marginal distribution as in Co-Optimal Transport. # ----------------------------------------------------------------------------------------- pi_sample, pi_feature = unbalanced_co_optimal_transport( X=X, Y=Y_noisy, reg_marginals=(10, 10), epsilon=0, divergence="kl", unbalanced_solver="mm", max_iter=1000, tol=1e-6, max_iter_ot=1000, tol_ot=1e-6, log=False, verbose=False, ) # %% # Visualizing the row and column alignments learned by unbalanced Co-Optimal Transport. # ----------------------------------------------------------------------------------------- # # Similar to Co-Optimal Transport, we are also be able to fully recover the clean OT plans. fig = pl.figure(4, (9, 7)) pl.clf() ax1 = pl.subplot(2, 2, 3) pl.imshow(X, vmin=-2, vmax=2) pl.xlabel("$X$") ax2 = pl.subplot(2, 2, 2) ax2.yaxis.tick_right() pl.imshow(np.transpose(Y_noisy), vmin=-2, vmax=2) pl.title("Transpose(Noisy $Y$)") ax2.xaxis.tick_top() for i in range(n1): j = np.argmax(pi_sample[i, :]) xyA = (d1 - 0.5, i) xyB = (j, d2 - 0.5) con = ConnectionPatch( xyA=xyA, xyB=xyB, coordsA=ax1.transData, coordsB=ax2.transData, color="black" ) fig.add_artist(con) for i in range(d1): j = np.argmax(pi_feature[i, :]) xyA = (i, -0.5) xyB = (-0.5, j) con = ConnectionPatch( xyA=xyA, xyB=xyB, coordsA=ax1.transData, coordsB=ax2.transData, color="blue" ) fig.add_artist(con) python-pot-0.9.7+dfsg/examples/others/plot_semidiscrete.py000066400000000000000000000163741525521106200240130ustar00rootroot00000000000000# -*- coding: utf-8 -*- r""" ================================== Semi-discrete OT: a toy 2D problem ================================== This example shows the :mod:`ot.semidiscrete` solver on a small 2D problem: a uniform source on :math:`[0, 1]^2` and 15 random target atoms with uniform weights. With so few atoms the Laguerre cells can be drawn by brute force on a grid. We call :func:`ot.semidiscrete.solve_semidiscrete` with its default arguments: the underlying algorithm is **Projected Averaged SGD**, and the default ``decreasing_reg=True`` adds the **DRAG** entropic-regularization schedule of [90]_, which improves convergence. For the returned potential :math:`g` we report: - the empirical Laguerre-cell masses (mean and max absolute deviation from :math:`1/15`); - the semi-dual objective :math:`\langle g, b\rangle + \mathbb{E}_X[\varphi_g(X)]` estimated by Monte Carlo, where the c-transform :math:`\varphi_g(x) = \min_j\big(c(x, y_j) - g_j\big)` is computed by :func:`ot.semidiscrete.semidiscrete_c_transform`. The solver **maximises** this objective. .. [90] Genans, F., Godichon-Baggioni, A., Vialard, F.-X., Wintenberger, O. (2025). *Decreasing Entropic Regularization Averaged Gradient for Semi-Discrete Optimal Transport.* NeurIPS 2025. """ # Author: Ferdinand Genans # # License: MIT License # sphinx_gallery_thumbnail_number = 1 import numpy as np import matplotlib.pyplot as plt from ot.semidiscrete import ( solve_semidiscrete, semidiscrete_atom_weights, semidiscrete_c_transform, semidiscrete_ot_map, ) ############################################################################## # Toy 2D problem # -------------- rng = np.random.default_rng(42) def source_sampler(batch_size): return rng.random((batch_size, 2)) n_atoms = 15 target_positions = 0.1 + 0.8 * np.random.default_rng(0).random((n_atoms, 2)) def plot_laguerre_cells(target, g, ax, title, resolution=300, alpha=0.55): xs = np.linspace(0, 1, resolution) ys = np.linspace(0, 1, resolution) XX, YY = np.meshgrid(xs, ys) grid = np.stack([XX.ravel(), YY.ravel()], axis=1) labels = semidiscrete_atom_weights(target, grid, g, reg=0.0).argmax(axis=1) image = labels.reshape(resolution, resolution) cmap = plt.get_cmap("tab20", target.shape[0]) ax.imshow( image, origin="lower", extent=(0, 1, 0, 1), cmap=cmap, alpha=alpha, vmin=-0.5, vmax=target.shape[0] - 0.5, interpolation="nearest", ) # Target points share the colour of their Laguerre cell. ax.scatter( target[:, 0], target[:, 1], s=80, c=[cmap(i) for i in range(target.shape[0])], edgecolor="black", linewidths=1.2, zorder=3, ) ax.set_title(title) ax.set_aspect("equal") ax.set_xlim(0, 1) ax.set_ylim(0, 1) ############################################################################## # Solve and visualise # ------------------- # # A single call to :func:`solve_semidiscrete` runs DRAG with the default # arguments (``decreasing_reg=True``). We show the initial Voronoi cells # (:math:`g = 0`) next to the Laguerre cells at the optimum. # With the squared-Euclidean cost (default ``metric='sqeuclidean'``), the cost # between a source point in :math:`[0, 1]^2` and an atom is # :math:`\|x - y\|^2 \le 2`. We clip the potential to # ``[-max_cost, max_cost] = [-2, 2]``, the localizing set where an optimal # potential lies ([90]_, Lemma 1), which speeds up convergence. g_drag = solve_semidiscrete( target_positions, source_sampler, max_iter=20_000, batch_size=32, max_cost=2.0, ) fig, axes = plt.subplots(1, 2, figsize=(11, 5.5)) plot_laguerre_cells(target_positions, np.zeros(n_atoms), axes[0], "Voronoi (g = 0)") plot_laguerre_cells(target_positions, g_drag, axes[1], "Approximated OT Laguerre cells") plt.tight_layout() plt.show() ############################################################################## # Transport map over the Laguerre cells # ------------------------------------- # # :func:`semidiscrete_ot_map` with ``reg=0`` is the hard Monge map: every # source point is sent to the atom of its Laguerre cell. Overlaying the map # (arrows on a source grid) on the *faded* cells shows each cell's mass # collapsing onto its atom -- a direct illustration of the mapping function. gx = np.linspace(0.04, 0.96, 14) grid = np.stack([a.ravel() for a in np.meshgrid(gx, gx)], axis=1) mapped = semidiscrete_ot_map(target_positions, grid, g_drag, reg=0.0) labels = semidiscrete_atom_weights(target_positions, grid, g_drag, reg=0.0).argmax( axis=1 ) cmap = plt.get_cmap("tab20", n_atoms) fig, ax = plt.subplots(figsize=(6.5, 6.5)) plot_laguerre_cells( target_positions, g_drag, ax, "Approximated OT map over Laguerre cells", alpha=0.22 ) ax.quiver( grid[:, 0], grid[:, 1], mapped[:, 0] - grid[:, 0], mapped[:, 1] - grid[:, 1], angles="xy", scale_units="xy", scale=1, width=0.005, headwidth=4, headlength=5, color=[cmap(i) for i in labels], zorder=2, ) plt.tight_layout() plt.show() ############################################################################## # Cell masses and Monte Carlo cost # -------------------------------- # # At the optimum each Laguerre cell should carry mass :math:`1/15`. We report # the empirical mass error and the semi-dual objective # # .. math:: # \mathcal{S}(g) = \langle g, b\rangle + \mathbb{E}_X[\varphi_g(X)] # # estimated by Monte Carlo. The solver maximises :math:`\mathcal{S}`. def cell_masses(target, g, sampler, n_samples=100_000): labels = semidiscrete_atom_weights(target, sampler(n_samples), g, reg=0.0).argmax( axis=1 ) counts = np.bincount(labels, minlength=target.shape[0]) return counts / n_samples def mc_cost(target, g, sampler, n_samples=100_000): b = np.full(target.shape[0], 1.0 / target.shape[0]) samples = sampler(n_samples) return float(g @ b + semidiscrete_c_transform(target, samples, g, reg=0.0).mean()) target_mass = 1.0 / n_atoms m_drag = cell_masses(target_positions, g_drag, source_sampler) cost_drag = mc_cost(target_positions, g_drag, source_sampler) print(f"Target mass per cell: {target_mass:.4f}") print( f"DRAG — mean abs. mass error: " f"{np.mean(np.abs(m_drag - target_mass)):.4f}" f" max: {np.max(np.abs(m_drag - target_mass)):.4f}" f" semi-dual cost (MC): {cost_drag:.5f}" ) ############################################################################## # Laguerre-cell masses # -------------------- # # At the optimum every cell carries the same mass :math:`1/15`. The bar plot # shows the empirical mass per cell against this ground truth (dashed line): # every cell sits close to the theoretical value. cmap = plt.get_cmap("tab20", n_atoms) fig, ax = plt.subplots(figsize=(7.5, 4)) ax.bar( np.arange(n_atoms), m_drag, color=[cmap(i) for i in range(n_atoms)], edgecolor="black", linewidth=0.6, ) ax.axhline( target_mass, ls="--", color="black", lw=1.5, label="theoretical mass per cell at the optimum", ) ax.set_ylim(0, 1.6 * target_mass) ax.set_xticks(np.arange(n_atoms)) ax.set_xlabel("atom index") ax.set_ylabel("Laguerre-cell mass") ax.set_title("Approximated OT: Laguerre-cell masses") ax.legend() plt.tight_layout() plt.show() python-pot-0.9.7+dfsg/examples/others/plot_sgot.py000066400000000000000000000421271525521106200223020ustar00rootroot00000000000000#!/usr/bin/env python # coding: utf-8 """ ========================================= Spectral-Grassmann OT on dynamical systems operators ========================================= This example presents a synthetic example of Spectral Grassmannian-Wasserstein Optimal Transport (SGOT) on linear dynamical systems. We consider a signal formed by the sum of two damped oscillatory modes evolving along a rotated direction in the plane. The signal is then associated with an underlying continuous linear dynamical system, and we study how its spectral representation varies under rotation. The SGOT cost and metric are used to compare the reference and rotated systems. .. [83] T. Germain; R. Flamary; V. R. Kostic; K. Lounici, A Spectral-Grassmann Wasserstein Metric for Operator Representations of Dynamical Systems, arXiv preprint arXiv:2509.24920, 2025. """ # Authors: Sienna O'Shea # Thibaut Germain # # License: MIT License import numpy as np import matplotlib.pyplot as plt from ot.sgot import sgot_metric, sgot_cost_matrix from scipy.linalg import eig # sampling parameters and time grid fs = 50 max_t = 5 time = np.linspace(0, max_t, fs * max_t) dt = 1 / fs # %% # Example: rotating a linear dynamical system in 3D # ------------------------------------------------- # # 1. Build a simple observed signal # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # We begin by assuming that the observed signal is made of two oscillatory # components: # # .. math:: # # x_{\text{ref}}(t)=e^{-\tau_1 t}\cos(2\pi\omega_1 t)\,\vec e(\theta) # \;+\; # e^{-\tau_2 t}\cos(2\pi\omega_2 t)\,\vec e(\theta), # # where :math:`\vec e(\theta)\in\mathbb{R}^2` is a fixed real vector. Thus, # :math:`x(t)` evolves along the one-dimensional subspace spanned by # :math:`\vec e(\theta)`, while its time dependence exhibits oscillatory and # dissipative behaviour. tau_0 = np.array([0.08, 0.18]) freq_0 = np.array([1.0, 2.0]) theta_0 = np.pi / 4 def rotation_matrix(theta): return np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]]) def generate_data(time, tau, freq, theta): t_ = np.sin(2 * np.pi * freq[None, :] * time[:, None]) * np.exp( -tau[None, :] * time[:, None] ) t_ = t_.sum(axis=1) traj_0 = np.zeros((t_.shape[0], 2)) traj_0[:, 0] = t_ R_ = rotation_matrix(theta) traj_0 = traj_0 @ R_.T return traj_0 traj_0 = generate_data(time, tau_0, freq_0, theta_0) traj_0_proj = traj_0 @ rotation_matrix(theta_0)[:, 0] # plot the observed signal components and their sum plt.figure(figsize=(10, 4)) plt.plot(time, traj_0_proj, label="projected trajectory", linewidth=2) plt.xlabel("time") plt.ylabel("amplitude") plt.legend() plt.title(r"Observed scalar signal along $\vec{e}(\theta)$") plt.show() # %% # 2. Interpret the signal as coming from a continuous linear dynamical system # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # We assume that :math:`x(t)` is generated by an underlying continuous linear # dynamical system. Since the observed signal is a superposition of two # sinusoidal modes, the corresponding linear dynamics are naturally described # by a fourth-order model. We therefore introduce the state vector # # .. math:: # # z(t)= # \begin{pmatrix} # x_1(t)\\ # x_2(t)\\ # \vdots\\ # x_1^{(3)}(t)\\ # x_2^{(3)}(t) # \end{pmatrix} # \in\mathbb{R}^8. # # where :math:`x^{(n)}(t)` denotes the n-th derivative of :math:`x(t)`. # # This allows us to rewrite the dynamics as a first-order linear system: # # .. math:: # # \dot{z}(t)=Az(t), # # where :math:`A\in\mathbb{R}^{8\times 8}`. Its solution is then given by # # .. math:: # # z(t)=e^{tA}z_0. fig = plt.figure(figsize=(9, 6)) ax = fig.add_subplot(projection="3d") ax.plot(time, traj_0[:, 0], traj_0[:, 1]) ax.set_xlabel("time") ax.set_ylabel("x�(t)") ax.set_title("Observed trajectory in time") ax.text2D(1.08, 0.5, "x₂(t)", transform=ax.transAxes, rotation=90, va="center") plt.show() # %% # 3. Sampling and preprocessing discrete trajectories of the dynamical system # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # We now have a bridge between the continuous system and the operator we later # aim to infer from sampled data. Since in practice we do not observe the full # continuous trajectory, we work instead with discrete samples of the signal. # We take snapshots at uniform time intervals :math:`\Delta t`, and write the # sampled signal as # # .. math:: # # S= # \begin{pmatrix} # x_1(0) & x_2(0)\\ # x_1(\Delta t) & x_2(\Delta t)\\ # \vdots\\ # x_1(N\Delta t) & x_2(N\Delta t)\\ # \end{pmatrix} # # The goal is now to use these observations to recover the operator governing # the evolution. To do this, we augment the signal :math:`s` using a sliding # window of length :math:`w`. For each :math:`k`, define # # .. math:: # # z_k = # \begin{pmatrix} # s_k\\ # s_{k+1}\\ # \vdots\\ # s_{k+w-1} # \end{pmatrix} # # We then form the data matrices # # .. math:: # # X= # \begin{pmatrix} # z_1\\ # z_2\\ # \vdots\\ # z_{N-w} # \end{pmatrix}, # \qquad # Y= # \begin{pmatrix} # z_2\\ # z_3\\ # \vdots\\ # z_{N-w+1} # \end{pmatrix}, # # so that :math:`X` contains the present windowed states and :math:`Y` the # corresponding shifted future states. # build a 4-dimensional state using delay embedding def augment(traj, window_length=2): Z = np.lib.stride_tricks.sliding_window_view(traj, (window_length, 1)) Z = Z.reshape(Z.shape[0], -1) return Z # create the embedded state matrix Z Z = augment(traj_0, 4) Z.shape # inspect one embedded state vector Z[0] # create X and Y for the SGOT metric X = Z[:-1] Y = Z[1:] # inspect shapes of X and Y print("X shape:", X.shape) print("Y shape:", Y.shape) # %% # 4. Estimate the discrete-time operator # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # We now identify the operator that maps :math:`X` to :math:`Y`. From # # .. math:: # # \dot{z}=Az, # # we have # # .. math:: # # z(t+\Delta t)=e^{\Delta tA}z(t). # # Setting # # .. math:: # # B=e^{\Delta tA}, # # the corresponding discrete-time evolution is governed by :math:`B`, and we # seek the best linear map satisfying # # .. math:: # # Y\approx X B^T. # # Equivalently, we solve the optimisation problem # # .. math:: # # \min_B \|Y-XB\|^2. # # We want to recover the best rank-:math:`r` operator, whose estimator is # defined as follows: # # .. math:: # # B = C_{xx}^{-\frac{1}{2}}[C_{xx}^{-\frac{1}{2}}C_{xy}]_r # \quad \text{s.t} \quad C_{xx} = X^T X \quad \text{and} \quad C_{xy} = X^TY. # # Here :math:`[\cdot]_r` denotes the best rank-:math:`r` estimator obtained via # SVD decomposition. [2] # # [2] Kostic, V., Novelli, P., Maurer, A., Ciliberto, C., Rosasco, L. and # Pontil, M., 2022. Learning dynamical systems via Koopman operator regression # in reproducing kernel Hilbert spaces. Advances in Neural Information # Processing Systems, 35, pp.4017-4031. def estimator(X, Y, rank=4, eps=1e-8): cxx = X.T @ X + eps * np.eye(X.shape[1]) U, S, Vt = np.linalg.svd(cxx) S_inv = np.divide(1, S, out=np.zeros_like(S), where=S != 0) cxx_inv_half = Vt.T @ np.diag(np.sqrt(S_inv)) @ U.T cxy = X.T @ Y T = cxx_inv_half @ cxy U, S, Vt = np.linalg.svd(T) S[rank:] = 0 T_rank = U @ np.diag(S) @ Vt T = cxx_inv_half @ T_rank # estimate spectral decomposition val, vl, vr = eig(T, left=True, right=True) sort_idx = np.argsort(np.abs(val))[::-1] val = val[sort_idx][:rank] vl = vl[:, sort_idx][:, :rank] vr = vr[:, sort_idx][:, :rank] return T, {"eig_val": val, "eig_vec_left": vl, "eig_vec_right": vr} B_0, B_0_spec = estimator(X, Y, rank=4) Y_pred = X @ B_0 plt.figure(figsize=(10, 4)) plt.plot(Y[:, 0], label="true") plt.plot(Y_pred[:, 0], "--", label="predicted") plt.xlabel("sample index") plt.ylabel("first state coordinate") plt.title("True Signal vs Predicted Signal") plt.legend() plt.show() # %% # The predicted signal is nearly indistinguishable from the true signal, # indicating that the estimated operator accurately captures the observed # dynamics. # %% # 6. Recover continuous-time spectral information from the discrete operator # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # To recover the continuous generator :math:`A`, we study the spectral # structure of :math:`B`. We diagonalise :math:`B` as # # .. math:: # # B=PDP^{-1}, # # where # # .. math:: # # D=\operatorname{diag}(\mu_1,\dots,\mu_n). # # The continuous-time eigenvalues are of the form # # .. math:: # # \lambda_k=-\tau_k+2\pi i\,\omega_k, # \qquad k\in\{1,2\}, # # and the corresponding eigenvalues of :math:`B` are # # .. math:: # # \mu_k=e^{\Delta t\lambda_k} # =e^{\Delta t(-\tau_k+2\pi i\omega_k)}. # # Since :math:`B=e^{\Delta tA}`, we recover :math:`A` by taking the logarithm: # # .. math:: # # A=P\,\frac{\log(D)}{\Delta t}\,P^{-1}. D_0 = np.log(B_0_spec["eig_val"]) * fs L_0 = B_0_spec["eig_vec_left"] R_0 = B_0_spec["eig_vec_right"] recovered_freqs = D_0.imag / (2 * np.pi) mask = recovered_freqs > 0 recovered_freqs = recovered_freqs[mask] decay = -D_0.real[mask] print(f"First mode: frequency: {recovered_freqs[0]:.2f} Hz -- decay: {decay[0]:.2f}") print(f"Second mode: frequency: {recovered_freqs[1]:.2f} Hz -- decay: {decay[1]:.2f}") # %% # Introduction to SGOT for linear operators # ----------------------------------------- # # To compare two linear operators through their spectral structure, we use the # SGOT framework introduced in Theorem 1 of [1]. For a non-defective # finite-rank operator :math:`T \in S_r(\mathcal H)`, the theorem associates a # discrete spectral measure # # .. math:: # # \mu(T) \triangleq \sum_{j\in[\ell]} # \frac{m_j}{m_{\mathrm{tot}}}\,\delta_{(\lambda_j,\mathcal V_j)}, # # where :math:`\lambda_j` are the eigenvalues of :math:`T`, :math:`m_j` their # algebraic multiplicities, and :math:`\mathcal V_j` the corresponding # eigenspaces. Thus, each spectral component of the operator is represented by # an atom of the form # # .. math:: # # (\lambda_j,\mathcal V_j), # # combining one eigenvalue with its associated invariant subspace. # # Theorem 1 then defines a ground cost between two such atoms by combining a # spectral discrepancy and a geometric discrepancy: # # .. math:: # # d_\eta\big((\lambda,\mathcal V),(\lambda',\mathcal V')\big) # \triangleq # \eta\,|\lambda-\lambda'| + (1-\eta)\, d_{\mathcal G}(\mathcal V,\mathcal V'), # # where :math:`d_{\mathcal G}` denotes the grassmann distance between # eigenspaces and :math:`\eta\in(0,1)` balances the contribution of eigenvalues # and eigenspaces. # # The SGOT distance between two operators :math:`T` and :math:`T'` is then the # Wasserstein distance between their associated spectral measures: # # .. math:: # # d_S(T,T') = W_{d_\eta,p}\big(\mu(T),\mu(T')\big). # # In this way, SGOT compares linear operators by optimally matching their # spectral atoms, taking into account both the location of eigenvalues and the # relative geometry of their eigenspaces. # %% # A wider delay window for the SGOT experiments below # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # The window of length 4 used above is enough to identify a single reference # operator, but the experiments below also probe signals whose two modes # nearly coincide in frequency (e.g. :math:`\omega_2'\to\omega_1`). Telling # such near-degenerate modes apart requires the delay embedding to span # enough time to "see" their differing decay, so we re-embed the reference # signal with a longer window before running the sweeps. sgot_window = 10 Z = augment(traj_0, sgot_window) _, B_0_spec_sgot = estimator(Z[:-1], Z[1:]) D_0_sgot = np.log(B_0_spec_sgot["eig_val"]) * fs L_0_sgot = B_0_spec_sgot["eig_vec_left"] R_0_sgot = B_0_spec_sgot["eig_vec_right"] # %% # SGOT distance versus rotation angle # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # We compare the reference signal with a rotated version obtained by changing # only the observation direction. The shifted signal is # # .. math:: # # x_{\mathrm{shift}}^{\mathrm{rot}}(t;\theta) # = # \sum_{i=1}^{2} # e^{-\tau_i t}\cos(2\pi\omega_i t)\,\vec e({\color{red}\theta}), # # while the reference signal is recovered at :math:`\theta=\theta_0`. Thus, # this experiment isolates the effect of rotating the underlying one-dimensional # subspace in the observation plane. thetas = np.linspace(0, np.pi / 2, 51) rotation_scores = [] for theta in thetas: Z = augment(generate_data(time, tau_0, freq_0, theta), sgot_window) B, B_spec = estimator(Z[:-1], Z[1:]) D = np.log(B_spec["eig_val"]) * fs L = B_spec["eig_vec_left"] R = B_spec["eig_vec_right"] rotation_scores.append( sgot_metric( D_0_sgot, R_0_sgot, L_0_sgot, D, R, L, eta=0.9, grassmann_metric="chordal" ) ) fig, ax = plt.subplots(figsize=(7, 4)) ax.plot(thetas, rotation_scores, linewidth=1.8) ax.axvline(theta_0, color="gray", linestyle="--", linewidth=0.8) ax.set_xlabel(r"Rotation angle $\theta$ (rad)") ax.set_ylabel(r"$d_S$") ax.set_title("SGOT distance vs. rotation angle") fig.tight_layout() plt.show() # %% # Comparison across Grassmannian metrics for SGOT distance versus rotation angle # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ metrics = ["chordal", "geodesic", "procrustes", "martin"] styles = {"chordal": "-", "geodesic": "--", "procrustes": "-.", "martin": ":"} rotation_results = {m: [] for m in metrics} for theta in thetas: Z = augment(generate_data(time, tau_0, freq_0, theta), sgot_window) B, B_spec = estimator(Z[:-1], Z[1:]) D = np.log(B_spec["eig_val"]) * fs L = B_spec["eig_vec_left"] R = B_spec["eig_vec_right"] for m in metrics: rotation_results[m].append( sgot_metric( D_0_sgot, R_0_sgot, L_0_sgot, D, R, L, eta=0.9, grassmann_metric=m ) ) fig, ax = plt.subplots(figsize=(7, 4)) for m in metrics: ax.plot(thetas, rotation_results[m], styles[m], label=m, linewidth=1.8) ax.axvline( theta_0, color="gray", linestyle="--", linewidth=0.8, label=r"$\theta_0 = \pi/4$ (reference)", ) ax.set_xlabel(r"Rotation angle $\theta$ (rad)") ax.set_ylabel(r"$d_S$") ax.set_title("SGOT distance vs. rotation angle across Grassmannian metrics") ax.legend() fig.tight_layout() plt.show() # %% # SGOT distance versus frequency # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # In this experiment, we keep the reference direction fixed and perturb one of # the oscillatory modes. The shifted signal is # # .. math:: # # x_{\mathrm{shift}}^{\omega}(t) # = # e^{-\tau_1 t}\cos(2\pi\omega_1 t)\,\vec e(\theta_0) # \;+\; # e^{-\tau_2 t}\cos(2\pi{\color{red}\omega_2'} t)\,\vec e(\theta_0), # # where only the second frequency is modified. We then study how the SGOT # distance changes as a function of the perturbed frequency :math:`\omega_2'`. omegas = np.linspace(0.5, 3.0, 21) frequency_scores = {m: [] for m in metrics} for omega in omegas: Z = augment( generate_data(time, tau_0, np.array([freq_0[0], omega]), theta_0), sgot_window ) B, B_spec = estimator(Z[:-1], Z[1:]) D = np.log(B_spec["eig_val"]) * fs L = B_spec["eig_vec_left"] R = B_spec["eig_vec_right"] for m in metrics: frequency_scores[m].append( sgot_metric( D_0_sgot, R_0_sgot, L_0_sgot, D, R, L, eta=0.9, grassmann_metric=m ) ) fig, ax = plt.subplots(figsize=(7, 4)) for m in metrics: ax.plot(omegas, frequency_scores[m], styles[m], label=m, linewidth=1.8) ax.axvline( freq_0[1], color="gray", linestyle="--", linewidth=0.8, label=r"$\omega_2 = 2.0$ Hz (reference)", ) ax.set_xlabel(r"Frequency $\omega_2'$ (Hz)") ax.set_ylabel(r"$d_S$") ax.set_title("SGOT distance vs. frequency across Grassmannian metrics") ax.legend() fig.tight_layout() plt.show() # %% # SGOT distance versus decay # ~~~~~~~~~~~~~~~~~~~~~~~~~~ # # We now study the effect of changing the decay rate while keeping the # observation direction fixed. The shifted signal is # # .. math:: # # x_{\mathrm{shift}}^{\mathrm{decay}}(t;\tau) # = # e^{-{\color{red}\tau} t}\cos(2\pi\omega_1 t)\,\vec e(\theta_0) # \;+\; # e^{-{\color{red}\tau} t}\cos(2\pi\omega_2' t)\,\vec e(\theta_0). # # In this way, both modes share the same modified decay parameter # :math:`\tau`, allowing us to isolate the influence of dissipation on the SGOT # distance. taus = np.linspace(0.1, 3.0, 21) decay_scores = {m: [] for m in metrics} for tau in taus: Z = augment(generate_data(time, np.array([tau, tau]), freq_0, theta_0), sgot_window) B, B_spec = estimator(Z[:-1], Z[1:]) D = np.log(B_spec["eig_val"]) * fs L = B_spec["eig_vec_left"] R = B_spec["eig_vec_right"] for m in metrics: decay_scores[m].append( sgot_metric( D_0_sgot, R_0_sgot, L_0_sgot, D, R, L, eta=0.9, grassmann_metric=m ) ) fig, ax = plt.subplots(figsize=(7, 4)) for m in metrics: ax.plot(taus, decay_scores[m], styles[m], label=m, linewidth=1.8) ax.set_xlabel(r"Decay rate $\tau$") ax.set_ylabel(r"$d_S$") ax.set_title("SGOT distance vs. decay across Grassmannian metrics") ax.legend() fig.tight_layout() plt.show() python-pot-0.9.7+dfsg/examples/others/plot_stochastic.py000066400000000000000000000116671525521106200234770ustar00rootroot00000000000000""" =================== Stochastic examples =================== This example is designed to show how to use the stochastic optimization algorithms for discrete and semi-continuous measures from the POT library. [18] Genevay, A., Cuturi, M., Peyré, G. & Bach, F. Stochastic Optimization for Large-scale Optimal Transport. Advances in Neural Information Processing Systems (2016). [19] Seguy, V., Bhushan Damodaran, B., Flamary, R., Courty, N., Rolet, A. & Blondel, M. Large-scale Optimal Transport and Mapping Estimation. International Conference on Learning Representation (2018) """ # Author: Kilian Fatras # # License: MIT License import matplotlib.pylab as pl import numpy as np import ot import ot.plot ############################################################################# # Compute the Transportation Matrix for the Semi-Dual Problem # ----------------------------------------------------------- # # Discrete case # ````````````` # # Sample two discrete measures for the discrete case and compute their cost # matrix c. n_source = 7 n_target = 4 reg = 1 numItermax = 1000 a = ot.utils.unif(n_source) b = ot.utils.unif(n_target) rng = np.random.RandomState(0) X_source = rng.randn(n_source, 2) Y_target = rng.randn(n_target, 2) M = ot.dist(X_source, Y_target) ############################################################################# # Call the "SAG" method to find the transportation matrix in the discrete case method = "SAG" sag_pi = ot.stochastic.solve_semi_dual_entropic(a, b, M, reg, method, numItermax) print(sag_pi) ############################################################################# # Semi-Continuous Case # ```````````````````` # # Sample one general measure a, one discrete measures b for the semicontinuous # case, the points where source and target measures are defined and compute the # cost matrix. n_source = 7 n_target = 4 reg = 1 numItermax = 1000 log = True a = ot.utils.unif(n_source) b = ot.utils.unif(n_target) rng = np.random.RandomState(0) X_source = rng.randn(n_source, 2) Y_target = rng.randn(n_target, 2) M = ot.dist(X_source, Y_target) ############################################################################# # Call the "ASGD" method to find the transportation matrix in the semicontinuous # case. method = "ASGD" asgd_pi, log_asgd = ot.stochastic.solve_semi_dual_entropic( a, b, M, reg, method, numItermax, log=log ) print(log_asgd["alpha"], log_asgd["beta"]) print(asgd_pi) ############################################################################# # Compare the results with the Sinkhorn algorithm sinkhorn_pi = ot.sinkhorn(a, b, M, reg) print(sinkhorn_pi) ############################################################################## # Plot Transportation Matrices # ```````````````````````````` # # For SAG pl.figure(4, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, sag_pi, "semi-dual : OT matrix SAG") pl.show() ############################################################################## # For ASGD pl.figure(4, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, asgd_pi, "semi-dual : OT matrix ASGD") pl.show() ############################################################################## # For Sinkhorn pl.figure(4, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, sinkhorn_pi, "OT matrix Sinkhorn") pl.show() ############################################################################# # Compute the Transportation Matrix for the Dual Problem # ------------------------------------------------------ # # Semi-continuous case # ```````````````````` # # Sample one general measure a, one discrete measures b for the semi-continuous # case and compute the cost matrix c. n_source = 7 n_target = 4 reg = 1 numItermax = 100000 lr = 0.1 batch_size = 3 log = True a = ot.utils.unif(n_source) b = ot.utils.unif(n_target) rng = np.random.RandomState(0) X_source = rng.randn(n_source, 2) Y_target = rng.randn(n_target, 2) M = ot.dist(X_source, Y_target) ############################################################################# # # Call the "SGD" dual method to find the transportation matrix in the # semi-continuous case sgd_dual_pi, log_sgd = ot.stochastic.solve_dual_entropic( a, b, M, reg, batch_size, numItermax, lr, log=log ) print(log_sgd["alpha"], log_sgd["beta"]) print(sgd_dual_pi) ############################################################################# # # Compare the results with the Sinkhorn algorithm # ``````````````````````````````````````````````` # # Call the Sinkhorn algorithm from POT sinkhorn_pi = ot.sinkhorn(a, b, M, reg) print(sinkhorn_pi) ############################################################################## # Plot Transportation Matrices # ```````````````````````````` # # For SGD pl.figure(4, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, sgd_dual_pi, "dual : OT matrix SGD") pl.show() ############################################################################## # For Sinkhorn pl.figure(4, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, sinkhorn_pi, "OT matrix Sinkhorn") pl.show() python-pot-0.9.7+dfsg/examples/plot_Intro_OT.py000066400000000000000000000317151525521106200215200ustar00rootroot00000000000000# coding: utf-8 """ ============================================= Introduction to Optimal Transport with Python ============================================= This example gives an introduction on how to use Optimal Transport in Python. """ # Author: Remi Flamary, Nicolas Courty, Aurelie Boisbunon # # License: MIT License # sphinx_gallery_thumbnail_number = 1 ############################################################################## # POT Python Optimal Transport Toolbox # ------------------------------------ # # POT installation # ``````````````````` # # * Install with pip:: # # pip install pot # * Install with conda:: # # conda install -c conda-forge pot # # Import the toolbox # ``````````````````` # import numpy as np # always need it import pylab as pl # do the plots import ot # ot import time ############################################################################## # Getting help # ````````````` # # Online documentation : ``_ # # Or inline help: # help(ot.dist) ############################################################################## # First OT Problem # ---------------- # # We will solve the Bakery/Cafés problem of transporting croissants from a # number of Bakeries to Cafés in a City (in this case Manhattan). We did a # quick google map search in Manhattan for bakeries and Cafés: # # .. image:: ../_static/images/bak.png # :align: center # :alt: bakery-cafe-manhattan # :width: 600px # :height: 280px # # We extracted from this search their positions and generated fictional # production and sale number (that both sum to the same value). # # We have access to the position of Bakeries ``bakery_pos`` and their # respective production ``bakery_prod`` which describe the source # distribution. The Cafés where the croissants are sold are defined also by # their position ``cafe_pos`` and ``cafe_prod``, and describe the target # distribution. For fun we also provide a # map ``Imap`` that will illustrate the position of these shops in the city. # # # Now we load the data # # data = np.load("../data/manhattan.npz") bakery_pos = data["bakery_pos"] bakery_prod = data["bakery_prod"] cafe_pos = data["cafe_pos"] cafe_prod = data["cafe_prod"] Imap = data["Imap"] print("Bakery production: {}".format(bakery_prod)) print("Cafe sale: {}".format(cafe_prod)) print("Total croissants : {}".format(cafe_prod.sum())) ############################################################################## # Plotting bakeries in the city # ----------------------------- # # Next we plot the position of the bakeries and cafés on the map. The size of # the circle is proportional to their production. # pl.figure(1, (7, 6)) pl.clf() pl.imshow(Imap, interpolation="bilinear") # plot the map pl.scatter( bakery_pos[:, 0], bakery_pos[:, 1], s=bakery_prod, c="r", ec="k", label="Bakeries" ) pl.scatter(cafe_pos[:, 0], cafe_pos[:, 1], s=cafe_prod, c="b", ec="k", label="Cafés") pl.legend() pl.title("Manhattan Bakeries and Cafés") ############################################################################## # Cost matrix # ----------- # # # We can now compute the cost matrix between the bakeries and the cafés, which # will be the transport cost matrix. This can be done using the # `ot.dist `_ function that # defaults to squared Euclidean distance but can return other things such as # cityblock (or Manhattan distance). # C = ot.dist(bakery_pos, cafe_pos) labels = [str(i) for i in range(len(bakery_prod))] f = pl.figure(2, (14, 7)) pl.clf() pl.subplot(121) pl.imshow(Imap, interpolation="bilinear") # plot the map for i in range(len(cafe_pos)): pl.text( cafe_pos[i, 0], cafe_pos[i, 1], labels[i], color="b", fontsize=14, fontweight="bold", ha="center", va="center", ) for i in range(len(bakery_pos)): pl.text( bakery_pos[i, 0], bakery_pos[i, 1], labels[i], color="r", fontsize=14, fontweight="bold", ha="center", va="center", ) pl.title("Manhattan Bakeries and Cafés") ax = pl.subplot(122) im = pl.imshow(C, cmap="coolwarm") pl.title("Cost matrix") cbar = pl.colorbar(im, ax=ax, shrink=0.5, use_gridspec=True) cbar.ax.set_ylabel("cost", rotation=-90, va="bottom") pl.xlabel("Cafés") pl.ylabel("Bakeries") pl.tight_layout() ############################################################################## # The red cells in the matrix image show the bakeries and cafés that are # further away, and thus more costly to transport from one to the other, while # the blue ones show those that are very close to each other, with respect to # the squared Euclidean distance. ############################################################################## # Solving the OT problem with `ot.emd `_ # ----------------------------------------------------------------------------------- start = time.time() ot_emd = ot.emd(bakery_prod, cafe_prod, C) time_emd = time.time() - start ############################################################################## # The function returns the transport matrix, which we can then visualize (next section). ############################################################################## # Transportation plan visualization # ````````````````````````````````` # # A good visualization of the OT matrix in the 2D plane is to denote the # transportation of mass between a Bakery and a Café by a line. This can easily # be done with a double ``for`` loop. # # In order to make it more interpretable one can also use the ``alpha`` # parameter of plot and set it to ``alpha=G[i,j]/G.max()``. # Plot the matrix and the map f = pl.figure(3, (14, 7)) pl.clf() pl.subplot(121) pl.imshow(Imap, interpolation="bilinear") # plot the map for i in range(len(bakery_pos)): for j in range(len(cafe_pos)): pl.plot( [bakery_pos[i, 0], cafe_pos[j, 0]], [bakery_pos[i, 1], cafe_pos[j, 1]], "-k", lw=3.0 * ot_emd[i, j] / ot_emd.max(), ) for i in range(len(cafe_pos)): pl.text( cafe_pos[i, 0], cafe_pos[i, 1], labels[i], color="b", fontsize=14, fontweight="bold", ha="center", va="center", ) for i in range(len(bakery_pos)): pl.text( bakery_pos[i, 0], bakery_pos[i, 1], labels[i], color="r", fontsize=14, fontweight="bold", ha="center", va="center", ) pl.title("Manhattan Bakeries and Cafés") ax = pl.subplot(122) im = pl.imshow(ot_emd) for i in range(len(bakery_prod)): for j in range(len(cafe_prod)): text = ax.text( j, i, "{0:g}".format(ot_emd[i, j]), ha="center", va="center", color="w" ) pl.title("Transport matrix") pl.xlabel("Cafés") pl.ylabel("Bakeries") pl.tight_layout() ############################################################################## # The transport matrix gives the number of croissants that can be transported # from each bakery to each café. We can see that the bakeries only need to # transport croissants to one or two cafés, the transport matrix being very # sparse. ############################################################################## # OT loss and dual variables # -------------------------- # # The resulting wasserstein loss loss is of the form: # # .. math:: # W=\sum_{i,j}\gamma_{i,j}C_{i,j} # # where :math:`\gamma` is the optimal transport matrix. # W = np.sum(ot_emd * C) print("Wasserstein loss (EMD) = {0:.2f}".format(W)) ############################################################################## # Regularized OT with Sinkhorn # ---------------------------- # # The Sinkhorn algorithm is very simple to code. You can implement it directly # using the following pseudo-code # # .. image:: ../_static/images/sinkhorn.png # :align: center # :alt: Sinkhorn algorithm # :width: 440px # :height: 240px # # In this algorithm, :math:`\oslash` corresponds to the element-wise division. # # An alternative is to use the POT toolbox with # `ot.sinkhorn `_ # # Be careful of numerical problems. A good pre-processing for Sinkhorn is to # divide the cost matrix ``C`` by its maximum value. ############################################################################## # Algorithm # ````````` # Compute Sinkhorn transport matrix from algorithm reg = 0.1 K = np.exp(-C / C.max() / reg) nit = 100 u = np.ones((len(bakery_prod),)) for i in range(1, nit): v = cafe_prod / np.dot(K.T, u) u = bakery_prod / (np.dot(K, v)) ot_sink_algo = np.atleast_2d(u).T * ( K * v.T ) # Equivalent to np.dot(np.diag(u), np.dot(K, np.diag(v))) # Compute Sinkhorn transport matrix with POT ot_sinkhorn = ot.sinkhorn(bakery_prod, cafe_prod, reg=reg, M=C / C.max()) # Difference between the 2 print( "Difference between algo and ot.sinkhorn = {0:.2g}".format( np.sum(np.power(ot_sink_algo - ot_sinkhorn, 2)) ) ) ############################################################################## # Plot the matrix and the map # ``````````````````````````` print("Min. of Sinkhorn's transport matrix = {0:.2g}".format(np.min(ot_sinkhorn))) f = pl.figure(4, (13, 6)) pl.clf() pl.subplot(121) pl.imshow(Imap, interpolation="bilinear") # plot the map for i in range(len(bakery_pos)): for j in range(len(cafe_pos)): pl.plot( [bakery_pos[i, 0], cafe_pos[j, 0]], [bakery_pos[i, 1], cafe_pos[j, 1]], "-k", lw=3.0 * ot_sinkhorn[i, j] / ot_sinkhorn.max(), ) for i in range(len(cafe_pos)): pl.text( cafe_pos[i, 0], cafe_pos[i, 1], labels[i], color="b", fontsize=14, fontweight="bold", ha="center", va="center", ) for i in range(len(bakery_pos)): pl.text( bakery_pos[i, 0], bakery_pos[i, 1], labels[i], color="r", fontsize=14, fontweight="bold", ha="center", va="center", ) pl.title("Manhattan Bakeries and Cafés") ax = pl.subplot(122) im = pl.imshow(ot_sinkhorn) for i in range(len(bakery_prod)): for j in range(len(cafe_prod)): text = ax.text( j, i, np.round(ot_sinkhorn[i, j], 1), ha="center", va="center", color="w" ) pl.title("Transport matrix") pl.xlabel("Cafés") pl.ylabel("Bakeries") pl.tight_layout() ############################################################################## # We notice right away that the matrix is not sparse at all with Sinkhorn, # each bakery delivering croissants to all 5 cafés with that solution. Also, # this solution gives a transport with fractions, which does not make sense # in the case of croissants. This was not the case with EMD. ############################################################################## # Varying the regularization parameter in Sinkhorn # ```````````````````````````````````````````````` # reg_parameter = np.logspace(-3, 0, 20) W_sinkhorn_reg = np.zeros((len(reg_parameter),)) time_sinkhorn_reg = np.zeros((len(reg_parameter),)) f = pl.figure(5, (14, 5)) pl.clf() max_ot = 100 # plot matrices with the same colorbar for k in range(len(reg_parameter)): start = time.time() ot_sinkhorn = ot.sinkhorn( bakery_prod, cafe_prod, reg=reg_parameter[k], M=C / C.max() ) time_sinkhorn_reg[k] = time.time() - start if k % 4 == 0 and k > 0: # we only plot a few ax = pl.subplot(1, 5, k // 4) im = pl.imshow(ot_sinkhorn, vmin=0, vmax=max_ot) pl.title("reg={0:.2g}".format(reg_parameter[k])) pl.xlabel("Cafés") pl.ylabel("Bakeries") # Compute the Wasserstein loss for Sinkhorn, and compare with EMD W_sinkhorn_reg[k] = np.sum(ot_sinkhorn * C) pl.tight_layout() ############################################################################## # This series of graph shows that the solution of Sinkhorn starts with something # very similar to EMD (although not sparse) for very small values of the # regularization parameter, and tends to a more uniform solution as the # regularization parameter increases. # ############################################################################## # Wasserstein loss and computational time # ``````````````````````````````````````` # # Plot the matrix and the map f = pl.figure(6, (4, 4)) pl.clf() pl.title("Comparison between Sinkhorn and EMD") pl.plot(reg_parameter, W_sinkhorn_reg, "o", label="Sinkhorn") XLim = pl.xlim() pl.plot(XLim, [W, W], "--k", label="EMD") pl.legend() pl.xlabel("reg") pl.ylabel("Wasserstein loss") ############################################################################## # In this last graph, we show the impact of the regularization parameter on # the Wasserstein loss. We can see that higher # values of ``reg`` leads to a much higher Wasserstein loss. # # The Wasserstein loss of EMD is displayed for # comparison. The Wasserstein loss of Sinkhorn can be a little lower than that # of EMD for low values of ``reg``, but it quickly gets much higher. # python-pot-0.9.7+dfsg/examples/plot_OT_1D.py000066400000000000000000000060351525521106200206660ustar00rootroot00000000000000# %% # -*- coding: utf-8 -*- """ ==================================== Optimal Transport for fixed support ==================================== This example illustrates the computation of EMD and Sinkhorn transport plans and their visualization. """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 3 import numpy as np import matplotlib.pylab as pl import ot import ot.plot from ot.datasets import make_1D_gauss as gauss ############################################################################## # Generate data # ------------- # %% parameters n = 100 # nb bins # bin positions x = np.arange(n, dtype=np.float64) # Gaussian distributions a = gauss(n, m=20, s=5) # m= mean, s= std b = gauss(n, m=60, s=10) # loss matrix M = ot.dist(x.reshape((n, 1)), x.reshape((n, 1))) M /= M.max() ############################################################################## # Plot distributions and loss matrix # ---------------------------------- # %% plot the distributions pl.figure(1, figsize=(6.4, 3)) pl.plot(x, a, "b", label="Source distribution") pl.plot(x, b, "r", label="Target distribution") pl.legend() # %% plot distributions and loss matrix pl.figure(2, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, M, "Cost matrix M") ############################################################################## # Solve Exact OT # --------- # %% EMD # use fast 1D solver G0 = ot.emd_1d(x, x, a, b) # Equivalent to # G0 = ot.emd(a, b, M) pl.figure(3, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, G0, "OT matrix G0") ############################################################################## # Solve Sinkhorn # -------------- # %% Sinkhorn lambd = 1e-3 Gs = ot.sinkhorn(a, b, M, lambd, verbose=True) pl.figure(4, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, Gs, "OT matrix Sinkhorn") pl.show() ############################################################################## # Solve Smooth OT # --------------- # We illustrate below Smooth and Sparse (KL an L2 reg.) OT and # sparsity-constrained OT, together with their visualizations. # # Reference: # # Blondel, M., Seguy, V., & Rolet, A. (2018). Smooth and Sparse Optimal # Transport. Proceedings of the # Twenty-First International Conference on # Artificial Intelligence and # Statistics (AISTATS). # %% Smooth OT with KL regularization lambd = 2e-3 Gsm = ot.smooth.smooth_ot_dual(a, b, M, lambd, reg_type="kl") pl.figure(3, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, Gsm, "OT matrix Smooth OT KL reg.") pl.show() # %% Smooth OT with squared l2 regularization lambd = 1e-1 Gsm = ot.smooth.smooth_ot_dual(a, b, M, lambd, reg_type="l2") pl.figure(5, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, Gsm, "OT matrix Smooth OT l2 reg.") pl.show() # %% Sparsity-constrained OT lambd = 1e-1 max_nz = 2 # two non-zero entries are permitted per column of the OT plan Gsc = ot.smooth.smooth_ot_dual( a, b, M, lambd, reg_type="sparsity_constrained", max_nz=max_nz ) pl.figure(6, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, Gsc, "Sparsity constrained OT matrix; k=2.") pl.show() python-pot-0.9.7+dfsg/examples/plot_OT_2D_samples.py000066400000000000000000000060531525521106200224130ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ==================================================== Optimal Transport between empirical distributions ==================================================== Illustration of optimal transport between distributions in 2D that are weighted sum of Diracs. The OT matrix is plotted with the samples. """ # Author: Remi Flamary # Kilian Fatras # # License: MIT License # sphinx_gallery_thumbnail_number = 4 import numpy as np import matplotlib.pylab as pl import ot import ot.plot ############################################################################## # Generate data # ------------- # %% parameters and data generation n = 50 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([4, 4]) cov_t = np.array([[1, -0.8], [-0.8, 1]]) xs = ot.datasets.make_2D_samples_gauss(n, mu_s, cov_s) xt = ot.datasets.make_2D_samples_gauss(n, mu_t, cov_t) a, b = np.ones((n,)) / n, np.ones((n,)) / n # uniform distribution on samples # loss matrix M = ot.dist(xs, xt) ############################################################################## # Plot data # --------- # %% plot samples pl.figure(1) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.legend(loc=0) pl.title("Source and target distributions") pl.figure(2) pl.imshow(M, interpolation="nearest", cmap="gray_r") pl.title("Cost matrix M") ############################################################################## # Compute EMD # ----------- # %% EMD G0 = ot.solve(M, a, b).plan pl.figure(3) pl.imshow(G0, interpolation="nearest", cmap="gray_r") pl.title("OT matrix G0") pl.figure(4) ot.plot.plot2D_samples_mat(xs, xt, G0, c=[0.5, 0.5, 1]) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.legend(loc=0) pl.title("OT matrix with samples") ############################################################################## # Compute Sinkhorn # ---------------- # %% sinkhorn # reg term lambd = 1e-1 Gs = ot.sinkhorn(a, b, M, lambd) pl.figure(5) pl.imshow(Gs, interpolation="nearest", cmap="gray_r") pl.title("OT matrix sinkhorn") pl.figure(6) ot.plot.plot2D_samples_mat(xs, xt, Gs, color=[0.5, 0.5, 1]) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.legend(loc=0) pl.title("OT matrix Sinkhorn with samples") pl.show() ############################################################################## # Empirical Sinkhorn # ------------------- # %% sinkhorn # reg term lambd = 1e-1 Ges = ot.bregman.empirical_sinkhorn(xs, xt, lambd) pl.figure(7) pl.imshow(Ges, interpolation="nearest", cmap="gray_r") pl.title("OT matrix empirical sinkhorn") pl.figure(8) ot.plot.plot2D_samples_mat(xs, xt, Ges, color=[0.5, 0.5, 1]) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.legend(loc=0) pl.title("OT matrix Sinkhorn from samples") pl.show() python-pot-0.9.7+dfsg/examples/plot_OT_L1_vs_L2.py000066400000000000000000000114371525521106200217450ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ================================================ Optimal Transport with different ground metrics ================================================ 2D OT on empirical distribution with different ground metric. Stole the figure idea from Fig. 1 and 2 in https://arxiv.org/pdf/1706.07650.pdf """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 3 import numpy as np import matplotlib.pylab as pl import ot import ot.plot # %% # Dataset 1 : uniform sampling # ---------------------------- n = 20 # nb samples xs = np.zeros((n, 2)) xs[:, 0] = np.arange(n) + 1 xs[:, 1] = (np.arange(n) + 1) * -0.001 # to make it strictly convex... xt = np.zeros((n, 2)) xt[:, 1] = np.arange(n) + 1 a, b = ot.unif(n), ot.unif(n) # uniform distribution on samples # loss matrix M1 = ot.dist(xs, xt, metric="euclidean") M1 /= M1.max() # loss matrix M2 = ot.dist(xs, xt, metric="sqeuclidean") M2 /= M2.max() # loss matrix Mp = ot.dist(xs, xt, metric="cityblock") Mp /= Mp.max() # Data pl.figure(1, figsize=(7, 3)) pl.clf() pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.axis("equal") pl.title("Source and target distributions") # Cost matrices pl.figure(2, figsize=(7, 3)) pl.subplot(1, 3, 1) pl.imshow(M1, interpolation="nearest") pl.title("Euclidean cost") pl.subplot(1, 3, 2) pl.imshow(M2, interpolation="nearest") pl.title("Squared Euclidean cost") pl.subplot(1, 3, 3) pl.imshow(Mp, interpolation="nearest") pl.title("L1 (cityblock cost") pl.tight_layout() ############################################################################## # Dataset 1 : Plot OT Matrices # ---------------------------- # %% EMD G1 = ot.emd(a, b, M1) G2 = ot.emd(a, b, M2) Gp = ot.emd(a, b, Mp) # OT matrices pl.figure(3, figsize=(7, 3)) pl.subplot(1, 3, 1) ot.plot.plot2D_samples_mat(xs, xt, G1, c=[0.5, 0.5, 1]) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.axis("equal") # pl.legend(loc=0) pl.title("OT Euclidean") pl.subplot(1, 3, 2) ot.plot.plot2D_samples_mat(xs, xt, G2, c=[0.5, 0.5, 1]) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.axis("equal") # pl.legend(loc=0) pl.title("OT squared Euclidean") pl.subplot(1, 3, 3) ot.plot.plot2D_samples_mat(xs, xt, Gp, c=[0.5, 0.5, 1]) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.axis("equal") # pl.legend(loc=0) pl.title("OT L1 (cityblock)") pl.tight_layout() pl.show() # %% # Dataset 2 : Partial circle # -------------------------- n = 20 # nb samples xtot = np.zeros((n + 1, 2)) xtot[:, 0] = np.cos((np.arange(n + 1) + 1.0) * 0.8 / (n + 2) * 2 * np.pi) xtot[:, 1] = np.sin((np.arange(n + 1) + 1.0) * 0.8 / (n + 2) * 2 * np.pi) xs = xtot[:n, :] xt = xtot[1:, :] a, b = ot.unif(n), ot.unif(n) # uniform distribution on samples # loss matrix M1 = ot.dist(xs, xt, metric="euclidean") M1 /= M1.max() # loss matrix M2 = ot.dist(xs, xt, metric="sqeuclidean") M2 /= M2.max() # loss matrix Mp = ot.dist(xs, xt, metric="cityblock") Mp /= Mp.max() # Data pl.figure(4, figsize=(7, 3)) pl.clf() pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.axis("equal") pl.title("Source and target distributions") # Cost matrices pl.figure(5, figsize=(7, 3)) pl.subplot(1, 3, 1) pl.imshow(M1, interpolation="nearest") pl.title("Euclidean cost") pl.subplot(1, 3, 2) pl.imshow(M2, interpolation="nearest") pl.title("Squared Euclidean cost") pl.subplot(1, 3, 3) pl.imshow(Mp, interpolation="nearest") pl.title("L1 (cityblock) cost") pl.tight_layout() ############################################################################## # Dataset 2 : Plot OT Matrices # ----------------------------- # # %% EMD G1 = ot.emd(a, b, M1) G2 = ot.emd(a, b, M2) Gp = ot.emd(a, b, Mp) # OT matrices pl.figure(6, figsize=(7, 3)) pl.subplot(1, 3, 1) ot.plot.plot2D_samples_mat(xs, xt, G1, c=[0.5, 0.5, 1]) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.axis("equal") # pl.legend(loc=0) pl.title("OT Euclidean") pl.subplot(1, 3, 2) ot.plot.plot2D_samples_mat(xs, xt, G2, c=[0.5, 0.5, 1]) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.axis("equal") # pl.legend(loc=0) pl.title("OT squared Euclidean") pl.subplot(1, 3, 3) ot.plot.plot2D_samples_mat(xs, xt, Gp, c=[0.5, 0.5, 1]) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.axis("equal") # pl.legend(loc=0) pl.title("OT L1 (cityblock)") pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/plot_bsp_ot.py000066400000000000000000000124271525521106200213100ustar00rootroot00000000000000""" =================== Fast and accurate transport bijections using BSP-OT =================== This example shows two use cases for the bijections provided by BSP-OT, between two large point clouds: shape morphing (and animated morphing) and full color transfer (pixel permutation). [84] Genest, B., Bonneel, N., Nivoliers, V., Coeurjolly, D. BSP-OT: Sparse transport plans between discrete measures in log-linear time ACM Transactions on Graphics, Siggraph Asia (2025). """ # Author: Baptiste Genest # # License: MIT License import ot.utils import ot.bsp import numpy as np import time import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation ############################################################################## # Shape interpolation and morphing # ---------------------------------- ############################################################################## # Data generation # ---------------------------------- # %% Two large 2D point clouds # For this example, let's create two large 2D point clouds, # one sampled from a single ball, and the other from two smaller balls. def sample_ball(n, radius=1.0, center=(0.0, 0.0)): np.random.seed(0) theta = 2 * np.pi * np.random.rand(n) r = radius * np.sqrt(np.random.rand(n)) x = r * np.cos(theta) + center[0] y = r * np.sin(theta) + center[1] return np.stack((x, y), axis=1) def sample_two_balls(n, radius=1.0, centers=((-1, 0), (1, 1))): assert n % 2 == 0, "n must be even" n_half = n // 2 X1 = sample_ball(n_half, radius, centers[0]) X2 = sample_ball(n_half, radius, centers[1]) return np.vstack((X1, X2)) # ---------------------------- # Load point clouds # ---------------------------- N = 100000 X_s = sample_ball(N) X_t = sample_two_balls(N, 0.5) N = X_s.shape[0] ############################################################################## # Bijection computation # ---------------------------------- ot.utils.tic() # %% call BSP-OT solver # The solver returns the transport cost, the final bijection and the # intermediary ones used to compute the final one (here we set k = 64). # Here we only use the final bijection. cost, perm, _ = ot.bsp.compute_bspot_bijection(X_s, X_t, 64, 2) print( "Bijection computed between {} points, with cost {} in {}s".format( N, cost, ot.utils.toc() ) ) # %% Reordering # As the plan is a bijection, it is simply stored as permutation (e.g. a list of numbers) # such that X_s[i] is assigned to X_t[perm[i]]. # For the sake of the animation, we reorder X_t according to the obtained bijection # such that the points are in correspondence along the morphing animation # using simply X_s*(1-t) + X_t*t X_t_perm = X_t[perm] ############################################################################## # Setup animation # ---------------------------- # Animation parameters FRAMES = 100 INTERVAL = 20 # Plot setup fig, ax = plt.subplots(figsize=(6, 6)) ax.set_aspect("equal") ax.set_title("Bijective Point Cloud Morphing") scat = ax.scatter(X_s[:, 0], X_s[:, 1], s=0.05, c="tab:blue") all_pts = np.vstack([X_s, X_t_perm]) pad = 0.5 ax.set_xlim(all_pts[:, 0].min() - pad, all_pts[:, 0].max() + pad) ax.set_ylim(all_pts[:, 1].min() - pad, all_pts[:, 1].max() + pad) # ---------------------------- # Animation update # ---------------------------- def update(frame): t = frame / (FRAMES - 1) t = t * t * (3 - 2 * t) P = (1 - t) * X_s + t * X_t_perm scat.set_offsets(P) return (scat,) # ---------------------------- # Run animation # ---------------------------- ani = FuncAnimation(fig, update, frames=FRAMES, interval=INTERVAL, blit=True) plt.show() ############################################################################## # Other example: full color transfer (pixel permutation) using BSP-OT # ---------------------------- import os from pathlib import Path import numpy as np from PIL import Image def im2mat(img): """Converts an image to matrix (one pixel per line)""" return img.reshape((img.shape[0] * img.shape[1], img.shape[2])) def mat2im(X, shape): """Converts back a matrix to an image""" return X.reshape(shape) def minmax(img): return np.clip(img, 0, 1) this_file = os.path.realpath("__file__") data_path = os.path.join(Path(this_file).parent.parent, "data") I1_pil = Image.open(os.path.join(data_path, "ocean_day.jpg")).convert("RGB") I2_pil = Image.open(os.path.join(data_path, "ocean_sunset.jpg")).convert("RGB") # force same size to obtain bijection between all pixels I1_pil = I1_pil.resize(I2_pil.size, Image.BILINEAR) I1 = np.asarray(I1_pil).astype(np.float64) / 255.0 I2 = np.asarray(I2_pil).astype(np.float64) / 255.0 X1 = im2mat(I1) X2 = im2mat(I2) start = ot.utils.tic() _, perm, _ = ot.bsp.compute_bspot_bijection(X1, X2, 16) print("Bijection computed between {} pixels in {}s".format(X1.shape[0], ot.utils.toc())) # reorder the second image according to the obtained bijection X2_perm = X2[perm] # reshape back to image I2_perm = mat2im(X2_perm, I1.shape) plt.figure(figsize=(12, 6)) plt.subplot(1, 3, 1) plt.imshow(I2) plt.title("Color image") plt.axis("off") plt.subplot(1, 3, 2) plt.imshow(I1) plt.title("Target image") plt.axis("off") plt.subplot(1, 3, 3) plt.imshow(I2_perm) plt.title("Permuted image") plt.axis("off") plt.tight_layout() plt.show() python-pot-0.9.7+dfsg/examples/plot_compute_emd.py000066400000000000000000000065001525521106200223160ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ================== Geometry of OT distances ================== Shows how to compute multiple Wasserstein and Sinkhorn with two different ground metrics and plot their values for different distributions. """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pylab as pl import ot from ot.datasets import make_1D_gauss as gauss ############################################################################## # Generate data # ------------- # %% parameters n = 100 # nb bins n_target = 20 # nb target distributions # bin positions x = np.arange(n, dtype=np.float64) lst_m = np.linspace(20, 90, n_target) # Gaussian distributions a = gauss(n, m=20, s=5) # m= mean, s= std B = np.zeros((n, n_target)) for i, m in enumerate(lst_m): B[:, i] = gauss(n, m=m, s=5) # loss matrix and normalization M = ot.dist(x.reshape((n, 1)), x.reshape((n, 1)), "euclidean") M /= M.max() * 0.1 M2 = ot.dist(x.reshape((n, 1)), x.reshape((n, 1)), "sqeuclidean") M2 /= M2.max() * 0.1 ############################################################################## # Plot data # --------- # %% plot the distributions pl.figure(1) pl.subplot(2, 1, 1) pl.plot(x, a, "r", label="Source distribution") pl.title("Source distribution") pl.subplot(2, 1, 2) for i in range(n_target): pl.plot(x, B[:, i], "b", alpha=i / n_target) pl.plot(x, B[:, -1], "b", label="Target distributions") pl.title("Target distributions") pl.tight_layout() ############################################################################## # Compute EMD for the different losses # ------------------------------------ # %% Compute and plot distributions and loss matrix d_emd = ot.emd2(a, B, M) # direct computation of OT loss d_emd2 = ot.emd2(a, B, M2) # direct computation of OT loss with metric M2 d_tv = [np.sum(abs(a - B[:, i])) for i in range(n_target)] pl.figure(2) pl.subplot(2, 1, 1) pl.plot(x, a, "r", label="Source distribution") pl.title("Distributions") for i in range(n_target): pl.plot(x, B[:, i], "b", alpha=i / n_target) pl.plot(x, B[:, -1], "b", label="Target distributions") pl.ylim((-0.01, 0.13)) pl.xticks(()) pl.legend() pl.subplot(2, 1, 2) pl.plot(d_emd, label="Euclidean OT") pl.plot(d_emd2, label="Squared Euclidean OT") pl.plot(d_tv, label="Total Variation (TV)") # pl.xlim((-7,23)) pl.xlabel("Displacement") pl.title("Divergences") pl.legend() ############################################################################## # Compute Sinkhorn for the different losses # ----------------------------------------- # %% reg = 1e-1 d_sinkhorn = ot.sinkhorn2(a, B, M, reg) d_sinkhorn2 = ot.sinkhorn2(a, B, M2, reg) pl.figure(3) pl.clf() pl.subplot(2, 1, 1) pl.plot(x, a, "r", label="Source distribution") pl.title("Distributions") for i in range(n_target): pl.plot(x, B[:, i], "b", alpha=i / n_target) pl.plot(x, B[:, -1], "b", label="Target distributions") pl.ylim((-0.01, 0.13)) pl.xticks(()) pl.legend() pl.subplot(2, 1, 2) pl.plot(d_emd, label="Euclidean OT") pl.plot(d_emd2, label="Squared Euclidean OT") pl.plot(d_sinkhorn, "+", label="Euclidean Sinkhorn") pl.plot(d_sinkhorn2, "+", label="Squared Euclidean Sinkhorn") pl.plot(d_tv, label="Total Variation (TV)") # pl.xlim((-7,23)) pl.xlabel("Displacement") pl.title("Divergences") pl.legend() pl.show() python-pot-0.9.7+dfsg/examples/plot_debias_sink_div.py000066400000000000000000000166641525521106200231460ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ====================================== Sinkhorn Divergence and Debiased OT solvers ====================================== This example shows how to use the debiased OT solvers in `ot.solve_sample` to compute Sinkhorn divergences and debiased Minibatch solutions. The debiased OT solvers can be used with balanced and unbalanced OT problems, and with different regularization types (entropic, L2, group lasso). """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 3 # %% import numpy as np import matplotlib.pylab as pl import ot import ot.plot from ot.datasets import make_1D_gauss as gauss ############################################################################## # Generate data # ------------- # %% def sample_ball(n, radius=1.0, center=(0.0, 0.0)): np.random.seed(0) theta = 2 * np.pi * np.random.rand(n) r = radius * np.sqrt(np.random.rand(n)) x = r * np.cos(theta) + center[0] y = r * np.sin(theta) + center[1] return np.stack((x, y), axis=1) def sample_two_balls(n, radius=1.0, sep=1): assert n % 2 == 0, "n must be even" centers = ((-sep, -sep), (sep, sep)) n_half = n // 2 X1 = sample_ball(n_half, radius, centers[0]) X2 = sample_ball(n_half, radius, centers[1]) perm = np.random.permutation(n_half * 2) X = np.vstack((X1, X2)) X = X[perm] return X n = 50 x1 = sample_ball(n, radius=1.0, center=(0, 0)) x2 = sample_two_balls(n, radius=1.0, sep=1.5) pl.figure(1, figsize=(5, 5)) pl.scatter(x1[:, 0], x1[:, 1], label="Source distribution", alpha=0.7) pl.scatter(x2[:, 0], x2[:, 1], label="Target distribution", alpha=0.7) pl.legend() pl.title("Two distributions") ax = pl.axis() ############################################################################## # Compute Sinkhorn divergence and visualize plans # ----------------------------------------------- # The Sinkhorn divergence is computed by setting the `debias` parameter to # `True` in the `ot.solve_sample` function. The resulting value is the Sinkhorn # divergence. The Sinkhorn divergences is computed as: # # .. math:: # S_\epsilon(\mu, \nu) = OT_\epsilon(\mu, \nu) - \frac{1}{2} OT_\epsilon(\mu, \mu) - \frac{1}{2} OT_\epsilon(\nu, \nu) # # The entropic OT plans for each of those terms can be accessed in the `log` # attribute of the result, and can be visualized using the # `ot.plot.plot2D_samples_mat` function. res = ot.solve_sample(x1, x2, reg=0.1, debias=True) print("Sinkhorn divergence: ", res.value) plan_11 = res.log["res_aa"].plan plan_12 = res.log["res_ab"].plan plan_22 = res.log["res_bb"].plan # pl.figure(2, figsize=(15, 5)) pl.subplot(1, 3, 1) ot.plot.plot2D_samples_mat(x1, x1, plan_11, thr=0.05) pl.scatter(x1[:, 0], x1[:, 1], label="Source distribution", zorder=2) pl.axis(ax) pl.title("Plan between source and source") pl.subplot(1, 3, 2) ot.plot.plot2D_samples_mat(x1, x2, plan_12, thr=0.05) pl.scatter(x1[:, 0], x1[:, 1], label="Source distribution", zorder=2) pl.scatter(x2[:, 0], x2[:, 1], label="Target distribution", zorder=2) pl.axis(ax) pl.title("Plan between source and target") pl.subplot(1, 3, 3) ot.plot.plot2D_samples_mat(x2, x2, plan_22, thr=0.05) pl.scatter(x2[:, 0], x2[:, 1], label="Target distribution", color="C1", zorder=2) pl.axis(ax) pl.title("Plan between target and target") ############################################################################## # Debiased Minibatch OT # --------------------------------- # # Doing OT on minibatches leads to a similar bias than using entropic # regularization since the average OT plan is densified due to the stochasticity # of the minibatch sampling. On a given minibatch, the debiased loss can be # computed by setting the `debias` parameter to `'split'`that split the data # points in each distributions in two and computes the debias OT loss as: # # .. math:: # \tilde{OT}_m(\mu, \nu) = \frac{1}{2}(\hat{OT}_m(\mu_1, \nu_1) + \hat{OT}_m(\mu_2, \nu_2) - \hat{OT}_m(\nu_1, \nu_2) - \hat{OT}_m(\mu_1, \nu_2)) # # %% solve OT minibtach and visualize the plans res = ot.solve_sample(x1, x2, debias="split") print("Debiased minibatch OT loss: ", res.value) # recover the plans for each of the four terms in the debiased loss plan_11 = res.log["res_aa"].plan plan_12 = res.log["res_ab1"].plan plan_21 = res.log["res_ab2"].plan plan_22 = res.log["res_bb"].plan sel_a1 = res.log["sel_a1"] sel_a2 = res.log["sel_a2"] sel_b1 = res.log["sel_b1"] sel_b2 = res.log["sel_b2"] nb1 = plan_11.shape[0] nb2 = plan_22.shape[0] pl.figure(4, figsize=(15, 3)) pl.subplot(1, 4, 1) pl.scatter(x1[sel_a1, 0], x1[sel_a1, 1], label="$\mu_1$", zorder=2) pl.scatter( x1[sel_a2, 0], x1[sel_a2, 1], label=r"$\mu_2$", zorder=2, color="C0", alpha=0.5 ) pl.scatter(x2[sel_b1, 0], x2[sel_b1, 1], label=r"$\nu_1$", zorder=2, color="C1") pl.scatter( x2[sel_b2, 0], x2[sel_b2, 1], label=r"$\nu_2$", zorder=2, color="C1", alpha=0.5 ) pl.title("Minibatch split") pl.axis(ax) pl.legend() pl.subplot(1, 4, 2) ot.plot.plot2D_samples_mat(x1[sel_a1], x1[sel_a2], plan_11, thr=0.05) pl.scatter(x1[sel_a1, 0], x1[sel_a1, 1], zorder=2) pl.scatter( x1[sel_a2, 0], x1[sel_a2, 1], zorder=2, color="C0", alpha=0.5, ) pl.axis(ax) pl.title("Plan between source and source") pl.subplot(1, 4, 3) ot.plot.plot2D_samples_mat(x1[sel_a1], x2[sel_b1], plan_12, thr=0.05) ot.plot.plot2D_samples_mat(x1[sel_a2], x2[sel_b2], plan_21, thr=0.05, alpha=0.5) pl.scatter(x1[sel_a1, 0], x1[sel_a1, 1], label="Source distribution", zorder=2) pl.scatter( x2[sel_b1, 0], x2[sel_b1, 1], label="Target distribution", zorder=2, color="C1" ) pl.scatter( x1[sel_a2, 0], x1[sel_a2, 1], label="Source distribution", zorder=2, color="C0", alpha=0.5, ) pl.scatter( x2[sel_b2, 0], x2[sel_b2, 1], label="Target distribution", zorder=2, color="C1", alpha=0.5, ) pl.axis(ax) pl.title("Plan between source and target") pl.subplot(1, 4, 4) ot.plot.plot2D_samples_mat(x2[sel_b1], x2[sel_b2], plan_22, thr=0.05) pl.scatter( x2[sel_b1, 0], x2[sel_b1, 1], label="Target distribution", zorder=2, color="C1" ) pl.scatter( x2[sel_b2, 0], x2[sel_b2, 1], label="Target distribution", zorder=2, color="C1", alpha=0.5, ) pl.axis(ax) pl.title("Plan between target and target") ############################################################################## # Comparison of the divergences # ------------------------------------------------- # %% move a distribution and compute Sinkhorn divergence and Sinkhorn distance reg = 0.1 sep_list = np.linspace(0, 1.0, 10) sink_list = [] sink_div_list = [] ot_mb_list = [] ot_mb_sink_list = [] for sep in sep_list: x2sep = sample_two_balls(n, radius=1.0, sep=sep) sink_list.append( ot.solve_sample( x1, x2sep, reg=reg, ).value ) sink_div_list.append(ot.solve_sample(x1, x2sep, reg=reg, debias=True).value) ot_mb_list.append(ot.solve_sample(x1, x2sep, debias="split").value) ot_mb_sink_list.append(ot.solve_sample(x1, x2sep, reg=1, debias="split").value) pl.figure(3) pl.plot(sep_list, sink_list, label="Sinkhorn loss", color="C0") pl.plot(sep_list, sink_div_list, label="Sinkhorn divergence", color="C1") pl.plot(sep_list, ot_mb_list, label="Debiased MB OT", color="C2") pl.plot(sep_list, ot_mb_sink_list, label="Debiased MB Sinkhorn", color="C3") pl.xlabel("Separation between distributions") pl.ylabel("Loss/Divergence") pl.title("Comparison of biased VS debiased OT losses") pl.grid() pl.legend() # %% python-pot-0.9.7+dfsg/examples/plot_optim_OTreg.py000066400000000000000000000066361525521106200222570ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ================================== Regularized OT with generic solver ================================== Illustrates the use of the generic solver for regularized OT with user-designed regularization term. It uses Conditional gradient as in [6] and generalized Conditional Gradient as proposed in [5,7]. [5] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, Optimal Transport for Domain Adaptation, in IEEE Transactions on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1. [6] Ferradans, S., Papadakis, N., Peyré, G., & Aujol, J. F. (2014). Regularized discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. [7] Rakotomamonjy, A., Flamary, R., & Courty, N. (2015). Generalized conditional gradient: analysis of convergence and applications. arXiv preprint arXiv:1510.06567. """ # sphinx_gallery_thumbnail_number = 5 import numpy as np import matplotlib.pylab as pl import ot import ot.plot ############################################################################## # Generate data # ------------- # %% parameters n = 100 # nb bins # bin positions x = np.arange(n, dtype=np.float64) # Gaussian distributions a = ot.datasets.make_1D_gauss(n, m=20, s=5) # m= mean, s= std b = ot.datasets.make_1D_gauss(n, m=60, s=10) # loss matrix M = ot.dist(x.reshape((n, 1)), x.reshape((n, 1))) M /= M.max() ############################################################################## # Solve EMD # --------- # %% EMD G0 = ot.emd(a, b, M) pl.figure(1, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, G0, "OT matrix G0") ############################################################################## # Solve EMD with Frobenius norm regularization # -------------------------------------------- # %% Example with Frobenius norm regularization def f(G): return 0.5 * np.sum(G**2) def df(G): return G reg = 1e-1 Gl2 = ot.optim.cg(a, b, M, reg, f, df, verbose=True) pl.figure(2) ot.plot.plot1D_mat(a, b, Gl2, "OT matrix Frob. reg") ############################################################################## # Solve EMD with entropic regularization # -------------------------------------- # %% Example with entropic regularization def f(G): return np.sum(G * np.log(G)) def df(G): return np.log(G) + 1.0 reg = 1e-3 Ge = ot.optim.cg(a, b, M, reg, f, df, verbose=True) pl.figure(3, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, Ge, "OT matrix Entrop. reg") ############################################################################## # Solve EMD with Frobenius norm + entropic regularization # ------------------------------------------------------- # %% Example with Frobenius norm + entropic regularization with gcg def f(G): return 0.5 * np.sum(G**2) def df(G): return G reg1 = 1e-3 reg2 = 1e-1 Gel2 = ot.optim.gcg(a, b, M, reg1, reg2, f, df, verbose=True) pl.figure(4, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, Gel2, "OT entropic + matrix Frob. reg") pl.show() # %% # Comparison of the OT matrices nvisu = 40 pl.figure(5, figsize=(10, 4)) pl.subplot(2, 2, 1) pl.imshow(G0[:nvisu, :], cmap="gray_r") pl.axis("off") pl.title("Exact OT") pl.subplot(2, 2, 2) pl.imshow(Gl2[:nvisu, :], cmap="gray_r") pl.axis("off") pl.title("Frobenius reg.") pl.subplot(2, 2, 3) pl.imshow(Ge[:nvisu, :], cmap="gray_r") pl.axis("off") pl.title("Entropic reg.") pl.subplot(2, 2, 4) pl.imshow(Gel2[:nvisu, :], cmap="gray_r") pl.axis("off") pl.title("Entropic + Frobenius reg.") python-pot-0.9.7+dfsg/examples/plot_quickstart_guide.py000066400000000000000000000565551525521106200234030ustar00rootroot00000000000000# coding: utf-8 """ ============================================= Quickstart Guide ============================================= Quickstart guide to the POT toolbox. For better readability, only the use of POT is provided and the plotting code with matplotlib is hidden (but is available in the source file of the example). .. note:: We use here the unified API of POT which is more flexible and allows to solve a wider range of problems with just a few functions. The classical API is still available (the unified API one is a convenient wrapper around the classical one) and we provide pointers to the classical API when needed. """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 18 # Import necessary libraries import numpy as np import pylab as pl import ot # %% # 2D data example # --------------- # # We first generate two sets of samples in 2D that 25 and 50 # samples respectively located on circles. The weights of the samples are # uniform. # Problem size n1 = 25 n2 = 50 # Generate random data np.random.seed(0) a = ot.utils.unif(n1) # weights of points in the source domain b = ot.utils.unif(n2) # weights of points in the target domain x1 = np.random.randn(n1, 2) x1 /= np.sqrt(np.sum(x1**2, 1, keepdims=True)) / 2 x2 = np.random.randn(n2, 2) x2 /= np.sqrt(np.sum(x2**2, 1, keepdims=True)) / 4 # Compute the cost matrix C = ot.dist(x1, x2) # Squared Euclidean cost matrix by default # sphinx_gallery_start_ignore style = {"markeredgecolor": "k"} def plot_plan(P=None, title="", axis=True): if P is not None: plot2D_samples_mat(x1, x2, P) pl.plot(x1[:, 0], x1[:, 1], "ob", label="Source samples", **style) pl.plot(x2[:, 0], x2[:, 1], "or", label="Target samples", **style) if not axis: pl.axis("off") pl.title(title) pl.figure(1, (4, 4)) plot_plan(title="Source and target distributions") pl.legend(loc=0) pl.show() pl.figure(2, (3.5, 1.7)) pl.imshow(C) pl.colorbar() pl.title("Cost matrix C") # sphinx_gallery_end_ignore # %% # # Solving exact Optimal Transport # ------------------------------- # Solve the Optimal Transport problem between the samples # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # The :func:`ot.solve_sample` function can be used to solve the Optimal Transport problem # between two sets of samples. The function takes as its two first arguments the # positions of the source and target samples, and returns an :class:`ot.utils.OTResult` object. # Solve the OT problem sol = ot.solve_sample(x1, x2, a, b) # get the OT plan P = sol.plan # get the OT loss loss = sol.value # get the dual potentials alpha, beta = sol.potentials print(f"OT loss = {loss:1.3f}") # sphinx_gallery_start_ignore from ot.plot import plot2D_samples_mat pl.figure(1, (8, 4)) pl.subplot(1, 2, 1) plot_plan(P, "OT plan P loss={:.3f}".format(loss)) pl.subplot(1, 2, 2) pl.scatter(x1[:, 0], x1[:, 1], c=alpha, cmap="viridis", edgecolors="k") pl.scatter(x2[:, 0], x2[:, 1], c=beta, cmap="plasma", edgecolors="k") pl.title("Dual potentials") pl.show() pl.figure(2, (3, 1.7)) pl.imshow(P, cmap="Greys") pl.title("OT plan") pl.show() # sphinx_gallery_end_ignore # %% # The figure above shows the Optimal Transport plan between the source and target # samples. The color intensity represents the amount of mass transported # between the samples. The dual potentials of the OT problem are also shown. # # The weights of the samples in the source and target domains :code:`a` and # :code:`b` are given to the function. If not provided, the weights are assumed # to be uniform See :func:`ot.solve_sample` for more details. # # The :class:`ot.utils.OTResult` object contains the following attributes: # # - :code:`value`: the value of the OT problem # - :code:`plan`: the OT matrix # - :code:`potentials`: Dual potentials of the OT problem # - :code:`log`: log dictionary of the solver # # The OT matrix :math:`P` is a matrix of size :code:`(n1, n2)` where # :code:`P[i,j]` is the amount of mass # transported from :code:`x1[i]` to :code:`x2[j]`. # # The OT loss is the sum of the element-wise product of the OT matrix and the # cost matrix taken by default as the Squared Euclidean distance. # # %% # Optimal Transport problem with a custom cost matrix # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # The cost matrix can be customized by passing it to the more general # :func:`ot.solve` function. The cost matrix should be a matrix of size # :code:`(n1, n2)` where :code:`C[i,j]` is the cost of transporting mass from # :code:`x1[i]` to :code:`x2[j]`. # # In this example, we use the Citybloc distance as the cost matrix. # Compute the cost matrix C_city = ot.dist(x1, x2, metric="cityblock") # Solve the OT problem with the custom cost matrix sol = ot.solve(C_city) # the parameters a and b are not provided so uniform weights are assumed P_city = sol.plan # on empirical data the same can be done with ot.solve_sample : # sol = ot.solve_sample(x1, x2, metric='cityblock') # Compute the OT loss (equivalent to ot.solve(C).value) loss_city = sol.value # same as np.sum(P_city * C) # sphinx_gallery_start_ignore pl.figure(1, (3, 3)) plot_plan(P_city, "OT plan (Citybloc) loss={:.3f}".format(loss_city)) pl.figure(2, (3, 1.7)) pl.imshow(P_city, cmap="Greys") pl.title("OT plan (Citybloc)") pl.show() # sphinx_gallery_end_ignore # %% # Note that we show here how to solve the OT problem with a custom cost matrix # with the more general :func:`ot.solve` function. # But the same can be done with the :func:`ot.solve_sample` function by passing # :code:`metric='cityblock'` as argument. # # The cost matrix can be computed with the :func:`ot.dist` function which # computes the pairwise distance between two sets of samples or can be provided # directly as a matrix by the user when no samples are available. # # .. note:: # The examples above use the unified API of POT. The classic API is still available # and and OT plan and loss can be computed with the :func:`ot.emd` and # the :func:`ot.emd2` functions as below: # # .. code-block:: python # # P = ot.emd(a, b, C) # loss = ot.emd2(a, b, C) # same as np.sum(P*C) but differentiable wrt a/b # # # Sinkhorn and Regularized OT # --------------------------- # # Entropic OT with Sinkhorn algorithm # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # Solve the Sinkhorn problem (just add reg parameter value) sol = ot.solve_sample(x1, x2, a, b, reg=1e-1) # get the OT plan and loss P_sink = sol.plan loss_sink = sol.value # objective value of the Sinkhorn problem (incl. entropy) loss_sink_linear = sol.value_linear # np.sum(P_sink * C) linear part of loss # sphinx_gallery_start_ignore pl.figure(1, (3, 3)) plot_plan(P_sink, "Sinkhorn OT plan loss={:.3f}".format(loss_sink)) pl.show() pl.figure(2, (3, 1.7)) pl.imshow(P_sink, cmap="Greys") pl.title("Sinkhorn OT plan") pl.show() # sphinx_gallery_end_ignore # %% # The Sinkhorn algorithm solves the Entropic Regularized OT problem. The # regularization strength can be controlled with the :code:`reg` parameter. # The Sinkhorn algorithm can be faster than the exact OT solver for large # regularization strength but the solution is only an approximation of the # exact OT problem and the OT plan is not sparse. # # Quadratic Regularized OT # ~~~~~~~~~~~~~~~~~~~~~~~~~ # # Use quadratic regularization P_quad = ot.solve_sample(x1, x2, a, b, reg=3, reg_type="L2").plan loss_quad = ot.solve_sample(x1, x2, a, b, reg=3, reg_type="L2").value # sphinx_gallery_start_ignore pl.figure(1, (9, 3)) pl.subplot(1, 3, 1) plot_plan(P, "OT plan loss={:.3f}".format(loss)) pl.subplot(1, 3, 2) plot_plan(P_sink, "Sinkhorn plan loss={:.3f}".format(loss_sink)) pl.subplot(1, 3, 3) plot_plan(P_quad, "Quadratic reg plan loss={:.3f}".format(loss_quad)) pl.show() # sphinx_gallery_end_ignore # %% # We plot above the OT plans obtained with different regularizations. The # quadratic regularization is another common choice for regularized OT and # preserves the sparsity of the OT plan. # # Solve the Regularized OT problem with user-defined regularization # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # Define a custom regularization function def f(G): return 0.5 * np.sum(G**2) def df(G): return G P_reg = ot.solve_sample(x1, x2, a, b, reg=3, reg_type=(f, df)).plan # sphinx_gallery_start_ignore pl.figure(1, (3, 3)) plot_plan(P_reg, "User-defined reg plan") pl.show() # sphinx_gallery_end_ignore # %% # # .. note:: # The examples above use the unified API of POT. The classic API is still available # and and the entropic OT plan and loss can be computed with the # :func:`ot.sinkhorn` # and :func:`ot.sinkhorn2` functions as below: # # .. code-block:: python # # Gs = ot.sinkhorn(a, b, C, reg=1e-1) # loss_sink = ot.sinkhorn2(a, b, C, reg=1e-1) # # For quadratic regularization, the :func:`ot.smooth.smooth_ot_dual` function # can be used to compute the solution of the regularized OT problem. For # user-defined regularization, the :func:`ot.optim.cg` function can be used # to solve the regularized OT problem with Conditional Gradient algorithm. # # Unbalanced and Partial OT # ---------------------------- # # Unbalanced Optimal Transport # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # Unbalanced OT relaxes the marginal constraints and allows for the source and # target total weights to be different. The :func:`ot.solve_sample` function can be # used to solve the unbalanced OT problem by setting the marginal penalization # :code:`unbalanced` parameter to a positive value. # # Solve the unbalanced OT problem with KL penalization P_unb_kl = ot.solve_sample(x1, x2, a, b, unbalanced=5e-2).plan # Unbalanced with KL penalization ad KL regularization P_unb_kl_reg = ot.solve_sample( x1, x2, a, b, unbalanced=5e-2, reg=1e-1 ).plan # also regularized # Unbalanced with L2 penalization P_unb_l2 = ot.solve_sample(x1, x2, a, b, unbalanced=7e1, unbalanced_type="L2").plan # sphinx_gallery_start_ignore pl.figure(1, (9, 3)) pl.subplot(1, 3, 1) plot_plan(P_unb_kl, "Unbalanced KL plan") pl.subplot(1, 3, 2) plot_plan(P_unb_kl_reg, "Unbalanced KL + reg plan") pl.subplot(1, 3, 3) plot_plan(P_unb_l2, "Unbalanced L2 plan") pl.show() # sphinx_gallery_end_ignore # %% # .. note:: # Solving the unbalanced OT problem with the classic API can be done with the # :func:`ot.unbalanced.sinkhorn_unbalanced` function as below: # # .. code-block:: python # # G_unb_kl = ot.unbalanced.sinkhorn_unbalanced(a, b, C, eps=reg, alpha=unbalanced) # # Partial Optimal Transport # ~~~~~~~~~~~~~~~~~~~~~~~~~ # # Solve the Unbalanced OT problem with TV penalization (equivalent) P_part_pen = ot.solve_sample(x1, x2, a, b, unbalanced=3, unbalanced_type="TV").plan # Solve the Partial OT problem with mass constraints (only classic API) P_part_const = ot.partial.partial_wasserstein(a, b, C, m=0.5) # 50% mass transported # sphinx_gallery_start_ignore pl.figure(1, (6, 3)) pl.subplot(1, 2, 1) plot_plan(P_part_pen, "Partial TV plan") pl.subplot(1, 2, 2) plot_plan(P_part_const, "Partial 50% mass plan") pl.show() # sphinx_gallery_end_ignore # %% # # Gromov-Wasserstein and Fused Gromov-Wasserstein # ------------------------------------- # # Gromov-Wasserstein and Entropic GW # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # The Gromov-Wasserstein distance is a similarity measure between metric # measure spaces. So it does not require the samples to be in the same space. # # Define the metric cost matrices in each spaces C1 = ot.dist(x1, x1, metric="sqeuclidean") C2 = ot.dist(x2, x2, metric="sqeuclidean") C1 /= C1.max() C2 /= C2.max() # Solve the Gromov-Wasserstein problem sol_gw = ot.solve_gromov(C1, C2, a=a, b=b) P_gw = sol_gw.plan loss_gw = sol_gw.value # quadratic + reg if reg>0 loss_gw_quad = sol_gw.value_quad # quadratic part of loss # Solve the Entropic Gromov-Wasserstein problem P_egw = ot.solve_gromov(C1, C2, a=a, b=b, reg=1e-2).plan # sphinx_gallery_start_ignore pl.figure(1, (6, 3)) pl.subplot(1, 2, 1) plot_plan(P_gw, "GW plan") pl.subplot(1, 2, 2) plot_plan(P_egw, "Entropic GW plan") pl.show() # sphinx_gallery_end_ignore # %% # .. note:: # The Gromov-Wasserstein problem can be solved with the classic API using the # :func:`ot.gromov.gromov_wasserstein` function and the Entropic # Gromov-Wasserstein problem can be solved with the # :func:`ot.gromov.entropic_gromov_wasserstein` function. # # .. code-block:: python # # P_gw = ot.gromov.gromov_wasserstein(C1, C2, a, b) # P_egw = ot.gromov.entropic_gromov_wasserstein(C1, C2, a, b, epsilon=reg) # # loss_gw = ot.gromov.gromov_wasserstein2(C1, C2, a, b) # loss_egw = ot.gromov.entropic_gromov_wasserstein2(C1, C2, a, b, epsilon=reg) # # Fused Gromov-Wasserstein # ~~~~~~~~~~~~~~~~~~~~~~~~~ # # Cost matrix M = C / np.max(C) # Solve FGW problem with alpha=0.1 sol = ot.solve_gromov(C1, C2, M, a=a, b=b, alpha=0.1) P_fgw = sol.plan loss_fgw = sol.value loss_fgw_linear = sol.value_linear # linear part of loss (wrt M) loss_fgw_quad = sol.value_quad # quadratic part of loss (wrt C1 and C2) # Solve entropic FGW problem with alpha=0.1 P_efgw = ot.solve_gromov(C1, C2, M, a=a, b=b, alpha=0.1, reg=1e-3).plan # sphinx_gallery_start_ignore pl.figure(1, (6, 3)) pl.subplot(1, 2, 1) plot_plan(P_fgw, "FGW plan") pl.subplot(1, 2, 2) plot_plan(P_efgw, "Entropic FGW plan") pl.show() # sphinx_gallery_end_ignore # %% # .. note:: # The Fused Gromov-Wasserstein problem can be solved with the classic API using # the :func:`ot.gromov.fused_gromov_wasserstein` function and the Entropic # Fused Gromov-Wasserstein problem can be solved with the # :func:`ot.gromov.entropic_fused_gromov_wasserstein` function. # # .. code-block:: python # # P_fgw = ot.gromov.fused_gromov_wasserstein(C1, C2, M, a, b, alpha=0.1) # P_efgw = ot.gromov.entropic_fused_gromov_wasserstein(C1, C2, M, a, b, alpha=0.1, epsilon=reg) # # loss_fgw = ot.gromov.fused_gromov_wasserstein2(C1, C2, M, a, b, alpha=0.1) # loss_efgw = ot.gromov.entropic_fused_gromov_wasserstein2(C1, C2, M, a, b, alpha=0.1, epsilon=reg) # # Large scale OT and approximations # --------------------------------- # # We discuss here strategies to solve large scale OT problems using approximations # of the exact OT problem. # # Large scale Sinkhorn # ~~~~~~~~~~~~~~~~~~~~ # # When having samples with a large number of points, the Sinkhorn algorithm can # be implemented in a Lazy version which is more memory efficient and avoids # the computation of the :math:`n \times m` cost matrix. # # POT provides two implementation of the lazy Sinkhorn algorithm that return their # results in a lazy form of type :class:`ot.utils.LazyTensor`. This object can be # used to compute the loss or the OT plan in a lazy way or to recover its values # in a dense form. # # Solve the Sinkhorn problem in a lazy way sol = ot.solve_sample(x1, x2, a, b, reg=1e-1, lazy=True) # Solve the sinkhoorn in a lazy way with geomloss sol_geo = ot.solve_sample(x1, x2, a, b, reg=1e-1, method="geomloss", lazy=True) # get the OT lazy plan and loss P_sink_lazy = sol.lazy_plan # recover values for Lazy plan P12 = P_sink_lazy[1, 2] P1dots = P_sink_lazy[1, :] # convert to dense matrix !!warning this can be memory consuming P_sink_lazy_dense = P_sink_lazy[:] # sphinx_gallery_start_ignore pl.figure(1, (3, 3)) plot_plan(P_sink_lazy_dense, "Lazy Sinkhorn OT plan") pl.show() pl.figure(2, (3, 1.7)) pl.imshow(P_sink_lazy_dense, cmap="Greys") pl.title("Lazy Sinkhorn OT plan") pl.show() # sphinx_gallery_end_ignore # %% # .. note:: # The lazy Sinkhorn algorithm can be found in the classic API with the # :func:`ot.bregman.empirical_sinkhorn` function with parameter # :code:`lazy=True`. Similarly the geoloss implementation is available # with the :func:`ot.bregman.empirical_sinkhorn2_geomloss`. # # # the first example shows how to solve the Sinkhorn problem in a lazy way with # the default POT implementation. The second example shows how to solve the # Sinkhorn problem in a lazy way with the PyKeops/Geomloss implementation that provides # a very efficient way to solve large scale problems on low dimensionality # samples. # # Factored and Low rank OT # ~~~~~~~~~~~~~~~~~~~~~~~~ # # The Sinkhorn algorithm can be implemented in a low rank version that # approximates the OT plan with a low rank matrix. This can be useful to # accelerate the computation of the OT plan for large scale problems. # A similar non-regularized version of low rank factorization is also available. # # Solve the Factored OT problem (use lazy=True for large scale) P_fact = ot.solve_sample(x1, x2, a, b, method="factored", rank=15).plan P_lowrank = ot.solve_sample(x1, x2, a, b, reg=0.1, method="lowrank", rank=10).plan # sphinx_gallery_start_ignore pl.figure(1, (6, 3)) pl.subplot(1, 2, 1) plot_plan(P_fact, "Factored OT plan") pl.subplot(1, 2, 2) plot_plan(P_lowrank, "Low rank OT plan") pl.show() pl.figure(2, (6, 1.7)) pl.subplot(1, 2, 1) pl.imshow(P_fact, cmap="Greys") pl.title("Factored OT plan") pl.subplot(1, 2, 2) pl.imshow(P_lowrank, cmap="Greys") pl.title("Low rank OT plan") pl.show() # sphinx_gallery_end_ignore # %% # .. note:: # The factored OT problem can be solved with the classic API using the # :func:`ot.factored.factored_optimal_transport` function and the low rank # OT problem can be solved with the :func:`ot.lowrank.lowrank_sinkhorn` function. # # Gaussian OT with Bures-Wasserstein # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # The Gaussian Wasserstein or Bures-Wasserstein distance is the Wasserstein distance # between Gaussian distributions. It can be used as an approximation of the # Wasserstein distance between empirical distributions by estimating the # covariance matrices of the samples. # # Compute the Bures-Wasserstein distance bw_value = ot.solve_sample(x1, x2, a, b, method="gaussian").value print(f"Exact OT loss = {loss:1.3f}") print(f"Bures-Wasserstein distance = {bw_value:1.3f}") # %% # One can also use the HD gaussian assumption (low rank covariance + diagonal) # that has better properties in high dimension. The rank of the covariance # matrices can be controlled with the :code:`rank` parameter. hdbw_value = ot.solve_sample(x1, x2, a, b, method="gaussian_hd", rank=1).value print(f"Bures-Wasserstein distance = {bw_value:1.3f}") print(f"High Dimensional Bures-Wasserstein distance = {hdbw_value}") # %% # .. note:: # The Gaussian Wasserstein problem can be solved with the classic API using the # :func:`ot.gaussian.empirical_bures_wasserstein_distance` function. # # Sliced Wasserstein # ~~~~~~~~~~~~~~~~~~ # # The Sliced Wasserstein distance is a Wasserstein distance between # empirical distributions that is computed by projecting the samples on random # directions and averaging the Wasserstein distances between the projected # distributions. It can be used as an approximation of the Wasserstein distance # between empirical distributions. sw_value = ot.solve_sample(x1, x2, a, b, method="sliced", n_projections=10).value max_sw_value = ot.solve_sample( x1, x2, a, b, method="max_sliced", n_projections=10 ).value print(f"Exact OT loss = {loss:1.3f}") print(f"Sliced Wasserstein distance = {sw_value:1.3f}") print(f"Max Sliced Wasserstein distance = {max_sw_value:1.3f}") # %% # Binary Space Partitioning (BSP) OT # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # One can also use the BSP OT approximation that is based on a recursive # partitioning of the space and computes the Wasserstein distance between the # empirical distributions by solving small OT problems between the samples in each # partition. The number of partitions can be controlled with the # `n_projections` parameter. # BSP can only find bijections so require same number of points x1_bsp = np.concatenate([x1, x1], axis=0) sol_bsp = ot.solve_sample(x1_bsp, x2, method="bsp", n_projections=10) bsp_value = sol_bsp.value bsp_sparse_plan = sol_bsp.sparse_plan # sphinx_gallery_start_ignore pl.figure(1, (3, 3)) plot2D_samples_mat(x1_bsp, x2, bsp_sparse_plan) pl.plot(x1_bsp[:, 0], x1_bsp[:, 1], "ob", label="Source samples", **style) pl.plot(x2[:, 0], x2[:, 1], "or", label="Target samples", **style) pl.title("BSP OT plan loss={:.3f}".format(bsp_value)) pl.show() # sphinx_gallery_end_ignore # %% # Comparing all OT plans # ---------------------- # # The figure below shows all the OT plans computed in this example. # The color intensity represents the amount of mass transported # between the samples. # # plot all plans # sphinx_gallery_start_ignore pl.figure(1, (9, 13)) pl.subplot(4, 3, 1) plot_plan(P, "OT plan", axis=False) pl.subplot(4, 3, 2) plot_plan(P_sink, "Sinkhorn plan", axis=False) pl.subplot(4, 3, 3) plot_plan(P_quad, "Quadratic reg. plan", axis=False) pl.subplot(4, 3, 4) plot_plan(P_unb_kl, "Unbalanced KL plan", axis=False) pl.subplot(4, 3, 5) plot_plan(P_unb_kl_reg, "Unbalanced KL + reg plan", axis=False) pl.subplot(4, 3, 6) plot_plan(P_unb_l2, "Unbalanced L2 plan", axis=False) pl.subplot(4, 3, 7) plot_plan(P_part_const, "Partial 50% mass plan", axis=False) pl.subplot(4, 3, 8) plot_plan(P_fact, "Factored OT plan", axis=False) pl.subplot(4, 3, 9) plot_plan(P_lowrank, "Low rank OT plan", axis=False) pl.subplot(4, 3, 10) plot_plan(P_gw, "GW plan", axis=False) pl.subplot(4, 3, 11) plot_plan(P_egw, "Entropic GW plan", axis=False) pl.subplot(4, 3, 12) plot_plan(P_fgw, "Fused GW plan", axis=False) pl.show() # sphinx_gallery_end_ignore # %% # # Solving barycenter problems # ------------------------------- # Solve Optimal transport barycenter problem with free support between several input distributions. # ~~~~~~~~~~~~~~~~~~~~ # # The :func:`ot.solve_bary_sample` function can be used to solve the Optimal Transport barycenter problem # between multiple sets of samples while optimizing the support of the barycenter and letting fixed their probability weights. # The function takes as its first argument the list of samples in each input distribution, # and as second argument the number of samples to learn in the barycenter. By default, the probability weights in each distribution and the barycentric weights are uniform but they can be customized by the user. # # The function returns an :class:`ot.utils.OTBaryResult` object that contains in part the barycenter samples and the OT plans between the barycenter and each input distribution. # # In the following, we illustrate the use of this function with the same 2D data as above considered as input distributions and compute their barycenter while using exact OT. # Notice that most of the arguments of the :func:`ot.solve_bary_sample` function are similar to those of the :func:`ot.solve_sample` function and that the same regularization and unbalanced parameters can be used to solve regularized and unbalanced barycenter problems. # Solve the OT barycenter problem (exact OT without any regularization) sol = ot.solve_bary_sample([x1, x2], n=35) # get the barycenter support X = sol.X # get the OT plans between the barycenter and each input distribution list_P = [sol.list_res[i].plan for i in range(2)] # get the barycenterOT loss loss = sol.value print(f"Barycenter OT loss = {loss:1.3f}") # sphinx_gallery_start_ignore pl.figure(1, (8, 8)) plot2D_samples_mat(x1, X, list_P[0]) plot2D_samples_mat(x2, X, list_P[1]) pl.plot(x1[:, 0], x1[:, 1], "ob", label="Source distribution 1", **style) pl.plot(x2[:, 0], x2[:, 1], "or", label="Source distribution 2", **style) pl.plot(X[:, 0], X[:, 1], "og", label="Barycenter distribution", **style) pl.title( "Barycenter samples and OT plans \n total loss= %s = 0.5 * %s + 0.5 * %s" % ( np.round(loss, 3), np.round(sol.list_res[0].value, 3), np.round(sol.list_res[1].value, 3), ) ) pl.legend(loc="best") pl.show() # sphinx_gallery_end_ignore # %% # python-pot-0.9.7+dfsg/examples/plot_solve_variants.py000066400000000000000000000121071525521106200230540ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ====================================== Optimal Transport solvers comparison ====================================== This example illustrates the solutions returns for different variants of exact, regularized and unbalanced OT solvers. """ # Author: Remi Flamary # # License: MIT License # sphinx_gallery_thumbnail_number = 3 # %% import numpy as np import matplotlib.pylab as pl import ot import ot.plot from ot.datasets import make_1D_gauss as gauss ############################################################################## # Generate data # ------------- # %% parameters n = 50 # nb bins # bin positions x = np.arange(n, dtype=np.float64) # Gaussian distributions a = 0.6 * gauss(n, m=15, s=5) + 0.4 * gauss(n, m=35, s=5) # m= mean, s= std b = gauss(n, m=25, s=5) # loss matrix M = ot.dist(x.reshape((n, 1)), x.reshape((n, 1))) M /= M.max() ############################################################################## # Plot distributions and loss matrix # ---------------------------------- # %% plot the distributions pl.figure(1, figsize=(6.4, 3)) pl.plot(x, a, "b", label="Source distribution") pl.plot(x, b, "r", label="Target distribution") pl.legend() # %% plot distributions and loss matrix pl.figure(2, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, M, "Cost matrix M") ############################################################################## # Define Group lasso regularization and gradient # ------------------------------------------------ # The groups are the first and second half of the columns of G def reg_gl(G): # group lasso + small l2 reg G1 = G[: n // 2, :] ** 2 G2 = G[n // 2 :, :] ** 2 gl1 = np.sum(np.sqrt(np.sum(G1, 0))) gl2 = np.sum(np.sqrt(np.sum(G2, 0))) return gl1 + gl2 + 0.1 * np.sum(G**2) def grad_gl(G): # gradient of group lasso + small l2 reg G1 = G[: n // 2, :] G2 = G[n // 2 :, :] gl1 = G1 / np.sqrt(np.sum(G1**2, 0, keepdims=True) + 1e-8) gl2 = G2 / np.sqrt(np.sum(G2**2, 0, keepdims=True) + 1e-8) return np.concatenate((gl1, gl2), axis=0) + 0.2 * G reg_type_gl = (reg_gl, grad_gl) # %% # Set up parameters for solvers and solve # --------------------------------------- lst_regs = ["No Reg.", "Entropic", "L2", "Group Lasso + L2"] lst_unbalanced = [ "Balanced", "Unbalanced KL", "Unbalanced L2", "Unb. TV (Partial)", ] # ["Balanced", "Unb. KL", "Unb. L2", "Unb L1 (partial)"] lst_solvers = [ # name, param for ot.solve function # balanced OT ("Exact OT", dict()), ("Entropic Reg. OT", dict(reg=0.005)), ("L2 Reg OT", dict(reg=1, reg_type="l2")), ("Group Lasso Reg. OT", dict(reg=0.1, reg_type=reg_type_gl)), # unbalanced OT KL ("Unbalanced KL No Reg.", dict(unbalanced=0.005)), ( "Unbalanced KL with KL Reg.", dict(reg=0.0005, unbalanced=0.005, unbalanced_type="kl", reg_type="kl"), ), ( "Unbalanced KL with L2 Reg.", dict(reg=0.5, reg_type="l2", unbalanced=0.005, unbalanced_type="kl"), ), ( "Unbalanced KL with Group Lasso Reg.", dict(reg=0.1, reg_type=reg_type_gl, unbalanced=0.05, unbalanced_type="kl"), ), # unbalanced OT L2 ("Unbalanced L2 No Reg.", dict(unbalanced=0.5, unbalanced_type="l2")), ( "Unbalanced L2 with KL Reg.", dict(reg=0.001, unbalanced=0.2, unbalanced_type="l2"), ), ( "Unbalanced L2 with L2 Reg.", dict(reg=0.1, reg_type="l2", unbalanced=0.2, unbalanced_type="l2"), ), ( "Unbalanced L2 with Group Lasso Reg.", dict(reg=0.05, reg_type=reg_type_gl, unbalanced=0.7, unbalanced_type="l2"), ), # unbalanced OT TV ("Unbalanced TV No Reg.", dict(unbalanced=0.1, unbalanced_type="tv")), ( "Unbalanced TV with KL Reg.", dict(reg=0.001, unbalanced=0.01, unbalanced_type="tv"), ), ( "Unbalanced TV with L2 Reg.", dict(reg=0.1, reg_type="l2", unbalanced=0.01, unbalanced_type="tv"), ), ( "Unbalanced TV with Group Lasso Reg.", dict(reg=0.02, reg_type=reg_type_gl, unbalanced=0.01, unbalanced_type="tv"), ), ] lst_plans = [] for name, param in lst_solvers: G = ot.solve(M, a, b, **param).plan lst_plans.append(G) ############################################################################## # Plot plans # ---------- pl.figure(3, figsize=(9, 9)) for i, bname in enumerate(lst_unbalanced): for j, rname in enumerate(lst_regs): pl.subplot(len(lst_unbalanced), len(lst_regs), i * len(lst_regs) + j + 1) plan = lst_plans[i * len(lst_regs) + j] m2 = plan.sum(0) m1 = plan.sum(1) m1, m2 = m1 / a.max(), m2 / b.max() pl.imshow(plan, cmap="Greys") pl.plot(x, m2 * 10, "r") pl.plot(m1 * 10, x, "b") pl.plot(x, b / b.max() * 10, "r", alpha=0.3) pl.plot(a / a.max() * 10, x, "b", alpha=0.3) # pl.axis('off') pl.tick_params( left=False, right=False, labelleft=False, labelbottom=False, bottom=False ) if i == 0: pl.title(rname) if j == 0: pl.ylabel(bname, fontsize=14) python-pot-0.9.7+dfsg/examples/plot_sparse_emd.py000066400000000000000000000112611525521106200221370ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ============================================ Sparse Optimal Transport ============================================ In many real-world optimal transport (OT) problems, the transport plan is naturally sparse: only a small fraction of all possible source-target pairs actually exchange mass. Using sparse OT solvers can provide significant computational speedups and memory savings compared to dense solvers. This example demonstrates how to use sparse cost matrices with POT's EMD solver, comparing sparse and dense formulations on both a minimal example and a larger concentric circles dataset. """ # Author: Nathan Neike # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pyplot as plt from scipy.sparse import coo_array import ot ############################################################################## # Example: concentric circles # ----------------------------------- # %% n_clusters = 8 points_per_cluster = 25 n = n_clusters * points_per_cluster k_neighbors = 8 rng = np.random.default_rng(0) r_source = 1.0 r_target = 2.0 noise_scale = 0.06 theta = np.linspace(0.0, 2.0 * np.pi, n, endpoint=False) cluster_labels = np.repeat(np.arange(n_clusters), points_per_cluster) X_large = np.column_stack( [r_source * np.cos(theta), r_source * np.sin(theta)] ) + rng.normal(scale=noise_scale, size=(n, 2)) Y_large = np.column_stack( [r_target * np.cos(theta), r_target * np.sin(theta)] ) + rng.normal(scale=noise_scale, size=(n, 2)) a_large = np.zeros(n) b_large = np.zeros(n) for k in range(n_clusters): idx = np.where(cluster_labels == k)[0] a_large[idx] = 1.0 / n_clusters / points_per_cluster b_large[idx] = 1.0 / n_clusters / points_per_cluster M_full = ot.dist(X_large, Y_large, metric="euclidean") # Build sparse cost matrix: intra-cluster k-nearest neighbors angles_X = np.arctan2(X_large[:, 1], X_large[:, 0]) angles_Y = np.arctan2(Y_large[:, 1], Y_large[:, 0]) rows = [] cols = [] vals = [] for k in range(n_clusters): src_idx = np.where(cluster_labels == k)[0] tgt_idx = np.where(cluster_labels == k)[0] for i in src_idx: diff = np.angle(np.exp(1j * (angles_Y[tgt_idx] - angles_X[i]))) idx = np.argsort(np.abs(diff))[:k_neighbors] for j_local in idx: j = tgt_idx[j_local] rows.append(i) cols.append(j) vals.append(M_full[i, j]) M_sparse_large = coo_array((vals, (rows, cols)), shape=(n, n)) allowed_sparse = set(zip(rows, cols)) ############################################################################## # Visualize edge structures # -------------------------- # %% plt.figure(figsize=(16, 6)) plt.subplot(1, 2, 1) for i in range(n): for j in range(n): plt.plot( [X_large[i, 0], Y_large[j, 0]], [X_large[i, 1], Y_large[j, 1]], color="blue", alpha=0.2, linewidth=0.05, ) plt.scatter(X_large[:, 0], X_large[:, 1], c="r", marker="o", s=20) plt.scatter(Y_large[:, 0], Y_large[:, 1], c="b", marker="x", s=20) plt.axis("equal") plt.title("Dense OT: All Possible Edges") plt.subplot(1, 2, 2) for i, j in allowed_sparse: plt.plot( [X_large[i, 0], Y_large[j, 0]], [X_large[i, 1], Y_large[j, 1]], color="blue", alpha=1, linewidth=0.05, ) plt.scatter(X_large[:, 0], X_large[:, 1], c="r", marker="o", s=20) plt.scatter(Y_large[:, 0], Y_large[:, 1], c="b", marker="x", s=20) plt.axis("equal") plt.title("Sparse OT: Intra-Cluster k-NN Edges") plt.tight_layout() plt.show() ############################################################################## # Solve and visualize transport plans # ------------------------------------ # %% G_dense = ot.emd(a_large, b_large, M_full) cost_dense = np.sum(G_dense * M_full) print(f"Dense OT cost: {cost_dense:.6f}") G_sparse, log_sparse = ot.emd(a_large, b_large, M_sparse_large, log=True) cost_sparse = log_sparse["cost"] print(f"Sparse OT cost: {cost_sparse:.6f}") plt.figure(figsize=(16, 6)) plt.subplot(1, 2, 1) ot.plot.plot2D_samples_mat( X_large, Y_large, G_dense, thr=1e-10, c=[0.5, 0.5, 1], alpha=0.5 ) plt.scatter(X_large[:, 0], X_large[:, 1], c="r", marker="o", s=20, zorder=3) plt.scatter(Y_large[:, 0], Y_large[:, 1], c="b", marker="x", s=20, zorder=3) plt.axis("equal") plt.title("Dense OT: Optimal Transport Plan") plt.subplot(1, 2, 2) ot.plot.plot2D_samples_mat( X_large, Y_large, G_sparse, thr=1e-10, c=[0.5, 0.5, 1], alpha=0.5 ) plt.scatter(X_large[:, 0], X_large[:, 1], c="r", marker="o", s=20, zorder=3) plt.scatter(Y_large[:, 0], Y_large[:, 1], c="b", marker="x", s=20, zorder=3) plt.axis("equal") plt.title("Sparse OT: Optimal Transport Plan") plt.tight_layout() plt.show() python-pot-0.9.7+dfsg/examples/sliced-wasserstein/000077500000000000000000000000001525521106200222145ustar00rootroot00000000000000python-pot-0.9.7+dfsg/examples/sliced-wasserstein/index.rst000066400000000000000000000002501525521106200240520ustar00rootroot00000000000000 OT in 1D and Sliced Wasserstein ------------------------------- .. minigallery:: ../../examples/plot_compute_emd.py ../../examples/sliced-wasserstein/plot_*.py python-pot-0.9.7+dfsg/examples/sliced-wasserstein/plot_compute_wasserstein_circle.py000066400000000000000000000127151525521106200312560ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ========================= OT distance on the Circle ========================= Shows how to compute the Wasserstein distance on the circle """ # Author: Clément Bonet # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pylab as pl import ot from scipy.special import iv ############################################################################## # Plot data # --------- # %% plot the distributions def pdf_von_Mises(theta, mu, kappa): pdf = np.exp(kappa * np.cos(theta - mu)) / (2.0 * np.pi * iv(0, kappa)) return pdf t = np.linspace(0, 2 * np.pi, 1000, endpoint=False) mu1 = 1 kappa1 = 20 mu_targets = np.linspace(mu1, mu1 + 2 * np.pi, 10) pdf1 = pdf_von_Mises(t, mu1, kappa1) pl.figure(1) for k, mu in enumerate(mu_targets): pdf_t = pdf_von_Mises(t, mu, kappa1) if k == 0: label = "Source distributions" else: label = None pl.plot(t / (2 * np.pi), pdf_t, c="b", label=label) pl.plot(t / (2 * np.pi), pdf1, c="r", label="Target distribution") pl.legend() mu2 = 0 kappa2 = kappa1 x1 = np.random.vonmises(mu1, kappa1, size=(10,)) + np.pi x2 = np.random.vonmises(mu2, kappa2, size=(10,)) + np.pi angles = np.linspace(0, 2 * np.pi, 150) pl.figure(2) pl.plot(np.cos(angles), np.sin(angles), c="k") pl.xlim(-1.25, 1.25) pl.ylim(-1.25, 1.25) pl.scatter(np.cos(x1), np.sin(x1), c="b") pl.scatter(np.cos(x2), np.sin(x2), c="r") ######################################################################################### # Compare the Euclidean Wasserstein distance with the Wasserstein distance on the circle # --------------------------------------------------------------------------------------- # This examples illustrates the periodicity of the Wasserstein distance on the circle. # We choose as target distribution a von Mises distribution with mean :math:`\mu_{\mathrm{target}}` # and :math:`\kappa=20`. Then, we compare the distances with samples obtained from a von Mises distribution # with parameters :math:`\mu_{\mathrm{source}}` and :math:`\kappa=20`. # The Wasserstein distance on the circle takes into account the periodicity # and attains its maximum in :math:`\mu_{\mathrm{target}}+1` (the antipodal point) contrary to the # Euclidean version. # %% Compute and plot distributions mu_targets = np.linspace(0, 2 * np.pi, 200) xs = np.random.vonmises(mu1 - np.pi, kappa1, size=(500,)) + np.pi n_try = 5 xts = np.zeros((n_try, 200, 500)) for i in range(n_try): for k, mu in enumerate(mu_targets): # np.random.vonmises deals with data on [-pi, pi[ xt = np.random.vonmises(mu - np.pi, kappa2, size=(500,)) + np.pi xts[i, k] = xt # Put data on S^1=[0,1[ xts2 = xts / (2 * np.pi) xs2 = np.concatenate([xs[None] for k in range(200)], axis=0) / (2 * np.pi) L_w2_circle = np.zeros((n_try, 200)) L_w2 = np.zeros((n_try, 200)) L_lcot = np.zeros((n_try, 200)) for i in range(n_try): w2_circle = ot.wasserstein_circle(xs2.T, xts2[i].T, p=2) w2 = ot.wasserstein_1d(xs2.T, xts2[i].T, p=2) w_lcot = ot.linear_circular_ot(xs2.T, xts2[i].T) L_w2_circle[i] = w2_circle L_w2[i] = w2 L_lcot[i] = w_lcot m_w2_circle = np.mean(L_w2_circle, axis=0) std_w2_circle = np.std(L_w2_circle, axis=0) m_w2_lcot = np.mean(L_lcot, axis=0) std_w2_lcot = np.std(L_lcot, axis=0) m_w2 = np.mean(L_w2, axis=0) std_w2 = np.std(L_w2, axis=0) pl.figure(1) pl.plot(mu_targets / (2 * np.pi), m_w2_circle, label="Wasserstein circle") pl.fill_between( mu_targets / (2 * np.pi), m_w2_circle - 2 * std_w2_circle, m_w2_circle + 2 * std_w2_circle, alpha=0.5, ) pl.plot(mu_targets / (2 * np.pi), m_w2, label="Euclidean Wasserstein") pl.fill_between( mu_targets / (2 * np.pi), m_w2 - 2 * std_w2, m_w2 + 2 * std_w2, alpha=0.5 ) pl.plot(mu_targets / (2 * np.pi), m_w2_lcot, label="Linear COT") pl.fill_between( mu_targets / (2 * np.pi), m_w2_lcot - 2 * std_w2_lcot, m_w2_lcot + 2 * std_w2_lcot, alpha=0.5, ) pl.vlines( x=[mu1 / (2 * np.pi)], ymin=0, ymax=np.max(w2), linestyle="--", color="k", label=r"$\mu_{\mathrm{target}}$", ) pl.legend() pl.xlabel(r"$\mu_{\mathrm{source}}$") pl.show() ######################################################################## # Wasserstein distance between von Mises and uniform for different kappa # ---------------------------------------------------------------------- # When :math:`\kappa=0`, the von Mises distribution is the uniform distribution on :math:`S^1`. # %% Compute Wasserstein between Von Mises and uniform kappas = np.logspace(-5, 2, 100) n_try = 20 xts = np.zeros((n_try, 100, 500)) for i in range(n_try): for k, kappa in enumerate(kappas): # np.random.vonmises deals with data on [-pi, pi[ xt = np.random.vonmises(0, kappa, size=(500,)) + np.pi xts[i, k] = xt / (2 * np.pi) L_w2 = np.zeros((n_try, 100)) L_lcot = np.zeros((n_try, 100)) for i in range(n_try): L_w2[i] = ot.semidiscrete_wasserstein2_unif_circle(xts[i].T) L_lcot[i] = ot.linear_circular_ot(xts[i].T) m_w2 = np.mean(L_w2, axis=0) std_w2 = np.std(L_w2, axis=0) m_lcot = np.mean(L_lcot, axis=0) std_lcot = np.std(L_lcot, axis=0) pl.figure(1) pl.plot(kappas, m_w2, label="Wasserstein") pl.fill_between(kappas, m_w2 - std_w2, m_w2 + std_w2, alpha=0.5) pl.plot(kappas, m_lcot, label="LCOT") pl.fill_between(kappas, m_lcot - std_lcot, m_lcot + std_lcot, alpha=0.5) pl.legend() pl.title(r"Evolution of $W_2^2(vM(0,\kappa), Unif(S^1))$") pl.xlabel(r"$\kappa$") pl.show() # %% python-pot-0.9.7+dfsg/examples/sliced-wasserstein/plot_sliced_plans.py000066400000000000000000000135501525521106200262700ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ =============== Sliced OT Plans =============== Compares different Sliced OT plans between two 2D point clouds. The min-Sliced transport plan was introduced in [85], and the Expected Sliced plan in [87], both were further studied theoretically in [86]. .. [85] Mahey, G., Chapel, L., Gasso, G., Bonet, C., & Courty, N. (2023). Fast Optimal Transport through Sliced Generalized Wasserstein Geodesics. Advances in Neural Information Processing Systems, 36, 35350–35385. .. [86] Tanguy, E., Chapel, L., Delon, J. (2025). Sliced Transport Plans. arXiv preprint 2506.03661. .. [87] Liu, X., Diaz Martin, R., Bai Y., Shahbazi A., Thorpe M., Aldroubi A., Kolouri, S. (2024). Expected Sliced Transport Plans. International Conference on Learning Representations. """ # Author: Eloi Tanguy # License: MIT License # sphinx_gallery_thumbnail_number = 1 ############################################################################## # Setup data and imports # ---------------------- import numpy as np import ot import matplotlib.pyplot as plt from ot.sliced import get_random_projections seed = 0 np.random.seed(seed) n = 20 m = 10 d = 2 X = np.random.randn(n, 2) Y = np.random.randn(m, 2) + np.array([5.0, 0.0])[None, :] n_proj = 50 projections = get_random_projections(d, n_proj) alpha = 0.3 ############################################################################## # Compute min-sliced transport plan # ------------------------------------ min_plan, min_cost, log_min = ot.min_sliced_transport_plan( X, Y, projections=projections, log=True ) ############################################################################## # Compute Expected Sliced Plan # ------------------------------------ expected_plan, expected_cost, log_expected = ot.expected_sliced_plan( X, Y, projections=projections, log=True ) ############################################################################## # Compute 2-Wasserstein Plan # ------------------------------------ a = np.ones(n, device=X.device) / n b = np.ones(m, device=Y.device) / m dists = ot.dist(X, Y) W2 = ot.emd2(a, b, dists) W2_plan = ot.emd(a, b, dists) ############################################################################## # Plot resulting assignments # ------------------------------------ fig, axs = plt.subplots(2, 3, figsize=(12, 4)) fig.suptitle("Sliced plans comparison", y=0.95, fontsize=16) # draw min sliced permutation axs[0, 0].set_title(f"Min Sliced Transport: cost={min_cost:.2f}") for i in range(X.shape[0]): for j in range(Y.shape[0]): if min_plan[i, j] > 0: axs[0, 0].plot( [X[i, 0], Y[j, 0]], [X[i, 1], Y[j, 1]], color="black", alpha=alpha, ) axs[1, 0].imshow(min_plan, interpolation="nearest", cmap="Blues") # draw expected sliced plan axs[0, 1].set_title(f"Expected Sliced: cost={expected_cost:.2f}") for i in range(n): for j in range(m): w = alpha * expected_plan[i, j].item() * n axs[0, 1].plot( [X[i, 0], Y[j, 0]], [X[i, 1], Y[j, 1]], color="black", alpha=w, label="Expected Sliced plan" if i == 0 and j == 0 else None, ) axs[1, 1].imshow(expected_plan, interpolation="nearest", cmap="Blues") # draw W2 plan axs[0, 2].set_title(f"W$_2$: cost={W2:.2f}") for i in range(n): for j in range(m): w = alpha * W2_plan[i, j].item() * n axs[0, 2].plot( [X[i, 0], Y[j, 0]], [X[i, 1], Y[j, 1]], color="black", alpha=w, label="W2 plan" if i == 0 and j == 0 else None, ) axs[1, 2].imshow(W2_plan, interpolation="nearest", cmap="Blues") for ax in axs[0, :]: ax.scatter(X[:, 0], X[:, 1], label="X") ax.scatter(Y[:, 0], Y[:, 1], label="Y") for ax in axs.flatten(): ax.set_aspect("equal") ax.set_xticks([]) ax.set_yticks([]) fig.tight_layout() ############################################################################## # Compare Expected Sliced plans with different inverse-temperatures beta # ------------------------------------ # As the temperature decreases, ES becomes sparser and approaches minPS betas = [0.0, 5.0, 50.0] n_plots = len(betas) + 1 size = 4 fig, axs = plt.subplots(2, n_plots, figsize=(size * n_plots, size)) fig.suptitle( "Expected Sliced plan varying $\\beta$ (inverse temperature)", y=0.95, fontsize=16 ) for beta_idx, beta in enumerate(betas): expected_plan, expected_cost = ot.expected_sliced_plan( X, Y, projections=projections, beta=beta ) print(f"beta={beta}: cost={expected_cost:.2f}") axs[0, beta_idx].set_title(f"$\\beta$={beta}: cost={expected_cost:.2f}") for i in range(n): for j in range(m): w = alpha * expected_plan[i, j].item() * n axs[0, beta_idx].plot( [X[i, 0], Y[j, 0]], [X[i, 1], Y[j, 1]], color="black", alpha=w, label="Expected Sliced plan" if i == 0 and j == 0 else None, ) axs[0, beta_idx].scatter(X[:, 0], X[:, 1], label="X") axs[0, beta_idx].scatter(Y[:, 0], Y[:, 1], label="Y") axs[1, beta_idx].imshow(expected_plan, interpolation="nearest", cmap="Blues") # draw min sliced permutation (limit when beta -> +inf) axs[0, -1].set_title(f"Min Sliced Transport: cost={min_cost:.2f}") for i in range(X.shape[0]): for j in range(Y.shape[0]): if min_plan[i, j] > 0: axs[0, -1].plot( [X[i, 0], Y[j, 0]], [X[i, 1], Y[j, 1]], color="black", alpha=alpha, ) axs[0, -1].scatter(X[:, 0], X[:, 1], label="X") axs[0, -1].scatter(Y[:, 0], Y[:, 1], label="Y") axs[1, -1].imshow(min_plan, interpolation="nearest", cmap="Blues") for ax in axs.flatten(): ax.set_aspect("equal") ax.set_xticks([]) ax.set_yticks([]) fig.tight_layout() python-pot-0.9.7+dfsg/examples/sliced-wasserstein/plot_sliced_wasserstein_scaler.py000066400000000000000000000072661525521106200310620ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ============================================================ Sliced Wasserstein Distance with input scaling (DataScaler) ============================================================ .. note:: Example added in release: 0.9.7. This example illustrates why input scaling matters when computing the Sliced Wasserstein Distance (SWD) between distributions whose features have very different magnitudes. Without scaling, the SWD is dominated by high-magnitude features and may miss meaningful differences in low-magnitude features. The :class:`ot.utils.DataScaler` class fits normalization statistics once on a representative sample and applies the same fixed transformation on every call. This is preferred over re-normalizing inside each SWD call because the transformation stays consistent across mini-batches during optimization. """ # Author: Harguna Sood # # License: MIT License import matplotlib.pylab as pl import numpy as np import ot ############################################################################## # Generate two 2D distributions with mismatched feature scales # ------------------------------------------------------------ # # Feature 1 is on the scale of 1000 with random noise. # Feature 2 is on the scale of 1 with a meaningful 5-sigma shift between # source and target distributions. # %% parameters and data generation rng = np.random.RandomState(0) n = 500 X_s = np.column_stack( [ rng.normal(1000, 100, n), # feature 1: large scale, no real signal rng.normal(0, 1, n), # feature 2: small scale, no shift ] ) X_t = np.column_stack( [ rng.normal(1000, 100, n), # feature 1: same distribution as source rng.normal(5, 1, n), # feature 2: shifted by 5 std ] ) ############################################################################## # SWD without scaling # ------------------- # # Because feature 1 has values ~1000x larger than feature 2, the random # projections used in SWD are dominated by feature 1. The meaningful shift # in feature 2 is buried. # %% SWD without scaling swd_raw = ot.sliced_wasserstein_distance(X_s, X_t, n_projections=200, seed=0) print("SWD without scaling: {:.4f}".format(swd_raw)) ############################################################################## # SWD with DataScaler # ------------------- # # Fit a standard scaler jointly on both distributions, then pass it to SWD. # The same fixed statistics are reused on every call, giving a stable loss # across mini-batches. # %% SWD with DataScaler scaler = ot.utils.DataScaler(norm="standard").fit([X_s, X_t]) swd_scaled = ot.sliced_wasserstein_distance( X_s, X_t, n_projections=200, seed=0, scaler=scaler ) print("SWD with DataScaler: {:.4f}".format(swd_scaled)) ############################################################################## # Visualize raw vs. scaled distributions # --------------------------------------- # %% plot distributions X_s_n = scaler.transform(X_s) X_t_n = scaler.transform(X_t) pl.figure(1, figsize=(12, 5)) pl.subplot(1, 2, 1) pl.scatter(X_s[:, 0], X_s[:, 1], alpha=0.5, label="$X_s$", s=10) pl.scatter(X_t[:, 0], X_t[:, 1], alpha=0.5, label="$X_t$", s=10) pl.title("Raw distributions\n(feature 2 signal hidden by feature 1 scale)") pl.xlabel("Feature 1 (large scale)") pl.ylabel("Feature 2 (small scale)") pl.legend() pl.subplot(1, 2, 2) pl.scatter(X_s_n[:, 0], X_s_n[:, 1], alpha=0.5, label="$X_s$ normalized", s=10) pl.scatter(X_t_n[:, 0], X_t_n[:, 1], alpha=0.5, label="$X_t$ normalized", s=10) pl.title("Normalized distributions\n(feature 2 shift clearly visible)") pl.xlabel("Feature 1 (normalized)") pl.ylabel("Feature 2 (normalized)") pl.legend() pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/sliced-wasserstein/plot_variance.py000066400000000000000000000047741525521106200254300ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ =============================================== Sliced Wasserstein Distance on 2D distributions =============================================== .. note:: Example added in release: 0.8.0. This example illustrates the computation of the sliced Wasserstein Distance as proposed in [31]. [31] Bonneel, Nicolas, et al. "Sliced and radon wasserstein barycenters of measures." Journal of Mathematical Imaging and Vision 51.1 (2015): 22-45 """ # Author: Adrien Corenflos # # License: MIT License # sphinx_gallery_thumbnail_number = 2 import matplotlib.pylab as pl import numpy as np import ot ############################################################################## # Generate data # ------------- # %% parameters and data generation n = 200 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([4, 4]) cov_t = np.array([[1, -0.8], [-0.8, 1]]) xs = ot.datasets.make_2D_samples_gauss(n, mu_s, cov_s) xt = ot.datasets.make_2D_samples_gauss(n, mu_t, cov_t) a, b = np.ones((n,)) / n, np.ones((n,)) / n # uniform distribution on samples ############################################################################## # Plot data # --------- # %% plot samples pl.figure(1) pl.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") pl.plot(xt[:, 0], xt[:, 1], "xr", label="Target samples") pl.legend(loc=0) pl.title("Source and target distributions") ############################################################################### # Sliced Wasserstein distance for different seeds and number of projections # ------------------------------------------------------------------------- n_seed = 20 n_projections_arr = np.logspace(0, 3, 10, dtype=int) res = np.empty((n_seed, 10)) # %% Compute statistics for seed in range(n_seed): for i, n_projections in enumerate(n_projections_arr): res[seed, i] = ot.sliced_wasserstein_distance( xs, xt, a, b, n_projections, seed=seed ) res_mean = np.mean(res, axis=0) res_std = np.std(res, axis=0) ############################################################################### # Plot Sliced Wasserstein Distance # -------------------------------- pl.figure(2) pl.plot(n_projections_arr, res_mean, label="SWD") pl.fill_between( n_projections_arr, res_mean - 2 * res_std, res_mean + 2 * res_std, alpha=0.5 ) pl.legend() pl.xscale("log") pl.xlabel("Number of projections") pl.ylabel("Distance") pl.title("Sliced Wasserstein Distance with 95% confidence interval") pl.show() python-pot-0.9.7+dfsg/examples/sliced-wasserstein/plot_variance_ssw.py000066400000000000000000000061131525521106200263110ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ==================================================== Spherical Sliced Wasserstein on distributions in S^2 ==================================================== .. note:: Example added in release: 0.8.0. This example illustrates the computation of the spherical sliced Wasserstein discrepancy as proposed in [46]. [46] Bonet, C., Berg, P., Courty, N., Septier, F., Drumetz, L., & Pham, M. T. (2023). 'Spherical Sliced-Wasserstein". International Conference on Learning Representations. """ # Author: Clément Bonet # # License: MIT License # sphinx_gallery_thumbnail_number = 1 import matplotlib.pylab as pl import numpy as np import ot ############################################################################## # Generate data # ------------- # %% parameters and data generation n = 200 # nb samples xs = np.random.randn(n, 3) xt = np.random.randn(n, 3) xs = xs / np.sqrt(np.sum(xs**2, -1, keepdims=True)) xt = xt / np.sqrt(np.sum(xt**2, -1, keepdims=True)) a, b = np.ones((n,)) / n, np.ones((n,)) / n # uniform distribution on samples ############################################################################## # Plot data # --------- # %% plot samples fig = pl.figure(figsize=(10, 10)) ax = pl.axes(projection="3d") ax.grid(False) u, v = np.mgrid[0 : 2 * np.pi : 30j, 0 : np.pi : 30j] x = np.cos(u) * np.sin(v) y = np.sin(u) * np.sin(v) z = np.cos(v) ax.plot_surface(x, y, z, color="gray", alpha=0.03) ax.plot_wireframe(x, y, z, linewidth=1, alpha=0.25, color="gray") ax.scatter(xs[:, 0], xs[:, 1], xs[:, 2], label="Source") ax.scatter(xt[:, 0], xt[:, 1], xt[:, 2], label="Target") fs = 10 # Labels ax.set_xlabel("x", fontsize=fs) ax.set_ylabel("y", fontsize=fs) ax.set_zlabel("z", fontsize=fs) ax.view_init(20, 120) ax.set_xlim(-1.5, 1.5) ax.set_ylim(-1.5, 1.5) ax.set_zlim(-1.5, 1.5) # Ticks ax.set_xticks([-1, 0, 1]) ax.set_yticks([-1, 0, 1]) ax.set_zticks([-1, 0, 1]) pl.legend(loc=0) pl.title("Source and Target distribution") ############################################################################### # Spherical Sliced Wasserstein for different seeds and number of projections # -------------------------------------------------------------------------- n_seed = 20 n_projections_arr = np.logspace(0, 3, 10, dtype=int) res = np.empty((n_seed, 10)) # %% Compute statistics for seed in range(n_seed): for i, n_projections in enumerate(n_projections_arr): res[seed, i] = ot.sliced_wasserstein_sphere( xs, xt, a, b, n_projections, seed=seed, p=1 ) res_mean = np.mean(res, axis=0) res_std = np.std(res, axis=0) ############################################################################### # Plot Spherical Sliced Wasserstein # --------------------------------- pl.figure(2) pl.plot(n_projections_arr, res_mean, label=r"$SSW_1$") pl.fill_between( n_projections_arr, res_mean - 2 * res_std, res_mean + 2 * res_std, alpha=0.5 ) pl.legend() pl.xscale("log") pl.xlabel("Number of projections") pl.ylabel("Distance") pl.title("Spherical Sliced Wasserstein Distance with 95% confidence interval") pl.show() python-pot-0.9.7+dfsg/examples/unbalanced-partial/000077500000000000000000000000001525521106200221325ustar00rootroot00000000000000python-pot-0.9.7+dfsg/examples/unbalanced-partial/index.rst000066400000000000000000000000641525521106200237730ustar00rootroot00000000000000 Unbalanced and Partial OT -------------------------python-pot-0.9.7+dfsg/examples/unbalanced-partial/plot_UOT_1D.py000066400000000000000000000077541525521106200245520ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ =============================== 1D Unbalanced optimal transport =============================== This example illustrates the computation of Unbalanced Optimal transport using a Kullback-Leibler relaxation. """ # Author: Hicham Janati # Clément Bonet # # License: MIT License # sphinx_gallery_thumbnail_number = 4 import numpy as np import matplotlib.pylab as pl import ot import ot.plot from ot.datasets import make_1D_gauss as gauss import torch ############################################################################## # Generate data # ------------- # %% parameters n = 100 # nb bins # bin positions x = np.arange(n, dtype=np.float64) # Gaussian distributions a = gauss(n, m=20, s=5) # m= mean, s= std b = gauss(n, m=60, s=10) # make distributions unbalanced b *= 5.0 # loss matrix M = ot.dist(x.reshape((n, 1)), x.reshape((n, 1))) ############################################################################## # Plot distributions and loss matrix # ---------------------------------- # %% plot the distributions pl.figure(1, figsize=(6.4, 3)) pl.plot(x, a, "b", label="Source distribution") pl.plot(x, b, "r", label="Target distribution") pl.legend() # plot distributions and loss matrix pl.figure(2, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, M, "Cost matrix M") ############################################################################## # Solve Unbalanced OT with MM Unbalanced # ----------------------------------- # %% MM Unbalanced alpha = 1.0 # Unbalanced KL relaxation parameter Gs, log = ot.unbalanced.mm_unbalanced(a, b, M / M.max(), alpha, verbose=False, log=True) pl.figure(3, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, Gs, "UOT plan") pl.show() pl.figure(4, figsize=(6.4, 3)) pl.plot(x, a, "b", label="Source distribution") pl.plot(x, b, "r", label="Target distribution") pl.fill(x, Gs.sum(1), "b", alpha=0.5, label="Transported source") pl.fill(x, Gs.sum(0), "r", alpha=0.5, label="Transported target") pl.legend(loc="upper right") pl.title("Distributions and transported mass for UOT") pl.show() print("Mass of reweighted marginals:", Gs.sum()) print("Unbalanced OT loss:", log["total_cost"] * M.max()) ############################################################################## # Solve 1D UOT with Frank-Wolfe # ----------------------------- # %% 1D UOT with FW alpha = M.max() # Unbalanced KL relaxation parameter a_reweighted, b_reweighted, loss = ot.unbalanced.uot_1d( torch.tensor(x, dtype=torch.float64), torch.tensor(x, dtype=torch.float64), alpha, u_weights=torch.tensor(a, dtype=torch.float64), v_weights=torch.tensor(b, dtype=torch.float64), p=2, returnCost="total", ) pl.figure(4, figsize=(6.4, 3)) pl.plot(x, a, "b", label="Source distribution") pl.plot(x, b, "r", label="Target distribution") pl.fill(x, a_reweighted, "b", alpha=0.5, label="Transported source") pl.fill(x, b_reweighted, "r", alpha=0.5, label="Transported target") pl.legend(loc="upper right") pl.title("Distributions and transported mass for UOT") pl.show() print("Mass of reweighted marginals:", a_reweighted.sum().item()) print("Unbalanced OT loss:", loss.item()) ############################################################################## # Solve Unbalanced Sinkhorn # ------------------------- # %% Sinkhorn UOT # Sinkhorn epsilon = 0.1 # entropy parameter alpha = 1.0 # Unbalanced KL relaxation parameter Gs = ot.unbalanced.sinkhorn_unbalanced(a, b, M / M.max(), epsilon, alpha, verbose=True) pl.figure(3, figsize=(5, 5)) ot.plot.plot1D_mat(a, b, Gs, "Entropic UOT plan") pl.show() pl.figure(4, figsize=(6.4, 3)) pl.plot(x, a, "b", label="Source distribution") pl.plot(x, b, "r", label="Target distribution") pl.fill(x, Gs.sum(1), "b", alpha=0.5, label="Transported source") pl.fill(x, Gs.sum(0), "r", alpha=0.5, label="Transported target") pl.legend(loc="upper right") pl.title("Distributions and transported mass for UOT") pl.show() print("Mass of reweighted marginals:", Gs.sum()) python-pot-0.9.7+dfsg/examples/unbalanced-partial/plot_UOT_barycenter_1D.py000066400000000000000000000075321525521106200267620ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ =========================================================== 1D Wasserstein barycenter demo for Unbalanced distributions =========================================================== This example illustrates the computation of regularized Wasserstein Barycenter as proposed in [10] for Unbalanced inputs. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. """ # Author: Hicham Janati # # License: MIT License # sphinx_gallery_thumbnail_number = 4 import numpy as np import matplotlib.pylab as pl import ot # necessary for 3d plot even if not used from mpl_toolkits.mplot3d import Axes3D # noqa from matplotlib.collections import PolyCollection ############################################################################## # Generate data # ------------- # parameters n = 100 # nb bins # bin positions x = np.arange(n, dtype=np.float64) # Gaussian distributions a1 = ot.datasets.make_1D_gauss(n, m=20, s=5) # m= mean, s= std a2 = ot.datasets.make_1D_gauss(n, m=60, s=8) # make unbalanced dists a2 *= 3.0 # creating matrix A containing all distributions A = np.vstack((a1, a2)).T n_distributions = A.shape[1] # loss matrix + normalization M = ot.utils.dist0(n) M /= M.max() ############################################################################## # Plot data # --------- # plot the distributions pl.figure(1, figsize=(6.4, 3)) for i in range(n_distributions): pl.plot(x, A[:, i]) pl.title("Distributions") pl.tight_layout() ############################################################################## # Barycenter computation # ---------------------- # non weighted barycenter computation weight = 0.5 # 0<=weight<=1 weights = np.array([1 - weight, weight]) # l2bary bary_l2 = A.dot(weights) # wasserstein reg = 1e-3 alpha = 1.0 bary_wass = ot.unbalanced.barycenter_unbalanced(A, M, reg, alpha, weights=weights) pl.figure(2) pl.clf() pl.subplot(2, 1, 1) for i in range(n_distributions): pl.plot(x, A[:, i]) pl.title("Distributions") pl.subplot(2, 1, 2) pl.plot(x, bary_l2, "r", label="l2") pl.plot(x, bary_wass, "g", label="Wasserstein") pl.legend() pl.title("Barycenters") pl.tight_layout() ############################################################################## # Barycentric interpolation # ------------------------- # barycenter interpolation n_weight = 11 weight_list = np.linspace(0, 1, n_weight) B_l2 = np.zeros((n, n_weight)) B_wass = np.copy(B_l2) for i in range(0, n_weight): weight = weight_list[i] weights = np.array([1 - weight, weight]) B_l2[:, i] = A.dot(weights) B_wass[:, i] = ot.unbalanced.barycenter_unbalanced( A, M, reg, alpha, weights=weights ) # plot interpolation pl.figure(3) cmap = pl.get_cmap("viridis") verts = [] zs = weight_list for i, z in enumerate(zs): ys = B_l2[:, i] verts.append(list(zip(x, ys))) ax = pl.gcf().add_subplot(projection="3d") poly = PolyCollection(verts, facecolors=[cmap(a) for a in weight_list]) poly.set_alpha(0.7) ax.add_collection3d(poly, zs=zs, zdir="y") ax.set_xlabel("x") ax.set_xlim3d(0, n) ax.set_ylabel(r"$\alpha$") ax.set_ylim3d(0, 1) ax.set_zlabel("") ax.set_zlim3d(0, B_l2.max() * 1.01) pl.title("Barycenter interpolation with l2") pl.tight_layout() pl.figure(4) cmap = pl.get_cmap("viridis") verts = [] zs = weight_list for i, z in enumerate(zs): ys = B_wass[:, i] verts.append(list(zip(x, ys))) ax = pl.gcf().add_subplot(projection="3d") poly = PolyCollection(verts, facecolors=[cmap(a) for a in weight_list]) poly.set_alpha(0.7) ax.add_collection3d(poly, zs=zs, zdir="y") ax.set_xlabel("x") ax.set_xlim3d(0, n) ax.set_ylabel(r"$\alpha$") ax.set_ylim3d(0, 1) ax.set_zlabel("") ax.set_zlim3d(0, B_l2.max() * 1.01) pl.title("Barycenter interpolation with Wasserstein") pl.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/unbalanced-partial/plot_UOT_sliced.py000066400000000000000000000344231525521106200255420ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ =================================== Sliced Unbalanced optimal transport =================================== This example illustrates the behavior of Sliced UOT versus Unbalanced Sliced OT, introduced in [82]. The first one removes outliers on each slice while the second one removes outliers of the original marginals. [82] Bonet, C., Nadjahi, K., Séjourné, T., Fatras, K., & Courty, N. (2025). Slicing Unbalanced Optimal Transport. Transactions on Machine Learning Research. """ # Author: Clément Bonet # Nicolas Courty # # License: MIT License # sphinx_gallery_thumbnail_number = 4 import numpy as np import matplotlib.pylab as pl import ot import torch import matplotlib.pyplot as plt import matplotlib.animation as animation from sklearn.neighbors import KernelDensity ############################################################################## # Generate data # ------------- # %% parameters np.random.seed(42) n_samples = 25 # 500 nb_outliers = 10 # 200 mu_s = np.array([0, 0]) - 0.5 cov_s = 0.2**2 * np.array([[1, 0], [0, 1]]) mu_s_outliers = -np.array([2, 0.5]) cov_s_outliers = 0.05**2 * np.array([[1, 0], [0, 1]]) mu_t = np.array([0, 0]) + 1.5 cov_t = 0.2**2 * np.array([[1, 0], [0, 1]]) def generate_dataset(n_samples): # Generate source data (with outliers) Xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s) Xs_outlier = ot.datasets.make_2D_samples_gauss( nb_outliers, mu_s_outliers, cov_s_outliers ) Xs = np.vstack((Xs, Xs_outlier)) Xs_torch = torch.from_numpy(Xs).type(torch.float) # Generate target data Xt = ot.datasets.make_2D_samples_gauss(n_samples, mu_t, cov_t) Xt_torch = torch.from_numpy(Xt).type(torch.float) return Xs_torch, Xt_torch Xs, Xt = generate_dataset(n_samples) pl.figure(1) pl.scatter(Xs[:, 0], Xs[:, 1], color="blue", label="Source data") pl.scatter(Xt[:, 0], Xt[:, 1], color="red", label="Target data") pl.xlim(-2.4, 2.4) pl.ylim(-1, 2.2) pl.legend() pl.show() ############################################################################## # Compute SUOT and USOT # ------------- # %% p = 2 num_proj = 180 a = torch.ones(Xs.shape[0], dtype=torch.float) b = torch.ones(Xt.shape[0], dtype=torch.float) # construct projections thetas = np.linspace(0, np.pi, num_proj) dir = np.array([(np.cos(theta), np.sin(theta)) for theta in thetas]) dir_torch = torch.from_numpy(dir).type(torch.float) # Coordinates of the projections Xps = (Xs @ dir_torch.T).T # shape (n_projs, n) Xpt = (Xt @ dir_torch.T).T # Projections on the lines projs_Xps = Xps[:, :, None] * dir_torch[:, None, :] # shape (n_projs, n, p) projs_Xpt = Xpt[:, :, None] * dir_torch[:, None, :] # Compute SUOT rho1_SUOT = 1 rho2_SUOT = 1 _, log = ot.unbalanced.sliced_unbalanced_ot( Xs, Xt, (rho1_SUOT, rho2_SUOT), a, b, num_proj, p, numItermax=10, projections=dir_torch.T, log=True, ) A_SUOT, B_SUOT = log["a_reweighted"].T, log["b_reweighted"].T # Compute USOT rho1_USOT = 1 rho2_USOT = 1 A_USOT, B_USOT, _ = ot.unbalanced_sliced_ot( Xs, Xt, (rho1_USOT, rho2_USOT), a, b, num_proj, p, numItermax=10, projections=dir_torch.T, ) ############################################################################## # Sliced Unbalanced OT # -------------------- # SUOT averages UOT problems on different slices. Depending on the slice, SUOT can keep or get rid of the outlier mode. # %% get_rot = lambda theta: np.array( [[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]] ) # visu parameters nb_slices = 180 # 60 offset_degree = int(180 / nb_slices) delta_degree = np.pi / nb_slices colors = plt.cm.Reds(np.linspace(0.3, 1, nb_slices)) X1 = np.array([-4, 0]) X2 = np.array([4, 0]) # max_weights = max(A_SUOT.max(), B_SUOT.max()) pl.figure(1) def _update_plot(i): weights_src = A_SUOT[i * offset_degree, :].cpu().numpy() weights_tgt = B_SUOT[i * offset_degree, :].cpu().numpy() max_weights = max(weights_src.max(), weights_tgt.max()) min_weights = min(weights_src.min(), weights_tgt.min()) weights_src = 0.1 + 0.9 * (weights_src - min_weights) / (max_weights - min_weights) weights_tgt = 0.1 + 0.9 * (weights_tgt - min_weights) / (max_weights - min_weights) R = get_rot(delta_degree * (-i)) X1_r = X1.dot(R) X2_r = X2.dot(R) pl.clf() pl.plot( [X1_r[0], X2_r[0]], [X1_r[1], X2_r[1]], color=colors[i], alpha=0.8, zorder=0 ) for j in range(len(Xs)): pl.plot( [Xs[j, 0], projs_Xps[i * offset_degree, j, 0]], [Xs[j, 1], projs_Xps[i * offset_degree, j, 1]], c="blue", alpha=weights_src[j], ) for j in range(len(Xt)): pl.plot( [Xt[j, 0], projs_Xpt[i * offset_degree, j, 0]], [Xt[j, 1], projs_Xpt[i * offset_degree, j, 1]], c="red", alpha=weights_tgt[j], ) pl.scatter( Xs[:, 0], Xs[:, 1], s=100 * weights_src, alpha=weights_src, zorder=1, color="blue", label="Source data", edgecolor="black", ) pl.scatter( Xt[:, 0], Xt[:, 1], s=100 * weights_tgt, alpha=weights_tgt, zorder=1, color="red", label="Target data", edgecolors="black", ) pl.xlim(-2.4, 2.4) pl.ylim(-1, 2.2) return 1 weights_src = A_SUOT[0, :].cpu().numpy() weights_tgt = B_SUOT[0, :].cpu().numpy() max_weights = max(weights_src.max(), weights_tgt.max()) min_weights = min(weights_src.min(), weights_tgt.min()) weights_src = 0.1 + 0.9 * (weights_src - min_weights) / (max_weights - min_weights) weights_tgt = 0.1 + 0.9 * (weights_tgt - min_weights) / (max_weights - min_weights) X1_r, X2_r = X1, X2 pl.plot( [X1_r[0], X2_r[0]], [X1_r[1], X2_r[1]], color=colors[0], alpha=0.8, zorder=0, label="Directions", ) for j in range(len(Xs)): pl.plot( [Xs[j, 0], projs_Xps[0, j, 0]], [Xs[j, 1], projs_Xps[0, j, 1]], c="blue", alpha=weights_src[j], ) for j in range(len(Xt)): pl.plot( [Xt[j, 0], projs_Xpt[0, j, 0]], [Xt[j, 1], projs_Xpt[0, j, 1]], c="red", alpha=weights_tgt[j], ) pl.scatter( Xs[:, 0], Xs[:, 1], s=100 * weights_src, alpha=weights_src, zorder=1, color="blue", label="Source data", edgecolor="black", ) pl.scatter( Xt[:, 0], Xt[:, 1], s=100 * weights_tgt, alpha=weights_tgt, zorder=1, color="red", label="Target data", edgecolors="black", ) pl.xlim(-2.4, 2.4) pl.ylim(-1, 2.2) ani = animation.FuncAnimation( pl.gcf(), _update_plot, nb_slices, interval=100, # , repeat_delay=2000 ) ############################################################################## # Unbalanced Sliced OT # -------------------- # USOT is able to get rid of the outlier mode on all slices, as it reweights the original distributions. # %% # visu parameters nb_slices = 3 offset_degree = int(180 / nb_slices) delta_degree = np.pi / nb_slices colors = plt.cm.Reds(np.linspace(0.3, 1, nb_slices)) plt.figure(1) for i in range(nb_slices): weights_src = A_USOT.cpu().numpy() weights_tgt = B_USOT.cpu().numpy() max_weights = max(weights_src.max(), weights_tgt.max()) min_weights = min(weights_src.min(), weights_tgt.min()) weights_src = 0.1 + 0.9 * (weights_src - min_weights) / (max_weights - min_weights) weights_tgt = 0.1 + 0.9 * (weights_tgt - min_weights) / (max_weights - min_weights) R = get_rot(delta_degree * (-i)) X1_r = X1.dot(R) X2_r = X2.dot(R) if i == 0: pl.plot( [X1_r[0], X2_r[0]], [X1_r[1], X2_r[1]], color=colors[i], alpha=0.8, zorder=0, label="Directions", ) else: pl.plot( [X1_r[0], X2_r[0]], [X1_r[1], X2_r[1]], color=colors[i], alpha=0.8, zorder=0 ) pl.scatter( Xs[:, 0], Xs[:, 1], s=100 * weights_src, alpha=weights_src, zorder=1, color="blue", label="Source data", edgecolors="black", ) pl.scatter( Xt[:, 0], Xt[:, 1], s=100 * weights_tgt, alpha=weights_tgt, zorder=1, color="red", label="Target data", edgecolors="black", ) pl.xlim(-2.4, 2.4) pl.ylim(-1, 2.2) pl.show() ############################################################################## # Utils plot # ---------- # %% def kde_sklearn(x, x_grid, weights=None, bandwidth=0.2, **kwargs): """Kernel Density Estimation with Scikit-learn""" kde_skl = KernelDensity(bandwidth=bandwidth, **kwargs) if weights is not None: kde_skl.fit(x[:, np.newaxis], sample_weight=weights) else: kde_skl.fit(x[:, np.newaxis]) # score_samples() returns the log-likelihood of the samples log_pdf = kde_skl.score_samples(x_grid[:, np.newaxis]) return np.exp(log_pdf) def plot_slices( col, nb_slices, x_grid, Xps, Xpt, Xps_weights, Xpt_weights, method, rho1, rho2, offset_degree, bw=0.05, ): """ Plot the density (using a kernel estimator) of the projections on each of the slices. Parameters ---------- col: int Column of the subplot nb_slices: int Number of slices on which we project x_grid: numpy array Grid of the x-abscisse Xps: array-like of shape (nb_slices, n_points) Projections of the 1st marginal in 1D Xpt: array-like of shape (nb_slices, m_points) Projections of the 2nd marginal in 1D Xps_weights: array_like of shape (nb_slices, n_points) Weights of the projections Xps Xpt_weights: array_like of shape (nb_slices, m_points) Weights of the projections Xpt method: str Legend rho1: int Legend rho2: int Legend offset_degree: int bw: float Bandwidth for the KDE estimation """ for i in range(nb_slices): ax = plt.subplot2grid((nb_slices, 3), (i, col)) if len(Xps_weights.shape) > 1: # SUOT weights_src = Xps_weights[i * offset_degree, :].cpu().numpy() weights_tgt = Xpt_weights[i * offset_degree, :].cpu().numpy() else: # USOT weights_src = Xps_weights.cpu().numpy() weights_tgt = Xpt_weights.cpu().numpy() samples_src = Xps[i * offset_degree, :].cpu().numpy() samples_tgt = Xpt[i * offset_degree, :].cpu().numpy() pdf_source = kde_sklearn(samples_src, x_grid, weights=weights_src, bandwidth=bw) pdf_target = kde_sklearn(samples_tgt, x_grid, weights=weights_tgt, bandwidth=bw) pdf_source_without_w = kde_sklearn(samples_src, x_grid, bandwidth=bw) pdf_target_without_w = kde_sklearn(samples_tgt, x_grid, bandwidth=bw) ax.plot(x_grid, pdf_source, color="blue", alpha=0.8, lw=2) ax.fill(x_grid, pdf_source_without_w, ec="grey", fc="grey", alpha=0.3) ax.fill(x_grid, pdf_source, ec="blue", fc="blue", alpha=0.3) ax.plot(x_grid, pdf_target, color="red", alpha=0.8, lw=2) ax.fill(x_grid, pdf_target_without_w, ec="grey", fc="grey", alpha=0.3) ax.fill(x_grid, pdf_target, ec="blue", fc="red", alpha=0.3) ax.set_xlim(xlim_min, xlim_max) if col == 1: ax.set_ylabel( r"$\theta=${}$^o$".format(i * offset_degree), color=colors[i], fontsize=13, ) ax.set_yticks([]) ax.set_xticks([]) ax.set_xlabel( r"{} $\rho_1={}$ $\rho_2={}$".format(method, rho1, rho2), fontsize=13 ) ############################################################################## # Plot reweighted distributions on several slices # ------------- # We plot the reweighted distributions on several slices (replicating Figure 1 of [82]). We see that for SUOT, # the mode of outliers is kept of some slices (e.g. for :math:`\theta=120°`) while USOT # is able to get rid of the outlier mode. # %% get_rot = lambda theta: np.array( [[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]] ) n_samples = 500 nb_outliers = 200 Xs, Xt = generate_dataset(n_samples) Xps = (Xs @ dir_torch.T).T # shape (n_projs, n) Xpt = (Xt @ dir_torch.T).T a = torch.ones(Xs.shape[0], dtype=torch.float) b = torch.ones(Xt.shape[0], dtype=torch.float) rho1_SUOT = 1 rho2_SUOT = 1 _, log = ot.unbalanced.sliced_unbalanced_ot( Xs, Xt, (rho1_SUOT, rho2_SUOT), a, b, num_proj, p, numItermax=10, projections=dir_torch.T, log=True, ) A_SUOT, B_SUOT = log["a_reweighted"].T, log["b_reweighted"].T rho1_USOT = 1 rho2_USOT = 1 A_USOT, B_USOT, _ = ot.unbalanced_sliced_ot( Xs, Xt, (rho1_USOT, rho2_USOT), a, b, num_proj, p, numItermax=10, projections=dir_torch.T, ) # define plotting grid xlim_min = -3 xlim_max = 3 x_grid = np.linspace(xlim_min, xlim_max, 200) # visu parameters nb_slices = 3 offset_degree = int(180 / nb_slices) delta_degree = np.pi / nb_slices colors = plt.cm.Reds(np.linspace(0.3, 1, nb_slices)) X1 = np.array([-4, 0]) X2 = np.array([4, 0]) fig = plt.figure(figsize=(9, 3)) ax1 = plt.subplot2grid((nb_slices, 3), (0, 0), rowspan=nb_slices) for i in range(nb_slices): R = get_rot(delta_degree * (-i)) X1_r = X1.dot(R) X2_r = X2.dot(R) if i == 0: ax1.plot( [X1_r[0], X2_r[0]], [X1_r[1], X2_r[1]], color=colors[i], alpha=0.8, zorder=0, label="Directions", ) else: ax1.plot( [X1_r[0], X2_r[0]], [X1_r[1], X2_r[1]], color=colors[i], alpha=0.8, zorder=0 ) ax1.scatter(Xs[:, 0], Xs[:, 1], zorder=1, color="blue", label="Source data") ax1.scatter(Xt[:, 0], Xt[:, 1], zorder=1, color="red", label="Target data") ax1.set_xlim([-3, 3]) ax1.set_ylim([-3, 3]) ax1.set_yticks([]) ax1.set_xticks([]) # ax1.legend(loc='best',fontsize=13) ax1.set_xlabel("Original distributions", fontsize=13) fig.subplots_adjust(hspace=0) fig.subplots_adjust(wspace=0.15) plot_slices( 1, nb_slices, x_grid, Xps, Xpt, A_SUOT, B_SUOT, "SUOT", rho1_SUOT, rho2_SUOT, offset_degree, ) plot_slices( 2, nb_slices, x_grid, Xps, Xpt, A_USOT, B_USOT, "USOT", rho1_USOT, rho2_USOT, offset_degree, ) plt.show() python-pot-0.9.7+dfsg/examples/unbalanced-partial/plot_conv_sinkhorn_ti.py000066400000000000000000000064151525521106200271240ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ =============================================================== Translation Invariant Sinkhorn for Unbalanced Optimal Transport =============================================================== .. note:: Example added in release: 0.9.5. This examples illustrates the better convergence of the translation invariance Sinkhorn algorithm proposed in [73] compared to the classical Sinkhorn algorithm. [73] Séjourné, T., Vialard, F. X., & Peyré, G. (2022). Faster unbalanced optimal transport: Translation invariant sinkhorn and 1-d frank-wolfe. In International Conference on Artificial Intelligence and Statistics (pp. 4995-5021). PMLR. """ # Author: Clément Bonet # License: MIT License import numpy as np import matplotlib.pylab as pl import ot ############################################################################## # Setting parameters # ------------- # %% parameters n_iter = 50 # nb iters n = 40 # nb samples num_iter_max = 100 n_noise = 10 reg = 0.005 reg_m_kl = 0.05 mu_s = np.array([-1, -1]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([4, 4]) cov_t = np.array([[1, -0.8], [-0.8, 1]]) ############################################################################## # Compute entropic kl-regularized UOT with Sinkhorn and Translation Invariant Sinkhorn # ----------- err_sinkhorn_uot = np.empty((n_iter, num_iter_max)) err_sinkhorn_uot_ti = np.empty((n_iter, num_iter_max)) for seed in range(n_iter): np.random.seed(seed) xs = ot.datasets.make_2D_samples_gauss(n, mu_s, cov_s) xt = ot.datasets.make_2D_samples_gauss(n, mu_t, cov_t) xs = np.concatenate((xs, (np.random.rand(n_noise, 2) - 4)), axis=0) xt = np.concatenate((xt, (np.random.rand(n_noise, 2) + 6)), axis=0) n = n + n_noise a, b = np.ones((n,)) / n, np.ones((n,)) / n # uniform distribution on samples # loss matrix M = ot.dist(xs, xt) M /= M.max() entropic_kl_uot, log_uot = ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg, reg_m_kl, reg_type="kl", log=True, numItermax=num_iter_max, stopThr=0, ) entropic_kl_uot_ti, log_uot_ti = ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg, reg_m_kl, reg_type="kl", method="sinkhorn_translation_invariant", log=True, numItermax=num_iter_max, stopThr=0, ) err_sinkhorn_uot[seed] = log_uot["err"] err_sinkhorn_uot_ti[seed] = log_uot_ti["err"] ############################################################################## # Plot the results # ---------------- mean_sinkh = np.mean(err_sinkhorn_uot, axis=0) std_sinkh = np.std(err_sinkhorn_uot, axis=0) mean_sinkh_ti = np.mean(err_sinkhorn_uot_ti, axis=0) std_sinkh_ti = np.std(err_sinkhorn_uot_ti, axis=0) absc = list(range(num_iter_max)) pl.plot(absc, mean_sinkh, label="Sinkhorn") pl.fill_between(absc, mean_sinkh - 2 * std_sinkh, mean_sinkh + 2 * std_sinkh, alpha=0.5) pl.plot(absc, mean_sinkh_ti, label="Translation Invariant Sinkhorn") pl.fill_between( absc, mean_sinkh_ti - 2 * std_sinkh_ti, mean_sinkh_ti + 2 * std_sinkh_ti, alpha=0.5 ) pl.yscale("log") pl.legend() pl.xlabel("Number of Iterations") pl.ylabel(r"$\|u-v\|_\infty$") pl.grid(True) pl.show() python-pot-0.9.7+dfsg/examples/unbalanced-partial/plot_entropic_partial_wasserstein_logscale.py000066400000000000000000000105741525521106200334100ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ========================================================================== Numerically-stable entropic partial Wasserstein (log-domain solver) ========================================================================== .. note:: Example added in release: 0.9.7. `ot.partial.entropic_partial_wasserstein` is numerically unstable at small regularisation: the iterates underflow to zero and the returned plan contains NaNs (see PythonOT/POT issue #723). This example reproduces the failure mode on a small problem and shows that the log-domain solver, selected with ``entropic_partial_wasserstein(..., method='sinkhorn_log')`` (equivalently :any:`ot.partial.entropic_partial_wasserstein_logscale`), produces a finite plan over the same sweep, agreeing with the original solver at large ``reg`` and degrading gracefully at small ``reg``. Following the :any:`ot.sinkhorn` convention, the solver to use is chosen through the ``method`` parameter: ``'sinkhorn'`` (default) for the classical solver and ``'sinkhorn_log'`` for the log-domain one. The log-domain solver is slower per iteration than the standard one, so the recommendation is to use the standard solver by default and fall back to the log-domain solver when ``reg`` is small enough to risk underflow. """ # Author: wzm2256 (original PR #724) # License: MIT License import numpy as np import scipy as sp import matplotlib.pylab as pl import ot ############################################################################## # Construct a 50x50 cost matrix # ----------------------------- # # Mirrors the cost-matrix scale (~50) used in PythonOT/POT issue #723. rng = np.random.RandomState(0) n = 50 xs = rng.rand(n, 2) xt = rng.rand(n, 2) M = sp.spatial.distance.cdist(xs, xt) * 50.0 a = np.ones(n) / n b = np.ones(n) / n m = 0.6 # transport ~60% of the mass ############################################################################## # Sweep regularisation # -------------------- # # Run both solvers across a range of ``reg`` values. On this 50×50 problem # at cost-scale 50 the standard solver returns NaN at the ``reg`` values # closest to the underflow boundary (typically ``reg`` ~0.05–0.01 in our # runs, though the exact transition depends on the BLAS / platform's # float64 underflow behaviour); the log-domain solver stays finite over # the whole sweep, including the very small ``reg`` regime where the # standard exp(−M/reg) path would underflow to zero everywhere. regs = [1.0, 0.5, 0.1, 0.05, 0.01, 5e-3, 1e-3, 5e-4] standard_finite = [] logscale_finite = [] standard_mass = [] logscale_mass = [] for reg in regs: G_std = ot.partial.entropic_partial_wasserstein( a, b, M, reg=reg, m=m, numItermax=2000 ) G_log = ot.partial.entropic_partial_wasserstein( a, b, M, reg=reg, m=m, method="sinkhorn_log", numItermax=2000 ) standard_finite.append(bool(np.isfinite(G_std).all())) logscale_finite.append(bool(np.isfinite(G_log).all())) standard_mass.append(float(G_std.sum()) if np.isfinite(G_std).all() else np.nan) logscale_mass.append(float(G_log.sum())) print( "reg standard_finite logscale_finite std_mass logscale_mass (target m={:.2f})".format( m ) ) for reg, sf, lf, sm, lm in zip( regs, standard_finite, logscale_finite, standard_mass, logscale_mass ): print(f"{reg:>10.4g} {str(sf):<14} {str(lf):<14} {sm:>8.3f} {lm:>8.3f}") ############################################################################## # Plot the resulting plans at large vs. small reg # ----------------------------------------------- fig, axes = pl.subplots(2, 2, figsize=(9, 8)) for ax, reg in zip(axes[:, 0], (1.0, 0.01)): G_std = ot.partial.entropic_partial_wasserstein( a, b, M, reg=reg, m=m, numItermax=2000 ) if not np.isfinite(G_std).all(): G_std = np.zeros_like(G_std) ax.set_title(f"standard, reg={reg} (NaN)") else: ax.set_title(f"standard, reg={reg}") ax.imshow(G_std, cmap="viridis", aspect="auto") ax.set_xlabel("target") ax.set_ylabel("source") for ax, reg in zip(axes[:, 1], (1.0, 0.01)): G_log = ot.partial.entropic_partial_wasserstein( a, b, M, reg=reg, m=m, method="sinkhorn_log", numItermax=2000 ) ax.set_title(f"logscale, reg={reg}") ax.imshow(G_log, cmap="viridis", aspect="auto") ax.set_xlabel("target") ax.set_ylabel("source") fig.tight_layout() pl.show() python-pot-0.9.7+dfsg/examples/unbalanced-partial/plot_partial_1d.py000066400000000000000000000047471525521106200255760ustar00rootroot00000000000000""" ========================= Partial Wasserstein in 1D ========================= This script demonstrates how to compute and visualize the Partial Wasserstein distance between two 1D discrete distributions using `ot.partial.partial_wasserstein_1d`. We illustrate the intermediate transport plans for all `k = 1...n`, where `n = min(len(x_a), len(x_b))`. """ # sphinx_gallery_thumbnail_number = 5 import numpy as np import matplotlib.pyplot as plt from ot.partial import partial_wasserstein_1d def plot_partial_transport( ax, x_a, x_b, indices_a=None, indices_b=None, marginal_costs=None ): y_a = np.ones_like(x_a) y_b = -np.ones_like(x_b) min_min = min(x_a.min(), x_b.min()) max_max = max(x_a.max(), x_b.max()) ax.plot([min_min - 1, max_max + 1], [1, 1], "k-", lw=0.5, alpha=0.5) ax.plot([min_min - 1, max_max + 1], [-1, -1], "k-", lw=0.5, alpha=0.5) # Plot transport lines if indices_a is not None and indices_b is not None: subset_a = np.sort(x_a[indices_a]) subset_b = np.sort(x_b[indices_b]) for x_a_i, x_b_j in zip(subset_a, subset_b): ax.plot([x_a_i, x_b_j], [1, -1], "k--", alpha=0.7) # Plot all points ax.plot(x_a, y_a, "o", color="C0", label="x_a", markersize=8) ax.plot(x_b, y_b, "o", color="C1", label="x_b", markersize=8) if marginal_costs is not None: k = len(marginal_costs) ax.set_title( f"Partial Transport - k = {k}, Cumulative Cost = {sum(marginal_costs):.2f}", fontsize=16, ) else: ax.set_title("Original 1D Discrete Distributions", fontsize=16) ax.legend(loc="upper right", fontsize=14) ax.set_yticks([]) ax.set_xticks([]) ax.set_ylim(-2, 2) ax.set_xlim(min(x_a.min(), x_b.min()) - 1, max(x_a.max(), x_b.max()) + 1) ax.axis("off") # Simulate two 1D discrete distributions np.random.seed(0) n = 6 x_a = np.sort(np.random.uniform(0, 10, size=n)) x_b = np.sort(np.random.uniform(0, 10, size=n)) # Plot original distributions plt.figure(figsize=(6, 2)) plot_partial_transport(plt.gca(), x_a, x_b) plt.show() # %% indices_a, indices_b, marginal_costs = partial_wasserstein_1d(x_a, x_b) # Compute cumulative cost cumulative_costs = np.cumsum(marginal_costs) # Visualize all partial transport plans for k in range(n): plt.figure(figsize=(6, 2)) plot_partial_transport( plt.gca(), x_a, x_b, indices_a[: k + 1], indices_b[: k + 1], marginal_costs[: k + 1], ) plt.show() python-pot-0.9.7+dfsg/examples/unbalanced-partial/plot_partial_wass_and_gromov.py000077500000000000000000000126301525521106200304530ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ================================================== Partial Wasserstein and Gromov-Wasserstein example ================================================== .. note:: Example added in release: 0.7.0. This example is designed to show how to use the Partial (Gromov-)Wasserstein distance computation in POT [29]. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. """ # Author: Laetitia Chapel # License: MIT License # sphinx_gallery_thumbnail_number = 2 # necessary for 3d plot even if not used from mpl_toolkits.mplot3d import Axes3D # noqa import scipy as sp import numpy as np import matplotlib.pylab as pl import ot ############################################################################# # # Sample two 2D Gaussian distributions and plot them # -------------------------------------------------- # # For demonstration purpose, we sample two Gaussian distributions in 2-d # spaces and add some random noise. n_samples = 20 # nb samples (gaussian) n_noise = 20 # nb of samples (noise) mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 2]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov) xs = np.append(xs, (np.random.rand(n_noise, 2) + 1) * 4).reshape((-1, 2)) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov) xt = np.append(xt, (np.random.rand(n_noise, 2) + 1) * -3).reshape((-1, 2)) M = sp.spatial.distance.cdist(xs, xt) fig = pl.figure() ax1 = fig.add_subplot(131) ax1.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") ax2 = fig.add_subplot(132) ax2.scatter(xt[:, 0], xt[:, 1], color="r") ax3 = fig.add_subplot(133) ax3.imshow(M) pl.show() ############################################################################# # # Compute partial Wasserstein plans and distance # ---------------------------------------------- p = ot.unif(n_samples + n_noise) q = ot.unif(n_samples + n_noise) w0, log0 = ot.partial.partial_wasserstein(p, q, M, m=0.5, log=True) w, log = ot.partial.entropic_partial_wasserstein(p, q, M, reg=0.1, m=0.5, log=True) print("Partial Wasserstein distance (m = 0.5): " + str(log0["partial_w_dist"])) print("Entropic partial Wasserstein distance (m = 0.5): " + str(log["partial_w_dist"])) pl.figure(1, (10, 5)) pl.subplot(1, 2, 1) pl.imshow(w0, cmap="gray_r") pl.title("Partial Wasserstein") pl.subplot(1, 2, 2) pl.imshow(w, cmap="gray_r") pl.title("Entropic partial Wasserstein") pl.show() ############################################################################# # # Sample one 2D and 3D Gaussian distributions and plot them # --------------------------------------------------------- # # The Gromov-Wasserstein distance allows to compute distances with samples that # do not belong to the same metric space. For demonstration purpose, we sample # two Gaussian distributions in 2- and 3-dimensional spaces. n_samples = 20 # nb samples n_noise = 10 # nb of samples (noise) p = ot.unif(n_samples + n_noise) q = ot.unif(n_samples + n_noise) mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([0, 0, 0]) cov_t = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s) xs = np.concatenate((xs, ((np.random.rand(n_noise, 2) + 1) * 4)), axis=0) P = sp.linalg.sqrtm(cov_t) xt = np.random.randn(n_samples, 3).dot(P) + mu_t xt = np.concatenate((xt, ((np.random.rand(n_noise, 3) + 1) * 10)), axis=0) fig = pl.figure() ax1 = fig.add_subplot(121) ax1.plot(xs[:, 0], xs[:, 1], "+b", label="Source samples") ax2 = fig.add_subplot(122, projection="3d") ax2.scatter(xt[:, 0], xt[:, 1], xt[:, 2], color="r") pl.show() ############################################################################# # # Compute partial Gromov-Wasserstein plans and distance # ----------------------------------------------------- C1 = sp.spatial.distance.cdist(xs, xs) C2 = sp.spatial.distance.cdist(xt, xt) # transport 100% of the mass print("------m = 1") m = 1 res0, log0 = ot.gromov.partial_gromov_wasserstein(C1, C2, p, q, m=m, log=True) res, log = ot.gromov.entropic_partial_gromov_wasserstein( C1, C2, p, q, 10, m=m, log=True, verbose=True ) print("Wasserstein distance (m = 1): " + str(log0["partial_gw_dist"])) print("Entropic Wasserstein distance (m = 1): " + str(log["partial_gw_dist"])) pl.figure(1, (10, 5)) pl.title("mass to be transported m = 1") pl.axis("off") pl.subplot(1, 2, 1) pl.imshow(res0, cmap="gray_r") pl.title("Gromov-Wasserstein") pl.subplot(1, 2, 2) pl.imshow(res, cmap="gray_r") pl.title("Entropic Gromov-Wasserstein") pl.show() # transport 2/3 of the mass print("------m = 2/3") m = 2 / 3 res0, log0 = ot.gromov.partial_gromov_wasserstein( C1, C2, p, q, m=m, log=True, verbose=True ) res, log = ot.gromov.entropic_partial_gromov_wasserstein( C1, C2, p, q, 10, m=m, log=True, verbose=True ) print("Partial Wasserstein distance (m = 2/3): " + str(log0["partial_gw_dist"])) print("Entropic partial Wasserstein distance (m = 2/3): " + str(log["partial_gw_dist"])) pl.figure(2, (10, 5)) pl.title("mass to be transported m = 2/3") pl.axis("off") pl.subplot(1, 2, 1) pl.imshow(res0, cmap="gray_r") pl.title("Partial Gromov-Wasserstein") pl.subplot(1, 2, 2) pl.imshow(res, cmap="gray_r") pl.title("Entropic partial Gromov-Wasserstein") pl.show() python-pot-0.9.7+dfsg/examples/unbalanced-partial/plot_regpath.py000066400000000000000000000165631525521106200252070ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ================================================================ Regularization path of l2-penalized unbalanced optimal transport ================================================================ .. note:: Example added in release: 0.8.0. This example illustrate the regularization path for 2D unbalanced optimal transport. We present here both the fully relaxed case and the semi-relaxed case. [Chapel et al., 2021] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. """ # Author: Haoran Wu # License: MIT License # sphinx_gallery_thumbnail_number = 2 import numpy as np import matplotlib.pylab as pl import ot import matplotlib.animation as animation ############################################################################## # Generate data # ------------- # %% parameters and data generation n = 20 # nb samples mu_s = np.array([-1, -1]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([4, 4]) cov_t = np.array([[1, -0.8], [-0.8, 1]]) np.random.seed(0) xs = ot.datasets.make_2D_samples_gauss(n, mu_s, cov_s) xt = ot.datasets.make_2D_samples_gauss(n, mu_t, cov_t) a, b = np.ones((n,)) / n, np.ones((n,)) / n # uniform distribution on samples # loss matrix M = ot.dist(xs, xt) M /= M.max() ############################################################################## # Plot data # --------- # %% plot 2 distribution samples pl.figure(1) pl.scatter(xs[:, 0], xs[:, 1], c="C0", label="Source") pl.scatter(xt[:, 0], xt[:, 1], c="C1", label="Target") pl.legend(loc=2) pl.title("Source and target distributions") pl.show() ############################################################################## # Compute semi-relaxed and fully relaxed regularization paths # ----------------------------------------------------------- # %% final_gamma = 1e-6 t, t_list, g_list = ot.regpath.regularization_path( a, b, M, reg=final_gamma, semi_relaxed=False ) t2, t_list2, g_list2 = ot.regpath.regularization_path( a, b, M, reg=final_gamma, semi_relaxed=True ) ############################################################################## # Plot the regularization path # ---------------------------- # # The OT plan is plotted as a function of $\gamma$ that is the inverse of the # weight on the marginal relaxations. # %% fully relaxed l2-penalized UOT pl.figure(2) selected_gamma = [2e-1, 1e-1, 5e-2, 1e-3] for p in range(4): tp = ot.regpath.compute_transport_plan(selected_gamma[p], g_list, t_list) P = tp.reshape((n, n)) pl.subplot(2, 2, p + 1) if P.sum() > 0: P = P / P.max() for i in range(n): for j in range(n): if P[i, j] > 0: pl.plot( [xs[i, 0], xt[j, 0]], [xs[i, 1], xt[j, 1]], color="C2", alpha=P[i, j] * 0.3, ) pl.scatter(xs[:, 0], xs[:, 1], c="C0", alpha=0.2) pl.scatter(xt[:, 0], xt[:, 1], c="C1", alpha=0.2) pl.scatter( xs[:, 0], xs[:, 1], c="C0", s=P.sum(1).ravel() * (1 + p) * 2, label="Re-weighted source", alpha=1, ) pl.scatter( xt[:, 0], xt[:, 1], c="C1", s=P.sum(0).ravel() * (1 + p) * 2, label="Re-weighted target", alpha=1, ) pl.plot([], [], color="C2", alpha=0.8, label="OT plan") pl.title(r"$\ell_2$ UOT $\gamma$={}".format(selected_gamma[p]), fontsize=11) if p < 2: pl.xticks(()) pl.show() # %% # Animation of the regpath for UOT l2 # ----------------------------------- nv = 50 g_list_v = np.logspace(-0.5, -2.5, nv) pl.figure(3) def _update_plot(iv): pl.clf() tp = ot.regpath.compute_transport_plan(g_list_v[iv], g_list, t_list) P = tp.reshape((n, n)) if P.sum() > 0: P = P / P.max() for i in range(n): for j in range(n): if P[i, j] > 0: pl.plot( [xs[i, 0], xt[j, 0]], [xs[i, 1], xt[j, 1]], color="C2", alpha=P[i, j] * 0.5, ) pl.scatter(xs[:, 0], xs[:, 1], c="C0", alpha=0.2) pl.scatter(xt[:, 0], xt[:, 1], c="C1", alpha=0.2) pl.scatter( xs[:, 0], xs[:, 1], c="C0", s=P.sum(1).ravel() * (1 + p) * 4, label="Re-weighted source", alpha=1, ) pl.scatter( xt[:, 0], xt[:, 1], c="C1", s=P.sum(0).ravel() * (1 + p) * 4, label="Re-weighted target", alpha=1, ) pl.plot([], [], color="C2", alpha=0.8, label="OT plan") pl.title(r"$\ell_2$ UOT $\gamma$={:1.3f}".format(g_list_v[iv]), fontsize=11) return 1 i = 0 _update_plot(i) ani = animation.FuncAnimation( pl.gcf(), _update_plot, nv, interval=100, repeat_delay=2000 ) ############################################################################## # Plot the semi-relaxed regularization path # ----------------------------------------- # %% semi-relaxed l2-penalized UOT pl.figure(4) selected_gamma = [10, 1, 1e-1, 1e-2] for p in range(4): tp = ot.regpath.compute_transport_plan(selected_gamma[p], g_list2, t_list2) P = tp.reshape((n, n)) pl.subplot(2, 2, p + 1) if P.sum() > 0: P = P / P.max() for i in range(n): for j in range(n): if P[i, j] > 0: pl.plot( [xs[i, 0], xt[j, 0]], [xs[i, 1], xt[j, 1]], color="C2", alpha=P[i, j] * 0.3, ) pl.scatter(xs[:, 0], xs[:, 1], c="C0", alpha=0.2) pl.scatter(xt[:, 0], xt[:, 1], c="C1", alpha=1, label="Target marginal") pl.scatter( xs[:, 0], xs[:, 1], c="C0", s=P.sum(1).ravel() * 2 * (1 + p), label="Source marginal", alpha=1, ) pl.plot([], [], color="C2", alpha=0.8, label="OT plan") pl.title( r"Semi-relaxed $l_2$ UOT $\gamma$={}".format(selected_gamma[p]), fontsize=11 ) if p < 2: pl.xticks(()) pl.show() # %% # Animation of the regpath for semi-relaxed UOT l2 # ------------------------------------------------ nv = 50 g_list_v = np.logspace(2, -2, nv) pl.figure(5) def _update_plot(iv): pl.clf() tp = ot.regpath.compute_transport_plan(g_list_v[iv], g_list2, t_list2) P = tp.reshape((n, n)) if P.sum() > 0: P = P / P.max() for i in range(n): for j in range(n): if P[i, j] > 0: pl.plot( [xs[i, 0], xt[j, 0]], [xs[i, 1], xt[j, 1]], color="C2", alpha=P[i, j] * 0.5, ) pl.scatter(xs[:, 0], xs[:, 1], c="C0", alpha=0.2) pl.scatter(xt[:, 0], xt[:, 1], c="C1", alpha=0.2) pl.scatter( xs[:, 0], xs[:, 1], c="C0", s=P.sum(1).ravel() * (1 + p) * 4, label="Re-weighted source", alpha=1, ) pl.scatter( xt[:, 0], xt[:, 1], c="C1", s=P.sum(0).ravel() * (1 + p) * 4, label="Re-weighted target", alpha=1, ) pl.plot([], [], color="C2", alpha=0.8, label="OT plan") pl.title( r"Semi-relaxed $\ell_2$ UOT $\gamma$={:1.3f}".format(g_list_v[iv]), fontsize=11 ) return 1 i = 0 _update_plot(i) ani = animation.FuncAnimation( pl.gcf(), _update_plot, nv, interval=100, repeat_delay=2000 ) python-pot-0.9.7+dfsg/examples/unbalanced-partial/plot_unbalanced_OT.py000066400000000000000000000072171525521106200262470ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ============================================================== 2D examples of exact and entropic unbalanced optimal transport ============================================================== .. note:: Example added in release: 0.8.2. This example is designed to show how to compute unbalanced and partial OT in POT. UOT aims at solving the following optimization problem: .. math:: W = \min_{\gamma} <\gamma, \mathbf{M}>_F + \mathrm{reg}\cdot\Omega(\gamma) + \mathrm{reg_m} \cdot \mathrm{div}(\gamma \mathbf{1}, \mathbf{a}) + \mathrm{reg_m} \cdot \mathrm{div}(\gamma^T \mathbf{1}, \mathbf{b}) s.t. \gamma \geq 0 where :math:`\mathrm{div}` is a divergence. When using the entropic UOT, :math:`\mathrm{reg}>0` and :math:`\mathrm{div}` should be the Kullback-Leibler divergence. When solving exact UOT, :math:`\mathrm{reg}=0` and :math:`\mathrm{div}` can be either the Kullback-Leibler or the quadratic divergence. Using :math:`\ell_1` norm gives the so-called partial OT. """ # Author: Laetitia Chapel # License: MIT License import numpy as np import matplotlib.pylab as pl import ot ############################################################################## # Generate data # ------------- # %% parameters and data generation n = 40 # nb samples mu_s = np.array([-1, -1]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([4, 4]) cov_t = np.array([[1, -0.8], [-0.8, 1]]) np.random.seed(0) xs = ot.datasets.make_2D_samples_gauss(n, mu_s, cov_s) xt = ot.datasets.make_2D_samples_gauss(n, mu_t, cov_t) n_noise = 10 xs = np.concatenate((xs, (np.random.rand(n_noise, 2) - 4)), axis=0) xt = np.concatenate((xt, (np.random.rand(n_noise, 2) + 6)), axis=0) n = n + n_noise a, b = np.ones((n,)) / n, np.ones((n,)) / n # uniform distribution on samples # loss matrix M = ot.dist(xs, xt) M /= M.max() ############################################################################## # Compute entropic kl-regularized UOT, kl- and l2-regularized UOT # ----------- reg = 0.005 reg_m_kl = 0.05 reg_m_l2 = 5 mass = 0.7 entropic_kl_uot = ot.unbalanced.sinkhorn_unbalanced(a, b, M, reg, reg_m_kl) kl_uot = ot.unbalanced.mm_unbalanced(a, b, M, reg_m_kl, div="kl") l2_uot = ot.unbalanced.mm_unbalanced(a, b, M, reg_m_l2, div="l2") partial_ot = ot.partial.partial_wasserstein(a, b, M, m=mass) ############################################################################## # Plot the results # ---------------- pl.figure(2) transp = [partial_ot, l2_uot, kl_uot, entropic_kl_uot] title = [ "partial OT \n m=" + str(mass), "$\ell_2$-UOT \n $\mathrm{reg_m}$=" + str(reg_m_l2), "kl-UOT \n $\mathrm{reg_m}$=" + str(reg_m_kl), "entropic kl-UOT \n $\mathrm{reg_m}$=" + str(reg_m_kl), ] for p in range(4): pl.subplot(2, 4, p + 1) P = transp[p] if P.sum() > 0: P = P / P.max() for i in range(n): for j in range(n): if P[i, j] > 0: pl.plot( [xs[i, 0], xt[j, 0]], [xs[i, 1], xt[j, 1]], color="C2", alpha=P[i, j] * 0.3, ) pl.scatter(xs[:, 0], xs[:, 1], c="C0", alpha=0.2) pl.scatter(xt[:, 0], xt[:, 1], c="C1", alpha=0.2) pl.scatter(xs[:, 0], xs[:, 1], c="C0", s=P.sum(1).ravel() * (1 + p) * 2) pl.scatter(xt[:, 0], xt[:, 1], c="C1", s=P.sum(0).ravel() * (1 + p) * 2) pl.title(title[p]) pl.yticks(()) pl.xticks(()) if p < 1: pl.ylabel("mappings") pl.subplot(2, 4, p + 5) pl.imshow(P, cmap="jet") pl.yticks(()) pl.xticks(()) if p < 1: pl.ylabel("transport plans") pl.show() python-pot-0.9.7+dfsg/ignore-words.txt000066400000000000000000000000631525521106200177450ustar00rootroot00000000000000embedd ot OT coo wass ccompiler ist lik ges mapp python-pot-0.9.7+dfsg/ot/000077500000000000000000000000001525521106200152105ustar00rootroot00000000000000python-pot-0.9.7+dfsg/ot/__init__.py000066400000000000000000000102541525521106200173230ustar00rootroot00000000000000""" .. warning:: The list of automatically imported sub-modules is as follows: :py:mod:`ot.lp`, :py:mod:`ot.bregman`, :py:mod:`ot.optim` :py:mod:`ot.utils`, :py:mod:`ot.datasets`, :py:mod:`ot.gromov`, :py:mod:`ot.smooth` :py:mod:`ot.stochastic`, :py:mod:`ot.partial`, :py:mod:`ot.regpath` , :py:mod:`ot.unbalanced`, :py:mod:`ot.sliced`, :py:mod:`ot.mapping` . The following sub-modules are not imported due to additional dependencies: - :any:`ot.dr` : depends on :code:`pymanopt` and :code:`autograd`. - :any:`ot.plot` : depends on :code:`matplotlib` """ # Author: Remi Flamary # Nicolas Courty # # License: MIT License # All submodules and packages from . import lp from . import bregman from . import optim from . import utils from . import datasets from . import da from . import gromov from . import smooth from . import stochastic from . import unbalanced from . import sliced from . import partial from . import backend from . import regpath from . import weak from . import factored from . import solvers from . import gaussian from . import lowrank from . import gmm from . import semidiscrete from . import sgot # OT functions from .lp import ( emd, emd2, emd2_lazy, emd_1d, emd2_1d, emd_1d_dual_backprop, wasserstein_1d, binary_search_circle, wasserstein_circle, semidiscrete_wasserstein2_unif_circle, linear_circular_ot, ) from .bregman import sinkhorn, sinkhorn2, barycenter from .unbalanced import ( sinkhorn_unbalanced, barycenter_unbalanced, sinkhorn_unbalanced2, uot_1d, unbalanced_sliced_ot, sliced_unbalanced_ot, ) from .da import sinkhorn_lpl1_mm from .sliced import ( sliced_wasserstein_distance, max_sliced_wasserstein_distance, sliced_wasserstein_sphere, sliced_wasserstein_sphere_unif, linear_sliced_wasserstein_sphere, min_sliced_transport_plan, expected_sliced_plan, ) from .gromov import ( gromov_wasserstein, gromov_wasserstein2, gromov_barycenters, fused_gromov_wasserstein, fused_gromov_wasserstein2, lowrank_gromov_wasserstein_samples, ) from .weak import weak_optimal_transport from .factored import factored_optimal_transport from .solvers import solve, solve_gromov, solve_sample, solve_bary_sample from .lowrank import lowrank_sinkhorn from .batch import solve_batch, solve_sample_batch, solve_gromov_batch, dist_batch from .bsp import compute_bspot_bijection, merge_bijections # utils functions from .utils import dist, unif, tic, toc, toq __version__ = "0.9.7" __all__ = [ "emd", "emd2", "emd2_lazy", "emd_1d", "sinkhorn", "sinkhorn2", "utils", "datasets", "bregman", "lp", "sliced", "tic", "toc", "toq", "gromov", "emd2_1d", "emd_1d_dual", "emd_1d_dual_backprop", "wasserstein_1d", "backend", "gaussian", "dist", "unif", "barycenter", "sinkhorn_lpl1_mm", "da", "optim", "sinkhorn_unbalanced", "barycenter_unbalanced", "sinkhorn_unbalanced2", "sliced_wasserstein_distance", "sliced_wasserstein_sphere", "uot_1d", "unbalanced_sliced_ot", "sliced_unbalanced_ot", "linear_sliced_wasserstein_sphere", "min_sliced_transport_plan", "expected_sliced_plan", "gromov_wasserstein", "gromov_wasserstein2", "gromov_barycenters", "fused_gromov_wasserstein", "fused_gromov_wasserstein2", "max_sliced_wasserstein_distance", "weak_optimal_transport", "factored_optimal_transport", "solve", "solve_gromov", "solve_sample", "solve_bary_sample", "smooth", "stochastic", "unbalanced", "partial", "regpath", "solvers", "weak", "factored", "lowrank", "gmm", "semidiscrete", "sgot", "binary_search_circle", "wasserstein_circle", "semidiscrete_wasserstein2_unif_circle", "linear_circular_ot", "sliced_wasserstein_sphere_unif", "sliced_wasserstein_sphere", "lowrank_sinkhorn", "lowrank_gromov_wasserstein_samples", "solve_batch", "solve_gromov_batch", "solve_sample_batch", "dist_batch", "compute_bspot_bijection", "merge_bijections", ] python-pot-0.9.7+dfsg/ot/backend.py000066400000000000000000003260721525521106200171630ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Multi-lib backend for POT The goal is to write backend-agnostic code. Whether you're using Numpy, PyTorch, Jax, Cupy, or Tensorflow, POT code should work nonetheless. To achieve that, POT provides backend classes which implements functions in their respective backend imitating Numpy API. As a convention, we use nx instead of np to refer to the backend. Examples -------- >>> from ot.utils import list_to_array >>> from ot.backend import get_backend >>> def f(a, b): # the function does not know which backend to use ... a, b = list_to_array(a, b) # if a list in given, make it an array ... nx = get_backend(a, b) # infer the backend from the arguments ... c = nx.dot(a, b) # now use the backend to do any calculation ... return c .. warning:: Tensorflow only works with the Numpy API. To activate it, please run the following: .. code-block:: from tensorflow.python.ops.numpy_ops import np_config np_config.enable_numpy_behavior() Performance ----------- - CPU: Intel(R) Xeon(R) Gold 6248 CPU @ 2.50GHz - GPU: Tesla V100-SXM2-32GB - Date of the benchmark: December 8th, 2021 - Commit of benchmark: PR #316, https://github.com/PythonOT/POT/pull/316 .. raw:: html
Sinkhorn Knopp - Averaged on 100 runs
Bitsize32 bits
DeviceCPUGPU
Sample sizeNumpyPytorchTensorflowCupyJaxPytorchTensorflow
500.00080.00220.01510.00950.01930.00510.0293
1000.00050.00130.00970.00570.01150.00290.0173
5000.00090.00160.01100.00580.01150.00290.0166
10000.00210.00210.01450.00560.01180.00290.0168
20000.00690.00430.02780.00590.01180.00300.0165
50000.07070.03140.13950.00740.01250.00350.0198
 
Bitsize64 bits
DeviceCPUGPU
Sample sizeNumpyPytorchTensorflowCupyJaxPytorchTensorflow
500.00080.00200.01540.00930.01910.00510.0328
1000.00050.00130.00940.00560.01140.00290.0169
5000.00130.00170.01200.00590.01160.00290.0168
10000.00340.00270.01770.00580.01180.00290.0167
20000.01460.00750.04360.00590.01200.00290.0165
50000.14670.05680.24680.00770.01460.00450.0204
""" # Author: Remi Flamary # Nicolas Courty # # License: MIT License import os import time import warnings import functools import numpy as np import scipy import scipy.linalg import scipy.special as special from scipy.sparse import coo_array, csr_matrix, issparse DISABLE_TORCH_KEY = "POT_BACKEND_DISABLE_PYTORCH" DISABLE_JAX_KEY = "POT_BACKEND_DISABLE_JAX" DISABLE_CUPY_KEY = "POT_BACKEND_DISABLE_CUPY" DISABLE_TF_KEY = "POT_BACKEND_DISABLE_TENSORFLOW" if not os.environ.get(DISABLE_TORCH_KEY, False): try: import torch torch_type = torch.Tensor except ImportError: torch = False torch_type = float else: torch = False torch_type = float if not os.environ.get(DISABLE_JAX_KEY, False): try: import jax import jax.numpy as jnp import jax.scipy.special as jspecial from jax.extend.backend import get_backend as _jax_get_backend jax_type = jax.numpy.ndarray jax_new_version = tuple([float(s) for s in jax.__version__.split(".")]) > ( 0, 4, 24, 0, ) @jax.custom_jvp def norm_1d_jax(z): return jnp.abs(z) @norm_1d_jax.defjvp def norm_1d_jax_jvp(primals, tangents): """ Enforce the gradient in 0 of jnp.abs to be 0 """ (z,) = primals z_is_zero = jnp.all(jnp.logical_not(z)) clean_z = jnp.where(z_is_zero, jnp.ones_like(z), z) primals, tangents = jax.jvp( functools.partial(jnp.abs), (clean_z,), tangents ) return jnp.abs(z), jnp.where(z_is_zero, 0.0, tangents) except ImportError: jax = False jax_type = float else: jax = False jax_type = float if not os.environ.get(DISABLE_CUPY_KEY, False): try: import cupy as cp import cupyx cp_type = cp.ndarray except ImportError: cp = False cp_type = float else: cp = False cp_type = float if not os.environ.get(DISABLE_TF_KEY, False): try: import tensorflow as tf import tensorflow.experimental.numpy as tnp tf_type = tf.Tensor except ImportError: tf = False tf_type = float else: tf = False tf_type = float str_type_error = ( "All array should be from the same type/backend. Current types are : {}" ) # Mapping between argument types and the existing backend _BACKEND_IMPLEMENTATIONS = [] _BACKENDS = {} def _register_backend_implementation(backend_impl): _BACKEND_IMPLEMENTATIONS.append(backend_impl) def _get_backend_instance(backend_impl): if backend_impl.__name__ not in _BACKENDS: _BACKENDS[backend_impl.__name__] = backend_impl() return _BACKENDS[backend_impl.__name__] def _check_args_backend(backend_impl, args): # Get backend instance to use issparse method backend = _get_backend_instance(backend_impl) # Check if each arg is either: # 1. An instance of backend.__type__ (e.g., np.ndarray for NumPy) # 2. A sparse matrix recognized by backend.issparse() (e.g., scipy.sparse for NumPy) is_instance = set( isinstance(arg, backend_impl.__type__) or backend.issparse(arg) for arg in args ) # check that all arguments matched or not the type if len(is_instance) == 1: return is_instance.pop() # Otherwise return an error raise ValueError(str_type_error.format([type(arg) for arg in args])) def get_backend_list(): """Returns instances of all available backends. Note that the function forces all detected implementations to be instantiated even if specific backend was not use before. Be careful as instantiation of the backend might lead to side effects, like GPU memory pre-allocation. See the documentation for more details. If you only need to know which implementations are available, use `:py:func:`ot.backend.get_available_backend_implementations`, which does not force instance of the backend object to be created. """ return [ _get_backend_instance(backend_impl) for backend_impl in get_available_backend_implementations() ] def get_available_backend_implementations(): """Returns the list of available backend implementations.""" return _BACKEND_IMPLEMENTATIONS def get_backend(*args): """Returns the proper backend for a list of input arrays Accepts None entries in the arguments, and ignores them Also raises TypeError if all arrays are not from the same backend """ args = [arg for arg in args if arg is not None] # exclude None entries # check that some arrays given if not len(args) > 0: raise ValueError(" The function takes at least one (non-None) parameter") for backend_impl in _BACKEND_IMPLEMENTATIONS: if _check_args_backend(backend_impl, args): return _get_backend_instance(backend_impl) raise ValueError("Unknown type of non implemented backend.") def to_numpy(*args): """Returns numpy arrays from any compatible backend""" if len(args) == 1: return get_backend(args[0]).to_numpy(args[0]) else: return [get_backend(a).to_numpy(a) for a in args] class Backend: """ Backend abstract class. Implementations: :py:class:`JaxBackend`, :py:class:`NumpyBackend`, :py:class:`TorchBackend`, :py:class:`CupyBackend`, :py:class:`TensorflowBackend` - The `__name__` class attribute refers to the name of the backend. - The `__type__` class attribute refers to the data structure used by the backend. """ __name__ = None __type__ = None __type_list__ = None rng_ = None def __str__(self): return self.__name__ # convert batch of tensors to numpy def to_numpy(self, *arrays): """Returns the numpy version of tensors""" if len(arrays) == 1: return self._to_numpy(arrays[0]) else: return [self._to_numpy(array) for array in arrays] # convert a tensor to numpy def _to_numpy(self, a): """Returns the numpy version of a tensor""" raise NotImplementedError() # convert batch of arrays from numpy def from_numpy(self, *arrays, type_as=None): """Creates tensors cloning a numpy array, with the given precision (defaulting to input's precision) and the given device (in case of GPUs)""" if len(arrays) == 1: return self._from_numpy(arrays[0], type_as=type_as) else: return [self._from_numpy(array, type_as=type_as) for array in arrays] # convert an array from numpy def _from_numpy(self, a, type_as=None): """Creates a tensor cloning a numpy array, with the given precision (defaulting to input's precision) and the given device (in case of GPUs)""" raise NotImplementedError() def set_gradients(self, val, inputs, grads): """Define the gradients for the value val wrt the inputs""" raise NotImplementedError() def detach(self, *arrays): """Detach the tensors from the computation graph See: https://pytorch.org/docs/stable/generated/torch.Tensor.detach.html""" if len(arrays) == 1: return self._detach(arrays[0]) else: return [self._detach(array) for array in arrays] def _detach(self, a): """Detach the tensor from the computation graph""" raise NotImplementedError() def zeros(self, shape, type_as=None): r""" Creates a tensor full of zeros. This function follows the api from :any:`numpy.zeros` See: https://numpy.org/doc/stable/reference/generated/numpy.zeros.html """ raise NotImplementedError() def ones(self, shape, type_as=None): r""" Creates a tensor full of ones. This function follows the api from :any:`numpy.ones` See: https://numpy.org/doc/stable/reference/generated/numpy.ones.html """ raise NotImplementedError() def arange(self, stop, start=0, step=1, type_as=None): r""" Returns evenly spaced values within a given interval. This function follows the api from :any:`numpy.arange` See: https://numpy.org/doc/stable/reference/generated/numpy.arange.html """ raise NotImplementedError() def full(self, shape, fill_value, type_as=None): r""" Creates a tensor with given shape, filled with given value. This function follows the api from :any:`numpy.full` See: https://numpy.org/doc/stable/reference/generated/numpy.full.html """ raise NotImplementedError() def eye(self, N, M=None, type_as=None): r""" Creates the identity matrix of given size. This function follows the api from :any:`numpy.eye` See: https://numpy.org/doc/stable/reference/generated/numpy.eye.html """ raise NotImplementedError() def sum(self, a, axis=None, keepdims=False): r""" Sums tensor elements over given dimensions. This function follows the api from :any:`numpy.sum` See: https://numpy.org/doc/stable/reference/generated/numpy.sum.html """ raise NotImplementedError() def cumsum(self, a, axis=None): r""" Returns the cumulative sum of tensor elements over given dimensions. This function follows the api from :any:`numpy.cumsum` See: https://numpy.org/doc/stable/reference/generated/numpy.cumsum.html """ raise NotImplementedError() def max(self, a, axis=None, keepdims=False): r""" Returns the maximum of an array or maximum along given dimensions. This function follows the api from :any:`numpy.amax` See: https://numpy.org/doc/stable/reference/generated/numpy.amax.html """ raise NotImplementedError() def min(self, a, axis=None, keepdims=False): r""" Returns the maximum of an array or maximum along given dimensions. This function follows the api from :any:`numpy.amin` See: https://numpy.org/doc/stable/reference/generated/numpy.amin.html """ raise NotImplementedError() def maximum(self, a, b): r""" Returns element-wise maximum of array elements. This function follows the api from :any:`numpy.maximum` See: https://numpy.org/doc/stable/reference/generated/numpy.maximum.html """ raise NotImplementedError() def minimum(self, a, b): r""" Returns element-wise minimum of array elements. This function follows the api from :any:`numpy.minimum` See: https://numpy.org/doc/stable/reference/generated/numpy.minimum.html """ raise NotImplementedError() def sign(self, a): r"""Returns an element-wise indication of the sign of a number. This function follows the api from :any:`numpy.sign` See: https://numpy.org/doc/stable/reference/generated/numpy.sign.html """ raise NotImplementedError() def dot(self, a, b): r""" Returns the dot product of two tensors. This function follows the api from :any:`numpy.dot` See: https://numpy.org/doc/stable/reference/generated/numpy.dot.html """ raise NotImplementedError() def abs(self, a): r""" Computes the absolute value element-wise. This function follows the api from :any:`numpy.absolute` See: https://numpy.org/doc/stable/reference/generated/numpy.absolute.html """ raise NotImplementedError() def exp(self, a): r""" Computes the exponential value element-wise. This function follows the api from :any:`numpy.exp` See: https://numpy.org/doc/stable/reference/generated/numpy.exp.html """ raise NotImplementedError() def log(self, a): r""" Computes the natural logarithm, element-wise. This function follows the api from :any:`numpy.log` See: https://numpy.org/doc/stable/reference/generated/numpy.log.html """ raise NotImplementedError() def logsumexp(self, a, axis=None, keepdims=False): r""" Computes the log of the sum of exponentials of input elements. This function follows the api from :any:`scipy.special.logsumexp` See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.logsumexp.html """ raise NotImplementedError() def sqrt(self, a): r""" Returns the non-ngeative square root of a tensor, element-wise. This function follows the api from :any:`numpy.sqrt` See: https://numpy.org/doc/stable/reference/generated/numpy.sqrt.html """ raise NotImplementedError() def power(self, a, exponents): r""" First tensor elements raised to powers from second tensor, element-wise. This function follows the api from :any:`numpy.power` See: https://numpy.org/doc/stable/reference/generated/numpy.power.html """ raise NotImplementedError() def norm(self, a, axis=None, keepdims=False): r""" Computes the matrix frobenius norm. This function follows the api from :any:`numpy.linalg.norm` See: https://numpy.org/doc/stable/reference/generated/numpy.linalg.norm.html """ raise NotImplementedError() def any(self, a): r""" Tests whether any tensor element along given dimensions evaluates to True. This function follows the api from :any:`numpy.any` See: https://numpy.org/doc/stable/reference/generated/numpy.any.html """ raise NotImplementedError() def isnan(self, a): r""" Tests element-wise for NaN and returns result as a boolean tensor. This function follows the api from :any:`numpy.isnan` See: https://numpy.org/doc/stable/reference/generated/numpy.isnan.html """ raise NotImplementedError() def isinf(self, a): r""" Tests element-wise for positive or negative infinity and returns result as a boolean tensor. This function follows the api from :any:`numpy.isinf` See: https://numpy.org/doc/stable/reference/generated/numpy.isinf.html """ raise NotImplementedError() def einsum(self, subscripts, *operands): r""" Evaluates the Einstein summation convention on the operands. This function follows the api from :any:`numpy.einsum` See: https://numpy.org/doc/stable/reference/generated/numpy.einsum.html """ raise NotImplementedError() def sort(self, a, axis=-1): r""" Returns a sorted copy of a tensor. This function follows the api from :any:`numpy.sort` See: https://numpy.org/doc/stable/reference/generated/numpy.sort.html """ raise NotImplementedError() def argsort(self, a, axis=None): r""" Returns the indices that would sort a tensor. This function follows the api from :any:`numpy.argsort` See: https://numpy.org/doc/stable/reference/generated/numpy.argsort.html """ raise NotImplementedError() def searchsorted(self, a, v, side="left"): r""" Finds indices where elements should be inserted to maintain order in given tensor. This function follows the api from :any:`numpy.searchsorted` See: https://numpy.org/doc/stable/reference/generated/numpy.searchsorted.html """ raise NotImplementedError() def flip(self, a, axis=None): r""" Reverses the order of elements in a tensor along given dimensions. This function follows the api from :any:`numpy.flip` See: https://numpy.org/doc/stable/reference/generated/numpy.flip.html """ raise NotImplementedError() def clip(self, a, a_min=None, a_max=None): """ Limits the values in a tensor. This function follows the api from :any:`numpy.clip` See: https://numpy.org/doc/stable/reference/generated/numpy.clip.html """ raise NotImplementedError() def real(self, a): """ Return the real part of the tensor element-wise. This function follows the api from :any:`numpy.real` See: https://numpy.org/doc/stable/reference/generated/numpy.real.html """ raise NotImplementedError() def imag(self, a): """ Return the imaginary part of the tensor element-wise. This function follows the api from :any:`numpy.imag` See: https://numpy.org/doc/stable/reference/generated/numpy.imag.html """ raise NotImplementedError() def conj(self, a): """ Return the complex conjugate, element-wise. This function follows the api from :any:`numpy.conj` See: https://numpy.org/doc/stable/reference/generated/numpy.conj.html """ raise NotImplementedError() def arccos(self, a): """ Trigonometric inverse cosine, element-wise. This function follows the api from :any:`numpy.arccos` See: https://numpy.org/doc/stable/reference/generated/numpy.arccos.html """ raise NotImplementedError() def repeat(self, a, repeats, axis=None): r""" Repeats elements of a tensor. This function follows the api from :any:`numpy.repeat` See: https://numpy.org/doc/stable/reference/generated/numpy.repeat.html """ raise NotImplementedError() def take_along_axis(self, arr, indices, axis): r""" Gathers elements of a tensor along given dimensions. This function follows the api from :any:`numpy.take_along_axis` See: https://numpy.org/doc/stable/reference/generated/numpy.take_along_axis.html """ raise NotImplementedError() def concatenate(self, arrays, axis=0): r""" Joins a sequence of tensors along an existing dimension. This function follows the api from :any:`numpy.concatenate` See: https://numpy.org/doc/stable/reference/generated/numpy.concatenate.html """ raise NotImplementedError() def zero_pad(self, a, pad_width, value=0): r""" Pads a tensor with a given value (0 by default). This function follows the api from :any:`numpy.pad` See: https://numpy.org/doc/stable/reference/generated/numpy.pad.html """ raise NotImplementedError() def argmax(self, a, axis=None): r""" Returns the indices of the maximum values of a tensor along given dimensions. This function follows the api from :any:`numpy.argmax` See: https://numpy.org/doc/stable/reference/generated/numpy.argmax.html """ raise NotImplementedError() def argmin(self, a, axis=None): r""" Returns the indices of the minimum values of a tensor along given dimensions. This function follows the api from :any:`numpy.argmin` See: https://numpy.org/doc/stable/reference/generated/numpy.argmin.html """ raise NotImplementedError() def mean(self, a, axis=None): r""" Computes the arithmetic mean of a tensor along given dimensions. This function follows the api from :any:`numpy.mean` See: https://numpy.org/doc/stable/reference/generated/numpy.mean.html """ raise NotImplementedError() def median(self, a, axis=None): r""" Computes the median of a tensor along given dimensions. This function follows the api from :any:`numpy.median` See: https://numpy.org/doc/stable/reference/generated/numpy.median.html """ raise NotImplementedError() def std(self, a, axis=None): r""" Computes the standard deviation of a tensor along given dimensions. This function follows the api from :any:`numpy.std` See: https://numpy.org/doc/stable/reference/generated/numpy.std.html """ raise NotImplementedError() def linspace(self, start, stop, num, type_as=None): r""" Returns a specified number of evenly spaced values over a given interval. This function follows the api from :any:`numpy.linspace` See: https://numpy.org/doc/stable/reference/generated/numpy.linspace.html """ raise NotImplementedError() def meshgrid(self, a, b): r""" Returns coordinate matrices from coordinate vectors (Numpy convention). This function follows the api from :any:`numpy.meshgrid` See: https://numpy.org/doc/stable/reference/generated/numpy.meshgrid.html """ raise NotImplementedError() def diag(self, a, k=0): r""" Extracts or constructs a diagonal tensor. This function follows the api from :any:`numpy.diag` See: https://numpy.org/doc/stable/reference/generated/numpy.diag.html """ raise NotImplementedError() def unique(self, a, return_inverse=False): r""" Finds unique elements of given tensor. This function follows the api from :any:`numpy.unique` See: https://numpy.org/doc/stable/reference/generated/numpy.unique.html """ raise NotImplementedError() def stack(self, arrays, axis=0): r""" Joins a sequence of tensors along a new dimension. This function follows the api from :any:`numpy.stack` See: https://numpy.org/doc/stable/reference/generated/numpy.stack.html """ raise NotImplementedError() def outer(self, a, b): r""" Computes the outer product between two vectors. This function follows the api from :any:`numpy.outer` See: https://numpy.org/doc/stable/reference/generated/numpy.outer.html """ raise NotImplementedError() def reshape(self, a, shape): r""" Gives a new shape to a tensor without changing its data. This function follows the api from :any:`numpy.reshape` See: https://numpy.org/doc/stable/reference/generated/numpy.reshape.html """ raise NotImplementedError() def seed(self, seed=None): r""" Sets the seed for the random generator. This function follows the api from :any:`numpy.random.seed` See: https://numpy.org/doc/stable/reference/random/generated/numpy.random.seed.html """ raise NotImplementedError() def rand(self, *size, type_as=None): r""" Generate uniform random numbers. This function follows the api from :any:`numpy.random.rand` See: https://numpy.org/doc/stable/reference/random/generated/numpy.random.rand.html """ raise NotImplementedError() def randn(self, *size, type_as=None): r""" Generate normal Gaussian random numbers. This function follows the api from :any:`numpy.random.rand` See: https://numpy.org/doc/stable/reference/random/generated/numpy.random.rand.html """ raise NotImplementedError() def randperm(self, size, type_as=None): r""" Returns a random permutation of integers from 0 to n-1. This function follows the api from :any:`torch.randperm` See: https://docs.pytorch.org/docs/stable/generated/torch.randperm.html """ raise NotImplementedError() def coo_matrix(self, data, rows, cols, shape=None, type_as=None): r""" Creates a sparse tensor in COOrdinate format. This function follows the api from :any:`scipy.sparse.coo_array` See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.coo_array.html """ raise NotImplementedError() def issparse(self, a): r""" Checks whether or not the input tensor is a sparse tensor. This function follows the api from :any:`scipy.sparse.issparse` See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.issparse.html """ raise NotImplementedError() def tocsr(self, a): r""" Converts this matrix to Compressed Sparse Row format. This function follows the api from :any:`scipy.sparse.coo_matrix.tocsr` See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.coo_matrix.tocsr.html """ raise NotImplementedError() def eliminate_zeros(self, a, threshold=0.0): r""" Removes entries smaller than the given threshold from the sparse tensor. This function follows the api from :any:`scipy.sparse.csr_matrix.eliminate_zeros` See: https://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.sparse.csr_matrix.eliminate_zeros.html """ raise NotImplementedError() def todense(self, a): r""" Converts a sparse tensor to a dense tensor. This function follows the api from :any:`scipy.sparse.csr_matrix.toarray` See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.toarray.html """ raise NotImplementedError() def sparse_coo_data(self, a): r""" Extracts COO format data (row, col, data, shape) from a sparse matrix. Returns row indices, column indices, data values, and shape as numpy arrays/tuple. This is used to interface with C++ solvers that require explicit edge lists. Parameters ---------- a : sparse matrix Sparse matrix in backend's COO format Returns ------- row : numpy.ndarray Row indices (1D array) col : numpy.ndarray Column indices (1D array) data : numpy.ndarray Data values (1D array) shape : tuple Shape of the matrix (n_rows, n_cols) """ raise NotImplementedError() def where(self, condition, x, y): r""" Returns elements chosen from x or y depending on condition. This function follows the api from :any:`numpy.where` See: https://numpy.org/doc/stable/reference/generated/numpy.where.html """ raise NotImplementedError() def copy(self, a): r""" Returns a copy of the given tensor. This function follows the api from :any:`numpy.copy` See: https://numpy.org/doc/stable/reference/generated/numpy.copy.html """ raise NotImplementedError() def allclose(self, a, b, rtol=1e-05, atol=1e-08, equal_nan=False): r""" Returns True if two arrays are element-wise equal within a tolerance. This function follows the api from :any:`numpy.allclose` See: https://numpy.org/doc/stable/reference/generated/numpy.allclose.html """ raise NotImplementedError() def dtype_device(self, a): r""" Returns the dtype and the device of the given tensor. """ raise NotImplementedError() def assert_same_dtype_device(self, a, b): r""" Checks whether or not the two given inputs have the same dtype as well as the same device """ raise NotImplementedError() def squeeze(self, a, axis=None): r""" Remove axes of length one from a. This function follows the api from :any:`numpy.squeeze`. See: https://numpy.org/doc/stable/reference/generated/numpy.squeeze.html """ raise NotImplementedError() def unsqueeze(self, a, axis): r""" Add a dimension of size one at the specified axis. This function follows the api from :any:`numpy.expand_dims`. See: https://numpy.org/doc/stable/reference/generated/numpy.expand_dims.html """ raise NotImplementedError() def bitsize(self, type_as): r""" Gives the number of bits used by the data type of the given tensor. """ raise NotImplementedError() def device_type(self, type_as): r""" Returns CPU or GPU depending on the device where the given tensor is located. """ raise NotImplementedError() def _bench(self, callable, *args, n_runs=1, warmup_runs=1): r""" Executes a benchmark of the given callable with the given arguments. """ raise NotImplementedError() def solve(self, a, b): r""" Solves a linear matrix equation, or system of linear scalar equations. This function follows the api from :any:`numpy.linalg.solve`. See: https://numpy.org/doc/stable/reference/generated/numpy.linalg.solve.html """ raise NotImplementedError() def trace(self, a): r""" Returns the sum along diagonals of the array. This function follows the api from :any:`numpy.trace`. See: https://numpy.org/doc/stable/reference/generated/numpy.trace.html """ raise NotImplementedError() def inv(self, a): r""" Computes the inverse of a matrix. This function follows the api from :any:`scipy.linalg.inv`. See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.inv.html """ raise NotImplementedError() def pinv(self, a, hermitian=False): r""" Computes the pseudo inverse of a matrix. This function follows the api from :any:`numpy.linalg.pinv`. See: https://numpy.org/devdocs/reference/generated/numpy.linalg.pinv.html """ raise NotImplementedError() def sqrtm(self, a): r""" Computes the matrix square root. Requires input symmetric positive semi-definite. This function follows the api from :any:`scipy.linalg.sqrtm`, allowing batches. See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.sqrtm.html """ raise NotImplementedError() def eigh(self, a): r""" Computes the eigenvalues and eigenvectors of a symmetric tensor. This function follows the api from :any:`scipy.linalg.eigh`. See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.eigh.html """ raise NotImplementedError() def kl_div(self, p, q, mass=False, eps=1e-16, axis=None): r""" Computes the (Generalized) Kullback-Leibler divergence. This function follows the api from :any:`scipy.stats.entropy`. Parameter eps is used to avoid numerical errors and is added in the log. .. math:: KL(p,q) = \langle \mathbf{p}, log(\mathbf{p} / \mathbf{q} + eps \rangle + \mathbb{1}_{mass=True} \langle \mathbf{q} - \mathbf{p}, \mathbf{1} \rangle See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.entropy.html """ raise NotImplementedError() def isfinite(self, a): r""" Tests element-wise for finiteness (not infinity and not Not a Number). This function follows the api from :any:`numpy.isfinite`. See: https://numpy.org/doc/stable/reference/generated/numpy.isfinite.html """ raise NotImplementedError() def array_equal(self, a, b): r""" True if two arrays have the same shape and elements, False otherwise. This function follows the api from :any:`numpy.array_equal`. See: https://numpy.org/doc/stable/reference/generated/numpy.array_equal.html """ raise NotImplementedError() def is_floating_point(self, a): r""" Returns whether or not the input consists of floats """ raise NotImplementedError() def tile(self, a, reps): r""" Construct an array by repeating a the number of times given by reps See: https://numpy.org/doc/stable/reference/generated/numpy.tile.html """ raise NotImplementedError() def floor(self, a): r""" Return the floor of the input element-wise See: https://numpy.org/doc/stable/reference/generated/numpy.floor.html """ raise NotImplementedError() def prod(self, a, axis=None): r""" Return the product of all elements. See: https://numpy.org/doc/stable/reference/generated/numpy.prod.html """ raise NotImplementedError() def sort2(self, a, axis=None): r""" Return the sorted array and the indices to sort the array See: https://pytorch.org/docs/stable/generated/torch.sort.html """ raise NotImplementedError() def qr(self, a): r""" Return the QR factorization See: https://numpy.org/doc/stable/reference/generated/numpy.linalg.qr.html """ raise NotImplementedError() def atan2(self, a, b): r""" Element wise arctangent See: https://numpy.org/doc/stable/reference/generated/numpy.arctan2.html """ raise NotImplementedError() def transpose(self, a, axes=None): r""" Returns a tensor that is a transposed version of a. The given dimensions dim0 and dim1 are swapped. See: https://numpy.org/doc/stable/reference/generated/numpy.transpose.html """ raise NotImplementedError() def matmul(self, a, b): r""" Matrix product of two arrays. See: https://numpy.org/doc/stable/reference/generated/numpy.matmul.html#numpy.matmul """ raise NotImplementedError() def nan_to_num(self, x, copy=True, nan=0.0, posinf=None, neginf=None): r""" Replace NaN with zero and infinity with large finite numbers or with the numbers defined by the user. See: https://numpy.org/doc/stable/reference/generated/numpy.nan_to_num.html#numpy.nan_to_num """ raise NotImplementedError() def det(self, a): r""" Compute the determinant of an array. See: https://numpy.org/doc/stable/reference/generated/numpy.linalg.det.html """ raise NotImplementedError() def slogdet(self, a): r""" Compute the sign and (natural) logarithm of the determinant of an array. See: https://numpy.org/doc/stable/reference/generated/numpy.linalg.slogdet.html """ raise NotImplementedError() class NumpyBackend(Backend): """ NumPy implementation of the backend - `__name__` is "numpy" - `__type__` is np.ndarray """ __name__ = "numpy" __type__ = np.ndarray __type_list__ = [np.array(1, dtype=np.float32), np.array(1, dtype=np.float64)] rng_ = np.random.RandomState() def _to_numpy(self, a): return a def _from_numpy(self, a, type_as=None): if type_as is None: return a elif isinstance(a, float): return a else: return np.asarray(a, dtype=type_as.dtype) def set_gradients(self, val, inputs, grads): # No gradients for numpy return val def _detach(self, a): # No gradients for numpy return a def zeros(self, shape, type_as=None): if type_as is None: return np.zeros(shape) else: return np.zeros(shape, dtype=type_as.dtype) def ones(self, shape, type_as=None): if type_as is None: return np.ones(shape) else: return np.ones(shape, dtype=type_as.dtype) def arange(self, stop, start=0, step=1, type_as=None): return np.arange(start, stop, step) def full(self, shape, fill_value, type_as=None): if type_as is None: return np.full(shape, fill_value) else: return np.full(shape, fill_value, dtype=type_as.dtype) def eye(self, N, M=None, type_as=None): if type_as is None: return np.eye(N, M) else: return np.eye(N, M, dtype=type_as.dtype) def sum(self, a, axis=None, keepdims=False): return np.sum(a, axis, keepdims=keepdims) def cumsum(self, a, axis=None): return np.cumsum(a, axis) def max(self, a, axis=None, keepdims=False): return np.max(a, axis, keepdims=keepdims) def min(self, a, axis=None, keepdims=False): return np.min(a, axis, keepdims=keepdims) def maximum(self, a, b): return np.maximum(a, b) def minimum(self, a, b): return np.minimum(a, b) def sign(self, a): return np.sign(a) def dot(self, a, b): return np.dot(a, b) def abs(self, a): return np.abs(a) def exp(self, a): return np.exp(a) def log(self, a): return np.log(a) def logsumexp(self, a, axis=None, keepdims=False): return special.logsumexp(a, axis=axis, keepdims=keepdims) def sqrt(self, a): return np.sqrt(a) def power(self, a, exponents): return np.power(a, exponents) def norm(self, a, axis=None, keepdims=False): return np.linalg.norm(a, axis=axis, keepdims=keepdims) def any(self, a): return np.any(a) def isnan(self, a): return np.isnan(a) def isinf(self, a): return np.isinf(a) def einsum(self, subscripts, *operands): return np.einsum(subscripts, *operands) def sort(self, a, axis=-1): return np.sort(a, axis) def argsort(self, a, axis=-1): return np.argsort(a, axis) def searchsorted(self, a, v, side="left"): if a.ndim == 1: return np.searchsorted(a, v, side) else: # this is a not very efficient way to make numpy # searchsorted work on 2d arrays ret = np.empty(v.shape, dtype=int) for i in range(a.shape[0]): ret[i, :] = np.searchsorted(a[i, :], v[i, :], side) return ret def flip(self, a, axis=None): return np.flip(a, axis) def outer(self, a, b): return np.outer(a, b) def clip(self, a, a_min=None, a_max=None): return np.clip(a, a_min, a_max) def real(self, a): return np.real(a) def imag(self, a): return np.imag(a) def conj(self, a): return np.conj(a) def arccos(self, a): return np.arccos(a) def repeat(self, a, repeats, axis=None): return np.repeat(a, repeats, axis) def take_along_axis(self, arr, indices, axis): return np.take_along_axis(arr, indices, axis) def concatenate(self, arrays, axis=0): return np.concatenate(arrays, axis) def zero_pad(self, a, pad_width, value=0): return np.pad(a, pad_width, constant_values=value) def argmax(self, a, axis=None): return np.argmax(a, axis=axis) def argmin(self, a, axis=None): return np.argmin(a, axis=axis) def mean(self, a, axis=None): return np.mean(a, axis=axis) def median(self, a, axis=None): return np.median(a, axis=axis) def std(self, a, axis=None): return np.std(a, axis=axis) def linspace(self, start, stop, num, type_as=None): if type_as is None: return np.linspace(start, stop, num) else: return np.linspace(start, stop, num, dtype=type_as.dtype) def meshgrid(self, a, b): return np.meshgrid(a, b) def diag(self, a, k=0): return np.diag(a, k) def unique(self, a, return_inverse=False): return np.unique(a, return_inverse=return_inverse) def stack(self, arrays, axis=0): return np.stack(arrays, axis) def reshape(self, a, shape): return np.reshape(a, shape) def seed(self, seed=None): if seed is not None: self.rng_.seed(seed) def rand(self, *size, type_as=None): return self.rng_.rand(*size) def randn(self, *size, type_as=None): return self.rng_.randn(*size) def randperm(self, size, type_as=None): if not isinstance(size, int): raise ValueError("size must be an integer") return self.rng_.permutation(size) def coo_matrix(self, data, rows, cols, shape=None, type_as=None): if type_as is None: return coo_array((data, (rows, cols)), shape=shape) else: return coo_array((data, (rows, cols)), shape=shape, dtype=type_as.dtype) def issparse(self, a): return issparse(a) def tocsr(self, a): if self.issparse(a): return a.tocsr() else: return csr_matrix(a) def eliminate_zeros(self, a, threshold=0.0): if threshold > 0: if self.issparse(a): a.data[self.abs(a.data) <= threshold] = 0 else: a[self.abs(a) <= threshold] = 0 if self.issparse(a): a.eliminate_zeros() return a def todense(self, a): if self.issparse(a): return a.toarray() else: return a def sparse_coo_data(self, a): # Convert to COO array format if needed if not isinstance(a, coo_array): a_coo = coo_array(a) else: a_coo = a return a_coo.row, a_coo.col, a_coo.data, a_coo.shape def where(self, condition, x=None, y=None): if x is None and y is None: return np.where(condition) else: return np.where(condition, x, y) def copy(self, a): return a.copy() def allclose(self, a, b, rtol=1e-05, atol=1e-08, equal_nan=False): return np.allclose(a, b, rtol=rtol, atol=atol, equal_nan=equal_nan) def dtype_device(self, a): if hasattr(a, "dtype"): return a.dtype, "cpu" else: return type(a), "cpu" def assert_same_dtype_device(self, a, b): # numpy has implicit type conversion so we automatically validate the test pass def squeeze(self, a, axis=None): return np.squeeze(a, axis=axis) def unsqueeze(self, a, axis): return np.expand_dims(a, axis=axis) def bitsize(self, type_as): return type_as.itemsize * 8 def device_type(self, type_as): return "CPU" def _bench(self, callable, *args, n_runs=1, warmup_runs=1): results = dict() for type_as in self.__type_list__: inputs = [self.from_numpy(arg, type_as=type_as) for arg in args] for _ in range(warmup_runs): callable(*inputs) t0 = time.perf_counter() for _ in range(n_runs): callable(*inputs) t1 = time.perf_counter() key = ("Numpy", self.device_type(type_as), self.bitsize(type_as)) results[key] = (t1 - t0) / n_runs return results def solve(self, a, b): return np.linalg.solve(a, b) def trace(self, a): return np.einsum("...ii", a) def inv(self, a): return scipy.linalg.inv(a) def pinv(self, a, hermitian=False): return np.linalg.pinv(a, hermitian=hermitian) def sqrtm(self, a): L, V = np.linalg.eigh(a) L = np.sqrt(L) # Q[...] = V[...] @ diag(L[...]) Q = np.einsum("...jk,...k->...jk", V, L) # R[...] = Q[...] @ V[...].T return np.einsum("...jk,...kl->...jl", Q, np.swapaxes(V, -1, -2)) def eigh(self, a): return np.linalg.eigh(a) def kl_div(self, p, q, mass=False, eps=1e-16, axis=None): value = np.sum(p * np.log(p / q + eps), axis=axis) if mass: value = value + np.sum(q - p, axis=axis) return value def isfinite(self, a): return np.isfinite(a) def array_equal(self, a, b): return np.array_equal(a, b) def is_floating_point(self, a): return a.dtype.kind == "f" def tile(self, a, reps): return np.tile(a, reps) def floor(self, a): return np.floor(a) def prod(self, a, axis=0): return np.prod(a, axis=axis) def sort2(self, a, axis=-1): return self.sort(a, axis), self.argsort(a, axis) def qr(self, a): np_version = tuple([int(k) for k in np.__version__.split(".")]) if np_version < (1, 22, 0): M, N = a.shape[-2], a.shape[-1] K = min(M, N) if len(a.shape) >= 3: n = a.shape[0] qs, rs = np.zeros((n, M, K)), np.zeros((n, K, N)) for i in range(a.shape[0]): qs[i], rs[i] = np.linalg.qr(a[i]) else: return np.linalg.qr(a) return qs, rs return np.linalg.qr(a) def atan2(self, a, b): return np.arctan2(a, b) def transpose(self, a, axes=None): return np.transpose(a, axes) def matmul(self, a, b): return np.matmul(a, b) def nan_to_num(self, x, copy=True, nan=0.0, posinf=None, neginf=None): return np.nan_to_num(x, copy=copy, nan=nan, posinf=posinf, neginf=neginf) def det(self, a): return np.linalg.det(a) def slogdet(self, a): return np.linalg.slogdet(a) _register_backend_implementation(NumpyBackend) class JaxBackend(Backend): """ JAX implementation of the backend - `__name__` is "jax" - `__type__` is jax.numpy.ndarray """ __name__ = "jax" __type__ = jax_type __type_list__ = None rng_ = None def __init__(self): self.rng_ = jax.random.PRNGKey(42) self.__type_list__ = [] # available_devices = jax.devices("cpu") available_devices = [] if _jax_get_backend().platform == "gpu": available_devices += jax.devices("gpu") for d in available_devices: self.__type_list__ += [ jax.device_put(jnp.array(1, dtype=jnp.float32), d), jax.device_put(jnp.array(1, dtype=jnp.float64), d), ] self.jax_new_version = jax_new_version def _to_numpy(self, a): return np.array(a) def _get_device(self, a): if self.jax_new_version: return list(a.devices())[0] else: return a.device_buffer.device() def _change_device(self, a, type_as): return jax.device_put(a, self._get_device(type_as)) def _from_numpy(self, a, type_as=None): if isinstance(a, float): a = np.array(a) if type_as is None: return jnp.array(a) else: return self._change_device(jnp.asarray(a, dtype=type_as.dtype), type_as) def set_gradients(self, val, inputs, grads): from jax.flatten_util import ravel_pytree (val,) = jax.lax.stop_gradient((val,)) ravelled_inputs, _ = ravel_pytree(inputs) ravelled_grads, _ = ravel_pytree(grads) aux = jnp.sum(ravelled_inputs * ravelled_grads) / 2 aux = aux - jax.lax.stop_gradient(aux) (val,) = jax.tree_util.tree_map(lambda z: z + aux, (val,)) return val def _detach(self, a): return jax.lax.stop_gradient(a) def zeros(self, shape, type_as=None): if type_as is None: return jnp.zeros(shape) else: return self._change_device(jnp.zeros(shape, dtype=type_as.dtype), type_as) def ones(self, shape, type_as=None): if type_as is None: return jnp.ones(shape) else: return self._change_device(jnp.ones(shape, dtype=type_as.dtype), type_as) def arange(self, stop, start=0, step=1, type_as=None): return jnp.arange(start, stop, step) def full(self, shape, fill_value, type_as=None): if type_as is None: return jnp.full(shape, fill_value) else: return self._change_device( jnp.full(shape, fill_value, dtype=type_as.dtype), type_as ) def eye(self, N, M=None, type_as=None): if type_as is None: return jnp.eye(N, M) else: return self._change_device(jnp.eye(N, M, dtype=type_as.dtype), type_as) def sum(self, a, axis=None, keepdims=False): return jnp.sum(a, axis, keepdims=keepdims) def cumsum(self, a, axis=None): return jnp.cumsum(a, axis) def max(self, a, axis=None, keepdims=False): return jnp.max(a, axis, keepdims=keepdims) def min(self, a, axis=None, keepdims=False): return jnp.min(a, axis, keepdims=keepdims) def maximum(self, a, b): return jnp.maximum(a, b) def minimum(self, a, b): return jnp.minimum(a, b) def sign(self, a): return jnp.sign(a) def dot(self, a, b): return jnp.dot(a, b) def abs(self, a): return norm_1d_jax(a) def exp(self, a): return jnp.exp(a) def log(self, a): return jnp.log(a) def logsumexp(self, a, axis=None, keepdims=False): return jspecial.logsumexp(a, axis=axis, keepdims=keepdims) def sqrt(self, a): return jnp.sqrt(a) def power(self, a, exponents): return jnp.power(a, exponents) def norm(self, a, axis=None, keepdims=False): return jnp.linalg.norm(a, axis=axis, keepdims=keepdims) def any(self, a): return jnp.any(a) def isnan(self, a): return jnp.isnan(a) def isinf(self, a): return jnp.isinf(a) def einsum(self, subscripts, *operands): return jnp.einsum(subscripts, *operands) def sort(self, a, axis=-1): return jnp.sort(a, axis) def argsort(self, a, axis=-1): return jnp.argsort(a, axis) def searchsorted(self, a, v, side="left"): if a.ndim == 1: return jnp.searchsorted(a, v, side) else: return jax.vmap(lambda b, u: jnp.searchsorted(b, u, side))(a, v) def flip(self, a, axis=None): return jnp.flip(a, axis) def outer(self, a, b): return jnp.outer(a, b) def clip(self, a, a_min=None, a_max=None): return jnp.clip(a, a_min, a_max) def real(self, a): return jnp.real(a) def imag(self, a): return jnp.imag(a) def conj(self, a): return jnp.conj(a) def arccos(self, a): return jnp.arccos(a) def repeat(self, a, repeats, axis=None): return jnp.repeat(a, repeats, axis) def take_along_axis(self, arr, indices, axis): return jnp.take_along_axis(arr, indices, axis) def concatenate(self, arrays, axis=0): return jnp.concatenate(arrays, axis) def zero_pad(self, a, pad_width, value=0): return jnp.pad(a, pad_width, constant_values=value) def argmax(self, a, axis=None): return jnp.argmax(a, axis=axis) def argmin(self, a, axis=None): return jnp.argmin(a, axis=axis) def mean(self, a, axis=None): return jnp.mean(a, axis=axis) def median(self, a, axis=None): return jnp.median(a, axis=axis) def std(self, a, axis=None): return jnp.std(a, axis=axis) def linspace(self, start, stop, num, type_as=None): if type_as is None: return jnp.linspace(start, stop, num) else: return self._change_device( jnp.linspace(start, stop, num, dtype=type_as.dtype), type_as ) def meshgrid(self, a, b): return jnp.meshgrid(a, b) def diag(self, a, k=0): return jnp.diag(a, k) def unique(self, a, return_inverse=False): return jnp.unique(a, return_inverse=return_inverse) def stack(self, arrays, axis=0): return jnp.stack(arrays, axis) def reshape(self, a, shape): return jnp.reshape(a, shape) def seed(self, seed=None): if seed is not None: self.rng_ = jax.random.PRNGKey(seed) def rand(self, *size, type_as=None): self.rng_, subkey = jax.random.split(self.rng_) if type_as is not None: return jax.random.uniform(subkey, shape=size, dtype=type_as.dtype) else: return jax.random.uniform(subkey, shape=size) def randn(self, *size, type_as=None): self.rng_, subkey = jax.random.split(self.rng_) if type_as is not None: return jax.random.normal(subkey, shape=size, dtype=type_as.dtype) else: return jax.random.normal(subkey, shape=size) def randperm(self, size, type_as=None): self.rng_, subkey = jax.random.split(self.rng_) if not isinstance(size, int): raise ValueError("size must be an integer") if type_as is not None: return jnp.asarray(jax.random.permutation(subkey, size)) else: return jax.random.permutation(subkey, size) def coo_matrix(self, data, rows, cols, shape=None, type_as=None): # Currently, JAX does not support sparse matrices data = self.to_numpy(data) rows = self.to_numpy(rows) cols = self.to_numpy(cols) nx = NumpyBackend() coo_matrix = nx.coo_matrix(data, rows, cols, shape=shape, type_as=type_as) matrix = nx.todense(coo_matrix) return self.from_numpy(matrix) def issparse(self, a): # Currently, JAX does not support sparse matrices return False def tocsr(self, a): # Currently, JAX does not support sparse matrices return a def eliminate_zeros(self, a, threshold=0.0): # Currently, JAX does not support sparse matrices if threshold > 0: return self.where(self.abs(a) <= threshold, self.zeros((1,), type_as=a), a) return a def todense(self, a): # Currently, JAX does not support sparse matrices return a def sparse_coo_data(self, a): # JAX doesn't support sparse matrices, so this shouldn't be called # But if it is, convert the dense array to sparse using scipy a_np = self.to_numpy(a) a_coo = coo_array(a_np) return a_coo.row, a_coo.col, a_coo.data, a_coo.shape def where(self, condition, x=None, y=None): if x is None and y is None: return jnp.where(condition) else: return jnp.where(condition, x, y) def copy(self, a): # No need to copy, JAX arrays are immutable return a def allclose(self, a, b, rtol=1e-05, atol=1e-08, equal_nan=False): return jnp.allclose(a, b, rtol=rtol, atol=atol, equal_nan=equal_nan) def dtype_device(self, a): if self.jax_new_version: return a.dtype, list(a.devices())[0] else: return a.dtype, a.device_buffer.device() def assert_same_dtype_device(self, a, b): a_dtype, a_device = self.dtype_device(a) b_dtype, b_device = self.dtype_device(b) assert a_dtype == b_dtype, "Dtype discrepancy" assert ( a_device == b_device ), f"Device discrepancy. First input is on {str(a_device)}, whereas second input is on {str(b_device)}" def squeeze(self, a, axis=None): return jnp.squeeze(a, axis=axis) def unsqueeze(self, a, axis): return jnp.expand_dims(a, axis=axis) def bitsize(self, type_as): return type_as.dtype.itemsize * 8 def device_type(self, type_as): return self.dtype_device(type_as)[1].platform.upper() def _bench(self, callable, *args, n_runs=1, warmup_runs=1): results = dict() for type_as in self.__type_list__: inputs = [self.from_numpy(arg, type_as=type_as) for arg in args] for _ in range(warmup_runs): a = callable(*inputs) a.block_until_ready() t0 = time.perf_counter() for _ in range(n_runs): a = callable(*inputs) a.block_until_ready() t1 = time.perf_counter() key = ("Jax", self.device_type(type_as), self.bitsize(type_as)) results[key] = (t1 - t0) / n_runs return results def solve(self, a, b): return jnp.linalg.solve(a, b) def trace(self, a): return jnp.diagonal(a, axis1=-2, axis2=-1).sum(-1) def inv(self, a): return jnp.linalg.inv(a) def pinv(self, a, hermitian=False): return jnp.linalg.pinv(a, hermitian=hermitian) def sqrtm(self, a): L, V = jnp.linalg.eigh(a) L = jnp.sqrt(L) # Q[...] = V[...] @ diag(L[...]) Q = jnp.einsum("...jk,...k->...jk", V, L) # R[...] = Q[...] @ V[...].T return jnp.einsum("...jk,...kl->...jl", Q, jnp.swapaxes(V, -1, -2)) def eigh(self, a): return jnp.linalg.eigh(a) def kl_div(self, p, q, mass=False, eps=1e-16, axis=None): value = jnp.sum(p * jnp.log(p / q + eps), axis=axis) if mass: value = value + jnp.sum(q - p, axis=axis) return value def isfinite(self, a): return jnp.isfinite(a) def array_equal(self, a, b): return jnp.array_equal(a, b) def is_floating_point(self, a): return a.dtype.kind == "f" def tile(self, a, reps): return jnp.tile(a, reps) def floor(self, a): return jnp.floor(a) def prod(self, a, axis=0): return jnp.prod(a, axis=axis) def sort2(self, a, axis=-1): return self.sort(a, axis), self.argsort(a, axis) def qr(self, a): return jnp.linalg.qr(a) def atan2(self, a, b): return jnp.arctan2(a, b) def transpose(self, a, axes=None): return jnp.transpose(a, axes) def matmul(self, a, b): return jnp.matmul(a, b) def nan_to_num(self, x, copy=True, nan=0.0, posinf=None, neginf=None): return jnp.nan_to_num(x, copy=copy, nan=nan, posinf=posinf, neginf=neginf) def det(self, x): return jnp.linalg.det(x) def slogdet(self, a): return jnp.linalg.slogdet(a) if jax: # Only register jax backend if it is installed _register_backend_implementation(JaxBackend) class TorchBackend(Backend): """ PyTorch implementation of the backend - `__name__` is "torch" - `__type__` is torch.Tensor """ __name__ = "torch" __type__ = torch_type __type_list__ = None rng_ = None def __init__(self): self.rng_ = torch.Generator("cpu") self.rng_.seed() self.__type_list__ = [ torch.tensor(1, dtype=torch.float32), torch.tensor(1, dtype=torch.float64), ] if torch.cuda.is_available(): self.rng_cuda_ = torch.Generator("cuda") self.rng_cuda_.seed() self.__type_list__.append( torch.tensor(1, dtype=torch.float32, device="cuda") ) self.__type_list__.append( torch.tensor(1, dtype=torch.float64, device="cuda") ) else: self.rng_cuda_ = torch.Generator("cpu") from torch.autograd import Function from torch.autograd.function import once_differentiable # define a function that takes inputs val and grads # ad returns a val tensor with proper gradients class ValFunction(Function): @staticmethod def forward(ctx, val, grads, *inputs): ctx.grads = grads return val @staticmethod def backward(ctx, grad_output): # the gradients are grad return (None, None) + tuple(g * grad_output for g in ctx.grads) # define a differentiable SPD matrix sqrt # with closed-form VJP class MatrixSqrtFunction(Function): @staticmethod def forward(ctx, a): a_sym = 0.5 * (a + a.transpose(-2, -1)) L, V = torch.linalg.eigh(a_sym) s = L.clamp_min(0).sqrt() y = (V * s.unsqueeze(-2)) @ V.transpose(-2, -1) ctx.save_for_backward(s, V) return y @staticmethod @once_differentiable def backward(ctx, g): s, V = ctx.saved_tensors g_sym = 0.5 * (g + g.transpose(-2, -1)) ghat = V.transpose(-2, -1) @ g_sym @ V d = s.unsqueeze(-1) + s.unsqueeze(-2) xhat = ghat / d xhat = xhat.masked_fill(d == 0, 0) return V @ xhat @ V.transpose(-2, -1) self.ValFunction = ValFunction self.MatrixSqrtFunction = MatrixSqrtFunction def _to_numpy(self, a): if isinstance(a, float) or isinstance(a, int) or isinstance(a, np.ndarray): return np.array(a) return a.cpu().detach().resolve_conj().numpy() def _from_numpy(self, a, type_as=None): if ( isinstance(a, float) or isinstance(a, int) or isinstance(a, np.float32) or isinstance(a, np.float64) ): a = np.array(a) if type_as is None: return torch.from_numpy(a) else: return torch.as_tensor(a, dtype=type_as.dtype, device=type_as.device) def set_gradients(self, val, inputs, grads): Func = self.ValFunction res = Func.apply(val, grads, *inputs) return res def _detach(self, a): return a.detach() def zeros(self, shape, type_as=None): if isinstance(shape, int): shape = (shape,) if type_as is None: return torch.zeros(shape) else: return torch.zeros(shape, dtype=type_as.dtype, device=type_as.device) def ones(self, shape, type_as=None): if isinstance(shape, int): shape = (shape,) if type_as is None: return torch.ones(shape) else: return torch.ones(shape, dtype=type_as.dtype, device=type_as.device) def arange(self, stop, start=0, step=1, type_as=None): if type_as is None: return torch.arange(start, stop, step) else: return torch.arange(start, stop, step, device=type_as.device) def full(self, shape, fill_value, type_as=None): if isinstance(shape, int): shape = (shape,) if type_as is None: return torch.full(shape, fill_value) else: return torch.full( shape, fill_value, dtype=type_as.dtype, device=type_as.device ) def eye(self, N, M=None, type_as=None): if M is None: M = N if type_as is None: return torch.eye(N, m=M) else: return torch.eye(N, m=M, dtype=type_as.dtype, device=type_as.device) def sum(self, a, axis=None, keepdims=False): if axis is None: return torch.sum(a) else: return torch.sum(a, axis, keepdim=keepdims) def cumsum(self, a, axis=None): if axis is None: return torch.cumsum(a.flatten(), 0) else: return torch.cumsum(a, axis) def max(self, a, axis=None, keepdims=False): if axis is None: return torch.max(a) else: return torch.max(a, axis, keepdim=keepdims)[0] def min(self, a, axis=None, keepdims=False): if axis is None: return torch.min(a) else: return torch.min(a, axis, keepdim=keepdims)[0] def maximum(self, a, b): if isinstance(a, int) or isinstance(a, float): a = torch.tensor([float(a)], dtype=b.dtype, device=b.device) if isinstance(b, int) or isinstance(b, float): b = torch.tensor([float(b)], dtype=a.dtype, device=a.device) if hasattr(torch, "maximum"): return torch.maximum(a, b) else: return torch.max(torch.stack(torch.broadcast_tensors(a, b)), axis=0)[0] def minimum(self, a, b): if isinstance(a, int) or isinstance(a, float): a = torch.tensor([float(a)], dtype=b.dtype, device=b.device) if isinstance(b, int) or isinstance(b, float): b = torch.tensor([float(b)], dtype=a.dtype, device=a.device) if hasattr(torch, "minimum"): return torch.minimum(a, b) else: return torch.min(torch.stack(torch.broadcast_tensors(a, b)), axis=0)[0] def sign(self, a): return torch.sign(a) def dot(self, a, b): return torch.matmul(a, b) def abs(self, a): return torch.abs(a) def exp(self, a): return torch.exp(a) def log(self, a): return torch.log(a) def sqrt(self, a): return torch.sqrt(a) def power(self, a, exponents): return torch.pow(a, exponents) def norm(self, a, axis=None, keepdims=False): return torch.linalg.norm(a, dim=axis, keepdims=keepdims) def any(self, a): return torch.any(a) def isnan(self, a): return torch.isnan(a) def isinf(self, a): return torch.isinf(a) def einsum(self, subscripts, *operands): return torch.einsum(subscripts, *operands) def sort(self, a, axis=-1): sorted0, indices = torch.sort(a, dim=axis) return sorted0 def argsort(self, a, axis=-1): sorted, indices = torch.sort(a, dim=axis) return indices def searchsorted(self, a, v, side="left"): right = side != "left" return torch.searchsorted(a, v, right=right) def flip(self, a, axis=None): if axis is None: return torch.flip(a, tuple(i for i in range(len(a.shape)))) if isinstance(axis, int): return torch.flip(a, (axis,)) else: return torch.flip(a, dims=axis) def outer(self, a, b): return torch.outer(a, b) def clip(self, a, a_min=None, a_max=None): return torch.clamp(a, a_min, a_max) def real(self, a): return torch.real(a) def imag(self, a): return torch.imag(a) def conj(self, a): return torch.conj(a) def arccos(self, a): return torch.acos(a) def repeat(self, a, repeats, axis=None): return torch.repeat_interleave(a, repeats, dim=axis) def take_along_axis(self, arr, indices, axis): return torch.gather(arr, axis, indices) def concatenate(self, arrays, axis=0): return torch.cat(arrays, dim=axis) def zero_pad(self, a, pad_width, value=0): from torch.nn.functional import pad # pad_width is an array of ndim tuples indicating how many 0 before and after # we need to add. We first need to make it compliant with torch syntax, that # starts with the last dim, then second last, etc. how_pad = tuple(element for tupl in pad_width[::-1] for element in tupl) return pad(a, how_pad, value=value) def argmax(self, a, axis=None): return torch.argmax(a, dim=axis) def argmin(self, a, axis=None): return torch.argmin(a, dim=axis) def mean(self, a, axis=None): if axis is not None: return torch.mean(a, dim=axis) else: return torch.mean(a) def median(self, a, axis=None): from packaging import version # Since version 1.11.0, interpolation is available if version.parse(torch.__version__) >= version.parse("1.11.0"): if axis is not None: return torch.quantile(a, 0.5, interpolation="midpoint", dim=axis) else: return torch.quantile(a, 0.5, interpolation="midpoint") # Else, use numpy warnings.warn( "The median is being computed using numpy and the array has been detached " "in the Pytorch backend." ) a_ = self.to_numpy(a) a_median = np.median(a_, axis=axis) return self.from_numpy(a_median, type_as=a) def std(self, a, axis=None): if axis is not None: return torch.std(a, dim=axis, unbiased=False) else: return torch.std(a, unbiased=False) def linspace(self, start, stop, num, type_as=None): if type_as is None: return torch.linspace(start, stop, num) else: return torch.linspace( start, stop, num, dtype=type_as.dtype, device=type_as.device ) def meshgrid(self, a, b): try: return torch.meshgrid(a, b, indexing="xy") except TypeError: X, Y = torch.meshgrid(a, b) return X.T, Y.T def diag(self, a, k=0): return torch.diag(a, diagonal=k) def unique(self, a, return_inverse=False): return torch.unique(a, return_inverse=return_inverse) def logsumexp(self, a, axis=None, keepdims=False): if axis is not None: return torch.logsumexp(a, dim=axis, keepdim=keepdims) else: return torch.logsumexp(a, dim=tuple(range(len(a.shape))), keepdim=keepdims) def stack(self, arrays, axis=0): return torch.stack(arrays, dim=axis) def reshape(self, a, shape): return torch.reshape(a, shape) def seed(self, seed=None): if seed is None: pass elif isinstance(seed, int): self.rng_.manual_seed(seed) self.rng_cuda_.manual_seed(seed) elif isinstance(seed, torch.Generator): if self.device_type(seed) == "GPU": self.rng_cuda_ = seed else: self.rng_ = seed else: raise ValueError("Non compatible seed : {}".format(seed)) def rand(self, *size, type_as=None): if type_as is not None: generator = ( self.rng_cuda_ if self.device_type(type_as) == "GPU" else self.rng_ ) return torch.rand( size=size, generator=generator, dtype=type_as.dtype, device=type_as.device, ) else: return torch.rand(size=size, generator=self.rng_) def randn(self, *size, type_as=None): if type_as is not None: generator = ( self.rng_cuda_ if self.device_type(type_as) == "GPU" else self.rng_ ) return torch.randn( size=size, dtype=type_as.dtype, generator=generator, device=type_as.device, ) else: return torch.randn(size=size, generator=self.rng_) def randperm(self, size, type_as=None): if not isinstance(size, int): raise ValueError("size must be an integer") if type_as is not None: generator = ( self.rng_cuda_ if self.device_type(type_as) == "GPU" else self.rng_ ) return torch.randperm( n=size, generator=generator, device=type_as.device, ) else: return torch.randperm(n=size, generator=self.rng_) def coo_matrix(self, data, rows, cols, shape=None, type_as=None): if type_as is None: return torch.sparse_coo_tensor(torch.stack([rows, cols]), data, size=shape) else: return torch.sparse_coo_tensor( torch.stack([rows, cols]), data, size=shape, dtype=type_as.dtype, device=type_as.device, ) def issparse(self, a): return getattr(a, "is_sparse", False) or getattr(a, "is_sparse_csr", False) def tocsr(self, a): # Versions older than 1.9 do not support CSR tensors. PyTorch 1.9 and 1.10 offer a very limited support return self.todense(a) def eliminate_zeros(self, a, threshold=0.0): if self.issparse(a): if threshold > 0: mask = self.abs(a) <= threshold mask = ~mask mask = mask.nonzero() else: mask = a._values().nonzero() nv = a._values().index_select(0, mask.view(-1)) ni = a._indices().index_select(1, mask.view(-1)) return self.coo_matrix(nv, ni[0], ni[1], shape=a.shape, type_as=a) else: if threshold > 0: a[self.abs(a) <= threshold] = 0 return a def todense(self, a): if self.issparse(a): return a.to_dense() else: return a def sparse_coo_data(self, a): # For torch sparse tensors, coalesce first to ensure unique indices a_coalesced = a.coalesce() indices = a_coalesced._indices() values = a_coalesced._values() # Convert to numpy row = self.to_numpy(indices[0]) col = self.to_numpy(indices[1]) data = self.to_numpy(values) shape = tuple(a_coalesced.shape) return row, col, data, shape def where(self, condition, x=None, y=None): if x is None and y is None: return torch.where(condition) else: return torch.where(condition, x, y) def copy(self, a): return torch.clone(a) def allclose(self, a, b, rtol=1e-05, atol=1e-08, equal_nan=False): return torch.allclose(a, b, rtol=rtol, atol=atol, equal_nan=equal_nan) def dtype_device(self, a): return a.dtype, a.device def assert_same_dtype_device(self, a, b): a_dtype, a_device = self.dtype_device(a) b_dtype, b_device = self.dtype_device(b) assert a_dtype == b_dtype, "Dtype discrepancy" assert ( a_device == b_device ), f"Device discrepancy. First input is on {str(a_device)}, whereas second input is on {str(b_device)}" def squeeze(self, a, axis=None): if axis is None: return torch.squeeze(a) else: return torch.squeeze(a, dim=axis) def unsqueeze(self, a, axis): return torch.unsqueeze(a, dim=axis) def bitsize(self, type_as): return torch.finfo(type_as.dtype).bits def device_type(self, type_as): return type_as.device.type.replace("cuda", "gpu").upper() def _bench(self, callable, *args, n_runs=1, warmup_runs=1): results = dict() for type_as in self.__type_list__: inputs = [self.from_numpy(arg, type_as=type_as) for arg in args] for _ in range(warmup_runs): callable(*inputs) if self.device_type(type_as) == "GPU": # pragma: no cover torch.cuda.synchronize() start = torch.cuda.Event(enable_timing=True) end = torch.cuda.Event(enable_timing=True) start.record() else: start = time.perf_counter() for _ in range(n_runs): callable(*inputs) if self.device_type(type_as) == "GPU": # pragma: no cover end.record() torch.cuda.synchronize() duration = start.elapsed_time(end) / 1000.0 else: end = time.perf_counter() duration = end - start key = ("Pytorch", self.device_type(type_as), self.bitsize(type_as)) results[key] = duration / n_runs if torch.cuda.is_available(): torch.cuda.empty_cache() return results def solve(self, a, b): return torch.linalg.solve(a, b) def trace(self, a): return torch.diagonal(a, dim1=-2, dim2=-1).sum(-1) def inv(self, a): return torch.linalg.inv(a) def pinv(self, a, hermitian=False): return torch.linalg.pinv(a, hermitian=hermitian) def sqrtm(self, a): return self.MatrixSqrtFunction.apply(a) def eigh(self, a): return torch.linalg.eigh(a) def kl_div(self, p, q, mass=False, eps=1e-16, axis=None): value = torch.sum(p * torch.log(p / q + eps), axis=axis) if mass: value = value + torch.sum(q - p, axis=axis) return value def isfinite(self, a): return torch.isfinite(a) def array_equal(self, a, b): return torch.equal(a, b) def is_floating_point(self, a): return a.dtype.is_floating_point def tile(self, a, reps): return a.repeat(reps) def floor(self, a): return torch.floor(a) def prod(self, a, axis=0): return torch.prod(a, dim=axis) def sort2(self, a, axis=-1): return torch.sort(a, axis) def qr(self, a): return torch.linalg.qr(a) def atan2(self, a, b): return torch.atan2(a, b) def transpose(self, a, axes=None): if axes is None: axes = tuple(range(a.ndim)[::-1]) return a.permute(axes) def matmul(self, a, b): return torch.matmul(a, b) def nan_to_num(self, x, copy=True, nan=0.0, posinf=None, neginf=None): out = None if copy else x return torch.nan_to_num(x, nan=nan, posinf=posinf, neginf=neginf, out=out) def det(self, x): return torch.linalg.det(x) def slogdet(self, a): return torch.linalg.slogdet(a) if torch: # Only register torch backend if it is installed _register_backend_implementation(TorchBackend) class CupyBackend(Backend): # pragma: no cover """ CuPy implementation of the backend - `__name__` is "cupy" - `__type__` is cp.ndarray """ __name__ = "cupy" __type__ = cp_type __type_list__ = None rng_ = None def __init__(self): self.rng_ = cp.random.RandomState() self.__type_list__ = [ cp.array(1, dtype=cp.float32), cp.array(1, dtype=cp.float64), ] def _to_numpy(self, a): return cp.asnumpy(a) def _from_numpy(self, a, type_as=None): if isinstance(a, float): a = np.array(a) if type_as is None: return cp.asarray(a) else: with cp.cuda.Device(type_as.device): return cp.asarray(a, dtype=type_as.dtype) def set_gradients(self, val, inputs, grads): # No gradients for cupy return val def _detach(self, a): return a def zeros(self, shape, type_as=None): if isinstance(shape, (list, tuple)): shape = tuple(int(i) for i in shape) if type_as is None: return cp.zeros(shape) else: with cp.cuda.Device(type_as.device): return cp.zeros(shape, dtype=type_as.dtype) def ones(self, shape, type_as=None): if isinstance(shape, (list, tuple)): shape = tuple(int(i) for i in shape) if type_as is None: return cp.ones(shape) else: with cp.cuda.Device(type_as.device): return cp.ones(shape, dtype=type_as.dtype) def arange(self, stop, start=0, step=1, type_as=None): return cp.arange(start, stop, step) def full(self, shape, fill_value, type_as=None): if isinstance(shape, (list, tuple)): shape = tuple(int(i) for i in shape) if type_as is None: return cp.full(shape, fill_value) else: with cp.cuda.Device(type_as.device): return cp.full(shape, fill_value, dtype=type_as.dtype) def eye(self, N, M=None, type_as=None): if type_as is None: return cp.eye(N, M) else: with cp.cuda.Device(type_as.device): return cp.eye(N, M, dtype=type_as.dtype) def sum(self, a, axis=None, keepdims=False): return cp.sum(a, axis, keepdims=keepdims) def cumsum(self, a, axis=None): return cp.cumsum(a, axis) def max(self, a, axis=None, keepdims=False): return cp.max(a, axis, keepdims=keepdims) def min(self, a, axis=None, keepdims=False): return cp.min(a, axis, keepdims=keepdims) def maximum(self, a, b): return cp.maximum(a, b) def minimum(self, a, b): return cp.minimum(a, b) def sign(self, a): return cp.sign(a) def abs(self, a): return cp.abs(a) def exp(self, a): return cp.exp(a) def log(self, a): return cp.log(a) def sqrt(self, a): return cp.sqrt(a) def power(self, a, exponents): return cp.power(a, exponents) def dot(self, a, b): return cp.dot(a, b) def norm(self, a, axis=None, keepdims=False): return cp.linalg.norm(a, axis=axis, keepdims=keepdims) def any(self, a): return cp.any(a) def isnan(self, a): return cp.isnan(a) def isinf(self, a): return cp.isinf(a) def einsum(self, subscripts, *operands): return cp.einsum(subscripts, *operands) def sort(self, a, axis=-1): return cp.sort(a, axis) def argsort(self, a, axis=-1): return cp.argsort(a, axis) def searchsorted(self, a, v, side="left"): if a.ndim == 1: return cp.searchsorted(a, v, side) else: # this is a not very efficient way to make numpy # searchsorted work on 2d arrays ret = cp.empty(v.shape, dtype=int) for i in range(a.shape[0]): ret[i, :] = cp.searchsorted(a[i, :], v[i, :], side) return ret def flip(self, a, axis=None): return cp.flip(a, axis) def outer(self, a, b): return cp.outer(a, b) def clip(self, a, a_min=None, a_max=None): return cp.clip(a, a_min, a_max) def real(self, a): return cp.real(a) def imag(self, a): return cp.imag(a) def conj(self, a): return cp.conj(a) def arccos(self, a): return cp.arccos(a) def repeat(self, a, repeats, axis=None): return cp.repeat(a, repeats, axis) def take_along_axis(self, arr, indices, axis): return cp.take_along_axis(arr, indices, axis) def concatenate(self, arrays, axis=0): return cp.concatenate(arrays, axis) def zero_pad(self, a, pad_width, value=0): return cp.pad(a, pad_width, constant_values=value) def argmax(self, a, axis=None): return cp.argmax(a, axis=axis) def argmin(self, a, axis=None): return cp.argmin(a, axis=axis) def mean(self, a, axis=None): return cp.mean(a, axis=axis) def median(self, a, axis=None): return cp.median(a, axis=axis) def std(self, a, axis=None): return cp.std(a, axis=axis) def linspace(self, start, stop, num, type_as=None): if type_as is None: return cp.linspace(start, stop, num) else: with cp.cuda.Device(type_as.device): return cp.linspace(start, stop, num, dtype=type_as.dtype) def meshgrid(self, a, b): return cp.meshgrid(a, b) def diag(self, a, k=0): return cp.diag(a, k) def unique(self, a, return_inverse=False): return cp.unique(a, return_inverse=return_inverse) def logsumexp(self, a, axis=None): # Taken from # https://github.com/scipy/scipy/blob/v1.7.1/scipy/special/_logsumexp.py#L7-L127 a_max = cp.amax(a, axis=axis, keepdims=True) if a_max.ndim > 0: a_max[~cp.isfinite(a_max)] = 0 elif not cp.isfinite(a_max): a_max = 0 tmp = cp.exp(a - a_max) s = cp.sum(tmp, axis=axis) out = cp.log(s) a_max = cp.squeeze(a_max, axis=axis) out += a_max return out def stack(self, arrays, axis=0): return cp.stack(arrays, axis) def reshape(self, a, shape): return cp.reshape(a, shape) def seed(self, seed=None): if seed is not None: self.rng_.seed(seed) def rand(self, *size, type_as=None): if type_as is None: return self.rng_.rand(*size) else: with cp.cuda.Device(type_as.device): return self.rng_.rand(*size, dtype=type_as.dtype) def randn(self, *size, type_as=None): if type_as is None: return self.rng_.randn(*size) else: with cp.cuda.Device(type_as.device): return self.rng_.randn(*size, dtype=type_as.dtype) def randperm(self, size, type_as=None): if not isinstance(size, int): raise ValueError("size must be an integer") if type_as is None: return self.rng_.permutation(size) else: with cp.cuda.Device(type_as.device): return cp.asarray(self.rng_.permutation(size)) def coo_matrix(self, data, rows, cols, shape=None, type_as=None): data = self.from_numpy(data) rows = self.from_numpy(rows) cols = self.from_numpy(cols) if type_as is None: return cupyx.scipy.sparse.coo_array((data, (rows, cols)), shape=shape) else: with cp.cuda.Device(type_as.device): return cupyx.scipy.sparse.coo_array( (data, (rows, cols)), shape=shape, dtype=type_as.dtype ) def issparse(self, a): return cupyx.scipy.sparse.issparse(a) def tocsr(self, a): if self.issparse(a): return a.tocsr() else: return cupyx.scipy.sparse.csr_matrix(a) def eliminate_zeros(self, a, threshold=0.0): if threshold > 0: if self.issparse(a): a.data[self.abs(a.data) <= threshold] = 0 else: a[self.abs(a) <= threshold] = 0 if self.issparse(a): a.eliminate_zeros() return a def todense(self, a): if self.issparse(a): return a.toarray() else: return a def where(self, condition, x=None, y=None): if x is None and y is None: return cp.where(condition) else: return cp.where(condition, x, y) def copy(self, a): return a.copy() def allclose(self, a, b, rtol=1e-05, atol=1e-08, equal_nan=False): return cp.allclose(a, b, rtol=rtol, atol=atol, equal_nan=equal_nan) def dtype_device(self, a): return a.dtype, a.device def assert_same_dtype_device(self, a, b): a_dtype, a_device = self.dtype_device(a) b_dtype, b_device = self.dtype_device(b) # cupy has implicit type conversion so # we automatically validate the test for type assert ( a_device == b_device ), f"Device discrepancy. First input is on {str(a_device)}, whereas second input is on {str(b_device)}" def squeeze(self, a, axis=None): return cp.squeeze(a, axis=axis) def bitsize(self, type_as): return type_as.itemsize * 8 def device_type(self, type_as): return "GPU" def _bench(self, callable, *args, n_runs=1, warmup_runs=1): mempool = cp.get_default_memory_pool() pinned_mempool = cp.get_default_pinned_memory_pool() results = dict() for type_as in self.__type_list__: inputs = [self.from_numpy(arg, type_as=type_as) for arg in args] start_gpu = cp.cuda.Event() end_gpu = cp.cuda.Event() for _ in range(warmup_runs): callable(*inputs) start_gpu.synchronize() start_gpu.record() for _ in range(n_runs): callable(*inputs) end_gpu.record() end_gpu.synchronize() key = ("Cupy", self.device_type(type_as), self.bitsize(type_as)) t_gpu = cp.cuda.get_elapsed_time(start_gpu, end_gpu) / 1000.0 results[key] = t_gpu / n_runs mempool.free_all_blocks() pinned_mempool.free_all_blocks() return results def solve(self, a, b): return cp.linalg.solve(a, b) def trace(self, a): return cp.trace(a, axis1=-2, axis2=-1) def inv(self, a): return cp.linalg.inv(a) def pinv(self, a, hermitian=False): return cp.linalg.pinv(a) def sqrtm(self, a): L, V = cp.linalg.eigh(a) L = cp.sqrt(L) # Q[...] = V[...] @ diag(L[...]) Q = cp.einsum("...jk,...k->...jk", V, L) # R[...] = Q[...] @ V[...].T return cp.einsum("...jk,...kl->...jl", Q, cp.swapaxes(V, -1, -2)) def eigh(self, a): return cp.linalg.eigh(a) def kl_div(self, p, q, mass=False, eps=1e-16, axis=None): value = cp.sum(p * cp.log(p / q + eps), axis=axis) if mass: value = value + cp.sum(q - p, axis=axis) return value def isfinite(self, a): return cp.isfinite(a) def array_equal(self, a, b): return cp.array_equal(a, b) def is_floating_point(self, a): return a.dtype.kind == "f" def tile(self, a, reps): return cp.tile(a, reps) def floor(self, a): return cp.floor(a) def prod(self, a, axis=0): return cp.prod(a, axis=axis) def sort2(self, a, axis=-1): return self.sort(a, axis), self.argsort(a, axis) def qr(self, a): return cp.linalg.qr(a) def atan2(self, a, b): return cp.arctan2(a, b) def transpose(self, a, axes=None): return cp.transpose(a, axes) def matmul(self, a, b): return cp.matmul(a, b) def nan_to_num(self, x, copy=True, nan=0.0, posinf=None, neginf=None): return cp.nan_to_num(x, copy=copy, nan=nan, posinf=posinf, neginf=neginf) def det(self, x): return cp.linalg.det(x) def slogdet(self, a): return cp.linalg.slogdet(a) if cp: # Only register cp backend if it is installed _register_backend_implementation(CupyBackend) class TensorflowBackend(Backend): __name__ = "tf" __type__ = tf_type __type_list__ = None rng_ = None def __init__(self): self.seed(None) self.__type_list__ = [ tf.convert_to_tensor([1], dtype=tf.float32), tf.convert_to_tensor([1], dtype=tf.float64), ] tmp = self.randn(15, 10) try: tmp.reshape((150, 1)) except AttributeError: warnings.warn( "To use TensorflowBackend, you need to activate the tensorflow " "numpy API. You can activate it by running: \n" "from tensorflow.python.ops.numpy_ops import np_config\n" "np_config.enable_numpy_behavior()", stacklevel=2, ) def _to_numpy(self, a): if isinstance(a, float) or isinstance(a, int) or isinstance(a, np.ndarray): return np.array(a) return a.numpy() def _from_numpy(self, a, type_as=None): if isinstance(a, float): a = np.array(a) if not isinstance(a, self.__type__): if type_as is None: return tf.convert_to_tensor(a) else: return tf.convert_to_tensor(a, dtype=type_as.dtype) else: if type_as is None: return a else: return tf.cast(a, dtype=type_as.dtype) def set_gradients(self, val, inputs, grads): @tf.custom_gradient def tmp(input): def grad(upstream): return grads return val, grad return tmp(inputs) def _detach(self, a): return tf.stop_gradient(a) def zeros(self, shape, type_as=None): if type_as is None: return tnp.zeros(shape) else: return tnp.zeros(shape, dtype=type_as.dtype) def ones(self, shape, type_as=None): if type_as is None: return tnp.ones(shape) else: return tnp.ones(shape, dtype=type_as.dtype) def arange(self, stop, start=0, step=1, type_as=None): return tnp.arange(start, stop, step) def full(self, shape, fill_value, type_as=None): if type_as is None: return tnp.full(shape, fill_value) else: return tnp.full(shape, fill_value, dtype=type_as.dtype) def eye(self, N, M=None, type_as=None): if type_as is None: return tnp.eye(N, M) else: return tnp.eye(N, M, dtype=type_as.dtype) def sum(self, a, axis=None, keepdims=False): return tnp.sum(a, axis, keepdims=keepdims) def cumsum(self, a, axis=None): return tnp.cumsum(a, axis) def max(self, a, axis=None, keepdims=False): return tnp.max(a, axis, keepdims=keepdims) def min(self, a, axis=None, keepdims=False): return tnp.min(a, axis, keepdims=keepdims) def maximum(self, a, b): return tnp.maximum(a, b) def minimum(self, a, b): return tnp.minimum(a, b) def sign(self, a): return tnp.sign(a) def dot(self, a, b): if len(b.shape) == 1: if len(a.shape) == 1: # inner product return tf.reduce_sum(tf.multiply(a, b)) else: # matrix vector return tf.linalg.matvec(a, b) else: if len(a.shape) == 1: return tf.linalg.matvec(b.T, a.T).T else: return tf.matmul(a, b) def abs(self, a): return tnp.abs(a) def exp(self, a): return tnp.exp(a) def log(self, a): return tnp.log(a) def sqrt(self, a): return tnp.sqrt(a) def power(self, a, exponents): return tnp.power(a, exponents) def norm(self, a, axis=None, keepdims=False): return tf.math.reduce_euclidean_norm(a, axis=axis, keepdims=keepdims) def any(self, a): return tnp.any(a) def isnan(self, a): return tnp.isnan(a) def isinf(self, a): return tnp.isinf(a) def einsum(self, subscripts, *operands): return tnp.einsum(subscripts, *operands) def sort(self, a, axis=-1): return tnp.sort(a, axis) def argsort(self, a, axis=-1): return tnp.argsort(a, axis) def searchsorted(self, a, v, side="left"): return tf.searchsorted(a, v, side=side) def flip(self, a, axis=None): return tnp.flip(a, axis) def outer(self, a, b): return tnp.outer(a, b) def clip(self, a, a_min=None, a_max=None): return tnp.clip(a, a_min, a_max) def real(self, a): return tnp.real(a) def imag(self, a): return tnp.imag(a) def conj(self, a): return tnp.conj(a) def arccos(self, a): return tnp.arccos(a) def repeat(self, a, repeats, axis=None): return tnp.repeat(a, repeats, axis) def take_along_axis(self, arr, indices, axis): return tnp.take_along_axis(arr, indices, axis) def concatenate(self, arrays, axis=0): return tnp.concatenate(arrays, axis) def zero_pad(self, a, pad_width, value=0): return tnp.pad(a, pad_width, mode="constant", constant_values=value) def argmax(self, a, axis=None): return tnp.argmax(a, axis=axis) def argmin(self, a, axis=None): return tnp.argmin(a, axis=axis) def mean(self, a, axis=None): return tnp.mean(a, axis=axis) def median(self, a, axis=None): warnings.warn( "The median is being computed using numpy and the array has been detached " "in the Tensorflow backend." ) a_ = self.to_numpy(a) a_median = np.median(a_, axis=axis) return self.from_numpy(a_median, type_as=a) def std(self, a, axis=None): return tnp.std(a, axis=axis) def linspace(self, start, stop, num, type_as=None): if type_as is None: return tnp.linspace(start, stop, num) else: return tnp.linspace(start, stop, num, dtype=type_as.dtype) def meshgrid(self, a, b): return tnp.meshgrid(a, b) def diag(self, a, k=0): return tnp.diag(a, k) def unique(self, a, return_inverse=False): y, idx = tf.unique(tf.reshape(a, [-1])) sort_idx = tf.argsort(y) y_prime = tf.gather(y, sort_idx) if return_inverse: inv_sort_idx = tf.math.invert_permutation(sort_idx) return y_prime, tf.gather(inv_sort_idx, idx) else: return y_prime def logsumexp(self, a, axis=None): return tf.math.reduce_logsumexp(a, axis=axis) def stack(self, arrays, axis=0): return tnp.stack(arrays, axis) def reshape(self, a, shape): return tnp.reshape(a, shape) def seed(self, seed=None): if isinstance(seed, int): self.rng_ = tf.random.Generator.from_seed(seed) elif isinstance(seed, tf.random.Generator): self.rng_ = seed elif seed is None: self.rng_ = tf.random.Generator.from_non_deterministic_state() else: raise ValueError("Non compatible seed : {}".format(seed)) def rand(self, *size, type_as=None): if type_as is None: return self.rng_.uniform(size, minval=0.0, maxval=1.0) else: return self.rng_.uniform(size, minval=0.0, maxval=1.0, dtype=type_as.dtype) def randn(self, *size, type_as=None): if type_as is None: return self.rng_.normal(size) else: return self.rng_.normal(size, dtype=type_as.dtype) def randperm(self, size, type_as=None): if not isinstance(size, int): raise ValueError("size must be an integer") local_seed = self.rng_.make_seeds(2)[0] if type_as is None: return tf.random.experimental.stateless_shuffle( tf.range(size), seed=local_seed ) else: return tf.random.experimental.stateless_shuffle( tf.range(size), seed=local_seed ) def _convert_to_index_for_coo(self, tensor): if isinstance(tensor, self.__type__): return int(self.max(tensor)) + 1 else: return int(np.max(tensor)) + 1 def coo_matrix(self, data, rows, cols, shape=None, type_as=None): if shape is None: shape = ( self._convert_to_index_for_coo(rows), self._convert_to_index_for_coo(cols), ) if type_as is not None: data = self.from_numpy(data, type_as=type_as) rows = tf.cast(rows, dtype=tf.int64) cols = tf.cast(cols, dtype=tf.int64) sparse_tensor = tf.sparse.SparseTensor( indices=tnp.stack([rows, cols]).T, values=data, dense_shape=shape ) # avoid duplicate indices by summing them up sparse_tensor = tf.sparse.reorder(sparse_tensor) sparse_tensor = tf.sparse.reduce_sum( tf.sparse.expand_dims(sparse_tensor, axis=0), axis=0 ) return self.todense(sparse_tensor) def issparse(self, a): return isinstance(a, tf.sparse.SparseTensor) def tocsr(self, a): return a def eliminate_zeros(self, a, threshold=0.0): if self.issparse(a): values = a.values if threshold > 0: mask = self.abs(values) <= threshold else: mask = values == 0 return tf.sparse.retain(a, ~mask) else: if threshold > 0: a = tnp.where(self.abs(a) > threshold, a, 0.0) return a def todense(self, a): if self.issparse(a): return tf.sparse.to_dense(tf.sparse.reorder(a)) else: return a def where(self, condition, x=None, y=None): if x is None and y is None: return tnp.where(condition) else: return tnp.where(condition, x, y) def copy(self, a): return tf.identity(a) def allclose(self, a, b, rtol=1e-05, atol=1e-08, equal_nan=False): return tnp.allclose(a, b, rtol=rtol, atol=atol, equal_nan=equal_nan) def dtype_device(self, a): return a.dtype, a.device.split("device:")[1] def assert_same_dtype_device(self, a, b): a_dtype, a_device = self.dtype_device(a) b_dtype, b_device = self.dtype_device(b) assert a_dtype == b_dtype, "Dtype discrepancy" assert ( a_device == b_device ), f"Device discrepancy. First input is on {str(a_device)}, whereas second input is on {str(b_device)}" def squeeze(self, a, axis=None): return tnp.squeeze(a, axis=axis) def unsqueeze(self, a, axis): return tnp.expand_dims(a, axis=axis) def bitsize(self, type_as): return type_as.dtype.size * 8 def device_type(self, type_as): return self.dtype_device(type_as)[1].split(":")[0] def _bench(self, callable, *args, n_runs=1, warmup_runs=1): results = dict() device_contexts = [tf.device("/CPU:0")] if len(tf.config.list_physical_devices("GPU")) > 0: # pragma: no cover device_contexts.append(tf.device("/GPU:0")) for device_context in device_contexts: with device_context: for type_as in self.__type_list__: inputs = [self.from_numpy(arg, type_as=type_as) for arg in args] for _ in range(warmup_runs): callable(*inputs) t0 = time.perf_counter() for _ in range(n_runs): res = callable(*inputs) _ = res.numpy() t1 = time.perf_counter() key = ( "Tensorflow", self.device_type(inputs[0]), self.bitsize(type_as), ) results[key] = (t1 - t0) / n_runs return results def solve(self, a, b): return tf.linalg.solve(a, b) def trace(self, a): return tf.linalg.trace(a) def inv(self, a): return tf.linalg.inv(a) def pinv(self, a, hermitian=False): return tf.linalg.pinv(a) def sqrtm(self, a): L, V = tf.linalg.eigh(a) L = tf.sqrt(L) # Q[...] = V[...] @ diag(L[...]) Q = tf.einsum("...jk,...k->...jk", V, L) # R[...] = Q[...] @ V[...].T return tf.einsum( "...jk,...kl->...jl", Q, tf.linalg.matrix_transpose(V, (0, 2, 1)) ) def eigh(self, a): return tf.linalg.eigh(a) def kl_div(self, p, q, mass=False, eps=1e-16, axis=None): value = tnp.sum(p * tnp.log(p / q + eps), axis=axis) if mass: value = value + tnp.sum(q - p, axis=axis) return value def isfinite(self, a): return tnp.isfinite(a) def array_equal(self, a, b): return tnp.array_equal(a, b) def is_floating_point(self, a): return a.dtype.is_floating def tile(self, a, reps): return tnp.tile(a, reps) def floor(self, a): return tf.floor(a) def prod(self, a, axis=0): return tnp.prod(a, axis=axis) def sort2(self, a, axis=-1): return self.sort(a, axis), self.argsort(a, axis) def qr(self, a): return tf.linalg.qr(a) def atan2(self, a, b): return tf.math.atan2(a, b) def transpose(self, a, axes=None): return tf.transpose(a, perm=axes) def matmul(self, a, b): return tnp.matmul(a, b) # todo(okachaiev): replace this with a more reasonable implementation def nan_to_num(self, x, copy=True, nan=0.0, posinf=None, neginf=None): x = self.to_numpy(x) x = np.nan_to_num(x, copy=copy, nan=nan, posinf=posinf, neginf=neginf) return self.from_numpy(x) def det(self, x): return tf.linalg.det(x) def slogdet(self, a): return tf.linalg.slogdet(a) if tf: # Only register tensorflow backend if it is installed _register_backend_implementation(TensorflowBackend) python-pot-0.9.7+dfsg/ot/batch/000077500000000000000000000000001525521106200162715ustar00rootroot00000000000000python-pot-0.9.7+dfsg/ot/batch/__init__.py000066400000000000000000000020161525521106200204010ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Batch operations for optimal transport. """ # Author: Remi Flamary # Paul Krzakala # Thibaut Germain # # License: MIT License from ._linear import ( solve_batch, solve_sample_batch, dist_batch, loss_linear_batch, loss_linear_samples_batch, ) from ._quadratic import ( solve_gromov_batch, loss_quadratic_batch, loss_quadratic_samples_batch, tensor_batch, ) from ._utils import ( bregman_log_projection_batch, bregman_projection_batch, entropy_batch, proximal_bregman_log_plan_batch, ) __all__ = [ "solve_batch", "solve_sample_batch", "solve_gromov_batch", "dist_batch", "loss_linear_batch", "loss_linear_samples_batch", "bregman_log_projection_batch", "bregman_projection_batch", "entropy_batch", "loss_quadratic_batch", "loss_quadratic_samples_batch", "tensor_batch", "proximal_bregman_log_plan_batch", ] python-pot-0.9.7+dfsg/ot/batch/_linear.py000066400000000000000000000426471525521106200202710ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Batch operations for linear optimal transport. """ # Author: Remi Flamary # Paul Krzakala # Thibaut Germain # # License: MIT License from ..backend import get_backend from ..utils import OTResult from ._utils import ( bregman_log_projection_batch, bregman_projection_batch, entropy_batch, proximal_bregman_log_plan_batch, ) solve_batch_method_lst = ["auto", "proximal", "log_sinkhorn", "sinkhorn"] solve_batch_reg_type_lst = ["kl", "entropy"] solve_batch_grad_lst = ["detach", "autodiff", "last_step", "envelope"] def dist_lp_batch(X, Y, p=2, q=1, nx=None): r"""Computes the cost matrix for a batch of samples using the Lp norm. .. math:: M_{bij} = ( \sum_{d} (x_{bid} - y_{bjd})^p )^{q/p} = ||x_{bi} - y_{bj}||_p^q Parameters ---------- X : array-like, shape (B, ns, d) Samples from source distribution Y : array-like, shape (B, nt, d) Samples from target distribution p : float, optional The order of the norm to use. Default is 2 (Euclidean distance). q : float, optional If None, use the Lp norm. If specified, it computes the Lp norm raised to the power of q. nx : backend, optional Backend to use for computations. If None, it will be inferred from the inputs. Returns ------- M : array-like, shape (B, ns, nt) Cost matrix where M[bij] is the cost between sample i in batch b and sample j in batch b. """ if nx is None: nx = get_backend(X, Y) M = nx.abs(X[:, :, None] - Y[:, None, :]) ** p M = M.sum(axis=-1) if q != p: M = M ** (q / p) return M def dist_euclidean_batch(X, Y, squared=True, nx=None): r"""Computes the squared Euclidean cost matrix for a batch of samples. .. math:: M_{bij} = \sum_{d} (x_{bid} - y_{bjd})^2 = ||x_{bi} - y_{bj}||_2^2 Parameters ---------- X : array-like, shape (B, ns, d) Samples from source distribution Y : array-like, shape (B, nt, d) Samples from target distribution squared : bool, optional If True, returns the squared Euclidean distance. Default is True. nx : backend, optional Backend to use for computations. If None, it will be inferred from the inputs. Returns ------- M : array-like, shape (B, ns, nt) Cost matrix where M[bij] is the cost between sample i in batch b and sample j in batch b. """ if nx is None: nx = get_backend(X, Y) XX = nx.sum(X**2, axis=-1, keepdims=True) YY = nx.sum(Y**2, axis=-1, keepdims=True) YY = nx.transpose(YY, axes=(0, 2, 1)) M = XX + YY - 2 * nx.einsum("bid,bjd->bij", X, Y) if not squared: M = nx.sqrt(M) return M def dist_kl_batch(X, Y, logits_X=False, nx=None, eps=1e-10): r"""Computes the KL divergence cost matrix for a batch of samples. .. math:: M_{bij} = \sum_{d} y_{bjd} * log(y_{bjd}/X_{bid}) = KL(y_{bj} || x_{bi}) Parameters ---------- X : array-like, shape (B, ns, d) Samples from source distribution Y : array-like, shape (B, nt, d) Samples from target distribution logits_X : bool, optional If True, X is assumed to be in log space (logits). Default is False. nx : backend, optional Backend to use for computations. If None, it will be inferred from the inputs. Returns ------- M : array-like, shape (B, ns, nt) Cost matrix where M[bij] is the cost between sample i in batch b and sample j in batch b. """ if nx is None: nx = get_backend(X, Y) entr_y = nx.sum(Y * nx.log(Y + eps), axis=-1) # B x m if logits_X: M = entr_y[:, None, :] - Y[:, None, :] * X[:, :, None] else: M = entr_y[:, None, :] - nx.sum( Y[:, None, :] * nx.log(X + eps)[:, :, None], axis=-1 ) return M def loss_linear_batch(M, T, nx=None): r"""Computes the linear optimal transport loss given a batch cost matrices and transport plans. .. math:: L(T, M)_b = \langle T_b, M_b \rangle_F Parameters ---------- M : array-like, shape (B, ns, nt) Cost matrix T : array-like, shape (B, ns, nt) Transport plan Returns ------- loss : array-like, shape (B,) Loss value for each batch element See Also -------- ot.batch.dist_batch : batched cost matrix computation for computing M. ot.batch.solve_batch : solver for computing the optimal T. """ if nx is None: nx = get_backend(M, T) return nx.sum(M * T, axis=(1, 2)) def loss_linear_samples_batch(X, Y, T, metric="sqeuclidean"): r"""Computes the linear optimal transport loss given samples and transport plan. This is the equivalent of calling `dist_batch` and then `loss_linear_batch`. Parameters ---------- X : array-like, shape (B, ns, d) Samples from source distribution Y : array-like, shape (B, nt, d) Samples from target distribution T : array-like, shape (B, ns, nt) Transport plan metric : str, optional 'sqeuclidean', 'euclidean', 'minkowski' or 'kl' Returns ------- loss : array-like, shape (B,) Loss value for each batch element See Also -------- ot.batch.dist_batch : batched cost matrix computation for computing M. ot.batch.solve_batch : solver for computing the optimal T. """ M = dist_batch(X, Y, metric=metric) return loss_linear_batch(M, T) def dist_batch( X1, X2=None, metric="sqeuclidean", p=2, nx=None, ): r"""Batched version of ot.dist, use it to compute many distance matrices in parallel. Parameters ---------- X1 : array-like, shape (b,n1,d) `b` matrices with `n1` samples of size `d` X2 : array-like, shape (b,n2,d), optional `b` matrices with `n2` samples of size `d` (if None then :math:`\mathbf{X_2} = \mathbf{X_1}`) metric : str, optional 'sqeuclidean', 'euclidean', 'minkowski' or 'kl' p : float, optional p-norm for the Minkowski metrics. Default value is 2. nx : Backend, optional Backend to perform computations on. If omitted, the backend defaults to that of `x1`. Returns ------- M : array-like, shape (`b`, `n1`, `n2`) distance matrix computed with given metric Examples -------- >>> import numpy as np >>> from ot.batch import dist_batch >>> X1 = np.random.randn(5, 10, 3) >>> X2 = np.random.randn(5, 15, 3) >>> M = dist_batch(X1, X2, metric="euclidean") >>> M.shape (5, 10, 15) See Also -------- ot.dist : equivalent non-batched function. """ X2 = X2 if X2 is not None else X1 metric = metric.lower() if metric == "sqeuclidean": M = dist_euclidean_batch(X1, X2, squared=True, nx=nx) elif metric == "euclidean": M = dist_euclidean_batch(X1, X2, squared=False, nx=nx) elif metric == "minkowski": M = dist_lp_batch(X1, X2, p=p, q=1, nx=nx) elif metric == "kl": M = dist_kl_batch(X1, X2, logits_X=False, nx=nx) else: raise ValueError(f"Unknown metric: {metric}") return M def solve_batch( M, reg=None, a=None, b=None, max_iter=1000, tol=1e-5, method="auto", inner_iter=1, inner_reg=1e-3, reg_type="entropy", grad="envelope", ): r""" Return solutions of a batch of discrete optimal transport problems in a :any:`OTResult` object. The function solves in parallel a batch of optimal transport problems: .. math:: \begin{aligned} \mathbf{T} = \mathop{\arg \min}_\mathbf{T} \quad & \langle \mathbf{T}, \mathbf{M} \rangle_F + \textit{reg} \cdot R(\mathbf{T}) \\ \text{s.t.} \quad & \mathbf{T} \mathbf{1} = \mathbf{a} \\ & \mathbf{T}^T \mathbf{1} = \mathbf{b} \\ & \mathbf{T} \geq 0 \end{aligned} The problem is solved with either a proximal point method :ref:`[92] ` or a Sinkhorn algorithm :ref:`[2] `. Unlike the Sinkhorn algorithm, which assumes a regularization term, the proximal point method can solve both regularized and unregularized optimal transport problems. When `method` is set to 'auto', the function automatically selects the appropriate method based on the value of `reg`. if `reg` is None or 0, the proximal point method is used. If `reg` is greater than 0, the Sinkhorn algorithm is used. Parameters ---------- M : array-like, shape (B, ns, nt) Cost matrix reg : float Regularization parameter. Default is None. a : array-like, shape (B, ns) Source distribution (optional). If None, uniform distribution is used. b : array-like, shape (B, nt) Target distribution (optional). If None, uniform distribution is used. max_iter : int Maximum number of iterations tol : float Tolerance for convergence method: str Method to use, either 'auto', 'proximal', 'log_sinkhorn' or 'sinkhorn'. Default is 'auto'. inner_iter : int Number of inner Bregman iterations for the proximal method. Default is 1. inner_reg : float Regularization parameter for the inner Bregman iterations in the proximal method. Default is 1e-3. reg_type : str, optional Type of regularization :math:`R` either "KL", or "entropy". Default is "entropy". grad : str, optional Type of gradient computation, either 'detach', 'autodiff', 'last_step' or 'envelope'. 'detach' does not compute the gradients. 'autodiff' provides gradients of all outputs (`plan, value, value_linear`) but with important memory cost. 'last_step' provides gradients of all outputs (`plan, value, value_linear`) only for the last method iteration, useful for memory saving. 'envelope' provides gradients only for `value`. Default is 'envelope'. Returns ------- res : OTResult() Result of the optimization problem. The information can be obtained as follows: - res.plan : OT plan :math:`\mathbf{T}` - res.potentials : OT dual potentials - res.value : Optimal value of the optimization problem - res.value_linear : Linear OT loss with the optimal OT plan See :any:`OTResult` for more information. Examples -------- >>> import numpy as np >>> from ot.batch import solve_batch, dist_batch >>> X = np.random.randn(5, 10, 3) # 5 batches of 10 samples in 3D >>> Y = np.random.randn(5, 15, 3) # 5 batches of 15 samples in 3D >>> M = dist_batch(X, Y, metric="euclidean") # Compute cost matrices >>> p_result = solve_batch(M) # Uses proximal method >>> reg = 0.1 >>> s_result = solve_batch(M, reg, method="log_sinkhorn") # Uses Sinkhorn method >>> s_result.plan.shape # Optimal transport plans for each batch (5, 10, 15) >>> s_result.value.shape # Optimal transport values for each batch (5,) See Also -------- ot.batch.dist_batch : batched cost matrix computation for computing M. ot.solve : non-batched version of the solve_batch function. .. _references-batch-solver: Reference ---------- .. [92] Xie, Y., Wang, X., Wang, R., & Zha, H. (2020, August). A fast proximal point method for computing exact wasserstein distance. In Uncertainty in artificial intelligence (pp. 433-453). PMLR. .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 """ if method not in solve_batch_method_lst: raise ValueError( f"Unknown method: {method}. Must be one of {solve_batch_method_lst}." ) if reg_type not in solve_batch_reg_type_lst: raise ValueError( f"Unknown reg_type: {reg_type}. Must be one of {solve_batch_reg_type_lst}." ) if grad not in solve_batch_grad_lst: raise ValueError( f"Unknown grad: {grad}. Must be one of {solve_batch_grad_lst}." ) if method in ["sinkhorn", "log_sinkhorn"] and (reg is None or reg <= 0): raise ValueError( "Sinkhorn methods require a strictly positive reg parameter. Please provide a valid reg value." ) if method == "auto": if reg is None or reg == 0: method = "proximal" else: method = "log_sinkhorn" nx = get_backend(a, b, M) B, n, m = M.shape if a is None: a = nx.ones((B, n), type_as=M) / n if b is None: b = nx.ones((B, m), type_as=M) / m if method == "log_sinkhorn": K = -M / reg out = bregman_log_projection_batch( K, a, b, nx=nx, max_iter=max_iter, tol=tol, grad=grad ) if method == "sinkhorn": K = nx.exp(-M / reg) out = bregman_projection_batch( K, a, b, nx=nx, max_iter=max_iter, tol=tol, grad=grad ) if method == "proximal": out = proximal_bregman_log_plan_batch( M, a, b, nx=nx, reg=reg, inner_reg=inner_reg, max_iter=max_iter, tol=tol, inner_iter=inner_iter, grad=grad, ) T = out["T"] if grad is None or grad == "detach": T = nx.detach(T) M = nx.detach(M) elif grad == "envelope": T = nx.detach(T) value_linear = loss_linear_batch(M, T) if reg_type == "entropy" and reg is not None: entr = -entropy_batch(T, nx=nx) value = value_linear + reg * entr elif reg_type == "kl" and reg is not None: ref = nx.einsum("bi,bj->bij", a, b) kl = nx.sum(T * nx.log(T / ref + 1e-16), axis=(1, 2)) value = value_linear + reg * kl else: value = value_linear log = {"n_iter": out["n_iters"]} res = OTResult( value=value, value_linear=value_linear, potentials=out["potentials"], plan=T, backend=nx, log=log, ) return res def solve_sample_batch( X_a, X_b, reg=None, a=None, b=None, metric="sqeuclidean", p=2, max_iter=1000, tol=1e-5, method="auto", inner_iter=1, inner_reg=1e-3, reg_type="entropy", grad="envelope", ): r""" Return solutions of a batch of discrete optimal transport problems in a :any:`OTResult` object computed from batches of source and target samples. The problem is solved with either a proximal point method :ref:`[91] ` or a Sinkhorn algorithm :ref:`[2] `. Unlike the Sinkhorn algorithm, which assumes a regularization term, the proximal point method can solve both regularized and unregularized optimal transport problems. When `method` is set to 'auto', the function automatically selects the appropriate method based on the value of `reg`. if `reg` is None or 0, the proximal point method is used. If `reg` is greater than 0, the Sinkhorn algorithm is used. Parameters ---------- X_a : array-like, shape (B, ns, d) Samples from source distribution X_b : array-like, shape (B, nt, d) Samples from target distribution metric : str, optional 'sqeuclidean', 'euclidean', 'minkowski' or 'kl' p : float, optional p-norm for the Minkowski metrics. Default value is 2. reg : float Regularization parameter. Default is None. a : array-like, shape (B, ns) Source distribution (optional). If None, uniform distribution is used. b : array-like, shape (B, nt) Target distribution (optional). If None, uniform distribution is used. max_iter : int Maximum number of iterations tol : float Tolerance for convergence method: str Method to use, either 'auto', 'proximal', 'log_sinkhorn' or 'sinkhorn'. Default is 'auto'. inner_iter : int Number of inner Bregman iterations for the proximal method. Default is 1. inner_reg : float Regularization parameter for the inner Bregman iterations in the proximal method. Default is 1e-3. reg_type : str, optional Type of regularization :math:`R` either "KL", or "entropy". Default is "entropy". grad : str, optional Type of gradient computation, either 'detach', 'autodiff', 'last_step' or 'envelope'. 'detach' does not compute the gradients. 'autodiff' provides gradients of all outputs (`plan, value, value_linear`) but with important memory cost. 'last_step' provides gradients of all outputs (`plan, value, value_linear`) only for the last method iteration, useful for memory saving. 'envelope' provides gradients only for `value`. Default is 'envelope'. Returns ------- res : OTResult() Result of the optimization problem. The information can be obtained as follows: - res.plan : OT plan :math:`\mathbf{T}` - res.potentials : OT dual potentials - res.value : Optimal value of the optimization problem - res.value_linear : Linear OT loss with the optimal OT plan See :any:`OTResult` for more information. See Also -------- ot.batch.solve_batch : function for computing the optimal T from arbitrary cost matrix M. """ M = dist_batch(X_a, X_b, metric=metric, p=p) return solve_batch( M, reg, a=a, b=b, max_iter=max_iter, tol=tol, method=method, inner_iter=inner_iter, inner_reg=inner_reg, reg_type=reg_type, grad=grad, ) python-pot-0.9.7+dfsg/ot/batch/_quadratic.py000066400000000000000000000715751525521106200207760ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Batch operations for quadratic optimal transport. """ # Author: Remi Flamary # Paul Krzakala # # License: MIT License from ..utils import OTResult from ot.backend import get_backend from ot.batch._linear import loss_linear_batch from ot.batch._utils import bmv, bop, bregman_log_projection_batch from ot.utils import deprecated, list_to_array def tensor_batch( a, b, Ca, Cb, symmetric=True, nx=None, loss="sqeuclidean", logits=None ): r""" Compute the Gromov-Wasserstein cost tensor for a batch of problems. The Gromov-Wasserstein distance can be expressed as: .. math:: \text{GW}(\mathbf{T}, \mathbf{C}_1, \mathbf{C}_2) = \sum_{ijkl} T_{ik} T_{jl} \ell(C_{1,ij}, C_{2,kl}) = \langle \mathcal{L} \times \mathbf{T}, \mathbf{T} \rangle where :math:`\mathcal{L}` is a 4D tensor with elements :math:`\mathcal{L}[i,j,k,l] = \ell(C_{1,ij}, C_{2,kl})`. For loss functions of the form :math:`\ell(a,b) = f_1(a) + f_2(b) - \langle h_1(a), h_2(b) \rangle`, the tensor product :math:`\mathcal{L} \times \mathbf{T}` can be computed efficiently without explicitly computing :math:`\mathcal{L}` [12]. This function precomputes all matrices that implicitly define `\mathcal{L}` for various loss functions. Parameters ---------- a : array-like, shape (B, n) Source distributions for each problem in the batch. b : array-like, shape (B, m) Target distributions for each problem in the batch. Ca : array-like, shape (B, n, n) or (B, n, n, d) Source cost matrices for each problem. Can be a 3D array for scalar costs or a 4D array for vector-valued costs (edge features). Cb : array-like, shape (B, m, m) or (B, n, n, d) Target cost matrices for each problem. Can be a 3D array for scalar costs or a 4D array for vector-valued costs (edge features). symmetric : bool, optional Whether the cost matrices are symmetric. Default is True. nx : backend object, optional Numerical backend to use for computations. If None, the default backend is used. loss : str, optional Loss function to use. Supported values: 'sqeuclidean', 'kl'. Default is 'sqeuclidean'. logits : bool, optional For KL divergence, whether inputs are logits (unnormalized log probabilities). If True, inputs are treated as logits. Default is None. Returns ------- dict Dictionary containing: - constC : array-like, shape (B, n, m) Constant term in the tensor product. - hCa : array-like, shape (B, n, n, d) or (B, n, n) - hCb : array-like, shape (B, m, m, d) or (B, m, m) - fCa : array-like, shape (B, n, n) - fCb : array-like, shape (B, m, m) Supported loss functions: ------------------------------ **Squared Euclidean loss**: .. math:: \ell(a, b) = \|a - b\|_2^2 = \sum_i (a_i - b_i)^2 **KL divergence**: .. math:: \ell(a, b) = \sum_i a_i \log\left(\frac{a_i}{b_i}\right) If ``logits=True``, the entries of Ca are treated as logits (unnormalized log probabilities) and the loss becomes: .. math:: \ell(x, y) = \sum_i y_i (\log(y_i) - x_i) Examples -------- >>> import numpy as np >>> from ot.batch import tensor_batch >>> # Create batch of cost matrices >>> Ca = np.random.rand(3, 5, 5) # 3 problems, 5x5 source matrices >>> Cb = np.random.rand(3, 4, 4) # 3 problems, 4x4 target matrices >>> a = np.ones((3, 5)) / 5 # Uniform source distributions >>> b = np.ones((3, 4)) / 4 # Uniform target distributions >>> L = tensor_batch(a, b, Ca, Cb, loss='sqeuclidean') References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. """ if nx is None: nx = get_backend(Ca) loss = loss.lower() if loss == "sqeuclidean" or loss == "l2": def f1(Ca): if Ca.ndim == 4: return nx.sum(Ca**2, axis=-1) else: return Ca**2 def f2(Cb): if Cb.ndim == 4: return nx.sum(Cb**2, axis=-1) else: return Cb**2 def h1(Ca): if Ca.ndim == 3: Ca = nx.unsqueeze(Ca, -1) return 2 * Ca def h2(Cb): if Cb.ndim == 3: Cb = nx.unsqueeze(Cb, -1) return Cb elif loss == "kl": assert logits in [ True, False, ], "logits must be either True or False for KL loss" def f1(Ca): return nx.zeros((Ca.shape[0], Ca.shape[1], Ca.shape[2]), type_as=Ca) def f2(Cb): assert Cb.ndim == 4, "Cb must be a bxnxnxd tensor" fCb = Cb * nx.log(Cb + 1e-15) # Avoid log(0) return nx.sum(fCb, axis=-1) def h1(Ca): return Ca if logits else nx.log(Ca + 1e-15) def h2(Cb): return Cb return compute_tensor_batch(f1, f2, h1, h2, a, b, Ca, Cb, symmetric=symmetric) def div_between_product_batch(mu, nu, alpha, beta, divergence, nx=None): r"""Fast computation of the Bregman divergence between batches of product measures. Only support for Kullback-Leibler and half-squared L2 divergences. For half-squared L2 divergence: .. math:: \frac{1}{2} || \mu \otimes \nu, \alpha \otimes \beta ||^2 = \frac{1}{2} \Big[ ||\alpha||^2 ||\beta||^2 + ||\mu||^2 ||\nu||^2 - 2 \langle \alpha, \mu \rangle \langle \beta, \nu \rangle \Big] For Kullback-Leibler divergence: .. math:: KL(\mu \otimes \nu, \alpha \otimes \beta) = m(\mu) * KL(\nu, \beta) + m(\nu) * KL(\mu, \alpha) + (m(\mu) - m(\alpha)) * (m(\nu) - m(\beta)) where: - :math:`\mu` and :math:`\alpha` are two measures having the same shape. - :math:`\nu` and :math:`\beta` are two measures having the same shape. - :math:`m` denotes the mass of the measure Parameters ---------- mu : array-like, shape (B, ...) First factor of each product measure in the batch. nu : array-like, shape (B, ...) Second factor of each product measure in the batch. alpha : array-like, shape (B, ...) Reference factor with the same shape as `mu`. beta : array-like, shape (B, ...) Reference factor with the same shape as `nu`. divergence : string, default = "kl" Bregman divergence, either "kl" (Kullback-Leibler divergence) or "l2" (half-squared L2 divergence) nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ---------- Bregman divergence between two product measures for each problem in the batch. """ if nx is None: nx = get_backend(mu, nu, alpha, beta) axis_mu = tuple(range(1, mu.ndim)) if mu.ndim > 1 else 0 axis_nu = tuple(range(1, nu.ndim)) if nu.ndim > 1 else 0 axis_alpha = tuple(range(1, alpha.ndim)) if alpha.ndim > 1 else 0 axis_beta = tuple(range(1, beta.ndim)) if beta.ndim > 1 else 0 if divergence == "kl": m_mu = nx.sum(mu, axis=axis_mu) m_nu = nx.sum(nu, axis=axis_nu) m_alpha = nx.sum(alpha, axis=axis_alpha) m_beta = nx.sum(beta, axis=axis_beta) const = (m_mu - m_alpha) * (m_nu - m_beta) res = ( m_nu * nx.kl_div(mu, alpha, mass=True, axis=axis_mu) + m_mu * nx.kl_div(nu, beta, mass=True, axis=axis_nu) + const ) elif divergence == "l2": res = ( nx.sum(alpha**2, axis=axis_alpha) * nx.sum(beta**2, axis=axis_beta) - 2 * nx.sum(alpha * mu, axis=axis_mu) * nx.sum(beta * nu, axis=axis_nu) + nx.sum(mu**2, axis=axis_mu) * nx.sum(nu**2, axis=axis_nu) ) / 2 return res def loss_quadratic_tensor_batch(L, T, recompute_const=False, symmetric=True, nx=None): r""" Computes the gromov-wasserstein cost given a cost tensor and transport plan. Batched version. Parameters ---------- L : dict Cost tensor as returned by `tensor_batch`. T : array-like, shape (B, n, m) Transport plan. recompute_const : bool, optional Whether to recompute the constant term. Default is False. This should be set to True if T does not satisfy the marginal constraints. symmetric : bool, optional Whether to use symmetric version. Default is True. nx : module, optional Backend to use. Default is None. Examples -------- >>> import numpy as np >>> from ot.batch import tensor_batch, loss_quadratic_batch >>> # Create batch of cost matrices >>> Ca = np.random.rand(3, 5, 5) # 3 problems, 5x5 source matrices >>> Cb = np.random.rand(3, 4, 4) # 3 problems, 4x4 target matrices >>> a = np.ones((3, 5)) / 5 # Uniform source distributions >>> b = np.ones((3, 4)) / 4 # Uniform target distributions >>> L = tensor_batch(a, b, Ca, Cb, loss='sqeuclidean') >>> # Use the uniform transport plan for testing >>> T = np.ones((3, 5, 4)) / (5 * 4) >>> loss = loss_quadratic_tensor_batch(L, T, recompute_const=True) >>> loss.shape (3,) See Also -------- ot.batch.tensor_batch : From computing the cost tensor L. ot.batch.solve_gromov_batch : For finding the optimal transport plan T. """ if nx is None: nx = get_backend(T) LT = tensor_product_batch( L, T, nx=nx, recompute_const=recompute_const, symmetric=symmetric ) return nx.sum(LT * T, axis=(1, 2)) @deprecated("Use ot.batch.loss_quadratic_batch instead.") def loss_quadratic_samples_batch( a, b, Ca, Cb, T, loss="sqeuclidean", symmetric=None, nx=None, logits=None, recompute_const=False, log=False, ): return loss_quadratic_batch( a, b, Ca, Cb, T, loss=loss, symmetric=symmetric, nx=nx, logits=logits, recompute_const=recompute_const, log=log, ) def loss_quadratic_batch( a, b, Ca, Cb, T, M=None, alpha=None, unbalanced=None, unbalanced_type="kl", loss="sqeuclidean", symmetric=True, nx=None, logits=None, recompute_const=False, log=False, ): r""" Computes the gromov-wasserstein for samples Ca, Cb and transport plan. Batched version. Parameters ---------- a : array-like, shape (B, n) Source distributions. b : array-like, shape (B, m) Target distributions. Ca : array-like, shape (B, n, n) or (B, n, n, d) Source cost matrices. Cb : array-like, shape (B, m, m) or (B, n, n, d) Target cost matrices. T : array-like, shape (B, n, m) Transport plan. M : array-like, shape (B, n, m) Cost matrix between features across domains (default is None). alpha : float, array-like or list (B,) optional Weight the quadratic term (alpha*Gromov) and the linear term ((1-alpha)*Wass) in the Fused Gromov-Wasserstein problem. Not used for Gromov problem (when M is not provided). By default ``alpha=None`` corresponds to ``alpha=1`` for Gromov problem (``M==None``) and ``alpha=0.5`` for Fused Gromov-Wasserstein problem (``M!=None``). If alpha is a scalar, it is used for all problems in the batch. unbalanced : float array-like or list(B,) optional Unbalanced penalization weight :math:`\lambda_u`. If unbalanced is a scalar, it is used for all problems in the batch. unbalanced_type : string, optional Type of unbalanced penalization function, either "kl" (Kullback-Leibler divergence) or "l2" (half-squared L2 divergence) loss : str, optional Loss function to use. Supported values: 'sqeuclidean', 'kl'. Default is 'sqeuclidean'. symmetric : bool, optional Whether to use symmetric version. Default is True. nx : module, optional Backend to use. Default is None. logits : bool, optional For KL divergence, whether inputs are logits (unnormalized log probabilities). If True, inputs are treated as logits. Default is None. recompute_const : bool, optional Whether to recompute the constant term. Default is False. This should be set to True if T does not satisfy the marginal constraints. Will be set to True if unbalanced is not None. log : bool, optional If True, also returns a dictionary containing the different terms of the loss. Examples -------- >>> import numpy as np >>> from ot.batch import loss_quadratic_samples_batch >>> # Create batch of cost matrices >>> Ca = np.random.rand(3, 5, 5) # 3 problems, 5x5 source matrices >>> Cb = np.random.rand(3, 4, 4) # 3 problems, 4x4 target matrices >>> a = np.ones((3, 5)) / 5 # Uniform source distributions >>> b = np.ones((3, 4)) / 4 # Uniform target distributions >>> # Use the uniform transport plan for testing >>> T = np.ones((3, 5, 4)) / (5 * 4) >>> loss = loss_quadratic_samples_batch(a, b, Ca, Cb, T, recompute_const=True) >>> loss.shape (3,) See Also -------- ot.batch.tensor_batch : From computing the cost tensor L. ot.batch.solve_gromov_batch : For finding the optimal transport plan T. """ if nx is None: nx = get_backend(T) if log: log_dict = {} log_dict["value_quadratic"] = None log_dict["value_linear"] = None log_dict["value_unbalanced"] = None else: log_dict = None if isinstance(loss, str) and loss in ["sqeuclidean", "kl", "l2"]: L = tensor_batch( a, b, Ca, Cb, symmetric=symmetric, nx=nx, loss=loss, logits=logits ) else: raise ValueError(f"Unknown loss function: {loss}") if unbalanced is not None: recompute_const = True quadratic = loss_quadratic_tensor_batch( L, T, recompute_const=recompute_const, symmetric=symmetric, nx=nx ) if log: log_dict["value_quadratic"] = quadratic if unbalanced is None and M is None: if log: log_dict["value"] = quadratic return quadratic, log_dict return quadratic B = T.shape[0] if unbalanced is not None: if unbalanced_type is None: raise ValueError( "unbalanced_type must be specified if unbalanced is not None" ) unbalanced_type = unbalanced_type.lower() if unbalanced_type not in ["kl", "l2"]: raise ValueError( f"Unknown unbalanced_type: {unbalanced_type}, expected 'kl' or 'l2'" ) if isinstance(unbalanced, list): unbalanced = list_to_array(unbalanced, nx=nx) if hasattr(unbalanced, "ndim") and unbalanced.ndim > 0: if unbalanced.ndim != 1 or unbalanced.shape[0] != B: raise ValueError( f"If reg_marginals is not a scalar, it must have shape ({B},), got {unbalanced.shape}" ) T1 = nx.sum(T, 2) T2 = nx.sum(T, 1) unbalanced_term = div_between_product_batch( T1, T2, a, b, divergence=unbalanced_type, nx=nx, ) if log: log_dict["value_unbalanced"] = unbalanced_term if M is not None: if alpha is None: alpha = 0.5 if isinstance(alpha, list): alpha = list_to_array(alpha, nx=nx) if hasattr(alpha, "ndim") and alpha.ndim > 0: if alpha.ndim != 1 or alpha.shape[0] != B: raise ValueError( f"If alpha is not a scalar, it must have shape ({B},), got {alpha.shape}" ) linear = loss_linear_batch(M, T, nx=nx) if log: log_dict["value_linear"] = linear if M is not None and unbalanced is not None: value = (1 - alpha) * linear + alpha * quadratic + unbalanced * unbalanced_term elif M is not None and unbalanced is None: value = (1 - alpha) * linear + alpha * quadratic else: value = quadratic + unbalanced * unbalanced_term if log: log_dict["value"] = value return value, log_dict return value def solve_gromov_batch( Ca, Cb, reg=1e-2, a=None, b=None, loss="sqeuclidean", symmetric=None, M=None, alpha=None, T_init=None, max_iter=50, tol=1e-5, max_iter_inner=50, tol_inner=1e-5, grad="envelope", logits=None, ): r""" Solves a batch of Gromov-Wasserstein optimal transport problems using proximal gradient [12, 81]. For each problem in the batch, solves: .. math:: \begin{aligned} \min_{\mathbf{T} \geq 0} \quad & \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} \\ \text{s.t.} \quad & \mathbf{T} \mathbf{1} = \mathbf{a} \\ & \mathbf{T}^T \mathbf{1} = \mathbf{b} \\ & \mathbf{T} \geq 0 \end{aligned} If :math:`\mathbf{M}` and :math:`\alpha` are given, solves the more general fused Gromov-Wasserstein problem: .. math:: \begin{aligned} \min_{\mathbf{T} \geq 0} \quad & (1-\alpha) \sum_{i,j} M_{i,j} \mathbf{T}_{i,j} + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} \\ \text{s.t.} \quad & \mathbf{T} \mathbf{1} = \mathbf{a} \\ & \mathbf{T}^T \mathbf{1} = \mathbf{b} \\ & \mathbf{T} \geq 0 \end{aligned} Writing the objective as :math:`(1-\alpha) \langle \mathbf{M}, \mathbf{T} \rangle + \alpha \langle \mathcal{L} \otimes \mathbf{T}, \mathbf{T} \rangle`, the solver uses proximal gradient descent where each iteration is: .. math:: \begin{aligned} \mathbf{T}_{k+1} = \mathop{\arg \min}_{\mathbf{T} \geq 0} \quad & \langle \mathbf{M}_k, \mathbf{T} \rangle + \epsilon \, \text{KL}(\mathbf{T} \| \mathbf{T}_k) \\ \text{where} \quad & \mathbf{M}_k = (1 - \alpha) \mathbf{M} + 2 \alpha \mathcal{L} \otimes \mathbf{T}_k \end{aligned} This can be rewritten as: .. math:: \begin{aligned} \mathbf{T}_{k+1} = \mathop{\arg \min}_{\mathbf{T} \geq 0} \quad & \langle \mathbf{M}_k - \epsilon \log(\mathbf{T}_k), \mathbf{T} \rangle - \epsilon H(\mathbf{T}) \end{aligned} where :math:`H(\mathbf{T})` is the entropy of :math:`\mathbf{T}`. Thus each iteration can be solved using the Bregman projection solver implemented in `bregman_log_projection_batch`. Note that the inner optimization problem does not need to be solved exactly. In practice it sufficient to set `max_iter_inner` to a low value (e.g. 20) and/or `tol_inner` to a high value (e.g. 1e-2). Parameters ---------- Ca : array-like, shape (B, n, n, d) or (B, n, n) Samples affinity matrices from source distribution Cb : array-like, shape (B, n, n, d) or (B, n, n) Samples affinity matrices from target distribution a : array-like, shape (B, n), optional Marginal distribution of the source samples. If None, uniform distribution is used. b : array-like, shape (B, m), optional Marginal distribution of the target samples. If None, uniform distribution is used. loss : str, optional Type of loss function, can be 'sqeuclidean' or 'kl' or a QuadraticMetric instance. symmetric : bool, optional Either Ca and Cb are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), Ca and Cb will be assumed symmetric (resp. asymmetric). M : array-like, shape (dim_a, dim_b), optional Linear cost matrix for Fused Gromov-Wasserstein (default is None). alpha : float, optional Weight the quadratic term (alpha*Gromov) and the linear term ((1-alpha)*Wass) in the Fused Gromov-Wasserstein problem. Not used for Gromov problem (when M is not provided). By default ``alpha=None`` corresponds to ``alpha=1`` for Gromov problem (``M==None``) and ``alpha=0.5`` for Fused Gromov-Wasserstein problem (``M!=None``) epsilon : float, optional Regularization parameter for proximal gradient descent. Default is 1e-2. T_init : array-like, shape (B, n, m), optional Initial transport plan. If None, it is initialized to uniform distribution. max_iter : int, optional Maximum number of iterations for the proximal gradient descent. Default is 50. tol : float, optional Tolerance for convergence of the proximal gradient descent. Default is 1e-5. max_iter_inner : int, optional Maximum number of iterations for the inner Bregman projection. Default is 50. tol_inner : float, optional Tolerance for convergence of the inner Bregman projection. Default is 1e-5. grad : str, optional Type of gradient computation, either or 'autodiff', 'envelope' or 'detach'. 'autodiff' provides gradients wrt all outputs (`plan, value, value_linear`) but with important memory cost. 'envelope' provides gradients only for (`value, value_linear`)`. `detach`` is the fastest option but provides no gradients. Default is 'detach'. assume_inner_convergence : bool, optional If True, assumes that the inner Bregman projection always converged i.e. the transport plan satisfies the marginal constraints. This enables faster computations of the tensor product but might results in inaccurate results (e.g. negative values of the loss). Default is True. Returns ------- res : OTResult() Result of the optimization problem. The information can be obtained as follows: - res.plan : OT plan :math:`\mathbf{T}` - res.potentials : OT dual potentials - res.value : Optimal value of the optimization problem - res.value_linear : Linear OT loss with the optimal OT plan - res.value_quad : Quadratic OT loss with the optimal OT plan See :any:`OTResult` for more information. See Also -------- ot.batch.tensor_batch : From computing the cost tensor L. ot.solve_gromov : Non-batched solver for Gromov-Wasserstein. Note that the non-batched solver uses a different algorithm (conditional gradient) and might not give the same results. References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. .. [81] Xu, H., Luo, D., & Carin, L. (2019). "Scalable Gromov-Wasserstein learning for graph partitioning and matching." Advances in neural information processing systems (NeurIPS). 2019. """ # -------------- Setup -------------- # nx = get_backend(a, b, M, Ca, Cb, T_init) B, n, m = (Ca.shape[0], Ca.shape[1], Cb.shape[1]) if a is None: a = nx.ones((B, n), type_as=Ca) / n if b is None: b = nx.ones((B, m), type_as=Cb) / m if symmetric is None: symmetric = nx.allclose(Ca, transpose(Ca, nx=nx), atol=1e-10) and nx.allclose( Cb, transpose(Cb, nx=nx), atol=1e-10 ) # -------------- Get cost_tensor (quadratic part) -------------- # if isinstance(loss, str): L = tensor_batch( a, b, Ca, Cb, symmetric=symmetric, nx=nx, loss=loss, logits=logits ) else: raise ValueError(f"Unknown loss function: {loss}") # -------------- Get cost_matrix (linear part) -------------- # if M is None and alpha is None: M = nx.zeros((B, n, m), type_as=Ca) alpha = 1.0 # Gromov problem elif M is not None and alpha is None: raise ValueError( "If M is provided, alpha must also be provided for Fused Gromov-Wasserstein problem" ) elif M is None and alpha is not None: raise ValueError( "If alpha is provided, M must also be provided for Fused Gromov-Wasserstein problem" ) elif M is not None and alpha is not None: if M.shape[0] != B or M.shape[1] != n or M.shape[2] != m: raise ValueError( f"Shape of M {M.shape} does not match the batch size {B} and dimensions {n}, {m}" ) if alpha < 0 or alpha > 1: raise ValueError( "Alpha must be in [0, 1] for Fused Gromov-Wasserstein problem" ) # -------------- Solver -------------- # if T_init is None: const = nx.sqrt(nx.sum(a, axis=1) * nx.sum(b, axis=1)) T_init = bop(a, b, nx=nx) / const[:, None, None] # B x n x m grad_inner = "autodiff" if grad == "autodiff" else "detach" T = T_init log_T = nx.log(T + 1e-15) # Avoid log(0) for iter in range(max_iter): T_prev = T Mk = (1 - alpha) * M + 2 * alpha * tensor_product_batch( L, T, nx=nx, recompute_const=False, symmetric=symmetric ) K = -Mk / reg + log_T out = bregman_log_projection_batch( K, a, b, nx=nx, max_iter=max_iter_inner, tol=tol_inner, grad=grad_inner ) T = out["T"] log_T = out["log_T"] max_err = nx.max(nx.mean(nx.abs(T_prev - T), axis=(1, 2))) if max_err < tol: break if grad == "detach": T = nx.detach(T) M = nx.detach(M) L = detach_cost_tensor(L, nx=nx) elif grad == "envelope": T = nx.detach(T) value_linear = loss_linear_batch(M, T, nx=nx) value_quadratic = loss_quadratic_tensor_batch( L, T, nx=nx, recompute_const=True, symmetric=symmetric ) # Always recompute const for accurate value value = (1 - alpha) * value_linear + alpha * value_quadratic log = {"n_iter": iter} res = OTResult( value=value, value_linear=value_linear * (1 - alpha), # Weight the linear value for consistency with ot.solve_gromov value_quad=value_quadratic * alpha, # idem plan=T, backend=nx, potentials=out["potentials"], log=log, ) return res ### --------------------- Utility functions for quadratic OT --------------------- ### def compute_tensor_batch(f1, f2, h1, h2, a, b, Ca, Cb, symmetric=True): """ Gromov-Wasserstein writes as: GW(T,Ca,Cb) = sum_ijkl T_ik T_jl l(Ca_ij, Cb_kl) = < LxT, T > Where L is a cost tensor L[i,j,k,l] = l(Ca_ij, Cb_kl). For loss function of form l(a,b) = f1(a) + f2(b) - < h1(a), h2(b) > The tensor product LxT can be computed fast using tensor_product [12]. References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. """ fCa = f1(Ca) fCb = f2(Cb) if not symmetric: fCa = 0.5 * (fCa + transpose(fCa)) fCb = 0.5 * (fCb + transpose(fCb)) hCa = h1(Ca) hCb = h2(Cb) constC = compute_const_from_marginals(fCa, fCb, a, b) L = {"constC": constC, "hCa": hCa, "hCb": hCb, "fCa": fCa, "fCb": fCb} return L def tensor_product_batch(L, T, nx=None, recompute_const=False, symmetric=True): """ Compute the tensor product LxT for the cost tensor L and transport plan T. The formula is: LxT = const - hCa T hCb^T const = < fCa a 1^T + 1 (fCb b)^T References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. """ if nx is None: nx = get_backend(T) if recompute_const: const = compute_const_from_marginals( L["fCa"], L["fCb"], nx.sum(T, axis=2), nx.sum(T, axis=1), nx=nx ) else: const = L["constC"] hCa = L["hCa"] hCb = L["hCb"] dot = nx.einsum("bijd,bjk->bikd", hCa, T) dot = nx.einsum("bikd,bjkd->bijd", dot, hCb) dot = nx.sum(dot, axis=-1) if not symmetric: dot_t = nx.einsum("bijd,bjk->bikd", transpose(hCa), T) dot_t = nx.einsum("bikd,bjkd->bijd", dot_t, transpose(hCb)) dot_t = nx.sum(dot_t, axis=-1) dot = (dot + dot_t) / 2 # Average the two symmetric terms return const - dot def transpose(C, nx=None): if nx is None: nx = get_backend(C) return nx.transpose(C, (0, 2, 1)) if C.ndim == 3 else nx.transpose(C, (0, 2, 1, 3)) def compute_const_from_marginals(fCa, fCb, a, b, nx=None): """ Compute the constant term f1(Ca) a 1^T + 1 b^T f2(Cb)^T """ if nx is None: nx = get_backend(fCa, fCb, a, b) fCaa = bmv(fCa, a, nx=nx) fCbb = bmv(fCb, b, nx=nx) constC = fCaa[:, :, None] + fCbb[:, None, :] return constC def detach_cost_tensor(L, nx=None): """ Detach the cost tensor L to avoid gradients. """ if nx is None: nx = get_backend(L["constC"], L["hCa"], L["hCb"]) L_detached = {} for key, value in L.items(): L_detached[key] = nx.detach(value) return L_detached python-pot-0.9.7+dfsg/ot/batch/_utils.py000066400000000000000000000364121525521106200201500ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Utility functions for batch operations in optimal transport. """ # Author: Remi Flamary # Paul Krzakala # Thibaut Germain # # License: MIT License from ot.backend import get_backend def entropy_batch(T, nx=None, eps=1e-16): r"""Computes the entropy of a batch of transport plans T. .. math:: H(T)_b = - \sum_{i,j} T_{b,i,j} \log(T_{b,i,j}) Parameters ---------- T : array-like, shape (b,n1,n2) `b` transport of size `n1` x `n2`. eps : float, optional Small constant to avoid numerical issues with log(0). Default is 1e-16. nx : Backend, optional Backend to perform computations on. If omitted, the backend defaults to that of `T`. """ if nx is None: nx = get_backend(T) return -nx.sum(T * nx.log(T + eps), axis=(1, 2)) def norm_batch(u, p=2, nx=None): """Computes the lp norm of a batch of vectors u.""" if nx is None: nx = get_backend(u) return nx.sum(u**p, axis=1) ** (1 / p) def bmv(A, b, nx): """ Batched matrix-vector multiplication for tensor A and vector b. """ return nx.einsum("bij,bj->bi", A, b) def bop(a, b, nx): """ Batched outer product for vectors a and b. """ return nx.einsum("bi,bj->bij", a, b) def bregman_projection_batch( K, a=None, b=None, nx=None, max_iter=10000, tol=1e-5, grad="detach" ): r""" Apply Bregman projection to a batch of affinity matrices :math:`\mathbf{K}`. The function solves the following optimization problem: .. math:: \begin{aligned} \mathbf{T} = \mathop{\arg \min}_\mathbf{T} \quad & \text{KL}(\mathbf{T} \| \mathbf{K}) \\ \text{s.t.} \quad & \mathbf{T} \mathbf{1} = \mathbf{a} \\ & \mathbf{T}^T \mathbf{1} = \mathbf{b} \\ & \mathbf{T} \geq 0 \end{aligned} This is equivalent to: .. math:: \begin{aligned} \mathbf{T} = \mathop{\arg \max}_\mathbf{T} \quad & \langle \mathbf{T}, \log(\mathbf{K}) \rangle_F \\ \text{s.t.} \quad & \mathbf{T} \mathbf{1} = \mathbf{a} \\ & \mathbf{T}^T \mathbf{1} = \mathbf{b} \\ & \mathbf{T} \geq 0 \end{aligned} The optimal solution has the form :math:`\mathbf{T} = \text{diag}(\mathbf{f}) \mathbf{K} \text{diag}(\mathbf{g})`, where the dual variables :math:`\mathbf{u}` and :math:`\mathbf{v}` are found iteratively using: .. math:: \mathbf{f}^{(k+1)} = \frac{\mathbf{a}}{\sum \mathbf{K} \mathbf{g}^{(k)}} \mathbf{g}^{(k+1)} = \frac{\mathbf{b}}{\sum \mathbf{K}^T \mathbf{f}^{(k)}} Parameters ---------- K : array-like, shape (B, n, m) Affinity matrix for each problem in the batch. a : array-like, shape (B, n), optional Source distribution for each problem. If None, uniform distribution is used. b : array-like, shape (B, m), optional Target distribution for each problem. If None, uniform distribution is used. nx : backend object, optional Numerical backend to use for computations. If None, the default backend is used. max_iter : int, optional Maximum number of iterations. tol : float, optional Tolerance for convergence. The solver stops when the maximum change in the dual variables is below this value. grad : str, optional Gradient computation mode: 'detach', 'autodiff', or 'last_step'. Returns ------- dict Dictionary containing: - 'T' : array-like, shape (B, n, m) Optimal transport plan for each problem. - 'potentials' : tuple of array-like, shapes ((B, n), (B, m)) Scaling factors (f, g). - 'n_iters' : int Number of iterations performed. Examples -------- >>> import numpy as np >>> from ot.batch import bregman_projection_batch >>> # Create batch of affinity matrices >>> K = np.random.randn(5, 10, 15) # 5 problems, 10x15 cost matrices >>> result = bregman_projection_batch(K) >>> T = result['T'] # Shape (5, 10, 15) See Also -------- ot.batch.bregman_log_projection_batch : Bregman projection in the log-domain. """ if nx is None: nx = get_backend(a, b, K) B, n, m = K.shape if a is None: a = nx.ones((B, n)) / n if b is None: b = nx.ones((B, m)) / m if grad == "detach": K = nx.detach(K) elif grad == "last_step": K_, K = K.clone(), nx.detach(K) f = nx.ones((B, n)) # a / nx.sum(K, axis=2) g = nx.ones((B, m)) # b / nx.sum(K, axis=1) for n_iters in range(max_iter): f = a / nx.sum(K * g[:, None, :], axis=2) g = b / nx.sum(K * f[:, :, None], axis=1) if n_iters % 10 == 0: T = K * f[:, :, None] * g[:, None, :] marginal = nx.sum(T, axis=2) err = nx.max(norm_batch(marginal - a)) if err < tol: break if grad == "last_step": summand_f = nx.sum(K_ * g[:, None, :], axis=2) f = a / summand_f summand_g = nx.sum(K_ * f[:, :, None], axis=1) g = b / summand_g T = K * f[:, :, None] * g[:, None, :] potentials = (f, g) out = {"T": T, "n_iters": n_iters, "potentials": potentials} return out def bregman_log_projection_batch( K, a=None, b=None, nx=None, max_iter=10000, tol=1e-5, grad="detach" ): r""" Apply Bregman projection to a batch of affinity matrices :math:`\exp(\mathbf{K})`. The function solves the following optimization problem: .. math:: \begin{aligned} \mathbf{T} = \mathop{\arg \min}_\mathbf{T} \quad & \text{KL}(\mathbf{T} \| \exp(\mathbf{K})) \\ \text{s.t.} \quad & \mathbf{T} \mathbf{1} = \mathbf{a} \\ & \mathbf{T}^T \mathbf{1} = \mathbf{b} \\ & \mathbf{T} \geq 0 \end{aligned} This is equivalent to: .. math:: \begin{aligned} \mathbf{T} = \mathop{\arg \max}_\mathbf{T} \quad & \langle \mathbf{T}, \mathbf{K} \rangle_F \\ \text{s.t.} \quad & \mathbf{T} \mathbf{1} = \mathbf{a} \\ & \mathbf{T}^T \mathbf{1} = \mathbf{b} \\ & \mathbf{T} \geq 0 \end{aligned} The optimal solution has the form :math:`\mathbf{T} = \text{diag}(\exp(\mathbf{u})) \exp(\mathbf{K}) \text{diag}(\exp(\mathbf{v}))`, where the dual variables :math:`\mathbf{u}` and :math:`\mathbf{v}` are found iteratively using: .. math:: \mathbf{u}^{(k+1)} = \log(\mathbf{a}) - \text{LSE}(\mathbf{K} + \mathbf{v}^{(k)}) \mathbf{v}^{(k+1)} = \log(\mathbf{b}) - \text{LSE}(\mathbf{K}^T + \mathbf{u}^{(k)}) where LSE denotes the log-sum-exp operation. The iterations are performed using the log-sum-exp trick to avoid numerical issues. Parameters ---------- K : array-like, shape (B, n, m) Affinity matrix for each problem in the batch. a : array-like, shape (B, n), optional Source distribution for each problem. If None, uniform distribution is used. b : array-like, shape (B, m), optional Target distribution for each problem. If None, uniform distribution is used. nx : backend object, optional Numerical backend to use for computations. If None, the default backend is used. max_iter : int, optional Maximum number of iterations. tol : float, optional Tolerance for convergence. The solver stops when the maximum change in the dual variables is below this value. grad : str, optional Gradient computation mode: 'detach', 'autodiff', or 'last_step'. Returns ------- dict Dictionary containing: - 'T' : array-like, shape (B, n, m) Optimal transport plan for each problem. - 'log_T' : array-like, shape (B, n, m) Logarithm of the optimal transport plan for each problem. - 'potentials' : tuple of array-like, shapes ((B, n), (B, m)) Log-scaling factors (u, v). - 'n_iters' : int Number of iterations performed. Examples -------- >>> import numpy as np >>> from ot.batch import bregman_log_projection_batch >>> # Create batch of affinity matrices >>> K = np.random.randn(5, 10, 15) # 5 problems, 10x15 cost matrices >>> result = bregman_log_projection_batch(K) >>> T = result['T'] # Shape (5, 10, 15) See Also -------- ot.batch.bregman_projection_batch : standard Bregman projection. """ if nx is None: nx = get_backend(a, b, K) B, n, m = K.shape if a is None: a = nx.ones((B, n)) / n if b is None: b = nx.ones((B, m)) / m u = nx.zeros((B, n), type_as=K) # u = nx.log(a) - nx.logsumexp(K, axis=2).squeeze() v = nx.zeros((B, m), type_as=K) # v = nx.log(b) - nx.logsumexp(K, axis=1).squeeze() loga = nx.log(a) logb = nx.log(b) if grad == "detach": K = nx.detach(K) elif grad == "last_step": K_, K = K.clone(), nx.detach(K) for n_iters in range(max_iter): u = loga - nx.logsumexp(K + v[:, None, :], axis=2) v = logb - nx.logsumexp(K + u[:, :, None], axis=1) # Check convergence once every 10 iterations if n_iters % 10 == 0: T = nx.exp(K + u[:, :, None] + v[:, None, :]) marginal = nx.sum(T, axis=2) err = nx.max(norm_batch(marginal - a)) if err < tol: break if grad == "last_step": summand_u = K_ + v[:, None, :] u = loga - nx.logsumexp(summand_u, axis=2) summand_v = K_ + u[:, :, None] v = logb - nx.logsumexp(summand_v, axis=1) log_T = K + u[:, :, None] + v[:, None, :] T = nx.exp(log_T) log_potentials = (u, v) return {"T": T, "log_T": log_T, "n_iters": n_iters, "potentials": log_potentials} def proximal_bregman_log_plan_batch( C, a=None, b=None, nx=None, reg=None, inner_reg=1e-3, max_iter=10000, tol=1e-5, inner_iter=1, grad="detach", ): r""" Returns optimal transport plans for a batch of cost matrices :math:`\mathbf{C}` using a Bregman divergence based proximal point method :ref:`[92] `. This function solves the following optimization problem: .. math:: \begin{aligned} \mathbf{T} = \mathop{\arg \min}_\mathbf{T} \quad & \langle \mathbf{T}, \mathbf{C} \rangle_F + \textit{reg} \cdot \sum_{i,j} \mathbf{T}_{i,j} \log \mathbf{T}_{i,j} \\ \text{s.t.} \quad & \mathbf{T} \mathbf{1} = \mathbf{a} \\ & \mathbf{T}^T \mathbf{1} = \mathbf{b} \\ & \mathbf{T} \geq 0 \end{aligned} The optimal transport plans are computed iteratively with a proximal point method based on a Bregman divergence where each iteration involves solving a Bregman projection problem: .. math:: \mathbf{T}^{(k+1)} = \mathop{\arg \min}_\mathbf{T} \quad \langle \mathbf{C} - \textit{inner\_reg} \cdot \log \mathbf{T}^{(k)}, \mathbf{T} \rangle + (\textit{reg} + \textit{inner\_reg}) \cdot \sum_{i,j} \mathbf{T}_{i,j} \log \mathbf{T}_{i,j} Denoting :math:`\mathbf{K}^{(k)} = - (\mathbf{C} + \textit{inner\_reg} \cdot \log \mathbf{T}^{(k)})/(\textit{reg} + \textit{inner\_reg})`, the affinity matrix at iteration :math:`k`, the Bregman projection problem is solved in the log-domain with a finite number of inner iterations :math:`\text{inner\_iter}`, i.e., the dual variables :math:`\mathbf{u}` and :math:`\mathbf{v}` are updated as follows: .. math:: \mathbf{u}^{(i+1)} = \log(\mathbf{a}) - \text{LSE}(\mathbf{K}^{(k)} + \mathbf{v}^{(i)}) \mathbf{v}^{(i+1)} = \log(\mathbf{b}) - \text{LSE}((\mathbf{K}^{(k)})^T + \mathbf{u}^{(i)}) where LSE denotes the log-sum-exp operation. Parameters ---------- C : array-like, shape (B, n, m) Cost matrix for each problem in the batch. a : array-like, shape (B, n), optional Source distribution for each problem. If None, uniform distribution is used. b : array-like, shape (B, m), optional Target distribution for each problem. If None, uniform distribution is used. nx : backend object, optional Numerical backend to use for computations. If None, the default backend is used. max_iter : int, optional Maximum number of iterations. inner_iter : int, optional Number of inner iterations for updating the dual variables u and v. Default is 1. inner_reg : float, optional Regularization parameter for the Bregman divergence. Default is 1e-3. tol : float, optional Tolerance for convergence. The solver stops when the maximum change in the dual variables is below this value. grad : str, optional Gradient computation mode: 'detach', 'autodiff', or 'last_step'. Returns ------- dict Dictionary containing: - 'T' : array-like, shape (B, n, m) Optimal transport plan for each problem. - 'log_T' : array-like, shape (B, n, m) Logarithm of the optimal transport plan for each problem. - 'potentials' : tuple of array-like, shapes ((B, n), (B, m)) Log-scaling factors (u, v). - 'n_iters' : int Number of iterations performed. Example -------- >>> import numpy as np >>> from ot.batch import proximal_bregman_log_plan_batch >>> # Create batch of affinity matrices >>> C = np.random.rand(5, 10, 15) # 5 problems, 10x15 cost matrices >>> result = proximal_bregman_log_plan_batch(C) >>> T = result['T'] # Shape (5, 10, 15) .. _references-OT-bregman-proximal-point: Reference ---------- .. [92] Xie, Y., Wang, X., Wang, R., & Zha, H. (2020, August). A fast proximal point method for computing exact wasserstein distance. In Uncertainty in artificial intelligence (pp. 433-453). PMLR. """ if nx is None: nx = get_backend(a, b, C) B, n, m = C.shape if a is None: a = nx.ones((B, n)) / n if b is None: b = nx.ones((B, m)) / m if reg is None: reg = 0.0 u = nx.zeros((B, n), type_as=C) v = nx.zeros((B, m), type_as=C) loga = nx.log(a) logb = nx.log(b) if grad == "detach": C = nx.detach(C) elif grad == "last_step": C_, C = C.clone(), nx.detach(C) log_T = nx.zeros(C.shape, type_as=C) for n_iters in range(max_iter): K_proj = -(C + inner_reg * log_T) / (reg + inner_reg) for _ in range(inner_iter): u = loga - nx.logsumexp(K_proj + v[:, None, :], axis=2) v = logb - nx.logsumexp(K_proj + u[:, :, None], axis=1) log_T = K_proj + u[:, :, None] + v[:, None, :] # Check convergence once every 10 iterations if n_iters % 10 == 0: T = nx.exp(log_T) marginal = nx.sum(T, axis=2) err = nx.max(norm_batch(marginal - a)) if err < tol: break if grad == "last_step": K_proj = -(C_ + inner_reg * log_T) / (reg + inner_reg) for _ in range(inner_iter): u = loga - nx.logsumexp(K_proj + v[:, None, :], axis=2) v = logb - nx.logsumexp(K_proj + u[:, :, None], axis=1) log_T = K_proj + u[:, :, None] + v[:, None, :] T = nx.exp(log_T) log_potentials = (u, v) return {"T": T, "log_T": log_T, "n_iters": n_iters, "potentials": log_potentials} python-pot-0.9.7+dfsg/ot/bregman/000077500000000000000000000000001525521106200166235ustar00rootroot00000000000000python-pot-0.9.7+dfsg/ot/bregman/__init__.py000066400000000000000000000034321525521106200207360ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Solvers related to Bregman projections for entropic regularized OT """ # Author: Remi Flamary # Cédric Vincent-Cuaz # # License: MIT License from ._utils import geometricBar, geometricMean, projR, projC from ._sinkhorn import ( sinkhorn, sinkhorn2, sinkhorn_knopp, sinkhorn_log, greenkhorn, sinkhorn_stabilized, sinkhorn_epsilon_scaling, ) from ._barycenter import ( barycenter, barycenter_sinkhorn, free_support_sinkhorn_barycenter, barycenter_stabilized, barycenter_debiased, jcpot_barycenter, ) from ._convolutional import ( convolutional_barycenter2d, convolutional_barycenter2d_debiased, ) from ._empirical import ( empirical_sinkhorn, empirical_sinkhorn2, empirical_sinkhorn_divergence, empirical_sinkhorn_nystroem, empirical_sinkhorn_nystroem2, ) from ._screenkhorn import screenkhorn from ._dictionary import unmix from ._geomloss import empirical_sinkhorn2_geomloss, geomloss, old_geomloss __all__ = [ "geometricBar", "geometricMean", "projR", "projC", "sinkhorn", "sinkhorn2", "sinkhorn_knopp", "sinkhorn_log", "greenkhorn", "sinkhorn_stabilized", "sinkhorn_epsilon_scaling", "barycenter", "barycenter_sinkhorn", "free_support_sinkhorn_barycenter", "barycenter_stabilized", "barycenter_debiased", "jcpot_barycenter", "convolutional_barycenter2d", "convolutional_barycenter2d_debiased", "empirical_sinkhorn", "empirical_sinkhorn2", "empirical_sinkhorn2_geomloss", "empirical_sinkhorn_divergence", "empirical_sinkhorn_nystroem", "empirical_sinkhorn_nystroem2", "geomloss", "old_geomloss", "screenkhorn", "unmix", ] python-pot-0.9.7+dfsg/ot/bregman/_barycenter.py000066400000000000000000000772631525521106200215110ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Bregman projections solvers for entropic regularized wasserstein barycenters """ # Author: Remi Flamary # Nicolas Courty # Hicham Janati # Ievgen Redko # # License: MIT License import warnings import numpy as np from ..utils import dist, list_to_array, unif from ..backend import get_backend from ._utils import geometricBar, geometricMean, projR, projC from ._sinkhorn import sinkhorn def barycenter( A, M, reg, weights=None, method="sinkhorn", numItermax=10000, stopThr=1e-4, verbose=False, log=False, warn=True, **kwargs, ): r"""Compute the entropic regularized wasserstein barycenter of distributions :math:`\mathbf{A}` The function solves the following optimization problem: .. math:: \mathbf{a} = \mathop{\arg \min}_\mathbf{a} \quad \sum_i W_{reg}(\mathbf{a},\mathbf{a}_i) where : - :math:`W_{reg}(\cdot,\cdot)` is the entropic regularized Wasserstein distance (see :py:func:`ot.bregman.sinkhorn`) if `method` is `sinkhorn` or `sinkhorn_stabilized` or `sinkhorn_log`. - :math:`\mathbf{a}_i` are training distributions in the columns of matrix :math:`\mathbf{A}` - `reg` and :math:`\mathbf{M}` are respectively the regularization term and the cost matrix for OT The algorithm used for solving the problem is the Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[3] ` Parameters ---------- A : array-like, shape (dim, n_hists) `n_hists` training distributions :math:`\mathbf{a}_i` of size `dim` M : array-like, shape (dim, dim) loss matrix for OT reg : float Regularization term > 0 method : str (optional) method used for the solver either 'sinkhorn' or 'sinkhorn_stabilized' or 'sinkhorn_log' weights : array-like, shape (n_hists,) Weights of each histogram :math:`\mathbf{a}_i` on the simplex (barycentric coordinates) numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. Returns ------- a : (dim,) array-like Wasserstein barycenter log : dict log dictionary return only if log==True in parameters .. _references-barycenter: References ---------- .. [3] Benamou, J. D., Carlier, G., Cuturi, M., Nenna, L., & Peyré, G. (2015). Iterative Bregman projections for regularized transportation problems. SIAM Journal on Scientific Computing, 37(2), A1111-A1138. """ if method.lower() == "sinkhorn": return barycenter_sinkhorn( A, M, reg, weights=weights, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, **kwargs, ) elif method.lower() == "sinkhorn_stabilized": return barycenter_stabilized( A, M, reg, weights=weights, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, **kwargs, ) elif method.lower() == "sinkhorn_log": return _barycenter_sinkhorn_log( A, M, reg, weights=weights, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, **kwargs, ) else: raise ValueError("Unknown method '%s'." % method) def barycenter_sinkhorn( A, M, reg, weights=None, numItermax=1000, stopThr=1e-4, verbose=False, log=False, warn=True, ): r"""Compute the entropic regularized wasserstein barycenter of distributions :math:`\mathbf{A}` The function solves the following optimization problem: .. math:: \mathbf{a} = \mathop{\arg \min}_\mathbf{a} \quad \sum_i W_{reg}(\mathbf{a},\mathbf{a}_i) where : - :math:`W_{reg}(\cdot,\cdot)` is the entropic regularized Wasserstein distance (see :py:func:`ot.bregman.sinkhorn`) - :math:`\mathbf{a}_i` are training distributions in the columns of matrix :math:`\mathbf{A}` - `reg` and :math:`\mathbf{M}` are respectively the regularization term and the cost matrix for OT The algorithm used for solving the problem is the Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[3]`. Parameters ---------- A : array-like, shape (dim, n_hists) `n_hists` training distributions :math:`\mathbf{a}_i` of size `dim` M : array-like, shape (dim, dim) loss matrix for OT reg : float Regularization term > 0 weights : array-like, shape (n_hists,) Weights of each histogram :math:`\mathbf{a}_i` on the simplex (barycentric coordinates) numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. Returns ------- a : (dim,) array-like Wasserstein barycenter log : dict log dictionary return only if log==True in parameters .. _references-barycenter-sinkhorn: References ---------- .. [3] Benamou, J. D., Carlier, G., Cuturi, M., Nenna, L., & Peyré, G. (2015). Iterative Bregman projections for regularized transportation problems. SIAM Journal on Scientific Computing, 37(2), A1111-A1138. """ A, M = list_to_array(A, M) nx = get_backend(A, M) if weights is None: weights = nx.ones((A.shape[1],), type_as=A) / A.shape[1] else: assert len(weights) == A.shape[1] if log: log = {"err": []} K = nx.exp(-M / reg) err = 1 UKv = nx.dot(K, (A.T / nx.sum(K, axis=0)).T) u = (geometricMean(UKv) / UKv.T).T for ii in range(numItermax): UKv = u * nx.dot(K.T, A / nx.dot(K, u)) u = (u.T * geometricBar(weights, UKv)).T / UKv if ii % 10 == 1: err = nx.sum(nx.std(UKv, axis=1)) # log and verbose print if log: log["err"].append(err) if err < stopThr: break if verbose: if ii % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(ii, err)) else: if warn: warnings.warn( "Sinkhorn did not converge. You might want to " "increase the number of iterations `numItermax` " "or the regularization parameter `reg`." ) if log: log["niter"] = ii return geometricBar(weights, UKv), log else: return geometricBar(weights, UKv) def free_support_sinkhorn_barycenter( measures_locations, measures_weights, X_init, reg, b=None, weights=None, numItermax=100, numInnerItermax=1000, stopThr=1e-7, verbose=False, log=None, **kwargs, ): r""" Solves the free support (locations of the barycenters are optimized, not the weights) regularized Wasserstein barycenter problem (i.e. the weighted Frechet mean for the 2-Sinkhorn divergence), formally: .. math:: \min_\mathbf{X} \quad \sum_{i=1}^N w_i W_{reg}^2(\mathbf{b}, \mathbf{X}, \mathbf{a}_i, \mathbf{X}_i) where : - :math:`w \in \mathbb{(0, 1)}^{N}`'s are the barycenter weights and sum to one - `measure_weights` denotes the :math:`\mathbf{a}_i \in \mathbb{R}^{k_i}`: empirical measures weights (on simplex) - `measures_locations` denotes the :math:`\mathbf{X}_i \in \mathbb{R}^{k_i, d}`: empirical measures atoms locations - :math:`\mathbf{b} \in \mathbb{R}^{k}` is the desired weights vector of the barycenter This problem is considered in :ref:`[20] ` (Algorithm 2). There are two differences with the following codes: - we do not optimize over the weights - we do not do line search for the locations updates, we use i.e. :math:`\theta = 1` in :ref:`[20] ` (Algorithm 2). This can be seen as a discrete implementation of the fixed-point algorithm of :ref:`[43] ` proposed in the continuous setting. - at each iteration, instead of solving an exact OT problem, we use the Sinkhorn algorithm for calculating the transport plan in :ref:`[20] ` (Algorithm 2). Parameters ---------- measures_locations : list of N (k_i,d) array-like The discrete support of a measure supported on :math:`k_i` locations of a `d`-dimensional space (:math:`k_i` can be different for each element of the list) measures_weights : list of N (k_i,) array-like Numpy arrays where each numpy array has :math:`k_i` non-negatives values summing to one representing the weights of each discrete input measure X_init : (k,d) array-like Initialization of the support locations (on `k` atoms) of the barycenter reg : float Regularization term >0 b : (k,) array-like Initialization of the weights of the barycenter (non-negatives, sum to 1) weights : (N,) array-like Initialization of the coefficients of the barycenter (non-negatives, sum to 1) numItermax : int, optional Max number of iterations numInnerItermax : int, optional Max number of iterations when calculating the transport plans with Sinkhorn stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- X : (k,d) array-like Support locations (on k atoms) of the barycenter See Also -------- ot.bregman.sinkhorn : Entropic regularized OT solver ot.lp.free_support_barycenter : Barycenter solver based on Linear Programming .. _references-free-support-barycenter: References ---------- .. [20] Cuturi, Marco, and Arnaud Doucet. "Fast computation of Wasserstein barycenters." International Conference on Machine Learning. 2014. .. [43] �lvarez-Esteban, Pedro C., et al. "A fixed-point approach to barycenters in Wasserstein space." Journal of Mathematical Analysis and Applications 441.2 (2016): 744-762. """ nx = get_backend(*measures_locations, *measures_weights, X_init) iter_count = 0 N = len(measures_locations) k = X_init.shape[0] d = X_init.shape[1] if b is None: b = nx.ones((k,), type_as=X_init) / k if weights is None: weights = nx.ones((N,), type_as=X_init) / N X = X_init log_dict = {} displacement_square_norms = [] displacement_square_norm = stopThr + 1.0 while displacement_square_norm > stopThr and iter_count < numItermax: T_sum = nx.zeros((k, d), type_as=X_init) for measure_locations_i, measure_weights_i, weight_i in zip( measures_locations, measures_weights, weights ): M_i = dist(X, measure_locations_i) T_i = sinkhorn( b, measure_weights_i, M_i, reg=reg, numItermax=numInnerItermax, **kwargs ) T_sum = T_sum + weight_i * 1.0 / b[:, None] * nx.dot( T_i, measure_locations_i ) displacement_square_norm = nx.sum((T_sum - X) ** 2) if log: displacement_square_norms.append(displacement_square_norm) X = T_sum if verbose: print( "iteration %d, displacement_square_norm=%f\n", iter_count, displacement_square_norm, ) iter_count += 1 if log: log_dict["displacement_square_norms"] = displacement_square_norms return X, log_dict else: return X def _barycenter_sinkhorn_log( A, M, reg, weights=None, numItermax=1000, stopThr=1e-4, verbose=False, log=False, warn=True, ): r"""Compute the entropic wasserstein barycenter in log-domain""" A, M = list_to_array(A, M) dim, n_hists = A.shape nx = get_backend(A, M) if nx.__name__ in ("jax", "tf"): raise NotImplementedError( "Log-domain functions are not yet implemented" " for Jax and tf. Use numpy or torch arrays instead." ) if weights is None: weights = nx.ones(n_hists, type_as=A) / n_hists else: assert len(weights) == A.shape[1] if log: log = {"err": []} M = -M / reg logA = nx.log(A + 1e-16) log_KU, G = nx.zeros((2, *logA.shape), type_as=A) err = 1 for ii in range(numItermax): log_bar = nx.zeros(dim, type_as=A) for k in range(n_hists): f = logA[:, k] - nx.logsumexp(M + G[None, :, k], axis=1) log_KU[:, k] = nx.logsumexp(M + f[:, None], axis=0) log_bar = log_bar + weights[k] * log_KU[:, k] if ii % 10 == 1: err = nx.exp(G + log_KU).std(axis=1).sum() # log and verbose print if log: log["err"].append(err) if err < stopThr: break if verbose: if ii % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(ii, err)) G = log_bar[:, None] - log_KU else: if warn: warnings.warn( "Sinkhorn did not converge. You might want to " "increase the number of iterations `numItermax` " "or the regularization parameter `reg`." ) if log: log["niter"] = ii return nx.exp(log_bar), log else: return nx.exp(log_bar) def barycenter_stabilized( A, M, reg, tau=1e10, weights=None, numItermax=1000, stopThr=1e-4, verbose=False, log=False, warn=True, ): r"""Compute the entropic regularized wasserstein barycenter of distributions :math:`\mathbf{A}` with stabilization. The function solves the following optimization problem: .. math:: \mathbf{a} = \mathop{\arg \min}_\mathbf{a} \quad \sum_i W_{reg}(\mathbf{a},\mathbf{a}_i) where : - :math:`W_{reg}(\cdot,\cdot)` is the entropic regularized Wasserstein distance (see :py:func:`ot.bregman.sinkhorn`) - :math:`\mathbf{a}_i` are training distributions in the columns of matrix :math:`\mathbf{A}` - `reg` and :math:`\mathbf{M}` are respectively the regularization term and the cost matrix for OT The algorithm used for solving the problem is the Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[3] ` Parameters ---------- A : array-like, shape (dim, n_hists) `n_hists` training distributions :math:`\mathbf{a}_i` of size `dim` M : array-like, shape (dim, dim) loss matrix for OT reg : float Regularization term > 0 tau : float threshold for max value in :math:`\mathbf{u}` or :math:`\mathbf{v}` for log scaling weights : array-like, shape (n_hists,) Weights of each histogram :math:`\mathbf{a}_i` on the simplex (barycentric coordinates) numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. Returns ------- a : (dim,) array-like Wasserstein barycenter log : dict log dictionary return only if log==True in parameters .. _references-barycenter-stabilized: References ---------- .. [3] Benamou, J. D., Carlier, G., Cuturi, M., Nenna, L., & Peyré, G. (2015). Iterative Bregman projections for regularized transportation problems. SIAM Journal on Scientific Computing, 37(2), A1111-A1138. """ A, M = list_to_array(A, M) nx = get_backend(A, M) dim, n_hists = A.shape if weights is None: weights = nx.ones((n_hists,), type_as=M) / n_hists else: assert len(weights) == A.shape[1] if log: log = {"err": []} u = nx.ones((dim, n_hists), type_as=M) / dim v = nx.ones((dim, n_hists), type_as=M) / dim K = nx.exp(-M / reg) err = 1.0 alpha = nx.zeros((dim,), type_as=M) beta = nx.zeros((dim,), type_as=M) q = nx.ones((dim,), type_as=M) / dim for ii in range(numItermax): qprev = q Kv = nx.dot(K, v) u = A / Kv Ktu = nx.dot(K.T, u) q = geometricBar(weights, Ktu) Q = q[:, None] v = Q / Ktu absorbing = False if nx.any(u > tau) or nx.any(v > tau): absorbing = True alpha += reg * nx.log(nx.max(u, 1)) beta += reg * nx.log(nx.max(v, 1)) K = nx.exp((alpha[:, None] + beta[None, :] - M) / reg) v = nx.ones(tuple(v.shape), type_as=v) Kv = nx.dot(K, v) if ( nx.any(Ktu == 0.0) or nx.any(nx.isnan(u)) or nx.any(nx.isnan(v)) or nx.any(nx.isinf(u)) or nx.any(nx.isinf(v)) ): # we have reached the machine precision # come back to previous solution and quit loop warnings.warn("Numerical errors at iteration %s" % ii) q = qprev break if (ii % 10 == 0 and not absorbing) or ii == 0: # we can speed up the process by checking for the error only all # the 10th iterations err = nx.max(nx.abs(u * Kv - A)) if log: log["err"].append(err) if err < stopThr: break if verbose: if ii % 50 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(ii, err)) else: if warn: warnings.warn( "Stabilized Sinkhorn did not converge." + "Try a larger entropy `reg`" + "Or a larger absorption threshold `tau`." ) if log: log["niter"] = ii log["logu"] = nx.log(u + 1e-16) log["logv"] = nx.log(v + 1e-16) return q, log else: return q def barycenter_debiased( A, M, reg, weights=None, method="sinkhorn", numItermax=10000, stopThr=1e-4, verbose=False, log=False, warn=True, **kwargs, ): r"""Compute the debiased Sinkhorn barycenter of distributions A The function solves the following optimization problem: .. math:: \mathbf{a} = \mathop{\arg \min}_\mathbf{a} \quad \sum_i S_{reg}(\mathbf{a},\mathbf{a}_i) where : - :math:`S_{reg}(\cdot,\cdot)` is the debiased Sinkhorn divergence (see :py:func:`ot.bregman.empirical_sinkhorn_divergence`) - :math:`\mathbf{a}_i` are training distributions in the columns of matrix :math:`\mathbf{A}` - `reg` and :math:`\mathbf{M}` are respectively the regularization term and the cost matrix for OT The algorithm used for solving the problem is the debiased Sinkhorn algorithm as proposed in :ref:`[37] ` Parameters ---------- A : array-like, shape (dim, n_hists) `n_hists` training distributions :math:`\mathbf{a}_i` of size `dim` M : array-like, shape (dim, dim) loss matrix for OT reg : float Regularization term > 0 method : str (optional) method used for the solver either 'sinkhorn' or 'sinkhorn_log' weights : array-like, shape (n_hists,) Weights of each histogram :math:`\mathbf{a}_i` on the simplex (barycentric coordinates) numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. Returns ------- a : (dim,) array-like Wasserstein barycenter log : dict log dictionary return only if log==True in parameters .. _references-barycenter-debiased: References ---------- .. [37] Janati, H., Cuturi, M., Gramfort, A. Proceedings of the 37th International Conference on Machine Learning, PMLR 119:4692-4701, 2020 """ if method.lower() == "sinkhorn": return _barycenter_debiased( A, M, reg, weights=weights, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, **kwargs, ) elif method.lower() == "sinkhorn_log": return _barycenter_debiased_log( A, M, reg, weights=weights, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, **kwargs, ) else: raise ValueError("Unknown method '%s'." % method) def _barycenter_debiased( A, M, reg, weights=None, numItermax=1000, stopThr=1e-4, verbose=False, log=False, warn=True, ): r"""Compute the debiased sinkhorn barycenter of distributions A.""" A, M = list_to_array(A, M) nx = get_backend(A, M) if weights is None: weights = nx.ones((A.shape[1],), type_as=A) / A.shape[1] else: assert len(weights) == A.shape[1] if log: log = {"err": []} K = nx.exp(-M / reg) err = 1 UKv = nx.dot(K, (A.T / nx.sum(K, axis=0)).T) u = (geometricMean(UKv) / UKv.T).T c = nx.ones(A.shape[0], type_as=A) bar = nx.ones(A.shape[0], type_as=A) for ii in range(numItermax): bold = bar UKv = nx.dot(K, A / nx.dot(K, u)) bar = c * geometricBar(weights, UKv) u = bar[:, None] / UKv c = (c * bar / nx.dot(K, c)) ** 0.5 if ii % 10 == 9: err = abs(bar - bold).max() / max(bar.max(), 1.0) # log and verbose print if log: log["err"].append(err) # debiased Sinkhorn does not converge monotonically # guarantee a few iterations are done before stopping if err < stopThr and ii > 20: break if verbose: if ii % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(ii, err)) else: if warn: warnings.warn( "Sinkhorn did not converge. You might want to " "increase the number of iterations `numItermax` " "or the regularization parameter `reg`." ) if log: log["niter"] = ii return bar, log else: return bar def _barycenter_debiased_log( A, M, reg, weights=None, numItermax=1000, stopThr=1e-4, verbose=False, log=False, warn=True, ): r"""Compute the debiased sinkhorn barycenter in log domain.""" A, M = list_to_array(A, M) dim, n_hists = A.shape nx = get_backend(A, M) if nx.__name__ in ("jax", "tf"): raise NotImplementedError( "Log-domain functions are not yet implemented" " for Jax and TF. Use numpy or torch arrays instead." ) if weights is None: weights = nx.ones(n_hists, type_as=A) / n_hists else: assert len(weights) == A.shape[1] if log: log = {"err": []} M = -M / reg logA = nx.log(A + 1e-16) log_KU, G = nx.zeros((2, *logA.shape), type_as=A) c = nx.zeros(dim, type_as=A) err = 1 for ii in range(numItermax): log_bar = nx.zeros(dim, type_as=A) for k in range(n_hists): f = logA[:, k] - nx.logsumexp(M + G[None, :, k], axis=1) log_KU[:, k] = nx.logsumexp(M + f[:, None], axis=0) log_bar += weights[k] * log_KU[:, k] log_bar += c if ii % 10 == 1: err = nx.exp(G + log_KU).std(axis=1).sum() # log and verbose print if log: log["err"].append(err) if err < stopThr and ii > 20: break if verbose: if ii % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(ii, err)) G = log_bar[:, None] - log_KU for _ in range(10): c = 0.5 * (c + log_bar - nx.logsumexp(M + c[:, None], axis=0)) else: if warn: warnings.warn( "Sinkhorn did not converge. You might want to " "increase the number of iterations `numItermax` " "or the regularization parameter `reg`." ) if log: log["niter"] = ii return nx.exp(log_bar), log else: return nx.exp(log_bar) def jcpot_barycenter( Xs, Ys, Xt, reg, metric="sqeuclidean", numItermax=100, stopThr=1e-6, verbose=False, log=False, warn=True, **kwargs, ): r"""Joint OT and proportion estimation for multi-source target shift as proposed in :ref:`[27] ` The function solves the following optimization problem: .. math:: \mathbf{h} = \mathop{\arg \min}_{\mathbf{h}} \quad \sum_{k=1}^{K} \lambda_k W_{reg}((\mathbf{D}_2^{(k)} \mathbf{h})^T, \mathbf{a}) s.t. \ \forall k, \mathbf{D}_1^{(k)} \gamma_k \mathbf{1}_n= \mathbf{h} where : - :math:`\lambda_k` is the weight of `k`-th source domain - :math:`W_{reg}(\cdot,\cdot)` is the entropic regularized Wasserstein distance (see :py:func:`ot.bregman.sinkhorn`) - :math:`\mathbf{D}_2^{(k)}` is a matrix of weights related to `k`-th source domain defined as in [p. 5, :ref:`27 `], its expected shape is :math:`(n_k, C)` where :math:`n_k` is the number of elements in the `k`-th source domain and `C` is the number of classes - :math:`\mathbf{h}` is a vector of estimated proportions in the target domain of size `C` - :math:`\mathbf{a}` is a uniform vector of weights in the target domain of size `n` - :math:`\mathbf{D}_1^{(k)}` is a matrix of class assignments defined as in [p. 5, :ref:`27 `], its expected shape is :math:`(n_k, C)` The problem consist in solving a Wasserstein barycenter problem to estimate the proportions :math:`\mathbf{h}` in the target domain. The algorithm used for solving the problem is the Iterative Bregman projections algorithm with two sets of marginal constraints related to the unknown vector :math:`\mathbf{h}` and uniform target distribution. Parameters ---------- Xs : list of K array-like(nsk,d) features of all source domains' samples Ys : list of K array-like(nsk,) labels of all source domains' samples Xt : array-like (nt,d) samples in the target domain reg : float Regularization term > 0 metric : string, optional (default="sqeuclidean") The ground metric for the Wasserstein problem numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on relative change in the barycenter (>0) verbose : bool, optional (default=False) Controls the verbosity of the optimization algorithm log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. Returns ------- h : (C,) array-like proportion estimation in the target domain log : dict log dictionary return only if log==True in parameters .. _references-jcpot-barycenter: References ---------- .. [27] Ievgen Redko, Nicolas Courty, Rémi Flamary, Devis Tuia "Optimal transport for multi-source domain adaptation under target shift", International Conference on Artificial Intelligence and Statistics (AISTATS), 2019. """ Xs = list_to_array(*Xs) Ys = list_to_array(*Ys) Xt = list_to_array(Xt) nx = get_backend(*Xs, *Ys, Xt) nbclasses = len(nx.unique(Ys[0])) nbdomains = len(Xs) # log dictionary if log: log = {"niter": 0, "err": [], "M": [], "D1": [], "D2": [], "gamma": []} K = [] M = [] D1 = [] D2 = [] # For each source domain, build cost matrices M, Gibbs kernels K and corresponding matrices D_1 and D_2 for d in range(nbdomains): dom = {} nsk = Xs[d].shape[0] # get number of elements for this domain dom["nbelem"] = nsk classes = nx.unique(Ys[d]) # get number of classes for this domain # format classes to start from 0 for convenience if nx.min(classes) != 0: Ys[d] -= nx.min(classes) classes = nx.unique(Ys[d]) # build the corresponding D_1 and D_2 matrices Dtmp1 = np.zeros((nbclasses, nsk)) Dtmp2 = np.zeros((nbclasses, nsk)) for c in classes: nbelemperclass = float(nx.sum(Ys[d] == c)) if nbelemperclass != 0: Dtmp1[int(c), nx.to_numpy(Ys[d] == c)] = 1.0 Dtmp2[int(c), nx.to_numpy(Ys[d] == c)] = 1.0 / (nbelemperclass) D1.append(nx.from_numpy(Dtmp1, type_as=Xs[0])) D2.append(nx.from_numpy(Dtmp2, type_as=Xs[0])) # build the cost matrix and the Gibbs kernel Mtmp = dist(Xs[d], Xt, metric=metric) M.append(Mtmp) Ktmp = nx.exp(-Mtmp / reg) K.append(Ktmp) # uniform target distribution a = nx.from_numpy(unif(Xt.shape[0]), type_as=Xs[0]) err = 1 old_bary = nx.ones((nbclasses,), type_as=Xs[0]) for ii in range(numItermax): bary = nx.zeros((nbclasses,), type_as=Xs[0]) # update coupling matrices for marginal constraints w.r.t. uniform target distribution for d in range(nbdomains): K[d] = projC(K[d], a) other = nx.sum(K[d], axis=1) bary += nx.log(nx.dot(D1[d], other)) / nbdomains bary = nx.exp(bary) # update coupling matrices for marginal constraints w.r.t. unknown proportions based on [Prop 4., 27] for d in range(nbdomains): new = nx.dot(D2[d].T, bary) K[d] = projR(K[d], new) err = nx.norm(bary - old_bary) old_bary = bary if log: log["err"].append(err) if err < stopThr: break if verbose: if ii % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(ii, err)) else: if warn: warnings.warn( "Algorithm did not converge. You might want to " "increase the number of iterations `numItermax` " "or the regularization parameter `reg`." ) bary = bary / nx.sum(bary) if log: log["niter"] = ii log["M"] = M log["D1"] = D1 log["D2"] = D2 log["gamma"] = K return bary, log else: return bary python-pot-0.9.7+dfsg/ot/bregman/_convolutional.py000066400000000000000000000362271525521106200222420ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Bregman projections solvers for entropic regularized Wasserstein convolutional barycenters """ # Author: Remi Flamary # Nicolas Courty # # License: MIT License import warnings from ..backend import get_backend from ..utils import list_to_array _warning_msg = ( "Convolutional Sinkhorn did not converge. " "Try a larger number of iterations `numItermax` " "or a larger entropy `reg`." ) def _get_convol_img_fn(nx, width, height, reg, type_as, log_domain=False): """Return the convolution operator for 2D images. The function constructed is equivalent to blurring on horizontal then vertical directions.""" t1 = nx.linspace(0, 1, width, type_as=type_as) Y1, X1 = nx.meshgrid(t1, t1) M1 = -((X1 - Y1) ** 2) / reg t2 = nx.linspace(0, 1, height, type_as=type_as) Y2, X2 = nx.meshgrid(t2, t2) M2 = -((X2 - Y2) ** 2) / reg # If normal domain is selected, we can use M1 and M2 to compute the convolution if not log_domain: K1, K2 = nx.exp(M1), nx.exp(M2) def convol_imgs(imgs): kx = nx.einsum("...ij,kjl->kil", K1, imgs) kxy = nx.einsum("...ij,klj->kli", K2, kx) return kxy # Else, we can use M1 and M2 to compute the convolution in log-domain else: def convol_imgs(log_imgs): log_imgs = nx.logsumexp(M1[:, :, None] + log_imgs[None], axis=1) log_imgs = nx.logsumexp(M2[:, :, None] + log_imgs.T[None], axis=1).T return log_imgs return convol_imgs def _print_report(ii, err): """Print the report of the iteration.""" if ii % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(ii, err)) def convolutional_barycenter2d( A, reg, weights=None, method="sinkhorn", numItermax=10000, stopThr=1e-4, verbose=False, log=False, warn=True, **kwargs, ): r"""Compute the entropic regularized wasserstein barycenter of distributions :math:`\mathbf{A}` where :math:`\mathbf{A}` is a collection of 2D images. The function solves the following optimization problem: .. math:: \mathbf{a} = \mathop{\arg \min}_\mathbf{a} \quad \sum_i W_{reg}(\mathbf{a},\mathbf{a}_i) where : - :math:`W_{reg}(\cdot,\cdot)` is the entropic regularized Wasserstein distance (see :py:func:`ot.bregman.sinkhorn`) - :math:`\mathbf{a}_i` are training distributions (2D images) in the mast two dimensions of matrix :math:`\mathbf{A}` - `reg` is the regularization strength scalar value The algorithm used for solving the problem is the Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[21] ` Parameters ---------- A : array-like, shape (n_hists, width, height) `n` distributions (2D images) of size `width` x `height` reg : float Regularization term >0 weights : array-like, shape (n_hists,) Weights of each image on the simplex (barycentric coordinates) method : string, optional method used for the solver either 'sinkhorn' or 'sinkhorn_log' numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (> 0) stabThr : float, optional Stabilization threshold to avoid numerical precision issue verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. Returns ------- a : array-like, shape (width, height) 2D Wasserstein barycenter log : dict log dictionary return only if log==True in parameters .. _references-convolutional-barycenter-2d: References ---------- .. [21] Solomon, J., De Goes, F., Peyré, G., Cuturi, M., Butscher, A., Nguyen, A. & Guibas, L. (2015). Convolutional wasserstein distances: Efficient optimal transportation on geometric domains. ACM Transactions on Graphics (TOG), 34(4), 66 .. [37] Janati, H., Cuturi, M., Gramfort, A. Proceedings of the 37th International Conference on Machine Learning, PMLR 119:4692-4701, 2020 """ if method.lower() == "sinkhorn": return _convolutional_barycenter2d( A, reg, weights=weights, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, **kwargs, ) elif method.lower() == "sinkhorn_log": return _convolutional_barycenter2d_log( A, reg, weights=weights, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, **kwargs, ) else: raise ValueError("Unknown method '%s'." % method) def _convolutional_barycenter2d( A, reg, weights=None, numItermax=10000, stopThr=1e-9, stabThr=1e-30, verbose=False, log=False, warn=True, ): r"""Compute the entropic regularized wasserstein barycenter of distributions A where A is a collection of 2D images. """ A = list_to_array(A) n_hists, width, height = A.shape nx = get_backend(A) if weights is None: weights = nx.ones((n_hists,), type_as=A) / n_hists else: assert len(weights) == n_hists if log: log = {"err": []} bar = nx.ones((width, height), type_as=A) bar /= nx.sum(bar) U = nx.ones(A.shape, type_as=A) V = nx.ones(A.shape, type_as=A) err = 1 # build the convolution operator convol_imgs = _get_convol_img_fn(nx, width, height, reg, type_as=A) KU = convol_imgs(U) for ii in range(numItermax): V = bar[None] / KU KV = convol_imgs(V) U = A / KV KU = convol_imgs(U) bar = nx.exp(nx.sum(weights[:, None, None] * nx.log(KU + stabThr), axis=0)) if ii % 10 == 9: err = nx.sum(nx.std(V * KU, axis=0)) # log and verbose print if log: log["err"].append(err) if verbose: _print_report(ii, err) if err < stopThr: break else: if warn: warnings.warn(_warning_msg) if log: log["niter"] = ii log["U"] = U log["V"] = V return bar, log else: return bar def _convolutional_barycenter2d_log( A, reg, weights=None, numItermax=10000, stopThr=1e-4, stabThr=1e-30, verbose=False, log=False, warn=True, ): r"""Compute the entropic regularized wasserstein barycenter of distributions A where A is a collection of 2D images in log-domain. """ A = list_to_array(A) nx = get_backend(A) # This error is raised because we are using mutable assignment in the line # `log_KU[k] = ...` which is not allowed in Jax and TF. if nx.__name__ in ("jax", "tf"): raise NotImplementedError( "Log-domain functions are not yet implemented" " for Jax and TF. Use numpy or torch arrays instead." ) n_hists, width, height = A.shape if weights is None: weights = nx.ones((n_hists,), type_as=A) / n_hists else: assert len(weights) == n_hists if log: log = {"err": []} err = 1 # build the convolution operator convol_img = _get_convol_img_fn(nx, width, height, reg, type_as=A, log_domain=True) logA = nx.log(A + stabThr) log_KU, G, F = nx.zeros((3, *logA.shape), type_as=A) err = 1 for ii in range(numItermax): log_bar = nx.zeros((width, height), type_as=A) for k in range(n_hists): f = logA[k] - convol_img(G[k]) log_KU[k] = convol_img(f) log_bar = log_bar + weights[k] * log_KU[k] if ii % 10 == 9: err = nx.exp(G + log_KU).std(axis=0).sum() # log and verbose print if log: log["err"].append(err) if verbose: _print_report(ii, err) if err < stopThr: break G = log_bar[None, :, :] - log_KU else: if warn: warnings.warn(_warning_msg) if log: log["niter"] = ii return nx.exp(log_bar), log else: return nx.exp(log_bar) def convolutional_barycenter2d_debiased( A, reg, weights=None, method="sinkhorn", numItermax=10000, stopThr=1e-3, verbose=False, log=False, warn=True, **kwargs, ): r"""Compute the debiased sinkhorn barycenter of distributions :math:`\mathbf{A}` where :math:`\mathbf{A}` is a collection of 2D images. The function solves the following optimization problem: .. math:: \mathbf{a} = \mathop{\arg \min}_\mathbf{a} \quad \sum_i S_{reg}(\mathbf{a},\mathbf{a}_i) where : - :math:`S_{reg}(\cdot,\cdot)` is the debiased entropic regularized Wasserstein distance (see :py:func:`ot.bregman.barycenter_debiased`) - :math:`\mathbf{a}_i` are training distributions (2D images) in the mast two dimensions of matrix :math:`\mathbf{A}` - `reg` is the regularization strength scalar value The algorithm used for solving the problem is the debiased Sinkhorn scaling algorithm as proposed in :ref:`[37] ` Parameters ---------- A : array-like, shape (n_hists, width, height) `n` distributions (2D images) of size `width` x `height` reg : float Regularization term >0 weights : array-like, shape (n_hists,) Weights of each image on the simplex (barycentric coordinates) method : string, optional method used for the solver either 'sinkhorn' or 'sinkhorn_log' numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (> 0) stabThr : float, optional Stabilization threshold to avoid numerical precision issue verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. Returns ------- a : array-like, shape (width, height) 2D Wasserstein barycenter log : dict log dictionary return only if log==True in parameters .. _references-convolutional-barycenter2d-debiased: References ---------- .. [37] Janati, H., Cuturi, M., Gramfort, A. Proceedings of the 37th International Conference on Machine Learning, PMLR 119:4692-4701, 2020 """ if method.lower() == "sinkhorn": return _convolutional_barycenter2d_debiased( A, reg, weights=weights, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, **kwargs, ) elif method.lower() == "sinkhorn_log": return _convolutional_barycenter2d_debiased_log( A, reg, weights=weights, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, **kwargs, ) else: raise ValueError("Unknown method '%s'." % method) def _convolutional_barycenter2d_debiased( A, reg, weights=None, numItermax=10000, stopThr=1e-3, stabThr=1e-15, verbose=False, log=False, warn=True, ): r"""Compute the debiased barycenter of 2D images via sinkhorn convolutions.""" A = list_to_array(A) n_hists, width, height = A.shape nx = get_backend(A) if weights is None: weights = nx.ones((n_hists,), type_as=A) / n_hists else: assert len(weights) == n_hists if log: log = {"err": []} bar = nx.ones((width, height), type_as=A) bar /= width * height U = nx.ones(A.shape, type_as=A) V = nx.ones(A.shape, type_as=A) c = nx.ones((width, height), type_as=A) err = 1 # build the convolution operator convol_imgs = _get_convol_img_fn(nx, width, height, reg, type_as=A) KU = convol_imgs(U) for ii in range(numItermax): V = bar[None] / KU KV = convol_imgs(V) U = A / KV KU = convol_imgs(U) bar = c * nx.exp(nx.sum(weights[:, None, None] * nx.log(KU + stabThr), axis=0)) for _ in range(10): c = (c * bar / nx.squeeze(convol_imgs(c[None]))) ** 0.5 if ii % 10 == 9: err = nx.sum(nx.std(V * KU, axis=0)) # log and verbose print if log: log["err"].append(err) if verbose: _print_report(ii, err) # debiased Sinkhorn does not converge monotonically # guarantee a few iterations are done before stopping if err < stopThr and ii > 20: break else: if warn: warnings.warn(_warning_msg) if log: log["niter"] = ii log["U"] = U log["V"] = V return bar, log else: return bar def _convolutional_barycenter2d_debiased_log( A, reg, weights=None, numItermax=10000, stopThr=1e-3, stabThr=1e-30, verbose=False, log=False, warn=True, ): r"""Compute the debiased barycenter of 2D images in log-domain.""" A = list_to_array(A) n_hists, width, height = A.shape nx = get_backend(A) # This error is raised because we are using mutable assignment in the line # `log_KU[k] = ...` which is not allowed in Jax and TF. if nx.__name__ in ("jax", "tf"): raise NotImplementedError( "Log-domain functions are not yet implemented" " for Jax and TF. Use numpy or torch arrays instead." ) if weights is None: weights = nx.ones((n_hists,), type_as=A) / n_hists else: assert len(weights) == A.shape[0] if log: log = {"err": []} err = 1 # build the convolution operator convol_img = _get_convol_img_fn(nx, width, height, reg, type_as=A, log_domain=True) logA = nx.log(A + stabThr) log_bar, c = nx.zeros((2, width, height), type_as=A) log_KU, G, F = nx.zeros((3, *logA.shape), type_as=A) err = 1 for ii in range(numItermax): log_bar = nx.zeros((width, height), type_as=A) for k in range(n_hists): f = logA[k] - convol_img(G[k]) log_KU[k] = convol_img(f) log_bar = log_bar + weights[k] * log_KU[k] log_bar += c for _ in range(10): c = 0.5 * (c + log_bar - convol_img(c)) if ii % 10 == 9: err = nx.sum(nx.std(nx.exp(G + log_KU), axis=0)) # log and verbose print if log: log["err"].append(err) if verbose: _print_report(ii, err) if err < stopThr and ii > 20: break G = log_bar[None, :, :] - log_KU else: if warn: warnings.warn(_warning_msg) if log: log["niter"] = ii return nx.exp(log_bar), log else: return nx.exp(log_bar) python-pot-0.9.7+dfsg/ot/bregman/_dictionary.py000066400000000000000000000113431525521106200215030ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Dictionary Learning based on Bregman projections for entropic regularized OT """ # Author: Remi Flamary # Nicolas Courty # # License: MIT License import warnings from ..utils import list_to_array from ..backend import get_backend from ._utils import projC, projR def unmix( a, D, M, M0, h0, reg, reg0, alpha, numItermax=1000, stopThr=1e-3, verbose=False, log=False, warn=True, ): r""" Compute the unmixing of an observation with a given dictionary using Wasserstein distance The function solve the following optimization problem: .. math:: \mathbf{h} = \mathop{\arg \min}_\mathbf{h} \quad (1 - \alpha) W_{\mathbf{M}, \mathrm{reg}}(\mathbf{a}, \mathbf{Dh}) + \alpha W_{\mathbf{M_0}, \mathrm{reg}_0}(\mathbf{h}_0, \mathbf{h}) where : - :math:`W_{M,reg}(\cdot,\cdot)` is the entropic regularized Wasserstein distance with :math:`\mathbf{M}` loss matrix (see :py:func:`ot.bregman.sinkhorn`) - :math:`\mathbf{D}` is a dictionary of `n_atoms` atoms of dimension `dim_a`, its expected shape is `(dim_a, n_atoms)` - :math:`\mathbf{h}` is the estimated unmixing of dimension `n_atoms` - :math:`\mathbf{a}` is an observed distribution of dimension `dim_a` - :math:`\mathbf{h}_0` is a prior on :math:`\mathbf{h}` of dimension `dim_prior` - `reg` and :math:`\mathbf{M}` are respectively the regularization term and the cost matrix (`dim_a`, `dim_a`) for OT data fitting - `reg`:math:`_0` and :math:`\mathbf{M_0}` are respectively the regularization term and the cost matrix (`dim_prior`, `n_atoms`) regularization - :math:`\alpha` weight data fitting and regularization The optimization problem is solved following the algorithm described in :ref:`[4] ` Parameters ---------- a : array-like, shape (dim_a) observed distribution (histogram, sums to 1) D : array-like, shape (dim_a, n_atoms) dictionary matrix M : array-like, shape (dim_a, dim_a) loss matrix M0 : array-like, shape (n_atoms, dim_prior) loss matrix h0 : array-like, shape (n_atoms,) prior on the estimated unmixing h reg : float Regularization term >0 (Wasserstein data fitting) reg0 : float Regularization term >0 (Wasserstein reg with h0) alpha : float How much should we trust the prior ([0,1]) numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. Returns ------- h : array-like, shape (n_atoms,) Wasserstein barycenter log : dict log dictionary return only if log==True in parameters .. _references-unmix: References ---------- .. [4] S. Nakhostin, N. Courty, R. Flamary, D. Tuia, T. Corpetti, Supervised planetary unmixing with optimal transport, Workshop on Hyperspectral Image and Signal Processing : Evolution in Remote Sensing (WHISPERS), 2016. """ a, D, M, M0, h0 = list_to_array(a, D, M, M0, h0) nx = get_backend(a, D, M, M0, h0) # M = M/np.median(M) K = nx.exp(-M / reg) # M0 = M0/np.median(M0) K0 = nx.exp(-M0 / reg0) old = h0 err = 1 # log = {'niter':0, 'all_err':[]} if log: log = {"err": []} for ii in range(numItermax): K = projC(K, a) K0 = projC(K0, h0) new = nx.sum(K0, axis=1) # we recombine the current selection from dictionary inv_new = nx.dot(D, new) other = nx.sum(K, axis=1) # geometric interpolation delta = nx.exp(alpha * nx.log(other) + (1 - alpha) * nx.log(inv_new)) K = projR(K, delta) K0 = nx.dot(D.T, delta / inv_new)[:, None] * K0 err = nx.norm(nx.sum(K0, axis=1) - old) old = new if log: log["err"].append(err) if verbose: if ii % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(ii, err)) if err < stopThr: break else: if warn: warnings.warn( "Unmixing algorithm did not converge. You might want to " "increase the number of iterations `numItermax` " "or the regularization parameter `reg`." ) if log: log["niter"] = ii return nx.sum(K0, axis=1), log else: return nx.sum(K0, axis=1) python-pot-0.9.7+dfsg/ot/bregman/_empirical.py000066400000000000000000000741401525521106200213070ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Bregman projections solvers for entropic regularized OT for empirical distributions """ # Author: Remi Flamary # Kilian Fatras # Quang Huy Tran # Titouan Vayer # # License: MIT License import warnings from ..utils import dist, list_to_array, unif, LazyTensor from ..backend import get_backend from ._sinkhorn import sinkhorn, sinkhorn2 from ..lowrank import ( kernel_nystroem, sinkhorn_low_rank_kernel, compute_lr_sqeuclidean_matrix, ) def get_sinkhorn_lazytensor(X_a, X_b, f, g, metric="sqeuclidean", reg=1e-1, nx=None): r"""Get a LazyTensor of Sinkhorn solution from the dual potentials The returned LazyTensor is :math:`\mathbf{T} = exp( \mathbf{f} \mathbf{1}_b^\top + \mathbf{1}_a \mathbf{g}^\top - \mathbf{C}/reg)`, where :math:`\mathbf{C}` is the pairwise metric matrix between samples :math:`\mathbf{X}_a` and :math:`\mathbf{X}_b`. Parameters ---------- X_a : array-like, shape (n_samples_a, dim) samples in the source domain X_b : array-like, shape (n_samples_b, dim) samples in the target domain f : array-like, shape (n_samples_a,) First dual potentials (log space) g : array-like, shape (n_samples_b,) Second dual potentials (log space) metric : str, default='sqeuclidean' Metric used for the cost matrix computation reg : float, default=1e-1 Regularization term >0 nx : Backend(), default=None Numerical backend used Returns ------- T : LazyTensor Sinkhorn solution tensor """ if nx is None: nx = get_backend(X_a, X_b, f, g) shape = (X_a.shape[0], X_b.shape[0]) def func(i, j, X_a, X_b, f, g, metric, reg): if isinstance(i, int): i = slice(i, i + 1) if isinstance(j, int): j = slice(j, j + 1) C = dist(X_a[i], X_b[j], metric=metric) return nx.exp(f[i, None] + g[None, j] - C / reg) T = LazyTensor(shape, func, X_a=X_a, X_b=X_b, f=f, g=g, metric=metric, reg=reg) return T def empirical_sinkhorn( X_s, X_t, reg, a=None, b=None, metric="sqeuclidean", numIterMax=10000, stopThr=1e-9, isLazy=False, batchSize=100, verbose=False, log=False, warn=True, warmstart=None, **kwargs, ): r""" Solve the entropic regularization optimal transport problem and return the OT matrix from empirical data The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot\Omega(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma^T \mathbf{1} &= \mathbf{b} \gamma &\geq 0 where : - :math:`\mathbf{M}` is the (`n_samples_a`, `n_samples_b`) metric cost matrix - :math:`\Omega` is the entropic regularization term :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) Parameters ---------- X_s : array-like, shape (n_samples_a, dim) samples in the source domain X_t : array-like, shape (n_samples_b, dim) samples in the target domain reg : float Regularization term >0 a : array-like, shape (n_samples_a,) samples weights in the source domain b : array-like, shape (n_samples_b,) samples weights in the target domain numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) isLazy: boolean, optional If True, then only calculate the cost matrix by block and return the dual potentials only (to save memory). If False, calculate full cost matrix and return outputs of sinkhorn function. batchSize: int or tuple of 2 int, optional Size of the batches used to compute the sinkhorn update without memory overhead. When a tuple is provided it sets the size of the left/right batches. verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the u,v sinkhorn scaling vectors) Returns ------- gamma : array-like, shape (n_samples_a, n_samples_b) Regularized optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters Examples -------- >>> import numpy as np >>> n_samples_a = 2 >>> n_samples_b = 2 >>> reg = 0.1 >>> X_s = np.reshape(np.arange(n_samples_a, dtype=np.float64), (n_samples_a, 1)) >>> X_t = np.reshape(np.arange(0, n_samples_b, dtype=np.float64), (n_samples_b, 1)) >>> empirical_sinkhorn(X_s, X_t, reg=reg, verbose=False) # doctest: +NORMALIZE_WHITESPACE array([[4.99977301e-01, 2.26989344e-05], [2.26989344e-05, 4.99977301e-01]]) References ---------- .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [9] Schmitzer, B. (2016). Stabilized Sparse Scaling Algorithms for Entropy Regularized Transport Problems. arXiv preprint arXiv:1610.06519. .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. """ X_s, X_t = list_to_array(X_s, X_t) nx = get_backend(X_s, X_t) ns, nt = X_s.shape[0], X_t.shape[0] if a is None: a = nx.from_numpy(unif(ns), type_as=X_s) if b is None: b = nx.from_numpy(unif(nt), type_as=X_s) if isLazy: if log: dict_log = {"err": []} log_a, log_b = nx.log(a), nx.log(b) if warmstart is None: f, g = nx.zeros((ns,), type_as=a), nx.zeros((nt,), type_as=a) else: f, g = warmstart if isinstance(batchSize, int): bs, bt = batchSize, batchSize elif isinstance(batchSize, tuple) and len(batchSize) == 2: bs, bt = batchSize[0], batchSize[1] else: raise ValueError("Batch size must be in integer or a tuple of two integers") range_s, range_t = range(0, ns, bs), range(0, nt, bt) lse_f = nx.zeros((ns,), type_as=a) lse_g = nx.zeros((nt,), type_as=a) X_s_np = nx.to_numpy(X_s) X_t_np = nx.to_numpy(X_t) for i_ot in range(numIterMax): lse_f_cols = [] for i in range_s: M = dist(X_s_np[i : i + bs, :], X_t_np, metric=metric) M = nx.from_numpy(M, type_as=a) lse_f_cols.append(nx.logsumexp(g[None, :] - M / reg, axis=1)) lse_f = nx.concatenate(lse_f_cols, axis=0) f = log_a - lse_f lse_g_cols = [] for j in range_t: M = dist(X_s_np, X_t_np[j : j + bt, :], metric=metric) M = nx.from_numpy(M, type_as=a) lse_g_cols.append(nx.logsumexp(f[:, None] - M / reg, axis=0)) lse_g = nx.concatenate(lse_g_cols, axis=0) g = log_b - lse_g if (i_ot + 1) % 10 == 0: m1_cols = [] for i in range_s: M = dist(X_s_np[i : i + bs, :], X_t_np, metric=metric) M = nx.from_numpy(M, type_as=a) m1_cols.append( nx.sum( nx.exp(f[i : i + bs, None] + g[None, :] - M / reg), axis=1 ) ) m1 = nx.concatenate(m1_cols, axis=0) err = nx.sum(nx.abs(m1 - a)) if log: dict_log["err"].append(err) if verbose and (i_ot + 1) % 100 == 0: print( "Error in marginal at iteration {} = {}".format(i_ot + 1, err) ) if err <= stopThr: break else: if warn: warnings.warn( "Sinkhorn did not converge. You might want to " "increase the number of iterations `numItermax` " "or the regularization parameter `reg`." ) if log: dict_log["u"] = f dict_log["v"] = g dict_log["niter"] = i_ot dict_log["lazy_plan"] = get_sinkhorn_lazytensor(X_s, X_t, f, g, metric, reg) return (f, g, dict_log) else: return (f, g) else: M = dist(X_s, X_t, metric=metric) if log: G, log = sinkhorn( a, b, M, reg, numItermax=numIterMax, stopThr=stopThr, verbose=verbose, log=True, warmstart=warmstart, **kwargs, ) return G, log else: G = sinkhorn( a, b, M, reg, numItermax=numIterMax, stopThr=stopThr, verbose=verbose, log=False, warmstart=warmstart, **kwargs, ) return G def empirical_sinkhorn2( X_s, X_t, reg, a=None, b=None, metric="sqeuclidean", numIterMax=10000, stopThr=1e-9, isLazy=False, batchSize=100, verbose=False, log=False, warn=True, warmstart=None, **kwargs, ): r""" Solve the entropic regularization optimal transport problem from empirical data and return the OT loss The function solves the following optimization problem: .. math:: W = \min_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot\Omega(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma^T \mathbf{1} &= \mathbf{b} \gamma &\geq 0 where : - :math:`\mathbf{M}` is the (`n_samples_a`, `n_samples_b`) metric cost matrix - :math:`\Omega` is the entropic regularization term :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) and returns :math:`\langle \gamma^*, \mathbf{M} \rangle_F` (without the entropic contribution). Parameters ---------- X_s : array-like, shape (n_samples_a, dim) samples in the source domain X_t : array-like, shape (n_samples_b, dim) samples in the target domain reg : float Regularization term >0 a : array-like, shape (n_samples_a,) samples weights in the source domain b : array-like, shape (n_samples_b,) samples weights in the target domain numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) isLazy: boolean, optional If True, then only calculate the cost matrix by block and return the dual potentials only (to save memory). If False, calculate full cost matrix and return outputs of sinkhorn function. batchSize: int or tuple of 2 int, optional Size of the batches used to compute the sinkhorn update without memory overhead. When a tuple is provided it sets the size of the left/right batches. verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the u,v sinkhorn scaling vectors) Returns ------- W : (n_hists) array-like or float Optimal transportation loss for the given parameters log : dict log dictionary return only if log==True in parameters Examples -------- >>> import numpy as np >>> n_samples_a = 2 >>> n_samples_b = 2 >>> reg = 0.1 >>> X_s = np.reshape(np.arange(n_samples_a, dtype=np.float64), (n_samples_a, 1)) >>> X_t = np.reshape(np.arange(0, n_samples_b, dtype=np.float64), (n_samples_b, 1)) >>> b = np.full((n_samples_b, 3), 1/n_samples_b) >>> empirical_sinkhorn2(X_s, X_t, b=b, reg=reg, verbose=False) array([4.53978687e-05, 4.53978687e-05, 4.53978687e-05]) References ---------- .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [9] Schmitzer, B. (2016). Stabilized Sparse Scaling Algorithms for Entropy Regularized Transport Problems. arXiv preprint arXiv:1610.06519. .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. """ X_s, X_t = list_to_array(X_s, X_t) nx = get_backend(X_s, X_t) ns, nt = X_s.shape[0], X_t.shape[0] if a is None: a = nx.from_numpy(unif(ns), type_as=X_s) if b is None: b = nx.from_numpy(unif(nt), type_as=X_s) if isLazy: if log: f, g, dict_log = empirical_sinkhorn( X_s, X_t, reg, a, b, metric, numIterMax=numIterMax, stopThr=stopThr, isLazy=isLazy, batchSize=batchSize, verbose=verbose, log=log, warn=warn, warmstart=warmstart, ) else: f, g = empirical_sinkhorn( X_s, X_t, reg, a, b, metric, numIterMax=numIterMax, stopThr=stopThr, isLazy=isLazy, batchSize=batchSize, verbose=verbose, log=log, warn=warn, warmstart=warmstart, ) bs = batchSize if isinstance(batchSize, int) else batchSize[0] range_s = range(0, ns, bs) loss = 0 X_s_np = nx.to_numpy(X_s) X_t_np = nx.to_numpy(X_t) for i in range_s: M_block = dist(X_s_np[i : i + bs, :], X_t_np, metric=metric) M_block = nx.from_numpy(M_block, type_as=a) pi_block = nx.exp(f[i : i + bs, None] + g[None, :] - M_block / reg) loss += nx.sum(M_block * pi_block) if log: return loss, dict_log else: return loss else: M = dist(X_s, X_t, metric=metric) if log: sinkhorn_loss, log = sinkhorn2( a, b, M, reg, numItermax=numIterMax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart, **kwargs, ) return sinkhorn_loss, log else: sinkhorn_loss = sinkhorn2( a, b, M, reg, numItermax=numIterMax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart, **kwargs, ) return sinkhorn_loss def empirical_sinkhorn_divergence( X_s, X_t, reg, a=None, b=None, metric="sqeuclidean", numIterMax=10000, stopThr=1e-9, verbose=False, log=False, warn=True, warmstart=None, **kwargs, ): r""" Compute the sinkhorn divergence loss from empirical data The function solves the following optimization problems and return the sinkhorn divergence :math:`S`: .. math:: W &= \min_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot\Omega(\gamma) W_a &= \min_{\gamma_a} \quad \langle \gamma_a, \mathbf{M_a} \rangle_F + \mathrm{reg} \cdot\Omega(\gamma_a) W_b &= \min_{\gamma_b} \quad \langle \gamma_b, \mathbf{M_b} \rangle_F + \mathrm{reg} \cdot\Omega(\gamma_b) S &= W - \frac{W_a + W_b}{2} .. math:: s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma^T \mathbf{1} &= \mathbf{b} \gamma &\geq 0 \gamma_a \mathbf{1} &= \mathbf{a} \gamma_a^T \mathbf{1} &= \mathbf{a} \gamma_a &\geq 0 \gamma_b \mathbf{1} &= \mathbf{b} \gamma_b^T \mathbf{1} &= \mathbf{b} \gamma_b &\geq 0 where : - :math:`\mathbf{M}` (resp. :math:`\mathbf{M_a}`, :math:`\mathbf{M_b}`) is the (`n_samples_a`, `n_samples_b`) metric cost matrix (resp (`n_samples_a, n_samples_a`) and (`n_samples_b`, `n_samples_b`)) - :math:`\Omega` is the entropic regularization term :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) and returns :math:`\langle \gamma^*, \mathbf{M} \rangle_F -(\langle \gamma^*_a, \mathbf{M_a} \rangle_F + \langle \gamma^*_b , \mathbf{M_b} \rangle_F)/2`. .. note: The current implementation does not account for the entropic contributions and thus differs from the Sinkhorn divergence as introduced in the literature. The possibility to account for the entropic contributions will be provided in a future release. Parameters ---------- X_s : array-like, shape (n_samples_a, dim) samples in the source domain X_t : array-like, shape (n_samples_b, dim) samples in the target domain reg : float Regularization term >0 a : array-like, shape (n_samples_a,) samples weights in the source domain b : array-like, shape (n_samples_b,) samples weights in the target domain numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the u,v sinkhorn scaling vectors) Returns ------- W : (1,) array-like Optimal transportation symmetrized loss for the given parameters log : dict log dictionary return only if log==True in parameters Examples -------- >>> import numpy as np >>> n_samples_a = 2 >>> n_samples_b = 4 >>> reg = 0.1 >>> X_s = np.reshape(np.arange(n_samples_a, dtype=np.float64), (n_samples_a, 1)) >>> X_t = np.reshape(np.arange(0, n_samples_b, dtype=np.float64), (n_samples_b, 1)) >>> empirical_sinkhorn_divergence(X_s, X_t, reg) # doctest: +ELLIPSIS 1.499887176049052 References ---------- .. [23] Aude Genevay, Gabriel Peyré, Marco Cuturi, Learning Generative Models with Sinkhorn Divergences, Proceedings of the Twenty-First International Conference on Artificial Intelligence and Statistics, (AISTATS) 21, 2018 """ X_s, X_t = list_to_array(X_s, X_t) nx = get_backend(X_s, X_t) if warmstart is None: warmstart_a, warmstart_b = None, None else: u, v = warmstart warmstart_a = (u, u) warmstart_b = (v, v) if log: sinkhorn_loss_ab, log_ab = empirical_sinkhorn2( X_s, X_t, reg, a, b, metric=metric, numIterMax=numIterMax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart, **kwargs, ) sinkhorn_loss_a, log_a = empirical_sinkhorn2( X_s, X_s, reg, a, a, metric=metric, numIterMax=numIterMax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart_a, **kwargs, ) sinkhorn_loss_b, log_b = empirical_sinkhorn2( X_t, X_t, reg, b, b, metric=metric, numIterMax=numIterMax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart_b, **kwargs, ) sinkhorn_div = sinkhorn_loss_ab - 0.5 * (sinkhorn_loss_a + sinkhorn_loss_b) log = {} log["sinkhorn_loss_ab"] = sinkhorn_loss_ab log["sinkhorn_loss_a"] = sinkhorn_loss_a log["sinkhorn_loss_b"] = sinkhorn_loss_b log["log_sinkhorn_ab"] = log_ab log["log_sinkhorn_a"] = log_a log["log_sinkhorn_b"] = log_b return nx.maximum(0, sinkhorn_div), log else: sinkhorn_loss_ab = empirical_sinkhorn2( X_s, X_t, reg, a, b, metric=metric, numIterMax=numIterMax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart, **kwargs, ) sinkhorn_loss_a = empirical_sinkhorn2( X_s, X_s, reg, a, a, metric=metric, numIterMax=numIterMax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart_a, **kwargs, ) sinkhorn_loss_b = empirical_sinkhorn2( X_t, X_t, reg, b, b, metric=metric, numIterMax=numIterMax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart_b, **kwargs, ) sinkhorn_div = sinkhorn_loss_ab - 0.5 * (sinkhorn_loss_a + sinkhorn_loss_b) return nx.maximum(0, sinkhorn_div) def empirical_sinkhorn_nystroem( X_s, X_t, reg=1.0, anchors=50, a=None, b=None, numItermax=1000, stopThr=1e-9, verbose=False, log=False, warn=True, warmstart=None, random_state=None, ): r""" Solves the entropic regularization optimal transport problem with Sinkhorn and Nystroem factorization [76] and returns the OT matrix from empirical data. Corresponds to an approximation of entropic OT (for a squared Euclidean cost) that runs in linear time. The number of anchors controls the level of approximation (the higher, the better the approximation, but the slower the computation becomes). Warning: with low level of regularization, the OT plan can have non-positive values. Parameters ---------- X_s : array-like, shape (n_samples_a, dim) samples in the source domain X_t : array-like, shape (n_samples_b, dim) samples in the target domain reg : float Regularization term >0 anchors : int, optional The total number of anchors sampled for the Nystroem approximation (anchors/2 in each distribution), default 50. a : array-like, shape (n_samples_a,) samples weights in the source domain b : array-like, shape (n_samples_b,) samples weights in the target domain numItermax : int, optional Max number of iterations of Sinkhorn stopThr : float, optional Stop threshold on error on the dual variables (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the u,v sinkhorn scaling vectors) random_state : int, optional The random state for sampling the components in each distribution. Returns ------- gamma : LazyTensor OT plan as lazy tensor. log : dict log dictionary return only if log==True in parameters Examples -------- >>> import numpy as np >>> n_samples_a = 2 >>> n_samples_b = 4 >>> reg = 0.1 >>> anchors = 3 >>> X_s = np.reshape(np.arange(n_samples_a, dtype=np.float64), (n_samples_a, 1)) >>> X_t = np.reshape(np.arange(0, n_samples_b, dtype=np.float64), (n_samples_b, 1)) >>> empirical_sinkhorn_nystroem(X_s, X_t, reg, anchors, random_state=42)[:] # doctest: +ELLIPSIS array([[0.125, 0.125, 0.125, 0.125], [0.125, 0.125, 0.125, 0.125]]) References ---------- .. [80] Massively scalable Sinkhorn distances via the Nyström method, Jason Altschuler, Francis Bach, Alessandro Rudi, Jonathan Niles-Weed, NeurIPS 2019. """ left_factor, right_factor = kernel_nystroem( X_s, X_t, anchors=anchors, sigma=(reg / 2.0) ** (0.5), random_state=random_state ) _, _, dict_log = sinkhorn_low_rank_kernel( K1=left_factor, K2=right_factor, a=a, b=b, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=True, warn=warn, warmstart=warmstart, ) if log: dict_log["left_factor"] = left_factor dict_log["right_factor"] = right_factor return dict_log["lazy_plan"], dict_log else: return dict_log["lazy_plan"] def empirical_sinkhorn_nystroem2( X_s, X_t, reg=1.0, anchors=50, a=None, b=None, numItermax=1000, stopThr=1e-9, verbose=False, log=False, warn=True, warmstart=None, random_state=None, ): r""" Solves the entropic regularization optimal transport problem with Sinkhorn and Nystroem factorization [76] and returns the OT loss from empirical data. Corresponds to an approximation of entropic OT (for a squared Euclidean cost) that runs in linear time. The number of anchors controls the level of approximation (the higher, the better the approximation, but the slower the computation becomes). Warning: with low level of regularization, the OT plan can have non-positive values. Parameters ---------- X_s : array-like, shape (n_samples_a, dim) samples in the source domain X_t : array-like, shape (n_samples_b, dim) samples in the target domain reg : float Regularization term >0 anchors : int, optional The total number of anchors sampled for the Nystroem approximation (anchors/2 in each distribution), default 50. a : array-like, shape (n_samples_a,) samples weights in the source domain b : array-like, shape (n_samples_b,) samples weights in the target domain numItermax : int, optional Max number of iterations of Sinkhorn stopThr : float, optional Stop threshold on error on the dual variables (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the u,v sinkhorn scaling vectors) random_state : int, optional The random state for sampling the components in each distribution. Returns ------- W : float Optimal transportation loss for the given parameters log : dict log dictionary return only if log==True in parameters Examples -------- >>> import numpy as np >>> n_samples_a = 2 >>> n_samples_b = 4 >>> reg = 0.1 >>> anchors = 3 >>> X_s = np.reshape(np.arange(n_samples_a, dtype=np.float64), (n_samples_a, 1)) >>> X_t = np.reshape(np.arange(0, n_samples_b, dtype=np.float64), (n_samples_b, 1)) >>> empirical_sinkhorn_nystroem2(X_s, X_t, reg, anchors, random_state=42) # doctest: +ELLIPSIS 2.5 References ---------- .. [80] Massively scalable Sinkhorn distances via the Nyström method, Jason Altschuler, Francis Bach, Alessandro Rudi, Jonathan Niles-Weed, NeurIPS 2019. """ nx = get_backend(X_s, X_t) M1, M2 = compute_lr_sqeuclidean_matrix(X_s, X_t, False, nx=nx) left_factor, right_factor = kernel_nystroem( X_s, X_t, anchors=anchors, sigma=(reg / 2.0) ** (0.5), random_state=random_state ) if log: u, v, dict_log = sinkhorn_low_rank_kernel( K1=left_factor, K2=right_factor, a=a, b=b, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=True, warn=warn, warmstart=warmstart, ) dict_log["left_factor"] = left_factor dict_log["right_factor"] = right_factor else: u, v = sinkhorn_low_rank_kernel( K1=left_factor, K2=right_factor, a=a, b=b, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=False, warn=warn, warmstart=warmstart, ) Q = u.reshape((-1, 1)) * left_factor R = v.reshape((-1, 1)) * right_factor # Compute the cost (using trace formula) A = Q.T @ M1 B = R.T @ M2 loss = nx.sum(A * B) if log: return loss, dict_log else: return loss python-pot-0.9.7+dfsg/ot/bregman/_geomloss.py000066400000000000000000000202301525521106200211610ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Wrapper functions for geomloss """ # Author: Remi Flamary # # License: MIT License import numpy as np try: import geomloss from geomloss import SamplesLoss import torch from torch.autograd import grad from ..utils import get_backend, LazyTensor, dist if geomloss.__version__ < "0.3.1": old_geomloss = True else: old_geomloss = False except ImportError: geomloss = False old_geomloss = False def get_sinkhorn_geomloss_lazytensor( X_a, X_b, f, g, a, b, metric="sqeuclidean", blur=0.1, nx=None ): """Get a LazyTensor of sinkhorn solution T = exp((f+g^T-C)/reg)*(ab^T) Parameters ---------- X_a : array-like, shape (n_samples_a, dim) samples in the source domain X_torch: array-like, shape (n_samples_b, dim) samples in the target domain f : array-like, shape (n_samples_a,) First dual potentials (log space) g : array-like, shape (n_samples_b,) Second dual potentials (log space) metric : str, default='sqeuclidean' Metric used for the cost matrix computation blur : float, default=1e-1 blur term (blur=sqrt(reg)) >0 nx : Backend(), default=None Numerical backend used Returns ------- T : LazyTensor Lowrank tensor T = exp((f+g^T-C)/reg)*(ab^T) """ if nx is None: nx = get_backend(X_a, X_b, f, g) shape = (X_a.shape[0], X_b.shape[0]) def func(i, j, X_a, X_b, f, g, a, b, metric, blur): X_a_i = X_a[i] X_b_j = X_b[j] if X_a_i.ndim == 1: X_a_i = X_a_i[None, :] if X_b_j.ndim == 1: X_b_j = X_b_j[None, :] if metric == "sqeuclidean": C = dist(X_a_i, X_b_j, metric=metric) / 2 else: C = dist(X_a_i, X_b_j, metric=metric) # Robust broadcasting using nx backend (handles both numpy and torch) # For scalars, slice to keep 1D; for arrays, index directly f_i = f[i : i + 1] if isinstance(i, int) else f[i] g_j = g[j : j + 1] if isinstance(j, int) else g[j] a_i = a[i : i + 1] if isinstance(i, int) else a[i] b_j = b[j : j + 1] if isinstance(j, int) else b[j] f_i = nx.reshape(f_i, (-1, 1)) g_j = nx.reshape(g_j, (1, -1)) a_i = nx.reshape(a_i, (-1, 1)) b_j = nx.reshape(b_j, (1, -1)) return nx.squeeze(nx.exp((f_i + g_j - C) / (blur**2)) * a_i * b_j) T = LazyTensor( shape, func, X_a=X_a, X_b=X_b, f=f, g=g, a=a, b=b, metric=metric, blur=blur ) return T def empirical_sinkhorn2_geomloss( X_s, X_t, reg, a=None, b=None, metric="sqeuclidean", scaling=0.95, verbose=False, debias=False, log=False, backend="auto", ): r"""Solve the entropic regularization optimal transport problem with geomloss The function solves the following optimization problem: .. math:: \gamma = arg\min_\gamma <\gamma,C>_F + reg\cdot\Omega(\gamma) s.t. \gamma 1 = a \gamma^T 1= b \gamma\geq 0 where : - :math:`C` is the cost matrix such that :math:`C_{i,j}=d(x_i^s,x_j^t)` and :math:`d` is a metric. - :math:`\Omega` is the entropic regularization term :math:`\Omega(\gamma)=\sum_{i,j}\gamma_{i,j}\log(\gamma_{i,j})-\gamma_{i,j}+1` - :math:`a` and :math:`b` are source and target weights (sum to 1) The algorithm used for solving the problem is the Sinkhorn-Knopp matrix scaling algorithm as proposed in and computed in log space for better stability and epsilon-scaling. The solution is computed in a lazy way using the Geomloss [60]_ and the KeOps library [61]_. .. warning:: The Geomloss library is required for this function to work. Also when setting `log=True`, the dual potentials are computed using autograd and may be slow for large problems and prevent computing backward gradients. Use the fynction :func:`ot.solve_sample` with `method='geomloss'` for better performance and gradient computation. Parameters ---------- X_s : array-like, shape (n_samples_a, dim) samples in the source domain X_t : array-like, shape (n_samples_b, dim) samples in the target domain reg : float Regularization term >0 a : array-like, shape (n_samples_a,), default=None samples weights in the source domain b : array-like, shape (n_samples_b,), default=None samples weights in the target domain metric : str, default='sqeuclidean' Metric used for the cost matrix computation Only accepted values are 'sqeuclidean' and 'euclidean'. scaling : float, default=0.95 Scaling parameter used for epsilon scaling. Value close to one promote precision while value close to zero promote speed. verbose : bool, default=False Print information debias : bool, default=False Use the debiased version of Sinkhorn algorithm [12]_. log : bool, default=False Return log dictionary containing all computed objects backend : str, default='auto' Numerical backend for geomloss. Only 'auto' and 'tensorized' 'online' and 'multiscale' are accepted values. Returns ------- value : float OT value log : dict Log dictionary return only if log==True in parameters References ---------- .. [60] Feydy, J., Roussillon, P., Trouvé, A., & Gori, P. (2019). [Fast and scalable optimal transport for brain tractograms. In Medical Image Computing and Computer Assisted Intervention–MICCAI 2019: 22nd International Conference, Shenzhen, China, October 13–17, 2019, Proceedings, Part III 22 (pp. 636-644). Springer International Publishing. .. [61] Charlier, B., Feydy, J., Glaunes, J. A., Collin, F. D., & Durif, G. (2021). Kernel operations on the gpu, with autodiff, without memory overflows. The Journal of Machine Learning Research, 22(1), 3457-3462. """ if not geomloss: raise ImportError("geomloss not installed") nx = get_backend(X_s, X_t, a, b) if nx.__name__ not in ["torch", "numpy"]: raise ValueError("geomloss only support torch or numpy backend") if a is None: a = nx.ones(X_s.shape[0], type_as=X_s) / X_s.shape[0] if b is None: b = nx.ones(X_t.shape[0], type_as=X_t) / X_t.shape[0] if nx.__name__ == "numpy": X_s_torch = torch.tensor(X_s) X_t_torch = torch.tensor(X_t) a_torch = torch.tensor(a) b_torch = torch.tensor(b) else: X_s_torch = X_s X_t_torch = X_t a_torch = a b_torch = b # after that we are all in torch # set blur value and p if metric == "sqeuclidean": p = 2 blur = np.sqrt(reg / 2) # because geomloss divides cost by two elif metric == "euclidean": p = 1 blur = np.sqrt(reg) else: raise ValueError("geomloss only supports sqeuclidean and euclidean metrics") if log: # force gradients for computing dual a_torch.requires_grad = True b_torch.requires_grad = True loss = SamplesLoss( loss="sinkhorn", p=p, blur=blur, backend=backend, debias=debias, scaling=scaling, verbose=verbose, ) # compute value value = loss(a_torch, X_s_torch, b_torch, X_t_torch) # linear + entropic/KL reg? # get dual potentials if log: # recover dual potentials f, g = grad(value, [a_torch, b_torch]) if metric == "sqeuclidean": value *= 2 # because geomloss divides cost by two if nx.__name__ == "numpy": f = f.cpu().detach().numpy() g = g.cpu().detach().numpy() value = value.cpu().detach().numpy() log = {} log["f"] = f log["g"] = g log["value"] = value log["lazy_plan"] = get_sinkhorn_geomloss_lazytensor( X_s, X_t, f, g, a, b, metric=metric, blur=blur, nx=nx ) return value, log else: if nx.__name__ == "numpy": value = value.cpu().detach().numpy() return value python-pot-0.9.7+dfsg/ot/bregman/_screenkhorn.py000066400000000000000000000332261525521106200216630ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Screening Sinkhorn Algorithms for Regularized Optimal Transport """ # Author: Remi Flamary # Mokhtar Z. Alaya # # License: MIT License import warnings import numpy as np from scipy.optimize import fmin_l_bfgs_b from ..utils import list_to_array from ..backend import get_backend def screenkhorn( a, b, M, reg, ns_budget=None, nt_budget=None, uniform=False, restricted=True, maxiter=10000, maxfun=10000, pgtol=1e-09, verbose=False, log=False, ): r""" Screening Sinkhorn Algorithm for Regularized Optimal Transport The function solves an approximate dual of Sinkhorn divergence :ref:`[2] ` which is written as the following optimization problem: .. math:: (\mathbf{u}, \mathbf{v}) = \mathop{\arg \min}_{\mathbf{u}, \mathbf{v}} \quad \mathbf{1}_{ns}^T \mathbf{B}(\mathbf{u}, \mathbf{v}) \mathbf{1}_{nt} - \langle \kappa \mathbf{u}, \mathbf{a} \rangle - \langle \frac{1}{\kappa} \mathbf{v}, \mathbf{b} \rangle where: .. math:: \mathbf{B}(\mathbf{u}, \mathbf{v}) = \mathrm{diag}(e^\mathbf{u}) \mathbf{K} \mathrm{diag}(e^\mathbf{v}) \text{, with } \mathbf{K} = e^{-\mathbf{M} / \mathrm{reg}} \text{ and} .. math:: s.t. \ e^{u_i} &\geq \epsilon / \kappa, \forall i \in \{1, \ldots, ns\} e^{v_j} &\geq \epsilon \kappa, \forall j \in \{1, \ldots, nt\} The parameters `kappa` and `epsilon` are determined w.r.t the couple number budget of points (`ns_budget`, `nt_budget`), see Equation (5) in :ref:`[26] ` Parameters ---------- a: array-like, shape=(ns,) samples weights in the source domain b: array-like, shape=(nt,) samples weights in the target domain M: array-like, shape=(ns, nt) Cost matrix reg: `float` Level of the entropy regularisation ns_budget: `int`, default=None Number budget of points to be kept in the source domain. If it is None then 50% of the source sample points will be kept nt_budget: `int`, default=None Number budget of points to be kept in the target domain. If it is None then 50% of the target sample points will be kept uniform: `bool`, default=False If `True`, the source and target distribution are supposed to be uniform, i.e., :math:`a_i = 1 / ns` and :math:`b_j = 1 / nt` restricted : `bool`, default=True If `True`, a warm-start initialization for the L-BFGS-B solver using a restricted Sinkhorn algorithm with at most 5 iterations maxiter: `int`, default=10000 Maximum number of iterations in LBFGS solver maxfun: `int`, default=10000 Maximum number of function evaluations in LBFGS solver pgtol: `float`, default=1e-09 Final objective function accuracy in LBFGS solver verbose: `bool`, default=False If `True`, display information about the cardinals of the active sets and the parameters kappa and epsilon .. admonition:: Dependency To gain more efficiency, :py:func:`ot.bregman.screenkhorn` needs to call the "Bottleneck" package (https://pypi.org/project/Bottleneck/) in the screening pre-processing step. If Bottleneck isn't installed, the following error message appears: "Bottleneck module doesn't exist. Install it from https://pypi.org/project/Bottleneck/" Returns ------- gamma : array-like, shape=(ns, nt) Screened optimal transportation matrix for the given parameters log : `dict`, default=False Log dictionary return only if log==True in parameters .. _references-screenkhorn: References ----------- .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [26] Alaya M. Z., Bérar M., Gasso G., Rakotomamonjy A. (2019). Screening Sinkhorn Algorithm for Regularized Optimal Transport (NIPS) 33, 2019 """ # check if bottleneck module exists try: import bottleneck except ImportError: warnings.warn( "Bottleneck module is not installed. Install it from" " https://pypi.org/project/Bottleneck/ for better performance." ) bottleneck = np a, b, M = list_to_array(a, b, M) nx = get_backend(M, a, b) if nx.__name__ in ("jax", "tf"): raise TypeError( "JAX or TF arrays have been received but screenkhorn is not " "compatible with neither JAX nor TF." ) ns, nt = M.shape # by default, we keep only 50% of the sample data points if ns_budget is None: ns_budget = int(np.floor(0.5 * ns)) if nt_budget is None: nt_budget = int(np.floor(0.5 * nt)) # calculate the Gibbs kernel K = nx.exp(-M / reg) def projection(u, epsilon): u = nx.maximum(u, epsilon) return u # ----------------------------------------------------------------------------------------------------------------# # Step 1: Screening pre-processing # # ----------------------------------------------------------------------------------------------------------------# if ns_budget == ns and nt_budget == nt: # full number of budget points (ns, nt) = (ns_budget, nt_budget) Isel = nx.from_numpy(np.ones(ns, dtype=bool)) Jsel = nx.from_numpy(np.ones(nt, dtype=bool)) epsilon = 0.0 kappa = 1.0 cst_u = 0.0 cst_v = 0.0 bounds_u = [(0.0, np.inf)] * ns bounds_v = [(0.0, np.inf)] * nt a_I = a b_J = b K_IJ = K K_IJc = [] K_IcJ = [] vec_eps_IJc = nx.zeros((nt,), type_as=M) vec_eps_IcJ = nx.zeros((ns,), type_as=M) else: # sum of rows and columns of K K_sum_cols = nx.sum(K, axis=1) K_sum_rows = nx.sum(K, axis=0) if uniform: if ns / ns_budget < 4: aK_sort = nx.sort(K_sum_cols) epsilon_u_square = a[0] / aK_sort[ns_budget - 1] else: aK_sort = nx.from_numpy( bottleneck.partition(nx.to_numpy(K_sum_cols), ns_budget - 1)[ ns_budget - 1 ], type_as=M, ) epsilon_u_square = a[0] / aK_sort if nt / nt_budget < 4: bK_sort = nx.sort(K_sum_rows) epsilon_v_square = b[0] / bK_sort[nt_budget - 1] else: bK_sort = nx.from_numpy( bottleneck.partition(nx.to_numpy(K_sum_rows), nt_budget - 1)[ nt_budget - 1 ], type_as=M, ) epsilon_v_square = b[0] / bK_sort else: aK = a / K_sum_cols bK = b / K_sum_rows aK_sort = nx.flip(nx.sort(aK), axis=0) epsilon_u_square = aK_sort[ns_budget - 1] bK_sort = nx.flip(nx.sort(bK), axis=0) epsilon_v_square = bK_sort[nt_budget - 1] # active sets I and J (see Lemma 1 in [26]) Isel = a >= epsilon_u_square * K_sum_cols Jsel = b >= epsilon_v_square * K_sum_rows if nx.sum(Isel) != ns_budget: if uniform: aK = a / K_sum_cols aK_sort = nx.flip(nx.sort(aK), axis=0) epsilon_u_square = nx.mean(aK_sort[ns_budget - 1 : ns_budget + 1]) Isel = a >= epsilon_u_square * K_sum_cols ns_budget = nx.sum(Isel) if nx.sum(Jsel) != nt_budget: if uniform: bK = b / K_sum_rows bK_sort = nx.flip(nx.sort(bK), axis=0) epsilon_v_square = nx.mean(bK_sort[nt_budget - 1 : nt_budget + 1]) Jsel = b >= epsilon_v_square * K_sum_rows nt_budget = nx.sum(Jsel) epsilon = (epsilon_u_square * epsilon_v_square) ** (1 / 4) kappa = (epsilon_v_square / epsilon_u_square) ** (1 / 2) if verbose: print("epsilon = %s\n" % epsilon) print("kappa = %s\n" % kappa) print( "Cardinality of selected points: |Isel| = %s \t |Jsel| = %s \n" % (sum(Isel), sum(Jsel)) ) # Ic, Jc: complementary of the active sets I and J Ic = ~Isel Jc = ~Jsel K_IJ = K[np.ix_(Isel, Jsel)] K_IcJ = K[np.ix_(Ic, Jsel)] K_IJc = K[np.ix_(Isel, Jc)] K_min = nx.min(K_IJ) if K_min == 0: K_min = float(np.finfo(float).tiny) # a_I, b_J, a_Ic, b_Jc a_I = a[Isel] b_J = b[Jsel] if not uniform: a_I_min = nx.min(a_I) a_I_max = nx.max(a_I) b_J_max = nx.max(b_J) b_J_min = nx.min(b_J) else: a_I_min = a_I[0] a_I_max = a_I[0] b_J_max = b_J[0] b_J_min = b_J[0] # box constraints in L-BFGS-B (see Proposition 1 in [26]) bounds_u = [ ( max( a_I_min / ( (nt - nt_budget) * epsilon + nt_budget * (b_J_max / (ns * epsilon * kappa * K_min)) ), epsilon / kappa, ), a_I_max / (nt * epsilon * K_min), ) ] * ns_budget bounds_v = [ ( max( b_J_min / ( (ns - ns_budget) * epsilon + ns_budget * (kappa * a_I_max / (nt * epsilon * K_min)) ), epsilon * kappa, ), b_J_max / (ns * epsilon * K_min), ) ] * nt_budget # pre-calculated constants for the objective vec_eps_IJc = ( epsilon * kappa * nx.sum(K_IJc * nx.ones((nt - nt_budget,), type_as=M)[None, :], axis=1) ) vec_eps_IcJ = (epsilon / kappa) * nx.sum( nx.ones((ns - ns_budget,), type_as=M)[:, None] * K_IcJ, axis=0 ) # initialisation u0 = nx.full((ns_budget,), 1.0 / ns_budget + epsilon / kappa, type_as=M) v0 = nx.full((nt_budget,), 1.0 / nt_budget + epsilon * kappa, type_as=M) # pre-calculed constants for Restricted Sinkhorn (see Algorithm 1 in supplementary of [26]) if restricted: if ns_budget != ns or nt_budget != nt: cst_u = kappa * epsilon * nx.sum(K_IJc, axis=1) cst_v = epsilon * nx.sum(K_IcJ, axis=0) / kappa for _ in range(5): # 5 iterations K_IJ_v = nx.dot(K_IJ.T, u0) + cst_v v0 = b_J / (kappa * K_IJ_v) KIJ_u = nx.dot(K_IJ, v0) + cst_u u0 = (kappa * a_I) / KIJ_u u0 = projection(u0, epsilon / kappa) v0 = projection(v0, epsilon * kappa) else: u0 = u0 v0 = v0 def restricted_sinkhorn(usc, vsc, max_iter=5): """ Restricted Sinkhorn Algorithm as a warm-start initialized pointfor L-BFGS-B) """ for _ in range(max_iter): K_IJ_v = nx.dot(K_IJ.T, usc) + cst_v vsc = b_J / (kappa * K_IJ_v) KIJ_u = nx.dot(K_IJ, vsc) + cst_u usc = (kappa * a_I) / KIJ_u usc = projection(usc, epsilon / kappa) vsc = projection(vsc, epsilon * kappa) return usc, vsc def screened_obj(usc, vsc): part_IJ = ( nx.dot(nx.dot(usc, K_IJ), vsc) - kappa * nx.dot(a_I, nx.log(usc)) - (1.0 / kappa) * nx.dot(b_J, nx.log(vsc)) ) part_IJc = nx.dot(usc, vec_eps_IJc) part_IcJ = nx.dot(vec_eps_IcJ, vsc) psi_epsilon = part_IJ + part_IJc + part_IcJ return psi_epsilon def screened_grad(usc, vsc): # gradients of Psi_(kappa,epsilon) w.r.t u and v grad_u = nx.dot(K_IJ, vsc) + vec_eps_IJc - kappa * a_I / usc grad_v = nx.dot(K_IJ.T, usc) + vec_eps_IcJ - (1.0 / kappa) * b_J / vsc return grad_u, grad_v def bfgspost(theta): u = theta[:ns_budget] v = theta[ns_budget:] # objective f = screened_obj(u, v) # gradient g_u, g_v = screened_grad(u, v) g = nx.concatenate([g_u, g_v], axis=0) return nx.to_numpy(f), nx.to_numpy(g) # ----------------------------------------------------------------------------------------------------------------# # Step 2: L-BFGS-B solver # # ----------------------------------------------------------------------------------------------------------------# u0, v0 = restricted_sinkhorn(u0, v0) theta0 = nx.concatenate([u0, v0], axis=0) bounds = bounds_u + bounds_v # constraint bounds def obj(theta): return bfgspost(nx.from_numpy(theta, type_as=M)) theta, _, _ = fmin_l_bfgs_b( func=obj, x0=theta0, bounds=bounds, maxfun=maxfun, pgtol=pgtol, maxiter=maxiter ) theta = nx.from_numpy(theta, type_as=M) usc = theta[:ns_budget] vsc = theta[ns_budget:] usc_full = nx.full((ns,), epsilon / kappa, type_as=M) vsc_full = nx.full((nt,), epsilon * kappa, type_as=M) usc_full[Isel] = usc vsc_full[Jsel] = vsc if log: log = {} log["u"] = usc_full log["v"] = vsc_full log["Isel"] = Isel log["Jsel"] = Jsel gamma = usc_full[:, None] * K * vsc_full[None, :] gamma = gamma / nx.sum(gamma) if log: return gamma, log else: return gamma python-pot-0.9.7+dfsg/ot/bregman/_sinkhorn.py000066400000000000000000001357001525521106200211750ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Bregman projections solvers for entropic regularized OT """ # Author: Remi Flamary # Nicolas Courty # Titouan Vayer # Alexander Tong # Quang Huy Tran # # License: MIT License import warnings import numpy as np from ..utils import list_to_array from ..backend import get_backend def sinkhorn( a, b, M, reg, method="sinkhorn", numItermax=1000, stopThr=1e-9, verbose=False, log=False, warn=True, warmstart=None, **kwargs, ): r""" Solve the entropic regularization optimal transport problem and return the OT matrix The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg}\cdot\Omega(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma^T \mathbf{1} &= \mathbf{b} \gamma &\geq 0 where : - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\Omega` is the entropic regularization term :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (histograms, both sum to 1) .. note:: This function is backend-compatible and will work on arrays from all compatible backends. The algorithm used for solving the problem is the Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[2] ` **Choosing a Sinkhorn solver** By default and when using a regularization parameter that is not too small the default sinkhorn solver should be enough. If you need to use a small regularization to get sharper OT matrices, you should use the :py:func:`ot.bregman.sinkhorn_stabilized` solver that will avoid numerical errors. This last solver can be very slow in practice and might not even converge to a reasonable OT matrix in a finite time. This is why :py:func:`ot.bregman.sinkhorn_epsilon_scaling` that relies on iterating the value of the regularization (and using warm start) sometimes leads to better solutions. Note that the greedy version of the sinkhorn :py:func:`ot.bregman.greenkhorn` can also lead to a speedup and the screening version of the sinkhorn :py:func:`ot.bregman.screenkhorn` aim at providing a fast approximation of the Sinkhorn problem. For use of GPU and gradient computation with small number of iterations we strongly recommend the :py:func:`ot.bregman.sinkhorn_log` solver that will no need to check for numerical problems. Parameters ---------- a : array-like, shape (dim_a,) samples weights in the source domain b : array-like, shape (dim_b,) or ndarray, shape (dim_b, n_hists) samples in the target domain, compute sinkhorn with multiple targets and fixed :math:`\mathbf{M}` if :math:`\mathbf{b}` is a matrix (return OT loss + dual variables in log) M : array-like, shape (dim_a, dim_b) loss matrix reg : float Regularization term >0 method : str method used for the solver either 'sinkhorn','sinkhorn_log', 'greenkhorn', 'sinkhorn_stabilized' or 'sinkhorn_epsilon_scaling', see those function for specific parameters numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the u,v sinkhorn scaling vectors) Returns ------- gamma : array-like, shape (dim_a, dim_b) Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters Examples -------- >>> import ot >>> a=[.5, .5] >>> b=[.5, .5] >>> M=[[0., 1.], [1., 0.]] >>> ot.sinkhorn(a, b, M, 1) array([[0.36552929, 0.13447071], [0.13447071, 0.36552929]]) .. _references-sinkhorn: References ---------- .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [9] Schmitzer, B. (2016). Stabilized Sparse Scaling Algorithms for Entropy Regularized Transport Problems. arXiv preprint arXiv:1610.06519. .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. .. [34] Feydy, J., Séjourné, T., Vialard, F. X., Amari, S. I., Trouvé, A., & Peyré, G. (2019, April). Interpolating between optimal transport and MMD using Sinkhorn divergences. In The 22nd International Conference on Artificial Intelligence and Statistics (pp. 2681-2690). PMLR. See Also -------- ot.lp.emd : Unregularized OT ot.optim.cg : General regularized OT ot.bregman.sinkhorn_knopp : Classic Sinkhorn :ref:`[2] ` ot.bregman.sinkhorn_stabilized: Stabilized sinkhorn :ref:`[9] ` :ref:`[10] ` ot.bregman.sinkhorn_epsilon_scaling: Sinkhorn with epsilon scaling :ref:`[9] ` :ref:`[10] ` """ if method.lower() == "sinkhorn": return sinkhorn_knopp( a, b, M, reg, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart, **kwargs, ) elif method.lower() == "sinkhorn_log": return sinkhorn_log( a, b, M, reg, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart, **kwargs, ) elif method.lower() == "greenkhorn": return greenkhorn( a, b, M, reg, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart, ) elif method.lower() == "sinkhorn_stabilized": return sinkhorn_stabilized( a, b, M, reg, numItermax=numItermax, stopThr=stopThr, warmstart=warmstart, verbose=verbose, log=log, warn=warn, **kwargs, ) elif method.lower() == "sinkhorn_epsilon_scaling": return sinkhorn_epsilon_scaling( a, b, M, reg, numItermax=numItermax, stopThr=stopThr, warmstart=warmstart, verbose=verbose, log=log, warn=warn, **kwargs, ) else: raise ValueError("Unknown method '%s'." % method) def sinkhorn2( a, b, M, reg, method="sinkhorn", numItermax=1000, stopThr=1e-9, verbose=False, log=False, warn=False, warmstart=None, **kwargs, ): r""" Solve the entropic regularization optimal transport problem and return the loss The function solves the following optimization problem: .. math:: W = \min_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg}\cdot\Omega(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma^T \mathbf{1} &= \mathbf{b} \gamma &\geq 0 where : - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\Omega` is the entropic regularization term :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (histograms, both sum to 1) and returns :math:`\langle \gamma^*, \mathbf{M} \rangle_F` (without the entropic contribution). .. note:: This function is backend-compatible and will work on arrays from all compatible backends. The algorithm used for solving the problem is the Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[2] ` **Choosing a Sinkhorn solver** By default and when using a regularization parameter that is not too small the default sinkhorn solver should be enough. If you need to use a small regularization to get sharper OT matrices, you should use the :py:func:`ot.bregman.sinkhorn_log` solver that will avoid numerical errors. This last solver can be very slow in practice and might not even converge to a reasonable OT matrix in a finite time. This is why :py:func:`ot.bregman.sinkhorn_epsilon_scaling` that relies on iterating the value of the regularization (and using warm start) sometimes leads to better solutions. Note that the greedy version of the sinkhorn :py:func:`ot.bregman.greenkhorn` can also lead to a speedup and the screening version of the sinkhorn :py:func:`ot.bregman.screenkhorn` aim a providing a fast approximation of the Sinkhorn problem. For use of GPU and gradient computation with small number of iterations we strongly recommend the :py:func:`ot.bregman.sinkhorn_log` solver that will no need to check for numerical problems. Parameters ---------- a : array-like, shape (dim_a,) samples weights in the source domain b : array-like, shape (dim_b,) or ndarray, shape (dim_b, n_hists) samples in the target domain, compute sinkhorn with multiple targets and fixed :math:`\mathbf{M}` if :math:`\mathbf{b}` is a matrix (return OT loss + dual variables in log) M : array-like, shape (dim_a, dim_b) loss matrix reg : float Regularization term >0 method : str method used for the solver either 'sinkhorn','sinkhorn_log', 'sinkhorn_stabilized', see those function for specific parameters numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the u,v sinkhorn scaling vectors) Returns ------- W : (n_hists) float/array-like Optimal transportation loss for the given parameters log : dict log dictionary return only if log==True in parameters Examples -------- >>> import ot >>> a=[.5, .5] >>> b=[.5, .5] >>> M=[[0., 1.], [1., 0.]] >>> ot.sinkhorn2(a, b, M, 1) 0.26894142136999516 .. _references-sinkhorn2: References ---------- .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [9] Schmitzer, B. (2016). Stabilized Sparse Scaling Algorithms for Entropy Regularized Transport Problems. arXiv preprint arXiv:1610.06519. .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. .. [21] Altschuler J., Weed J., Rigollet P. : Near-linear time approximation algorithms for optimal transport via Sinkhorn iteration, Advances in Neural Information Processing Systems (NIPS) 31, 2017 .. [34] Feydy, J., Séjourné, T., Vialard, F. X., Amari, S. I., Trouvé, A., & Peyré, G. (2019, April). Interpolating between optimal transport and MMD using Sinkhorn divergences. In The 22nd International Conference on Artificial Intelligence and Statistics (pp. 2681-2690). PMLR. See Also -------- ot.lp.emd : Unregularized OT ot.optim.cg : General regularized OT ot.bregman.sinkhorn_knopp : Classic Sinkhorn :ref:`[2] ` ot.bregman.greenkhorn : Greenkhorn :ref:`[21] ` ot.bregman.sinkhorn_stabilized: Stabilized sinkhorn :ref:`[9] ` :ref:`[10] ` """ M, a, b = list_to_array(M, a, b) nx = get_backend(M, a, b) if len(b.shape) < 2: if method.lower() == "sinkhorn": res = sinkhorn_knopp( a, b, M, reg, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart, **kwargs, ) elif method.lower() == "sinkhorn_log": res = sinkhorn_log( a, b, M, reg, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart, **kwargs, ) elif method.lower() == "sinkhorn_stabilized": res = sinkhorn_stabilized( a, b, M, reg, numItermax=numItermax, stopThr=stopThr, warmstart=warmstart, verbose=verbose, log=log, warn=warn, **kwargs, ) else: raise ValueError("Unknown method '%s'." % method) if log: return nx.sum(M * res[0]), res[1] else: return nx.sum(M * res) else: if method.lower() == "sinkhorn": return sinkhorn_knopp( a, b, M, reg, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart, **kwargs, ) elif method.lower() == "sinkhorn_log": return sinkhorn_log( a, b, M, reg, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warn=warn, warmstart=warmstart, **kwargs, ) elif method.lower() == "sinkhorn_stabilized": return sinkhorn_stabilized( a, b, M, reg, numItermax=numItermax, stopThr=stopThr, warmstart=warmstart, verbose=verbose, log=log, warn=warn, **kwargs, ) else: raise ValueError("Unknown method '%s'." % method) def sinkhorn_knopp( a, b, M, reg, numItermax=1000, stopThr=1e-9, verbose=False, log=False, warn=True, warmstart=None, **kwargs, ): r""" Solve the entropic regularization optimal transport problem and return the OT matrix The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg}\cdot\Omega(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma^T \mathbf{1} &= \mathbf{b} \gamma &\geq 0 where : - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\Omega` is the entropic regularization term :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (histograms, both sum to 1) The algorithm used for solving the problem is the Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[2] ` Parameters ---------- a : array-like, shape (dim_a,) samples weights in the source domain b : array-like, shape (dim_b,) or array-like, shape (dim_b, n_hists) samples in the target domain, compute sinkhorn with multiple targets and fixed :math:`\mathbf{M}` if :math:`\mathbf{b}` is a matrix (return OT loss + dual variables in log) M : array-like, shape (dim_a, dim_b) loss matrix reg : float Regularization term >0 numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the u,v sinkhorn scaling vectors) Returns ------- gamma : array-like, shape (dim_a, dim_b) Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters Examples -------- >>> import ot >>> a=[.5, .5] >>> b=[.5, .5] >>> M=[[0., 1.], [1., 0.]] >>> ot.sinkhorn(a, b, M, 1) array([[0.36552929, 0.13447071], [0.13447071, 0.36552929]]) .. _references-sinkhorn-knopp: References ---------- .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 See Also -------- ot.lp.emd : Unregularized OT ot.optim.cg : General regularized OT """ a, b, M = list_to_array(a, b, M) nx = get_backend(M, a, b) if len(a) == 0: a = nx.full((M.shape[0],), 1.0 / M.shape[0], type_as=M) if len(b) == 0: b = nx.full((M.shape[1],), 1.0 / M.shape[1], type_as=M) # init data dim_a = len(a) dim_b = b.shape[0] if len(b.shape) > 1: n_hists = b.shape[1] else: n_hists = 0 if log: log = {"err": []} # we assume that no distances are null except those of the diagonal of # distances if warmstart is None: if n_hists: u = nx.ones((dim_a, n_hists), type_as=M) / dim_a v = nx.ones((dim_b, n_hists), type_as=M) / dim_b else: u = nx.ones(dim_a, type_as=M) / dim_a v = nx.ones(dim_b, type_as=M) / dim_b else: u, v = nx.exp(warmstart[0]), nx.exp(warmstart[1]) K = nx.exp(M / (-reg)) Kp = (1 / a).reshape(-1, 1) * K err = 1 for ii in range(numItermax): uprev = u vprev = v KtransposeU = nx.dot(K.T, u) v = b / KtransposeU u = 1.0 / nx.dot(Kp, v) if ( nx.any(KtransposeU == 0) or nx.any(nx.isnan(u)) or nx.any(nx.isnan(v)) or nx.any(nx.isinf(u)) or nx.any(nx.isinf(v)) ): # we have reached the machine precision # come back to previous solution and quit loop warnings.warn("Warning: numerical errors at iteration %d" % ii) u = uprev v = vprev break if ii % 10 == 0: # we can speed up the process by checking for the error only all # the 10th iterations if n_hists: tmp2 = nx.einsum("ik,ij,jk->jk", u, K, v) else: # compute right marginal tmp2= (diag(u)Kdiag(v))^T1 tmp2 = nx.einsum("i,ij,j->j", u, K, v) err = nx.norm(tmp2 - b) # violation of marginal if log: log["err"].append(err) if err < stopThr: break if verbose: if ii % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(ii, err)) else: if warn: warnings.warn( "Sinkhorn did not converge. You might want to " "increase the number of iterations `numItermax` " "or the regularization parameter `reg`." ) if log: log["niter"] = ii log["u"] = u log["v"] = v if n_hists: # return only loss res = nx.einsum("ik,ij,jk,ij->k", u, K, v, M) if log: return res, log else: return res else: # return OT matrix if log: return u.reshape((-1, 1)) * K * v.reshape((1, -1)), log else: return u.reshape((-1, 1)) * K * v.reshape((1, -1)) def sinkhorn_log( a, b, M, reg, numItermax=1000, stopThr=1e-9, verbose=False, log=False, warn=True, warmstart=None, **kwargs, ): r""" Solve the entropic regularization optimal transport problem in log space and return the OT matrix The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg}\cdot\Omega(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma^T \mathbf{1} &= \mathbf{b} \gamma &\geq 0 where : - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\Omega` is the entropic regularization term :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (histograms, both sum to 1) The algorithm used for solving the problem is the Sinkhorn-Knopp matrix scaling algorithm :ref:`[2] ` with the implementation from :ref:`[34] ` Parameters ---------- a : array-like, shape (dim_a,) samples weights in the source domain b : array-like, shape (dim_b,) or array-like, shape (dim_b, n_hists) samples in the target domain, compute sinkhorn with multiple targets and fixed :math:`\mathbf{M}` if :math:`\mathbf{b}` is a matrix (return OT loss + dual variables in log) M : array-like, shape (dim_a, dim_b) loss matrix reg : float Regularization term >0 numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the u,v sinkhorn scaling vectors) Returns ------- gamma : array-like, shape (dim_a, dim_b) Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters Examples -------- >>> import ot >>> a=[.5, .5] >>> b=[.5, .5] >>> M=[[0., 1.], [1., 0.]] >>> ot.sinkhorn(a, b, M, 1) array([[0.36552929, 0.13447071], [0.13447071, 0.36552929]]) .. _references-sinkhorn-log: References ---------- .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [34] Feydy, J., Séjourné, T., Vialard, F. X., Amari, S. I., Trouvé, A., & Peyré, G. (2019, April). Interpolating between optimal transport and MMD using Sinkhorn divergences. In The 22nd International Conference on Artificial Intelligence and Statistics (pp. 2681-2690). PMLR. See Also -------- ot.lp.emd : Unregularized OT ot.optim.cg : General regularized OT """ a, b, M = list_to_array(a, b, M) nx = get_backend(M, a, b) if len(a) == 0: a = nx.full((M.shape[0],), 1.0 / M.shape[0], type_as=M) if len(b) == 0: b = nx.full((M.shape[1],), 1.0 / M.shape[1], type_as=M) # init data dim_a = len(a) dim_b = b.shape[0] if len(b.shape) > 1: n_hists = b.shape[1] else: n_hists = 0 # in case of multiple histograms if n_hists > 1 and warmstart is None: warmstart = [None] * n_hists if n_hists: # we do not want to use tensors sor we do a loop lst_loss = [] lst_u = [] lst_v = [] for k in range(n_hists): res = sinkhorn_log( a, b[:, k], M, reg, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, warmstart=warmstart[k], **kwargs, ) if log: lst_loss.append(nx.sum(M * res[0])) lst_u.append(res[1]["log_u"]) lst_v.append(res[1]["log_v"]) else: lst_loss.append(nx.sum(M * res)) res = nx.stack(lst_loss) if log: log = { "log_u": nx.stack(lst_u, 1), "log_v": nx.stack(lst_v, 1), } log["u"] = nx.exp(log["log_u"]) log["v"] = nx.exp(log["log_v"]) return res, log else: return res else: if log: log = {"err": []} Mr = -M / reg # we assume that no distances are null except those of the diagonal of # distances if warmstart is None: u = nx.zeros(dim_a, type_as=M) v = nx.zeros(dim_b, type_as=M) else: u, v = warmstart def get_logT(u, v): if n_hists: return Mr[:, :, None] + u + v else: return Mr + u[:, None] + v[None, :] loga = nx.log(a) logb = nx.log(b) err = 1 for ii in range(numItermax): v = logb - nx.logsumexp(Mr + u[:, None], 0) u = loga - nx.logsumexp(Mr + v[None, :], 1) if ii % 10 == 0: # we can speed up the process by checking for the error only all # the 10th iterations # compute right marginal tmp2= (diag(u)Kdiag(v))^T1 tmp2 = nx.sum(nx.exp(get_logT(u, v)), 0) err = nx.norm(tmp2 - b) # violation of marginal if log: log["err"].append(err) if verbose: if ii % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(ii, err)) if err < stopThr: break else: if warn: warnings.warn( "Sinkhorn did not converge. You might want to " "increase the number of iterations `numItermax` " "or the regularization parameter `reg`." ) if log: log["niter"] = ii log["log_u"] = u log["log_v"] = v log["u"] = nx.exp(u) log["v"] = nx.exp(v) return nx.exp(get_logT(u, v)), log else: return nx.exp(get_logT(u, v)) def greenkhorn( a, b, M, reg, numItermax=10000, stopThr=1e-9, verbose=False, log=False, warn=True, warmstart=None, ): r""" Solve the entropic regularization optimal transport problem and return the OT matrix The algorithm used is based on the paper :ref:`[22] ` which is a stochastic version of the Sinkhorn-Knopp algorithm :ref:`[2] ` The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg}\cdot\Omega(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma^T \mathbf{1} &= \mathbf{b} \gamma &\geq 0 where : - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\Omega` is the entropic regularization term :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (histograms, both sum to 1) Parameters ---------- a : array-like, shape (dim_a,) samples weights in the source domain b : array-like, shape (dim_b,) or array-like, shape (dim_b, n_hists) samples in the target domain, compute sinkhorn with multiple targets and fixed :math:`\mathbf{M}` if :math:`\mathbf{b}` is a matrix (return OT loss + dual variables in log) M : array-like, shape (dim_a, dim_b) loss matrix reg : float Regularization term >0 numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the u,v sinkhorn scaling vectors) Returns ------- gamma : array-like, shape (dim_a, dim_b) Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters Examples -------- >>> import ot >>> a=[.5, .5] >>> b=[.5, .5] >>> M=[[0., 1.], [1., 0.]] >>> ot.bregman.greenkhorn(a, b, M, 1) array([[0.36552929, 0.13447071], [0.13447071, 0.36552929]]) .. _references-greenkhorn: References ---------- .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [22] J. Altschuler, J.Weed, P. Rigollet : Near-linear time approximation algorithms for optimal transport via Sinkhorn iteration, Advances in Neural Information Processing Systems (NIPS) 31, 2017 See Also -------- ot.lp.emd : Unregularized OT ot.optim.cg : General regularized OT """ a, b, M = list_to_array(a, b, M) nx = get_backend(M, a, b) if nx.__name__ in ("jax", "tf"): raise TypeError( "JAX or TF arrays have been received. Greenkhorn is not " "compatible with neither JAX nor TF" ) if len(a) == 0: a = nx.ones((M.shape[0],), type_as=M) / M.shape[0] if len(b) == 0: b = nx.ones((M.shape[1],), type_as=M) / M.shape[1] dim_a = a.shape[0] dim_b = b.shape[0] K = nx.exp(-M / reg) if warmstart is None: u = nx.full((dim_a,), 1.0 / dim_a, type_as=K) v = nx.full((dim_b,), 1.0 / dim_b, type_as=K) else: u, v = nx.exp(warmstart[0]), nx.exp(warmstart[1]) G = u[:, None] * K * v[None, :] viol = nx.sum(G, axis=1) - a viol_2 = nx.sum(G, axis=0) - b stopThr_val = 1 if log: log = dict() log["u"] = u log["v"] = v for ii in range(numItermax): i_1 = nx.argmax(nx.abs(viol)) i_2 = nx.argmax(nx.abs(viol_2)) m_viol_1 = nx.abs(viol[i_1]) m_viol_2 = nx.abs(viol_2[i_2]) stopThr_val = nx.maximum(m_viol_1, m_viol_2) if m_viol_1 > m_viol_2: old_u = u[i_1] new_u = a[i_1] / nx.dot(K[i_1, :], v) G[i_1, :] = new_u * K[i_1, :] * v viol[i_1] = nx.dot(new_u * K[i_1, :], v) - a[i_1] viol_2 += K[i_1, :].T * (new_u - old_u) * v u[i_1] = new_u else: old_v = v[i_2] new_v = b[i_2] / nx.dot(K[:, i_2].T, u) G[:, i_2] = u * K[:, i_2] * new_v # aviol = (G@one_m - a) # aviol_2 = (G.T@one_n - b) viol += (-old_v + new_v) * K[:, i_2] * u viol_2[i_2] = new_v * nx.dot(K[:, i_2], u) - b[i_2] v[i_2] = new_v if stopThr_val <= stopThr: break else: if warn: warnings.warn( "Sinkhorn did not converge. You might want to " "increase the number of iterations `numItermax` " "or the regularization parameter `reg`." ) if log: log["n_iter"] = ii log["u"] = u log["v"] = v if log: return G, log else: return G def sinkhorn_stabilized( a, b, M, reg, numItermax=1000, tau=1e3, stopThr=1e-9, warmstart=None, verbose=False, print_period=20, log=False, warn=True, **kwargs, ): r""" Solve the entropic regularization OT problem with log stabilization The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg}\cdot\Omega(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma^T \mathbf{1} &= \mathbf{b} \gamma &\geq 0 where : - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\Omega` is the entropic regularization term :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (histograms, both sum to 1) The algorithm used for solving the problem is the Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[2] ` but with the log stabilization proposed in :ref:`[10] ` an defined in :ref:`[9] ` (Algo 3.1) . Parameters ---------- a : array-like, shape (dim_a,) samples weights in the source domain b : array-like, shape (dim_b,) samples in the target domain M : array-like, shape (dim_a, dim_b) loss matrix reg : float Regularization term >0 tau : float threshold for max value in :math:`\mathbf{u}` or :math:`\mathbf{v}` for log scaling warmstart : table of vectors if given then starting values for alpha and beta log scalings numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. Returns ------- gamma : array-like, shape (dim_a, dim_b) Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters Examples -------- >>> import ot >>> a=[.5,.5] >>> b=[.5,.5] >>> M=[[0.,1.],[1.,0.]] >>> ot.bregman.sinkhorn_stabilized(a, b, M, 1) array([[0.36552929, 0.13447071], [0.13447071, 0.36552929]]) .. _references-sinkhorn-stabilized: References ---------- .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [9] Schmitzer, B. (2016). Stabilized Sparse Scaling Algorithms for Entropy Regularized Transport Problems. arXiv preprint arXiv:1610.06519. .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. See Also -------- ot.lp.emd : Unregularized OT ot.optim.cg : General regularized OT """ a, b, M = list_to_array(a, b, M) nx = get_backend(M, a, b) if len(a) == 0: a = nx.ones((M.shape[0],), type_as=M) / M.shape[0] if len(b) == 0: b = nx.ones((M.shape[1],), type_as=M) / M.shape[1] # test if multiple target if len(b.shape) > 1: n_hists = b.shape[1] a = a[:, None] else: n_hists = 0 # init data dim_a = len(a) dim_b = len(b) if log: log = {"err": []} # we assume that no distances are null except those of the diagonal of # distances if warmstart is None: alpha, beta = nx.zeros(dim_a, type_as=M), nx.zeros(dim_b, type_as=M) else: alpha, beta = warmstart if n_hists: u = nx.ones((dim_a, n_hists), type_as=M) / dim_a v = nx.ones((dim_b, n_hists), type_as=M) / dim_b else: u, v = nx.ones(dim_a, type_as=M), nx.ones(dim_b, type_as=M) u /= dim_a v /= dim_b def get_K(alpha, beta): """log space computation""" return nx.exp(-(M - alpha.reshape((dim_a, 1)) - beta.reshape((1, dim_b))) / reg) def get_Gamma(alpha, beta, u, v): """log space gamma computation""" return nx.exp( -(M - alpha.reshape((dim_a, 1)) - beta.reshape((1, dim_b))) / reg + nx.log(u.reshape((dim_a, 1))) + nx.log(v.reshape((1, dim_b))) ) K = get_K(alpha, beta) transp = K err = 1 for ii in range(numItermax): uprev = u vprev = v # sinkhorn update v = b / (nx.dot(K.T, u)) u = a / (nx.dot(K, v)) # remove numerical problems and store them in K if nx.max(nx.abs(u)) > tau or nx.max(nx.abs(v)) > tau: if n_hists: alpha, beta = ( alpha + reg * nx.max(nx.log(u), 1), beta + reg * nx.max(nx.log(v)), ) else: alpha, beta = alpha + reg * nx.log(u), beta + reg * nx.log(v) if n_hists: u = nx.ones((dim_a, n_hists), type_as=M) / dim_a v = nx.ones((dim_b, n_hists), type_as=M) / dim_b else: u = nx.ones(dim_a, type_as=M) / dim_a v = nx.ones(dim_b, type_as=M) / dim_b K = get_K(alpha, beta) if ii % print_period == 0: # we can speed up the process by checking for the error only all # the 10th iterations if n_hists: err_u = nx.max(nx.abs(u - uprev)) err_u /= max(nx.max(nx.abs(u)), nx.max(nx.abs(uprev)), 1.0) err_v = nx.max(nx.abs(v - vprev)) err_v /= max(nx.max(nx.abs(v)), nx.max(nx.abs(vprev)), 1.0) err = 0.5 * (err_u + err_v) else: transp = get_Gamma(alpha, beta, u, v) err = nx.norm(nx.sum(transp, axis=0) - b) if log: log["err"].append(err) if verbose: if ii % (print_period * 20) == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(ii, err)) if err <= stopThr: break if nx.any(nx.isnan(u)) or nx.any(nx.isnan(v)): # we have reached the machine precision # come back to previous solution and quit loop warnings.warn("Numerical errors at iteration %d" % ii) u = uprev v = vprev break else: if warn: warnings.warn( "Sinkhorn did not converge. You might want to " "increase the number of iterations `numItermax` " "or the regularization parameter `reg`." ) if log: if n_hists: alpha = alpha[:, None] beta = beta[:, None] logu = alpha / reg + nx.log(u) logv = beta / reg + nx.log(v) log["n_iter"] = ii log["logu"] = logu log["logv"] = logv log["alpha"] = alpha + reg * nx.log(u) log["beta"] = beta + reg * nx.log(v) log["warmstart"] = (log["alpha"], log["beta"]) if n_hists: res = nx.stack( [ nx.sum(get_Gamma(alpha, beta, u[:, i], v[:, i]) * M) for i in range(n_hists) ] ) return res, log else: return get_Gamma(alpha, beta, u, v), log else: if n_hists: res = nx.stack( [ nx.sum(get_Gamma(alpha, beta, u[:, i], v[:, i]) * M) for i in range(n_hists) ] ) return res else: return get_Gamma(alpha, beta, u, v) def sinkhorn_epsilon_scaling( a, b, M, reg, numItermax=100, epsilon0=1e4, numInnerItermax=100, tau=1e3, stopThr=1e-9, warmstart=None, verbose=False, print_period=10, log=False, warn=True, **kwargs, ): r""" Solve the entropic regularization optimal transport problem with log stabilization and epsilon scaling. The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg}\cdot\Omega(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma^T \mathbf{1} &= \mathbf{b} \gamma &\geq 0 where : - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\Omega` is the entropic regularization term :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (histograms, both sum to 1) The algorithm used for solving the problem is the Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[2] ` but with the log stabilization proposed in :ref:`[10] ` and the log scaling proposed in :ref:`[9] ` algorithm 3.2 Parameters ---------- a : array-like, shape (dim_a,) samples weights in the source domain b : array-like, shape (dim_b,) samples in the target domain M : array-like, shape (dim_a, dim_b) loss matrix reg : float Regularization term >0 tau : float threshold for max value in :math:`\mathbf{u}` or :math:`\mathbf{b}` for log scaling warmstart : tuple of vectors if given then starting values for alpha and beta log scalings numItermax : int, optional Max number of iterations numInnerItermax : int, optional Max number of iterations in the inner slog stabilized sinkhorn epsilon0 : int, optional first epsilon regularization value (then exponential decrease to reg) stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. Returns ------- gamma : array-like, shape (dim_a, dim_b) Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters Examples -------- >>> import ot >>> a=[.5, .5] >>> b=[.5, .5] >>> M=[[0., 1.], [1., 0.]] >>> ot.bregman.sinkhorn_epsilon_scaling(a, b, M, 1) array([[0.36552929, 0.13447071], [0.13447071, 0.36552929]]) .. _references-sinkhorn-epsilon-scaling: References ---------- .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [9] Schmitzer, B. (2016). Stabilized Sparse Scaling Algorithms for Entropy Regularized Transport Problems. arXiv preprint arXiv:1610.06519. .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. See Also -------- ot.lp.emd : Unregularized OT ot.optim.cg : General regularized OT """ a, b, M = list_to_array(a, b, M) nx = get_backend(M, a, b) if len(a) == 0: a = nx.ones((M.shape[0],), type_as=M) / M.shape[0] if len(b) == 0: b = nx.ones((M.shape[1],), type_as=M) / M.shape[1] # init data dim_a = len(a) dim_b = len(b) # nrelative umerical precision with 64 bits numItermin = 35 numItermax = max(numItermin, numItermax) # ensure that last velue is exact ii = 0 if log: log = {"err": []} # we assume that no distances are null except those of the diagonal of # distances if warmstart is None: alpha, beta = nx.zeros(dim_a, type_as=M), nx.zeros(dim_b, type_as=M) else: alpha, beta = warmstart # print(np.min(K)) def get_reg(n): # exponential decreasing return (epsilon0 - reg) * np.exp(-n) + reg err = 1 for ii in range(numItermax): regi = get_reg(ii) G, logi = sinkhorn_stabilized( a, b, M, regi, numItermax=numInnerItermax, stopThr=stopThr, warmstart=(alpha, beta), verbose=False, print_period=20, tau=tau, log=True, ) alpha = logi["alpha"] beta = logi["beta"] if ii % (print_period) == 0: # spsion nearly converged # we can speed up the process by checking for the error only all # the 10th iterations transp = G err = ( nx.norm(nx.sum(transp, axis=0) - b) ** 2 + nx.norm(nx.sum(transp, axis=1) - a) ** 2 ) if log: log["err"].append(err) if verbose: if ii % (print_period * 10) == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(ii, err)) if err <= stopThr and ii > numItermin: break else: if warn: warnings.warn( "Sinkhorn did not converge. You might want to " "increase the number of iterations `numItermax` " "or the regularization parameter `reg`." ) if log: log["alpha"] = alpha log["beta"] = beta log["warmstart"] = (log["alpha"], log["beta"]) log["niter"] = ii return G, log else: return G python-pot-0.9.7+dfsg/ot/bregman/_utils.py000066400000000000000000000023641525521106200205010ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Common tools of Bregman projections solvers for entropic regularized OT """ # Author: Remi Flamary # Nicolas Courty # # License: MIT License from ..utils import list_to_array from ..backend import get_backend def geometricBar(weights, alldistribT): """return the weighted geometric mean of distributions""" weights, alldistribT = list_to_array(weights, alldistribT) nx = get_backend(weights, alldistribT) assert len(weights) == alldistribT.shape[1] return nx.exp(nx.dot(nx.log(alldistribT), weights.T)) def geometricMean(alldistribT): """return the geometric mean of distributions""" alldistribT = list_to_array(alldistribT) nx = get_backend(alldistribT) return nx.exp(nx.mean(nx.log(alldistribT), axis=1)) def projR(gamma, p): """return the KL projection on the row constraints""" gamma, p = list_to_array(gamma, p) nx = get_backend(gamma, p) return (gamma.T * p / nx.maximum(nx.sum(gamma, axis=1), 1e-10)).T def projC(gamma, q): """return the KL projection on the column constraints""" gamma, q = list_to_array(gamma, q) nx = get_backend(gamma, q) return gamma * q / nx.maximum(nx.sum(gamma, axis=0), 1e-10) python-pot-0.9.7+dfsg/ot/coot.py000066400000000000000000000507731525521106200165420ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ CO-Optimal Transport solver """ # Author: Quang Huy Tran # # License: MIT License import warnings from .lp import emd from .utils import list_to_array from .backend import get_backend from .bregman import sinkhorn def co_optimal_transport( X, Y, wx_samp=None, wx_feat=None, wy_samp=None, wy_feat=None, epsilon=0, alpha=0, M_samp=None, M_feat=None, warmstart=None, nits_bcd=100, tol_bcd=1e-7, eval_bcd=1, nits_ot=500, tol_sinkhorn=1e-7, method_sinkhorn="sinkhorn", early_stopping_tol=1e-6, log=False, verbose=False, ): r"""Compute the CO-Optimal Transport between two matrices. Return the sample and feature transport plans between :math:`(\mathbf{X}, \mathbf{w}_{xs}, \mathbf{w}_{xf})` and :math:`(\mathbf{Y}, \mathbf{w}_{ys}, \mathbf{w}_{yf})`. The function solves the following CO-Optimal Transport (COOT) problem: .. math:: \mathbf{COOT}_{\alpha, \varepsilon} = \mathop{\arg \min}_{\mathbf{P}, \mathbf{Q}} &\quad \sum_{i,j,k,l} (\mathbf{X}_{i,k} - \mathbf{Y}_{j,l})^2 \mathbf{P}_{i,j} \mathbf{Q}_{k,l} + \alpha_s \sum_{i,j} \mathbf{P}_{i,j} \mathbf{M^{(s)}}_{i, j} \\ &+ \alpha_f \sum_{k, l} \mathbf{Q}_{k,l} \mathbf{M^{(f)}}_{k, l} + \varepsilon_s \mathbf{KL}(\mathbf{P} | \mathbf{w}_{xs} \mathbf{w}_{ys}^T) + \varepsilon_f \mathbf{KL}(\mathbf{Q} | \mathbf{w}_{xf} \mathbf{w}_{yf}^T) Where : - :math:`\mathbf{X}`: Data matrix in the source space - :math:`\mathbf{Y}`: Data matrix in the target space - :math:`\mathbf{M^{(s)}}`: Additional sample matrix - :math:`\mathbf{M^{(f)}}`: Additional feature matrix - :math:`\mathbf{w}_{xs}`: Distribution of the samples in the source space - :math:`\mathbf{w}_{xf}`: Distribution of the features in the source space - :math:`\mathbf{w}_{ys}`: Distribution of the samples in the target space - :math:`\mathbf{w}_{yf}`: Distribution of the features in the target space .. note:: This function allows epsilon to be zero. In that case, the :any:`ot.lp.emd` solver of POT will be used. Parameters ---------- X : (n_sample_x, n_feature_x) array-like, float First input matrix. Y : (n_sample_y, n_feature_y) array-like, float Second input matrix. wx_samp : (n_sample_x, ) array-like, float, optional (default = None) Histogram assigned on rows (samples) of matrix X. Uniform distribution by default. wx_feat : (n_feature_x, ) array-like, float, optional (default = None) Histogram assigned on columns (features) of matrix X. Uniform distribution by default. wy_samp : (n_sample_y, ) array-like, float, optional (default = None) Histogram assigned on rows (samples) of matrix Y. Uniform distribution by default. wy_feat : (n_feature_y, ) array-like, float, optional (default = None) Histogram assigned on columns (features) of matrix Y. Uniform distribution by default. epsilon : scalar or indexable object of length 2, float or int, optional (default = 0) Regularization parameters for entropic approximation of sample and feature couplings. Allow the case where epsilon contains 0. In that case, the EMD solver is used instead of Sinkhorn solver. If epsilon is scalar, then the same epsilon is applied to both regularization of sample and feature couplings. alpha : scalar or indexable object of length 2, float or int, optional (default = 0) Coefficient parameter of linear terms with respect to the sample and feature couplings. If alpha is scalar, then the same alpha is applied to both linear terms. M_samp : (n_sample_x, n_sample_y), float, optional (default = None) Sample matrix with respect to the linear term on sample coupling. M_feat : (n_feature_x, n_feature_y), float, optional (default = None) Feature matrix with respect to the linear term on feature coupling. warmstart : dictionary, optional (default = None) Contains 4 keys: - "duals_sample" and "duals_feature" whose values are tuples of 2 vectors of size (n_sample_x, n_sample_y) and (n_feature_x, n_feature_y). Initialization of sample and feature dual vectors if using Sinkhorn algorithm. Zero vectors by default. - "pi_sample" and "pi_feature" whose values are matrices of size (n_sample_x, n_sample_y) and (n_feature_x, n_feature_y). Initialization of sample and feature couplings. Uniform distributions by default. nits_bcd : int, optional (default = 100) Number of Block Coordinate Descent (BCD) iterations to solve COOT. tol_bcd : float, optional (default = 1e-7) Tolerance of BCD scheme. If the L1-norm between the current and previous sample couplings is under this threshold, then stop BCD scheme. eval_bcd : int, optional (default = 1) Multiplier of iteration at which the COOT cost is evaluated. For example, if `eval_bcd = 8`, then the cost is calculated at iterations 8, 16, 24, etc... nits_ot : int, optional (default = 100) Number of iterations to solve each of the two optimal transport problems in each BCD iteration. tol_sinkhorn : float, optional (default = 1e-7) Tolerance of Sinkhorn algorithm to stop the Sinkhorn scheme for entropic optimal transport problem (if any) in each BCD iteration. Only triggered when Sinkhorn solver is used. method_sinkhorn : string, optional (default = "sinkhorn") Method used in POT's `ot.sinkhorn` solver. Only support "sinkhorn" and "sinkhorn_log". early_stopping_tol : float, optional (default = 1e-6) Tolerance for the early stopping. If the absolute difference between the last 2 recorded COOT distances is under this tolerance, then stop BCD scheme. log : bool, optional (default = False) If True then the cost and 4 dual vectors, including 2 from sample and 2 from feature couplings, are recorded. verbose : bool, optional (default = False) If True then print the COOT cost at every multiplier of `eval_bcd`-th iteration. Returns ------- pi_samp : (n_sample_x, n_sample_y) array-like, float Sample coupling matrix. pi_feat : (n_feature_x, n_feature_y) array-like, float Feature coupling matrix. log : dictionary, optional Returned if `log` is True. The keys are: duals_sample : (n_sample_x, n_sample_y) tuple, float Pair of dual vectors when solving OT problem w.r.t the sample coupling. duals_feature : (n_feature_x, n_feature_y) tuple, float Pair of dual vectors when solving OT problem w.r.t the feature coupling. distances : list, float List of COOT distances along iterations. References ---------- .. [49] I. Redko, T. Vayer, R. Flamary, and N. Courty, CO-Optimal Transport, Advances in Neural Information Processing ny_sampstems, 33 (2020). """ # Main function if method_sinkhorn not in ["sinkhorn", "sinkhorn_log"]: raise ValueError( "Method {} is not supported in CO-Optimal Transport.".format( method_sinkhorn ) ) X, Y = list_to_array(X, Y) nx = get_backend(X, Y) if isinstance(epsilon, float) or isinstance(epsilon, int): eps_samp, eps_feat = epsilon, epsilon else: if len(epsilon) != 2: raise ValueError( "Epsilon must be either a scalar or an indexable object of length 2." ) else: eps_samp, eps_feat = epsilon[0], epsilon[1] if isinstance(alpha, float) or isinstance(alpha, int): alpha_samp, alpha_feat = alpha, alpha else: if len(alpha) != 2: raise ValueError( "Alpha must be either a scalar or an indexable object of length 2." ) else: alpha_samp, alpha_feat = alpha[0], alpha[1] # constant input variables if M_samp is None or alpha_samp == 0: M_samp, alpha_samp = 0, 0 if M_feat is None or alpha_feat == 0: M_feat, alpha_feat = 0, 0 nx_samp, nx_feat = X.shape ny_samp, ny_feat = Y.shape # measures on rows and columns if wx_samp is None: wx_samp = nx.ones(nx_samp, type_as=X) / nx_samp if wx_feat is None: wx_feat = nx.ones(nx_feat, type_as=X) / nx_feat if wy_samp is None: wy_samp = nx.ones(ny_samp, type_as=Y) / ny_samp if wy_feat is None: wy_feat = nx.ones(ny_feat, type_as=Y) / ny_feat wxy_samp = wx_samp[:, None] * wy_samp[None, :] wxy_feat = wx_feat[:, None] * wy_feat[None, :] # pre-calculate cost constants XY_sqr = (X**2 @ wx_feat)[:, None] + (Y**2 @ wy_feat)[None, :] + alpha_samp * M_samp XY_sqr_T = ( ((X.T) ** 2 @ wx_samp)[:, None] + ((Y.T) ** 2 @ wy_samp)[None, :] + alpha_feat * M_feat ) # initialize coupling and dual vectors if warmstart is None: pi_samp, pi_feat = ( wxy_samp, wxy_feat, ) # shape nx_samp x ny_samp and nx_feat x ny_feat duals_samp = ( nx.zeros(nx_samp, type_as=X), nx.zeros(ny_samp, type_as=Y), ) # shape nx_samp, ny_samp duals_feat = ( nx.zeros(nx_feat, type_as=X), nx.zeros(ny_feat, type_as=Y), ) # shape nx_feat, ny_feat else: pi_samp, pi_feat = warmstart["pi_sample"], warmstart["pi_feature"] duals_samp, duals_feat = warmstart["duals_sample"], warmstart["duals_feature"] # initialize log list_coot = [float("inf")] err = tol_bcd + 1e-3 for idx in range(nits_bcd): pi_samp_prev = nx.copy(pi_samp) # update sample coupling ot_cost = XY_sqr - 2 * X @ pi_feat @ Y.T # size nx_samp x ny_samp if eps_samp > 0: pi_samp, dict_log = sinkhorn( a=wx_samp, b=wy_samp, M=ot_cost, reg=eps_samp, method=method_sinkhorn, numItermax=nits_ot, stopThr=tol_sinkhorn, log=True, warmstart=duals_samp, ) duals_samp = (nx.log(dict_log["u"]), nx.log(dict_log["v"])) elif eps_samp == 0: pi_samp, dict_log = emd( a=wx_samp, b=wy_samp, M=ot_cost, numItermax=nits_ot, log=True ) duals_samp = (dict_log["u"], dict_log["v"]) # update feature coupling ot_cost = XY_sqr_T - 2 * X.T @ pi_samp @ Y # size nx_feat x ny_feat if eps_feat > 0: pi_feat, dict_log = sinkhorn( a=wx_feat, b=wy_feat, M=ot_cost, reg=eps_feat, method=method_sinkhorn, numItermax=nits_ot, stopThr=tol_sinkhorn, log=True, warmstart=duals_feat, ) duals_feat = (nx.log(dict_log["u"]), nx.log(dict_log["v"])) elif eps_feat == 0: pi_feat, dict_log = emd( a=wx_feat, b=wy_feat, M=ot_cost, numItermax=nits_ot, log=True ) duals_feat = (dict_log["u"], dict_log["v"]) if idx % eval_bcd == 0: # update error err = nx.sum(nx.abs(pi_samp - pi_samp_prev)) # COOT part coot = nx.sum(ot_cost * pi_feat) if alpha_samp != 0: coot = coot + alpha_samp * nx.sum(M_samp * pi_samp) # Entropic part if eps_samp != 0: coot = coot + eps_samp * nx.kl_div(pi_samp, wxy_samp) if eps_feat != 0: coot = coot + eps_feat * nx.kl_div(pi_feat, wxy_feat) list_coot.append(coot) if err < tol_bcd or abs(list_coot[-2] - list_coot[-1]) < early_stopping_tol: break if verbose: print( "CO-Optimal Transport cost at iteration {}: {}".format( idx + 1, coot ) ) # sanity check if nx.sum(nx.isnan(pi_samp)) > 0 or nx.sum(nx.isnan(pi_feat)) > 0: warnings.warn("There is NaN in coupling.") if log: dict_log = { "duals_sample": duals_samp, "duals_feature": duals_feat, "distances": list_coot[1:], } return pi_samp, pi_feat, dict_log else: return pi_samp, pi_feat def co_optimal_transport2( X, Y, wx_samp=None, wx_feat=None, wy_samp=None, wy_feat=None, epsilon=0, alpha=0, M_samp=None, M_feat=None, warmstart=None, log=False, verbose=False, early_stopping_tol=1e-6, nits_bcd=100, tol_bcd=1e-7, eval_bcd=1, nits_ot=500, tol_sinkhorn=1e-7, method_sinkhorn="sinkhorn", ): r"""Compute the CO-Optimal Transport distance between two measures. Returns the CO-Optimal Transport distance between :math:`(\mathbf{X}, \mathbf{w}_{xs}, \mathbf{w}_{xf})` and :math:`(\mathbf{Y}, \mathbf{w}_{ys}, \mathbf{w}_{yf})`. The function solves the following CO-Optimal Transport (COOT) problem: .. math:: \mathbf{COOT}_{\alpha, \varepsilon} = \mathop{\arg \min}_{\mathbf{P}, \mathbf{Q}} &\quad \sum_{i,j,k,l} (\mathbf{X}_{i,k} - \mathbf{Y}_{j,l})^2 \mathbf{P}_{i,j} \mathbf{Q}_{k,l} + \alpha_1 \sum_{i,j} \mathbf{P}_{i,j} \mathbf{M^{(s)}}_{i, j} \\ &+ \alpha_2 \sum_{k, l} \mathbf{Q}_{k,l} \mathbf{M^{(f)}}_{k, l} + \varepsilon_1 \mathbf{KL}(\mathbf{P} | \mathbf{w}_{xs} \mathbf{w}_{ys}^T) + \varepsilon_2 \mathbf{KL}(\mathbf{Q} | \mathbf{w}_{xf} \mathbf{w}_{yf}^T) where : - :math:`\mathbf{X}`: Data matrix in the source space - :math:`\mathbf{Y}`: Data matrix in the target space - :math:`\mathbf{M^{(s)}}`: Additional sample matrix - :math:`\mathbf{M^{(f)}}`: Additional feature matrix - :math:`\mathbf{w}_{xs}`: Distribution of the samples in the source space - :math:`\mathbf{w}_{xf}`: Distribution of the features in the source space - :math:`\mathbf{w}_{ys}`: Distribution of the samples in the target space - :math:`\mathbf{w}_{yf}`: Distribution of the features in the target space .. note:: This function allows epsilon to be zero. In that case, the :any:`ot.lp.emd` solver of POT will be used. Parameters ---------- X : (n_sample_x, n_feature_x) array-like, float First input matrix. Y : (n_sample_y, n_feature_y) array-like, float Second input matrix. wx_samp : (n_sample_x, ) array-like, float, optional (default = None) Histogram assigned on rows (samples) of matrix X. Uniform distribution by default. wx_feat : (n_feature_x, ) array-like, float, optional (default = None) Histogram assigned on columns (features) of matrix X. Uniform distribution by default. wy_samp : (n_sample_y, ) array-like, float, optional (default = None) Histogram assigned on rows (samples) of matrix Y. Uniform distribution by default. wy_feat : (n_feature_y, ) array-like, float, optional (default = None) Histogram assigned on columns (features) of matrix Y. Uniform distribution by default. epsilon : scalar or indexable object of length 2, float or int, optional (default = 0) Regularization parameters for entropic approximation of sample and feature couplings. Allow the case where epsilon contains 0. In that case, the EMD solver is used instead of Sinkhorn solver. If epsilon is scalar, then the same epsilon is applied to both regularization of sample and feature couplings. alpha : scalar or indexable object of length 2, float or int, optional (default = 0) Coefficient parameter of linear terms with respect to the sample and feature couplings. If alpha is scalar, then the same alpha is applied to both linear terms. M_samp : (n_sample_x, n_sample_y), float, optional (default = None) Sample matrix with respect to the linear term on sample coupling. M_feat : (n_feature_x, n_feature_y), float, optional (default = None) Feature matrix with respect to the linear term on feature coupling. warmstart : dictionary, optional (default = None) Contains 4 keys: - "duals_sample" and "duals_feature" whose values are tuples of 2 vectors of size (n_sample_x, n_sample_y) and (n_feature_x, n_feature_y). Initialization of sample and feature dual vectors if using Sinkhorn algorithm. Zero vectors by default. - "pi_sample" and "pi_feature" whose values are matrices of size (n_sample_x, n_sample_y) and (n_feature_x, n_feature_y). Initialization of sample and feature couplings. Uniform distributions by default. nits_bcd : int, optional (default = 100) Number of Block Coordinate Descent (BCD) iterations to solve COOT. tol_bcd : float, optional (default = 1e-7) Tolerance of BCD scheme. If the L1-norm between the current and previous sample couplings is under this threshold, then stop BCD scheme. eval_bcd : int, optional (default = 1) Multiplier of iteration at which the COOT cost is evaluated. For example, if `eval_bcd = 8`, then the cost is calculated at iterations 8, 16, 24, etc... nits_ot : int, optional (default = 100) Number of iterations to solve each of the two optimal transport problems in each BCD iteration. tol_sinkhorn : float, optional (default = 1e-7) Tolerance of Sinkhorn algorithm to stop the Sinkhorn scheme for entropic optimal transport problem (if any) in each BCD iteration. Only triggered when Sinkhorn solver is used. method_sinkhorn : string, optional (default = "sinkhorn") Method used in POT's `ot.sinkhorn` solver. Only support "sinkhorn" and "sinkhorn_log". early_stopping_tol : float, optional (default = 1e-6) Tolerance for the early stopping. If the absolute difference between the last 2 recorded COOT distances is under this tolerance, then stop BCD scheme. log : bool, optional (default = False) If True then the cost and 4 dual vectors, including 2 from sample and 2 from feature couplings, are recorded. verbose : bool, optional (default = False) If True then print the COOT cost at every multiplier of `eval_bcd`-th iteration. Returns ------- float CO-Optimal Transport distance. dict Contains logged information from :any:`co_optimal_transport` solver. Only returned if `log` parameter is True References ---------- .. [47] I. Redko, T. Vayer, R. Flamary, and N. Courty, CO-Optimal Transport, Advances in Neural Information Processing ny_sampstems, 33 (2020). """ pi_samp, pi_feat, dict_log = co_optimal_transport( X=X, Y=Y, wx_samp=wx_samp, wx_feat=wx_feat, wy_samp=wy_samp, wy_feat=wy_feat, epsilon=epsilon, alpha=alpha, M_samp=M_samp, M_feat=M_feat, warmstart=warmstart, nits_bcd=nits_bcd, tol_bcd=tol_bcd, eval_bcd=eval_bcd, nits_ot=nits_ot, tol_sinkhorn=tol_sinkhorn, method_sinkhorn=method_sinkhorn, early_stopping_tol=early_stopping_tol, log=True, verbose=verbose, ) X, Y = list_to_array(X, Y) nx = get_backend(X, Y) nx_samp, nx_feat = X.shape ny_samp, ny_feat = Y.shape # measures on rows and columns if wx_samp is None: wx_samp = nx.ones(nx_samp, type_as=X) / nx_samp if wx_feat is None: wx_feat = nx.ones(nx_feat, type_as=X) / nx_feat if wy_samp is None: wy_samp = nx.ones(ny_samp, type_as=Y) / ny_samp if wy_feat is None: wy_feat = nx.ones(ny_feat, type_as=Y) / ny_feat vx_samp, vy_samp = dict_log["duals_sample"] vx_feat, vy_feat = dict_log["duals_feature"] gradX = ( 2 * X * (wx_samp[:, None] * wx_feat[None, :]) - 2 * pi_samp @ Y @ pi_feat.T ) # shape (nx_samp, nx_feat) gradY = ( 2 * Y * (wy_samp[:, None] * wy_feat[None, :]) - 2 * pi_samp.T @ X @ pi_feat ) # shape (ny_samp, ny_feat) coot = dict_log["distances"][-1] coot = nx.set_gradients( coot, (wx_samp, wx_feat, wy_samp, wy_feat, X, Y), (vx_samp, vx_feat, vy_samp, vy_feat, gradX, gradY), ) if log: return coot, dict_log else: return coot python-pot-0.9.7+dfsg/ot/da.py000066400000000000000000003023441525521106200161540ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Domain adaptation with optimal transport """ # Author: Remi Flamary # Nicolas Courty # Michael Perrot # Nathalie Gayraud # Ievgen Redko # Eloi Tanguy # # License: MIT License import numpy as np import warnings from .backend import get_backend from .bregman import sinkhorn, jcpot_barycenter from .lp import emd from .utils import ( unif, dist, kernel, cost_normalization, label_normalization, laplacian, dots, ) from .utils import ( BaseEstimator, check_params, deprecated, labels_to_masks, list_to_array, ) from .unbalanced import sinkhorn_unbalanced from .gaussian import ( empirical_bures_wasserstein_mapping, empirical_gaussian_gromov_wasserstein_mapping, ) from .optim import cg from .optim import gcg from .mapping import ( nearest_brenier_potential_fit, nearest_brenier_potential_predict_bounds, joint_OT_mapping_linear, joint_OT_mapping_kernel, ) # value used in ys/yt to mark a sample whose label is unknown MISSING_LABEL = -1 def sinkhorn_lpl1_mm( a, labels_a, b, M, reg, eta=0.1, numItermax=10, numInnerItermax=200, stopInnerThr=1e-9, verbose=False, log=False, ): r""" Solve the entropic regularization optimal transport problem with non-convex group lasso regularization The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot \Omega_e(\gamma) + \eta \ \Omega_g(\gamma) s.t. \ \gamma \mathbf{1} = \mathbf{a} \gamma^T \mathbf{1} = \mathbf{b} \gamma \geq 0 where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`\Omega_e` is the entropic regularization term :math:`\Omega_e (\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\Omega_g` is the group lasso regularization term :math:`\Omega_g(\gamma)=\sum_{i,c} \|\gamma_{i,\mathcal{I}_c}\|^{1/2}_1` where :math:`\mathcal{I}_c` are the index of samples from class `c` in the source domain. - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) The algorithm used for solving the problem is the generalized conditional gradient as proposed in :ref:`[5, 7] `. Parameters ---------- a : array-like (ns,) samples weights in the source domain labels_a : array-like (ns,) labels of samples in the source domain b : array-like (nt,) samples weights in the target domain M : array-like (ns,nt) loss matrix reg : float Regularization term for entropic regularization >0 eta : float, optional Regularization term for group lasso regularization >0 numItermax : int, optional Max number of iterations numInnerItermax : int, optional Max number of iterations (inner sinkhorn solver) stopInnerThr : float, optional Stop threshold on error (inner sinkhorn solver) (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- gamma : (ns, nt) array-like Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters .. _references-sinkhorn-lpl1-mm: References ---------- .. [5] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, "Optimal Transport for Domain Adaptation," in IEEE Transactions on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 .. [7] Rakotomamonjy, A., Flamary, R., & Courty, N. (2015). Generalized conditional gradient: analysis of convergence and applications. arXiv preprint arXiv:1510.06567. See Also -------- ot.lp.emd : Unregularized OT ot.bregman.sinkhorn : Entropic regularized OT ot.optim.cg : General regularized OT """ a, labels_a, b, M = list_to_array(a, labels_a, b, M) nx = get_backend(a, labels_a, b, M) p = 0.5 epsilon = 1e-3 labels_u, labels_idx = nx.unique(labels_a, return_inverse=True) n_labels = labels_u.shape[0] unroll_labels_idx = nx.eye(n_labels, type_as=M)[labels_idx] W = nx.zeros(M.shape, type_as=M) for _ in range(numItermax): Mreg = M + eta * W if log: transp, log = sinkhorn( a, b, Mreg, reg, numItermax=numInnerItermax, stopThr=stopInnerThr, log=True, ) else: transp = sinkhorn( a, b, Mreg, reg, numItermax=numInnerItermax, stopThr=stopInnerThr ) # the transport has been computed # check if classes are really separated W = ( nx.repeat(transp.T[:, :, None], n_labels, axis=2) * unroll_labels_idx[None, :, :] ) W = nx.sum(W, axis=1) W = nx.dot(W, unroll_labels_idx.T) W = p * ((W.T + epsilon) ** (p - 1)) if log: return transp, log else: return transp def sinkhorn_l1l2_gl( a, labels_a, b, M, reg, eta=0.1, numItermax=10, numInnerItermax=200, stopInnerThr=1e-9, eps=1e-12, verbose=False, log=False, ): r""" Solve the entropic regularization optimal transport problem with group lasso regularization The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot \Omega_e(\gamma) + \eta \ \Omega_g(\gamma) s.t. \ \gamma \mathbf{1} = \mathbf{a} \gamma^T \mathbf{1} = \mathbf{b} \gamma \geq 0 where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`\Omega_e` is the entropic regularization term :math:`\Omega_e(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\Omega_g` is the group lasso regularization term :math:`\Omega_g(\gamma)=\sum_{i,c} \|\gamma_{i,\mathcal{I}_c}\|^2` where :math:`\mathcal{I}_c` are the index of samples from class `c` in the source domain. - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) The algorithm used for solving the problem is the generalized conditional gradient as proposed in :ref:`[5, 7] `. Parameters ---------- a : array-like (ns,) samples weights in the source domain labels_a : array-like (ns,) labels of samples in the source domain b : array-like (nt,) samples in the target domain M : array-like (ns,nt) loss matrix reg : float Regularization term for entropic regularization >0 eta : float, optional Regularization term for group lasso regularization >0 numItermax : int, optional Max number of iterations numInnerItermax : int, optional Max number of iterations (inner sinkhorn solver) stopInnerThr : float, optional Stop threshold on error (inner sinkhorn solver) (>0) eps: float, optional (default=1e-12) Small value to avoid division by zero verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- gamma : (ns, nt) array-like Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters .. _references-sinkhorn-l1l2-gl: References ---------- .. [5] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, "Optimal Transport for Domain Adaptation," in IEEE Transactions on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 .. [7] Rakotomamonjy, A., Flamary, R., & Courty, N. (2015). Generalized conditional gradient: analysis of convergence and applications. arXiv preprint arXiv:1510.06567. See Also -------- ot.optim.gcg : Generalized conditional gradient for OT problems """ a, labels_a, b, M = list_to_array(a, labels_a, b, M) nx = get_backend(a, labels_a, b, M) labels_u, labels_idx = nx.unique(labels_a, return_inverse=True) n_labels = labels_u.shape[0] unroll_labels_idx = nx.eye(n_labels, type_as=labels_u)[None, labels_idx] def f(G): G_split = nx.repeat(G.T[:, :, None], n_labels, axis=2) return nx.sum(nx.norm(G_split * unroll_labels_idx, axis=1)) def df(G): G_split = nx.repeat(G.T[:, :, None], n_labels, axis=2) * unroll_labels_idx W = nx.norm(G_split * unroll_labels_idx, axis=1, keepdims=True) G_norm = G_split / nx.clip(W, eps, None) return nx.sum(G_norm, axis=2).T return gcg( a, b, M, reg, eta, f, df, G0=None, numItermax=numItermax, numInnerItermax=numInnerItermax, stopThr=stopInnerThr, verbose=verbose, log=log, ) OT_mapping_linear = deprecated(empirical_bures_wasserstein_mapping) def emd_laplace( a, b, xs, xt, M, sim="knn", sim_param=None, reg="pos", eta=1, alpha=0.5, numItermax=100, stopThr=1e-9, numInnerItermax=100000, stopInnerThr=1e-9, log=False, verbose=False, ): r"""Solve the optimal transport problem (OT) with Laplacian regularization .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \eta \cdot \Omega_\alpha(\gamma) s.t. \ \gamma \mathbf{1} = \mathbf{a} \gamma^T \mathbf{1} = \mathbf{b} \gamma \geq 0 where: - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) - :math:`\mathbf{x_s}` and :math:`\mathbf{x_t}` are source and target samples - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`\Omega_\alpha` is the Laplacian regularization term .. math:: \Omega_\alpha = \frac{1 - \alpha}{n_s^2} \sum_{i,j} \mathbf{S^s}_{i,j} \|T(\mathbf{x}^s_i) - T(\mathbf{x}^s_j) \|^2 + \frac{\alpha}{n_t^2} \sum_{i,j} \mathbf{S^t}_{i,j} \|T(\mathbf{x}^t_i) - T(\mathbf{x}^t_j) \|^2 with :math:`\mathbf{S^s}_{i,j}, \mathbf{S^t}_{i,j}` denoting source and target similarity matrices and :math:`T(\cdot)` being a barycentric mapping. The algorithm used for solving the problem is the conditional gradient algorithm as proposed in :ref:`[5] `. Parameters ---------- a : array-like (ns,) samples weights in the source domain b : array-like (nt,) samples weights in the target domain xs : array-like (ns,d) samples in the source domain xt : array-like (nt,d) samples in the target domain M : array-like (ns,nt) loss matrix sim : string, optional Type of similarity ('knn' or 'gauss') used to construct the Laplacian. sim_param : int or float, optional Parameter (number of the nearest neighbors for sim='knn' or bandwidth for sim='gauss') used to compute the Laplacian. reg : string Type of Laplacian regularization eta : float Regularization term for Laplacian regularization alpha : float Regularization term for source domain's importance in regularization numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (inner emd solver) (>0) numInnerItermax : int, optional Max number of iterations (inner CG solver) stopInnerThr : float, optional Stop threshold on error (inner CG solver) (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- gamma : (ns, nt) array-like Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters .. _references-emd-laplace: References ---------- .. [5] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, "Optimal Transport for Domain Adaptation," in IEEE Transactions on Pattern Analysis and Machine Intelligence, vol.PP, no.99, pp.1-1 .. [30] R. Flamary, N. Courty, D. Tuia, A. Rakotomamonjy, "Optimal transport with Laplacian regularization: Applications to domain adaptation and shape matching," in NIPS Workshop on Optimal Transport and Machine Learning OTML, 2014. See Also -------- ot.lp.emd : Unregularized OT ot.optim.cg : General regularized OT """ if not isinstance(sim_param, (int, float, type(None))): raise ValueError( "Similarity parameter should be an int or a float. Got {type} instead.".format( type=type(sim_param).__name__ ) ) a, b, xs, xt, M = list_to_array(a, b, xs, xt, M) nx = get_backend(a, b, xs, xt, M) if sim == "gauss": if sim_param is None: sim_param = 1 / (2 * (nx.mean(dist(xs, xs, "sqeuclidean")) ** 2)) sS = kernel(xs, xs, method=sim, sigma=sim_param) sT = kernel(xt, xt, method=sim, sigma=sim_param) elif sim == "knn": if sim_param is None: sim_param = 3 try: from sklearn.neighbors import kneighbors_graph except ImportError: raise ValueError( "scikit-learn must be installed to use knn similarity. Install with `$pip install scikit-learn`." ) sS = nx.from_numpy( kneighbors_graph(X=nx.to_numpy(xs), n_neighbors=int(sim_param)).toarray(), type_as=xs, ) sS = (sS + sS.T) / 2 sT = nx.from_numpy( kneighbors_graph(X=nx.to_numpy(xt), n_neighbors=int(sim_param)).toarray(), type_as=xt, ) sT = (sT + sT.T) / 2 else: raise ValueError( 'Unknown similarity type {sim}. Currently supported similarity types are "knn" and "gauss".'.format( sim=sim ) ) lS = laplacian(sS) lT = laplacian(sT) def f(G): return alpha * nx.trace(dots(xt.T, G.T, lS, G, xt)) + (1 - alpha) * nx.trace( dots(xs.T, G, lT, G.T, xs) ) ls2 = lS + lS.T lt2 = lT + lT.T xt2 = nx.dot(xt, xt.T) if reg == "disp": Cs = -eta * alpha / xs.shape[0] * dots(ls2, xs, xt.T) Ct = -eta * (1 - alpha) / xt.shape[0] * dots(xs, xt.T, lt2) M = M + Cs + Ct def df(G): return alpha * dots(ls2, G, xt2) + (1 - alpha) * dots(xs, xs.T, G, lt2) return cg( a, b, M, reg=eta, f=f, df=df, G0=None, numItermax=numItermax, numItermaxEmd=numInnerItermax, stopThr=stopThr, stopThr2=stopInnerThr, verbose=verbose, log=log, ) def distribution_estimation_uniform(X): r"""estimates a uniform distribution from an array of samples :math:`\mathbf{X}` Parameters ---------- X : array-like, shape (n_samples, n_features) The array of samples Returns ------- mu : array-like, shape (n_samples,) The uniform distribution estimated from :math:`\mathbf{X}` """ return unif(X.shape[0], type_as=X) class BaseTransport(BaseEstimator): """Base class for OTDA objects .. note:: All estimators should specify all the parameters that can be set at the class level in their ``__init__`` as explicit keyword arguments (no ``*args`` or ``**kwargs``). The fit method should: - estimate a cost matrix and store it in a `cost_` attribute - estimate a coupling matrix and store it in a `coupling_` attribute - estimate distributions from source and target data and store them in `mu_s` and `mu_t` attributes - store `Xs` and `Xt` in attributes to be used later on in `transform` and `inverse_transform` methods `transform` method should always get as input a `Xs` parameter `inverse_transform` method should always get as input a `Xt` parameter `transform_labels` method should always get as input a `ys` parameter `inverse_transform_labels` method should always get as input a `yt` parameter """ def fit(self, Xs=None, ys=None, Xt=None, yt=None): r"""Build a coupling matrix from source and target sets of samples :math:`(\mathbf{X_s}, \mathbf{y_s})` and :math:`(\mathbf{X_t}, \mathbf{y_t})` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. ys : array-like, shape (n_source_samples,) The training class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label Returns ------- self : object Returns self. """ nx = self._get_backend(Xs, ys, Xt, yt) # check the necessary inputs parameters are here if check_params(Xs=Xs, Xt=Xt): # pairwise distance self.cost_ = dist(Xs, Xt, metric=self.metric) self.cost_, self.norm_cost_ = cost_normalization( self.cost_, self.norm, return_value=True ) if (ys is not None) and (yt is not None): if self.limit_max != np.inf: self.limit_max = self.limit_max * nx.max(self.cost_) # present_labels is a (ns, nt) matrix of {0, 1} such that # the cell (i, j) is 1 iff both ys[i] and yt[j] are labeled # (i.e. neither is masked with MISSING_LABEL) present_ys = (ys != MISSING_LABEL) + nx.zeros(ys.shape, type_as=ys) present_yt = (yt != MISSING_LABEL) + nx.zeros(yt.shape, type_as=yt) present_labels = present_ys[:, None] @ present_yt[None, :] # label_mismatch is a (ns, nt) matrix of {True, False} such that # the cell (i, j) is True if ys[i] != yt[j] label_mismatch = (ys[:, None] - yt[None, :]) != 0 # cost correction is a (ns, nt) matrix of {-Inf, float, Inf} such # that he cells (i, j) has -Inf where there's no correction necessary # by 'correction' we mean setting cost to a large value when two # labeled samples have different labels # we suppress potential RuntimeWarning caused by Inf multiplication # (as we explicitly cover potential NANs later) with warnings.catch_warnings(): warnings.simplefilter("ignore", category=RuntimeWarning) cost_correction = label_mismatch * present_labels * self.limit_max # this operation is necessary because 0 * Inf = NAN # thus is irrelevant when limit_max is finite cost_correction = nx.nan_to_num(cost_correction, -np.inf) self.cost_ = nx.maximum(self.cost_, cost_correction) # distribution estimation self.mu_s = self.distribution_estimation(Xs) self.mu_t = self.distribution_estimation(Xt) # store arrays of samples self.xs_ = Xs self.xt_ = Xt return self def fit_transform(self, Xs=None, ys=None, Xt=None, yt=None): r"""Build a coupling matrix from source and target sets of samples :math:`(\mathbf{X_s}, \mathbf{y_s})` and :math:`(\mathbf{X_t}, \mathbf{y_t})` and transports source samples :math:`\mathbf{X_s}` onto target ones :math:`\mathbf{X_t}` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. ys : array-like, shape (n_source_samples,) The class labels for training samples Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label Returns ------- transp_Xs : array-like, shape (n_source_samples, n_features) The source samples samples. """ return self.fit(Xs, ys, Xt, yt).transform(Xs, ys, Xt, yt) def transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): r"""Transports source samples :math:`\mathbf{X_s}` onto target ones :math:`\mathbf{X_t}` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The source input samples. ys : array-like, shape (n_source_samples,) The class labels for source samples Xt : array-like, shape (n_target_samples, n_features) The target input samples. yt : array-like, shape (n_target_samples,) The class labels for target. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label batch_size : int, optional (default=128) The batch size for out of sample inverse transform Returns ------- transp_Xs : array-like, shape (n_source_samples, n_features) The transport source samples. """ nx = self.nx # check the necessary inputs parameters are here if check_params(Xs=Xs): if nx.array_equal(self.xs_, Xs): # perform standard barycentric mapping transp = self.coupling_ / nx.sum(self.coupling_, axis=1)[:, None] # set nans to 0 transp = nx.nan_to_num(transp, nan=0, posinf=0, neginf=0) # compute transported samples transp_Xs = nx.dot(transp, self.xt_) else: # perform out of sample mapping indices = nx.arange(Xs.shape[0]) batch_ind = [ indices[i : i + batch_size] for i in range(0, len(indices), batch_size) ] transp_Xs = [] for bi in batch_ind: # get the nearest neighbor in the source domain D0 = dist(Xs[bi], self.xs_) idx = nx.argmin(D0, axis=1) # transport the source samples transp = self.coupling_ / nx.sum(self.coupling_, axis=1)[:, None] transp = nx.nan_to_num(transp, nan=0, posinf=0, neginf=0) transp_Xs_ = nx.dot(transp, self.xt_) # define the transported points transp_Xs_ = transp_Xs_[idx, :] + Xs[bi] - self.xs_[idx, :] transp_Xs.append(transp_Xs_) transp_Xs = nx.concatenate(transp_Xs, axis=0) return transp_Xs def transform_labels(self, ys=None): r"""Propagate source labels :math:`\mathbf{y_s}` to obtain estimated target labels as in :ref:`[27] `. Parameters ---------- ys : array-like, shape (n_source_samples,) The source class labels Returns ------- transp_ys : array-like, shape (n_target_samples, nb_classes) Estimated soft target labels. .. _references-basetransport-transform-labels: References ---------- .. [27] Ievgen Redko, Nicolas Courty, Rémi Flamary, Devis Tuia "Optimal transport for multi-source domain adaptation under target shift", International Conference on Artificial Intelligence and Statistics (AISTATS), 2019. """ nx = self.nx # check the necessary inputs parameters are here if check_params(ys=ys): # perform label propagation transp = self.coupling_ / nx.sum(self.coupling_, axis=0)[None, :] # set nans to 0 transp = nx.nan_to_num(transp, nan=0, posinf=0, neginf=0) # compute propagated labels labels = label_normalization(ys) masks = labels_to_masks(labels, nx=nx, type_as=transp) transp_ys = nx.dot(masks.T, transp) return transp_ys.T def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): r"""Transports target samples :math:`\mathbf{X_t}` onto source samples :math:`\mathbf{X_s}` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The source input samples. ys : array-like, shape (n_source_samples,) The source class labels Xt : array-like, shape (n_target_samples, n_features) The target input samples. yt : array-like, shape (n_target_samples,) The target class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label batch_size : int, optional (default=128) The batch size for out of sample inverse transform Returns ------- transp_Xt : array-like, shape (n_source_samples, n_features) The transported target samples. """ nx = self.nx # check the necessary inputs parameters are here if check_params(Xt=Xt): if nx.array_equal(self.xt_, Xt): # perform standard barycentric mapping transp_ = self.coupling_.T / nx.sum(self.coupling_, 0)[:, None] # set nans to 0 transp_ = nx.nan_to_num(transp_, nan=0, posinf=0, neginf=0) # compute transported samples transp_Xt = nx.dot(transp_, self.xs_) else: # perform out of sample mapping indices = nx.arange(Xt.shape[0]) batch_ind = [ indices[i : i + batch_size] for i in range(0, len(indices), batch_size) ] transp_Xt = [] for bi in batch_ind: D0 = dist(Xt[bi], self.xt_) idx = nx.argmin(D0, axis=1) # transport the target samples transp_ = self.coupling_.T / nx.sum(self.coupling_, 0)[:, None] transp_ = nx.nan_to_num(transp_, nan=0, posinf=0, neginf=0) transp_Xt_ = nx.dot(transp_, self.xs_) # define the transported points transp_Xt_ = transp_Xt_[idx, :] + Xt[bi] - self.xt_[idx, :] transp_Xt.append(transp_Xt_) transp_Xt = nx.concatenate(transp_Xt, axis=0) return transp_Xt def inverse_transform_labels(self, yt=None): r"""Propagate target labels :math:`\mathbf{y_t}` to obtain estimated source labels :math:`\mathbf{y_s}` Parameters ---------- yt : array-like, shape (n_target_samples,) Returns ------- transp_ys : array-like, shape (n_source_samples, nb_classes) Estimated soft source labels. """ nx = self.nx # check the necessary inputs parameters are here if check_params(yt=yt): # perform label propagation transp = self.coupling_ / nx.sum(self.coupling_, 1)[:, None] # set nans to 0 transp = nx.nan_to_num(transp, nan=0, posinf=0, neginf=0) # compute propagated labels labels = label_normalization(yt) masks = labels_to_masks(labels, nx=nx, type_as=transp) transp_ys = nx.dot(masks.T, transp.T) return transp_ys.T class LinearTransport(BaseTransport): r"""OT linear operator between empirical distributions The function estimates the optimal linear operator that aligns the two empirical distributions. This is equivalent to estimating the closed form mapping between two Gaussian distributions :math:`\mathcal{N}(\mu_s,\Sigma_s)` and :math:`\mathcal{N}(\mu_t,\Sigma_t)` as proposed in :ref:`[14] ` and discussed in remark 2.29 in :ref:`[15] `. The linear operator from source to target :math:`M` .. math:: M(\mathbf{x})= \mathbf{A} \mathbf{x} + \mathbf{b} where : .. math:: \mathbf{A} &= \Sigma_s^{-1/2} \left(\Sigma_s^{1/2}\Sigma_t\Sigma_s^{1/2} \right)^{1/2} \Sigma_s^{-1/2} \mathbf{b} &= \mu_t - \mathbf{A} \mu_s Parameters ---------- reg : float,optional regularization added to the daigonals of covariances (>0) bias: boolean, optional estimate bias :math:`\mathbf{b}` else :math:`\mathbf{b} = 0` (default:True) log : bool, optional record log if True .. _references-lineartransport: References ---------- .. [14] Knott, M. and Smith, C. S. "On the optimal mapping of distributions", Journal of Optimization Theory and Applications Vol 43, 1984 .. [15] Peyré, G., & Cuturi, M. (2017). "Computational Optimal Transport", 2018. """ def __init__( self, reg=1e-8, bias=True, log=False, distribution_estimation=distribution_estimation_uniform, ): self.bias = bias self.log = log self.reg = reg self.distribution_estimation = distribution_estimation def fit(self, Xs=None, ys=None, Xt=None, yt=None): r"""Build a coupling matrix from source and target sets of samples :math:`(\mathbf{X_s}, \mathbf{y_s})` and :math:`(\mathbf{X_t}, \mathbf{y_t})` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. ys : array-like, shape (n_source_samples,) The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label Returns ------- self : object Returns self. """ nx = self._get_backend(Xs, ys, Xt, yt) self.nx = nx self.mu_s = self.distribution_estimation(Xs) self.mu_t = self.distribution_estimation(Xt) # coupling estimation returned_ = empirical_bures_wasserstein_mapping( Xs, Xt, reg=self.reg, ws=nx.reshape(self.mu_s, (-1, 1)), wt=nx.reshape(self.mu_t, (-1, 1)), bias=self.bias, log=self.log, ) # deal with the value of log if self.log: self.A_, self.B_, self.log_ = returned_ else: ( self.A_, self.B_, ) = returned_ self.log_ = dict() # re compute inverse mapping self.A1_ = nx.inv(self.A_) self.B1_ = -nx.dot(self.B_, self.A1_) return self def transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): r"""Transports source samples :math:`\mathbf{X_s}` onto target ones :math:`\mathbf{X_t}` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. ys : array-like, shape (n_source_samples,) The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label batch_size : int, optional (default=128) The batch size for out of sample inverse transform Returns ------- transp_Xs : array-like, shape (n_source_samples, n_features) The transport source samples. """ nx = self.nx # check the necessary inputs parameters are here if check_params(Xs=Xs): transp_Xs = nx.dot(Xs, self.A_) + self.B_ return transp_Xs def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): r"""Transports target samples :math:`\mathbf{X_t}` onto source samples :math:`\mathbf{X_s}` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. ys : array-like, shape (n_source_samples,) The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label batch_size : int, optional (default=128) The batch size for out of sample inverse transform Returns ------- transp_Xt : array-like, shape (n_source_samples, n_features) The transported target samples. """ nx = self.nx # check the necessary inputs parameters are here if check_params(Xt=Xt): transp_Xt = nx.dot(Xt, self.A1_) + self.B1_ return transp_Xt class LinearGWTransport(LinearTransport): r"""OT Gaussian Gromov-Wasserstein linear operator between empirical distributions The function estimates the optimal linear operator that aligns the two empirical distributions optimally wrt the Gromov-Wasserstein distance. This is equivalent to estimating the closed form mapping between two Gaussian distributions :math:`\mathcal{N}(\mu_s,\Sigma_s)` and :math:`\mathcal{N}(\mu_t,\Sigma_t)` as proposed in :ref:`[57] `. The linear operator from source to target :math:`M` .. math:: M(\mathbf{x})= \mathbf{A} \mathbf{x} + \mathbf{b} where the matrix :math:`\mathbf{A}` and the vector :math:`\mathbf{b}` are defined in :ref:`[57] `. Parameters ---------- sign_eigs : array-like (n_features), str, optional sign of the eigenvalues of the mapping matrix, by default all signs will be positive. If 'skewness' is provided, the sign of the eigenvalues is selected as the product of the sign of the skewness of the projected data. log : bool, optional record log if True .. _references-lineargwtransport: References ---------- .. [57] Delon, J., Desolneux, A., & Salmona, A. (2022). Gromov–Wasserstein distances between Gaussian distributions. Journal of Applied Probability, 59(4), 1178-1198. """ def __init__( self, log=False, sign_eigs=None, distribution_estimation=distribution_estimation_uniform, ): self.sign_eigs = sign_eigs self.log = log self.distribution_estimation = distribution_estimation def fit(self, Xs=None, ys=None, Xt=None, yt=None): r"""Build a coupling matrix from source and target sets of samples :math:`(\mathbf{X_s}, \mathbf{y_s})` and :math:`(\mathbf{X_t}, \mathbf{y_t})` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. ys : array-like, shape (n_source_samples,) The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label Returns ------- self : object Returns self. """ nx = self._get_backend(Xs, ys, Xt, yt) self.nx = nx self.mu_s = self.distribution_estimation(Xs) self.mu_t = self.distribution_estimation(Xt) # coupling estimation returned_ = empirical_gaussian_gromov_wasserstein_mapping( Xs, Xt, ws=self.mu_s[:, None], wt=self.mu_t[:, None], sign_eigs=self.sign_eigs, log=self.log, ) # deal with the value of log if self.log: self.A_, self.B_, self.log_ = returned_ else: ( self.A_, self.B_, ) = returned_ self.log_ = dict() # re compute inverse mapping returned_1_ = empirical_gaussian_gromov_wasserstein_mapping( Xt, Xs, ws=self.mu_t[:, None], wt=self.mu_s[:, None], sign_eigs=self.sign_eigs, log=self.log, ) if self.log: self.A1_, self.B1_, self.log_1_ = returned_1_ else: ( self.A1_, self.B1_, ) = returned_1_ self.log_ = dict() return self class SinkhornTransport(BaseTransport): """Domain Adaptation OT method based on Sinkhorn Algorithm Parameters ---------- reg_e : float, optional (default=1) Entropic regularization parameter max_iter : int, float, optional (default=1000) The minimum number of iteration before stopping the optimization algorithm if it has not converged tol : float, optional (default=10e-9) The precision required to stop the optimization algorithm. verbose : bool, optional (default=False) Controls the verbosity of the optimization algorithm log : int, optional (default=False) Controls the logs of the optimization algorithm metric : string, optional (default="sqeuclidean") The ground metric for the Wasserstein problem norm : string, optional (default=None) If given, normalize the ground metric to avoid numerical errors that can occur with large metric values. Accepted values are 'median', 'max', 'log' and 'loglog'. distribution_estimation : callable, optional (defaults to the uniform) The kind of distribution estimation to employ out_of_sample_map : string, optional (default="continuous") The kind of out of sample mapping to apply to transport samples from a domain into another one. Currently the only possible option is "ferradans" which uses the nearest neighbor method proposed in :ref:`[6] ` while "continuous" use the out of sample method from :ref:`[66] ` and :ref:`[19] `. limit_max: float, optional (default=np.inf) Controls the semi supervised mode. Transport between labeled source and target samples of different classes will exhibit an cost defined by this variable Attributes ---------- coupling_ : array-like, shape (n_source_samples, n_target_samples) The optimal coupling log_ : dictionary The dictionary of log, empty dict if parameter log is not True .. _references-sinkhorntransport: References ---------- .. [1] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, "Optimal Transport for Domain Adaptation," in IEEE Transactions on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [6] Ferradans, S., Papadakis, N., Peyré, G., & Aujol, J. F. (2014). Regularized discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. .. [19] Seguy, V., Bhushan Damodaran, B., Flamary, R., Courty, N., Rolet, A. & Blondel, M. Large-scale Optimal Transport and Mapping Estimation. International Conference on Learning Representation (2018) .. [66] Pooladian, Aram-Alexandre, and Jonathan Niles-Weed. "Entropic estimation of optimal transport maps." arXiv preprint arXiv:2109.12004 (2021). """ def __init__( self, reg_e=1.0, method="sinkhorn_log", max_iter=1000, tol=10e-9, verbose=False, log=False, metric="sqeuclidean", norm=None, distribution_estimation=distribution_estimation_uniform, out_of_sample_map="continuous", limit_max=np.inf, ): if out_of_sample_map not in ["ferradans", "continuous"]: raise ValueError("Unknown out_of_sample_map method") self.reg_e = reg_e self.method = method self.max_iter = max_iter self.tol = tol self.verbose = verbose self.log = log self.metric = metric self.norm = norm self.limit_max = limit_max self.distribution_estimation = distribution_estimation self.out_of_sample_map = out_of_sample_map def fit(self, Xs=None, ys=None, Xt=None, yt=None): r"""Build a coupling matrix from source and target sets of samples :math:`(\mathbf{X_s}, \mathbf{y_s})` and :math:`(\mathbf{X_t}, \mathbf{y_t})` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. ys : array-like, shape (n_source_samples,) The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label Returns ------- self : object Returns self. """ super(SinkhornTransport, self).fit(Xs, ys, Xt, yt) if self.out_of_sample_map == "continuous": self.log = True if not self.method == "sinkhorn_log": self.method = "sinkhorn_log" warnings.warn( "The method has been set to 'sinkhorn_log' as it is the only method available for out_of_sample_map='continuous'" ) # coupling estimation returned_ = sinkhorn( a=self.mu_s, b=self.mu_t, M=self.cost_, reg=self.reg_e, method=self.method, numItermax=self.max_iter, stopThr=self.tol, verbose=self.verbose, log=self.log, ) # deal with the value of log if self.log: self.coupling_, self.log_ = returned_ else: self.coupling_ = returned_ self.log_ = dict() return self def transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): r"""Transports source samples :math:`\mathbf{X_s}` onto target ones :math:`\mathbf{X_t}` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The source input samples. ys : array-like, shape (n_source_samples,) The class labels for source samples Xt : array-like, shape (n_target_samples, n_features) The target input samples. yt : array-like, shape (n_target_samples,) The class labels for target. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label batch_size : int, optional (default=128) The batch size for out of sample inverse transform Returns ------- transp_Xs : array-like, shape (n_source_samples, n_features) The transport source samples. """ nx = self.nx if self.out_of_sample_map == "ferradans": return super(SinkhornTransport, self).transform(Xs, ys, Xt, yt, batch_size) else: # self.out_of_sample_map == 'continuous': # check the necessary inputs parameters are here g = self.log_["log_v"] indices = nx.arange(Xs.shape[0]) batch_ind = [ indices[i : i + batch_size] for i in range(0, len(indices), batch_size) ] transp_Xs = [] for bi in batch_ind: # get the nearest neighbor in the source domain M = dist(Xs[bi], self.xt_, metric=self.metric) M = cost_normalization(M, self.norm, value=self.norm_cost_) K = nx.exp(-M / self.reg_e + g[None, :]) transp_Xs_ = nx.dot(K, self.xt_) / nx.sum(K, axis=1)[:, None] transp_Xs.append(transp_Xs_) transp_Xs = nx.concatenate(transp_Xs, axis=0) return transp_Xs def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): r"""Transports target samples :math:`\mathbf{X_t}` onto source samples :math:`\mathbf{X_s}` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The source input samples. ys : array-like, shape (n_source_samples,) The class labels for source samples Xt : array-like, shape (n_target_samples, n_features) The target input samples. yt : array-like, shape (n_target_samples,) The class labels for target. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label batch_size : int, optional (default=128) The batch size for out of sample inverse transform Returns ------- transp_Xt : array-like, shape (n_source_samples, n_features) The transport target samples. """ nx = self.nx if self.out_of_sample_map == "ferradans": return super(SinkhornTransport, self).inverse_transform( Xs, ys, Xt, yt, batch_size ) else: # self.out_of_sample_map == 'continuous': f = self.log_["log_u"] indices = nx.arange(Xt.shape[0]) batch_ind = [ indices[i : i + batch_size] for i in range(0, len(indices), batch_size) ] transp_Xt = [] for bi in batch_ind: M = dist(Xt[bi], self.xs_, metric=self.metric) M = cost_normalization(M, self.norm, value=self.norm_cost_) K = nx.exp(-M / self.reg_e + f[None, :]) transp_Xt_ = nx.dot(K, self.xs_) / nx.sum(K, axis=1)[:, None] transp_Xt.append(transp_Xt_) transp_Xt = nx.concatenate(transp_Xt, axis=0) return transp_Xt class EMDTransport(BaseTransport): """Domain Adaptation OT method based on Earth Mover's Distance Parameters ---------- metric : string, optional (default="sqeuclidean") The ground metric for the Wasserstein problem norm : string, optional (default=None) If given, normalize the ground metric to avoid numerical errors that can occur with large metric values. log : int, optional (default=False) Controls the logs of the optimization algorithm distribution_estimation : callable, optional (defaults to the uniform) The kind of distribution estimation to employ out_of_sample_map : string, optional (default="ferradans") The kind of out of sample mapping to apply to transport samples from a domain into another one. Currently the only possible option is "ferradans" which uses the method proposed in :ref:`[6] `. limit_max: float, optional (default=10) Controls the semi supervised mode. Transport between labeled source and target samples of different classes will exhibit an infinite cost (10 times the maximum value of the cost matrix) max_iter : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. Attributes ---------- coupling_ : array-like, shape (n_source_samples, n_target_samples) The optimal coupling .. _references-emdtransport: References ---------- .. [1] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, "Optimal Transport for Domain Adaptation," in IEEE Transactions on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 .. [6] Ferradans, S., Papadakis, N., Peyré, G., & Aujol, J. F. (2014). Regularized discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. """ def __init__( self, metric="sqeuclidean", norm=None, log=False, distribution_estimation=distribution_estimation_uniform, out_of_sample_map="ferradans", limit_max=10, max_iter=100000, ): self.metric = metric self.norm = norm self.log = log self.limit_max = limit_max self.distribution_estimation = distribution_estimation self.out_of_sample_map = out_of_sample_map self.max_iter = max_iter def fit(self, Xs, ys=None, Xt=None, yt=None): r"""Build a coupling matrix from source and target sets of samples :math:`(\mathbf{X_s}, \mathbf{y_s})` and :math:`(\mathbf{X_t}, \mathbf{y_t})` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. ys : array-like, shape (n_source_samples,) The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label Returns ------- self : object Returns self. """ super(EMDTransport, self).fit(Xs, ys, Xt, yt) returned_ = emd( a=self.mu_s, b=self.mu_t, M=self.cost_, numItermax=self.max_iter, log=self.log, ) # coupling estimation if self.log: self.coupling_, self.log_ = returned_ else: self.coupling_ = returned_ self.log_ = dict() return self class SinkhornLpl1Transport(BaseTransport): r"""Domain Adaptation OT method based on sinkhorn algorithm + LpL1 class regularization. Parameters ---------- reg_e : float, optional (default=1) Entropic regularization parameter reg_cl : float, optional (default=0.1) Class regularization parameter max_iter : int, float, optional (default=10) The minimum number of iteration before stopping the optimization algorithm if it has not converged max_inner_iter : int, float, optional (default=200) The number of iteration in the inner loop log : bool, optional (default=False) Controls the logs of the optimization algorithm tol : float, optional (default=10e-9) Stop threshold on error (inner sinkhorn solver) (>0) verbose : bool, optional (default=False) Controls the verbosity of the optimization algorithm metric : string, optional (default="sqeuclidean") The ground metric for the Wasserstein problem norm : string, optional (default=None) If given, normalize the ground metric to avoid numerical errors that can occur with large metric values. distribution_estimation : callable, optional (defaults to the uniform) The kind of distribution estimation to employ out_of_sample_map : string, optional (default="ferradans") The kind of out of sample mapping to apply to transport samples from a domain into another one. Currently the only possible option is "ferradans" which uses the method proposed in :ref:`[6] `. limit_max: float, optional (default=np.inf) Controls the semi supervised mode. Transport between labeled source and target samples of different classes will exhibit a cost defined by limit_max. Attributes ---------- coupling_ : array-like, shape (n_source_samples, n_target_samples) The optimal coupling .. _references-sinkhornlpl1transport: References ---------- .. [1] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, "Optimal Transport for Domain Adaptation," in IEEE Transactions on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 .. [2] Rakotomamonjy, A., Flamary, R., & Courty, N. (2015). Generalized conditional gradient: analysis of convergence and applications. arXiv preprint arXiv:1510.06567. .. [6] Ferradans, S., Papadakis, N., Peyré, G., & Aujol, J. F. (2014). Regularized discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. """ def __init__( self, reg_e=1.0, reg_cl=0.1, max_iter=10, max_inner_iter=200, log=False, tol=10e-9, verbose=False, metric="sqeuclidean", norm=None, distribution_estimation=distribution_estimation_uniform, out_of_sample_map="ferradans", limit_max=np.inf, ): self.reg_e = reg_e self.reg_cl = reg_cl self.max_iter = max_iter self.max_inner_iter = max_inner_iter self.tol = tol self.log = log self.verbose = verbose self.metric = metric self.norm = norm self.distribution_estimation = distribution_estimation self.out_of_sample_map = out_of_sample_map self.limit_max = limit_max def fit(self, Xs, ys=None, Xt=None, yt=None): r"""Build a coupling matrix from source and target sets of samples :math:`(\mathbf{X_s}, \mathbf{y_s})` and :math:`(\mathbf{X_t}, \mathbf{y_t})` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. ys : array-like, shape (n_source_samples,) The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label Returns ------- self : object Returns self. """ # check the necessary inputs parameters are here if check_params(Xs=Xs, Xt=Xt, ys=ys): super(SinkhornLpl1Transport, self).fit(Xs, ys, Xt, yt) returned_ = sinkhorn_lpl1_mm( a=self.mu_s, labels_a=ys, b=self.mu_t, M=self.cost_, reg=self.reg_e, eta=self.reg_cl, numItermax=self.max_iter, numInnerItermax=self.max_inner_iter, stopInnerThr=self.tol, verbose=self.verbose, log=self.log, ) # deal with the value of log if self.log: self.coupling_, self.log_ = returned_ else: self.coupling_ = returned_ self.log_ = dict() return self class EMDLaplaceTransport(BaseTransport): """Domain Adaptation OT method based on Earth Mover's Distance with Laplacian regularization Parameters ---------- reg_type : string optional (default='pos') Type of the regularization term: 'pos' and 'disp' for regularization term defined in :ref:`[2] ` and :ref:`[6] `, respectively. reg_lap : float, optional (default=1) Laplacian regularization parameter reg_src : float, optional (default=0.5) Source relative importance in regularization metric : string, optional (default="sqeuclidean") The ground metric for the Wasserstein problem norm : string, optional (default=None) If given, normalize the ground metric to avoid numerical errors that can occur with large metric values. similarity : string, optional (default="knn") The similarity to use either knn or gaussian similarity_param : int or float, optional (default=None) Parameter for the similarity: number of nearest neighbors or bandwidth if similarity="knn" or "gaussian", respectively. If None is provided, it is set to 3 or the average pairwise squared Euclidean distance, respectively. max_iter : int, optional (default=100) Max number of BCD iterations tol : float, optional (default=1e-5) Stop threshold on relative loss decrease (>0) max_inner_iter : int, optional (default=10) Max number of iterations (inner CG solver) inner_tol : float, optional (default=1e-6) Stop threshold on error (inner CG solver) (>0) log : int, optional (default=False) Controls the logs of the optimization algorithm distribution_estimation : callable, optional (defaults to the uniform) The kind of distribution estimation to employ out_of_sample_map : string, optional (default="ferradans") The kind of out of sample mapping to apply to transport samples from a domain into another one. Currently the only possible option is "ferradans" which uses the method proposed in :ref:`[6] `. Attributes ---------- coupling_ : array-like, shape (n_source_samples, n_target_samples) The optimal coupling .. _references-emdlaplacetransport: References ---------- .. [1] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, "Optimal Transport for Domain Adaptation," in IEEE Transactions on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 .. [2] R. Flamary, N. Courty, D. Tuia, A. Rakotomamonjy, "Optimal transport with Laplacian regularization: Applications to domain adaptation and shape matching," in NIPS Workshop on Optimal Transport and Machine Learning OTML, 2014. .. [6] Ferradans, S., Papadakis, N., Peyré, G., & Aujol, J. F. (2014). Regularized discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. """ def __init__( self, reg_type="pos", reg_lap=1.0, reg_src=1.0, metric="sqeuclidean", norm=None, similarity="knn", similarity_param=None, max_iter=100, tol=1e-9, max_inner_iter=100000, inner_tol=1e-9, log=False, verbose=False, distribution_estimation=distribution_estimation_uniform, out_of_sample_map="ferradans", ): self.reg = reg_type self.reg_lap = reg_lap self.reg_src = reg_src self.metric = metric self.norm = norm self.similarity = similarity self.sim_param = similarity_param self.max_iter = max_iter self.tol = tol self.max_inner_iter = max_inner_iter self.inner_tol = inner_tol self.log = log self.verbose = verbose self.distribution_estimation = distribution_estimation self.out_of_sample_map = out_of_sample_map def fit(self, Xs, ys=None, Xt=None, yt=None): r"""Build a coupling matrix from source and target sets of samples :math:`(\mathbf{X_s}, \mathbf{y_s})` and :math:`(\mathbf{X_t}, \mathbf{y_t})` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. ys : array-like, shape (n_source_samples,) The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label Returns ------- self : object Returns self. """ super(EMDLaplaceTransport, self).fit(Xs, ys, Xt, yt) returned_ = emd_laplace( a=self.mu_s, b=self.mu_t, xs=self.xs_, xt=self.xt_, M=self.cost_, sim=self.similarity, sim_param=self.sim_param, reg=self.reg, eta=self.reg_lap, alpha=self.reg_src, numItermax=self.max_iter, stopThr=self.tol, numInnerItermax=self.max_inner_iter, stopInnerThr=self.inner_tol, log=self.log, verbose=self.verbose, ) # coupling estimation if self.log: self.coupling_, self.log_ = returned_ else: self.coupling_ = returned_ self.log_ = dict() return self class SinkhornL1l2Transport(BaseTransport): """Domain Adaptation OT method based on sinkhorn algorithm + L1L2 class regularization. Parameters ---------- reg_e : float, optional (default=1) Entropic regularization parameter reg_cl : float, optional (default=0.1) Class regularization parameter max_iter : int, float, optional (default=10) The minimum number of iteration before stopping the optimization algorithm if it has not converged max_inner_iter : int, float, optional (default=200) The number of iteration in the inner loop tol : float, optional (default=10e-9) Stop threshold on error (inner sinkhorn solver) (>0) verbose : bool, optional (default=False) Controls the verbosity of the optimization algorithm log : bool, optional (default=False) Controls the logs of the optimization algorithm metric : string, optional (default="sqeuclidean") The ground metric for the Wasserstein problem norm : string, optional (default=None) If given, normalize the ground metric to avoid numerical errors that can occur with large metric values. distribution_estimation : callable, optional (defaults to the uniform) The kind of distribution estimation to employ out_of_sample_map : string, optional (default="ferradans") The kind of out of sample mapping to apply to transport samples from a domain into another one. Currently the only possible option is "ferradans" which uses the method proposed in :ref:`[6] `. limit_max: float, optional (default=10) Controls the semi supervised mode. Transport between labeled source and target samples of different classes will exhibit an infinite cost (10 times the maximum value of the cost matrix) Attributes ---------- coupling_ : array-like, shape (n_source_samples, n_target_samples) The optimal coupling log_ : dictionary The dictionary of log, empty dict if parameter log is not True .. _references-sinkhornl1l2transport: References ---------- .. [1] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, "Optimal Transport for Domain Adaptation," in IEEE Transactions on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 .. [2] Rakotomamonjy, A., Flamary, R., & Courty, N. (2015). Generalized conditional gradient: analysis of convergence and applications. arXiv preprint arXiv:1510.06567. .. [6] Ferradans, S., Papadakis, N., Peyré, G., & Aujol, J. F. (2014). Regularized discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. """ def __init__( self, reg_e=1.0, reg_cl=0.1, max_iter=10, max_inner_iter=200, tol=10e-9, verbose=False, log=False, metric="sqeuclidean", norm=None, distribution_estimation=distribution_estimation_uniform, out_of_sample_map="ferradans", limit_max=10, ): self.reg_e = reg_e self.reg_cl = reg_cl self.max_iter = max_iter self.max_inner_iter = max_inner_iter self.tol = tol self.verbose = verbose self.log = log self.metric = metric self.norm = norm self.distribution_estimation = distribution_estimation self.out_of_sample_map = out_of_sample_map self.limit_max = limit_max def fit(self, Xs, ys=None, Xt=None, yt=None): r"""Build a coupling matrix from source and target sets of samples :math:`(\mathbf{X_s}, \mathbf{y_s})` and :math:`(\mathbf{X_t}, \mathbf{y_t})` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. ys : array-like, shape (n_source_samples,) The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label Returns ------- self : object Returns self. """ # check the necessary inputs parameters are here if check_params(Xs=Xs, Xt=Xt, ys=ys): super(SinkhornL1l2Transport, self).fit(Xs, ys, Xt, yt) returned_ = sinkhorn_l1l2_gl( a=self.mu_s, labels_a=ys, b=self.mu_t, M=self.cost_, reg=self.reg_e, eta=self.reg_cl, numItermax=self.max_iter, numInnerItermax=self.max_inner_iter, stopInnerThr=self.tol, verbose=self.verbose, log=self.log, ) # deal with the value of log if self.log: self.coupling_, self.log_ = returned_ else: self.coupling_ = returned_ self.log_ = dict() return self class MappingTransport(BaseEstimator): """MappingTransport: DA methods that aims at jointly estimating a optimal transport coupling and the associated mapping Parameters ---------- mu : float, optional (default=1) Weight for the linear OT loss (>0) eta : float, optional (default=0.001) Regularization term for the linear mapping `L` (>0) bias : bool, optional (default=False) Estimate linear mapping with constant bias metric : string, optional (default="sqeuclidean") The ground metric for the Wasserstein problem norm : string, optional (default=None) If given, normalize the ground metric to avoid numerical errors that can occur with large metric values. kernel : string, optional (default="linear") The kernel to use either linear or gaussian sigma : float, optional (default=1) The gaussian kernel parameter max_iter : int, optional (default=100) Max number of BCD iterations tol : float, optional (default=1e-5) Stop threshold on relative loss decrease (>0) max_inner_iter : int, optional (default=10) Max number of iterations (inner CG solver) inner_tol : float, optional (default=1e-6) Stop threshold on error (inner CG solver) (>0) log : bool, optional (default=False) record log if True verbose : bool, optional (default=False) Print information along iterations verbose2 : bool, optional (default=False) Print information along iterations Attributes ---------- coupling_ : array-like, shape (n_source_samples, n_target_samples) The optimal coupling mapping_ : The associated mapping - array-like, shape (`n_features` (+ 1), `n_features`), (if bias) for kernel == linear - array-like, shape (`n_source_samples` (+ 1), `n_features`), (if bias) for kernel == gaussian log_ : dictionary The dictionary of log, empty dict if parameter log is not True References ---------- .. [8] M. Perrot, N. Courty, R. Flamary, A. Habrard, "Mapping estimation for discrete optimal transport", Neural Information Processing Systems (NIPS), 2016. """ def __init__( self, mu=1, eta=0.001, bias=False, metric="sqeuclidean", norm=None, kernel="linear", sigma=1, max_iter=100, tol=1e-5, max_inner_iter=10, inner_tol=1e-6, log=False, verbose=False, verbose2=False, ): self.metric = metric self.norm = norm self.mu = mu self.eta = eta self.bias = bias self.kernel = kernel self.sigma = sigma self.max_iter = max_iter self.tol = tol self.max_inner_iter = max_inner_iter self.inner_tol = inner_tol self.log = log self.verbose = verbose self.verbose2 = verbose2 def fit(self, Xs=None, ys=None, Xt=None, yt=None): r"""Builds an optimal coupling and estimates the associated mapping from source and target sets of samples :math:`(\mathbf{X_s}, \mathbf{y_s})` and :math:`(\mathbf{X_t}, \mathbf{y_t})` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. ys : array-like, shape (n_source_samples,) The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label Returns ------- self : object Returns self """ self._get_backend(Xs, ys, Xt, yt) # check the necessary inputs parameters are here if check_params(Xs=Xs, Xt=Xt): self.xs_ = Xs self.xt_ = Xt if self.kernel == "linear": returned_ = joint_OT_mapping_linear( Xs, Xt, mu=self.mu, eta=self.eta, bias=self.bias, verbose=self.verbose, verbose2=self.verbose2, numItermax=self.max_iter, numInnerItermax=self.max_inner_iter, stopThr=self.tol, stopInnerThr=self.inner_tol, log=self.log, ) elif self.kernel == "gaussian": returned_ = joint_OT_mapping_kernel( Xs, Xt, mu=self.mu, eta=self.eta, bias=self.bias, sigma=self.sigma, verbose=self.verbose, verbose2=self.verbose, numItermax=self.max_iter, numInnerItermax=self.max_inner_iter, stopInnerThr=self.inner_tol, stopThr=self.tol, log=self.log, ) # deal with the value of log if self.log: self.coupling_, self.mapping_, self.log_ = returned_ else: self.coupling_, self.mapping_ = returned_ self.log_ = dict() return self def transform(self, Xs): r"""Transports source samples :math:`\mathbf{X_s}` onto target ones :math:`\mathbf{X_t}` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. Returns ------- transp_Xs : array-like, shape (n_source_samples, n_features) The transport source samples. """ nx = self.nx # check the necessary inputs parameters are here if check_params(Xs=Xs): if nx.array_equal(self.xs_, Xs): # perform standard barycentric mapping transp = self.coupling_ / nx.sum(self.coupling_, 1)[:, None] # set nans to 0 transp = nx.nan_to_num(transp, nan=0, posinf=0, neginf=0) # compute transported samples transp_Xs = nx.dot(transp, self.xt_) else: if self.kernel == "gaussian": K = kernel(Xs, self.xs_, method=self.kernel, sigma=self.sigma) elif self.kernel == "linear": K = Xs if self.bias: K = nx.concatenate( [K, nx.ones((Xs.shape[0], 1), type_as=K)], axis=1 ) transp_Xs = nx.dot(K, self.mapping_) return transp_Xs class UnbalancedSinkhornTransport(BaseTransport): """Domain Adaptation unbalanced OT method based on sinkhorn algorithm Parameters ---------- reg_e : float, optional (default=1) Entropic regularization parameter reg_m : float, optional (default=0.1) Mass regularization parameter method : str method used for the solver either 'sinkhorn', 'sinkhorn_stabilized' or 'sinkhorn_epsilon_scaling', see those function for specific parameters max_iter : int, float, optional (default=10) The minimum number of iteration before stopping the optimization algorithm if it has not converged tol : float, optional (default=10e-9) Stop threshold on error (inner sinkhorn solver) (>0) verbose : bool, optional (default=False) Controls the verbosity of the optimization algorithm log : bool, optional (default=False) Controls the logs of the optimization algorithm metric : string, optional (default="sqeuclidean") The ground metric for the Wasserstein problem norm : string, optional (default=None) If given, normalize the ground metric to avoid numerical errors that can occur with large metric values. distribution_estimation : callable, optional (defaults to the uniform) The kind of distribution estimation to employ out_of_sample_map : string, optional (default="ferradans") The kind of out of sample mapping to apply to transport samples from a domain into another one. Currently the only possible option is "ferradans" which uses the method proposed in :ref:`[6] `. limit_max: float, optional (default=10) Controls the semi supervised mode. Transport between labeled source and target samples of different classes will exhibit an infinite cost (10 times the maximum value of the cost matrix) Attributes ---------- coupling_ : array-like, shape (n_source_samples, n_target_samples) The optimal coupling log_ : dictionary The dictionary of log, empty dict if parameter log is not True .. _references-unbalancedsinkhorntransport: References ---------- .. [1] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. .. [6] Ferradans, S., Papadakis, N., Peyré, G., & Aujol, J. F. (2014). Regularized discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. """ def __init__( self, reg_e=1.0, reg_m=0.1, method="sinkhorn", max_iter=10, tol=1e-9, verbose=False, log=False, metric="sqeuclidean", norm=None, distribution_estimation=distribution_estimation_uniform, out_of_sample_map="ferradans", limit_max=10, ): self.reg_e = reg_e self.reg_m = reg_m self.method = method self.max_iter = max_iter self.tol = tol self.verbose = verbose self.log = log self.metric = metric self.norm = norm self.distribution_estimation = distribution_estimation self.out_of_sample_map = out_of_sample_map self.limit_max = limit_max def fit(self, Xs, ys=None, Xt=None, yt=None): r"""Build a coupling matrix from source and target sets of samples :math:`(\mathbf{X_s}, \mathbf{y_s})` and :math:`(\mathbf{X_t}, \mathbf{y_t})` Parameters ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. ys : array-like, shape (n_source_samples,) The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label Returns ------- self : object Returns self. """ # check the necessary inputs parameters are here if check_params(Xs=Xs, Xt=Xt): super(UnbalancedSinkhornTransport, self).fit(Xs, ys, Xt, yt) returned_ = sinkhorn_unbalanced( a=self.mu_s, b=self.mu_t, M=self.cost_, reg=self.reg_e, reg_m=self.reg_m, method=self.method, numItermax=self.max_iter, stopThr=self.tol, verbose=self.verbose, log=self.log, ) # deal with the value of log if self.log: self.coupling_, self.log_ = returned_ else: self.coupling_ = returned_ self.log_ = dict() return self class JCPOTTransport(BaseTransport): """Domain Adaptation OT method for multi-source target shift based on Wasserstein barycenter algorithm. Parameters ---------- reg_e : float, optional (default=1) Entropic regularization parameter max_iter : int, float, optional (default=10) The minimum number of iteration before stopping the optimization algorithm if it has not converged tol : float, optional (default=10e-9) Stop threshold on error (inner sinkhorn solver) (>0) verbose : bool, optional (default=False) Controls the verbosity of the optimization algorithm log : bool, optional (default=False) Controls the logs of the optimization algorithm metric : string, optional (default="sqeuclidean") The ground metric for the Wasserstein problem norm : string, optional (default=None) If given, normalize the ground metric to avoid numerical errors that can occur with large metric values. distribution_estimation : callable, optional (defaults to the uniform) The kind of distribution estimation to employ out_of_sample_map : string, optional (default="ferradans") The kind of out of sample mapping to apply to transport samples from a domain into another one. Currently the only possible option is "ferradans" which uses the method proposed in :ref:`[6] `. Attributes ---------- coupling_ : list of array-like objects, shape K x (n_source_samples, n_target_samples) A set of optimal couplings between each source domain and the target domain proportions_ : array-like, shape (n_classes,) Estimated class proportions in the target domain log_ : dictionary The dictionary of log, empty dict if parameter log is not True .. _references-jcpottransport: References ---------- .. [1] Ievgen Redko, Nicolas Courty, Rémi Flamary, Devis Tuia "Optimal transport for multi-source domain adaptation under target shift", International Conference on Artificial Intelligence and Statistics (AISTATS), vol. 89, p.849-858, 2019. .. [6] Ferradans, S., Papadakis, N., Peyré, G., & Aujol, J. F. (2014). Regularized discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. """ def __init__( self, reg_e=0.1, max_iter=10, tol=10e-9, verbose=False, log=False, metric="sqeuclidean", out_of_sample_map="ferradans", ): self.reg_e = reg_e self.max_iter = max_iter self.tol = tol self.verbose = verbose self.log = log self.metric = metric self.out_of_sample_map = out_of_sample_map def fit(self, Xs, ys=None, Xt=None, yt=None): r"""Building coupling matrices from a list of source and target sets of samples :math:`(\mathbf{X_s}, \mathbf{y_s})` and :math:`(\mathbf{X_t}, \mathbf{y_t})` Parameters ---------- Xs : list of K array-like objects, shape K x (nk_source_samples, n_features) A list of the training input samples. ys : list of K array-like objects, shape K x (nk_source_samples,) A list of the class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label Returns ------- self : object Returns self. """ self._get_backend(*Xs, *ys, Xt, yt) # check the necessary inputs parameters are here if check_params(Xs=Xs, Xt=Xt, ys=ys): self.xs_ = Xs self.xt_ = Xt returned_ = jcpot_barycenter( Xs=Xs, Ys=ys, Xt=Xt, reg=self.reg_e, metric=self.metric, distrinumItermax=self.max_iter, stopThr=self.tol, verbose=self.verbose, log=True, ) self.coupling_ = returned_[1]["gamma"] # deal with the value of log if self.log: self.proportions_, self.log_ = returned_ else: self.proportions_ = returned_ self.log_ = dict() return self def transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): r"""Transports source samples :math:`\mathbf{X_s}` onto target ones :math:`\mathbf{X_t}` Parameters ---------- Xs : list of K array-like objects, shape K x (nk_source_samples, n_features) A list of the training input samples. ys : list of K array-like objects, shape K x (nk_source_samples,) A list of the class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) The class labels. If some target samples are unlabelled, fill the :math:`\mathbf{y_t}`'s elements with -1. Warning: Note that, due to this convention -1 cannot be used as a class label batch_size : int, optional (default=128) The batch size for out of sample inverse transform """ nx = self.nx transp_Xs = [] # check the necessary inputs parameters are here if check_params(Xs=Xs): if all([nx.allclose(x, y) for x, y in zip(self.xs_, Xs)]): # perform standard barycentric mapping for each source domain for coupling in self.coupling_: transp = coupling / nx.sum(coupling, 1)[:, None] # set nans to 0 transp = nx.nan_to_num(transp, nan=0, posinf=0, neginf=0) # compute transported samples transp_Xs.append(nx.dot(transp, self.xt_)) else: # perform out of sample mapping indices = nx.arange(Xs.shape[0]) batch_ind = [ indices[i : i + batch_size] for i in range(0, len(indices), batch_size) ] transp_Xs = [] for bi in batch_ind: transp_Xs_ = [] # get the nearest neighbor in the sources domains xs = nx.concatenate(self.xs_, axis=0) idx = nx.argmin(dist(Xs[bi], xs), axis=1) # transport the source samples for coupling in self.coupling_: transp = coupling / nx.sum(coupling, 1)[:, None] transp = nx.nan_to_num(transp, nan=0, posinf=0, neginf=0) transp_Xs_.append(nx.dot(transp, self.xt_)) transp_Xs_ = nx.concatenate(transp_Xs_, axis=0) # define the transported points transp_Xs_ = transp_Xs_[idx, :] + Xs[bi] - xs[idx, :] transp_Xs.append(transp_Xs_) transp_Xs = nx.concatenate(transp_Xs, axis=0) return transp_Xs def transform_labels(self, ys=None): r"""Propagate source labels :math:`\mathbf{y_s}` to obtain target labels as in :ref:`[27] ` Parameters ---------- ys : list of K array-like objects, shape K x (nk_source_samples,) A list of the class labels Returns ------- yt : array-like, shape (n_target_samples, nb_classes) Estimated soft target labels. .. _references-jcpottransport-transform-labels: References ---------- .. [27] Ievgen Redko, Nicolas Courty, Rémi Flamary, Devis Tuia "Optimal transport for multi-source domain adaptation under target shift", International Conference on Artificial Intelligence and Statistics (AISTATS), 2019. """ nx = self.nx # check the necessary inputs parameters are here if check_params(ys=ys): yt = nx.zeros( (len(nx.unique(nx.concatenate(ys))), self.xt_.shape[0]), type_as=ys[0] ) for i in range(len(ys)): ysTemp = label_normalization(ys[i]) classes = nx.unique(ysTemp) n = len(classes) ns = len(ysTemp) # perform label propagation transp = self.coupling_[i] / nx.sum(self.coupling_[i], 1)[:, None] # set nans to 0 transp = nx.nan_to_num(transp, nan=0, posinf=0, neginf=0) if self.log: D1 = self.log_["D1"][i] else: D1 = nx.zeros((n, ns), type_as=transp) for c in classes: D1[int(c), ysTemp == c] = 1 # compute propagated labels yt = yt + nx.dot(D1, transp) / len(ys) return yt.T def inverse_transform_labels(self, yt=None): r"""Propagate target labels :math:`\mathbf{y_t}` to obtain estimated source labels :math:`\mathbf{y_s}` Parameters ---------- yt : array-like, shape (n_target_samples,) The target class labels Returns ------- transp_ys : list of K array-like objects, shape K x (nk_source_samples, nb_classes) A list of estimated soft source labels """ nx = self.nx # check the necessary inputs parameters are here if check_params(yt=yt): transp_ys = [] ytTemp = label_normalization(yt) classes = nx.unique(ytTemp) n = len(classes) D1 = nx.zeros((n, len(ytTemp)), type_as=self.coupling_[0]) for c in classes: D1[int(c), ytTemp == c] = 1 for i in range(len(self.xs_)): # perform label propagation transp = self.coupling_[i] / nx.sum(self.coupling_[i], 1)[:, None] # set nans to 0 transp = nx.nan_to_num(transp, nan=0, posinf=0, neginf=0) # compute propagated labels transp_ys.append(nx.dot(D1, transp.T).T) return transp_ys class NearestBrenierPotential(BaseTransport): r""" Smooth Strongly Convex Nearest Brenier Potentials (SSNB) is a method from :ref:`[58]` that computes an l-strongly convex potential :math:`\varphi` with an L-Lipschitz gradient such that :math:`\nabla \varphi \# \mu \approx \nu`. This regularity can be enforced only on the components of a partition of the ambient space (encoded by point classes), which is a relaxation compared to imposing global regularity. SSNBs approach the target measure by solving the optimisation problem: .. math:: :nowrap: \begin{gather*} \varphi \in \text{argmin}_{\varphi \in \mathcal{F}}\ \text{W}_2(\nabla \varphi \#\mu_s, \mu_t), \end{gather*} where :math:`\mathcal{F}` is the space functions that are on every set :math:`E_k` l-strongly convex with an L-Lipschitz gradient, given :math:`(E_k)_{k \in [K]}` a partition of the ambient source space. The problem is solved on "fitting" source and target data via a convex Quadratically Constrained Quadratic Program, yielding the values :code:`phi` and the gradients :code:`G` at at the source points. The images of "new" source samples are then found by solving a (simpler) Quadratically Constrained Linear Program at each point, using the fitting "parameters" :code:`phi` and :code:`G`. We provide two possible images, which correspond to "lower" and "upper potentials" (:ref:`[59]`, Theorem 3.14). Each of these two images are optimal solutions of the SSNB problem, and can be used in practice. .. warning:: This function requires the CVXPY library .. warning:: Accepts any backend but will convert to Numpy then back to the backend. Parameters ---------- strongly_convex_constant : float, optional constant for the strong convexity of the input potential phi, defaults to 0.6 gradient_lipschitz_constant : float, optional constant for the Lipschitz property of the input gradient G, defaults to 1.4 its: int, optional number of iterations, defaults to 100 log : bool, optional record log if true seed: int or RandomState or None, optional Seed used for random number generator (for the initialisation in :code:`fit`. References ---------- .. [58] François-Pierre Paty, Alexandre d’Aspremont, and Marco Cuturi. Regularity as regularization: Smooth and strongly convex brenier potentials in optimal transport. In International Conference on Artificial Intelligence and Statistics, pages 1222–1232. PMLR, 2020. .. [59] Adrien B Taylor. Convex interpolation and performance estimation of first-order methods for convex optimization. PhD thesis, Catholic University of Louvain, Louvain-la-Neuve, Belgium, 2017. See Also -------- ot.mapping.nearest_brenier_potential_fit : Fitting the SSNB on source and target data ot.mapping.nearest_brenier_potential_predict_bounds : Predicting SSNB images on new source data """ def __init__( self, strongly_convex_constant=0.6, gradient_lipschitz_constant=1.4, log=False, its=100, seed=None, ): self.strongly_convex_constant = strongly_convex_constant self.gradient_lipschitz_constant = gradient_lipschitz_constant self.log = log self.its = its self.seed = seed self.fit_log, self.predict_log = None, None self.phi, self.G = None, None self.fit_Xs, self.fit_ys, self.fit_Xt = None, None, None def fit(self, Xs=None, ys=None, Xt=None, yt=None): r""" Fits the Smooth Strongly Convex Nearest Brenier Potential [58] to the source data :code:`Xs` to the target data :code:`Xt`, with the partition given by the (optional) labels :code:`ys`. Wrapper for :code:`ot.mapping.nearest_brenier_potential_fit`. .. warning:: This function requires the CVXPY library .. warning:: Accepts any backend but will convert to Numpy then back to the backend. Parameters ---------- Xs : array-like (n, d) source points used to compute the optimal values phi and G ys : array-like (n,), optional classes of the reference points, defaults to a single class Xt : array-like (n, d) values of the gradients at the reference points X yt : optional ignored. Returns ------- self : object Returns self. References ---------- .. [58] François-Pierre Paty, Alexandre d’Aspremont, and Marco Cuturi. Regularity as regularization: Smooth and strongly convex brenier potentials in optimal transport. In International Conference on Artificial Intelligence and Statistics, pages 1222–1232. PMLR, 2020. See Also -------- ot.mapping.nearest_brenier_potential_fit : Fitting the SSNB on source and target data """ self.fit_Xs, self.fit_ys, self.fit_Xt = Xs, ys, Xt returned = nearest_brenier_potential_fit( Xs, Xt, X_classes=ys, strongly_convex_constant=self.strongly_convex_constant, gradient_lipschitz_constant=self.gradient_lipschitz_constant, its=self.its, log=self.log, ) if self.log: self.phi, self.G, self.fit_log = returned else: self.phi, self.G = returned return self def transform(self, Xs, ys=None): r""" Computes the images of the new source samples :code:`Xs` of classes :code:`ys` by the fitted Smooth Strongly Convex Nearest Brenier Potentials (SSNB) :ref:`[58]`. The output is the images of two SSNB optimal maps, called 'lower' and 'upper' potentials (from :ref:`[59]`, Theorem 3.14). Wrapper for :code:`nearest_brenier_potential_predict_bounds`. .. warning:: This function requires the CVXPY library .. warning:: Accepts any backend but will convert to Numpy then back to the backend. Parameters ---------- Xs : array-like (m, d) input source points ys : : array_like (m,), optional classes of the input source points, defaults to a single class Returns ------- G_lu : array-like (2, m, d) gradients of the lower and upper bounding potentials at Y (images of the source inputs) References ---------- .. [58] François-Pierre Paty, Alexandre d’Aspremont, and Marco Cuturi. Regularity as regularization: Smooth and strongly convex brenier potentials in optimal transport. In International Conference on Artificial Intelligence and Statistics, pages 1222–1232. PMLR, 2020. .. [59] Adrien B Taylor. Convex interpolation and performance estimation of first-order methods for convex optimization. PhD thesis, Catholic University of Louvain, Louvain-la-Neuve, Belgium, 2017. See Also -------- ot.mapping.nearest_brenier_potential_predict_bounds : Predicting SSNB images on new source data """ returned = nearest_brenier_potential_predict_bounds( self.fit_Xs, self.phi, self.G, Xs, X_classes=self.fit_ys, Y_classes=ys, strongly_convex_constant=self.strongly_convex_constant, gradient_lipschitz_constant=self.gradient_lipschitz_constant, log=self.log, ) if self.log: _, G_lu, self.predict_log = returned else: _, G_lu = returned return G_lu python-pot-0.9.7+dfsg/ot/datasets.py000066400000000000000000000203521525521106200173740ustar00rootroot00000000000000""" Simple example datasets """ # Author: Remi Flamary # # License: MIT License import numpy as np import scipy as sp from scipy.stats import ortho_group, multivariate_normal from .utils import check_random_state, deprecated def make_1D_gauss(n, m, s): """return a 1D histogram for a gaussian distribution (`n` bins, mean `m` and std `s`) Parameters ---------- n : int number of bins in the histogram m : float mean value of the gaussian distribution s : float standard deviation of the gaussian distribution Returns ------- h : ndarray (`n`,) 1D histogram for a gaussian distribution """ x = np.arange(n, dtype=np.float64) h = np.exp(-((x - m) ** 2) / (2 * s**2)) return h / h.sum() @deprecated() def get_1D_gauss(n, m, sigma): """Deprecated see make_1D_gauss""" return make_1D_gauss(n, m, sigma) def make_2D_samples_gauss(n, m, sigma, random_state=None): r"""Return `n` samples drawn from 2D gaussian :math:`\mathcal{N}(m, \sigma)` Parameters ---------- n : int number of samples to make m : ndarray, shape (2,) mean value of the gaussian distribution sigma : ndarray, shape (2, 2) covariance matrix of the gaussian distribution random_state : int, RandomState instance or None, optional (default=None) If int, random_state is the seed used by the random number generator; If RandomState instance, random_state is the random number generator; If None, the random number generator is the RandomState instance used by `np.random`. Returns ------- X : ndarray, shape (`n`, 2) n samples drawn from :math:`\mathcal{N}(m, \sigma)`. """ generator = check_random_state(random_state) if np.isscalar(sigma): sigma = np.array( [ sigma, ] ) if len(sigma) > 1: P = sp.linalg.sqrtm(sigma) res = generator.randn(n, 2).dot(P) + m else: res = generator.randn(n, 2) * np.sqrt(sigma) + m return res @deprecated() def get_2D_samples_gauss(n, m, sigma, random_state=None): """Deprecated see make_2D_samples_gauss""" return make_2D_samples_gauss(n, m, sigma, random_state=None) def make_data_classif(dataset, n, nz=0.5, theta=0, p=0.5, random_state=None, **kwargs): """Dataset generation for classification problems Parameters ---------- dataset : str type of classification problem (see code) n : int number of training samples nz : float noise level (>0) p : float proportion of one class in the binary setting random_state : int, RandomState instance or None, optional (default=None) If int, random_state is the seed used by the random number generator; If RandomState instance, random_state is the random number generator; If None, the random number generator is the RandomState instance used by `np.random`. Returns ------- X : ndarray, shape (n, d) `n` observation of size `d` y : ndarray, shape (n,) labels of the samples. """ generator = check_random_state(random_state) if dataset.lower() == "3gauss": y = np.floor((np.arange(n) * 1.0 / n * 3)) + 1 x = np.zeros((n, 2)) # class 1 x[y == 1, 0] = -1.0 x[y == 1, 1] = -1.0 x[y == 2, 0] = -1.0 x[y == 2, 1] = 1.0 x[y == 3, 0] = 1.0 x[y == 3, 1] = 0 x[y != 3, :] += 1.5 * nz * generator.randn(sum(y != 3), 2) x[y == 3, :] += 2 * nz * generator.randn(sum(y == 3), 2) elif dataset.lower() == "3gauss2": y = np.floor((np.arange(n) * 1.0 / n * 3)) + 1 x = np.zeros((n, 2)) y[y == 4] = 3 # class 1 x[y == 1, 0] = -2.0 x[y == 1, 1] = -2.0 x[y == 2, 0] = -2.0 x[y == 2, 1] = 2.0 x[y == 3, 0] = 2.0 x[y == 3, 1] = 0 x[y != 3, :] += nz * generator.randn(sum(y != 3), 2) x[y == 3, :] += 2 * nz * generator.randn(sum(y == 3), 2) elif dataset.lower() == "gaussrot": rot = np.array( [[np.cos(theta), np.sin(theta)], [-np.sin(theta), np.cos(theta)]] ) m1 = np.array([-1, 1]) m2 = np.array([1, -1]) y = np.floor((np.arange(n) * 1.0 / n * 2)) + 1 n1 = np.sum(y == 1) n2 = np.sum(y == 2) x = np.zeros((n, 2)) x[y == 1, :] = make_2D_samples_gauss(n1, m1, nz, random_state=generator) x[y == 2, :] = make_2D_samples_gauss(n2, m2, nz, random_state=generator) x = x.dot(rot) elif dataset.lower() == "2gauss_prop": y = np.concatenate((np.ones(int(p * n)), np.zeros(int((1 - p) * n)))) x = np.hstack((0 * y[:, None] - 0, 1 - 2 * y[:, None])) + nz * generator.randn( len(y), 2 ) if ("bias" not in kwargs) and ("b" not in kwargs): kwargs["bias"] = np.array([0, 2]) x[:, 0] += kwargs["bias"][0] x[:, 1] += kwargs["bias"][1] else: x = np.array(0) y = np.array(0) print("unknown dataset") return x, y.astype(int) @deprecated() def get_data_classif(dataset, n, nz=0.5, theta=0, random_state=None, **kwargs): """Deprecated see make_data_classif""" return make_data_classif(dataset, n, nz=0.5, theta=0, random_state=None, **kwargs) def make_gauss_hd( ns, nt, p=100, dim=5, m_diff=3.0, a=(10.0, 15.0), b=(3.0, 3.0), sub_the_same=False ): """Generation of source and target domains from Gaussian HD distributions Parameters ---------- ns : int number of samples (source) nt : int number of samples (target) p : int dimension of the ambient space the data live in dim : (int,int) or int the intrinsic dimensions of the source and target Gaussian HD distriutions. If a single int the intrinsic dimension is assumed to be the same m_diff : float the shift in the first coordinate of the means of the Gaussian HD distributions, i.e. ms_0 and mt_0, respectively (see code) a : (float, float) positive floating numbers corresponding to the isotropic variances in the principal subspace, for the source and target distributions, respectively. The same as \delta in :ref:`[1] `, Proposition 2.2 b : (float, float) positive floating numbers corresponding to the isotropic variance outside the principal subspace for the source and target distributions, respectively. sub_the_same : bool should the source/target Gaussian HD distributions live in the same principal subspace? Returns ------- Xs : ndarray, shape (ns, p) `ns` observations of size `p` (source) Xt : ndarray, shape (nt, p) `nt` observations of size `p` (destination) pmts : list a list containing the parameters of the Gaussian HD distributions .. _references-make_gauss_hd: References ---------- .. [1] Bouveyron, C. & Corneli, M. ("Scaling Optimal Transport to High-Dimensional Gaussian Distributions") """ d = (dim, dim) if isinstance(dim, int) else dim mu = np.zeros((2, p)) S = [] mu[1, 0] = m_diff Q = [ortho_group.rvs(p) for _ in range(2)] if sub_the_same: Q[1] = Q[0] S.append( Q[0] @ np.diag(np.hstack((np.full(d[0], a[0]), np.full(p - d[0], b[0])))) @ Q[0].T ) S.append( Q[1] @ np.diag(np.hstack((np.full(d[1], a[1]), np.full(p - d[1], b[1])))) @ Q[1].T ) Xs = multivariate_normal.rvs(mean=mu[0], cov=S[0], size=ns) Xt = multivariate_normal.rvs(mean=mu[1], cov=S[1], size=ns) ms = mu[0] mt = mu[1] ds = d[0] dt = d[1] sigma2_s = np.array(b[0]) sigma2_t = np.array(b[1]) ls = np.repeat(a[0], ds) - sigma2_s lt = np.repeat(a[1], dt) - sigma2_t Us = Q[0][:, :ds] Ut = Q[1][:, :dt] ds = np.array([ds]) dt = np.array([dt]) prmts = { "ms": ms, "mt": mt, "sigma2_s": sigma2_s, "sigma2_t": sigma2_t, "ls": ls, "lt": lt, "Us": Us, "Ut": Ut, "ds": ds, "dt": dt, "Cs": S[0], "Ct": S[1], } return Xs, Xt, prmts python-pot-0.9.7+dfsg/ot/dr.py000066400000000000000000000413051525521106200161720ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Dimension reduction with OT .. warning:: Note that by default the module is not imported in :mod:`ot`. In order to use it you need to explicitly import :mod:`ot.dr` """ # Author: Remi Flamary # Minhui Huang # Jakub Zadrozny # Antoine Collas # # License: MIT License from scipy import linalg try: import autograd.numpy as np from sklearn.decomposition import PCA import pymanopt import pymanopt.manifolds import pymanopt.optimizers except ImportError: raise ImportError( "Missing dependency for ot.dr. Requires autograd, pymanopt, scikit-learn. You can install with install with 'pip install POT[dr]', or 'conda install autograd pymanopt scikit-learn'" ) from .bregman import sinkhorn as sinkhorn_bregman from .utils import dist as dist_utils, check_random_state def dist(x1, x2): r"""Compute squared euclidean distance between samples (autograd)""" x1p2 = np.sum(np.square(x1), 1) x2p2 = np.sum(np.square(x2), 1) return x1p2.reshape((-1, 1)) + x2p2.reshape((1, -1)) - 2 * np.dot(x1, x2.T) def sinkhorn(w1, w2, M, reg, k): r"""Sinkhorn algorithm with fixed number of iteration (autograd)""" K = np.exp(-M / reg) ui = np.ones((M.shape[0],)) vi = np.ones((M.shape[1],)) for i in range(k): vi = w2 / (np.dot(K.T, ui) + 1e-50) ui = w1 / (np.dot(K, vi) + 1e-50) G = ui.reshape((M.shape[0], 1)) * K * vi.reshape((1, M.shape[1])) return G def logsumexp(M, axis): r"""Log-sum-exp reduction compatible with autograd (no numpy implementation)""" amax = np.amax(M, axis=axis, keepdims=True) return np.log(np.sum(np.exp(M - amax), axis=axis)) + np.squeeze(amax, axis=axis) def sinkhorn_log(w1, w2, M, reg, k): r"""Sinkhorn algorithm in log-domain with fixed number of iteration (autograd)""" Mr = -M / reg ui = np.zeros((M.shape[0],)) vi = np.zeros((M.shape[1],)) log_w1 = np.log(w1) log_w2 = np.log(w2) for i in range(k): vi = log_w2 - logsumexp(Mr + ui[:, None], 0) ui = log_w1 - logsumexp(Mr + vi[None, :], 1) G = np.exp(ui[:, None] + Mr + vi[None, :]) return G def split_classes(X, y): r"""split samples in :math:`\mathbf{X}` by classes in :math:`\mathbf{y}`""" lstsclass = np.unique(y) return [X[y == i, :].astype(np.float32) for i in lstsclass] def fda(X, y, p=2, reg=1e-16): r"""Fisher Discriminant Analysis Parameters ---------- X : ndarray, shape (n, d) Training samples. y : ndarray, shape (n,) Labels for training samples. p : int, optional Size of dimensionality reduction. reg : float, optional Regularization term >0 (ridge regularization) Returns ------- P : ndarray, shape (d, p) Optimal transportation matrix for the given parameters proj : callable projection function including mean centering """ mx = np.mean(X) X -= mx.reshape((1, -1)) # data split between classes d = X.shape[1] xc = split_classes(X, y) nc = len(xc) p = min(nc - 1, p) Cw = 0 for x in xc: Cw += np.cov(x, rowvar=False) Cw /= nc mxc = np.zeros((d, nc)) for i in range(nc): mxc[:, i] = np.mean(xc[i]) mx0 = np.mean(mxc, 1) Cb = 0 for i in range(nc): Cb += (mxc[:, i] - mx0).reshape((-1, 1)) * (mxc[:, i] - mx0).reshape((1, -1)) w, V = linalg.eig(Cb, Cw + reg * np.eye(d)) idx = np.argsort(w.real) Popt = V[:, idx[-p:]] def proj(X): return (X - mx.reshape((1, -1))).dot(Popt) return Popt, proj def wda( X, y, p=2, reg=1, k=10, solver=None, sinkhorn_method="sinkhorn", maxiter=100, verbose=0, P0=None, normalize=False, ): r""" Wasserstein Discriminant Analysis :ref:`[11] ` The function solves the following optimization problem: .. math:: \mathbf{P} = \mathop{\arg \min}_\mathbf{P} \quad \frac{\sum\limits_i W(P \mathbf{X}^i, P \mathbf{X}^i)}{\sum\limits_{i, j \neq i} W(P \mathbf{X}^i, P \mathbf{X}^j)} where : - :math:`P` is a linear projection operator in the Stiefel(`p`, `d`) manifold - :math:`W` is entropic regularized Wasserstein distances - :math:`\mathbf{X}^i` are samples in the dataset corresponding to class i **Choosing a Sinkhorn solver** By default and when using a regularization parameter that is not too small the default sinkhorn solver should be enough. If you need to use a small regularization to get sparse cost matrices, you should use the :py:func:`ot.dr.sinkhorn_log` solver that will avoid numerical errors, but can be slow in practice. Parameters ---------- X : ndarray, shape (n, d) Training samples. y : ndarray, shape (n,) Labels for training samples. p : int, optional Size of dimensionality reduction. reg : float, optional Regularization term >0 (entropic regularization) solver : None | str, optional None for steepest descent or 'TrustRegions' for trust regions algorithm else should be a pymanopt.solvers sinkhorn_method : str method used for the Sinkhorn solver, either 'sinkhorn' or 'sinkhorn_log' P0 : ndarray, shape (d, p) Initial starting point for projection. normalize : bool, optional Normalize the Wasserstaiun distance by the average distance on P0 (default : False) verbose : int, optional Print information along iterations. Returns ------- P : ndarray, shape (d, p) Optimal transportation matrix for the given parameters proj : callable Projection function including mean centering. .. _references-wda: References ---------- .. [11] Flamary, R., Cuturi, M., Courty, N., & Rakotomamonjy, A. (2016). Wasserstein Discriminant Analysis. arXiv preprint arXiv:1608.08063. """ # noqa if sinkhorn_method.lower() == "sinkhorn": sinkhorn_solver = sinkhorn elif sinkhorn_method.lower() == "sinkhorn_log": sinkhorn_solver = sinkhorn_log else: raise ValueError("Unknown Sinkhorn method '%s'." % sinkhorn_method) mx = np.mean(X) X -= mx.reshape((1, -1)) # data split between classes d = X.shape[1] xc = split_classes(X, y) # compute uniform weighs wc = [np.ones((x.shape[0]), dtype=np.float32) / x.shape[0] for x in xc] # pre-compute reg_c,c' if P0 is not None and normalize: regmean = np.zeros((len(xc), len(xc))) for i, xi in enumerate(xc): xi = np.dot(xi, P0) for j, xj in enumerate(xc[i:]): xj = np.dot(xj, P0) M = dist(xi, xj) regmean[i, j] = np.sum(M) / (len(xi) * len(xj)) else: regmean = np.ones((len(xc), len(xc))) manifold = pymanopt.manifolds.Stiefel(d, p) @pymanopt.function.autograd(manifold) def cost(P): # wda loss loss_b = 0 loss_w = 0 for i, xi in enumerate(xc): xi = np.dot(xi, P) for j, xj in enumerate(xc[i:]): xj = np.dot(xj, P) M = dist(xi, xj) G = sinkhorn_solver(wc[i], wc[j + i], M, reg * regmean[i, j], k) if j == 0: loss_w += np.sum(G * M) else: loss_b += np.sum(G * M) # loss inversed because minimization return loss_w / loss_b # declare manifold and problem problem = pymanopt.Problem(manifold=manifold, cost=cost) # declare solver and solve if solver is None: solver = pymanopt.optimizers.SteepestDescent( max_iterations=maxiter, log_verbosity=verbose ) elif solver in ["tr", "TrustRegions"]: solver = pymanopt.optimizers.TrustRegions( max_iterations=maxiter, log_verbosity=verbose ) Popt = solver.run(problem, initial_point=P0) def proj(X): return (X - mx.reshape((1, -1))).dot(Popt.point) return Popt.point, proj def projection_robust_wasserstein( X, Y, a, b, tau, U0=None, reg=0.1, k=2, stopThr=1e-3, maxiter=100, verbose=0, random_state=None, ): r""" Projection Robust Wasserstein Distance :ref:`[32] ` The function solves the following optimization problem: .. math:: \max_{U \in St(d, k)} \ \min_{\pi \in \Pi(\mu,\nu)} \quad \sum_{i,j} \pi_{i,j} \|U^T(\mathbf{x}_i - \mathbf{y}_j)\|^2 - \mathrm{reg} \cdot H(\pi) - :math:`U` is a linear projection operator in the Stiefel(`d`, `k`) manifold - :math:`H(\pi)` is entropy regularizer - :math:`\mathbf{x}_i`, :math:`\mathbf{y}_j` are samples of measures :math:`\mu` and :math:`\nu` respectively Parameters ---------- X : ndarray, shape (n, d) Samples from measure :math:`\mu` Y : ndarray, shape (n, d) Samples from measure :math:`\nu` a : ndarray, shape (n, ) weights for measure :math:`\mu` b : ndarray, shape (n, ) weights for measure :math:`\nu` tau : float stepsize for Riemannian Gradient Descent U0 : ndarray, shape (d, p) Initial starting point for projection. reg : float, optional Regularization term >0 (entropic regularization) k : int Subspace dimension stopThr : float, optional Stop threshold on error (>0) verbose : int, optional Print information along iterations. random_state : int, RandomState instance or None, default=None Determines random number generation for initial value of projection operator when U0 is not given. Returns ------- pi : ndarray, shape (n, n) Optimal transportation matrix for the given parameters U : ndarray, shape (d, k) Projection operator. .. _references-projection-robust-wasserstein: References ---------- .. [32] Huang, M. , Ma S. & Lai L. (2021). A Riemannian Block Coordinate Descent Method for Computing the Projection Robust Wasserstein Distance, ICML. """ # noqa # initialization n, d = X.shape m, d = Y.shape a = np.asarray(a, dtype=np.float64) b = np.asarray(b, dtype=np.float64) u = np.ones(n) / n v = np.ones(m) / m ones = np.ones((n, m)) assert d > k if U0 is None: rng = check_random_state(random_state) U = rng.randn(d, k) U, _ = np.linalg.qr(U) else: U = U0 def Vpi(X, Y, a, b, pi): # Return the second order matrix of the displacements: sum_ij { (pi)_ij (X_i-Y_j)(X_i-Y_j)^T }. A = X.T.dot(pi).dot(Y) return ( X.T.dot(np.diag(a)).dot(X) + Y.T.dot(np.diag(np.sum(pi, 0))).dot(Y) - A - A.T ) err = 1 iter = 0 while err > stopThr and iter < maxiter: # Projected cost matrix UUT = U.dot(U.T) M = ( np.diag(np.diag(X.dot(UUT.dot(X.T)))).dot(ones) + ones.dot(np.diag(np.diag(Y.dot(UUT.dot(Y.T))))) - 2 * X.dot(UUT.dot(Y.T)) ) A = np.empty(M.shape, dtype=M.dtype) np.divide(M, -reg, out=A) np.exp(A, out=A) # Sinkhorn update Ap = (1 / a).reshape(-1, 1) * A AtransposeU = np.dot(A.T, u) v = np.divide(b, AtransposeU) u = 1.0 / np.dot(Ap, v) pi = u.reshape((-1, 1)) * A * v.reshape((1, -1)) V = Vpi(X, Y, a, b, pi) # Riemannian gradient descent G = 2 / reg * V.dot(U) GTU = G.T.dot(U) xi = G - U.dot(GTU + GTU.T) / 2 # Riemannian gradient U, _ = np.linalg.qr(U + tau * xi) # Retraction by QR decomposition grad_norm = np.linalg.norm(xi) err = max(reg * grad_norm, np.linalg.norm(np.sum(pi, 0) - b, 1)) f_val = np.trace(U.T.dot(V.dot(U))) if verbose: print("RBCD Iteration: ", iter, " error", err, "\t fval: ", f_val) iter = iter + 1 return pi, U def ewca( X, U0=None, reg=1, k=2, method="BCD", sinkhorn_method="sinkhorn", stopThr=1e-6, maxiter=100, maxiter_sink=1000, maxiter_MM=10, verbose=0, ): r""" Entropic Wasserstein Component Analysis :ref:`[52] `. The function solves the following optimization problem: .. math:: \mathbf{U} = \mathop{\arg \min}_\mathbf{U} \quad W(\mathbf{X}, \mathbf{U}\mathbf{U}^T \mathbf{X}) where : - :math:`\mathbf{U}` is a matrix in the Stiefel(`p`, `d`) manifold - :math:`W` is entropic regularized Wasserstein distances - :math:`\mathbf{X}` are samples Parameters ---------- X : ndarray, shape (n, d) Samples from measure :math:`\mu`. U0 : ndarray, shape (d, k), optional Initial starting point for projection. reg : float, optional Regularization term >0 (entropic regularization). k : int, optional Subspace dimension. method : str, optional Eather 'BCD' or 'MM' (Block Coordinate Descent or Majorization-Minimization). Prefer MM when d is large. sinkhorn_method : str Method used for the Sinkhorn solver, see :ref:`ot.bregman.sinkhorn` for more details. stopThr : float, optional Stop threshold on error (>0). maxiter : int, optional Maximum number of iterations of the BCD/MM. maxiter_sink : int, optional Maximum number of iterations of the Sinkhorn solver. maxiter_MM : int, optional Maximum number of iterations of the MM (only used when method='MM'). verbose : int, optional Print information along iterations. Returns ------- pi : ndarray, shape (n, n) Optimal transportation matrix for the given parameters. U : ndarray, shape (d, k) Matrix Stiefel manifold. .. _references-entropic-wasserstein-component_analysis: References ---------- .. [52] Collas, A., Vayer, T., Flamary, F., & Breloy, A. (2023). Entropic Wasserstein Component Analysis. """ # noqa n, d = X.shape X = X - X.mean(0) if U0 is None: pca_fitted = PCA(n_components=k).fit(X) U = pca_fitted.components_.T if method == "MM": lambda_scm = pca_fitted.explained_variance_[0] else: U = U0 # marginals u0 = (1.0 / n) * np.ones(n) # print iterations if verbose > 0: print( "{:4s}|{:13s}|{:12s}|{:12s}".format("It.", "Loss", "Crit.", "Thres.") + "\n" + "-" * 40 ) def compute_loss(M, pi, reg): return np.sum(M * pi) + reg * np.sum(pi * (np.log(pi) - 1)) def grassmann_distance(U1, U2): proj = U1.T @ U2 _, s, _ = np.linalg.svd(proj) s[s > 1] = 1 s = np.arccos(s) return np.linalg.norm(s) # loop it = 0 crit = np.inf sinkhorn_warmstart = None while (it < maxiter) and (crit > stopThr): U_old = U # Solve transport M = dist_utils(X, (X @ U) @ U.T) pi, log_sinkhorn = sinkhorn_bregman( u0, u0, M, reg, numItermax=maxiter_sink, method=sinkhorn_method, warmstart=sinkhorn_warmstart, warn=False, log=True, ) key_warmstart = "warmstart" if key_warmstart in log_sinkhorn: sinkhorn_warmstart = log_sinkhorn[key_warmstart] if (pi >= 1e-300).all(): loss = compute_loss(M, pi, reg) else: loss = np.inf # Solve PCA pi_sym = (pi + pi.T) / 2 if method == "BCD": # block coordinate descent S = X.T @ (2 * pi_sym - (1.0 / n) * np.eye(n)) @ X _, U = np.linalg.eigh(S) U = U[:, ::-1][:, :k] elif method == "MM": # majorization-minimization eig, _ = np.linalg.eigh(pi_sym) lambda_pi = eig[0] for _ in range(maxiter_MM): X_proj = X @ U X_T_X_proj = X.T @ X_proj R = (1 / n) * X_T_X_proj alpha = 1 - 2 * n * lambda_pi if alpha > 0: R = alpha * (R - lambda_scm * U) else: R = alpha * R R = R - (2 * X.T @ (pi_sym @ X_proj)) + (2 * lambda_pi * X_T_X_proj) U, _ = np.linalg.qr(R) else: raise ValueError(f"Unknown method '{method}', use 'BCD' or 'MM'.") # stop or not it += 1 crit = grassmann_distance(U_old, U) # print if verbose > 0: print("{:4d}|{:8e}|{:8e}|{:8e}".format(it, loss, crit, stopThr)) return pi, U python-pot-0.9.7+dfsg/ot/factored.py000066400000000000000000000102171525521106200173520ustar00rootroot00000000000000""" Factored OT solvers (low rank, cost or OT plan) """ # Author: Remi Flamary # # License: MIT License from .backend import get_backend from .utils import dist, get_lowrank_lazytensor from .lp import emd from .bregman import sinkhorn __all__ = ["factored_optimal_transport"] def factored_optimal_transport( Xa, Xb, a=None, b=None, reg=0.0, r=100, X0=None, stopThr=1e-7, numItermax=100, verbose=False, log=False, **kwargs, ): r"""Solves factored OT problem and return OT plans and intermediate distribution This function solve the following OT problem [40]_ .. math:: \mathop{\arg \min}_\mu \quad W_2^2(\mu_a,\mu)+ W_2^2(\mu,\mu_b) where : - :math:`\mu_a` and :math:`\mu_b` are empirical distributions. - :math:`\mu` is an empirical distribution with r samples And returns the two OT plans between .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. Uses the conditional gradient algorithm to solve the problem proposed in :ref:`[39] `. Parameters ---------- Xa : (ns,d) array-like, float Source samples Xb : (nt,d) array-like, float Target samples a : (ns,) array-like, float Source histogram (uniform weight if empty list) b : (nt,) array-like, float Target histogram (uniform weight if empty list)) numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on the relative variation (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- Ga: array-like, shape (ns, r) Optimal transportation matrix between source and the intermediate distribution Gb: array-like, shape (r, nt) Optimal transportation matrix between the intermediate and target distribution X: array-like, shape (r, d) Support of the intermediate distribution log: dict, optional If input log is true, a dictionary containing the cost and dual variables and exit status .. _references-factored: References ---------- .. [40] Forrow, A., Hütter, J. C., Nitzan, M., Rigollet, P., Schiebinger, G., & Weed, J. (2019, April). Statistical optimal transport via factored couplings. In The 22nd International Conference on Artificial Intelligence and Statistics (pp. 2454-2465). PMLR. See Also -------- ot.bregman.sinkhorn : Entropic regularized OT ot.optim.cg : General regularized OT """ nx = get_backend(Xa, Xb) n_a = Xa.shape[0] n_b = Xb.shape[0] d = Xa.shape[1] if a is None: a = nx.ones((n_a), type_as=Xa) / n_a if b is None: b = nx.ones((n_b), type_as=Xb) / n_b if X0 is None: X = nx.randn(r, d, type_as=Xa) else: X = X0 w = nx.ones(r, type_as=Xa) / r def solve_ot(X1, X2, w1, w2): M = dist(X1, X2) if reg > 0: G, log = sinkhorn(w1, w2, M, reg, log=True, **kwargs) log["cost"] = nx.sum(G * M) return G, log else: return emd(w1, w2, M, log=True, **kwargs) norm_delta = [] # solve the barycenter for i in range(numItermax): old_X = X # solve OT with template Ga, loga = solve_ot(Xa, X, a, w) Gb, logb = solve_ot(X, Xb, w, b) X = 0.5 * (nx.dot(Ga.T, Xa) + nx.dot(Gb, Xb)) * r delta = nx.norm(X - old_X) if delta < stopThr: break if log: norm_delta.append(delta) if log: log_dic = { "delta_iter": norm_delta, "ua": loga["u"], "va": loga["v"], "ub": logb["u"], "vb": logb["v"], "costa": loga["cost"], "costb": logb["cost"], "lazy_plan": get_lowrank_lazytensor(Ga * r, Gb.T, nx=nx), } return Ga, Gb, X, log_dic return Ga, Gb, X python-pot-0.9.7+dfsg/ot/gaussian.py000066400000000000000000001534631525521106200174100ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Optimal transport for Gaussian distributions """ # Author: Theo Gnassounou # Remi Flamary # # License: MIT License import warnings import numpy as np from .backend import get_backend from .utils import dots, is_all_finite, list_to_array, exp_bures def bures_wasserstein_mapping(ms, mt, Cs, Ct, log=False): r"""Return OT linear operator between samples. The function estimates the optimal linear operator that aligns the two empirical distributions. This is equivalent to estimating the closed form mapping between two Gaussian distributions :math:`\mathcal{N}(\mu_s,\Sigma_s)` and :math:`\mathcal{N}(\mu_t,\Sigma_t)` as proposed in :ref:`[1] ` and discussed in remark 2.29 in :ref:`[2] `. The linear operator from source to target :math:`M` .. math:: M(\mathbf{x})= \mathbf{A} \mathbf{x} + \mathbf{b} where : .. math:: \mathbf{A} &= \Sigma_s^{-1/2} \left(\Sigma_s^{1/2}\Sigma_t\Sigma_s^{1/2} \right)^{1/2} \Sigma_s^{-1/2} \mathbf{b} &= \mu_t - \mathbf{A} \mu_s Parameters ---------- ms : array-like (d,) mean of the source distribution mt : array-like (d,) mean of the target distribution Cs : array-like (d,d) covariance of the source distribution Ct : array-like (d,d) covariance of the target distribution log : bool, optional record log if True Returns ------- A : (d, d) array-like Linear operator b : (1, d) array-like bias log : dict log dictionary return only if log==True in parameters .. _references-OT-mapping-linear: References ---------- .. [1] Knott, M. and Smith, C. S. "On the optimal mapping of distributions", Journal of Optimization Theory and Applications Vol 43, 1984 .. [2] Peyré, G., & Cuturi, M. (2017). "Computational Optimal Transport", 2018. """ ms, mt, Cs, Ct = list_to_array(ms, mt, Cs, Ct) nx = get_backend(ms, mt, Cs, Ct) Cs12 = nx.sqrtm(Cs) Cs12inv = nx.inv(Cs12) M0 = nx.sqrtm(dots(Cs12, Ct, Cs12)) A = dots(Cs12inv, M0, Cs12inv) b = mt - nx.dot(ms, A) if log: log = {} log["Cs12"] = Cs12 log["Cs12inv"] = Cs12inv return A, b, log else: return A, b def bures_wasserstein_mapping_hd(ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t, log=False): r"""Return OT linear operator between HD Gaussian distritutions. The function estimates the optimal linear operator that aligns the two HD Gaussian distributions :math:`\mathcal{N}(\mu_s, U_s, l_s, \sigma_s^2, d_s)` and :math:`\mathcal{N}(\mu_t, U_t, l_t, \sigma_t^2, d_t)` as proposed in :ref:`[88] `, Th. 2.9 . The linear operator from source to target :math:`M` .. math:: M(\mathbf{x})= \mathbf{A} \mathbf{x} + \mathbf{b} where : .. math:: \mathbf{A} &= \Sigma_s^{-1/2} \left(\Sigma_s^{1/2}\Sigma_t\Sigma_s^{1/2} \right)^{1/2} \Sigma_s^{-1/2} \\ \Sigma_s^{1/2} &=\sigma_s I_p + U_s C_s U_s^T \\ C_s &=\text{diag}(\sqrt{l_{s1} + \sigma_s^2} - \sigma_s, \dots, \sqrt{l_{sd_s} + \sigma_s^2} - \sigma_s) \\ \Sigma_s^{-1/2} &= \frac{1}{\sigma_s} (I_p - U_s D_s U_s^T ) \\ D_s &= \text{diag}((\sqrt{l_{s1} + \sigma_s^2} - \sigma_s)/\sqrt{l_{s1} + \sigma_s^2}, \dots, (\sqrt{l_{sd_s} + \sigma_s^2} - \sigma_s)/\sqrt{l_{sd_s} + \sigma_s^2}) \\ \Sigma_t &= U_t \text{diag}(l_t) U_t^T + \sigma_t^2 I_p \\ \mathbf{b} &= \mu_t - \mathbf{A} \mu_s Parameters ---------- ms : array-like (p,) mean of the source distribution mt : array-like (p,) mean of the target distribution Us : array-like (p,ds) orthogonal matrix spanning the principal subspace of the source distribution Ut : array-like (p,dt) orthogonal matrix spanning the principal subspace of the target distribution ls : array-like (ds,) the variances associated with the principal sub-axes for the source distribution lt : array-like (dt,) the variances associated with the principal sub-axes for the target distribution sigma_s^2 : array-like (1,) the residual variance of the source distribution sigma_t^2 : array-like (1,) the residual variance of the target distribution log : bool, optional record log if True Returns ------- A : (d, d) array-like Linear operator b : (1, d) array-like bias log : dict log dictionary return only if log==True in parameters .. _references-OT-mapping-linear: References ---------- .. [1] Knott, M. and Smith, C. S. "On the optimal mapping of distributions", Journal of Optimization Theory and Applications Vol 43, 1984 .. [2] Peyré, G., & Cuturi, M. (2017). "Computational Optimal Transport", 2018. .. [88] Bouveyron, C. & Corneli, M. ("Scaling Optimal Transport to High-Dimensional Gaussian Distributions") """ nx = get_backend(ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t) p = Us.shape[0] # source Cs = nx.diag(nx.sqrt(ls + sigma2_s) - nx.sqrt(sigma2_s)) Ss_sq = dots(Us, Cs, Us.T) + nx.sqrt(sigma2_s) * nx.eye(p) Ds = nx.diag((nx.sqrt(ls + sigma2_s) - nx.sqrt(sigma2_s)) / nx.sqrt(ls + sigma2_s)) Ss_sqinv = (1 / nx.sqrt(sigma2_s)) * (nx.eye(p) - dots(Us, Ds, Us.T)) # destination St = dots(Ut, nx.diag(lt), Ut.T) + sigma2_t * nx.eye(p) M0 = nx.sqrtm(dots(Ss_sq, St, Ss_sq)) A = dots(Ss_sqinv, M0, Ss_sqinv) b = mt - nx.dot(ms, A) if log: log = {} log["Ss_sq"] = Ss_sq log["Ss_sqinv"] = Ss_sqinv return A, b, log else: return A, b def empirical_bures_wasserstein_mapping( xs, xt, reg=1e-6, ws=None, wt=None, bias=True, log=False ): r"""Return OT linear operator between samples. The function estimates the optimal linear operator that aligns the two empirical distributions. This is equivalent to estimating the closed form mapping between two Gaussian distributions :math:`\mathcal{N}(\mu_s,\Sigma_s)` and :math:`\mathcal{N}(\mu_t,\Sigma_t)` as proposed in :ref:`[1] ` and discussed in remark 2.29 in :ref:`[2] `. The linear operator from source to target :math:`M` .. math:: M(\mathbf{x})= \mathbf{A} \mathbf{x} + \mathbf{b} where : .. math:: \mathbf{A} &= \Sigma_s^{-1/2} \left(\Sigma_s^{1/2}\Sigma_t\Sigma_s^{1/2} \right)^{1/2} \Sigma_s^{-1/2} \mathbf{b} &= \mu_t - \mathbf{A} \mu_s Parameters ---------- xs : array-like (ns,d) samples in the source domain xt : array-like (nt,d) samples in the target domain reg : float,optional regularization added to the diagonals of covariances (>0) ws : array-like (ns,1), optional weights for the source samples wt : array-like (nt,1), optional weights for the target samples bias: boolean, optional estimate bias :math:`\mathbf{b}` else :math:`\mathbf{b} = 0` (default:True) log : bool, optional record log if True Returns ------- A : (d, d) array-like Linear operator b : (1, d) array-like bias log : dict log dictionary return only if log==True in parameters .. _references-OT-mapping-linear: References ---------- .. [1] Knott, M. and Smith, C. S. "On the optimal mapping of distributions", Journal of Optimization Theory and Applications Vol 43, 1984 .. [2] Peyré, G., & Cuturi, M. (2017). "Computational Optimal Transport", 2018. """ xs, xt = list_to_array(xs, xt) nx = get_backend(xs, xt) is_input_finite = is_all_finite(xs, xt) d = xs.shape[1] if ws is None: ws = nx.ones((xs.shape[0], 1), type_as=xs) / xs.shape[0] if wt is None: wt = nx.ones((xt.shape[0], 1), type_as=xt) / xt.shape[0] if bias: mxs = nx.dot(ws.T, xs) / nx.sum(ws) mxt = nx.dot(wt.T, xt) / nx.sum(wt) xs = xs - mxs xt = xt - mxt else: mxs = nx.zeros((1, d), type_as=xs) mxt = nx.zeros((1, d), type_as=xs) Cs = nx.dot((xs * ws).T, xs) / nx.sum(ws) + reg * nx.eye(d, type_as=xs) Ct = nx.dot((xt * wt).T, xt) / nx.sum(wt) + reg * nx.eye(d, type_as=xt) if log: A, b, log = bures_wasserstein_mapping(mxs, mxt, Cs, Ct, log=log) else: A, b = bures_wasserstein_mapping(mxs, mxt, Cs, Ct) if is_input_finite and not is_all_finite(A, b): warnings.warn( "Numerical errors were encountered in ot.gaussian.empirical_bures_wasserstein_mapping. " "Consider increasing the regularization parameter `reg`." ) if log: log["Cs"] = Cs log["Ct"] = Ct return A, b, log else: return A, b def empirical_bures_wasserstein_mapping_hd( xs, xt, d_intrinsic, reg=0.0, ws=None, wt=None, bias=True, log=False ): r"""Return OT HD linear operator between samples. The function estimates the optimal linear HD operator that aligns the two empirical distributions. This is equivalent to estimating the closed form mapping between two HD Gaussian distributions :math:`\mathcal{N}(\mu_s, U_s, l_s, \sigma_s^2, d_s)` and :math:`\mathcal{N}(\mu_t, U_t, l_t, \sigma_t^2, d_t)` as proposed in :ref:`[88] `, Th. 2.9. The linear operator from source to target :math:`M` .. math:: M(\mathbf{x})= \mathbf{A} \mathbf{x} + \mathbf{b} where : .. math:: \mathbf{A} &= \Sigma_s^{-1/2} \left(\Sigma_s^{1/2}\Sigma_t\Sigma_s^{1/2} \right)^{1/2} \Sigma_s^{-1/2} \\ \Sigma_s^{1/2} &=\sigma_s I_p + U_s C_s U_s^T \\ C_s &=\text{diag}(\sqrt{l_{s1} + \sigma_s^2} - \sigma_s, \dots, \sqrt{l_{sd_s} + \sigma_s^2} - \sigma_s) \\ \Sigma_s^{-1/2} &= \frac{1}{\sigma_s} (I_p - U_s D_s U_s^T ) \\ D_s &= \text{diag}((\sqrt{l_{s1} + \sigma_s^2} - \sigma_s)/\sqrt{l_{s1} + \sigma_s^2}, \dots, (\sqrt{l_{sd_s} + \sigma_s^2} - \sigma_s)/\sqrt{l_{sd_s} + \sigma_s^2}) \\ \Sigma_t &= U_t \text{diag}(l_t) U_t^T + \sigma_t^2 I_p \\ \mathbf{b} &= \mu_t - \mathbf{A} \mu_s Assuming that the source and destination data samples have been generated from HD Gaussian distributions, the probabilistic PCA estimators proposed in :ref: `[89] ` are used to estimate the model parameters and plugged into the above formulas. Parameters ---------- xs : array-like (ns,p) samples in the source domain xt : array-like (nt,p) samples in the target domain d_instrinsic : int or list of two ints the intrinsic dimensions of the source and destination distribution, respectively. If d_intrinsic is int it is assumed the intrinsic dimension is the same reg : float,optional regularization added to the diagonals of covariances (null by default) ws : array-like (ns,1), optional weights for the source samples wt : array-like (nt,1), optional weights for the target samples bias: boolean, optional estimate bias :math:`\mathbf{b}` else :math:`\mathbf{b} = 0` (default:True) log : bool, optional record log if True Returns ------- A : (p, p) array-like Linear operator b : (1, p) array-like bias log : dict log dictionary return only if log==True in parameters .. _references-empirical-bures-wasserstein-hd: References ---------- .. [88] Bouveyron, C. & Corneli, M. ("Scaling Optimal Transport to High-Dimensional Gaussian Distributions") [89] Tipping, M.E. & Bishop, C.M, ("Probabilistic Principal Component Analysis") """ (ds, dt) = ( (d_intrinsic, d_intrinsic) if isinstance(d_intrinsic, int) else d_intrinsic ) nx = get_backend(xs, xt) is_input_finite = is_all_finite(xs, xt) p = xs.shape[1] if ws is None: ws = nx.ones((xs.shape[0], 1), type_as=xs) / xs.shape[0] if wt is None: wt = nx.ones((xt.shape[0], 1), type_as=xt) / xt.shape[0] if bias: mxs = nx.dot(ws.T, xs) / nx.sum(ws) mxt = nx.dot(wt.T, xt) / nx.sum(wt) xs = xs - mxs xt = xt - mxt else: mxs = nx.zeros((1, p), type_as=xs) mxt = nx.zeros((1, p), type_as=xs) Cs = nx.dot((xs * ws).T, xs) / nx.sum(ws) + reg * nx.eye(p, type_as=xs) Ct = nx.dot((xt * wt).T, xt) / nx.sum(wt) + reg * nx.eye(p, type_as=xt) eigs = nx.eigh(Cs) a_s = eigs[0][-ds:] sgm2_s = (nx.trace(Cs) - nx.sum(a_s)) / (p - ds) Qs = eigs[1] Us = Qs[:, -ds:] ls = a_s - sgm2_s eigt = nx.eigh(Ct) a_t = eigt[0][-dt:] sgm2_t = (nx.trace(Ct) - nx.sum(a_t)) / (p - dt) Qt = eigt[1] Ut = Qt[:, -dt:] lt = a_t - sgm2_t sgm2_s = nx.maximum(nx.unsqueeze(sgm2_s, 0), reg) sgm2_t = nx.maximum(nx.unsqueeze(sgm2_t, 0), reg) if log: A, b, log = bures_wasserstein_mapping_hd( mxs, mxt, Us, Ut, ls, lt, sgm2_s, sgm2_t, log=log ) else: A, b = bures_wasserstein_mapping_hd(mxs, mxt, Us, Ut, ls, lt, sgm2_s, sgm2_t) if is_input_finite and not is_all_finite(A, b): warnings.warn( "Numerical errors were encountered in ot.gaussian.empirical_bures_wasserstein_mapping_hd. " "Consider increasing the regularization parameter `reg` or reducing the intrinsic dimensions ds/dt." ) if log: log["Us"] = Us log["Ut"] = Ut log["ls"] = ls log["lt"] = lt log["sigma2_s"] = sgm2_s log["sigma2_t"] = sgm2_t return A, b, log else: return A, b def bures_distance(Cs, Ct, paired=False, log=False, nx=None): r"""Return Bures distance. The function computes the Bures distance between :math:`\mu_s=\mathcal{N}(0,\Sigma_s)` and :math:`\mu_t=\mathcal{N}(0,\Sigma_t)`, given by (see e.g. Remark 2.31 :ref:`[15] `): .. math:: \mathbf{B}(\Sigma_s, \Sigma_t)^{2} = \text{Tr}\left(\Sigma_s + \Sigma_t - 2 \sqrt{\Sigma_s^{1/2}\Sigma_t\Sigma_s^{1/2}} \right) Parameters ---------- Cs : array-like (d,d) or (n,d,d) covariance of the source distribution Ct : array-like (d,d) or (m,d,d) covariance of the target distribution paired: bool, optional if True and n==m, return the paired distances and crossed distance otherwise log : bool, optional record log if True nx : module, optional The numerical backend module to use. If not provided, the backend will be fetched from the input matrices `Cs, Ct`. Returns ------- W : float if Cs and Cd of shape (d,d), array-like (n,m) if Cs of shape (n,d,d) and Ct of shape (m,d,d), array-like (n,) if Cs and Ct of shape (n, d, d) and paired is True Bures Wasserstein distance log : dict log dictionary return only if log==True in parameters .. _references-bures-wasserstein-distance: References ---------- .. [15] Peyré, G., & Cuturi, M. (2019). Computational optimal transport: With applications to data science. Foundations and Trends® in Machine Learning, 11(5-6), 355-607. """ Cs, Ct = list_to_array(Cs, Ct) if nx is None: nx = get_backend(Cs, Ct) assert Cs.shape[-1] == Ct.shape[-1], "All Gaussian must have the same dimension" Cs12 = nx.sqrtm(Cs) if len(Cs.shape) == 2 and len(Ct.shape) == 2: # Return float bw2 = nx.trace(Cs + Ct - 2 * nx.sqrtm(dots(Cs12, Ct, Cs12))) else: assert ( len(Cs.shape) == 3 and len(Ct.shape) == 3 ), "Both Cs and Ct should be batched" if paired and len(Cs) == len(Ct): # Return shape (n,) M = nx.einsum("nij, njk, nkl -> nil", Cs12, Ct, Cs12) bw2 = nx.trace(Cs + Ct - 2 * nx.sqrtm(M)) else: # Return shape (n,m) M = nx.einsum("nij, mjk, nkl -> nmil", Cs12, Ct, Cs12) bw2 = nx.trace(Cs[:, None] + Ct[None] - 2 * nx.sqrtm(M)) W = nx.sqrt(nx.maximum(bw2, 0)) if log: log = {} log["Cs12"] = Cs12 return W, log else: return W def bures_wasserstein_distance(ms, mt, Cs, Ct, paired=False, log=False): r"""Return Bures Wasserstein distance between samples. The function computes the Bures-Wasserstein distance between :math:`\mu_s=\mathcal{N}(m_s,\Sigma_s)` and :math:`\mu_t=\mathcal{N}(m_t,\Sigma_t)`, as discussed in remark 2.31 :ref:`[15] `. .. math:: \mathcal{W}(\mu_s, \mu_t)_2^2= \left\lVert \mathbf{m}_s - \mathbf{m}_t \right\rVert^2 + \mathcal{B}(\Sigma_s, \Sigma_t)^{2} where : .. math:: \mathbf{B}(\Sigma_s, \Sigma_t)^{2} = \text{Tr}\left(\Sigma_s + \Sigma_t - 2 \sqrt{\Sigma_s^{1/2}\Sigma_t\Sigma_s^{1/2}} \right) Parameters ---------- ms : array-like (d,) or (n,d) mean of the source distribution mt : array-like (d,) or (m,d) mean of the target distribution Cs : array-like (d,d) or (n,d,d) covariance of the source distribution Ct : array-like (d,d) or (m,d,d) covariance of the target distribution paired: bool, optional if True and n==m, return the paired distances and crossed distance otherwise log : bool, optional record log if True Returns ------- W : float if ms and md of shape (d,), array-like (n,m) if ms of shape (n,d) and mt of shape (m,d), array-like (n,) if ms and mt of shape (n,d) and paired is True Bures Wasserstein distance log : dict log dictionary return only if log==True in parameters .. _references-bures-wasserstein-distance: References ---------- .. [15] Peyré, G., & Cuturi, M. (2019). Computational optimal transport: With applications to data science. Foundations and Trends® in Machine Learning, 11(5-6), 355-607. """ ms, mt, Cs, Ct = list_to_array(ms, mt, Cs, Ct) nx = get_backend(ms, mt, Cs, Ct) assert ( ms.shape[0] == Cs.shape[0] ), "Source Gaussians has different amount of components" assert ( mt.shape[0] == Ct.shape[0] ), "Target Gaussians has different amount of components" assert ( ms.shape[-1] == mt.shape[-1] == Cs.shape[-1] == Ct.shape[-1] ), "All Gaussian must have the same dimension" if log: bw, log_dict = bures_distance(Cs, Ct, paired=paired, log=log, nx=nx) Cs12 = log_dict["Cs12"] else: bw = bures_distance(Cs, Ct, paired=paired, nx=nx) if len(ms.shape) == 1 and len(mt.shape) == 1: # Return float squared_dist_m = nx.norm(ms - mt) ** 2 else: assert ( len(ms.shape) == 2 and len(mt.shape) == 2 ), "Both ms and mt should be batched" if paired and len(ms.shape) == len(mt.shape): # Return shape (n,) squared_dist_m = nx.norm(ms - mt, axis=-1) ** 2 else: # Return shape (n,m) squared_dist_m = nx.norm(ms[:, None] - mt[None], axis=-1) ** 2 W = nx.sqrt(nx.maximum(squared_dist_m + bw**2, 0)) if log: log = {} log["Cs12"] = Cs12 return W, log else: return W def bures_wasserstein_distance_hd( ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t, log=False ): r"""Return the 2-Wassestein distance between HD Gaussian distritutions. The function estimates the 2-Wasserstein distance between the two HD Gaussian distributions :math:`\mathcal{N}(\mu_s, U_s, l_s, \sigma_s^2, d_s)` and :math:`\mathcal{N}(\mu_t, U_t, l_t, \sigma_t^2, d_t)` as proposed in :ref:`[88] `, Prop. 2.3 .. math:: \mathcal{W}(\mu_s, \mu_t)_2^2= \left\lVert \mathbf{m}_s - \mathbf{m}_t \right\rVert^2 + \text{Tr}(\Lambda_s) + \text{Tr}(\Lambda_t) + p(\sigma_s^2 + \sigma_t^2) - 2\text{Tr}((\Sigma_s^{(1/2)}\Sigma_t \Sigma_s^{1/2})^{1/2}) where : .. math:: \Lambda_s &= \text{diag}(ls) \\ \Lambda_t &= \text{diag}(lt) \\ \Sigma_t &= U_t \Lambda_t U_t^T + \sigma_t^2 I_p \\ \Sigma_s^{1/2} &=\sigma_s I_p + U_s C_s U_s^T \\ C_s &=\text{diag}(\sqrt{l_{s1} + \sigma_s^2} - \sigma_s, \dots, \sqrt{l_{sd_s} + \sigma_s^2} - \sigma_s) \\ Parameters ---------- ms : array-like (p,) mean of the source distribution mt : array-like (p,) mean of the target distribution Us : array-like (p,ds) orthogonal matrix spanning the principal subspace of the source distribution Ut : array-like (p,dt) orthogonal matrix spanning the principal subspace of the target distribution ls : array-like (ds,) the variances associated with the principal sub-axes for the source distribution lt : array-like (dt,) the variances associated with the principal sub-axes for the target distribution sigma_s^2 : array-like (1,) the residual variance of the source distribution sigma_t^2 : array-like(1,) the residual variance of the target distribution log : bool, optional record log if True Returns ------- W : float Bures Wasserstein distance .. _references-bures-wasserstein-distance-hd: References ---------- .. [1] Knott, M. and Smith, C. S. "On the optimal mapping of distributions", Journal of Optimization Theory and Applications Vol 43, 1984 .. [2] Peyré, G., & Cuturi, M. (2017). "Computational Optimal Transport", 2018. .. [88] Bouveyron, C. & Corneli, M. ("Scaling Optimal Transport to High-Dimensional Gaussian Distributions") """ nx = get_backend(ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t) p = Us.shape[0] # source Cs = nx.diag(nx.sqrt(ls + sigma2_s) - nx.sqrt(sigma2_s)) Ss_sq = dots(Us, Cs, Us.T) + nx.sqrt(sigma2_s) * nx.eye(p) # destination St = dots(Ut, nx.diag(lt), Ut.T) + sigma2_t * nx.eye(p) A = dots(Ss_sq, St, Ss_sq) W2 = ( nx.sum((ms - mt) ** 2) + nx.sum(ls) + nx.sum(lt) + p * (sigma2_s + sigma2_t) - 2 * nx.trace(nx.sqrtm(A)) ) W = nx.sqrt(W2) if log: log = {} log["Ss_sq"] = Ss_sq log["St"] = St return W, log else: return W def empirical_bures_wasserstein_distance( xs, xt, reg=1e-6, ws=None, wt=None, bias=True, log=False ): r"""Return Bures Wasserstein distance from mean and covariance of distribution. The function estimates the Bures-Wasserstein distance between two empirical distributions source :math:`\mu_s` and target :math:`\mu_t`, discussed in remark 2.31 :ref:`[1] `. The Bures Wasserstein distance between source and target distribution :math:`\mathcal{W}` .. math:: \mathcal{W}(\mu_s, \mu_t)_2^2= \left\lVert \mathbf{m}_s - \mathbf{m}_t \right\rVert^2 + \mathcal{B}(\Sigma_s, \Sigma_t)^{2} where : .. math:: \mathbf{B}(\Sigma_s, \Sigma_t)^{2} = \text{Tr}\left(\Sigma_s + \Sigma_t - 2 \sqrt{\Sigma_s^{1/2}\Sigma_t\Sigma_s^{1/2}} \right) Parameters ---------- xs : array-like (ns,d) samples in the source domain xt : array-like (nt,d) samples in the target domain reg : float,optional regularization added to the diagonals of covariances (>0) ws : array-like (ns), optional weights for the source samples wt : array-like (ns), optional weights for the target samples bias: boolean, optional estimate bias :math:`\mathbf{b}` else :math:`\mathbf{b} = 0` (default:True) log : bool, optional record log if True Returns ------- W : float Bures Wasserstein distance log : dict log dictionary return only if log==True in parameters .. _references-bures-wasserstein-distance: References ---------- .. [1] Peyré, G., & Cuturi, M. (2017). "Computational Optimal Transport", 2018. """ xs, xt = list_to_array(xs, xt) nx = get_backend(xs, xt) d = xs.shape[1] if ws is None: ws = nx.ones((xs.shape[0], 1), type_as=xs) / xs.shape[0] if wt is None: wt = nx.ones((xt.shape[0], 1), type_as=xt) / xt.shape[0] if bias: mxs = nx.dot(ws.T, xs) / nx.sum(ws) mxt = nx.dot(wt.T, xt) / nx.sum(wt) xs = xs - mxs xt = xt - mxt else: mxs = nx.zeros((1, d), type_as=xs) mxt = nx.zeros((1, d), type_as=xs) Cs = nx.dot((xs * ws).T, xs) / nx.sum(ws) + reg * nx.eye(d, type_as=xs) Ct = nx.dot((xt * wt).T, xt) / nx.sum(wt) + reg * nx.eye(d, type_as=xt) if log: W, log = bures_wasserstein_distance(mxs[0], mxt[0], Cs, Ct, log=log) log["Cs"] = Cs log["Ct"] = Ct return W, log else: W = bures_wasserstein_distance(mxs[0], mxt[0], Cs, Ct) return W def empirical_bures_wasserstein_distance_hd( xs, xt, d_intrinsic, reg=0.0, ws=None, wt=None, bias=True, log=False ): r"""Return 2D Wasserstein between high-dimensional (HD) Gaussian distributions. The function estimates the 2-Wasserstein distance between the two HD Gaussian distributions :math:`\mathcal{N}(\mu_s, U_s, l_s, \sigma_s^2, d_s)` and :math:`\mathcal{N}(\mu_t, U_t, l_t, \sigma_t^2, d_t)` as proposed in :ref:`[88] `, Prop. 2.3 .. math:: \mathcal{W}(\mu_s, \mu_t)_2^2= \left\lVert \mathbf{m}_s - \mathbf{m}_t \right\rVert^2 + \text{Tr}(\Lambda_s) + \text{Tr}(\Lambda_t) + p(\sigma_s^2 + \sigma_t^2) - 2\text{Tr}((\Sigma_s^{(1/2)}\Sigma_t \Sigma_s^{1/2})^{1/2}) where : .. math:: \Lambda_s &= \text{diag}(ls) \\ \Lambda_t &= \text{diag}(lt) \\ \Sigma_t &= U_t \Lambda_t U_t^T + \sigma_t^2 I_p \\ \Sigma_s^{1/2} &=\sigma_s I_p + U_s C_s U_s^T \\ C_s &=\text{diag}(\sqrt{l_{s1} + \sigma_s^2} - \sigma_s, \dots, \sqrt{l_{sd_s} + \sigma_s^2} - \sigma_s) \\ Assuming that the source and destination data samples have been generated from HD Gaussian distributions, the probabilistic PCA estimators proposed in :ref: `[89] ` are used to estimate the model parameters and plugged into the above formula of the Wasserstein distance. Parameters ---------- xs : array-like (ns,p) samples in the source domain xt : array-like (nt,p) samples in the target domain d_instrinsic : int or list of two ints the intrinsic dimensions of the source and destination distribution, respectively. If d_intrinsic is int it is assumed the intrinsic dimension is the same reg : float,optional regularization added to the diagonals of covariances (null by default) ws : array-like (ns,1), optional weights for the source samples wt : array-like (nt,1), optional weights for the target samples bias: boolean, optional estimate bias :math:`\mathbf{b}` else :math:`\mathbf{b} = 0` (default:True) log : bool, optional record log if True Returns ------- A : (p, p) array-like Linear operator b : (1, p) array-like bias log : dict log dictionary return only if log==True in parameters .. _references-empirical-bures-wasserstein-distance-hd: References ---------- .. [88] Bouveyron, C. & Corneli, M. ("Scaling Optimal Transport to High-Dimensional Gaussian Distributions") [89] Tipping, M.E. & Bishop, C.M, ("Probabilistic Principal Component Analysis") """ (ds, dt) = ( (d_intrinsic, d_intrinsic) if isinstance(d_intrinsic, int) else d_intrinsic ) nx = get_backend(xs, xt) is_input_finite = is_all_finite(xs, xt) p = xs.shape[1] if ws is None: ws = nx.ones((xs.shape[0], 1), type_as=xs) / xs.shape[0] if wt is None: wt = nx.ones((xt.shape[0], 1), type_as=xt) / xt.shape[0] if bias: mxs = nx.dot(ws.T, xs) / nx.sum(ws) mxt = nx.dot(wt.T, xt) / nx.sum(wt) xs = xs - mxs xt = xt - mxt else: mxs = nx.zeros((1, p), type_as=xs) mxt = nx.zeros((1, p), type_as=xs) Cs = nx.dot((xs * ws).T, xs) / nx.sum(ws) + reg * nx.eye(p, type_as=xs) Ct = nx.dot((xt * wt).T, xt) / nx.sum(wt) + reg * nx.eye(p, type_as=xt) eigs = nx.eigh(Cs) a_s = eigs[0][-ds:] sgm2_s = (nx.trace(Cs) - nx.sum(a_s)) / (p - ds + reg) Qs = eigs[1] Us = Qs[:, -ds:] ls = a_s - sgm2_s eigt = nx.eigh(Ct) a_t = eigt[0][-dt:] sgm2_t = (nx.trace(Ct) - nx.sum(a_t)) / (p - dt + reg) Qt = eigt[1] Ut = Qt[:, -dt:] lt = a_t - sgm2_t sgm2_s = nx.maximum(nx.unsqueeze(sgm2_s, 0), reg) sgm2_t = nx.maximum(nx.unsqueeze(sgm2_t, 0), reg) if log: W, log = bures_wasserstein_distance_hd( mxs, mxt, Us, Ut, ls, lt, sgm2_s, sgm2_t, log=log ) else: W = bures_wasserstein_distance_hd(mxs, mxt, Us, Ut, ls, lt, sgm2_s, sgm2_t) if is_input_finite and not is_all_finite(W): warnings.warn( "Numerical errors were encountered in ot.gaussian.empirical_bures_wasserstein_distance_hd. " "Consider increasing the regularization parameter `reg` or reducing the intrinsic dimensions ds/dt." ) if log: return W, log else: return W def bures_barycenter_fixpoint( C, weights=None, num_iter=1000, eps=1e-7, log=False, nx=None ): r"""Return the (Bures-)Wasserstein barycenter between centered Gaussian distributions. The function estimates the (Bures)-Wasserstein barycenter between centered Gaussian distributions :math:`\big(\mathcal{N}(0,\Sigma_i)\big)_{i=1}^n` :ref:`[16] ` by solving .. math:: \Sigma_b = \mathrm{argmin}_{\Sigma \in S_d^{++}(\mathbb{R})}\ \sum_{i=1}^n w_i W_2^2\big(\mathcal{N}(0,\Sigma), \mathcal{N}(0, \Sigma_i)\big). The barycenter still follows a Gaussian distribution :math:`\mathcal{N}(0,\Sigma_b)` where :math:`\Sigma_b` is solution of the following fixed-point algorithm: .. math:: \Sigma_b = \sum_{i=1}^n w_i \left(\Sigma_b^{1/2}\Sigma_i^{1/2}\Sigma_b^{1/2}\right)^{1/2}. Parameters ---------- C : array-like (k,d,d) covariance of k distributions weights : array-like (k), optional weights for each distribution method : str method used for the solver, either 'fixed_point' or 'gradient_descent' num_iter : int, optional number of iteration for the fixed point algorithm eps : float, optional tolerance for the fixed point algorithm log : bool, optional record log if True nx : module, optional The numerical backend module to use. If not provided, the backend will be fetched from the input matrices `C`. Returns ------- Cb : (d, d) array-like covariance of the barycenter log : dict log dictionary return only if log==True in parameters .. _references-OT-bures-barycenter-fixed-point: References ---------- .. [16] M. Agueh and G. Carlier, "Barycenters in the Wasserstein space", SIAM Journal on Mathematical Analysis, vol. 43, no. 2, pp. 904-924, 2011. """ if nx is None: nx = get_backend( *C, ) if weights is None: weights = nx.ones(C.shape[0], type_as=C[0]) / C.shape[0] # Init the covariance barycenter Cb = nx.mean(C * weights[:, None, None], axis=0) for it in range(num_iter): # fixed point update Cb12 = nx.sqrtm(Cb) Cnew = nx.sqrtm(Cb12 @ C @ Cb12) Cnew *= weights[:, None, None] Cnew = nx.sum(Cnew, axis=0) # check convergence diff = nx.norm(Cb - Cnew) if diff <= eps: break Cb = Cnew if diff > eps: print("Dit not converge.") if log: log = {} log["num_iter"] = it log["final_diff"] = diff return Cb, log else: return Cb def bures_barycenter_gradient_descent( C, weights=None, num_iter=1000, eps=1e-7, log=False, step_size=1, batch_size=None, averaged=False, nx=None, ): r"""Return the (Bures-)Wasserstein barycenter between centered Gaussian distributions. The function estimates the (Bures)-Wasserstein barycenter between centered Gaussian distributions :math:`\big(\mathcal{N}(0,\Sigma_i)\big)_{i=1}^n` by using a gradient descent in the Wasserstein space :ref:`[74, 75] ` on the objective .. math:: \mathcal{L}(\Sigma) = \sum_{i=1}^n w_i W_2^2\big(\mathcal{N}(0,\Sigma), \mathcal{N}(0,\Sigma_i)\big). Parameters ---------- C : array-like (k,d,d) covariance of k distributions weights : array-like (k), optional weights for each distribution method : str method used for the solver, either 'fixed_point' or 'gradient_descent' num_iter : int, optional number of iteration for the fixed point algorithm eps : float, optional tolerance for the fixed point algorithm log : bool, optional record log if True step_size : float, optional step size for the gradient descent, 1 by default batch_size : int, optional batch size if use a stochastic gradient descent averaged : bool, optional if True, use the averaged procedure of :ref:`[74] ` nx : module, optional The numerical backend module to use. If not provided, the backend will be fetched from the input matrices `C`. Returns ------- Cb : (d, d) array-like covariance of the barycenter log : dict log dictionary return only if log==True in parameters .. _references-OT-bures-barycenter-gradient_descent: References ---------- .. [74] Chewi, S., Maunu, T., Rigollet, P., & Stromme, A. J. (2020). Gradient descent algorithms for Bures-Wasserstein barycenters. In Conference on Learning Theory (pp. 1276-1304). PMLR. .. [75] Altschuler, J., Chewi, S., Gerber, P. R., & Stromme, A. (2021). Averaging on the Bures-Wasserstein manifold: dimension-free convergence of gradient descent. Advances in Neural Information Processing Systems, 34, 22132-22145. """ if nx is None: nx = get_backend( *C, ) n = C.shape[0] if weights is None: weights = nx.ones(C.shape[0], type_as=C[0]) / n # Init the covariance barycenter Cb = nx.mean(C * weights[:, None, None], axis=0) Id = nx.eye(C.shape[-1], type_as=Cb) L_diff = [] Cb_averaged = nx.copy(Cb) for it in range(num_iter): Cb12 = nx.sqrtm(Cb) Cb12_ = nx.inv(Cb12) if batch_size is not None and batch_size < n: # if stochastic gradient descent if batch_size <= 0: raise ValueError( "batch_size must be an integer between 0 and {}".format(n) ) inds = np.random.choice( n, batch_size, replace=True, p=nx._to_numpy(weights) ) M = nx.sqrtm(nx.einsum("ij,njk,kl -> nil", Cb12, C[inds], Cb12)) ot_maps = nx.einsum("ij,njk,kl -> nil", Cb12_, M, Cb12_) grad_bw = Id - nx.mean(ot_maps, axis=0) # step size from [74] (page 15) step_size = 2 / (0.7 * (it + 2 / 0.7 + 1)) else: # gradient descent M = nx.sqrtm(nx.einsum("ij,njk,kl -> nil", Cb12, C, Cb12)) ot_maps = nx.einsum("ij,njk,kl -> nil", Cb12_, M, Cb12_) grad_bw = Id - nx.sum(ot_maps * weights[:, None, None], axis=0) Cnew = exp_bures(Cb, -step_size * grad_bw, nx=nx) if averaged: # ot map between Cb_averaged and Cnew Cb_averaged12 = nx.sqrtm(Cb_averaged) Cb_averaged12inv = nx.inv(Cb_averaged12) M = nx.sqrtm(nx.einsum("ij,jk,kl->il", Cb_averaged12, Cnew, Cb_averaged12)) ot_map = nx.einsum("ij,jk,kl->il", Cb_averaged12inv, M, Cb_averaged12inv) map = Id * step_size / (step_size + 1) + ot_map / (step_size + 1) Cb_averaged = nx.einsum("ij,jk,kl->il", map, Cb_averaged, map) # check convergence L_diff.append(nx.norm(Cb - Cnew)) # Criteria to stop if np.mean(L_diff[-100:]) <= eps: break Cb = Cnew if averaged: Cb = Cb_averaged if log: dict_log = {} dict_log["num_iter"] = it dict_log["final_diff"] = L_diff[-1] return Cb, dict_log else: return Cb def bures_wasserstein_barycenter( m, C, weights=None, method="fixed_point", num_iter=1000, eps=1e-7, log=False, step_size=1, batch_size=None, ): r"""Return the (Bures-)Wasserstein barycenter between Gaussian distributions. The function estimates the (Bures)-Wasserstein barycenter between Gaussian distributions :math:`\big(\mathcal{N}(\mu_i,\Sigma_i)\big)_{i=1}^n` :ref:`[16, 74, 75] ` by solving .. math:: (\mu_b, \Sigma_b) = \mathrm{argmin}_{\mu,\Sigma}\ \sum_{i=1}^n w_i W_2^2\big(\mathcal{N}(\mu,\Sigma), \mathcal{N}(\mu_i, \Sigma_i)\big) The barycenter still follows a Gaussian distribution :math:`\mathcal{N}(\mu_b,\Sigma_b)` where: .. math:: \mu_b = \sum_{i=1}^n w_i \mu_i, and the barycentric covariance is the solution of the following fixed-point algorithm: .. math:: \Sigma_b = \sum_{i=1}^n w_i \left(\Sigma_b^{1/2}\Sigma_i^{1/2}\Sigma_b^{1/2}\right)^{1/2} We propose two solvers: one based on solving the previous fixed-point problem [16]. Another based on gradient descent in the Bures-Wasserstein space [74,75]. Parameters ---------- m : array-like (k,d) mean of k distributions C : array-like (k,d,d) covariance of k distributions weights : array-like (k), optional weights for each distribution method : str method used for the solver, either 'fixed_point', 'gradient_descent', 'stochastic_gradient_descent' or 'averaged_stochastic_gradient_descent' num_iter : int, optional number of iteration for the fixed point algorithm eps : float, optional tolerance for the fixed point algorithm log : bool, optional record log if True step_size : float, optional step size for the gradient descent, 1 by default batch_size : int, optional batch size if use a stochastic gradient descent. If not None, use method='gradient_descent' Returns ------- mb : (d,) array-like mean of the barycenter Cb : (d, d) array-like covariance of the barycenter log : dict log dictionary return only if log==True in parameters .. _references-OT-bures_wasserstein-barycenter: References ---------- .. [16] M. Agueh and G. Carlier, "Barycenters in the Wasserstein space", SIAM Journal on Mathematical Analysis, vol. 43, no. 2, pp. 904-924, 2011. .. [74] Chewi, S., Maunu, T., Rigollet, P., & Stromme, A. J. (2020). Gradient descent algorithms for Bures-Wasserstein barycenters. In Conference on Learning Theory (pp. 1276-1304). PMLR. .. [75] Altschuler, J., Chewi, S., Gerber, P. R., & Stromme, A. (2021). Averaging on the Bures-Wasserstein manifold: dimension-free convergence of gradient descent. Advances in Neural Information Processing Systems, 34, 22132-22145. """ nx = get_backend( *m, ) if weights is None: weights = nx.ones(C.shape[0], type_as=C[0]) / C.shape[0] # Compute the mean barycenter mb = nx.sum(m * weights[:, None], axis=0) if method == "gradient_descent": out = bures_barycenter_gradient_descent( C, weights=weights, num_iter=num_iter, eps=eps, log=log, step_size=step_size, nx=nx, ) elif method == "stochastic_gradient_descent": out = bures_barycenter_gradient_descent( C, weights=weights, num_iter=num_iter, eps=eps, log=log, batch_size=1 if batch_size is None else batch_size, nx=nx, ) elif method == "averaged_stochastic_gradient_descent": out = bures_barycenter_gradient_descent( C, weights=weights, num_iter=num_iter, eps=eps, log=log, batch_size=1 if batch_size is None else batch_size, averaged=True, nx=nx, ) elif method == "fixed_point": out = bures_barycenter_fixpoint( C, weights=weights, num_iter=num_iter, eps=eps, log=log, nx=nx ) else: raise ValueError("Unknown method '%s'." % method) if log: Cb, log = out return mb, Cb, log else: Cb = out return mb, Cb def empirical_bures_wasserstein_barycenter( X, reg=1e-6, weights=None, num_iter=1000, eps=1e-7, w=None, bias=True, log=False ): r"""Return OT linear operator between samples. The function estimates the optimal barycenter of the empirical distributions. This is equivalent to resolving the fixed point algorithm for multiple Gaussian distributions :math:`\left\{\mathcal{N}(\mu,\Sigma)\right\}_{i=1}^n` :ref:`[1] `. The barycenter still following a Gaussian distribution :math:`\mathcal{N}(\mu_b,\Sigma_b)` where : .. math:: \mu_b = \sum_{i=1}^n w_i \mu_i And the barycentric covariance is the solution of the following fixed-point algorithm: .. math:: \Sigma_b = \sum_{i=1}^n w_i \left(\Sigma_b^{1/2}\Sigma_i^{1/2}\Sigma_b^{1/2}\right)^{1/2} Parameters ---------- X : list of array-like (n,d) samples in each distribution reg : float,optional regularization added to the diagonals of covariances (>0) weights : array-like (n,), optional weights for each distribution num_iter : int, optional number of iteration for the fixed point algorithm eps : float, optional tolerance for the fixed point algorithm w : list of array-like (n,), optional weights for each sample in each distribution bias: boolean, optional estimate bias :math:`\mathbf{b}` else :math:`\mathbf{b} = 0` (default:True) log : bool, optional record log if True Returns ------- mb : (d,) array-like mean of the barycenter Cb : (d, d) array-like covariance of the barycenter log : dict log dictionary return only if log==True in parameters .. _references-OT-mapping-linear-barycenter: References ---------- .. [1] M. Agueh and G. Carlier, "Barycenters in the Wasserstein space", SIAM Journal on Mathematical Analysis, vol. 43, no. 2, pp. 904-924, 2011. """ X = list_to_array(*X) nx = get_backend(*X) k = len(X) d = [X[i].shape[1] for i in range(k)] if w is None: w = [ nx.ones((X[i].shape[0], 1), type_as=X[i]) / X[i].shape[0] for i in range(k) ] if bias: m = [nx.dot(w[i].T, X[i]) / nx.sum(w[i]) for i in range(k)] X = [X[i] - m[i] for i in range(k)] else: m = [nx.zeros((1, d[i]), type_as=X[i]) for i in range(k)] C = [ nx.dot((X[i] * w[i]).T, X[i]) / nx.sum(w[i]) + reg * nx.eye(d[i], type_as=X[i]) for i in range(k) ] m = nx.stack(m, axis=0)[:, 0] C = nx.stack(C, axis=0) if log: mb, Cb, log = bures_wasserstein_barycenter( m, C, weights=weights, num_iter=num_iter, eps=eps, log=log ) return mb, Cb, log else: mb, Cb = bures_wasserstein_barycenter( m, C, weights=weights, num_iter=num_iter, eps=eps, log=log ) return mb, Cb def gaussian_gromov_wasserstein_distance(Cov_s, Cov_t, log=False): r"""Return the Gaussian Gromov-Wasserstein value from [57]. This function return the closed form value of the Gaussian Gromov-Wasserstein distance between two Gaussian distributions :math:`\mathcal{N}(\mu_s,\Sigma_s)` and :math:`\mathcal{N}(\mu_t,\Sigma_t)` when the OT plan is assumed to be also Gaussian. See [57] Theorem 4.1 for more details. Parameters ---------- Cov_s : array-like (ds,ds) covariance of the source distribution Cov_t : array-like (dt,dt) covariance of the target distribution Returns ------- G : float Gaussian Gromov-Wasserstein distance .. _references-gaussien_gromov_wasserstein_distance: References ---------- .. [57] Delon, J., Desolneux, A., & Salmona, A. (2022). Gromov–Wasserstein distances between Gaussian distributions. Journal of Applied Probability, 59(4), 1178-1198. """ nx = get_backend(Cov_s, Cov_t) # ensure that Cov_s is the largest covariance matrix # that is m >= n if Cov_s.shape[0] < Cov_t.shape[0]: Cov_s, Cov_t = Cov_t, Cov_s n = Cov_t.shape[0] # compte and sort eigenvalues decerasingly d_s = nx.flip(nx.sort(nx.eigh(Cov_s)[0])) d_t = nx.flip(nx.sort(nx.eigh(Cov_t)[0])) # compute the gaussien Gromov-Wasserstein distance res = ( 4 * (nx.sum(d_s) - nx.sum(d_t)) ** 2 + 8 * nx.sum((d_s[:n] - d_t) ** 2) + 8 * nx.sum((d_s[n:]) ** 2) ) if log: log = {} log["d_s"] = d_s log["d_t"] = d_t return nx.sqrt(res), log else: return nx.sqrt(res) def empirical_gaussian_gromov_wasserstein_distance(xs, xt, ws=None, wt=None, log=False): r"""Return Gaussian Gromov-Wasserstein distance between samples. The function estimates the Gaussian Gromov-Wasserstein distance between two Gaussien distributions source :math:`\mu_s` and target :math:`\mu_t`, whose parameters are estimated from the provided samples :math:`\mathcal{X}_s` and :math:`\mathcal{X}_t`. See [57] Theorem 4.1 for more details. Parameters ---------- xs : array-like (ns,d) samples in the source domain xt : array-like (nt,d) samples in the target domain ws : array-like (ns,1), optional weights for the source samples wt : array-like (ns,1), optional weights for the target samples log : bool, optional record log if True Returns ------- G : float Gaussian Gromov-Wasserstein distance .. _references-gaussien_gromov_wasserstein: References ---------- .. [57] Delon, J., Desolneux, A., & Salmona, A. (2022). Gromov–Wasserstein distances between Gaussian distributions. Journal of Applied Probability, 59(4), 1178-1198. """ xs, xt = list_to_array(xs, xt) nx = get_backend(xs, xt) if ws is None: ws = nx.ones((xs.shape[0], 1), type_as=xs) / xs.shape[0] if wt is None: wt = nx.ones((xt.shape[0], 1), type_as=xt) / xt.shape[0] mxs = nx.dot(ws.T, xs) / nx.sum(ws) mxt = nx.dot(wt.T, xt) / nx.sum(wt) xs = xs - mxs xt = xt - mxt Cs = nx.dot((xs * ws).T, xs) / nx.sum(ws) Ct = nx.dot((xt * wt).T, xt) / nx.sum(wt) if log: G, log = gaussian_gromov_wasserstein_distance(Cs, Ct, log=log) log["Cov_s"] = Cs log["Cov_t"] = Ct return G, log else: G = gaussian_gromov_wasserstein_distance(Cs, Ct) return G def gaussian_gromov_wasserstein_mapping( mu_s, mu_t, Cov_s, Cov_t, sign_eigs=None, log=False ): r"""Return the Gaussian Gromov-Wasserstein mapping from [57]. This function return the closed form value of the Gaussian Gromov-Wasserstein mapping between two Gaussian distributions :math:`\mathcal{N}(\mu_s,\Sigma_s)` and :math:`\mathcal{N}(\mu_t,\Sigma_t)` when the OT plan is assumed to be also Gaussian. See [57] Theorem 4.1 for more details. Parameters ---------- mu_s : array-like (ds,) mean of the source distribution mu_t : array-like (dt,) mean of the target distribution Cov_s : array-like (ds,ds) covariance of the source distribution Cov_t : array-like (dt,dt) covariance of the target distribution log : bool, optional record log if True Returns ------- A : (dt, ds) array-like Linear operator b : (1, dt) array-like bias .. _references-gaussien_gromov_wasserstein_mapping: References ---------- .. [57] Delon, J., Desolneux, A., & Salmona, A. (2022). Gromov–Wasserstein distances between Gaussian distributions. Journal of Applied Probability, 59(4), 1178-1198. """ nx = get_backend(mu_s, mu_t, Cov_s, Cov_t) n = Cov_t.shape[0] m = Cov_s.shape[0] # compte and sort eigenvalues/eigenvectors decreasingly d_s, U_s = nx.eigh(Cov_s) id_s = nx.flip(nx.argsort(d_s)) d_s, U_s = d_s[id_s], U_s[:, id_s] d_t, U_t = nx.eigh(Cov_t) id_t = nx.flip(nx.argsort(d_t)) d_t, U_t = d_t[id_t], U_t[:, id_t] if sign_eigs is None: sign_eigs = nx.ones(min(m, n), type_as=mu_s) if m >= n: A = nx.concatenate( ( nx.diag(sign_eigs * nx.sqrt(d_t) / nx.sqrt(d_s[:n])), nx.zeros((n, m - n), type_as=mu_s), ), axis=1, ).T else: A = nx.concatenate( ( nx.diag(sign_eigs * nx.sqrt(d_t[:m]) / nx.sqrt(d_s)), nx.zeros((n - m, m), type_as=mu_s), ), axis=0, ).T A = nx.dot(nx.dot(U_s, A), U_t.T) # compute the gaussien Gromov-Wasserstein dis b = mu_t - nx.dot(mu_s, A) if log: log = {} log["d_s"] = d_s log["d_t"] = d_t log["U_s"] = U_s log["U_t"] = U_t return A, b, log else: return A, b def empirical_gaussian_gromov_wasserstein_mapping( xs, xt, ws=None, wt=None, sign_eigs=None, log=False ): r"""Return Gaussian Gromov-Wasserstein mapping between samples. The function estimates the Gaussian Gromov-Wasserstein mapping between two Gaussian distributions source :math:`\mu_s` and target :math:`\mu_t`, whose parameters are estimated from the provided samples :math:`\mathcal{X}_s` and :math:`\mathcal{X}_t`. See [57] Theorem 4.1 for more details. Parameters ---------- xs : array-like (ns,ds) samples in the source domain xt : array-like (nt,dt) samples in the target domain ws : array-like (ns,1), optional weights for the source samples wt : array-like (ns,1), optional weights for the target samples sign_eigs : array-like (min(ds,dt),) or string, optional sign of the eigenvalues of the mapping matrix, by default all signs will be positive. If 'skewness' is provided, the sign of the eigenvalues is selected as the product of the sign of the skewness of the projected data. log : bool, optional record log if True Returns ------- A : (dt, ds) array-like Linear operator b : (1, dt) array-like bias .. _references-empirical_gaussian_gromov_wasserstein_mapping: References ---------- .. [57] Delon, J., Desolneux, A., & Salmona, A. (2022). Gromov–Wasserstein distances between Gaussian distributions. Journal of Applied Probability, 59(4), 1178-1198. """ xs, xt = list_to_array(xs, xt) nx = get_backend(xs, xt) m = xs.shape[1] n = xt.shape[1] if ws is None: ws = nx.ones((xs.shape[0], 1), type_as=xs) / xs.shape[0] if wt is None: wt = nx.ones((xt.shape[0], 1), type_as=xt) / xt.shape[0] # estimate mean and covariance mu_s = nx.dot(ws.T, xs) / nx.sum(ws) mu_t = nx.dot(wt.T, xt) / nx.sum(wt) xs = xs - mu_s xt = xt - mu_t Cov_s = nx.dot((xs * ws).T, xs) / nx.sum(ws) Cov_t = nx.dot((xt * wt).T, xt) / nx.sum(wt) # compute and sort eigenvalues/eigenvectors decreasingly d_s, U_s = nx.eigh(Cov_s) id_s = nx.flip(nx.argsort(d_s)) d_s, U_s = d_s[id_s], U_s[:, id_s] d_t, U_t = nx.eigh(Cov_t) id_t = nx.flip(nx.argsort(d_t)) d_t, U_t = d_t[id_t], U_t[:, id_t] # select the sign of the eigenvalues if sign_eigs is None: sign_eigs = nx.ones(min(m, n), type_as=mu_s) elif sign_eigs == "skewness": size = min(m, n) skew_s = nx.sum((nx.dot(xs, U_s[:, :size])) ** 3 * ws, axis=0) skew_t = nx.sum((nx.dot(xt, U_t[:, :size])) ** 3 * wt, axis=0) sign_eigs = nx.sign(skew_t * skew_s) if m >= n: A = nx.concatenate( ( nx.diag(sign_eigs * nx.sqrt(d_t) / nx.sqrt(d_s[:n])), nx.zeros((n, m - n), type_as=mu_s), ), axis=1, ).T else: A = nx.concatenate( ( nx.diag(sign_eigs * nx.sqrt(d_t[:m]) / nx.sqrt(d_s)), nx.zeros((n - m, m), type_as=mu_s), ), axis=0, ).T A = nx.dot(nx.dot(U_s, A), U_t.T) # compute the gaussien Gromov-Wasserstein dis b = mu_t - nx.dot(mu_s, A) if log: log = {} log["d_s"] = d_s log["d_t"] = d_t log["U_s"] = U_s log["U_t"] = U_t log["Cov_s"] = Cov_s log["Cov_t"] = Cov_t return A, b, log else: return A, b python-pot-0.9.7+dfsg/ot/gmm.py000066400000000000000000000504551525521106200163530ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Optimal transport for Gaussian Mixtures """ # Author: Eloi Tanguy # Remi Flamary # Julie Delon # # License: MIT License from .backend import get_backend from .lp import emd2, emd import numpy as np from .utils import dist from .gaussian import bures_wasserstein_mapping, bures_wasserstein_barycenter def gaussian_logpdf(x, m, C): r""" Compute the log of the probability density function of a multivariate Gaussian distribution. Parameters ---------- x : array-like, shape (..., d) The input samples. m : array-like, shape (d,) The mean vector of the Gaussian distribution. C : array-like, shape (d, d) The covariance matrix of the Gaussian distribution. Returns ------- pdf : array-like, shape (...,) The probability density function evaluated at each sample. """ assert ( x.shape[-1] == m.shape[-1] == C.shape[-1] == C.shape[-2] ), "Dimension mismatch" nx = get_backend(x, m, C) d = m.shape[0] diff = x - m inv_C = nx.inv(C) z = nx.sum(diff * (diff @ inv_C), axis=-1) _, log_det_C = nx.slogdet(C) return -0.5 * (d * np.log(2 * np.pi) + log_det_C + z) def gaussian_pdf(x, m, C): r""" Compute the probability density function of a multivariate Gaussian distribution. Parameters ---------- x : array-like, shape (..., d) The input samples. m : array-like, shape (d,) The mean vector of the Gaussian distribution. C : array-like, shape (d, d) The covariance matrix of the Gaussian distribution. Returns ------- pdf : array-like, shape (...,) The probability density function evaluated at each sample. """ return get_backend(x, m, C).exp(gaussian_logpdf(x, m, C)) def gmm_pdf(x, m, C, w): r""" Compute the probability density function (PDF) of a Gaussian Mixture Model (GMM) at given points. Parameters ---------- x : array-like, shape (..., d) The input samples. m : array-like, shape (n_components, d) The means of the Gaussian components. C : array-like, shape (n_components, d, d) The covariance matrices of the Gaussian components. w : array-like, shape (n_components,) The weights of the Gaussian components. Returns ------- out : array-like, shape (...,) The PDF values at the given points. """ assert ( m.shape[0] == C.shape[0] == w.shape[0] ), "All GMM parameters must have the same amount of components" nx = get_backend(x, m, C, w) out = nx.zeros((x.shape[:-1])) for k in range(m.shape[0]): out = out + w[k] * gaussian_pdf(x, m[k], C[k]) return out def dist_bures_squared(m_s, m_t, C_s, C_t): r""" Compute the matrix of the squared Bures distances between the components of two Gaussian Mixture Models (GMMs). Used to compute the GMM Optimal Transport distance [69]. Parameters ---------- m_s : array-like, shape (k_s, d) Mean vectors of the source GMM. m_t : array-like, shape (k_t, d) Mean vectors of the target GMM. C_s : array-like, shape (k_s, d, d) Covariance matrices of the source GMM. C_t : array-like, shape (k_t, d, d) Covariance matrices of the target GMM. Returns ------- dist : array-like, shape (k_s, k_t) Matrix of squared Bures distances between the components of the source and target GMMs. References ---------- .. [69] Delon, J., & Desolneux, A. (2020). A Wasserstein-type distance in the space of Gaussian mixture models. SIAM Journal on Imaging Sciences, 13(2), 936-970. """ nx = get_backend(m_s, C_s, m_t, C_t) assert m_s.shape[0] == C_s.shape[0], "Source GMM has different amount of components" assert m_t.shape[0] == C_t.shape[0], "Target GMM has different amount of components" assert ( m_s.shape[-1] == m_t.shape[-1] == C_s.shape[-1] == C_t.shape[-1] ), "All GMMs must have the same dimension" D_means = dist(m_s, m_t, metric="sqeuclidean") # C2[i, j] = Cs12[i] @ C_t[j] @ Cs12[i], shape (k_s, k_t, d, d) Cs12 = nx.sqrtm(C_s) # broadcasts matrix sqrt over (k_s,) C2 = nx.einsum("ikl,jlm,imn->ijkn", Cs12, C_t, Cs12) C = nx.sqrtm(C2) # broadcasts matrix sqrt over (k_s, k_t) # D_covs[i,j] = trace(C_s[i] + C_t[j] - 2C[i,j]) trace_C_s = nx.einsum("ikk->i", C_s)[:, None] # (k_s, 1) trace_C_t = nx.einsum("ikk->i", C_t)[None, :] # (1, k_t) D_covs = trace_C_s + trace_C_t # broadcasts to (k_s, k_t) D_covs -= 2 * nx.einsum("ijkk->ij", C) return nx.maximum(D_means + D_covs, 0) def gmm_ot_loss(m_s, m_t, C_s, C_t, w_s, w_t, log=False): r""" Compute the Gaussian Mixture Model (GMM) Optimal Transport distance between two GMMs introduced in [69]. Parameters ---------- m_s : array-like, shape (k_s, d) Mean vectors of the source GMM. m_t : array-like, shape (k_t, d) Mean vectors of the target GMM. C_s : array-like, shape (k_s, d, d) Covariance matrices of the source GMM. C_t : array-like, shape (k_t, d, d) Covariance matrices of the target GMM. w_s : array-like, shape (k_s,) Weights of the source GMM components. w_t : array-like, shape (k_t,) Weights of the target GMM components. log: bool, optional (default=False) If True, returns a dictionary containing the cost and dual variables. Otherwise returns only the GMM optimal transportation cost. Returns ------- loss : float or array-like The GMM-OT loss. log : dict, optional If input log is true, a dictionary containing the cost and dual variables and exit status References ---------- .. [69] Delon, J., & Desolneux, A. (2020). A Wasserstein-type distance in the space of Gaussian mixture models. SIAM Journal on Imaging Sciences, 13(2), 936-970. """ get_backend(m_s, C_s, w_s, m_t, C_t, w_t) assert m_s.shape[0] == w_s.shape[0], "Source GMM has different amount of components" assert m_t.shape[0] == w_t.shape[0], "Target GMM has different amount of components" D = dist_bures_squared(m_s, m_t, C_s, C_t) return emd2(w_s, w_t, D, log=log) def gmm_ot_plan(m_s, m_t, C_s, C_t, w_s, w_t, log=False): r""" Compute the Gaussian Mixture Model (GMM) Optimal Transport plan between two GMMs introduced in [69]. Parameters ---------- m_s : array-like, shape (k_s, d) Mean vectors of the source GMM. m_t : array-like, shape (k_t, d) Mean vectors of the target GMM. C_s : array-like, shape (k_s, d, d) Covariance matrices of the source GMM. C_t : array-like, shape (k_t, d, d) Covariance matrices of the target GMM. w_s : array-like, shape (k_s,) Weights of the source GMM components. w_t : array-like, shape (k_t,) Weights of the target GMM components. log : bool, optional (default=False) If True, returns a dictionary containing the cost and dual variables. Otherwise returns only the GMM optimal transportation matrix. Returns ------- plan : array-like, shape (k_s, k_t) The GMM-OT plan. log : dict, optional If input log is true, a dictionary containing the cost and dual variables and exit status References ---------- .. [69] Delon, J., & Desolneux, A. (2020). A Wasserstein-type distance in the space of Gaussian mixture models. SIAM Journal on Imaging Sciences, 13(2), 936-970. """ get_backend(m_s, C_s, w_s, m_t, C_t, w_t) assert m_s.shape[0] == w_s.shape[0], "Source GMM has different amount of components" assert m_t.shape[0] == w_t.shape[0], "Target GMM has different amount of components" D = dist_bures_squared(m_s, m_t, C_s, C_t) return emd(w_s, w_t, D, log=log) def gmm_ot_apply_map( x, m_s, m_t, C_s, C_t, w_s, w_t, plan=None, method="bary", seed=None ): r""" Apply Gaussian Mixture Model (GMM) optimal transport (OT) mapping to input data. The 'barycentric' mapping corresponds to the barycentric projection of the GMM-OT plan, and is called T_bary in [69]. The 'random' mapping takes for each input point a random pair (i,j) of components of the GMMs and applied the Gaussian map, it is called T_rand in [69]. Parameters ---------- x : array-like, shape (n_samples, d) Input data points. m_s : array-like, shape (k_s, d) Mean vectors of the source GMM components. m_t : array-like, shape (k_t, d) Mean vectors of the target GMM components. C_s : array-like, shape (k_s, d, d) Covariance matrices of the source GMM components. C_t : array-like, shape (k_t, d, d) Covariance matrices of the target GMM components. w_s : array-like, shape (k_s,) Weights of the source GMM components. w_t : array-like, shape (k_t,) Weights of the target GMM components. plan : array-like, shape (k_s, k_t), optional Optimal transport plan between the source and target GMM components. If not provided, it will be computed internally. method : {'bary', 'rand'}, optional Method for applying the GMM OT mapping. 'bary' uses barycentric mapping, while 'rand' uses random sampling. Default is 'bary'. seed : int, optional Seed for the random number generator. Only used when method='rand'. Returns ------- out : array-like, shape (n_samples, d) Output data points after applying the GMM OT mapping. References ---------- .. [69] Delon, J., & Desolneux, A. (2020). A Wasserstein-type distance in the space of Gaussian mixture models. SIAM Journal on Imaging Sciences, 13(2), 936-970. """ if plan is None: plan = gmm_ot_plan(m_s, m_t, C_s, C_t, w_s, w_t) nx = get_backend(x, m_s, m_t, C_s, C_t, w_s, w_t) else: nx = get_backend(x, m_s, m_t, C_s, C_t, w_s, w_t, plan) k_s, k_t = m_s.shape[0], m_t.shape[0] d = m_s.shape[1] n_samples = x.shape[0] if method == "bary": out = nx.zeros(x.shape) logpdf = nx.stack( [gaussian_logpdf(x, m_s[k], C_s[k])[:, None] for k in range(k_s)] ) # only need to compute for non-zero plan entries for i, j in zip(*nx.where(plan > 0)): Cs12 = nx.sqrtm(C_s[i]) Cs12inv = nx.inv(Cs12) M0 = nx.sqrtm(Cs12 @ C_t[j] @ Cs12) A = Cs12inv @ M0 @ Cs12inv b = m_t[j] - A @ m_s[i] # gaussian mapping between components i and j applied to x T_ij_x = x @ A + b z = w_s[:, None, None] * nx.exp(logpdf - logpdf[i][None, :, :]) denom = nx.sum(z, axis=0) out = out + plan[i, j] * T_ij_x / denom return out else: # rand # A[i, j] is the linear part of the gaussian mapping between components # i and j, b[i, j] is the translation part rng = np.random.RandomState(seed) A = nx.zeros((k_s, k_t, d, d)) b = nx.zeros((k_s, k_t, d)) # only need to compute for non-zero plan entries for i, j in zip(*nx.where(plan > 0)): Cs12 = nx.sqrtm(C_s[i]) Cs12inv = nx.inv(Cs12) M0 = nx.sqrtm(Cs12 @ C_t[j] @ Cs12) A[i, j] = Cs12inv @ M0 @ Cs12inv b[i, j] = m_t[j] - A[i, j] @ m_s[i] logpdf = nx.stack( [gaussian_logpdf(x, m_s[k], C_s[k]) for k in range(k_s)], axis=-1 ) # (n_samples, k_s) out = nx.zeros(x.shape) for i_sample in range(n_samples): log_g = logpdf[i_sample] log_diff = log_g[:, None] - log_g[None, :] weighted_exp = w_s[:, None] * nx.exp(log_diff) denom = nx.sum(weighted_exp, axis=0)[:, None] * nx.ones(plan.shape[1]) p_mat = plan / denom p = p_mat.reshape(k_s * k_t) # stack line-by-line # sample between 0 and k_s * k_t - 1 ij_mat = rng.choice(k_s * k_t, p=p) i = ij_mat // k_t j = ij_mat % k_t out[i_sample] = A[i, j] @ x[i_sample] + b[i, j] return out def gmm_ot_plan_density(x, y, m_s, m_t, C_s, C_t, w_s, w_t, plan=None, atol=1e-2): """ Compute the density of the Gaussian Mixture Model - Optimal Transport coupling between GMMS at given points, as introduced in [69]. Given two arrays of points x and y, the function computes the density at each point `(x[i], y[i])` of the product space. Parameters ---------- x : array-like, shape (n, d) Entry points in source space for density computation. y : array-like, shape (m, d) Entry points in target space for density computation. m_s : array-like, shape (k_s, d) The means of the source GMM components. m_t : array-like, shape (k_t, d) The means of the target GMM components. C_s : array-like, shape (k_s, d, d) The covariance matrices of the source GMM components. C_t : array-like, shape (k_t, d, d) The covariance matrices of the target GMM components. w_s : array-like, shape (k_s,) The weights of the source GMM components. w_t : array-like, shape (k_t,) The weights of the target GMM components. plan : array-like, shape (k_s, k_t), optional The optimal transport plan between the source and target GMMs. If not provided, it will be computed using `gmm_ot_plan`. atol : float, optional The absolute tolerance used to determine the support of the GMM-OT coupling. Returns ------- density : array-like, shape (n, m) The density of the GMM-OT coupling between the two GMMs. References ---------- .. [69] Delon, J., & Desolneux, A. (2020). A Wasserstein-type distance in the space of Gaussian mixture models. SIAM Journal on Imaging Sciences, 13(2), 936-970. """ assert ( x.shape[-1] == y.shape[-1] ), "x (n, d) and y (m, d) must have the same dimension d" n, m = x.shape[0], y.shape[0] nx = get_backend(x, y, m_s, m_t, C_s, C_t, w_s, w_t) # hand-made d-variate meshgrid in ij indexing xx = x[:, None, :] * nx.ones((1, m, 1)) # shapes (n, m, d) yy = y[None, :, :] * nx.ones((n, 1, 1)) # shapes (n, m, d) if plan is None: plan = gmm_ot_plan(m_s, m_t, C_s, C_t, w_s, w_t) def Tk0k1(k0, k1): A, b = bures_wasserstein_mapping(m_s[k0], m_t[k1], C_s[k0], C_t[k1]) Tx = xx @ A + b g = gaussian_pdf(xx, m_s[k0], C_s[k0]) out = plan[k0, k1] * g norms = nx.norm(Tx - yy, axis=-1) out = out * ((norms < atol) * 1.0) return out mat = nx.stack( [ nx.stack([Tk0k1(k0, k1) for k1 in range(m_t.shape[0])]) for k0 in range(m_s.shape[0]) ] ) return nx.sum(mat, axis=(0, 1)) def gmm_barycenter_fixed_point( means_list, covs_list, w_list, means_init, covs_init, weights, w_bar=None, iterations=100, log=False, barycentric_proj_method="euclidean", ): r""" Solves the Gaussian Mixture Model OT barycenter problem (defined in [69]) using the fixed point algorithm (proposed in [77]). The weights of the barycenter are not optimized, and stay the same as the input `w_list` or are initialized to uniform. The algorithm uses barycentric projections of GMM-OT plans, and these can be computed either through Bures Barycenters (slow but accurate, barycentric_proj_method='bures') or by convex combination (fast, barycentric_proj_method='euclidean', default). This is a special case of the generic free-support barycenter solver `ot.lp.free_support_barycenter_generic_costs`. Parameters ---------- means_list : list of array-like List of K (m_k, d) GMM means. covs_list : list of array-like List of K (m_k, d, d) GMM covariances. w_list : list of array-like List of K (m_k) arrays of weights. means_init : array-like Initial (n, d) GMM means. covs_init : array-like Initial (n, d, d) GMM covariances. weights : array-like Array (K,) of the barycentre coefficients. w_bar : array-like, optional Initial weights (n) of the barycentre GMM. If None, initialized to uniform. iterations : int, optional Number of iterations (default is 100). log : bool, optional Whether to return the list of iterations (default is False). barycentric_proj_method : str, optional Method to project the barycentre weights: 'euclidean' (default) or 'bures'. Returns ------- means : array-like (n, d) barycentre GMM means. covs : array-like (n, d, d) barycentre GMM covariances. log_dict : dict, optional Dictionary containing the list of iterations if log is True. References ---------- .. [69] Delon, J., & Desolneux, A. (2020). A Wasserstein-type distance in the space of Gaussian mixture models. SIAM Journal on Imaging Sciences, 13(2), 936-970. .. [77] Tanguy, Eloi and Delon, Julie and Gozlan, Nathaël (2024). Computing barycenters of Measures for Generic Transport Costs. arXiv preprint 2501.04016 (2024) See Also -------- ot.lp.free_support_barycenter_generic_costs : Compute barycenter of measures for generic transport costs. """ nx = get_backend( means_init, covs_init, means_list[0], covs_list[0], w_list[0], weights ) K = len(means_list) n = means_init.shape[0] d = means_init.shape[1] means_its = [nx.copy(means_init)] covs_its = [nx.copy(covs_init)] means, covs = means_init, covs_init if w_bar is None: w_bar = nx.ones(n, type_as=means) / n for _ in range(iterations): pi_list = [ gmm_ot_plan(means, means_list[k], covs, covs_list[k], w_bar, w_list[k]) for k in range(K) ] # filled in the euclidean case means_selection, covs_selection = None, None # in the euclidean case, the selection of Gaussians from each K sources # comes from a barycentric projection: it is a convex combination of the # selected means and covariances, which can be computed without a # for loop on i = 0, ..., n -1 if barycentric_proj_method == "euclidean": means_selection = nx.zeros((n, K, d), type_as=means) covs_selection = nx.zeros((n, K, d, d), type_as=means) for k in range(K): means_selection[:, k, :] = n * pi_list[k] @ means_list[k] covs_selection[:, k, :, :] = ( nx.einsum("ij,jab->iab", pi_list[k], covs_list[k]) * n ) # each component i of the barycentre will be a Bures barycentre of the # selected components of the K GMMs. In the 'bures' barycentric # projection option, the selected components are also Bures barycentres. for i in range(n): # means_selection_i (K, d) is the selected means, each comes from a # Gaussian barycentre along the disintegration of pi_k at i # covs_selection_i (K, d, d) are the selected covariances means_selection_i = None covs_selection_i = None # use previous computation (convex combination) if barycentric_proj_method == "euclidean": means_selection_i = means_selection[i] covs_selection_i = covs_selection[i] # compute Bures barycentre of certain components to get the # selection at i elif barycentric_proj_method == "bures": means_selection_i = nx.zeros((K, d), type_as=means) covs_selection_i = nx.zeros((K, d, d), type_as=means) for k in range(K): w = (1 / w_bar[i]) * pi_list[k][i, :] m, C = bures_wasserstein_barycenter(means_list[k], covs_list[k], w) means_selection_i[k] = m covs_selection_i[k] = C else: raise ValueError("Unknown barycentric_proj_method") means[i], covs[i] = bures_wasserstein_barycenter( means_selection_i, covs_selection_i, weights ) if log: means_its.append(nx.copy(means)) covs_its.append(nx.copy(covs)) if log: return means, covs, {"means_its": means_its, "covs_its": covs_its} return means, covs python-pot-0.9.7+dfsg/ot/gnn/000077500000000000000000000000001525521106200157725ustar00rootroot00000000000000python-pot-0.9.7+dfsg/ot/gnn/__init__.py000066400000000000000000000013651525521106200201100ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Layers and functions for optimal transport in Graph Neural Networks. .. warning:: Note that by default the module is not imported in :mod:`ot`. In order to use it you need to explicitly import :mod:`ot.gnn`. This module is PyTorch Geometric dependent. The layers are compatible with their API. """ # Author: Sonia Mazelet # Rémi Flamary # # License: MIT License # All submodules and packages from ._utils import FGW_distance_to_templates, wasserstein_distance_to_templates from ._layers import TFGWPooling, TWPooling __all__ = [ "FGW_distance_to_templates", "wasserstein_distance_to_templates", "TFGWPooling", "TWPooling", ] python-pot-0.9.7+dfsg/ot/gnn/_layers.py000066400000000000000000000255501525521106200200110ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Template Fused Gromov Wasserstein """ # Author: Sonia Mazelet # Rémi Flamary # # License: MIT License try: import torch import torch.nn as nn from ._utils import ( TFGW_template_initialization, FGW_distance_to_templates, wasserstein_distance_to_templates, ) except ImportError: pass class TFGWPooling(nn.Module): r""" Template Fused Gromov-Wasserstein (TFGW) layer. This layer is a pooling layer for graph neural networks. Computes the fused Gromov-Wasserstein distances between the graph and a set of templates. .. math:: TFGW_{ \overline{ \mathcal{G} }, \alpha }(C,F,h)=[ FGW_{\alpha}(C,F,h,\overline{C}_k,\overline{F}_k,\overline{h}_k)]_{k=1}^{K} where : - :math:`\mathcal{G}=\{(\overline{C}_k,\overline{F}_k,\overline{h}_k) \}_{k \in \{1,...,K \}} \}` is the set of :math:`K` templates characterized by their adjacency matrices :math:`\overline{C}_k`, their feature matrices :math:`\overline{F}_k` and their node weights :math:`\overline{h}_k`. - :math:`C`, :math:`F` and :math:`h` are respectively the adjacency matrix, the feature matrix and the node weights of the graph. - :math:`\alpha` is the trade-off parameter between features and structure for the Fused Gromov-Wasserstein distance. Parameters ---------- n_features : int Feature dimension of the nodes. n_tplt : int Number of graph templates. n_tplt_nodes : int Number of nodes in each template. alpha : float, optional FGW trade-off parameter (0 < alpha < 1). If None alpha is trained, else it is fixed at the given value. Weights features (alpha=0) and structure (alpha=1). train_node_weights : bool, optional If True, the templates node weights are learned. Else, they are uniform. multi_alpha: bool, optional If True, the alpha parameter is a vector of size n_tplt. feature_init_mean: float, optional Mean of the random normal law to initialize the template features. feature_init_std: float, optional Standard deviation of the random normal law to initialize the template features. References ---------- .. [53] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Template based graph neural network with optimal transport distances" """ def __init__( self, n_features, n_tplt=2, n_tplt_nodes=2, alpha=None, train_node_weights=True, multi_alpha=False, feature_init_mean=0.0, feature_init_std=1.0, ): r""" Template Fused Gromov-Wasserstein (TFGW) layer. This layer is a pooling layer for graph neural networks. Computes the fused Gromov-Wasserstein distances between the graph and a set of templates. .. math:: TFGW_{\overline{\mathcal{G}},\alpha}(C,F,h)=[FGW_{\alpha}(C,F,h,\overline{C}_k,\overline{F}_k,\overline{h}_k)]_{k=1}^{K} where : - :math:`\mathcal{G}=\{(\overline{C}_k,\overline{F}_k,\overline{h}_k) \}_{k \in \{1,...,K \}} }` is the set of :math:`K` templates charactersised by their adjacency matrices :math:`\overline{C}_k`, their feature matrices :math:`\overline{F}_k` and their node weights :math:`\overline{h}_k`. - :math:`C`, :math:`F` and :math:`h` are respectively the adjacency matrix, the feature matrix and the node weights of the graph. - :math:`\alpha` is the trade-off parameter between features and structure for the Fused Gromov-Wasserstein distance. Parameters ---------- n_features : int Feature dimension of the nodes. n_tplt : int Number of graph templates. n_tplt_nodes : int Number of nodes in each template. alpha : float, optional FGW trade-off parameter (0 < alpha < 1). If None alpha is trained, else it is fixed at the given value. Weights features (alpha=0) and structure (alpha=1). train_node_weights : bool, optional If True, the templates node weights are learned. Else, they are uniform. multi_alpha: bool, optional If True, the alpha parameter is a vector of size n_tplt. feature_init_mean: float, optional Mean of the random normal law to initialize the template features. feature_init_std: float, optional Standard deviation of the random normal law to initialize the template features. References ---------- .. [53] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Template based graph neural network with optimal transport distances" """ super().__init__() self.n_tplt = n_tplt self.n_tplt_nodes = n_tplt_nodes self.n_features = n_features self.multi_alpha = multi_alpha self.feature_init_mean = feature_init_mean self.feature_init_std = feature_init_std tplt_adjacencies, tplt_features, self.q0 = TFGW_template_initialization( self.n_tplt, self.n_tplt_nodes, self.n_features, self.feature_init_mean, self.feature_init_std, ) self.tplt_adjacencies = nn.Parameter(tplt_adjacencies) self.tplt_features = nn.Parameter(tplt_features) self.softmax = nn.Softmax(dim=1) if train_node_weights: self.q0 = nn.Parameter(self.q0) if alpha is None: if multi_alpha: self.alpha0 = torch.Tensor([0] * self.n_tplt) else: self.alpha0 = torch.Tensor([0]) self.alpha0 = nn.Parameter(self.alpha0) else: if multi_alpha: self.alpha0 = torch.Tensor([alpha] * self.n_tplt) else: self.alpha0 = torch.Tensor([alpha]) self.alpha0 = torch.logit(self.alpha0) def forward(self, x, edge_index, batch=None): """ Parameters ---------- x : torch.Tensor Node features. edge_index : torch.Tensor Edge indices. batch : torch.Tensor, optional Batch vector which assigns each node to its graph. """ alpha = torch.sigmoid(self.alpha0) q = self.softmax(self.q0) x = FGW_distance_to_templates( edge_index, self.tplt_adjacencies, x, self.tplt_features, q, alpha, self.multi_alpha, batch, ) return x class TWPooling(nn.Module): r""" Template Wasserstein (TW) layer, also known as OT-GNN layer. This layer is a pooling layer for graph neural networks. Computes the Wasserstein distances between the features of the graph features and a set of templates. .. math:: TW_{\overline{\mathcal{G}}}(C,F,h)=[W(F,h,\overline{F}_k,\overline{h}_k)]_{k=1}^{K} where : - :math:`\mathcal{G}=\{(\overline{F}_k,\overline{h}_k) \}_{k \in \{1,...,K \}} \}` is the set of :math:`K` templates characterized by their feature matrices :math:`\overline{F}_k` and their node weights :math:`\overline{h}_k`. - :math:`F` and :math:`h` are respectively the feature matrix and the node weights of the graph. Parameters ---------- n_features : int Feature dimension of the nodes. n_tplt : int Number of graph templates. n_tplt_nodes : int Number of nodes in each template. train_node_weights : bool, optional If True, the templates node weights are learned. Else, they are uniform. feature_init_mean: float, optional Mean of the random normal law to initialize the template features. feature_init_std: float, optional Standard deviation of the random normal law to initialize the template features. References ---------- .. [54] Bécigneul, G., Ganea, O. E., Chen, B., Barzilay, R., & Jaakkola, T. S. (2020). [Optimal transport graph neural networks] """ def __init__( self, n_features, n_tplt=2, n_tplt_nodes=2, train_node_weights=True, feature_init_mean=0.0, feature_init_std=1.0, ): r""" Template Wasserstein (TW) layer, also known as OT-GNN layer. This layer is a pooling layer for graph neural networks. Computes the Wasserstein distances between the features of the graph features and a set of templates. .. math:: TW_{\overline{\mathcal{G}}}(C,F,h)=[W(F,h,\overline{F}_k,\overline{h}_k)]_{k=1}^{K} where : - :math:`\mathcal{G}=\{(\overline{F}_k,\overline{h}_k) \}_{k \in \llbracket 1;K \rrbracket }` is the set of :math:`K` templates characterized by their feature matrices :math:`\overline{F}_k` and their node weights :math:`\overline{h}_k`. - :math:`F` and :math:`h` are respectively the feature matrix and the node weights of the graph. Parameters ---------- n_features : int Feature dimension of the nodes. n_tplt : int Number of graph templates. n_tplt_nodes : int Number of nodes in each template. train_node_weights : bool, optional If True, the templates node weights are learned. Else, they are uniform. feature_init_mean: float, optional Mean of the random normal law to initialize the template features. feature_init_std: float, optional Standard deviation of the random normal law to initialize the template features. References ---------- .. [54] Bécigneul, G., Ganea, O. E., Chen, B., Barzilay, R., & Jaakkola, T. S. (2020). [Optimal transport graph neural networks] """ super().__init__() self.n_tplt = n_tplt self.n_tplt_nodes = n_tplt_nodes self.n_features = n_features self.feature_init_mean = feature_init_mean self.feature_init_std = feature_init_std _, tplt_features, self.q0 = TFGW_template_initialization( self.n_tplt, self.n_tplt_nodes, self.n_features, self.feature_init_mean, self.feature_init_std, ) self.tplt_features = nn.Parameter(tplt_features) self.softmax = nn.Softmax(dim=1) if train_node_weights: self.q0 = nn.Parameter(self.q0) def forward(self, x, edge_index=None, batch=None): """ Parameters ---------- x : torch.Tensor Node features. edge_index : torch.Tensor Edge indices. batch : torch.Tensor, optional Batch vector which assigns each node to its graph. """ q = self.softmax(self.q0) x = wasserstein_distance_to_templates(x, self.tplt_features, q, batch) return x python-pot-0.9.7+dfsg/ot/gnn/_utils.py000066400000000000000000000241171525521106200176500ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ GNN layers utils """ # Author: Sonia Mazelet # Rémi Flamary # # License: MIT License import torch from ..utils import dist from ..gromov import fused_gromov_wasserstein2 from ..lp import emd2 import warnings try: from torch_geometric.utils import subgraph except ImportError: warnings.warn( "torch_geometric is not installed. The ot.gnn module requires torch_geometric to be installed." ) pass def TFGW_template_initialization( n_tplt, n_tplt_nodes, n_features, feature_init_mean=0.0, feature_init_std=1.0 ): """ Initializes templates for the Template Fused Gromov Wasserstein layer. Returns the adjacency matrices and the features of the nodes of the templates. Adjacency matrices are initialized uniformly with values in :math:`[0,1]`. Node features are initialized following a normal distribution. Parameters ---------- n_tplt: int Number of templates. n_tplt_nodes: int Number of nodes per template. n_features: int Number of features for the nodes. feature_init_mean: float, optional Mean of the random normal law to initialize the template features. feature_init_std: float, optional Standard deviation of the random normal law to initialize the template features. Returns ---------- tplt_adjacencies: torch.Tensor, shape (n_templates, n_template_nodes, n_template_nodes) Adjacency matrices for the templates. tplt_features: torch.Tensor, shape (n_templates, n_template_nodes, n_features) Node features for each template. q: torch.Tensor, shape (n_templates, n_template_nodes) weight on the template nodes. """ tplt_adjacencies = torch.rand((n_tplt, n_tplt_nodes, n_tplt_nodes)) tplt_features = torch.Tensor(n_tplt, n_tplt_nodes, n_features) torch.nn.init.normal_(tplt_features, mean=feature_init_mean, std=feature_init_std) q = torch.zeros(n_tplt, n_tplt_nodes) tplt_adjacencies = 0.5 * ( tplt_adjacencies + torch.transpose(tplt_adjacencies, 1, 2) ) return tplt_adjacencies, tplt_features, q def FGW_distance_to_templates( G_edges, tplt_adjacencies, G_features, tplt_features, tplt_weights, alpha=0.5, multi_alpha=False, batch=None, ): """ Computes the FGW distances between a graph and templates. Parameters ---------- G_edges : torch.Tensor, shape (n_edges, 2) Edge indices of the graph in the Pytorch Geometric format. tplt_adjacencies : list of torch.Tensor, shape (n_templates, n_template_nodes, n_templates_nodes) List of the adjacency matrices of the templates. G_features : torch.Tensor, shape (n_nodes, n_features) Graph node features. tplt_features : list of torch.Tensor, shape (n_templates, n_template_nodes, n_features) List of the node features of the templates. weights : torch.Tensor, shape (n_templates, n_template_nodes) Weights on the nodes of the templates. alpha : float, optional Trade-off parameter (0 < alpha < 1). Weights features (alpha=0) and structure (alpha=1). multi_alpha: bool, optional If True, the alpha parameter is a vector of size n_templates. batch: torch.Tensor, optional Batch vector which assigns each node to its graph. Returns ------- distances : torch.Tensor, shape (n_templates) if batch=None, else shape (n_graphs, n_templates). Vector of fused Gromov-Wasserstein distances between the graph and the templates. """ if batch is None: n, n_feat = G_features.shape n_T, _, n_feat_T = tplt_features.shape weights_G = torch.ones(n) / n C = torch.sparse_coo_tensor( G_edges, torch.ones(len(G_edges[0])), size=(n, n) ).type(torch.float) C = C.to_dense() if not n_feat == n_feat_T: raise ValueError( "The templates and the graphs must have the same feature dimension." ) distances = torch.zeros(n_T) for j in range(n_T): template_features = tplt_features[j].reshape( len(tplt_features[j]), n_feat_T ) M = dist(G_features, template_features).type(torch.float) # if alpha is zero the emd distance is used if multi_alpha and torch.any(alpha > 0): embedding = fused_gromov_wasserstein2( M, C, tplt_adjacencies[j], weights_G, tplt_weights[j], alpha=alpha[j], symmetric=True, max_iter=50, ) elif not multi_alpha and torch.all(alpha == 0): embedding = emd2(weights_G, tplt_weights[j], M, numItermax=50) elif not multi_alpha and alpha > 0: embedding = fused_gromov_wasserstein2( M, C, tplt_adjacencies[j], weights_G, tplt_weights[j], alpha=alpha, symmetric=True, max_iter=50, ) else: embedding = emd2(weights_G, tplt_weights[j], M, numItermax=50) distances[j] = embedding else: n_T, _, n_feat_T = tplt_features.shape num_graphs = torch.max(batch) + 1 distances = torch.zeros(num_graphs, n_T) # iterate over the graphs in the batch for i in range(num_graphs): nodes = torch.where(batch == i)[0] G_edges_i, _ = subgraph(nodes, edge_index=G_edges, relabel_nodes=True) G_features_i = G_features[nodes] n, n_feat = G_features_i.shape weights_G = torch.ones(n) / n n_edges = len(G_edges_i[0]) C = torch.sparse_coo_tensor( G_edges_i, torch.ones(n_edges), size=(n, n) ).type(torch.float) C = C.to_dense() if not n_feat == n_feat_T: raise ValueError( "The templates and the graphs must have the same feature dimension." ) for j in range(n_T): template_features = tplt_features[j].reshape( len(tplt_features[j]), n_feat_T ) M = dist(G_features_i, template_features).type(torch.float) # if alpha is zero the emd distance is used if multi_alpha and torch.any(alpha > 0): embedding = fused_gromov_wasserstein2( M, C, tplt_adjacencies[j], weights_G, tplt_weights[j], alpha=alpha[j], symmetric=True, max_iter=50, ) elif not multi_alpha and torch.all(alpha == 0): embedding = emd2(weights_G, tplt_weights[j], M, numItermax=50) elif not multi_alpha and alpha > 0: embedding = fused_gromov_wasserstein2( M, C, tplt_adjacencies[j], weights_G, tplt_weights[j], alpha=alpha, symmetric=True, max_iter=50, ) else: embedding = emd2(weights_G, tplt_weights[j], M, numItermax=50) distances[i, j] = embedding return distances def wasserstein_distance_to_templates( G_features, tplt_features, tplt_weights, batch=None ): """ Computes the Wasserstein distances between a graph and graph templates. Parameters ---------- G_features : torch.Tensor, shape (n_nodes, n_features) Node features of the graph. tplt_features : list of torch.Tensor, shape (n_templates, n_template_nodes, n_features) List of the node features of the templates. weights : torch.Tensor, shape (n_templates, n_template_nodes) Weights on the nodes of the templates. batch: torch.Tensor, optional Batch vector which assigns each node to its graph. Returns ------- distances : torch.Tensor, shape (n_templates) if batch=None, else shape (n_graphs, n_templates) Vector of Wasserstein distances between the graph and the templates. """ if batch is None: n, n_feat = G_features.shape n_T, _, n_feat_T = tplt_features.shape weights_G = torch.ones(n) / n if not n_feat == n_feat_T: raise ValueError( "The templates and the graphs must have the same feature dimension." ) distances = torch.zeros(n_T) for j in range(n_T): template_features = tplt_features[j].reshape( len(tplt_features[j]), n_feat_T ) M = dist(G_features, template_features).type(torch.float) distances[j] = emd2(weights_G, tplt_weights[j], M, numItermax=50) else: n_T, _, n_feat_T = tplt_features.shape num_graphs = torch.max(batch) + 1 distances = torch.zeros(num_graphs, n_T) # iterate over the graphs in the batch for i in range(num_graphs): nodes = torch.where(batch == i)[0] G_features_i = G_features[nodes] n, n_feat = G_features_i.shape weights_G = torch.ones(n) / n if not n_feat == n_feat_T: raise ValueError( "The templates and the graphs must have the same feature dimension." ) for j in range(n_T): template_features = tplt_features[j].reshape( len(tplt_features[j]), n_feat_T ) M = dist(G_features_i, template_features).type(torch.float) distances[i, j] = emd2(weights_G, tplt_weights[j], M, numItermax=50) return distances python-pot-0.9.7+dfsg/ot/gromov/000077500000000000000000000000001525521106200165215ustar00rootroot00000000000000python-pot-0.9.7+dfsg/ot/gromov/__init__.py000066400000000000000000000131151525521106200206330ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Solvers related to Gromov-Wasserstein problems. """ # Author: Remi Flamary # Cedric Vincent-Cuaz # Quang Huy Tran # # License: MIT License # All submodules and packages from ._utils import ( init_matrix, tensor_product, gwloss, gwggrad, init_matrix_semirelaxed, semirelaxed_init_plan, update_barycenter_structure, update_barycenter_feature, div_between_product, div_to_product, fused_unbalanced_across_spaces_cost, uot_cost_matrix, uot_parameters_and_measures, ) from ._gw import ( gromov_wasserstein, gromov_wasserstein2, fused_gromov_wasserstein, fused_gromov_wasserstein2, solve_gromov_linesearch, gromov_barycenters, fgw_barycenters, ) from ._bregman import ( entropic_gromov_wasserstein, entropic_gromov_wasserstein2, BAPG_gromov_wasserstein, BAPG_gromov_wasserstein2, entropic_gromov_barycenters, entropic_fused_gromov_wasserstein, entropic_fused_gromov_wasserstein2, BAPG_fused_gromov_wasserstein, BAPG_fused_gromov_wasserstein2, entropic_fused_gromov_barycenters, ) from ._estimators import ( GW_distance_estimation, pointwise_gromov_wasserstein, sampled_gromov_wasserstein, ) from ._semirelaxed import ( semirelaxed_gromov_wasserstein, semirelaxed_gromov_wasserstein2, semirelaxed_fused_gromov_wasserstein, semirelaxed_fused_gromov_wasserstein2, solve_semirelaxed_gromov_linesearch, entropic_semirelaxed_gromov_wasserstein, entropic_semirelaxed_gromov_wasserstein2, entropic_semirelaxed_fused_gromov_wasserstein, entropic_semirelaxed_fused_gromov_wasserstein2, semirelaxed_gromov_barycenters, semirelaxed_fgw_barycenters, ) from ._dictionary import ( gromov_wasserstein_dictionary_learning, gromov_wasserstein_linear_unmixing, fused_gromov_wasserstein_dictionary_learning, fused_gromov_wasserstein_linear_unmixing, ) from ._lowrank import lowrank_gromov_wasserstein_samples from ._quantized import ( quantized_fused_gromov_wasserstein_partitioned, get_graph_partition, get_graph_representants, format_partitioned_graph, quantized_fused_gromov_wasserstein, get_partition_and_representants_samples, format_partitioned_samples, quantized_fused_gromov_wasserstein_samples, ) from ._unbalanced import ( fused_unbalanced_gromov_wasserstein, fused_unbalanced_gromov_wasserstein2, unbalanced_co_optimal_transport, unbalanced_co_optimal_transport2, fused_unbalanced_across_spaces_divergence, ) from ._partial import ( partial_gromov_wasserstein, partial_gromov_wasserstein2, partial_fused_gromov_wasserstein, partial_fused_gromov_wasserstein2, solve_partial_gromov_linesearch, entropic_partial_gromov_wasserstein, entropic_partial_gromov_wasserstein2, entropic_partial_fused_gromov_wasserstein, entropic_partial_fused_gromov_wasserstein2, ) __all__ = [ "init_matrix", "tensor_product", "gwloss", "gwggrad", "init_matrix_semirelaxed", "semirelaxed_init_plan", "update_barycenter_structure", "update_barycenter_feature", "div_between_product", "div_to_product", "fused_unbalanced_across_spaces_cost", "uot_cost_matrix", "uot_parameters_and_measures", "gromov_wasserstein", "gromov_wasserstein2", "fused_gromov_wasserstein", "fused_gromov_wasserstein2", "solve_gromov_linesearch", "gromov_barycenters", "fgw_barycenters", "entropic_gromov_wasserstein", "entropic_gromov_wasserstein2", "BAPG_gromov_wasserstein", "BAPG_gromov_wasserstein2", "entropic_gromov_barycenters", "entropic_fused_gromov_wasserstein", "entropic_fused_gromov_wasserstein2", "BAPG_fused_gromov_wasserstein", "BAPG_fused_gromov_wasserstein2", "entropic_fused_gromov_barycenters", "GW_distance_estimation", "pointwise_gromov_wasserstein", "sampled_gromov_wasserstein", "semirelaxed_gromov_wasserstein", "semirelaxed_gromov_wasserstein2", "semirelaxed_fused_gromov_wasserstein", "semirelaxed_fused_gromov_wasserstein2", "solve_semirelaxed_gromov_linesearch", "entropic_semirelaxed_gromov_wasserstein", "entropic_semirelaxed_gromov_wasserstein2", "entropic_semirelaxed_fused_gromov_wasserstein", "entropic_semirelaxed_fused_gromov_wasserstein2", "semirelaxed_fgw_barycenters", "semirelaxed_gromov_barycenters", "gromov_wasserstein_dictionary_learning", "gromov_wasserstein_linear_unmixing", "fused_gromov_wasserstein_dictionary_learning", "fused_gromov_wasserstein_linear_unmixing", "lowrank_gromov_wasserstein_samples", "quantized_fused_gromov_wasserstein_partitioned", "get_graph_partition", "get_graph_representants", "format_partitioned_graph", "quantized_fused_gromov_wasserstein", "get_partition_and_representants_samples", "format_partitioned_samples", "quantized_fused_gromov_wasserstein_samples", "fused_unbalanced_gromov_wasserstein", "fused_unbalanced_gromov_wasserstein2", "unbalanced_co_optimal_transport", "unbalanced_co_optimal_transport2", "fused_unbalanced_across_spaces_divergence", "partial_gromov_wasserstein", "partial_gromov_wasserstein2", "partial_fused_gromov_wasserstein", "partial_fused_gromov_wasserstein2", "solve_partial_gromov_linesearch", "entropic_partial_gromov_wasserstein", "entropic_partial_gromov_wasserstein2", "entropic_partial_fused_gromov_wasserstein", "entropic_partial_fused_gromov_wasserstein2", ] python-pot-0.9.7+dfsg/ot/gromov/_bregman.py000066400000000000000000002163721525521106200206600ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Bregman projections solvers for entropic Gromov-Wasserstein """ # Author: Erwan Vautier # Nicolas Courty # Rémi Flamary # Titouan Vayer # Cédric Vincent-Cuaz # # License: MIT License import numpy as np import warnings from ..bregman import sinkhorn from ..utils import dist, UndefinedParameter, list_to_array, check_random_state, unif from ..backend import get_backend from ._utils import init_matrix, gwloss, gwggrad from ._utils import update_barycenter_structure, update_barycenter_feature def entropic_gromov_wasserstein( C1, C2, p=None, q=None, loss_fun="square_loss", epsilon=0.1, symmetric=None, G0=None, max_iter=1000, tol=1e-9, solver="PGD", warmstart=False, verbose=False, log=False, **kwargs, ): r""" Returns the Gromov-Wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})` estimated using Sinkhorn projections. If `solver="PGD"`, the function solves the following entropic-regularized Gromov-Wasserstein optimization problem using Projected Gradient Descent [12]: .. math:: \mathbf{T}^* \in \mathop{\arg\min}_\mathbf{T} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} - \epsilon H(\mathbf{T}) s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Else if `solver="PPA"`, the function solves the following Gromov-Wasserstein optimization problem using Proximal Point Algorithm [51]: .. math:: \mathbf{T}^* \in \mathop{\arg\min}_\mathbf{T} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - `L`: loss function to account for the misfit between the similarity matrices - `H`: entropy .. note:: If the inner solver `ot.sinkhorn` did not convergence, the optimal coupling :math:`\mathbf{T}` returned by this function does not necessarily satisfy the marginal constraints :math:`\mathbf{T}\mathbf{1}=\mathbf{p}` and :math:`\mathbf{T}^T\mathbf{1}=\mathbf{q}`. So the returned Gromov-Wasserstein loss does not necessarily satisfy distance properties and may be negative. Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. loss_fun : string, optional (default='square_loss') Loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float, optional Regularization term >0 symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). G0: array-like, shape (ns,nt), optional If None the initial transport plan of the solver is pq^T. Otherwise G0 will be used as initial transport of the solver. G0 is not required to satisfy marginal constraints but we strongly recommend it to correctly estimate the GW distance. max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) solver: string, optional Solver to use either 'PGD' for Projected Gradient Descent or 'PPA' for Proximal Point Algorithm. Default value is 'PGD'. warmstart: bool, optional Either to perform warmstart of dual potentials in the successive Sinkhorn projections. verbose : bool, optional Print information along iterations log : bool, optional Record log if True. **kwargs: dict parameters can be directly passed to the ot.sinkhorn solver. Such as `numItermax` and `stopThr` to control its estimation precision, e.g [51] suggests to use `numItermax=1`. Returns ------- T : array-like, shape (`ns`, `nt`) Optimal coupling between the two spaces References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. .. [47] Chowdhury, S., & Mémoli, F. (2019). The gromov–wasserstein distance between networks and stable network invariants. Information and Inference: A Journal of the IMA, 8(4), 757-787. .. [51] Xu, H., Luo, D., Zha, H., & Carin, L. (2019). Gromov-wasserstein learning for graph matching and node embedding. In International Conference on Machine Learning (ICML), 2019. """ if solver not in ["PGD", "PPA"]: raise ValueError("Unknown solver '%s'. Pick one in ['PGD', 'PPA']." % solver) if loss_fun not in ("square_loss", "kl_loss"): raise ValueError( f"Unknown `loss_fun='{loss_fun}'`. Use one of: {'square_loss', 'kl_loss'}." ) C1, C2 = list_to_array(C1, C2) arr = [C1, C2] if p is not None: arr.append(list_to_array(p)) else: p = unif(C1.shape[0], type_as=C1) if q is not None: arr.append(list_to_array(q)) else: q = unif(C2.shape[0], type_as=C2) if G0 is not None: arr.append(G0) nx = get_backend(*arr) if G0 is None: G0 = nx.outer(p, q) T = G0 constC, hC1, hC2 = init_matrix(C1, C2, p, q, loss_fun, nx) if symmetric is None: symmetric = nx.allclose(C1, C1.T, atol=1e-10) and nx.allclose( C2, C2.T, atol=1e-10 ) if not symmetric: constCt, hC1t, hC2t = init_matrix(C1.T, C2.T, p, q, loss_fun, nx) cpt = 0 err = 1 if warmstart: # initialize potentials to cope with ot.sinkhorn initialization N1, N2 = C1.shape[0], C2.shape[0] mu = nx.zeros(N1, type_as=C1) - np.log(N1) nu = nx.zeros(N2, type_as=C2) - np.log(N2) if log: log = {"err": []} while err > tol and cpt < max_iter: Tprev = T # compute the gradient if symmetric: tens = gwggrad(constC, hC1, hC2, T, nx) else: tens = 0.5 * ( gwggrad(constC, hC1, hC2, T, nx) + gwggrad(constCt, hC1t, hC2t, T, nx) ) if solver == "PPA": tens = tens - epsilon * nx.log(T) if warmstart: T, loginn = sinkhorn( p, q, tens, epsilon, method="sinkhorn", log=True, warmstart=(mu, nu), **kwargs, ) mu = epsilon * nx.log(loginn["u"]) nu = epsilon * nx.log(loginn["v"]) else: T = sinkhorn(p, q, tens, epsilon, method="sinkhorn", **kwargs) if cpt % 10 == 0: # we can speed up the process by checking for the error only all # the 10th iterations err = nx.norm(T - Tprev) if log: log["err"].append(err) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err)) cpt += 1 if abs(nx.sum(T) - 1) > 1e-5: warnings.warn( "Solver failed to produce a transport plan. You might " "want to increase the regularization parameter `epsilon`." ) if log: log["gw_dist"] = gwloss(constC, hC1, hC2, T, nx) return T, log else: return T def entropic_gromov_wasserstein2( C1, C2, p=None, q=None, loss_fun="square_loss", epsilon=0.1, symmetric=None, G0=None, max_iter=1000, tol=1e-9, solver="PGD", warmstart=False, verbose=False, log=False, **kwargs, ): r""" Returns the Gromov-Wasserstein loss :math:`\mathbf{GW}` between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})` estimated using Sinkhorn projections. To recover the Gromov-Wasserstein distance as defined in [13] compute :math:`d_{GW} = \frac{1}{2} \sqrt{\mathbf{GW}}`. If `solver="PGD"`, the function solves the following entropic-regularized Gromov-Wasserstein optimization problem using Projected Gradient Descent [12]: .. math:: \mathbf{GW} = \mathop{\min}_\mathbf{T} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} - \epsilon H(\mathbf{T}) s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Else if `solver="PPA"`, the function solves the following Gromov-Wasserstein optimization problem using Proximal Point Algorithm [51]: .. math:: \mathbf{GW} = \mathop{\min}_\mathbf{T} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - `L`: loss function to account for the misfit between the similarity matrices - `H`: entropy .. note:: If the inner solver `ot.sinkhorn` did not convergence, the optimal coupling :math:`\mathbf{T}` returned by this function does not necessarily satisfy the marginal constraints :math:`\mathbf{T}\mathbf{1}=\mathbf{p}` and :math:`\mathbf{T}^T\mathbf{1}=\mathbf{q}`. So the returned Gromov-Wasserstein loss does not necessarily satisfy distance properties and may be negative. Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. loss_fun : string, optional (default='square_loss') Loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float, optional Regularization term >0 symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). G0: array-like, shape (ns,nt), optional If None the initial transport plan of the solver is pq^T. Otherwise G0 will be used as initial transport of the solver. G0 is not required to satisfy marginal constraints but we strongly recommend it to correctly estimate the GW distance. max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) solver: string, optional Solver to use either 'PGD' for Projected Gradient Descent or 'PPA' for Proximal Point Algorithm. Default value is 'PGD'. warmstart: bool, optional Either to perform warmstart of dual potentials in the successive Sinkhorn projections. verbose : bool, optional Print information along iterations log : bool, optional Record log if True. **kwargs: dict parameters can be directly passed to the ot.sinkhorn solver. Such as `numItermax` and `stopThr` to control its estimation precision, e.g [51] suggests to use `numItermax=1`. Returns ------- gw_dist : float Gromov-Wasserstein distance References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. .. [51] Xu, H., Luo, D., Zha, H., & Carin, L. (2019). Gromov-wasserstein learning for graph matching and node embedding. In International Conference on Machine Learning (ICML), 2019. """ T, logv = entropic_gromov_wasserstein( C1, C2, p, q, loss_fun, epsilon, symmetric, G0, max_iter, tol, solver, warmstart, verbose, log=True, **kwargs, ) logv["T"] = T if log: return logv["gw_dist"], logv else: return logv["gw_dist"] def BAPG_gromov_wasserstein( C1, C2, p=None, q=None, loss_fun="square_loss", epsilon=0.1, symmetric=None, G0=None, max_iter=1000, tol=1e-9, marginal_loss=False, verbose=False, log=False, ): r""" Returns the Gromov-Wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})` estimated using Bregman Alternated Projected Gradient method. If `marginal_loss=True`, the function solves the following Gromov-Wasserstein optimization problem : .. math:: \mathbf{T}^* \in \mathop{\arg\min}_\mathbf{T} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Else, the function solves an equivalent problem [63], where constant terms only depending on the marginals :math:`\mathbf{p}` and :math:`\mathbf{q}` are discarded while assuming that L decomposes as in Proposition 1 in [12]: .. math:: \mathbf{T}^* \in\mathop{\arg\min}_\mathbf{T} \quad - \langle h_1(\mathbf{C}_1) \mathbf{T} h_2(\mathbf{C_2})^\top , \mathbf{T} \rangle_F s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - `L`: loss function to account for the misfit between the similarity matrices satisfying :math:`L(a, b) = f_1(a) + f_2(b) - h_1(a) h_2(b)` .. note:: By algorithmic design the optimal coupling :math:`\mathbf{T}` returned by this function does not necessarily satisfy the marginal constraints :math:`\mathbf{T}\mathbf{1}=\mathbf{p}` and :math:`\mathbf{T}^T\mathbf{1}=\mathbf{q}`. So the returned Gromov-Wasserstein loss does not necessarily satisfy distance properties and may be negative. Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. loss_fun : string, optional (default='square_loss') Loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float, optional Regularization term >0 symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). G0: array-like, shape (ns,nt), optional If None the initial transport plan of the solver is pq^T. Otherwise G0 will be used as initial transport of the solver. G0 is not required to satisfy marginal constraints but we strongly recommend it to correctly estimate the GW distance. max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) marginal_loss: bool, optional. Default is False. Include constant marginal terms or not in the objective function. verbose : bool, optional Print information along iterations log : bool, optional Record log if True. Returns ------- T : array-like, shape (`ns`, `nt`) Optimal coupling between the two spaces References ---------- .. [63] Li, J., Tang, J., Kong, L., Liu, H., Li, J., So, A. M. C., & Blanchet, J. "A Convergent Single-Loop Algorithm for Relaxation of Gromov-Wasserstein in Graph Data". International Conference on Learning Representations (ICLR), 2022. """ if loss_fun not in ("square_loss", "kl_loss"): raise ValueError( f"Unknown `loss_fun='{loss_fun}'`. Use one of: {'square_loss', 'kl_loss'}." ) C1, C2 = list_to_array(C1, C2) arr = [C1, C2] if p is not None: arr.append(list_to_array(p)) else: p = unif(C1.shape[0], type_as=C1) if q is not None: arr.append(list_to_array(q)) else: q = unif(C2.shape[0], type_as=C2) if G0 is not None: arr.append(G0) nx = get_backend(*arr) if G0 is None: G0 = nx.outer(p, q) T = G0 constC, hC1, hC2 = init_matrix(C1, C2, p, q, loss_fun, nx) if symmetric is None: symmetric = nx.allclose(C1, C1.T, atol=1e-10) and nx.allclose( C2, C2.T, atol=1e-10 ) if not symmetric: constCt, hC1t, hC2t = init_matrix(C1.T, C2.T, p, q, loss_fun, nx) if marginal_loss: if symmetric: def df(T): return gwggrad(constC, hC1, hC2, T, nx) else: def df(T): return 0.5 * ( gwggrad(constC, hC1, hC2, T, nx) + gwggrad(constCt, hC1t, hC2t, T, nx) ) else: if symmetric: def df(T): A = -nx.dot(nx.dot(hC1, T), hC2.T) return 2 * A else: def df(T): A = -nx.dot(nx.dot(hC1, T), hC2t) At = -nx.dot(nx.dot(hC1t, T), hC2) return A + At cpt = 0 err = 1e15 if log: log = {"err": []} while err > tol and cpt < max_iter: Tprev = T # rows update T = T * nx.exp(-df(T) / epsilon) row_scaling = p / nx.sum(T, 1) T = nx.reshape(row_scaling, (-1, 1)) * T # columns update T = T * nx.exp(-df(T) / epsilon) column_scaling = q / nx.sum(T, 0) T = nx.reshape(column_scaling, (1, -1)) * T if cpt % 10 == 0: # we can speed up the process by checking for the error only all # the 10th iterations err = nx.norm(T - Tprev) if log: log["err"].append(err) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err)) cpt += 1 if nx.any(nx.isnan(T)): warnings.warn( "Solver failed to produce a transport plan. You might " "want to increase the regularization parameter `epsilon`.", UserWarning, ) if log: log["gw_dist"] = gwloss(constC, hC1, hC2, T, nx) if not marginal_loss: log["loss"] = log["gw_dist"] - nx.sum(constC * T) return T, log else: return T def BAPG_gromov_wasserstein2( C1, C2, p=None, q=None, loss_fun="square_loss", epsilon=0.1, symmetric=None, G0=None, max_iter=1000, tol=1e-9, marginal_loss=False, verbose=False, log=False, ): r""" Returns the Gromov-Wasserstein loss :math:`\mathbf{GW}` between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})` estimated using Bregman Alternated Projected Gradient method. If `marginal_loss=True`, the function solves the following Gromov-Wasserstein optimization problem : .. math:: \mathbf{GW} = \mathop{\min}_\mathbf{T} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Else, the function solves an equivalent problem [63, 64], where constant terms only depending on the marginals :math:`\mathbf{p}` and :math:`\mathbf{q}` are discarded while assuming that L decomposes as in Proposition 1 in [12]: .. math:: \mathop{\min}_\mathbf{T} \quad - \langle h_1(\mathbf{C}_1) \mathbf{T} h_2(\mathbf{C_2})^\top , \mathbf{T} \rangle_F s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - `L`: loss function to account for the misfit between the similarity matrices satisfying :math:`L(a, b) = f_1(a) + f_2(b) - h_1(a) h_2(b)` .. note:: By algorithmic design the optimal coupling :math:`\mathbf{T}` returned by this function does not necessarily satisfy the marginal constraints :math:`\mathbf{T}\mathbf{1}=\mathbf{p}` and :math:`\mathbf{T}^T\mathbf{1}=\mathbf{q}`. So the returned Gromov-Wasserstein loss does not necessarily satisfy distance properties and may be negative. Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. loss_fun : string, optional (default='square_loss') Loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float, optional Regularization term >0 symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). G0: array-like, shape (ns,nt), optional If None the initial transport plan of the solver is pq^T. Otherwise G0 will be used as initial transport of the solver. G0 is not required to satisfy marginal constraints but we strongly recommend it to correctly estimate the GW distance. max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) marginal_loss: bool, optional. Default is False. Include constant marginal terms or not in the objective function. verbose : bool, optional Print information along iterations log : bool, optional Record log if True. Returns ------- gw_dist : float Gromov-Wasserstein distance References ---------- .. [63] Li, J., Tang, J., Kong, L., Liu, H., Li, J., So, A. M. C., & Blanchet, J. "A Convergent Single-Loop Algorithm for Relaxation of Gromov-Wasserstein in Graph Data". International Conference on Learning Representations (ICLR), 2023. """ T, logv = BAPG_gromov_wasserstein( C1, C2, p, q, loss_fun, epsilon, symmetric, G0, max_iter, tol, marginal_loss, verbose, log=True, ) logv["T"] = T if log: return logv["gw_dist"], logv else: return logv["gw_dist"] def entropic_gromov_barycenters( N, Cs, ps=None, p=None, lambdas=None, loss_fun="square_loss", epsilon=0.1, symmetric=True, max_iter=1000, tol=1e-9, stop_criterion="barycenter", warmstartT=False, verbose=False, log=False, init_C=None, random_state=None, **kwargs, ): r""" Returns the Gromov-Wasserstein barycenters of `S` measured similarity matrices :math:`(\mathbf{C}_s)_{1 \leq s \leq S}` estimated using Gromov-Wasserstein transports from Sinkhorn projections. The function solves the following optimization problem: .. math:: \mathbf{C}^* = \mathop{\arg \min}_{\mathbf{C}\in \mathbb{R}^{N \times N}} \quad \sum_s \lambda_s \mathrm{GW}(\mathbf{C}, \mathbf{C}_s, \mathbf{p}, \mathbf{p}_s) Where : - :math:`\mathbf{C}_s`: metric cost matrix - :math:`\mathbf{p}_s`: distribution Parameters ---------- N : int Size of the targeted barycenter Cs : list of S array-like of shape (ns,ns) Metric cost matrices ps : list of S array-like of shape (ns,), optional Sample weights in the `S` spaces. If let to its default value None, uniform distributions are taken. p : array-like, shape (N,), optional Weights in the targeted barycenter. If let to its default value None, uniform distribution is taken. lambdas : list of float, optional List of the `S` spaces' weights. If let to its default value None, uniform weights are taken. loss_fun : string, optional (default='square_loss') Loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float, optional Regularization term >0 symmetric : bool, optional. Either structures are to be assumed symmetric or not. Default value is True. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) stop_criterion : str, optional. Default is 'barycenter'. Convergence criterion taking values in ['barycenter', 'loss']. If set to 'barycenter' uses absolute norm variations of estimated barycenters. Else if set to 'loss' uses the relative variations of the loss. warmstartT: bool, optional Either to perform warmstart of transport plans in the successive gromov-wasserstein transport problems. verbose : bool, optional Print information along iterations. log : bool, optional Record log if True. init_C : bool | array-like, shape (N, N) Random initial value for the :math:`\mathbf{C}` matrix provided by user. random_state : int or RandomState instance, optional Fix the seed for reproducibility **kwargs: dict parameters can be directly passed to the `ot.entropic_gromov_wasserstein` solver. Returns ------- C : array-like, shape (`N`, `N`) Similarity matrix in the barycenter space (permutated arbitrarily) log : dict Only returned when log=True. It contains the keys: - :math:`\mathbf{T}`: list of (`N`, `ns`) transport matrices - :math:`\mathbf{p}`: (`N`,) barycenter weights - values used in convergence evaluation. References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. """ if loss_fun not in ("square_loss", "kl_loss"): raise ValueError( f"Unknown `loss_fun='{loss_fun}'`. Use one of: {'square_loss', 'kl_loss'}." ) if stop_criterion not in ["barycenter", "loss"]: raise ValueError( f"Unknown `stop_criterion='{stop_criterion}'`. Use one of: {'barycenter', 'loss'}." ) if isinstance(Cs[0], list): raise ValueError( "Deprecated feature in POT 0.9.4: structures Cs[i] are lists and should be arrays from a supported backend (e.g numpy)." ) arr = [*Cs] if ps is not None: if isinstance(ps[0], list): raise ValueError( "Deprecated feature in POT 0.9.4: weights ps[i] are lists and should be arrays from a supported backend (e.g numpy)." ) arr += [*ps] else: ps = [unif(C.shape[0], type_as=C) for C in Cs] if p is not None: arr.append(list_to_array(p)) else: p = unif(N, type_as=Cs[0]) nx = get_backend(*arr) S = len(Cs) if lambdas is None: lambdas = [1.0 / S] * S # Initialization of C : random SPD matrix (if not provided by user) if init_C is None: generator = check_random_state(random_state) xalea = generator.randn(N, 2) C = dist(xalea, xalea) C /= C.max() C = nx.from_numpy(C, type_as=p) else: C = init_C cpt = 0 err = 1e15 # either the error on 'barycenter' or 'loss' if warmstartT: T = [None] * S if stop_criterion == "barycenter": inner_log = False else: inner_log = True curr_loss = 1e15 if log: log_ = {} log_["err"] = [] if stop_criterion == "loss": log_["loss"] = [] while (err > tol) and (cpt < max_iter): if stop_criterion == "barycenter": Cprev = C else: prev_loss = curr_loss # get transport plans if warmstartT: res = [ entropic_gromov_wasserstein( C, Cs[s], p, ps[s], loss_fun, epsilon, symmetric, T[s], max_iter, 1e-4, verbose=verbose, log=inner_log, **kwargs, ) for s in range(S) ] else: res = [ entropic_gromov_wasserstein( C, Cs[s], p, ps[s], loss_fun, epsilon, symmetric, None, max_iter, 1e-4, verbose=verbose, log=inner_log, **kwargs, ) for s in range(S) ] if stop_criterion == "barycenter": T = res else: T = [output[0] for output in res] curr_loss = np.sum([output[1]["gw_dist"] for output in res]) # update barycenters C = update_barycenter_structure( T, Cs, lambdas, p, loss_fun, target=False, check_zeros=False, nx=nx ) # update convergence criterion if stop_criterion == "barycenter": err = nx.norm(C - Cprev) if log: log_["err"].append(err) else: err = abs(curr_loss - prev_loss) / prev_loss if prev_loss != 0.0 else np.nan if log: log_["loss"].append(curr_loss) log_["err"].append(err) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err)) cpt += 1 if log: log_["T"] = T log_["p"] = p return C, log_ else: return C def entropic_fused_gromov_wasserstein( M, C1, C2, p=None, q=None, loss_fun="square_loss", epsilon=0.1, symmetric=None, alpha=0.5, G0=None, max_iter=1000, tol=1e-9, solver="PGD", warmstart=False, verbose=False, log=False, **kwargs, ): r""" Returns the Fused Gromov-Wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{Y_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{Y_2}, \mathbf{q})` with pairwise distance matrix :math:`\mathbf{M}` between node feature matrices :math:`\mathbf{Y_1}` and :math:`\mathbf{Y_2}`, estimated using Sinkhorn projections. If `solver="PGD"`, the function solves the following entropic-regularized Fused Gromov-Wasserstein optimization problem using Projected Gradient Descent [12]: .. math:: \mathbf{T}^* \in \mathop{\arg\min}_\mathbf{T} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} - \epsilon H(\mathbf{T}) s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Else if `solver="PPA"`, the function solves the following Fused Gromov-Wasserstein optimization problem using Proximal Point Algorithm [51]: .. math:: \mathbf{T}^* \in\mathop{\arg\min}_\mathbf{T} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{M}`: metric cost matrix between features across domains - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - `L`: loss function to account for the misfit between the similarity and feature matrices - `H`: entropy - :math:`\alpha`: trade-off parameter .. note:: If the inner solver `ot.sinkhorn` did not convergence, the optimal coupling :math:`\mathbf{T}` returned by this function does not necessarily satisfy the marginal constraints :math:`\mathbf{T}\mathbf{1}=\mathbf{p}` and :math:`\mathbf{T}^T\mathbf{1}=\mathbf{q}`. So the returned Fused Gromov-Wasserstein loss does not necessarily satisfy distance properties and may be negative. Parameters ---------- M : array-like, shape (ns, nt) Metric cost matrix between features across domains C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. loss_fun : string, optional (default='square_loss') Loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float, optional Regularization term >0 symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). alpha : float, optional Trade-off parameter (0 < alpha < 1) G0: array-like, shape (ns,nt), optional If None the initial transport plan of the solver is pq^T. Otherwise G0 will be used as initial transport of the solver. G0 is not required to satisfy marginal constraints but we strongly recommend it to correctly estimate the GW distance. max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) solver: string, optional Solver to use either 'PGD' for Projected Gradient Descent or 'PPA' for Proximal Point Algorithm. Default value is 'PGD'. warmstart: bool, optional Either to perform warmstart of dual potentials in the successive Sinkhorn projections. verbose : bool, optional Print information along iterations log : bool, optional Record log if True. **kwargs: dict parameters can be directly passed to the ot.sinkhorn solver. Such as `numItermax` and `stopThr` to control its estimation precision, e.g [51] suggests to use `numItermax=1`. Returns ------- T : array-like, shape (`ns`, `nt`) Optimal coupling between the two joint spaces References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. .. [47] Chowdhury, S., & Mémoli, F. (2019). The gromov–wasserstein distance between networks and stable network invariants. Information and Inference: A Journal of the IMA, 8(4), 757-787. .. [51] Xu, H., Luo, D., Zha, H., & Carin, L. (2019). Gromov-wasserstein learning for graph matching and node embedding. In International Conference on Machine Learning (ICML), 2019. .. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs", International Conference on Machine Learning (ICML). 2019. """ if solver not in ["PGD", "PPA"]: raise ValueError("Unknown solver '%s'. Pick one in ['PGD', 'PPA']." % solver) if loss_fun not in ("square_loss", "kl_loss"): raise ValueError( f"Unknown `loss_fun='{loss_fun}'`. Use one of: {'square_loss', 'kl_loss'}." ) M, C1, C2 = list_to_array(M, C1, C2) arr = [M, C1, C2] if p is not None: arr.append(list_to_array(p)) else: p = unif(C1.shape[0], type_as=C1) if q is not None: arr.append(list_to_array(q)) else: q = unif(C2.shape[0], type_as=C2) if G0 is not None: arr.append(G0) nx = get_backend(*arr) if G0 is None: G0 = nx.outer(p, q) T = G0 constC, hC1, hC2 = init_matrix(C1, C2, p, q, loss_fun, nx) if symmetric is None: symmetric = nx.allclose(C1, C1.T, atol=1e-10) and nx.allclose( C2, C2.T, atol=1e-10 ) if not symmetric: constCt, hC1t, hC2t = init_matrix(C1.T, C2.T, p, q, loss_fun, nx) cpt = 0 err = 1 if warmstart: # initialize potentials to cope with ot.sinkhorn initialization N1, N2 = C1.shape[0], C2.shape[0] mu = nx.zeros(N1, type_as=C1) - np.log(N1) nu = nx.zeros(N2, type_as=C2) - np.log(N2) if log: log = {"err": []} while err > tol and cpt < max_iter: Tprev = T # compute the gradient if symmetric: tens = alpha * gwggrad(constC, hC1, hC2, T, nx) + (1 - alpha) * M else: tens = (alpha * 0.5) * ( gwggrad(constC, hC1, hC2, T, nx) + gwggrad(constCt, hC1t, hC2t, T, nx) ) + (1 - alpha) * M if solver == "PPA": tens = tens - epsilon * nx.log(T) if warmstart: T, loginn = sinkhorn( p, q, tens, epsilon, method="sinkhorn", log=True, warmstart=(mu, nu), **kwargs, ) mu = epsilon * nx.log(loginn["u"]) nu = epsilon * nx.log(loginn["v"]) else: T = sinkhorn(p, q, tens, epsilon, method="sinkhorn", **kwargs) if cpt % 10 == 0: # we can speed up the process by checking for the error only all # the 10th iterations err = nx.norm(T - Tprev) if log: log["err"].append(err) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err)) cpt += 1 if abs(nx.sum(T) - 1) > 1e-5: warnings.warn( "Solver failed to produce a transport plan. You might " "want to increase the regularization parameter `epsilon`." ) if log: log["fgw_dist"] = (1 - alpha) * nx.sum(M * T) + alpha * gwloss( constC, hC1, hC2, T, nx ) return T, log else: return T def entropic_fused_gromov_wasserstein2( M, C1, C2, p=None, q=None, loss_fun="square_loss", epsilon=0.1, symmetric=None, alpha=0.5, G0=None, max_iter=1000, tol=1e-9, solver="PGD", warmstart=False, verbose=False, log=False, **kwargs, ): r""" Returns the Fused Gromov-Wasserstein distance between :math:`(\mathbf{C_1}, \mathbf{Y_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{Y_2}, \mathbf{q})` with pairwise distance matrix :math:`\mathbf{M}` between node feature matrices :math:`\mathbf{Y_1}` and :math:`\mathbf{Y_2}`, estimated using Sinkhorn projections. If `solver="PGD"`, the function solves the following entropic-regularized Fused Gromov-Wasserstein optimization problem using Projected Gradient Descent [12]: .. math:: \mathbf{FGW} = \mathop{\min}_\mathbf{T} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} - \epsilon H(\mathbf{T}) s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Else if `solver="PPA"`, the function solves the following Fused Gromov-Wasserstein optimization problem using Proximal Point Algorithm [51]: .. math:: \mathbf{FGW} = \mathop{\min}_\mathbf{T} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{M}`: metric cost matrix between features across domains - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - `L`: loss function to account for the misfit between the similarity and feature matrices - `H`: entropy - :math:`\alpha`: trade-off parameter .. note:: If the inner solver `ot.sinkhorn` did not convergence, the optimal coupling :math:`\mathbf{T}` returned by this function does not necessarily satisfy the marginal constraints :math:`\mathbf{T}\mathbf{1}=\mathbf{p}` and :math:`\mathbf{T}^T\mathbf{1}=\mathbf{q}`. So the returned Fused Gromov-Wasserstein loss does not necessarily satisfy distance properties and may be negative. Parameters ---------- M : array-like, shape (ns, nt) Metric cost matrix between features across domains C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. loss_fun : string, optional (default='square_loss') Loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float, optional Regularization term >0 symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). alpha : float, optional Trade-off parameter (0 < alpha < 1) G0: array-like, shape (ns,nt), optional If None the initial transport plan of the solver is pq^T. Otherwise G0 will be used as initial transport of the solver. G0 is not required to satisfy marginal constraints but we strongly recommend it to correctly estimate the GW distance. max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional Record log if True. Returns ------- fgw_dist : float Fused Gromov-Wasserstein distance References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. .. [51] Xu, H., Luo, D., Zha, H., & Carin, L. (2019). Gromov-wasserstein learning for graph matching and node embedding. In International Conference on Machine Learning (ICML), 2019. .. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs", International Conference on Machine Learning (ICML). 2019. """ nx = get_backend(M, C1, C2) T, logv = entropic_fused_gromov_wasserstein( M, C1, C2, p, q, loss_fun, epsilon, symmetric, alpha, G0, max_iter, tol, solver, warmstart, verbose, log=True, **kwargs, ) logv["T"] = T lin_term = nx.sum(T * M) logv["quad_loss"] = logv["fgw_dist"] - (1 - alpha) * lin_term logv["lin_loss"] = lin_term * (1 - alpha) if log: return logv["fgw_dist"], logv else: return logv["fgw_dist"] def BAPG_fused_gromov_wasserstein( M, C1, C2, p=None, q=None, loss_fun="square_loss", epsilon=0.1, symmetric=None, alpha=0.5, G0=None, max_iter=1000, tol=1e-9, marginal_loss=False, verbose=False, log=False, ): r""" Returns the Fused Gromov-Wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{Y_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{Y_2}, \mathbf{q})` with pairwise distance matrix :math:`\mathbf{M}` between node feature matrices :math:`\mathbf{Y_1}` and :math:`\mathbf{Y_2}`, estimated using Bregman Alternated Projected Gradient method. If `marginal_loss=True`, the function solves the following Fused Gromov-Wasserstein optimization problem : .. math:: \mathbf{T}^* \in\mathop{\arg\min}_\mathbf{T} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Else, the function solves an equivalent problem [63, 64], where constant terms only depending on the marginals :math:`\mathbf{p}` and :math:`\mathbf{q}` are discarded while assuming that L decomposes as in Proposition 1 in [12]: .. math:: \mathbf{T}^* \in\mathop{\arg\min}_\mathbf{T} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F - \alpha \langle h_1(\mathbf{C}_1) \mathbf{T} h_2(\mathbf{C_2})^\top , \mathbf{T} \rangle_F s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{M}`: pairwise relation matrix between features across domains - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - `L`: loss function to account for the misfit between the similarity and feature matrices satisfying :math:`L(a, b) = f_1(a) + f_2(b) - h_1(a) h_2(b)` - :math:`\alpha`: trade-off parameter .. note:: By algorithmic design the optimal coupling :math:`\mathbf{T}` returned by this function does not necessarily satisfy the marginal constraints :math:`\mathbf{T}\mathbf{1}=\mathbf{p}` and :math:`\mathbf{T}^T\mathbf{1}=\mathbf{q}`. So the returned Fused Gromov-Wasserstein loss does not necessarily satisfy distance properties and may be negative. Parameters ---------- M : array-like, shape (ns, nt) Pairwise relation matrix between features across domains C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. loss_fun : string, optional (default='square_loss') Loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float, optional Regularization term >0 symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). alpha : float, optional Trade-off parameter (0 < alpha < 1) G0: array-like, shape (ns,nt), optional If None the initial transport plan of the solver is pq^T. Otherwise G0 will be used as initial transport of the solver. G0 is not required to satisfy marginal constraints but we strongly recommend it to correctly estimate the GW distance. max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) marginal_loss: bool, optional. Default is False. Include constant marginal terms or not in the objective function. verbose : bool, optional Print information along iterations log : bool, optional Record log if True. Returns ------- T : array-like, shape (`ns`, `nt`) Optimal coupling between the two joint spaces References ---------- .. [63] Li, J., Tang, J., Kong, L., Liu, H., Li, J., So, A. M. C., & Blanchet, J. "A Convergent Single-Loop Algorithm for Relaxation of Gromov-Wasserstein in Graph Data". International Conference on Learning Representations (ICLR), 2023. .. [64] Ma, X., Chu, X., Wang, Y., Lin, Y., Zhao, J., Ma, L., & Zhu, W. "Fused Gromov-Wasserstein Graph Mixup for Graph-level Classifications". In Thirty-seventh Conference on Neural Information Processing Systems. """ if loss_fun not in ("square_loss", "kl_loss"): raise ValueError( f"Unknown `loss_fun='{loss_fun}'`. Use one of: {'square_loss', 'kl_loss'}." ) M, C1, C2 = list_to_array(M, C1, C2) arr = [M, C1, C2] if p is not None: arr.append(list_to_array(p)) else: p = unif(C1.shape[0], type_as=C1) if q is not None: arr.append(list_to_array(q)) else: q = unif(C2.shape[0], type_as=C2) if G0 is not None: arr.append(G0) nx = get_backend(*arr) if G0 is None: G0 = nx.outer(p, q) T = G0 constC, hC1, hC2 = init_matrix(C1, C2, p, q, loss_fun, nx) if symmetric is None: symmetric = nx.allclose(C1, C1.T, atol=1e-10) and nx.allclose( C2, C2.T, atol=1e-10 ) if not symmetric: constCt, hC1t, hC2t = init_matrix(C1.T, C2.T, p, q, loss_fun, nx) # Define gradients if marginal_loss: if symmetric: def df(T): return alpha * gwggrad(constC, hC1, hC2, T, nx) + (1 - alpha) * M else: def df(T): return (alpha * 0.5) * ( gwggrad(constC, hC1, hC2, T, nx) + gwggrad(constCt, hC1t, hC2t, T, nx) ) + (1 - alpha) * M else: if symmetric: def df(T): A = -nx.dot(nx.dot(hC1, T), hC2.T) return 2 * alpha * A + (1 - alpha) * M else: def df(T): A = -nx.dot(nx.dot(hC1, T), hC2t) At = -nx.dot(nx.dot(hC1t, T), hC2) return alpha * (A + At) + (1 - alpha) * M cpt = 0 err = 1e15 if log: log = {"err": []} while err > tol and cpt < max_iter: Tprev = T # rows update T = T * nx.exp(-df(T) / epsilon) row_scaling = p / nx.sum(T, 1) T = nx.reshape(row_scaling, (-1, 1)) * T # columns update T = T * nx.exp(-df(T) / epsilon) column_scaling = q / nx.sum(T, 0) T = nx.reshape(column_scaling, (1, -1)) * T if cpt % 10 == 0: # we can speed up the process by checking for the error only all # the 10th iterations err = nx.norm(T - Tprev) if log: log["err"].append(err) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err)) cpt += 1 if nx.any(nx.isnan(T)): warnings.warn( "Solver failed to produce a transport plan. You might " "want to increase the regularization parameter `epsilon`.", UserWarning, ) if log: log["fgw_dist"] = (1 - alpha) * nx.sum(M * T) + alpha * gwloss( constC, hC1, hC2, T, nx ) if not marginal_loss: log["loss"] = log["fgw_dist"] - alpha * nx.sum(constC * T) return T, log else: return T def BAPG_fused_gromov_wasserstein2( M, C1, C2, p=None, q=None, loss_fun="square_loss", epsilon=0.1, symmetric=None, alpha=0.5, G0=None, max_iter=1000, tol=1e-9, marginal_loss=False, verbose=False, log=False, ): r""" Returns the Fused Gromov-Wasserstein loss between :math:`(\mathbf{C_1}, \mathbf{Y_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{Y_2}, \mathbf{q})` with pairwise distance matrix :math:`\mathbf{M}` between node feature matrices :math:`\mathbf{Y_1}` and :math:`\mathbf{Y_2}`, estimated using Bregman Alternated Projected Gradient method. If `marginal_loss=True`, the function solves the following Fused Gromov-Wasserstein optimization problem : .. math:: \mathbf{FGW} = \mathop{\min}_\mathbf{T} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Else, the function solves an equivalent problem [63, 64], where constant terms only depending on the marginals :math:`\mathbf{p}` and :math:`\mathbf{q}` are discarded while assuming that L decomposes as in Proposition 1 in [12]: .. math:: \mathop{\min}_\mathbf{T} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F - \alpha \langle h_1(\mathbf{C}_1) \mathbf{T} h_2(\mathbf{C_2})^\top , \mathbf{T} \rangle_F s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{M}`: metric cost matrix between features across domains - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - `L`: loss function to account for the misfit between the similarity and feature matrices satisfying :math:`L(a, b) = f_1(a) + f_2(b) - h_1(a) h_2(b)` - :math:`\alpha`: trade-off parameter .. note:: By algorithmic design the optimal coupling :math:`\mathbf{T}` returned by this function does not necessarily satisfy the marginal constraints :math:`\mathbf{T}\mathbf{1}=\mathbf{p}` and :math:`\mathbf{T}^T\mathbf{1}=\mathbf{q}`. So the returned Fused Gromov-Wasserstein loss does not necessarily satisfy distance properties and may be negative. Parameters ---------- M : array-like, shape (ns, nt) Metric cost matrix between features across domains C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. loss_fun : string, optional (default='square_loss') Loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float, optional Regularization term >0 symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). alpha : float, optional Trade-off parameter (0 < alpha < 1) G0: array-like, shape (ns,nt), optional If None the initial transport plan of the solver is pq^T. Otherwise G0 will be used as initial transport of the solver. G0 is not required to satisfy marginal constraints but we strongly recommend it to correctly estimate the GW distance. max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) marginal_loss: bool, optional. Default is False. Include constant marginal terms or not in the objective function. verbose : bool, optional Print information along iterations log : bool, optional Record log if True. Returns ------- T : array-like, shape (`ns`, `nt`) Optimal coupling between the two joint spaces References ---------- .. [63] Li, J., Tang, J., Kong, L., Liu, H., Li, J., So, A. M. C., & Blanchet, J. "A Convergent Single-Loop Algorithm for Relaxation of Gromov-Wasserstein in Graph Data". International Conference on Learning Representations (ICLR), 2023. .. [64] Ma, X., Chu, X., Wang, Y., Lin, Y., Zhao, J., Ma, L., & Zhu, W. "Fused Gromov-Wasserstein Graph Mixup for Graph-level Classifications". In Thirty-seventh Conference on Neural Information Processing Systems. """ nx = get_backend(M, C1, C2) T, logv = BAPG_fused_gromov_wasserstein( M, C1, C2, p, q, loss_fun, epsilon, symmetric, alpha, G0, max_iter, tol, marginal_loss, verbose, log=True, ) logv["T"] = T lin_term = nx.sum(T * M) logv["quad_loss"] = logv["fgw_dist"] - (1 - alpha) * lin_term logv["lin_loss"] = lin_term * (1 - alpha) if log: return logv["fgw_dist"], logv else: return logv["fgw_dist"] def entropic_fused_gromov_barycenters( N, Ys, Cs, ps=None, p=None, lambdas=None, loss_fun="square_loss", epsilon=0.1, symmetric=True, alpha=0.5, max_iter=1000, tol=1e-9, stop_criterion="barycenter", warmstartT=False, verbose=False, log=False, init_C=None, init_Y=None, fixed_structure=False, fixed_features=False, random_state=None, **kwargs, ): r""" Returns the Fused Gromov-Wasserstein barycenters of `S` measurable networks with node features :math:`(\mathbf{C}_s, \mathbf{Y}_s, \mathbf{p}_s)_{1 \leq s \leq S}` estimated using Fused Gromov-Wasserstein transports from Sinkhorn projections. The function solves the following optimization problem: .. math:: \mathbf{C}^*, \mathbf{Y}^* = \mathop{\arg \min}_{\mathbf{C}\in \mathbb{R}^{N \times N}, \mathbf{Y}\in \mathbb{Y}^{N \times d}} \quad \sum_s \lambda_s \mathrm{FGW}_{\alpha}(\mathbf{C}, \mathbf{C}_s, \mathbf{Y}, \mathbf{Y}_s, \mathbf{p}, \mathbf{p}_s) Where : - :math:`\mathbf{Y}_s`: feature matrix - :math:`\mathbf{C}_s`: metric cost matrix - :math:`\mathbf{p}_s`: distribution Parameters ---------- N : int Size of the targeted barycenter Ys: list of array-like, each element has shape (ns,d) Features of all samples Cs : list of S array-like of shape (ns,ns) Metric cost matrices ps : list of S array-like of shape (ns,), optional Sample weights in the `S` spaces. If let to its default value None, uniform distributions are taken. p : array-like, shape (N,), optional Weights in the targeted barycenter. If let to its default value None, uniform distribution is taken. lambdas : list of float, optional List of the `S` spaces' weights. If let to its default value None, uniform weights are taken. loss_fun : string, optional (default='square_loss') Loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float, optional Regularization term >0 symmetric : bool, optional. Either structures are to be assumed symmetric or not. Default value is True. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). alpha : float, optional Trade-off parameter (0 < alpha < 1) max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) stop_criterion : str, optional. Default is 'barycenter'. Stop criterion taking values in ['barycenter', 'loss']. If set to 'barycenter' uses absolute norm variations of estimated barycenters. Else if set to 'loss' uses the relative variations of the loss. warmstartT: bool, optional Either to perform warmstart of transport plans in the successive fused gromov-wasserstein transport problems. verbose : bool, optional Print information along iterations. log : bool, optional Record log if True. init_C : bool | array-like, shape (N, N) Random initial value for the :math:`\mathbf{C}` matrix provided by user. init_Y : array-like, shape (N,d), optional Initialization for the barycenters' features. If not set a random init is used. fixed_structure : bool, optional Whether to fix the structure of the barycenter during the updates. fixed_features : bool, optional Whether to fix the feature of the barycenter during the updates random_state : int or RandomState instance, optional Fix the seed for reproducibility **kwargs: dict parameters can be directly passed to the `ot.entropic_fused_gromov_wasserstein` solver. Returns ------- Y : array-like, shape (`N`, `d`) Feature matrix in the barycenter space (permutated arbitrarily) C : array-like, shape (`N`, `N`) Similarity matrix in the barycenter space (permutated as Y's rows) log : dict Only returned when log=True. It contains the keys: - :math:`\mathbf{T}`: list of (`N`, `ns`) transport matrices - :math:`\mathbf{p}`: (`N`,) barycenter weights - :math:`(\mathbf{M}_s)_s`: all distance matrices between the feature of the barycenter and the other features :math:`(dist(\mathbf{X}, \mathbf{Y}_s))_s` shape (`N`, `ns`) - values used in convergence evaluation. References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. .. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs" International Conference on Machine Learning (ICML). 2019. """ if loss_fun not in ("square_loss", "kl_loss"): raise ValueError( f"Unknown `loss_fun='{loss_fun}'`. Use one of: {'square_loss', 'kl_loss'}." ) if stop_criterion not in ["barycenter", "loss"]: raise ValueError( f"Unknown `stop_criterion='{stop_criterion}'`. Use one of: {'barycenter', 'loss'}." ) if isinstance(Cs[0], list) or isinstance(Ys[0], list): raise ValueError( "Deprecated feature in POT 0.9.4: structures Cs[i] and/or features Ys[i] are lists and should be arrays from a supported backend (e.g numpy)." ) arr = [*Cs, *Ys] if ps is not None: if isinstance(ps[0], list): raise ValueError( "Deprecated feature in POT 0.9.4: weights ps[i] are lists and should be arrays from a supported backend (e.g numpy)." ) arr += [*ps] else: ps = [unif(C.shape[0], type_as=C) for C in Cs] if p is not None: arr.append(list_to_array(p)) else: p = unif(N, type_as=Cs[0]) nx = get_backend(*arr) S = len(Cs) if lambdas is None: lambdas = [1.0 / S] * S d = Ys[0].shape[1] # dimension on the node features # Initialization of C : random euclidean distance matrix (if not provided by user) if fixed_structure: if init_C is None: raise UndefinedParameter("If C is fixed it must be provided in init_C") else: C = init_C else: if init_C is None: rng = check_random_state(random_state) xalea = rng.randn(N, 2) C = dist(xalea, xalea) C = nx.from_numpy(C, type_as=ps[0]) else: C = init_C # Initialization of Y if fixed_features: if init_Y is None: raise UndefinedParameter("If Y is fixed it must be provided in init_Y") else: Y = init_Y else: if init_Y is None: Y = nx.zeros((N, d), type_as=ps[0]) else: Y = init_Y Ms = [dist(Y, Ys[s]) for s in range(len(Ys))] if warmstartT: T = [None] * S if stop_criterion == "barycenter": inner_log = False else: inner_log = True curr_loss = 1e15 if log: log_ = {} if stop_criterion == "barycenter": log_["err_feature"] = [] log_["err_structure"] = [] log_["Ts_iter"] = [] else: log_["loss"] = [] log_["err_rel_loss"] = [] for cpt in range(max_iter): # break if specified errors are below tol. if stop_criterion == "barycenter": Cprev = C Yprev = Y else: prev_loss = curr_loss # get transport plans if warmstartT: res = [ entropic_fused_gromov_wasserstein( Ms[s], C, Cs[s], p, ps[s], loss_fun, epsilon, symmetric, alpha, T[s], max_iter, 1e-4, verbose=verbose, log=inner_log, **kwargs, ) for s in range(S) ] else: res = [ entropic_fused_gromov_wasserstein( Ms[s], C, Cs[s], p, ps[s], loss_fun, epsilon, symmetric, alpha, None, max_iter, 1e-4, verbose=verbose, log=inner_log, **kwargs, ) for s in range(S) ] if stop_criterion == "barycenter": T = res else: T = [output[0] for output in res] curr_loss = np.sum([output[1]["fgw_dist"] for output in res]) # update barycenters if not fixed_features: X = update_barycenter_feature( T, Ys, lambdas, p, target=False, check_zeros=False, nx=nx ) Ms = [dist(X, Ys[s]) for s in range(len(Ys))] if not fixed_structure: C = update_barycenter_structure( T, Cs, lambdas, p, loss_fun, target=False, check_zeros=False, nx=nx ) # update convergence criterion if stop_criterion == "barycenter": err_feature, err_structure = 0.0, 0.0 if not fixed_features: err_feature = nx.norm(Y - Yprev) if not fixed_structure: err_structure = nx.norm(C - Cprev) if log: log_["err_feature"].append(err_feature) log_["err_structure"].append(err_structure) log_["Ts_iter"].append(T) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err_structure)) print("{:5d}|{:8e}|".format(cpt, err_feature)) if (err_feature <= tol) or (err_structure <= tol): break else: err_rel_loss = ( abs(curr_loss - prev_loss) / prev_loss if prev_loss != 0.0 else np.nan ) if log: log_["loss"].append(curr_loss) log_["err_rel_loss"].append(err_rel_loss) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err_rel_loss)) if err_rel_loss <= tol: break if log: log_["T"] = T log_["p"] = p log_["Ms"] = Ms return Y, C, log_ else: return Y, C python-pot-0.9.7+dfsg/ot/gromov/_dictionary.py000066400000000000000000001457431525521106200214150ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ (Fused) Gromov-Wasserstein dictionary learning. """ # Author: Rémi Flamary # Cédric Vincent-Cuaz # # License: MIT License import numpy as np from ..utils import unif, check_random_state from ..backend import get_backend from ._gw import gromov_wasserstein, fused_gromov_wasserstein def gromov_wasserstein_dictionary_learning( Cs, D, nt, reg=0.0, ps=None, q=None, epochs=20, batch_size=32, learning_rate=1.0, Cdict_init=None, projection="nonnegative_symmetric", use_log=True, tol_outer=10 ** (-5), tol_inner=10 ** (-5), max_iter_outer=20, max_iter_inner=200, use_adam_optimizer=True, verbose=False, random_state=None, **kwargs, ): r""" Infer Gromov-Wasserstein linear dictionary :math:`\{ (\mathbf{C_{dict}[d]}, q) \}_{d \in [D]}` from the list of structures :math:`\{ (\mathbf{C_s},\mathbf{p_s}) \}_s` .. math:: \min_{\mathbf{C_{dict}}, \{\mathbf{w_s} \}_{s \leq S}} \sum_{s=1}^S GW_2(\mathbf{C_s}, \sum_{d=1}^D w_{s,d}\mathbf{C_{dict}[d]}, \mathbf{p_s}, \mathbf{q}) - reg\| \mathbf{w_s} \|_2^2 such that, :math:`\forall s \leq S` : - :math:`\mathbf{w_s}^\top \mathbf{1}_D = 1` - :math:`\mathbf{w_s} \geq \mathbf{0}_D` Where : - :math:`\forall s \leq S, \mathbf{C_s}` is a (ns,ns) pairwise similarity matrix of variable size ns. - :math:`\mathbf{C_{dict}}` is a (D, nt, nt) tensor of D pairwise similarity matrix of fixed size nt. - :math:`\forall s \leq S, \mathbf{p_s}` is the source distribution corresponding to :math:`\mathbf{C_s}` - :math:`\mathbf{q}` is the target distribution assigned to every structures in the embedding space. - reg is the regularization coefficient. The stochastic algorithm used for estimating the graph dictionary atoms as proposed in [38]_ Parameters ---------- Cs : list of S symmetric array-like, shape (ns, ns) List of Metric/Graph cost matrices of variable size (ns, ns). D: int Number of dictionary atoms to learn nt: int Number of samples within each dictionary atoms reg : float, optional Coefficient of the negative quadratic regularization used to promote sparsity of w. The default is 0. ps : list of S array-like, shape (ns,), optional Distribution in each source space C of Cs. Default is None and corresponds to uniform distributions. q : array-like, shape (nt,), optional Distribution in the embedding space whose structure will be learned. Default is None and corresponds to uniform distributions. epochs: int, optional Number of epochs used to learn the dictionary. Default is 32. batch_size: int, optional Batch size for each stochastic gradient update of the dictionary. Set to the dataset size if the provided batch_size is higher than the dataset size. Default is 32. learning_rate: float, optional Learning rate used for the stochastic gradient descent. Default is 1. Cdict_init: list of D array-like with shape (nt, nt), optional Used to initialize the dictionary. If set to None (Default), the dictionary will be initialized randomly. Else Cdict must have shape (D, nt, nt) i.e match provided shape features. projection: str , optional If 'nonnegative' and/or 'symmetric' is in projection, the corresponding projection will be performed at each stochastic update of the dictionary Else the set of atoms is :math:`R^{nt * nt}`. Default is 'nonnegative_symmetric' log: bool, optional If set to True, losses evolution by batches and epochs are tracked. Default is False. use_adam_optimizer: bool, optional If set to True, adam optimizer with default settings is used as adaptative learning rate strategy. Else perform SGD with fixed learning rate. Default is True. tol_outer : float, optional Solver precision for the BCD algorithm, measured by absolute relative error on consecutive losses. Default is :math:`10^{-5}`. tol_inner : float, optional Solver precision for the Conjugate Gradient algorithm used to get optimal w at a fixed transport, measured by absolute relative error on consecutive losses. Default is :math:`10^{-5}`. max_iter_outer : int, optional Maximum number of iterations for the BCD. Default is 20. max_iter_inner : int, optional Maximum number of iterations for the Conjugate Gradient. Default is 200. verbose : bool, optional Print the reconstruction loss every epoch. Default is False. random_state : int, RandomState instance or None, default=None Determines random number generation. Pass an int for reproducible output across multiple function calls. Returns ------- Cdict_best_state : D array-like, shape (D,nt,nt) Metric/Graph cost matrices composing the dictionary. The dictionary leading to the best loss over an epoch is saved and returned. log: dict If use_log is True, contains loss evolutions by batches and epochs. References ------- .. [38] C. Vincent-Cuaz, T. Vayer, R. Flamary, M. Corneli, N. Courty, Online Graph Dictionary Learning, International Conference on Machine Learning (ICML), 2021. """ # Handle backend of non-optional arguments Cs0 = Cs nx = get_backend(*Cs0) Cs = [nx.to_numpy(C) for C in Cs0] dataset_size = len(Cs) # Handle backend of optional arguments if ps is None: ps = [unif(C.shape[0]) for C in Cs] else: ps = [nx.to_numpy(p) for p in ps] if q is None: q = unif(nt) else: q = nx.to_numpy(q) rng = check_random_state(random_state) if Cdict_init is None: # Initialize randomly structures of dictionary atoms based on samples dataset_means = [C.mean() for C in Cs] Cdict = rng.normal( loc=np.mean(dataset_means), scale=np.std(dataset_means), size=(D, nt, nt) ) else: Cdict = nx.to_numpy(Cdict_init).copy() assert Cdict.shape == (D, nt, nt) if "symmetric" in projection: Cdict = 0.5 * (Cdict + Cdict.transpose((0, 2, 1))) symmetric = True else: symmetric = False if "nonnegative" in projection: Cdict[Cdict < 0.0] = 0 if use_adam_optimizer: adam_moments = _initialize_adam_optimizer(Cdict) log = {"loss_batches": [], "loss_epochs": []} const_q = q[:, None] * q[None, :] Cdict_best_state = Cdict.copy() loss_best_state = np.inf if batch_size > dataset_size: batch_size = dataset_size iter_by_epoch = dataset_size // batch_size + int((dataset_size % batch_size) > 0) for epoch in range(epochs): cumulated_loss_over_epoch = 0.0 for _ in range(iter_by_epoch): # batch sampling batch = rng.choice(range(dataset_size), size=batch_size, replace=False) cumulated_loss_over_batch = 0.0 unmixings = np.zeros((batch_size, D)) Cs_embedded = np.zeros((batch_size, nt, nt)) Ts = [None] * batch_size for batch_idx, C_idx in enumerate(batch): # BCD solver for Gromov-Wasserstein linear unmixing used independently on each structure of the sampled batch ( unmixings[batch_idx], Cs_embedded[batch_idx], Ts[batch_idx], current_loss, ) = gromov_wasserstein_linear_unmixing( Cs[C_idx], Cdict, reg=reg, p=ps[C_idx], q=q, tol_outer=tol_outer, tol_inner=tol_inner, max_iter_outer=max_iter_outer, max_iter_inner=max_iter_inner, symmetric=symmetric, **kwargs, ) cumulated_loss_over_batch += current_loss cumulated_loss_over_epoch += cumulated_loss_over_batch if use_log: log["loss_batches"].append(cumulated_loss_over_batch) # Stochastic projected gradient step over dictionary atoms grad_Cdict = np.zeros_like(Cdict) for batch_idx, C_idx in enumerate(batch): shared_term_structures = Cs_embedded[batch_idx] * const_q - ( Cs[C_idx].dot(Ts[batch_idx]) ).T.dot(Ts[batch_idx]) grad_Cdict += ( unmixings[batch_idx][:, None, None] * shared_term_structures[None, :, :] ) grad_Cdict *= 2 / batch_size if use_adam_optimizer: Cdict, adam_moments = _adam_stochastic_updates( Cdict, grad_Cdict, learning_rate, adam_moments ) else: Cdict -= learning_rate * grad_Cdict if "symmetric" in projection: Cdict = 0.5 * (Cdict + Cdict.transpose((0, 2, 1))) if "nonnegative" in projection: Cdict[Cdict < 0.0] = 0.0 if use_log: log["loss_epochs"].append(cumulated_loss_over_epoch) if loss_best_state > cumulated_loss_over_epoch: loss_best_state = cumulated_loss_over_epoch Cdict_best_state = Cdict.copy() if verbose: print( "--- epoch =", epoch, " cumulated reconstruction error: ", cumulated_loss_over_epoch, ) return nx.from_numpy(Cdict_best_state), log def _initialize_adam_optimizer(variable): # Initialization for our numpy implementation of adam optimizer atoms_adam_m = np.zeros_like(variable) # Initialize first moment tensor atoms_adam_v = np.zeros_like(variable) # Initialize second moment tensor atoms_adam_count = 1 return {"mean": atoms_adam_m, "var": atoms_adam_v, "count": atoms_adam_count} def _adam_stochastic_updates( variable, grad, learning_rate, adam_moments, beta_1=0.9, beta_2=0.99, eps=1e-09 ): adam_moments["mean"] = beta_1 * adam_moments["mean"] + (1 - beta_1) * grad adam_moments["var"] = beta_2 * adam_moments["var"] + (1 - beta_2) * (grad**2) unbiased_m = adam_moments["mean"] / (1 - beta_1 ** adam_moments["count"]) unbiased_v = adam_moments["var"] / (1 - beta_2 ** adam_moments["count"]) variable -= learning_rate * unbiased_m / (np.sqrt(unbiased_v) + eps) adam_moments["count"] += 1 return variable, adam_moments def gromov_wasserstein_linear_unmixing( C, Cdict, reg=0.0, p=None, q=None, tol_outer=10 ** (-5), tol_inner=10 ** (-5), max_iter_outer=20, max_iter_inner=200, symmetric=None, **kwargs, ): r""" Returns the Gromov-Wasserstein linear unmixing of :math:`(\mathbf{C},\mathbf{p})` onto the dictionary :math:`\{ (\mathbf{C_{dict}[d]}, \mathbf{q}) \}_{d \in [D]}`. .. math:: \min_{ \mathbf{w}} GW_2(\mathbf{C}, \sum_{d=1}^D w_d\mathbf{C_{dict}[d]}, \mathbf{p}, \mathbf{q}) - reg \| \mathbf{w} \|_2^2 such that: - :math:`\mathbf{w}^\top \mathbf{1}_D = 1` - :math:`\mathbf{w} \geq \mathbf{0}_D` Where : - :math:`\mathbf{C}` is the (ns,ns) pairwise similarity matrix. - :math:`\mathbf{C_{dict}}` is a (D, nt, nt) tensor of D pairwise similarity matrices of size nt. - :math:`\mathbf{p}` and :math:`\mathbf{q}` are source and target weights. - reg is the regularization coefficient. The algorithm used for solving the problem is a Block Coordinate Descent as discussed in [38]_ , algorithm 1. Parameters ---------- C : array-like, shape (ns, ns) Metric/Graph cost matrix. Cdict : D array-like, shape (D,nt,nt) Metric/Graph cost matrices composing the dictionary on which to embed C. reg : float, optional. Coefficient of the negative quadratic regularization used to promote sparsity of w. Default is 0. p : array-like, shape (ns,), optional Distribution in the source space C. Default is None and corresponds to uniform distribution. q : array-like, shape (nt,), optional Distribution in the space depicted by the dictionary. Default is None and corresponds to uniform distribution. tol_outer : float, optional Solver precision for the BCD algorithm. tol_inner : float, optional Solver precision for the Conjugate Gradient algorithm used to get optimal w at a fixed transport. Default is :math:`10^{-5}`. max_iter_outer : int, optional Maximum number of iterations for the BCD. Default is 20. max_iter_inner : int, optional Maximum number of iterations for the Conjugate Gradient. Default is 200. Returns ------- w: array-like, shape (D,) Gromov-Wasserstein linear unmixing of :math:`(\mathbf{C},\mathbf{p})` onto the span of the dictionary. Cembedded: array-like, shape (nt,nt) embedded structure of :math:`(\mathbf{C},\mathbf{p})` onto the dictionary, :math:`\sum_d w_d\mathbf{C_{dict}[d]}`. T: array-like (ns, nt) Gromov-Wasserstein transport plan between :math:`(\mathbf{C},\mathbf{p})` and :math:`(\sum_d w_d\mathbf{C_{dict}[d]}, \mathbf{q})` current_loss: float reconstruction error References ------- .. [38] C. Vincent-Cuaz, T. Vayer, R. Flamary, M. Corneli, N. Courty, Online Graph Dictionary Learning, International Conference on Machine Learning (ICML), 2021. """ C0, Cdict0 = C, Cdict nx = get_backend(C0, Cdict0) C = nx.to_numpy(C0) Cdict = nx.to_numpy(Cdict0) if p is None: p = unif(C.shape[0]) else: p = nx.to_numpy(p) if q is None: q = unif(Cdict.shape[-1]) else: q = nx.to_numpy(q) T = p[:, None] * q[None, :] D = len(Cdict) w = unif(D) # Initialize uniformly the unmixing w Cembedded = np.sum(w[:, None, None] * Cdict, axis=0) const_q = q[:, None] * q[None, :] # Trackers for BCD convergence convergence_criterion = np.inf current_loss = 10**15 outer_count = 0 while (convergence_criterion > tol_outer) and (outer_count < max_iter_outer): previous_loss = current_loss # 1. Solve GW transport between (C,p) and (\sum_d Cdictionary[d],q) fixing the unmixing w T, log = gromov_wasserstein( C1=C, C2=Cembedded, p=p, q=q, loss_fun="square_loss", G0=T, max_iter=max_iter_inner, tol_rel=tol_inner, tol_abs=0.0, log=True, armijo=False, symmetric=symmetric, **kwargs, ) current_loss = log["gw_dist"] if reg != 0: current_loss -= reg * np.sum(w**2) # 2. Solve linear unmixing problem over w with a fixed transport plan T w, Cembedded, current_loss = _cg_gromov_wasserstein_unmixing( C=C, Cdict=Cdict, Cembedded=Cembedded, w=w, const_q=const_q, T=T, starting_loss=current_loss, reg=reg, tol=tol_inner, max_iter=max_iter_inner, **kwargs, ) if previous_loss != 0: convergence_criterion = abs(previous_loss - current_loss) / abs( previous_loss ) else: # handle numerical issues around 0 convergence_criterion = abs(previous_loss - current_loss) / 10 ** (-15) outer_count += 1 return ( nx.from_numpy(w), nx.from_numpy(Cembedded), nx.from_numpy(T), nx.from_numpy(current_loss), ) def _cg_gromov_wasserstein_unmixing( C, Cdict, Cembedded, w, const_q, T, starting_loss, reg=0.0, tol=10 ** (-5), max_iter=200, **kwargs, ): r""" Returns for a fixed admissible transport plan, the linear unmixing w minimizing the Gromov-Wasserstein cost between :math:`(\mathbf{C},\mathbf{p})` and :math:`(\sum_d w[d]*\mathbf{C_{dict}[d]}, \mathbf{q})` .. math:: \min_{\mathbf{w}} \sum_{ijkl} (C_{i,j} - \sum_{d=1}^D w_d*C_{dict}[d]_{k,l} )^2 T_{i,k}T_{j,l} - reg* \| \mathbf{w} \|_2^2 Such that: - :math:`\mathbf{w}^\top \mathbf{1}_D = 1` - :math:`\mathbf{w} \geq \mathbf{0}_D` Where : - :math:`\mathbf{C}` is the (ns,ns) pairwise similarity matrix. - :math:`\mathbf{C_{dict}}` is a (D, nt, nt) tensor of D pairwise similarity matrices of nt points. - :math:`\mathbf{p}` and :math:`\mathbf{q}` are source and target weights. - :math:`\mathbf{w}` is the linear unmixing of :math:`(\mathbf{C}, \mathbf{p})` onto :math:`(\sum_d w_d \mathbf{Cdict[d]}, \mathbf{q})`. - :math:`\mathbf{T}` is the optimal transport plan conditioned by the current state of :math:`\mathbf{w}`. - reg is the regularization coefficient. The algorithm used for solving the problem is a Conditional Gradient Descent as discussed in [38]_ Parameters ---------- C : array-like, shape (ns, ns) Metric/Graph cost matrix. Cdict : list of D array-like, shape (nt,nt) Metric/Graph cost matrices composing the dictionary on which to embed C. Each matrix in the dictionary must have the same size (nt,nt). Cembedded: array-like, shape (nt,nt) Embedded structure :math:`(\sum_d w[d]*Cdict[d],q)` of :math:`(\mathbf{C},\mathbf{p})` onto the dictionary. Used to avoid redundant computations. w: array-like, shape (D,) Linear unmixing of the input structure onto the dictionary const_q: array-like, shape (nt,nt) product matrix :math:`\mathbf{q}\mathbf{q}^\top` where q is the target space distribution. Used to avoid redundant computations. T: array-like, shape (ns,nt) fixed transport plan between the input structure and its representation in the dictionary. p : array-like, shape (ns,) Distribution in the source space. q : array-like, shape (nt,) Distribution in the embedding space depicted by the dictionary. reg : float, optional. Coefficient of the negative quadratic regularization used to promote sparsity of w. Default is 0. Returns ------- w: ndarray (D,) optimal unmixing of :math:`(\mathbf{C},\mathbf{p})` onto the dictionary span given OT starting from previously optimal unmixing. """ convergence_criterion = np.inf current_loss = starting_loss count = 0 const_TCT = np.transpose(C.dot(T)).dot(T) while (convergence_criterion > tol) and (count < max_iter): previous_loss = current_loss # 1) Compute gradient at current point w grad_w = 2 * np.sum( Cdict * (Cembedded[None, :, :] * const_q[None, :, :] - const_TCT[None, :, :]), axis=(1, 2), ) grad_w -= 2 * reg * w # 2) Conditional gradient direction finding: x= \argmin_x x^T.grad_w min_ = np.min(grad_w) x = (grad_w == min_).astype(np.float64) x /= np.sum(x) # 3) Line-search step: solve \argmin_{\gamma \in [0,1]} a*gamma^2 + b*gamma + c gamma, a, b, Cembedded_diff = _linesearch_gromov_wasserstein_unmixing( w, grad_w, x, Cdict, Cembedded, const_q, const_TCT, reg ) # 4) Updates: w <-- (1-gamma)*w + gamma*x w += gamma * (x - w) Cembedded += gamma * Cembedded_diff current_loss += a * (gamma**2) + b * gamma if previous_loss != 0: # not that the loss can be negative if reg >0 convergence_criterion = abs(previous_loss - current_loss) / abs( previous_loss ) else: # handle numerical issues around 0 convergence_criterion = abs(previous_loss - current_loss) / 10 ** (-15) count += 1 return w, Cembedded, current_loss def _linesearch_gromov_wasserstein_unmixing( w, grad_w, x, Cdict, Cembedded, const_q, const_TCT, reg, **kwargs ): r""" Compute optimal steps for the line search problem of Gromov-Wasserstein linear unmixing .. math:: \min_{\gamma \in [0,1]} \sum_{ijkl} (C_{i,j} - \sum_{d=1}^D z_d(\gamma)C_{dict}[d]_{k,l} )^2 T_{i,k}T_{j,l} - reg\| \mathbf{z}(\gamma) \|_2^2 Such that: - :math:`\mathbf{z}(\gamma) = (1- \gamma)\mathbf{w} + \gamma \mathbf{x}` Parameters ---------- w : array-like, shape (D,) Unmixing. grad_w : array-like, shape (D, D) Gradient of the reconstruction loss with respect to w. x: array-like, shape (D,) Conditional gradient direction. Cdict : list of D array-like, shape (nt,nt) Metric/Graph cost matrices composing the dictionary on which to embed C. Each matrix in the dictionary must have the same size (nt,nt). Cembedded: array-like, shape (nt,nt) Embedded structure :math:`(\sum_d w_dCdict[d],q)` of :math:`(\mathbf{C},\mathbf{p})` onto the dictionary. Used to avoid redundant computations. const_q: array-like, shape (nt,nt) product matrix :math:`\mathbf{q}\mathbf{q}^\top` where q is the target space distribution. Used to avoid redundant computations. const_TCT: array-like, shape (nt, nt) :math:`\mathbf{T}^\top \mathbf{C}^\top \mathbf{T}`. Used to avoid redundant computations. Returns ------- gamma: float Optimal value for the line-search step a: float Constant factor appearing in the factorization :math:`a \gamma^2 + b \gamma +c` of the reconstruction loss b: float Constant factor appearing in the factorization :math:`a \gamma^2 + b \gamma +c` of the reconstruction loss Cembedded_diff: numpy array, shape (nt, nt) Difference between models evaluated in :math:`\mathbf{w}` and in :math:`\mathbf{w}`. reg : float, optional. Coefficient of the negative quadratic regularization used to promote sparsity of :math:`\mathbf{w}`. """ # 3) Line-search step: solve \argmin_{\gamma \in [0,1]} a*gamma^2 + b*gamma + c Cembedded_x = np.sum(x[:, None, None] * Cdict, axis=0) Cembedded_diff = Cembedded_x - Cembedded trace_diffx = np.sum(Cembedded_diff * Cembedded_x * const_q) trace_diffw = np.sum(Cembedded_diff * Cembedded * const_q) a = trace_diffx - trace_diffw b = 2 * (trace_diffw - np.sum(Cembedded_diff * const_TCT)) if reg != 0: a -= reg * np.sum((x - w) ** 2) b -= 2 * reg * np.sum(w * (x - w)) if a > 0: gamma = min(1, max(0, -b / (2 * a))) elif a + b < 0: gamma = 1 else: gamma = 0 return gamma, a, b, Cembedded_diff def fused_gromov_wasserstein_dictionary_learning( Cs, Ys, D, nt, alpha, reg=0.0, ps=None, q=None, epochs=20, batch_size=32, learning_rate_C=1.0, learning_rate_Y=1.0, Cdict_init=None, Ydict_init=None, projection="nonnegative_symmetric", use_log=False, tol_outer=10 ** (-5), tol_inner=10 ** (-5), max_iter_outer=20, max_iter_inner=200, use_adam_optimizer=True, verbose=False, random_state=None, **kwargs, ): r""" Infer Fused Gromov-Wasserstein linear dictionary :math:`\{ (\mathbf{C_{dict}[d]}, \mathbf{Y_{dict}[d]}, \mathbf{q}) \}_{d \in [D]}` from the list of S attributed structures :math:`\{ (\mathbf{C_s}, \mathbf{Y_s},\mathbf{p_s}) \}_s` .. math:: \min_{\mathbf{C_{dict}},\mathbf{Y_{dict}}, \{\mathbf{w_s}\}_{s}} \sum_{s=1}^S FGW_{2,\alpha}(\mathbf{C_s}, \mathbf{Y_s}, \sum_{d=1}^D w_{s,d}\mathbf{C_{dict}[d]},\sum_{d=1}^D w_{s,d}\mathbf{Y_{dict}[d]}, \mathbf{p_s}, \mathbf{q}) \\ - reg\| \mathbf{w_s} \|_2^2 Such that :math:`\forall s \leq S` : - :math:`\mathbf{w_s}^\top \mathbf{1}_D = 1` - :math:`\mathbf{w_s} \geq \mathbf{0}_D` Where : - :math:`\forall s \leq S, \mathbf{C_s}` is a (ns,ns) pairwise similarity matrix of variable size ns. - :math:`\forall s \leq S, \mathbf{Y_s}` is a (ns,d) features matrix of variable size ns and fixed dimension d. - :math:`\mathbf{C_{dict}}` is a (D, nt, nt) tensor of D pairwise similarity matrix of fixed size nt. - :math:`\mathbf{Y_{dict}}` is a (D, nt, d) tensor of D features matrix of fixed size nt and fixed dimension d. - :math:`\forall s \leq S, \mathbf{p_s}` is the source distribution corresponding to :math:`\mathbf{C_s}` - :math:`\mathbf{q}` is the target distribution assigned to every structures in the embedding space. - :math:`\alpha` is the trade-off parameter of Fused Gromov-Wasserstein - reg is the regularization coefficient. The stochastic algorithm used for estimating the attributed graph dictionary atoms as proposed in [38]_ Parameters ---------- Cs : list of S symmetric array-like, shape (ns, ns) List of Metric/Graph cost matrices of variable size (ns,ns). Ys : list of S array-like, shape (ns, d) List of feature matrix of variable size (ns,d) with d fixed. D: int Number of dictionary atoms to learn nt: int Number of samples within each dictionary atoms alpha : float Trade-off parameter of Fused Gromov-Wasserstein reg : float, optional Coefficient of the negative quadratic regularization used to promote sparsity of w. The default is 0. ps : list of S array-like, shape (ns,), optional Distribution in each source space C of Cs. Default is None and corresponds to uniform distributions. q : array-like, shape (nt,), optional Distribution in the embedding space whose structure will be learned. Default is None and corresponds to uniform distributions. epochs: int, optional Number of epochs used to learn the dictionary. Default is 32. batch_size: int, optional Batch size for each stochastic gradient update of the dictionary. Set to the dataset size if the provided batch_size is higher than the dataset size. Default is 32. learning_rate_C: float, optional Learning rate used for the stochastic gradient descent on Cdict. Default is 1. learning_rate_Y: float, optional Learning rate used for the stochastic gradient descent on Ydict. Default is 1. Cdict_init: list of D array-like with shape (nt, nt), optional Used to initialize the dictionary structures Cdict. If set to None (Default), the dictionary will be initialized randomly. Else Cdict must have shape (D, nt, nt) i.e match provided shape features. Ydict_init: list of D array-like with shape (nt, d), optional Used to initialize the dictionary features Ydict. If set to None, the dictionary features will be initialized randomly. Else Ydict must have shape (D, nt, d) where d is the features dimension of inputs Ys and also match provided shape features. projection: str, optional If 'nonnegative' and/or 'symmetric' is in projection, the corresponding projection will be performed at each stochastic update of the dictionary Else the set of atoms is :math:`R^{nt * nt}`. Default is 'nonnegative_symmetric' log: bool, optional If set to True, losses evolution by batches and epochs are tracked. Default is False. use_adam_optimizer: bool, optional If set to True, adam optimizer with default settings is used as adaptative learning rate strategy. Else perform SGD with fixed learning rate. Default is True. tol_outer : float, optional Solver precision for the BCD algorithm, measured by absolute relative error on consecutive losses. Default is :math:`10^{-5}`. tol_inner : float, optional Solver precision for the Conjugate Gradient algorithm used to get optimal w at a fixed transport, measured by absolute relative error on consecutive losses. Default is :math:`10^{-5}`. max_iter_outer : int, optional Maximum number of iterations for the BCD. Default is 20. max_iter_inner : int, optional Maximum number of iterations for the Conjugate Gradient. Default is 200. verbose : bool, optional Print the reconstruction loss every epoch. Default is False. random_state : int, RandomState instance or None, default=None Determines random number generation. Pass an int for reproducible output across multiple function calls. Returns ------- Cdict_best_state : D array-like, shape (D,nt,nt) Metric/Graph cost matrices composing the dictionary. The dictionary leading to the best loss over an epoch is saved and returned. Ydict_best_state : D array-like, shape (D,nt,d) Feature matrices composing the dictionary. The dictionary leading to the best loss over an epoch is saved and returned. log: dict If use_log is True, contains loss evolutions by batches and epochs. References ------- .. [38] C. Vincent-Cuaz, T. Vayer, R. Flamary, M. Corneli, N. Courty, Online Graph Dictionary Learning, International Conference on Machine Learning (ICML), 2021. """ Cs0, Ys0 = Cs, Ys nx = get_backend(*Cs0, *Ys0) Cs = [nx.to_numpy(C) for C in Cs0] Ys = [nx.to_numpy(Y) for Y in Ys0] d = Ys[0].shape[-1] dataset_size = len(Cs) if ps is None: ps = [unif(C.shape[0]) for C in Cs] else: ps = [nx.to_numpy(p) for p in ps] if q is None: q = unif(nt) else: q = nx.to_numpy(q) rng = check_random_state(random_state) if Cdict_init is None: # Initialize randomly structures of dictionary atoms based on samples dataset_means = [C.mean() for C in Cs] Cdict = rng.normal( loc=np.mean(dataset_means), scale=np.std(dataset_means), size=(D, nt, nt) ) else: Cdict = nx.to_numpy(Cdict_init).copy() assert Cdict.shape == (D, nt, nt) if Ydict_init is None: # Initialize randomly features of dictionary atoms based on samples distribution by feature component dataset_feature_means = np.stack([F.mean(axis=0) for F in Ys]) Ydict = rng.normal( loc=dataset_feature_means.mean(axis=0), scale=dataset_feature_means.std(axis=0), size=(D, nt, d), ) else: Ydict = nx.to_numpy(Ydict_init).copy() assert Ydict.shape == (D, nt, d) if "symmetric" in projection: Cdict = 0.5 * (Cdict + Cdict.transpose((0, 2, 1))) symmetric = True else: symmetric = False if "nonnegative" in projection: Cdict[Cdict < 0.0] = 0.0 if use_adam_optimizer: adam_moments_C = _initialize_adam_optimizer(Cdict) adam_moments_Y = _initialize_adam_optimizer(Ydict) log = {"loss_batches": [], "loss_epochs": []} const_q = q[:, None] * q[None, :] diag_q = np.diag(q) Cdict_best_state = Cdict.copy() Ydict_best_state = Ydict.copy() loss_best_state = np.inf if batch_size > dataset_size: batch_size = dataset_size iter_by_epoch = dataset_size // batch_size + int((dataset_size % batch_size) > 0) for epoch in range(epochs): cumulated_loss_over_epoch = 0.0 for _ in range(iter_by_epoch): # Batch iterations batch = rng.choice(range(dataset_size), size=batch_size, replace=False) cumulated_loss_over_batch = 0.0 unmixings = np.zeros((batch_size, D)) Cs_embedded = np.zeros((batch_size, nt, nt)) Ys_embedded = np.zeros((batch_size, nt, d)) Ts = [None] * batch_size for batch_idx, C_idx in enumerate(batch): # BCD solver for Gromov-Wasserstein linear unmixing used independently on each structure of the sampled batch ( unmixings[batch_idx], Cs_embedded[batch_idx], Ys_embedded[batch_idx], Ts[batch_idx], current_loss, ) = fused_gromov_wasserstein_linear_unmixing( Cs[C_idx], Ys[C_idx], Cdict, Ydict, alpha, reg=reg, p=ps[C_idx], q=q, tol_outer=tol_outer, tol_inner=tol_inner, max_iter_outer=max_iter_outer, max_iter_inner=max_iter_inner, symmetric=symmetric, **kwargs, ) cumulated_loss_over_batch += current_loss cumulated_loss_over_epoch += cumulated_loss_over_batch if use_log: log["loss_batches"].append(cumulated_loss_over_batch) # Stochastic projected gradient step over dictionary atoms grad_Cdict = np.zeros_like(Cdict) grad_Ydict = np.zeros_like(Ydict) for batch_idx, C_idx in enumerate(batch): shared_term_structures = Cs_embedded[batch_idx] * const_q - ( Cs[C_idx].dot(Ts[batch_idx]) ).T.dot(Ts[batch_idx]) shared_term_features = diag_q.dot(Ys_embedded[batch_idx]) - Ts[ batch_idx ].T.dot(Ys[C_idx]) grad_Cdict += ( alpha * unmixings[batch_idx][:, None, None] * shared_term_structures[None, :, :] ) grad_Ydict += ( (1 - alpha) * unmixings[batch_idx][:, None, None] * shared_term_features[None, :, :] ) grad_Cdict *= 2 / batch_size grad_Ydict *= 2 / batch_size if use_adam_optimizer: Cdict, adam_moments_C = _adam_stochastic_updates( Cdict, grad_Cdict, learning_rate_C, adam_moments_C ) Ydict, adam_moments_Y = _adam_stochastic_updates( Ydict, grad_Ydict, learning_rate_Y, adam_moments_Y ) else: Cdict -= learning_rate_C * grad_Cdict Ydict -= learning_rate_Y * grad_Ydict if "symmetric" in projection: Cdict = 0.5 * (Cdict + Cdict.transpose((0, 2, 1))) if "nonnegative" in projection: Cdict[Cdict < 0.0] = 0.0 if use_log: log["loss_epochs"].append(cumulated_loss_over_epoch) if loss_best_state > cumulated_loss_over_epoch: loss_best_state = cumulated_loss_over_epoch Cdict_best_state = Cdict.copy() Ydict_best_state = Ydict.copy() if verbose: print( "--- epoch: ", epoch, " cumulated reconstruction error: ", cumulated_loss_over_epoch, ) return nx.from_numpy(Cdict_best_state), nx.from_numpy(Ydict_best_state), log def fused_gromov_wasserstein_linear_unmixing( C, Y, Cdict, Ydict, alpha, reg=0.0, p=None, q=None, tol_outer=10 ** (-5), tol_inner=10 ** (-5), max_iter_outer=20, max_iter_inner=200, symmetric=True, **kwargs, ): r""" Returns the Fused Gromov-Wasserstein linear unmixing of :math:`(\mathbf{C},\mathbf{Y},\mathbf{p})` onto the attributed dictionary atoms :math:`\{ (\mathbf{C_{dict}[d]},\mathbf{Y_{dict}[d]}, \mathbf{q}) \}_{d \in [D]}` .. math:: \min_{\mathbf{w}} FGW_{2,\alpha}(\mathbf{C},\mathbf{Y}, \sum_{d=1}^D w_d\mathbf{C_{dict}[d]},\sum_{d=1}^D w_d\mathbf{Y_{dict}[d]}, \mathbf{p}, \mathbf{q}) - reg \| \mathbf{w} \|_2^2 such that, :math:`\forall s \leq S` : - :math:`\mathbf{w_s}^\top \mathbf{1}_D = 1` - :math:`\mathbf{w_s} \geq \mathbf{0}_D` Where : - :math:`\mathbf{C}` is a (ns,ns) pairwise similarity matrix of variable size ns. - :math:`\mathbf{Y}` is a (ns,d) features matrix of variable size ns and fixed dimension d. - :math:`\mathbf{C_{dict}}` is a (D, nt, nt) tensor of D pairwise similarity matrix of fixed size nt. - :math:`\mathbf{Y_{dict}}` is a (D, nt, d) tensor of D features matrix of fixed size nt and fixed dimension d. - :math:`\mathbf{p}` is the source distribution corresponding to :math:`\mathbf{C_s}` - :math:`\mathbf{q}` is the target distribution assigned to every structures in the embedding space. - :math:`\alpha` is the trade-off parameter of Fused Gromov-Wasserstein - reg is the regularization coefficient. The algorithm used for solving the problem is a Block Coordinate Descent as discussed in [38]_, algorithm 6. Parameters ---------- C : array-like, shape (ns, ns) Metric/Graph cost matrix. Y : array-like, shape (ns, d) Feature matrix. Cdict : D array-like, shape (D,nt,nt) Metric/Graph cost matrices composing the dictionary on which to embed (C,Y). Ydict : D array-like, shape (D,nt,d) Feature matrices composing the dictionary on which to embed (C,Y). alpha: float, Trade-off parameter of Fused Gromov-Wasserstein. reg : float, optional Coefficient of the negative quadratic regularization used to promote sparsity of w. The default is 0. p : array-like, shape (ns,), optional Distribution in the source space C. Default is None and corresponds to uniform distribution. q : array-like, shape (nt,), optional Distribution in the space depicted by the dictionary. Default is None and corresponds to uniform distribution. tol_outer : float, optional Solver precision for the BCD algorithm. tol_inner : float, optional Solver precision for the Conjugate Gradient algorithm used to get optimal w at a fixed transport. Default is :math:`10^{-5}`. max_iter_outer : int, optional Maximum number of iterations for the BCD. Default is 20. max_iter_inner : int, optional Maximum number of iterations for the Conjugate Gradient. Default is 200. Returns ------- w: array-like, shape (D,) fused Gromov-Wasserstein linear unmixing of (C,Y,p) onto the span of the dictionary. Cembedded: array-like, shape (nt,nt) embedded structure of :math:`(\mathbf{C},\mathbf{Y}, \mathbf{p})` onto the dictionary, :math:`\sum_d w_d\mathbf{C_{dict}[d]}`. Yembedded: array-like, shape (nt,d) embedded features of :math:`(\mathbf{C},\mathbf{Y}, \mathbf{p})` onto the dictionary, :math:`\sum_d w_d\mathbf{Y_{dict}[d]}`. T: array-like (ns,nt) Fused Gromov-Wasserstein transport plan between :math:`(\mathbf{C},\mathbf{p})` and :math:`(\sum_d w_d\mathbf{C_{dict}[d]}, \sum_d w_d\mathbf{Y_{dict}[d]},\mathbf{q})`. current_loss: float reconstruction error References ------- .. [38] C. Vincent-Cuaz, T. Vayer, R. Flamary, M. Corneli, N. Courty, Online Graph Dictionary Learning, International Conference on Machine Learning (ICML), 2021. """ C0, Y0, Cdict0, Ydict0 = C, Y, Cdict, Ydict nx = get_backend(C0, Y0, Cdict0, Ydict0) C = nx.to_numpy(C0) Y = nx.to_numpy(Y0) Cdict = nx.to_numpy(Cdict0) Ydict = nx.to_numpy(Ydict0) if p is None: p = unif(C.shape[0]) else: p = nx.to_numpy(p) if q is None: q = unif(Cdict.shape[-1]) else: q = nx.to_numpy(q) T = p[:, None] * q[None, :] D = len(Cdict) d = Y.shape[-1] w = unif(D) # Initialize with uniform weights ns = C.shape[-1] nt = Cdict.shape[-1] # modeling (C,Y) Cembedded = np.sum(w[:, None, None] * Cdict, axis=0) Yembedded = np.sum(w[:, None, None] * Ydict, axis=0) # constants depending on q const_q = q[:, None] * q[None, :] diag_q = np.diag(q) # Trackers for BCD convergence convergence_criterion = np.inf current_loss = 10**15 outer_count = 0 Ys_constM = (Y**2).dot( np.ones((d, nt)) ) # constant in computing euclidean pairwise feature matrix while (convergence_criterion > tol_outer) and (outer_count < max_iter_outer): previous_loss = current_loss # 1. Solve GW transport between (C,p) and (\sum_d Cdictionary[d],q) fixing the unmixing w Yt_varM = (np.ones((ns, d))).dot((Yembedded**2).T) M = ( Ys_constM + Yt_varM - 2 * Y.dot(Yembedded.T) ) # euclidean distance matrix between features T, log = fused_gromov_wasserstein( M, C, Cembedded, p, q, loss_fun="square_loss", alpha=alpha, max_iter=max_iter_inner, tol_rel=tol_inner, tol_abs=0.0, armijo=False, G0=T, log=True, symmetric=symmetric, **kwargs, ) current_loss = log["fgw_dist"] if reg != 0: current_loss -= reg * np.sum(w**2) # 2. Solve linear unmixing problem over w with a fixed transport plan T w, Cembedded, Yembedded, current_loss = _cg_fused_gromov_wasserstein_unmixing( C, Y, Cdict, Ydict, Cembedded, Yembedded, w, T, p, q, const_q, diag_q, current_loss, alpha, reg, tol=tol_inner, max_iter=max_iter_inner, **kwargs, ) if previous_loss != 0: convergence_criterion = abs(previous_loss - current_loss) / abs( previous_loss ) else: convergence_criterion = abs(previous_loss - current_loss) / 10 ** (-12) outer_count += 1 return ( nx.from_numpy(w), nx.from_numpy(Cembedded), nx.from_numpy(Yembedded), nx.from_numpy(T), nx.from_numpy(current_loss), ) def _cg_fused_gromov_wasserstein_unmixing( C, Y, Cdict, Ydict, Cembedded, Yembedded, w, T, p, q, const_q, diag_q, starting_loss, alpha, reg, tol=10 ** (-6), max_iter=200, **kwargs, ): r""" Returns for a fixed admissible transport plan, the optimal linear unmixing :math:`\mathbf{w}` minimizing the Fused Gromov-Wasserstein cost between :math:`(\mathbf{C},\mathbf{Y},\mathbf{p})` and :math:`(\sum_d w_d \mathbf{C_{dict}[d]},\sum_d w_d*\mathbf{Y_{dict}[d]}, \mathbf{q})` .. math:: \min_{\mathbf{w}} \alpha \sum_{ijkl} (C_{i,j} - \sum_{d=1}^D w_d C_{dict}[d]_{k,l} )^2 T_{i,k}T_{j,l} \\+ (1-\alpha) \sum_{ij} \| \mathbf{Y_i} - \sum_d w_d \mathbf{Y_{dict}[d]_j} \|_2^2 T_{ij}- reg \| \mathbf{w} \|_2^2 Such that : - :math:`\mathbf{w}^\top \mathbf{1}_D = 1` - :math:`\mathbf{w} \geq \mathbf{0}_D` Where : - :math:`\mathbf{C}` is a (ns,ns) pairwise similarity matrix of variable size ns. - :math:`\mathbf{Y}` is a (ns,d) features matrix of variable size ns and fixed dimension d. - :math:`\mathbf{C_{dict}}` is a (D, nt, nt) tensor of D pairwise similarity matrix of fixed size nt. - :math:`\mathbf{Y_{dict}}` is a (D, nt, d) tensor of D features matrix of fixed size nt and fixed dimension d. - :math:`\mathbf{p}` is the source distribution corresponding to :math:`\mathbf{C_s}` - :math:`\mathbf{q}` is the target distribution assigned to every structures in the embedding space. - :math:`\mathbf{T}` is the optimal transport plan conditioned by the previous state of :math:`\mathbf{w}` - :math:`\alpha` is the trade-off parameter of Fused Gromov-Wasserstein - reg is the regularization coefficient. The algorithm used for solving the problem is a Conditional Gradient Descent as discussed in [38]_, algorithm 7. Parameters ---------- C : array-like, shape (ns, ns) Metric/Graph cost matrix. Y : array-like, shape (ns, d) Feature matrix. Cdict : list of D array-like, shape (nt,nt) Metric/Graph cost matrices composing the dictionary on which to embed (C,Y). Each matrix in the dictionary must have the same size (nt,nt). Ydict : list of D array-like, shape (nt,d) Feature matrices composing the dictionary on which to embed (C,Y). Each matrix in the dictionary must have the same size (nt,d). Cembedded: array-like, shape (nt,nt) Embedded structure of (C,Y) onto the dictionary Yembedded: array-like, shape (nt,d) Embedded features of (C,Y) onto the dictionary w: array-like, shape (n_D,) Linear unmixing of (C,Y) onto (Cdict,Ydict) const_q: array-like, shape (nt,nt) product matrix :math:`\mathbf{qq}^\top` where :math:`\mathbf{q}` is the target space distribution. diag_q: array-like, shape (nt,nt) diagonal matrix with values of q on the diagonal. T: array-like, shape (ns,nt) fixed transport plan between (C,Y) and its model p : array-like, shape (ns,) Distribution in the source space (C,Y). q : array-like, shape (nt,) Distribution in the embedding space depicted by the dictionary. alpha: float, Trade-off parameter of Fused Gromov-Wasserstein. reg : float, optional Coefficient of the negative quadratic regularization used to promote sparsity of w. Returns ------- w: ndarray (D,) linear unmixing of :math:`(\mathbf{C},\mathbf{Y},\mathbf{p})` onto the span of :math:`(C_{dict},Y_{dict})` given OT corresponding to previous unmixing. """ convergence_criterion = np.inf current_loss = starting_loss count = 0 const_TCT = np.transpose(C.dot(T)).dot(T) ones_ns_d = np.ones(Y.shape) while (convergence_criterion > tol) and (count < max_iter): previous_loss = current_loss # 1) Compute gradient at current point w # structure grad_w = alpha * np.sum( Cdict * (Cembedded[None, :, :] * const_q[None, :, :] - const_TCT[None, :, :]), axis=(1, 2), ) # feature grad_w += (1 - alpha) * np.sum( Ydict * (diag_q.dot(Yembedded)[None, :, :] - T.T.dot(Y)[None, :, :]), axis=(1, 2), ) grad_w -= reg * w grad_w *= 2 # 2) Conditional gradient direction finding: x= \argmin_x x^T.grad_w min_ = np.min(grad_w) x = (grad_w == min_).astype(np.float64) x /= np.sum(x) # 3) Line-search step: solve \argmin_{\gamma \in [0,1]} a*gamma^2 + b*gamma + c gamma, a, b, Cembedded_diff, Yembedded_diff = ( _linesearch_fused_gromov_wasserstein_unmixing( w, grad_w, x, Y, Cdict, Ydict, Cembedded, Yembedded, T, const_q, const_TCT, ones_ns_d, alpha, reg, ) ) # 4) Updates: w <-- (1-gamma)*w + gamma*x w += gamma * (x - w) Cembedded += gamma * Cembedded_diff Yembedded += gamma * Yembedded_diff current_loss += a * (gamma**2) + b * gamma if previous_loss != 0: convergence_criterion = abs(previous_loss - current_loss) / abs( previous_loss ) else: convergence_criterion = abs(previous_loss - current_loss) / 10 ** (-12) count += 1 return w, Cembedded, Yembedded, current_loss def _linesearch_fused_gromov_wasserstein_unmixing( w, grad_w, x, Y, Cdict, Ydict, Cembedded, Yembedded, T, const_q, const_TCT, ones_ns_d, alpha, reg, **kwargs, ): r""" Compute optimal steps for the line search problem of Fused Gromov-Wasserstein linear unmixing .. math:: \min_{\gamma \in [0,1]} \alpha \sum_{ijkl} (C_{i,j} - \sum_{d=1}^D z_d(\gamma)C_{dict}[d]_{k,l} )^2 T_{i,k}T_{j,l} \\ + (1-\alpha) \sum_{ij} \| \mathbf{Y_i} - \sum_d z_d(\gamma) \mathbf{Y_{dict}[d]_j} \|_2^2 - reg\| \mathbf{z}(\gamma) \|_2^2 Such that : - :math:`\mathbf{z}(\gamma) = (1- \gamma)\mathbf{w} + \gamma \mathbf{x}` Parameters ---------- w : array-like, shape (D,) Unmixing. grad_w : array-like, shape (D, D) Gradient of the reconstruction loss with respect to w. x: array-like, shape (D,) Conditional gradient direction. Y: arrat-like, shape (ns,d) Feature matrix of the input space Cdict : list of D array-like, shape (nt, nt) Metric/Graph cost matrices composing the dictionary on which to embed (C,Y). Each matrix in the dictionary must have the same size (nt,nt). Ydict : list of D array-like, shape (nt, d) Feature matrices composing the dictionary on which to embed (C,Y). Each matrix in the dictionary must have the same size (nt,d). Cembedded: array-like, shape (nt, nt) Embedded structure of (C,Y) onto the dictionary Yembedded: array-like, shape (nt, d) Embedded features of (C,Y) onto the dictionary T: array-like, shape (ns, nt) Fixed transport plan between (C,Y) and its current model. const_q: array-like, shape (nt,nt) product matrix :math:`\mathbf{q}\mathbf{q}^\top` where q is the target space distribution. Used to avoid redundant computations. const_TCT: array-like, shape (nt, nt) :math:`\mathbf{T}^\top \mathbf{C}^\top \mathbf{T}`. Used to avoid redundant computations. ones_ns_d: array-like, shape (ns, d) :math:`\mathbf{1}_{ ns \times d}`. Used to avoid redundant computations. alpha: float, Trade-off parameter of Fused Gromov-Wasserstein. reg : float, optional Coefficient of the negative quadratic regularization used to promote sparsity of w. Returns ------- gamma: float Optimal value for the line-search step a: float Constant factor appearing in the factorization :math:`a \gamma^2 + b \gamma +c` of the reconstruction loss b: float Constant factor appearing in the factorization :math:`a \gamma^2 + b \gamma +c` of the reconstruction loss Cembedded_diff: numpy array, shape (nt, nt) Difference between structure matrix of models evaluated in :math:`\mathbf{w}` and in :math:`\mathbf{w}`. Yembedded_diff: numpy array, shape (nt, nt) Difference between feature matrix of models evaluated in :math:`\mathbf{w}` and in :math:`\mathbf{w}`. """ # polynomial coefficients from quadratic objective (with respect to w) on structures Cembedded_x = np.sum(x[:, None, None] * Cdict, axis=0) Cembedded_diff = Cembedded_x - Cembedded trace_diffx = np.sum(Cembedded_diff * Cembedded_x * const_q) trace_diffw = np.sum(Cembedded_diff * Cembedded * const_q) # Constant factor appearing in the factorization a*gamma^2 + b*g + c of the Gromov-Wasserstein reconstruction loss a_gw = trace_diffx - trace_diffw b_gw = 2 * (trace_diffw - np.sum(Cembedded_diff * const_TCT)) # polynomial coefficient from quadratic objective (with respect to w) on features Yembedded_x = np.sum(x[:, None, None] * Ydict, axis=0) Yembedded_diff = Yembedded_x - Yembedded # Constant factor appearing in the factorization a*gamma^2 + b*g + c of the Gromov-Wasserstein reconstruction loss a_w = np.sum(ones_ns_d.dot((Yembedded_diff**2).T) * T) b_w = 2 * np.sum( T * (ones_ns_d.dot((Yembedded * Yembedded_diff).T) - Y.dot(Yembedded_diff.T)) ) a = alpha * a_gw + (1 - alpha) * a_w b = alpha * b_gw + (1 - alpha) * b_w if reg != 0: a -= reg * np.sum((x - w) ** 2) b -= 2 * reg * np.sum(w * (x - w)) if a > 0: gamma = min(1, max(0, -b / (2 * a))) elif a + b < 0: gamma = 1 else: gamma = 0 return gamma, a, b, Cembedded_diff, Yembedded_diff python-pot-0.9.7+dfsg/ot/gromov/_estimators.py000066400000000000000000000404351525521106200214320ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Gromov-Wasserstein and Fused-Gromov-Wasserstein stochastic estimators. """ # Author: Rémi Flamary # Tanguy Kerdoncuff # # License: MIT License import numpy as np from ..bregman import sinkhorn from ..utils import list_to_array, check_random_state from ..lp import emd_1d, emd from ..backend import get_backend def GW_distance_estimation( C1, C2, p, q, loss_fun, T, nb_samples_p=None, nb_samples_q=None, std=True, random_state=None, ): r""" Returns an approximation of the Gromov-Wasserstein loss between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})` with a fixed transport plan :math:`\mathbf{T}`. To recover an approximation of the Gromov-Wasserstein distance as defined in [13] compute :math:`d_{GW} = \frac{1}{2} \sqrt{\mathbf{GW}}`. The function gives an unbiased approximation of the following equation: .. math:: \mathbf{GW} = \sum_{i,j,k,l} L(\mathbf{C_{1}}_{i,k}, \mathbf{C_{2}}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - `L` : Loss function to account for the misfit between the similarity matrices - :math:`\mathbf{T}`: Matrix with marginal :math:`\mathbf{p}` and :math:`\mathbf{q}` Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,) Distribution in the source space q : array-like, shape (nt,) Distribution in the target space loss_fun : function: :math:`\mathbb{R} \times \mathbb{R} \mapsto \mathbb{R}` Loss function used for the distance, the transport plan does not depend on the loss function T : csr or array-like, shape (ns, nt) Transport plan matrix, either a sparse csr or a dense matrix nb_samples_p : int, optional `nb_samples_p` is the number of samples (without replacement) along the first dimension of :math:`\mathbf{T}` nb_samples_q : int, optional `nb_samples_q` is the number of samples along the second dimension of :math:`\mathbf{T}`, for each sample along the first std : bool, optional Standard deviation associated with the prediction of the gromov-wasserstein cost random_state : int or RandomState instance, optional Fix the seed for reproducibility Returns ------- : float Gromov-wasserstein cost References ---------- .. [14] Kerdoncuff, Tanguy, Emonet, Rémi, Sebban, Marc "Sampled Gromov Wasserstein." Machine Learning Journal (MLJ). 2021. """ C1, C2, p, q = list_to_array(C1, C2, p, q) nx = get_backend(C1, C2, p, q) generator = check_random_state(random_state) len_p = p.shape[0] len_q = q.shape[0] # It is always better to sample from the biggest distribution first. if len_p < len_q: p, q = q, p len_p, len_q = len_q, len_p C1, C2 = C2, C1 T = T.T if nb_samples_p is None: if nx.issparse(T): # If T is sparse, it probably mean that PoGroW was used, thus the number of sample is reduced nb_samples_p = min(int(5 * (len_p * np.log(len_p)) ** 0.5), len_p) else: nb_samples_p = len_p else: # The number of sample along the first dimension is without replacement. nb_samples_p = min(nb_samples_p, len_p) if nb_samples_q is None: nb_samples_q = 1 if std: nb_samples_q = max(2, nb_samples_q) index_k = np.zeros((nb_samples_p, nb_samples_q), dtype=int) index_l = np.zeros((nb_samples_p, nb_samples_q), dtype=int) index_i = generator.choice( len_p, size=nb_samples_p, p=nx.to_numpy(p), replace=False ) index_j = generator.choice( len_p, size=nb_samples_p, p=nx.to_numpy(p), replace=False ) for i in range(nb_samples_p): if nx.issparse(T): T_indexi = nx.reshape(nx.todense(T[[index_i[i]], :]), (-1,)) T_indexj = nx.reshape(nx.todense(T[[index_j[i]], :]), (-1,)) else: T_indexi = T[index_i[i], :] T_indexj = T[index_j[i], :] # For each of the row sampled, the column is sampled. index_k[i] = generator.choice( len_q, size=nb_samples_q, p=nx.to_numpy(T_indexi / nx.sum(T_indexi)), replace=True, ) index_l[i] = generator.choice( len_q, size=nb_samples_q, p=nx.to_numpy(T_indexj / nx.sum(T_indexj)), replace=True, ) list_value_sample = nx.stack( [ loss_fun( C1[np.ix_(index_i, index_j)], C2[np.ix_(index_k[:, n], index_l[:, n])] ) for n in range(nb_samples_q) ], axis=2, ) if std: std_value = nx.sum(nx.std(list_value_sample, axis=2) ** 2) ** 0.5 return nx.mean(list_value_sample), std_value / (nb_samples_p * nb_samples_p) else: return nx.mean(list_value_sample) def pointwise_gromov_wasserstein( C1, C2, p, q, loss_fun, alpha=1, max_iter=100, threshold_plan=0, log=False, verbose=False, random_state=None, ): r""" Returns the gromov-wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})` using a stochastic Frank-Wolfe. This method has a :math:`\mathcal{O}(\mathrm{max\_iter} \times PN^2)` time complexity with `P` the number of Sinkhorn iterations. The function solves the following optimization problem: .. math:: \mathbf{GW} = \mathop{\arg \min}_\mathbf{T} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - `L`: loss function to account for the misfit between the similarity matrices Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,) Distribution in the source space q : array-like, shape (nt,) Distribution in the target space loss_fun : function: :math:`\mathbb{R} \times \mathbb{R} \mapsto \mathbb{R}` Loss function used for the distance, the transport plan does not depend on the loss function alpha : float Step of the Frank-Wolfe algorithm, should be between 0 and 1 max_iter : int, optional Max number of iterations threshold_plan : float, optional Deleting very small values in the transport plan. If above zero, it violates the marginal constraints. verbose : bool, optional Print information along iterations log : bool, optional Gives the distance estimated and the standard deviation random_state : int or RandomState instance, optional Fix the seed for reproducibility Returns ------- T : array-like, shape (`ns`, `nt`) Optimal coupling between the two spaces References ---------- .. [14] Kerdoncuff, Tanguy, Emonet, Rémi, Sebban, Marc "Sampled Gromov Wasserstein." Machine Learning Journal (MLJ). 2021. """ C1, C2, p, q = list_to_array(C1, C2, p, q) nx = get_backend(C1, C2, p, q) len_p = p.shape[0] len_q = q.shape[0] generator = check_random_state(random_state) index = np.zeros(2, dtype=int) # Initialize with default marginal index[0] = int(generator.choice(len_p, size=1, p=nx.to_numpy(p)).item()) index[1] = int(generator.choice(len_q, size=1, p=nx.to_numpy(q)).item()) T = nx.tocsr(emd_1d(C1[index[0]], C2[index[1]], a=p, b=q, dense=False)) best_gw_dist_estimated = np.inf for cpt in range(max_iter): index[0] = int(generator.choice(len_p, size=1, p=nx.to_numpy(p)).item()) T_index0 = nx.reshape(nx.todense(T[[index[0]], :]), (-1,)) index[1] = int( generator.choice( len_q, size=1, p=nx.to_numpy(T_index0 / nx.sum(T_index0)) ).item() ) if alpha == 1: T = nx.tocsr(emd_1d(C1[index[0]], C2[index[1]], a=p, b=q, dense=False)) else: new_T = nx.tocsr(emd_1d(C1[index[0]], C2[index[1]], a=p, b=q, dense=False)) T = (1 - alpha) * T + alpha * new_T # To limit the number of non 0, the values below the threshold are set to 0. T = nx.eliminate_zeros(T, threshold=threshold_plan) if cpt % 10 == 0 or cpt == (max_iter - 1): gw_dist_estimated = GW_distance_estimation( C1=C1, C2=C2, loss_fun=loss_fun, p=p, q=q, T=T, std=False, random_state=generator, ) if gw_dist_estimated < best_gw_dist_estimated: best_gw_dist_estimated = gw_dist_estimated best_T = nx.copy(T) if verbose: if cpt % 200 == 0: print( "{:5s}|{:12s}".format("It.", "Best gw estimated") + "\n" + "-" * 19 ) print("{:5d}|{:8e}|".format(cpt, best_gw_dist_estimated)) if log: log = {} log["gw_dist_estimated"], log["gw_dist_std"] = GW_distance_estimation( C1=C1, C2=C2, loss_fun=loss_fun, p=p, q=q, T=best_T, random_state=generator ) return best_T, log return best_T def sampled_gromov_wasserstein( C1, C2, p, q, loss_fun, nb_samples_grad=100, epsilon=1, max_iter=500, log=False, verbose=False, random_state=None, ): r""" Returns the gromov-wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})` using a 1-stochastic Frank-Wolfe. This method has a :math:`\mathcal{O}(\mathrm{max\_iter} \times N \log(N))` time complexity by relying on the 1D Optimal Transport solver. The function solves the following optimization problem: .. math:: \mathbf{GW} = \mathop{\arg \min}_\mathbf{T} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - `L`: loss function to account for the misfit between the similarity matrices Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,) Distribution in the source space q : array-like, shape (nt,) Distribution in the target space loss_fun : function: :math:`\mathbb{R} \times \mathbb{R} \mapsto \mathbb{R}` Loss function used for the distance, the transport plan does not depend on the loss function nb_samples_grad : int Number of samples to approximate the gradient epsilon : float Weight of the Kullback-Leibler regularization max_iter : int, optional Max number of iterations verbose : bool, optional Print information along iterations log : bool, optional Gives the distance estimated and the standard deviation random_state : int or RandomState instance, optional Fix the seed for reproducibility Returns ------- T : array-like, shape (`ns`, `nt`) Optimal coupling between the two spaces References ---------- .. [14] Kerdoncuff, Tanguy, Emonet, Rémi, Sebban, Marc "Sampled Gromov Wasserstein." Machine Learning Journal (MLJ). 2021. """ C1, C2, p, q = list_to_array(C1, C2, p, q) nx = get_backend(C1, C2, p, q) len_p = p.shape[0] len_q = q.shape[0] generator = check_random_state(random_state) # The most natural way to define nb_sample is with a simple integer. if isinstance(nb_samples_grad, int): if nb_samples_grad > len_p: # As the sampling along the first dimension is done without replacement, the rest is reported to the second # dimension. nb_samples_grad_p, nb_samples_grad_q = len_p, nb_samples_grad // len_p else: nb_samples_grad_p, nb_samples_grad_q = nb_samples_grad, 1 else: nb_samples_grad_p, nb_samples_grad_q = nb_samples_grad T = nx.outer(p, q) # continue_loop allows to stop the loop if there is several successive small modification of T. continue_loop = 0 # The gradient of GW is more complex if the two matrices are not symmetric. C_are_symmetric = nx.allclose(C1, C1.T, rtol=1e-10, atol=1e-10) and nx.allclose( C2, C2.T, rtol=1e-10, atol=1e-10 ) for cpt in range(max_iter): index0 = generator.choice( len_p, size=nb_samples_grad_p, p=nx.to_numpy(p), replace=False ) Lik = 0 for i, index0_i in enumerate(index0): T_row = ( nx.reshape(nx.todense(T[[index0_i], :]), (-1,)) if nx.issparse(T) else T[index0_i, :] ) index1 = generator.choice( len_q, size=nb_samples_grad_q, p=nx.to_numpy(T_row / nx.sum(T_row)), replace=False, ) # If the matrices C are not symmetric, the gradient has 2 terms, thus the term is chosen randomly. if (not C_are_symmetric) and generator.rand(1) > 0.5: Lik += nx.mean( loss_fun( C1[:, [index0[i]] * nb_samples_grad_q][:, None, :], C2[:, index1][None, :, :], ), axis=2, ) else: Lik += nx.mean( loss_fun( C1[[index0[i]] * nb_samples_grad_q, :][:, :, None], C2[index1, :][:, None, :], ), axis=0, ) max_Lik = nx.max(Lik) if max_Lik == 0: continue # This division by the max is here to facilitate the choice of epsilon. Lik /= max_Lik if epsilon > 0: # Set to infinity all the numbers below exp(-200) to avoid log of 0. log_T = nx.log(nx.clip(T, np.exp(-200), 1)) log_T = nx.where(log_T == -200, -np.inf, log_T) Lik = Lik - epsilon * log_T try: new_T = sinkhorn(a=p, b=q, M=Lik, reg=epsilon) except (RuntimeWarning, UserWarning): print("Warning caught in Sinkhorn: Return last stable T") break else: new_T = emd(a=p, b=q, M=Lik) change_T = nx.mean((T - new_T) ** 2) if change_T <= 10e-20: continue_loop += 1 if continue_loop > 100: # Number max of low modifications of T T = nx.copy(new_T) break else: continue_loop = 0 if verbose and cpt % 10 == 0: if cpt % 200 == 0: print( "{:5s}|{:12s}".format("It.", "||T_n - T_{n+1}||") + "\n" + "-" * 19 ) print("{:5d}|{:8e}|".format(cpt, change_T)) T = nx.copy(new_T) if log: log = {} log["gw_dist_estimated"], log["gw_dist_std"] = GW_distance_estimation( C1=C1, C2=C2, loss_fun=loss_fun, p=p, q=q, T=T, random_state=generator ) return T, log return T python-pot-0.9.7+dfsg/ot/gromov/_gw.py000066400000000000000000001440011525521106200176470ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Gromov-Wasserstein and Fused-Gromov-Wasserstein conditional gradient solvers. """ # Author: Erwan Vautier # Nicolas Courty # Rémi Flamary # Titouan Vayer # Cédric Vincent-Cuaz # # License: MIT License import numpy as np import warnings from ..utils import dist, UndefinedParameter, list_to_array from ..optim import cg, line_search_armijo, solve_1d_linesearch_quad from ..utils import check_random_state, unif from ..backend import get_backend, NumpyBackend from ._utils import init_matrix, gwloss, gwggrad from ._utils import update_barycenter_structure, update_barycenter_feature def gromov_wasserstein( C1, C2, p=None, q=None, loss_fun="square_loss", symmetric=None, log=False, armijo=False, G0=None, max_iter=1e4, tol_rel=1e-9, tol_abs=1e-9, **kwargs, ): r""" Returns the Gromov-Wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})`. The function solves the following optimization problem using Conditional Gradient: .. math:: \mathbf{T}^* \in \mathop{\arg \min}_\mathbf{T} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space. - :math:`\mathbf{C_2}`: Metric cost matrix in the target space. - :math:`\mathbf{p}`: Distribution in the source space. - :math:`\mathbf{q}`: Distribution in the target space. - `L`: Loss function to account for the misfit between the similarity matrices. .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. .. note:: All computations in the conditional gradient solver are done with numpy to limit memory overhead. .. note:: This function will cast the computed transport plan to the data type of the provided input :math:`\mathbf{C}_1`. Casting to an integer tensor might result in a loss of precision. If this behaviour is unwanted, please make sure to provide a floating point input. Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space. C2 : array-like, shape (nt, nt) Metric cost matrix in the target space. p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. loss_fun : str, optional Loss function used for the solver either 'square_loss' or 'kl_loss'. symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). verbose : bool, optional Print information along iterations. log : bool, optional Record log if True. armijo : bool, optional If True, the step of the line-search is found via an armijo search. Else closed form is used. If there are convergence issues, use False. G0: array-like, shape (ns,nt), optional If None, the initial transport plan of the solver is pq^T. Otherwise G0 must satisfy marginal constraints and will be used as initial transport of the solver. max_iter : int, optional Max number of iterations. tol_rel : float, optional Stop threshold on relative error (>0). tol_abs : float, optional Stop threshold on absolute error (>0). **kwargs : dict Parameters can be directly passed to the ot.optim.cg solver. Returns ------- T : array-like, shape (`ns`, `nt`) Coupling between the two spaces that minimizes: :math:`\sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l}` log : dict Convergence information and loss. References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. .. [13] Mémoli, Facundo. Gromov–Wasserstein distances and the metric approach to object matching. Foundations of computational mathematics 11.4 (2011): 417-487. .. [47] Chowdhury, S., & Mémoli, F. (2019). The gromov–wasserstein distance between networks and stable network invariants. Information and Inference: A Journal of the IMA, 8(4), 757-787. """ arr = [C1, C2] if p is not None: arr.append(list_to_array(p)) else: p = unif(C1.shape[0], type_as=C1) if q is not None: arr.append(list_to_array(q)) else: q = unif(C2.shape[0], type_as=C1) if G0 is not None: G0_ = G0 arr.append(G0) nx = get_backend(*arr) p0, q0, C10, C20 = p, q, C1, C2 p = nx.to_numpy(p0) q = nx.to_numpy(q0) C1 = nx.to_numpy(C10) C2 = nx.to_numpy(C20) if symmetric is None: symmetric = np.allclose(C1, C1.T, atol=1e-10) and np.allclose( C2, C2.T, atol=1e-10 ) if G0 is None: G0 = p[:, None] * q[None, :] else: G0 = nx.to_numpy(G0_) # Check marginals of G0 np.testing.assert_allclose(G0.sum(axis=1), p, atol=1e-08) np.testing.assert_allclose(G0.sum(axis=0), q, atol=1e-08) # cg for GW is implemented using numpy on CPU np_ = NumpyBackend() constC, hC1, hC2 = init_matrix(C1, C2, p, q, loss_fun, np_) def f(G): return gwloss(constC, hC1, hC2, G, np_) if symmetric: def df(G): return gwggrad(constC, hC1, hC2, G, np_) else: constCt, hC1t, hC2t = init_matrix(C1.T, C2.T, p, q, loss_fun, np_) def df(G): return 0.5 * ( gwggrad(constC, hC1, hC2, G, np_) + gwggrad(constCt, hC1t, hC2t, G, np_) ) if armijo: def line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs): return line_search_armijo(cost, G, deltaG, Mi, cost_G, nx=np_, **kwargs) else: def line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs): return solve_gromov_linesearch( G, deltaG, cost_G, hC1, hC2, M=0.0, reg=1.0, nx=np_, symmetric=symmetric, **kwargs, ) if not nx.is_floating_point(C10): warnings.warn( "Input structure matrix consists of integers. The transport plan will be " "casted accordingly, possibly resulting in a loss of precision. " "If this behaviour is unwanted, please make sure your input " "structure matrix consists of floating point elements.", stacklevel=2, ) if log: res, log = cg( p, q, 0.0, 1.0, f, df, G0, line_search, log=True, numItermax=max_iter, stopThr=tol_rel, stopThr2=tol_abs, **kwargs, ) log["gw_dist"] = nx.from_numpy(log["loss"][-1], type_as=C10) log["u"] = nx.from_numpy(log["u"], type_as=C10) log["v"] = nx.from_numpy(log["v"], type_as=C10) return nx.from_numpy(res, type_as=C10), log else: return nx.from_numpy( cg( p, q, 0.0, 1.0, f, df, G0, line_search, log=False, numItermax=max_iter, stopThr=tol_rel, stopThr2=tol_abs, **kwargs, ), type_as=C10, ) def gromov_wasserstein2( C1, C2, p=None, q=None, loss_fun="square_loss", symmetric=None, log=False, armijo=False, G0=None, max_iter=1e4, tol_rel=1e-9, tol_abs=1e-9, **kwargs, ): r""" Returns the Gromov-Wasserstein loss :math:`\mathbf{GW}` between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})`. To recover the Gromov-Wasserstein distance as defined in [13] compute :math:`d_{GW} = \frac{1}{2} \sqrt{\mathbf{GW}}`. The function solves the following optimization problem using Conditional Gradient: .. math:: \mathbf{GW} = \min_\mathbf{T} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - `L`: loss function to account for the misfit between the similarity matrices Note that when using backends, this loss function is differentiable wrt the matrices (C1, C2) and weights (p, q) for quadratic loss using the gradients from [38]_. .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. .. note:: All computations in the conditional gradient solver are done with numpy to limit memory overhead. .. note:: This function will cast the computed transport plan to the data type of the provided input :math:`\mathbf{C}_1`. Casting to an integer tensor might result in a loss of precision. If this behaviour is unwanted, please make sure to provide a floating point input. Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. loss_fun : str loss function used for the solver either 'square_loss' or 'kl_loss' symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). verbose : bool, optional Print information along iterations log : bool, optional record log if True armijo : bool, optional If True the step of the line-search is found via an armijo research. Else closed form is used. If there are convergence issues use False. G0: array-like, shape (ns,nt), optional If None the initial transport plan of the solver is pq^T. Otherwise G0 must satisfy marginal constraints and will be used as initial transport of the solver. max_iter : int, optional Max number of iterations tol_rel : float, optional Stop threshold on relative error (>0) tol_abs : float, optional Stop threshold on absolute error (>0) **kwargs : dict parameters can be directly passed to the ot.optim.cg solver Returns ------- gw_dist : float Gromov-Wasserstein distance log : dict convergence information and Coupling matrix References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. .. [13] Mémoli, Facundo. Gromov–Wasserstein distances and the metric approach to object matching. Foundations of computational mathematics 11.4 (2011): 417-487. .. [38] C. Vincent-Cuaz, T. Vayer, R. Flamary, M. Corneli, N. Courty, Online Graph Dictionary Learning, International Conference on Machine Learning (ICML), 2021. .. [47] Chowdhury, S., & Mémoli, F. (2019). The gromov–wasserstein distance between networks and stable network invariants. Information and Inference: A Journal of the IMA, 8(4), 757-787. """ # simple get_backend as the full one will be handled in gromov_wasserstein nx = get_backend(C1, C2) # init marginals if set as None if p is None: p = unif(C1.shape[0], type_as=C1) if q is None: q = unif(C2.shape[0], type_as=C1) T, log_gw = gromov_wasserstein( C1, C2, p, q, loss_fun, symmetric, log=True, armijo=armijo, G0=G0, max_iter=max_iter, tol_rel=tol_rel, tol_abs=tol_abs, **kwargs, ) log_gw["T"] = T gw = log_gw["gw_dist"] if loss_fun == "square_loss": gC1 = 2 * C1 * nx.outer(p, p) - 2 * nx.dot(T, nx.dot(C2, T.T)) gC2 = 2 * C2 * nx.outer(q, q) - 2 * nx.dot(T.T, nx.dot(C1, T)) elif loss_fun == "kl_loss": gC1 = nx.log(C1 + 1e-15) * nx.outer(p, p) - nx.dot( T, nx.dot(nx.log(C2 + 1e-15), T.T) ) gC2 = -nx.dot(T.T, nx.dot(C1, T)) / (C2 + 1e-15) + nx.outer(q, q) gw = nx.set_gradients( gw, (p, q, C1, C2), ( log_gw["u"] - nx.mean(log_gw["u"]), log_gw["v"] - nx.mean(log_gw["v"]), gC1, gC2, ), ) if log: return gw, log_gw else: return gw def fused_gromov_wasserstein( M, C1, C2, p=None, q=None, loss_fun="square_loss", symmetric=None, alpha=0.5, armijo=False, G0=None, log=False, max_iter=1e4, tol_rel=1e-9, tol_abs=1e-9, **kwargs, ): r""" Returns the Fused Gromov-Wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{Y_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{Y_2}, \mathbf{q})` with pairwise distance matrix :math:`\mathbf{M}` between node feature matrices :math:`\mathbf{Y_1}` and :math:`\mathbf{Y_2}` (see :ref:`[24] `). The function solves the following optimization problem using Conditional Gradient: .. math:: \mathbf{T}^* \in\mathop{\arg\min}_\mathbf{T} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{M}`: metric cost matrix between features across domains - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - `L`: loss function to account for the misfit between the similarity and feature matrices - :math:`\alpha`: trade-off parameter .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. .. note:: All computations in the conditional gradient solver are done with numpy to limit memory overhead. .. note:: This function will cast the computed transport plan to the data type of the provided input :math:`\mathbf{M}`. Casting to an integer tensor might result in a loss of precision. If this behaviour is unwanted, please make sure to provide a floating point input. Parameters ---------- M : array-like, shape (ns, nt) Metric cost matrix between features across domains C1 : array-like, shape (ns, ns) Metric cost matrix representative of the structure in the source space C2 : array-like, shape (nt, nt) Metric cost matrix representative of the structure in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. loss_fun : str, optional Loss function used for the solver symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). alpha : float, optional Trade-off parameter (0 < alpha < 1) armijo : bool, optional If True the step of the line-search is found via an armijo research. Else closed form is used. If there are convergence issues use False. G0: array-like, shape (ns,nt), optional If None the initial transport plan of the solver is pq^T. Otherwise G0 must satisfy marginal constraints and will be used as initial transport of the solver. log : bool, optional record log if True max_iter : int, optional Max number of iterations tol_rel : float, optional Stop threshold on relative error (>0) tol_abs : float, optional Stop threshold on absolute error (>0) **kwargs : dict parameters can be directly passed to the ot.optim.cg solver Returns ------- T : array-like, shape (`ns`, `nt`) Optimal transportation matrix for the given parameters. log : dict Log dictionary return only if log==True in parameters. .. _references-fused-gromov-wasserstein: References ---------- .. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs", International Conference on Machine Learning (ICML). 2019. .. [47] Chowdhury, S., & Mémoli, F. (2019). The gromov–wasserstein distance between networks and stable network invariants. Information and Inference: A Journal of the IMA, 8(4), 757-787. """ arr = [C1, C2, M] if p is not None: arr.append(list_to_array(p)) else: p = unif(C1.shape[0], type_as=M) if q is not None: arr.append(list_to_array(q)) else: q = unif(C2.shape[0], type_as=M) if G0 is not None: G0_ = G0 arr.append(G0) nx = get_backend(*arr) p0, q0, C10, C20, M0, alpha0 = p, q, C1, C2, M, alpha p = nx.to_numpy(p0) q = nx.to_numpy(q0) C1 = nx.to_numpy(C10) C2 = nx.to_numpy(C20) M = nx.to_numpy(M0) alpha = nx.to_numpy(alpha0) if symmetric is None: symmetric = np.allclose(C1, C1.T, atol=1e-10) and np.allclose( C2, C2.T, atol=1e-10 ) if G0 is None: G0 = p[:, None] * q[None, :] else: G0 = nx.to_numpy(G0_) # Check marginals of G0 np.testing.assert_allclose(G0.sum(axis=1), p, atol=1e-08) np.testing.assert_allclose(G0.sum(axis=0), q, atol=1e-08) # cg for GW is implemented using numpy on CPU np_ = NumpyBackend() constC, hC1, hC2 = init_matrix(C1, C2, p, q, loss_fun, np_) def f(G): return gwloss(constC, hC1, hC2, G, np_) if symmetric: def df(G): return gwggrad(constC, hC1, hC2, G, np_) else: constCt, hC1t, hC2t = init_matrix(C1.T, C2.T, p, q, loss_fun, np_) def df(G): return 0.5 * ( gwggrad(constC, hC1, hC2, G, np_) + gwggrad(constCt, hC1t, hC2t, G, np_) ) if armijo: def line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs): return line_search_armijo(cost, G, deltaG, Mi, cost_G, nx=np_, **kwargs) else: def line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs): return solve_gromov_linesearch( G, deltaG, cost_G, hC1, hC2, M=(1 - alpha) * M, reg=alpha, nx=np_, symmetric=symmetric, **kwargs, ) if not nx.is_floating_point(M0): warnings.warn( "Input feature matrix consists of integer. The transport plan will be " "casted accordingly, possibly resulting in a loss of precision. " "If this behaviour is unwanted, please make sure your input " "feature matrix consists of floating point elements.", stacklevel=2, ) if log: res, log = cg( p, q, (1 - alpha) * M, alpha, f, df, G0, line_search, log=True, numItermax=max_iter, stopThr=tol_rel, stopThr2=tol_abs, **kwargs, ) log["fgw_dist"] = nx.from_numpy(log["loss"][-1], type_as=M0) log["u"] = nx.from_numpy(log["u"], type_as=M0) log["v"] = nx.from_numpy(log["v"], type_as=M0) return nx.from_numpy(res, type_as=M0), log else: return nx.from_numpy( cg( p, q, (1 - alpha) * M, alpha, f, df, G0, line_search, log=False, numItermax=max_iter, stopThr=tol_rel, stopThr2=tol_abs, **kwargs, ), type_as=M0, ) def fused_gromov_wasserstein2( M, C1, C2, p=None, q=None, loss_fun="square_loss", symmetric=None, alpha=0.5, armijo=False, G0=None, log=False, max_iter=1e4, tol_rel=1e-9, tol_abs=1e-9, **kwargs, ): r""" Returns the Fused Gromov-Wasserstein distance between :math:`(\mathbf{C_1}, \mathbf{Y_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{Y_2}, \mathbf{q})` with pairwise distance matrix :math:`\mathbf{M}` between node feature matrices :math:`\mathbf{Y_1}` and :math:`\mathbf{Y_2}` (see :ref:`[24] `). The function solves the following optimization problem using Conditional Gradient: .. math:: \mathbf{FGW} = \mathop{\min}_\mathbf{T} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{M}`: metric cost matrix between features across domains - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - `L`: loss function to account for the misfit between the similarity and feature matrices - :math:`\alpha`: trade-off parameter Note that when using backends, this loss function is differentiable wrt the matrices (C1, C2, M) and weights (p, q) for quadratic loss using the gradients from [38]_. .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. .. note:: All computations in the conditional gradient solver are done with numpy to limit memory overhead. .. note:: This function will cast the computed transport plan to the data type of the provided input :math:`\mathbf{M}`. Casting to an integer tensor might result in a loss of precision. If this behaviour is unwanted, please make sure to provide a floating point input. Parameters ---------- M : array-like, shape (ns, nt) Metric cost matrix between features across domains C1 : array-like, shape (ns, ns) Metric cost matrix representative of the structure in the source space. C2 : array-like, shape (nt, nt) Metric cost matrix representative of the structure in the target space. p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. loss_fun : str, optional Loss function used for the solver. symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). alpha : float, optional Trade-off parameter (0 < alpha < 1) armijo : bool, optional If True the step of the line-search is found via an armijo research. Else closed form is used. If there are convergence issues use False. G0: array-like, shape (ns,nt), optional If None the initial transport plan of the solver is pq^T. Otherwise G0 must satisfy marginal constraints and will be used as initial transport of the solver. log : bool, optional Record log if True. max_iter : int, optional Max number of iterations tol_rel : float, optional Stop threshold on relative error (>0) tol_abs : float, optional Stop threshold on absolute error (>0) **kwargs : dict Parameters can be directly passed to the ot.optim.cg solver. Returns ------- fgw-distance : float Fused Gromov-Wasserstein distance for the given parameters. log : dict Log dictionary return only if log==True in parameters. .. _references-fused-gromov-wasserstein2: References ---------- .. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs" International Conference on Machine Learning (ICML). 2019. .. [38] C. Vincent-Cuaz, T. Vayer, R. Flamary, M. Corneli, N. Courty, Online Graph Dictionary Learning, International Conference on Machine Learning (ICML), 2021. .. [47] Chowdhury, S., & Mémoli, F. (2019). The gromov–wasserstein distance between networks and stable network invariants. Information and Inference: A Journal of the IMA, 8(4), 757-787. """ nx = get_backend(C1, C2, M) # init marginals if set as None if p is None: p = unif(C1.shape[0], type_as=M) if q is None: q = unif(C2.shape[0], type_as=M) T, log_fgw = fused_gromov_wasserstein( M, C1, C2, p, q, loss_fun, symmetric, alpha, armijo, G0, log=True, max_iter=max_iter, tol_rel=tol_rel, tol_abs=tol_abs, **kwargs, ) fgw_dist = log_fgw["fgw_dist"] log_fgw["T"] = T # compute separate terms for gradients and log lin_term = nx.sum(T * M) log_fgw["quad_loss"] = fgw_dist - (1 - alpha) * lin_term log_fgw["lin_loss"] = lin_term * (1 - alpha) gw_term = log_fgw["quad_loss"] / alpha if loss_fun == "square_loss": gC1 = 2 * C1 * nx.outer(p, p) - 2 * nx.dot(T, nx.dot(C2, T.T)) gC2 = 2 * C2 * nx.outer(q, q) - 2 * nx.dot(T.T, nx.dot(C1, T)) elif loss_fun == "kl_loss": gC1 = nx.log(C1 + 1e-15) * nx.outer(p, p) - nx.dot( T, nx.dot(nx.log(C2 + 1e-15), T.T) ) gC2 = -nx.dot(T.T, nx.dot(C1, T)) / (C2 + 1e-15) + nx.outer(q, q) if isinstance(alpha, int) or isinstance(alpha, float): fgw_dist = nx.set_gradients( fgw_dist, (p, q, C1, C2, M), ( log_fgw["u"] - nx.mean(log_fgw["u"]), log_fgw["v"] - nx.mean(log_fgw["v"]), alpha * gC1, alpha * gC2, (1 - alpha) * T, ), ) else: fgw_dist = nx.set_gradients( fgw_dist, (p, q, C1, C2, M, alpha), ( log_fgw["u"] - nx.mean(log_fgw["u"]), log_fgw["v"] - nx.mean(log_fgw["v"]), alpha * gC1, alpha * gC2, (1 - alpha) * T, gw_term - lin_term, ), ) if log: return fgw_dist, log_fgw else: return fgw_dist def solve_gromov_linesearch( G, deltaG, cost_G, C1, C2, M, reg, alpha_min=None, alpha_max=None, nx=None, symmetric=False, **kwargs, ): """ Solve the linesearch in the FW iterations for any inner loss that decomposes as in Proposition 1 in :ref:`[12] `. Parameters ---------- G : array-like, shape(ns,nt) The transport map at a given iteration of the FW deltaG : array-like (ns,nt) Difference between the optimal map found by linearization in the FW algorithm and the value at a given iteration cost_G : float Value of the cost at `G` C1 : array-like (ns,ns), optional Transformed Structure matrix in the source domain. For the 'square_loss' and 'kl_loss', we provide hC1 from ot.gromov.init_matrix C2 : array-like (nt,nt), optional Transformed Structure matrix in the source domain. For the 'square_loss' and 'kl_loss', we provide hC2 from ot.gromov.init_matrix M : array-like (ns,nt) Cost matrix between the features. reg : float Regularization parameter. alpha_min : float, optional Minimum value for alpha alpha_max : float, optional Maximum value for alpha nx : backend, optional If let to its default value None, a backend test will be conducted. symmetric : bool, optional Either structures are to be assumed symmetric or not. Default value is False. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). Returns ------- alpha : float The optimal step size of the FW fc : int nb of function call. Useless here cost_G : float The value of the cost for the next iteration .. _references-solve-linesearch: References ---------- .. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs" International Conference on Machine Learning (ICML). 2019. .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. """ if nx is None: if isinstance(M, int) or isinstance(M, float): nx = get_backend(G, deltaG, C1, C2) else: nx = get_backend(G, deltaG, C1, C2, M) dot = nx.dot(nx.dot(C1, deltaG), C2.T) a = -reg * nx.sum(dot * deltaG) if symmetric: b = nx.sum(M * deltaG) - 2 * reg * nx.sum(dot * G) else: b = nx.sum(M * deltaG) - reg * ( nx.sum(dot * G) + nx.sum(nx.dot(nx.dot(C1, G), C2.T) * deltaG) ) alpha = solve_1d_linesearch_quad(a, b) if alpha_min is not None or alpha_max is not None: alpha = np.clip(alpha, alpha_min, alpha_max) # the new cost is deduced from the line search quadratic function cost_G = cost_G + a * (alpha**2) + b * alpha return alpha, 1, cost_G def gromov_barycenters( N, Cs, ps=None, p=None, lambdas=None, loss_fun="square_loss", symmetric=True, armijo=False, max_iter=1000, tol=1e-9, stop_criterion="barycenter", warmstartT=False, verbose=False, log=False, init_C=None, random_state=None, **kwargs, ): r""" Returns the Gromov-Wasserstein barycenters of `S` measured similarity matrices :math:`(\mathbf{C}_s)_{1 \leq s \leq S}` The function solves the following optimization problem with block coordinate descent: .. math:: \mathbf{C}^* = \mathop{\arg \min}_{\mathbf{C}\in \mathbb{R}^{N \times N}} \quad \sum_s \lambda_s \mathrm{GW}(\mathbf{C}, \mathbf{C}_s, \mathbf{p}, \mathbf{p}_s) Where : - :math:`\mathbf{C}_s`: metric cost matrix - :math:`\mathbf{p}_s`: distribution Parameters ---------- N : int Size of the targeted barycenter Cs : list of S array-like of shape (ns, ns) Metric cost matrices ps : list of S array-like of shape (ns,), optional Sample weights in the `S` spaces. If let to its default value None, uniform distributions are taken. p : array-like, shape (N,), optional Weights in the targeted barycenter. If let to its default value None, uniform distribution is taken. lambdas : list of float, optional List of the `S` spaces' weights. If let to its default value None, uniform weights are taken. loss_fun : callable, optional tensor-matrix multiplication function based on specific loss function symmetric : bool, optional. Either structures are to be assumed symmetric or not. Default value is True. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). armijo : bool, optional If True the step of the line-search is found via an armijo research. Else closed form is used. If there are convergence issues use False. max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on relative error (>0) stop_criterion : str, optional. Default is 'barycenter'. Stop criterion taking values in ['barycenter', 'loss']. If set to 'barycenter' uses absolute norm variations of estimated barycenters. Else if set to 'loss' uses the relative variations of the loss. warmstartT: bool, optional Either to perform warmstart of transport plans in the successive fused gromov-wasserstein transport problems.s verbose : bool, optional Print information along iterations. log : bool, optional Record log if True. init_C : bool | array-like, shape(N,N) Random initial value for the :math:`\mathbf{C}` matrix provided by user. random_state : int or RandomState instance, optional Fix the seed for reproducibility Returns ------- C : array-like, shape (`N`, `N`) Similarity matrix in the barycenter space (permutated arbitrarily) log : dict Only returned when log=True. It contains the keys: - :math:`\mathbf{T}`: list of (`N`, `ns`) transport matrices - :math:`\mathbf{p}`: (`N`,) barycenter weights - values used in convergence evaluation. References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. """ if stop_criterion not in ["barycenter", "loss"]: raise ValueError( f"Unknown `stop_criterion='{stop_criterion}'`. Use one of: {'barycenter', 'loss'}." ) if isinstance(Cs[0], list): raise ValueError( "Deprecated feature in POT 0.9.4: structures Cs[i] are lists and should be arrays from a supported backend (e.g numpy)." ) arr = [*Cs] if ps is not None: if isinstance(ps[0], list): raise ValueError( "Deprecated feature in POT 0.9.4: weights ps[i] are lists and should be arrays from a supported backend (e.g numpy)." ) arr += [*ps] else: ps = [unif(C.shape[0], type_as=C) for C in Cs] if p is not None: arr.append(list_to_array(p)) else: p = unif(N, type_as=Cs[0]) nx = get_backend(*arr) S = len(Cs) if lambdas is None: lambdas = [1.0 / S] * S # Initialization of C : random SPD matrix (if not provided by user) if init_C is None: rng = check_random_state(random_state) xalea = rng.randn(N, 2) C = dist(xalea, xalea) C /= C.max() C = nx.from_numpy(C, type_as=p) else: C = init_C if warmstartT: T = [None] * S if stop_criterion == "barycenter": inner_log = False else: inner_log = True curr_loss = 1e15 if log: log_ = {} log_["err"] = [] if stop_criterion == "loss": log_["loss"] = [] for cpt in range(max_iter): if stop_criterion == "barycenter": Cprev = C else: prev_loss = curr_loss # get transport plans if warmstartT: res = [ gromov_wasserstein( C, Cs[s], p, ps[s], loss_fun, symmetric=symmetric, armijo=armijo, G0=T[s], max_iter=max_iter, tol_rel=1e-5, tol_abs=0.0, log=inner_log, verbose=verbose, **kwargs, ) for s in range(S) ] else: res = [ gromov_wasserstein( C, Cs[s], p, ps[s], loss_fun, symmetric=symmetric, armijo=armijo, G0=None, max_iter=max_iter, tol_rel=1e-5, tol_abs=0.0, log=inner_log, verbose=verbose, **kwargs, ) for s in range(S) ] if stop_criterion == "barycenter": T = res else: T = [output[0] for output in res] curr_loss = np.sum([output[1]["gw_dist"] for output in res]) # update barycenters C = update_barycenter_structure( T, Cs, lambdas, p, loss_fun, target=False, check_zeros=False, nx=nx ) # update convergence criterion if stop_criterion == "barycenter": err = nx.norm(C - Cprev) if log: log_["err"].append(err) else: err = abs(curr_loss - prev_loss) / prev_loss if prev_loss != 0.0 else np.nan if log: log_["loss"].append(curr_loss) log_["err"].append(err) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err)) if err <= tol: break if log: log_["T"] = T log_["p"] = p return C, log_ else: return C def fgw_barycenters( N, Ys, Cs, ps=None, lambdas=None, alpha=0.5, fixed_structure=False, fixed_features=False, p=None, loss_fun="square_loss", armijo=False, symmetric=True, max_iter=100, tol=1e-9, stop_criterion="barycenter", warmstartT=False, verbose=False, log=False, init_C=None, init_X=None, random_state=None, **kwargs, ): r""" Returns the Fused Gromov-Wasserstein barycenters of `S` measurable networks with node features :math:`(\mathbf{C}_s, \mathbf{Y}_s, \mathbf{p}_s)_{1 \leq s \leq S}` (see eq (5) in :ref:`[24] `), estimated using Fused Gromov-Wasserstein transports from Conditional Gradient solvers. The function solves the following optimization problem: .. math:: \mathbf{C}^*, \mathbf{Y}^* = \mathop{\arg \min}_{\mathbf{C}\in \mathbb{R}^{N \times N}, \mathbf{Y}\in \mathbb{Y}^{N \times d}} \quad \sum_s \lambda_s \mathrm{FGW}_{\alpha}(\mathbf{C}, \mathbf{C}_s, \mathbf{Y}, \mathbf{Y}_s, \mathbf{p}, \mathbf{p}_s) Where : - :math:`\mathbf{Y}_s`: feature matrix - :math:`\mathbf{C}_s`: metric cost matrix - :math:`\mathbf{p}_s`: distribution Parameters ---------- N : int Desired number of samples of the target barycenter Ys: list of array-like, each element has shape (ns,d) Features of all samples Cs : list of array-like, each element has shape (ns,ns) Structure matrices of all samples ps : list of array-like, each element has shape (ns,), optional Masses of all samples. If let to its default value None, uniform distributions are taken. lambdas : list of float, optional List of the `S` spaces' weights. If let to its default value None, uniform weights are taken. alpha : float, optional Alpha parameter for the fgw distance. fixed_structure : bool, optional Whether to fix the structure of the barycenter during the updates. fixed_features : bool, optional Whether to fix the feature of the barycenter during the updates p : array-like, shape (N,), optional Weights in the targeted barycenter. If let to its default value None, uniform distribution is taken. loss_fun : str, optional Loss function used for the solver either 'square_loss' or 'kl_loss' armijo : bool, optional If True the step of the line-search is found via an armijo research. Else closed form is used. If there are convergence issues use False. symmetric : bool, optional Either structures are to be assumed symmetric or not. Default value is True. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on relative error (>0) stop_criterion : str, optional. Default is 'barycenter'. Stop criterion taking values in ['barycenter', 'loss']. If set to 'barycenter' uses absolute norm variations of estimated barycenters. Else if set to 'loss' uses the relative variations of the loss. warmstartT: bool, optional Either to perform warmstart of transport plans in the successive fused gromov-wasserstein transport problems. verbose : bool, optional Print information along iterations. log : bool, optional Record log if True. init_C : array-like, shape (N,N), optional Initialization for the barycenters' structure matrix. If not set a random init is used. init_X : array-like, shape (N,d), optional Initialization for the barycenters' features. If not set a random init is used. random_state : int or RandomState instance, optional Fix the seed for reproducibility Returns ------- X : array-like, shape (`N`, `d`) Barycenters' features C : array-like, shape (`N`, `N`) Barycenters' structure matrix log : dict Only returned when log=True. It contains the keys: - :math:`\mathbf{T}`: list of (`N`, `ns`) transport matrices - :math:`\mathbf{p}`: (`N`,) barycenter weights - :math:`(\mathbf{M}_s)_s`: all distance matrices between the feature of the barycenter and the other features :math:`(dist(\mathbf{X}, \mathbf{Y}_s))_s` shape (`N`, `ns`) - values used in convergence evaluation. .. _references-fgw-barycenters: References ---------- .. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs" International Conference on Machine Learning (ICML). 2019. """ if stop_criterion not in ["barycenter", "loss"]: raise ValueError( f"Unknown `stop_criterion='{stop_criterion}'`. Use one of: {'barycenter', 'loss'}." ) if isinstance(Cs[0], list) or isinstance(Ys[0], list): raise ValueError( "Deprecated feature in POT 0.9.4: structures Cs[i] and/or features Ys[i] are lists and should be arrays from a supported backend (e.g numpy)." ) arr = [*Cs, *Ys] if ps is not None: if isinstance(ps[0], list): raise ValueError( "Deprecated feature in POT 0.9.4: weights ps[i] are lists and should be arrays from a supported backend (e.g numpy)." ) arr += [*ps] else: ps = [unif(C.shape[0], type_as=C) for C in Cs] if p is not None: arr.append(list_to_array(p)) else: p = unif(N, type_as=Cs[0]) nx = get_backend(*arr) S = len(Cs) if lambdas is None: lambdas = [1.0 / S] * S d = Ys[0].shape[1] # dimension on the node features if fixed_structure: if init_C is None: raise UndefinedParameter("If C is fixed it must be provided in init_C") else: C = init_C else: if init_C is None: rng = check_random_state(random_state) xalea = rng.randn(N, 2) C = dist(xalea, xalea) C = nx.from_numpy(C, type_as=ps[0]) else: C = init_C if fixed_features: if init_X is None: raise UndefinedParameter("If X is fixed it must be provided in init_X") else: X = init_X else: if init_X is None: X = nx.zeros((N, d), type_as=ps[0]) else: X = init_X Ms = [dist(X, Ys[s]) for s in range(len(Ys))] if warmstartT: T = [None] * S if stop_criterion == "barycenter": inner_log = False else: inner_log = True curr_loss = 1e15 if log: log_ = {} if stop_criterion == "barycenter": log_["err_feature"] = [] log_["err_structure"] = [] log_["Ts_iter"] = [] else: log_["loss"] = [] log_["err_rel_loss"] = [] for cpt in range(max_iter): # break if specified errors are below tol. if stop_criterion == "barycenter": Cprev = C Xprev = X else: prev_loss = curr_loss # get transport plans if warmstartT: res = [ fused_gromov_wasserstein( Ms[s], C, Cs[s], p, ps[s], loss_fun=loss_fun, alpha=alpha, armijo=armijo, symmetric=symmetric, G0=T[s], max_iter=max_iter, tol_rel=1e-5, tol_abs=0.0, log=inner_log, verbose=verbose, **kwargs, ) for s in range(S) ] else: res = [ fused_gromov_wasserstein( Ms[s], C, Cs[s], p, ps[s], loss_fun=loss_fun, alpha=alpha, armijo=armijo, symmetric=symmetric, G0=None, max_iter=max_iter, tol_rel=1e-5, tol_abs=0.0, log=inner_log, verbose=verbose, **kwargs, ) for s in range(S) ] if stop_criterion == "barycenter": T = res else: T = [output[0] for output in res] curr_loss = np.sum([output[1]["fgw_dist"] for output in res]) # update barycenters if not fixed_features: X = update_barycenter_feature( T, Ys, lambdas, p, target=False, check_zeros=False, nx=nx ) Ms = [dist(X, Ys[s]) for s in range(len(Ys))] if not fixed_structure: C = update_barycenter_structure( T, Cs, lambdas, p, loss_fun, target=False, check_zeros=False, nx=nx ) # update convergence criterion if stop_criterion == "barycenter": err_feature, err_structure = 0.0, 0.0 if not fixed_features: err_feature = nx.norm(X - Xprev) if not fixed_structure: err_structure = nx.norm(C - Cprev) if log: log_["err_feature"].append(err_feature) log_["err_structure"].append(err_structure) log_["Ts_iter"].append(T) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err_structure)) print("{:5d}|{:8e}|".format(cpt, err_feature)) if (err_feature <= tol) or (err_structure <= tol): break else: err_rel_loss = ( abs(curr_loss - prev_loss) / prev_loss if prev_loss != 0.0 else np.nan ) if log: log_["loss"].append(curr_loss) log_["err_rel_loss"].append(err_rel_loss) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err_rel_loss)) if err_rel_loss <= tol: break if log: log_["T"] = T log_["p"] = p log_["Ms"] = Ms return X, C, log_ else: return X, C python-pot-0.9.7+dfsg/ot/gromov/_lowrank.py000066400000000000000000000271241525521106200207150ustar00rootroot00000000000000""" Low rank Gromov-Wasserstein solver """ # Author: Laurène David # # License: MIT License import warnings from ..utils import unif, get_lowrank_lazytensor from ..backend import get_backend from ..lowrank import compute_lr_sqeuclidean_matrix, _init_lr_sinkhorn, _LR_Dysktra def _flat_product_operator(X, nx=None): r""" Implementation of the flattened out-product operator. This function is used in low rank gromov wasserstein to compute the low rank decomposition of a cost matrix's squared hadamard product (page 6 in paper). Parameters ---------- X: array-like, shape (n_samples, n_col) Input matrix for operator nx: default None POT backend Returns ---------- X_flat: array-like, shape (n_samples, n_col**2) Matrix with flattened out-product operator applied on each row References ---------- .. [67] Scetbon, M., Peyré, G. & Cuturi, M. (2022). "Linear-Time GromovWasserstein Distances using Low Rank Couplings and Costs". In International Conference on Machine Learning (ICML), 2022. """ if nx is None: nx = get_backend(X) n = X.shape[0] x1 = X[0, :][:, None] X_flat = nx.dot(x1, x1.T).flatten()[:, None] for i in range(1, n): x = X[i, :][:, None] x_out = nx.dot(x, x.T).flatten()[:, None] X_flat = nx.concatenate((X_flat, x_out), axis=1) X_flat = X_flat.T return X_flat def lowrank_gromov_wasserstein_samples( X_s, X_t, a=None, b=None, reg=0, rank=None, alpha=1e-10, gamma_init="rescale", rescale_cost=True, cost_factorized_Xs=None, cost_factorized_Xt=None, stopThr=1e-4, numItermax=1000, stopThr_dykstra=1e-3, numItermax_dykstra=10000, seed_init=49, warn=True, warn_dykstra=False, log=False, ): r""" Solve the entropic regularization Gromov-Wasserstein transport problem under low-nonnegative rank constraints on the couplings and cost matrices. Squared euclidean distance matrices are considered for the target and source distributions. The function solves the following optimization problem: .. math:: \mathop{\min_{(Q,R,g) \in \mathcal{C(a,b,r)}}} \mathcal{Q}_{A,B}(Q\mathrm{diag}(1/g)R^T) - \epsilon \cdot H((Q,R,g)) where : - :math:`A` is the (`dim_a`, `dim_a`) square pairwise cost matrix of the source domain. - :math:`B` is the (`dim_b`, `dim_b`) square pairwise cost matrix of the target domain. - :math:`\mathcal{Q}_{A,B}` is quadratic objective function of the Gromov Wasserstein plan. - :math:`Q` and `R` are the low-rank matrix decomposition of the Gromov-Wasserstein plan. - :math:`g` is the weight vector for the low-rank decomposition of the Gromov-Wasserstein plan. - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (histograms, both sum to 1). - :math:`r` is the rank of the Gromov-Wasserstein plan. - :math:`\mathcal{C(a,b,r)}` are the low-rank couplings of the OT problem. - :math:`H((Q,R,g))` is the values of the three respective entropies evaluated for each term. Parameters ---------- X_s : array-like, shape (n_samples_a, dim_Xs) Samples in the source domain X_t : array-like, shape (n_samples_b, dim_Xt) Samples in the target domain a : array-like, shape (n_samples_a,), optional Samples weights in the source domain If let to its default value None, uniform distribution is taken. b : array-like, shape (n_samples_b,), optional Samples weights in the target domain If let to its default value None, uniform distribution is taken. reg : float, optional Regularization term >=0 rank : int, optional. Default is None. (>0) Nonnegative rank of the OT plan. If None, min(ns, nt) is considered. alpha : float, optional. Default is 1e-10. (>0 and <1/r) Lower bound for the weight vector g. rescale_cost : bool, optional. Default is True Rescale the low rank factorization of the sqeuclidean cost matrix seed_init : int, optional. Default is 49. (>0) Random state for the 'random' initialization of low rank couplings gamma_init : str, optional. Default is "rescale". Initialization strategy for gamma. 'rescale', or 'theory' Gamma is a constant that scales the convergence criterion of the Mirror Descent optimization scheme used to compute the low-rank couplings (Q, R and g) numItermax : int, optional. Default is 1000. Max number of iterations for Low Rank GW stopThr : float, optional. Default is 1e-4. Stop threshold on error (>0) for Low Rank GW The error is the sum of Kullback Divergences computed for each low rank coupling (Q, R and g) and scaled using gamma. numItermax_dykstra : int, optional. Default is 10000. Max number of iterations for the Dykstra algorithm stopThr_dykstra : float, optional. Default is 1e-3. Stop threshold on error (>0) in Dykstra cost_factorized_Xs: tuple, optional. Default is None Tuple with two pre-computed low rank decompositions (A1, A2) of the source cost matrix. Both matrices should have a shape of (n_samples_a, dim_Xs + 2). If None, the low rank cost matrices will be computed as sqeuclidean cost matrices. cost_factorized_Xt: tuple, optional. Default is None Tuple with two pre-computed low rank decompositions (B1, B2) of the target cost matrix. Both matrices should have a shape of (n_samples_b, dim_Xt + 2). If None, the low rank cost matrices will be computed as sqeuclidean cost matrices. warn : bool, optional if True, raises a warning if the low rank GW algorithm doesn't convergence. warn_dykstra: bool, optional if True, raises a warning if the Dykstra algorithm doesn't convergence. log : bool, optional record log if True Returns --------- Q : array-like, shape (n_samples_a, r) First low-rank matrix decomposition of the OT plan R: array-like, shape (n_samples_b, r) Second low-rank matrix decomposition of the OT plan g : array-like, shape (r, ) Weight vector for the low-rank decomposition of the OT log : dict (lazy_plan, value and value_linear) log dictionary return only if log==True in parameters References ---------- .. [67] Scetbon, M., Peyré, G. & Cuturi, M. (2022). "Linear-Time GromovWasserstein Distances using Low Rank Couplings and Costs". In International Conference on Machine Learning (ICML), 2022. """ # POT backend nx = get_backend(X_s, X_t) ns, nt = X_s.shape[0], X_t.shape[0] # Initialize weights a, b if a is None: a = unif(ns, type_as=X_s) if b is None: b = unif(nt, type_as=X_t) # Compute rank (see Section 3.1, def 1) r = rank if rank is None: r = min(ns, nt) else: r = min(ns, nt, rank) if r <= 0: raise ValueError("The rank parameter cannot have a negative value") # Dykstra won't converge if 1/rank < alpha (see Section 3.2) if 1 / r < alpha: raise ValueError( "alpha ({a}) should be smaller than 1/rank ({r}) for the Dykstra algorithm to converge.".format( a=alpha, r=1 / rank ) ) if cost_factorized_Xs is not None: A1, A2 = cost_factorized_Xs else: A1, A2 = compute_lr_sqeuclidean_matrix(X_s, X_s, rescale_cost, nx=nx) if cost_factorized_Xt is not None: B1, B2 = cost_factorized_Xt else: B1, B2 = compute_lr_sqeuclidean_matrix(X_t, X_t, rescale_cost, nx=nx) # Initial values for LR couplings (Q, R, g) with LOT Q, R, g = _init_lr_sinkhorn( X_s, X_t, a, b, r, init="random", random_state=seed_init, reg_init=None, nx=nx ) # Gamma initialization if gamma_init == "theory": L = (27 * nx.norm(A1) * nx.norm(A2)) / alpha**4 gamma = 1 / (2 * L) if gamma_init not in ["rescale", "theory"]: raise ( NotImplementedError('Not implemented gamma_init="{}"'.format(gamma_init)) ) # initial value of error err = 1 for ii in range(numItermax): Q_prev = Q R_prev = R g_prev = g if err > stopThr: # Compute cost matrices C1 = nx.dot(A2.T, Q * (1 / g)[None, :]) C1 = -4 * nx.dot(A1, C1) C2 = nx.dot(R.T, B1) C2 = nx.dot(C2, B2.T) diag_g = (1 / g)[None, :] # Compute C*R dot using the lr decomposition of C CR = nx.dot(C2, R) CR = nx.dot(C1, CR) CR_g = CR * diag_g # Compute C.T * Q using the lr decomposition of C CQ = nx.dot(C1.T, Q) CQ = nx.dot(C2.T, CQ) CQ_g = CQ * diag_g # Compute omega omega = nx.diag(nx.dot(Q.T, CR)) # Rescale gamma at each iteration if gamma_init == "rescale": norm_1 = nx.max(nx.abs(CR_g + reg * nx.log(Q))) ** 2 norm_2 = nx.max(nx.abs(CQ_g + reg * nx.log(R))) ** 2 norm_3 = nx.max(nx.abs(-omega * (diag_g**2))) ** 2 gamma = 10 / max(norm_1, norm_2, norm_3) K1 = nx.exp(-gamma * CR_g - ((gamma * reg) - 1) * nx.log(Q)) K2 = nx.exp(-gamma * CQ_g - ((gamma * reg) - 1) * nx.log(R)) K3 = nx.exp((gamma * omega / (g**2)) - (gamma * reg - 1) * nx.log(g)) # Update couplings with LR Dykstra algorithm Q, R, g = _LR_Dysktra( K1, K2, K3, a, b, alpha, stopThr_dykstra, numItermax_dykstra, warn_dykstra, nx, ) # Update error with kullback-divergence err_1 = ((1 / gamma) ** 2) * (nx.kl_div(Q, Q_prev) + nx.kl_div(Q_prev, Q)) err_2 = ((1 / gamma) ** 2) * (nx.kl_div(R, R_prev) + nx.kl_div(R_prev, R)) err_3 = ((1 / gamma) ** 2) * (nx.kl_div(g, g_prev) + nx.kl_div(g_prev, g)) err = err_1 + err_2 + err_3 # fix divide by zero Q = Q + 1e-16 R = R + 1e-16 g = g + 1e-16 else: break else: if warn: warnings.warn( "Low Rank GW did not converge. You might want to " "increase the number of iterations `numItermax` " ) # Update low rank costs C1 = nx.dot(A2.T, Q * (1 / g)[None, :]) C1 = -4 * nx.dot(A1, C1) C2 = nx.dot(R.T, B1) C2 = nx.dot(C2, B2.T) # Compute lazy plan (using LazyTensor class) lazy_plan = get_lowrank_lazytensor(Q, R, 1 / g) # Compute value_quad A1_, A2_ = _flat_product_operator(A1, nx), _flat_product_operator(A2, nx) B1_, B2_ = _flat_product_operator(B1, nx), _flat_product_operator(B2, nx) x_ = nx.dot(A1_, nx.dot(A2_.T, a)) y_ = nx.dot(B1_, nx.dot(B2_.T, b)) c1 = nx.dot(x_, a) + nx.dot(y_, b) G = nx.dot(C1, nx.dot(C2, R)) G = nx.dot(Q.T, G * diag_g) value_quad = c1 + nx.trace(G) / 2 if reg != 0: reg_Q = nx.sum(Q * nx.log(Q + 1e-16)) # entropy for Q reg_g = nx.sum(g * nx.log(g + 1e-16)) # entropy for g reg_R = nx.sum(R * nx.log(R + 1e-16)) # entropy for R value = value_quad + reg * (reg_Q + reg_g + reg_R) else: value = value_quad if log: dict_log = dict() dict_log["value"] = value dict_log["value_quad"] = value_quad dict_log["lazy_plan"] = lazy_plan return Q, R, g, dict_log return Q, R, g python-pot-0.9.7+dfsg/ot/gromov/_partial.py000066400000000000000000001643471525521106200207050ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Partial (Fused) Gromov-Wasserstein solvers. """ # Author: Laetitia Chapel # Cédric Vincent-Cuaz # Yikun Bai < yikun.bai@vanderbilt.edu > # # License: MIT License from ..utils import list_to_array, unif from ..backend import get_backend, NumpyBackend from ..partial import entropic_partial_wasserstein from ._utils import _transform_matrix, gwloss, gwggrad from ..optim import partial_cg, solve_1d_linesearch_quad import numpy as np import warnings def partial_gromov_wasserstein( C1, C2, p=None, q=None, m=None, loss_fun="square_loss", nb_dummies=1, G0=None, thres=1, numItermax=1e4, tol=1e-8, symmetric=None, warn=True, log=False, verbose=False, **kwargs, ): r""" Returns the Partial Gromov-Wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})`. The function solves the following optimization problem using Conditional Gradient: .. math:: \mathbf{T}^* \in \mathop{\arg \min}_\mathbf{T} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) T_{i,j} T_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 \mathbf{1}^T \mathbf{T}^T \mathbf{1} = m &\leq \min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\} where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space. - :math:`\mathbf{C_2}`: Metric cost matrix in the target space. - :math:`\mathbf{p}`: Distribution in the source space. - :math:`\mathbf{q}`: Distribution in the target space. - `m` is the amount of mass to be transported - `L`: Loss function to account for the misfit between the similarity matrices. The formulation of the problem has been proposed in :ref:`[29] ` .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. .. note:: All computations in the conjugate gradient solver are done with numpy to limit memory overhead. .. note:: This function will cast the computed transport plan to the data type of the provided input :math:`\mathbf{C}_1`. Casting to an integer tensor might result in a loss of precision. If this behaviour is unwanted, please make sure to provide a floating point input. Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric costfr matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. m : float, optional Amount of mass to be transported (default: :math:`\min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\}`) loss_fun : str, optional Loss function used for the solver either 'square_loss' or 'kl_loss'. nb_dummies : int, optional Number of dummy points to add (avoid instabilities in the EMD solver) G0 : array-like, shape (ns, nt), optional Initialization of the transportation matrix thres : float, optional quantile of the gradient matrix to populate the cost matrix when 0 (default: 1) numItermax : int, optional Max number of iterations tol : float, optional tolerance for stopping iterations symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). warn: bool, optional. Whether to raise a warning when EMD did not converge. log : bool, optional return log if True verbose : bool, optional Print information along iterations **kwargs : dict parameters can be directly passed to the emd solver Returns ------- T : array-like, shape (`ns`, `nt`) Optimal transport matrix between the two spaces. log : dict Convergence information and loss. Examples -------- >>> from ot.gromov import partial_gromov_wasserstein >>> import scipy as sp >>> a = np.array([0.25] * 4) >>> b = np.array([0.25] * 4) >>> x = np.array([1,2,100,200]).reshape((-1,1)) >>> y = np.array([3,2,98,199]).reshape((-1,1)) >>> C1 = sp.spatial.distance.cdist(x, x) >>> C2 = sp.spatial.distance.cdist(y, y) >>> np.round(partial_gromov_wasserstein(C1, C2, a, b),2) array([[0. , 0.25, 0. , 0. ], [0.25, 0. , 0. , 0. ], [0. , 0. , 0.25, 0. ], [0. , 0. , 0. , 0.25]]) >>> np.round(partial_gromov_wasserstein(C1, C2, a, b, m=0.25),2) array([[0. , 0. , 0. , 0. ], [0. , 0. , 0. , 0. ], [0. , 0. , 0.25, 0. ], [0. , 0. , 0. , 0. ]]) .. _references-partial-gromov-wasserstein: References ---------- .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. """ arr = [C1, C2] if p is not None: arr.append(list_to_array(p)) else: p = unif(C1.shape[0], type_as=C1) if q is not None: arr.append(list_to_array(q)) else: q = unif(C2.shape[0], type_as=C1) if G0 is not None: G0_ = G0 arr.append(G0) nx = get_backend(*arr) p0, q0, C10, C20 = p, q, C1, C2 p = nx.to_numpy(p0) q = nx.to_numpy(q0) C1 = nx.to_numpy(C10) C2 = nx.to_numpy(C20) if symmetric is None: symmetric = np.allclose(C1, C1.T, atol=1e-10) and np.allclose( C2, C2.T, atol=1e-10 ) if m is None: m = min(np.sum(p), np.sum(q)) elif m < 0: raise ValueError("Problem infeasible. Parameter m should be greater than 0.") elif m > min(np.sum(p), np.sum(q)): raise ValueError( "Problem infeasible. Parameter m should lower or" " equal than min(|a|_1, |b|_1)." ) if G0 is None: G0 = ( np.outer(p, q) * m / (np.sum(p) * np.sum(q)) ) # make sure |G0|=m, G01_m\leq p, G0.T1_n\leq q. else: G0 = nx.to_numpy(G0_) # Check marginals of G0 assert np.all(G0.sum(1) <= p) assert np.all(G0.sum(0) <= q) q_extended = np.append(q, [(np.sum(p) - m) / nb_dummies] * nb_dummies) p_extended = np.append(p, [(np.sum(q) - m) / nb_dummies] * nb_dummies) # cg for GW is implemented using numpy on CPU np_ = NumpyBackend() fC1, fC2, hC1, hC2 = _transform_matrix(C1, C2, loss_fun, np_) fC2t = fC2.T if not symmetric: fC1t, hC1t, hC2t = fC1.T, hC1.T, hC2.T ones_p = np_.ones(p.shape[0], type_as=p) ones_q = np_.ones(q.shape[0], type_as=q) def f(G): pG = G.sum(1) qG = G.sum(0) constC1 = np.outer(np.dot(fC1, pG), ones_q) constC2 = np.outer(ones_p, np.dot(qG, fC2t)) return gwloss(constC1 + constC2, hC1, hC2, G, np_) if symmetric: def df(G): pG = G.sum(1) qG = G.sum(0) constC1 = np.outer(np.dot(fC1, pG), ones_q) constC2 = np.outer(ones_p, np.dot(qG, fC2t)) return gwggrad(constC1 + constC2, hC1, hC2, G, np_) else: def df(G): pG = G.sum(1) qG = G.sum(0) constC1 = np.outer(np.dot(fC1, pG), ones_q) constC2 = np.outer(ones_p, np.dot(qG, fC2t)) constC1t = np.outer(np.dot(fC1t, pG), ones_q) constC2t = np.outer(ones_p, np.dot(qG, fC2)) return 0.5 * ( gwggrad(constC1 + constC2, hC1, hC2, G, np_) + gwggrad(constC1t + constC2t, hC1t, hC2t, G, np_) ) def line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs): df_Gc = df(deltaG + G) return solve_partial_gromov_linesearch( G, deltaG, cost_G, df_G, df_Gc, M=0.0, reg=1.0, nx=np_, **kwargs ) if not nx.is_floating_point(C10): warnings.warn( "Input structure matrix consists of integers. The transport plan will be " "casted accordingly, possibly resulting in a loss of precision. " "If this behaviour is unwanted, please make sure your input " "structure matrix consists of floating point elements.", stacklevel=2, ) if log: res, log = partial_cg( p, q, p_extended, q_extended, 0.0, 1.0, f, df, G0, line_search, log=True, numItermax=numItermax, stopThr=tol, stopThr2=0.0, warn=warn, **kwargs, ) log["partial_gw_dist"] = nx.from_numpy(log["loss"][-1], type_as=C10) return nx.from_numpy(res, type_as=C10), log else: return nx.from_numpy( partial_cg( p, q, p_extended, q_extended, 0.0, 1.0, f, df, G0, line_search, log=False, numItermax=numItermax, stopThr=tol, stopThr2=0.0, **kwargs, ), type_as=C10, ) def partial_gromov_wasserstein2( C1, C2, p=None, q=None, m=None, loss_fun="square_loss", nb_dummies=1, G0=None, thres=1, numItermax=1e4, tol=1e-7, symmetric=None, warn=False, log=False, verbose=False, **kwargs, ): r""" Returns the Partial Gromov-Wasserstein discrepancy between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})`. The function solves the following optimization problem using Conditional Gradient: .. math:: \mathbf{PGW} = \mathop{\min}_\mathbf{T} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) T_{i,j} T_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 \mathbf{1}^T \mathbf{T}^T \mathbf{1} = m &\leq \min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\} where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space. - :math:`\mathbf{C_2}`: Metric cost matrix in the target space. - :math:`\mathbf{p}`: Distribution in the source space. - :math:`\mathbf{q}`: Distribution in the target space. - `m` is the amount of mass to be transported - `L`: Loss function to account for the misfit between the similarity matrices. The formulation of the problem has been proposed in :ref:`[29] ` Note that when using backends, this loss function is differentiable wrt the matrices (C1, C2). .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. .. note:: All computations in the conjugate gradient solver are done with numpy to limit memory overhead. .. note:: This function will cast the computed transport plan to the data type of the provided input :math:`\mathbf{C}_1`. Casting to an integer tensor might result in a loss of precision. If this behaviour is unwanted, please make sure to provide a floating point input. Parameters ---------- C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space C2 : ndarray, shape (nt, nt) Metric cost matrix in the target space p : ndarray, shape (ns,) Distribution in the source space q : ndarray, shape (nt,) Distribution in the target space m : float, optional Amount of mass to be transported (default: :math:`\min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\}`) loss_fun : str, optional Loss function used for the solver either 'square_loss' or 'kl_loss'. nb_dummies : int, optional Number of dummy points to add (avoid instabilities in the EMD solver) G0 : ndarray, shape (ns, nt), optional Initialization of the transportation matrix thres : float, optional quantile of the gradient matrix to populate the cost matrix when 0 (default: 1) numItermax : int, optional Max number of iterations tol : float, optional tolerance for stopping iterations symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). warn: bool, optional. Whether to raise a warning when EMD did not converge. log : bool, optional return log if True verbose : bool, optional Print information along iterations **kwargs : dict parameters can be directly passed to the emd solver .. warning:: When dealing with a large number of points, the EMD solver may face some instabilities, especially when the mass associated to the dummy point is large. To avoid them, increase the number of dummy points (allows a smoother repartition of the mass over the points). Returns ------- partial_gw_dist : float partial GW discrepancy log : dict log dictionary returned only if `log` is `True` Examples -------- >>> from ot.gromov import partial_gromov_wasserstein2 >>> import scipy as sp >>> a = np.array([0.25] * 4) >>> b = np.array([0.25] * 4) >>> x = np.array([1,2,100,200]).reshape((-1,1)) >>> y = np.array([3,2,98,199]).reshape((-1,1)) >>> C1 = sp.spatial.distance.cdist(x, x) >>> C2 = sp.spatial.distance.cdist(y, y) >>> np.round(partial_gromov_wasserstein2(C1, C2, a, b),2) 3.38 >>> np.round(partial_gromov_wasserstein2(C1, C2, a, b, m=0.25),2) 0.0 .. _references-partial-gromov-wasserstein2: References ---------- .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. """ # simple get_backend as the full one will be handled in gromov_wasserstein nx = get_backend(C1, C2) # init marginals if set as None if p is None: p = unif(C1.shape[0], type_as=C1) if q is None: q = unif(C2.shape[0], type_as=C1) T, log_pgw = partial_gromov_wasserstein( C1, C2, p, q, m, loss_fun, nb_dummies, G0, thres, numItermax, tol, symmetric, warn, True, verbose, **kwargs, ) log_pgw["T"] = T pgw = log_pgw["partial_gw_dist"] if loss_fun == "square_loss": gC1 = 2 * C1 * nx.outer(p, p) - 2 * nx.dot(T, nx.dot(C2, T.T)) gC2 = 2 * C2 * nx.outer(q, q) - 2 * nx.dot(T.T, nx.dot(C1, T)) elif loss_fun == "kl_loss": gC1 = nx.log(C1 + 1e-15) * nx.outer(p, p) - nx.dot( T, nx.dot(nx.log(C2 + 1e-15), T.T) ) gC2 = -nx.dot(T.T, nx.dot(C1, T)) / (C2 + 1e-15) + nx.outer(q, q) pgw = nx.set_gradients(pgw, (C1, C2), (gC1, gC2)) if log: return pgw, log_pgw else: return pgw def partial_fused_gromov_wasserstein( M, C1, C2, p=None, q=None, m=None, loss_fun="square_loss", alpha=0.5, nb_dummies=1, G0=None, thres=1, numItermax=1e4, tol=1e-8, symmetric=None, warn=True, log=False, verbose=False, **kwargs, ): r""" Returns the Partial Fused Gromov-Wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{F_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{F_2}, \mathbf{q})`, with pairwise distance matrix :math:`\mathbf{M}` between node feature matrices. The function solves the following optimization problem using Conditional Gradient: .. math:: \mathbf{T}^* \in \mathop{\arg \min}_\mathbf{T} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) T_{i,j} T_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 \mathbf{1}^T \mathbf{T}^T \mathbf{1} = m &\leq \min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\} where : - :math:`\mathbf{M}`: metric cost matrix between features across domains - :math:`\mathbf{C_1}`: Metric cost matrix in the source space. - :math:`\mathbf{C_2}`: Metric cost matrix in the target space. - :math:`\mathbf{p}`: Distribution in the source space. - :math:`\mathbf{q}`: Distribution in the target space. - `m` is the amount of mass to be transported - `L`: Loss function to account for the misfit between the similarity matrices. The formulation of the problem has been proposed in :ref:`[29] ` .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. .. note:: All computations in the conjugate gradient solver are done with numpy to limit memory overhead. .. note:: This function will cast the computed transport plan to the data type of the provided input :math:`\mathbf{C}_1`. Casting to an integer tensor might result in a loss of precision. If this behaviour is unwanted, please make sure to provide a floating point input. Parameters ---------- M : array-like, shape (ns, nt) Metric cost matrix between features across domains C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric costfr matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. m : float, optional Amount of mass to be transported (default: :math:`\min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\}`) loss_fun : str, optional Loss function used for the solver either 'square_loss' or 'kl_loss'. alpha : float, optional Trade-off parameter (0 < alpha < 1) nb_dummies : int, optional Number of dummy points to add (avoid instabilities in the EMD solver) G0 : array-like, shape (ns, nt), optional Initialization of the transportation matrix thres : float, optional quantile of the gradient matrix to populate the cost matrix when 0 (default: 1) numItermax : int, optional Max number of iterations tol : float, optional tolerance for stopping iterations symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). warn: bool, optional. Whether to raise a warning when EMD did not converge. log : bool, optional return log if True verbose : bool, optional Print information along iterations **kwargs : dict parameters can be directly passed to the emd solver Returns ------- T : array-like, shape (`ns`, `nt`) Optimal transport matrix between the two spaces. log : dict Convergence information and loss. .. _references-partial-gromov-wasserstein: References ---------- .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. .. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs", International Conference on Machine Learning (ICML). 2019. """ arr = [M, C1, C2] if p is not None: arr.append(list_to_array(p)) else: p = unif(C1.shape[0], type_as=C1) if q is not None: arr.append(list_to_array(q)) else: q = unif(C2.shape[0], type_as=C1) if G0 is not None: G0_ = G0 arr.append(G0) nx = get_backend(*arr) p0, q0, M0, C10, C20, alpha0 = p, q, M, C1, C2, alpha p = nx.to_numpy(p0) q = nx.to_numpy(q0) M = nx.to_numpy(M0) C1 = nx.to_numpy(C10) C2 = nx.to_numpy(C20) alpha = nx.to_numpy(alpha0) if symmetric is None: symmetric = np.allclose(C1, C1.T, atol=1e-10) and np.allclose( C2, C2.T, atol=1e-10 ) if m is None: m = min(np.sum(p), np.sum(q)) elif m < 0: raise ValueError("Problem infeasible. Parameter m should be greater than 0.") elif m > min(np.sum(p), np.sum(q)): raise ValueError( "Problem infeasible. Parameter m should lower or" " equal than min(|a|_1, |b|_1)." ) if G0 is None: G0 = ( np.outer(p, q) * m / (np.sum(p) * np.sum(q)) ) # make sure |G0|=m, G01_m\leq p, G0.T1_n\leq q. else: G0 = nx.to_numpy(G0_) # Check marginals of G0 assert np.all(G0.sum(1) <= p) assert np.all(G0.sum(0) <= q) q_extended = np.append(q, [(np.sum(p) - m) / nb_dummies] * nb_dummies) p_extended = np.append(p, [(np.sum(q) - m) / nb_dummies] * nb_dummies) # cg for GW is implemented using numpy on CPU np_ = NumpyBackend() fC1, fC2, hC1, hC2 = _transform_matrix(C1, C2, loss_fun, np_) fC2t = fC2.T if not symmetric: fC1t, hC1t, hC2t = fC1.T, hC1.T, hC2.T ones_p = np_.ones(p.shape[0], type_as=p) ones_q = np_.ones(q.shape[0], type_as=q) def f(G): pG = G.sum(1) qG = G.sum(0) constC1 = np.outer(np.dot(fC1, pG), ones_q) constC2 = np.outer(ones_p, np.dot(qG, fC2t)) return gwloss(constC1 + constC2, hC1, hC2, G, np_) if symmetric: def df(G): pG = G.sum(1) qG = G.sum(0) constC1 = np.outer(np.dot(fC1, pG), ones_q) constC2 = np.outer(ones_p, np.dot(qG, fC2t)) return gwggrad(constC1 + constC2, hC1, hC2, G, np_) else: def df(G): pG = G.sum(1) qG = G.sum(0) constC1 = np.outer(np.dot(fC1, pG), ones_q) constC2 = np.outer(ones_p, np.dot(qG, fC2t)) constC1t = np.outer(np.dot(fC1t, pG), ones_q) constC2t = np.outer(ones_p, np.dot(qG, fC2)) return 0.5 * ( gwggrad(constC1 + constC2, hC1, hC2, G, np_) + gwggrad(constC1t + constC2t, hC1t, hC2t, G, np_) ) def line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs): df_Gc = df(deltaG + G) return solve_partial_gromov_linesearch( G, deltaG, cost_G, df_G, df_Gc, M=(1 - alpha) * M, reg=alpha, nx=np_, **kwargs, ) if not nx.is_floating_point(C10): warnings.warn( "Input structure matrix consists of integers. The transport plan will be " "casted accordingly, possibly resulting in a loss of precision. " "If this behaviour is unwanted, please make sure your input " "structure matrix consists of floating point elements.", stacklevel=2, ) if log: res, log = partial_cg( p, q, p_extended, q_extended, (1 - alpha) * M, alpha, f, df, G0, line_search, log=True, numItermax=numItermax, stopThr=tol, stopThr2=0.0, warn=warn, **kwargs, ) log["partial_fgw_dist"] = nx.from_numpy(log["loss"][-1], type_as=C10) return nx.from_numpy(res, type_as=C10), log else: return nx.from_numpy( partial_cg( p, q, p_extended, q_extended, (1 - alpha) * M, alpha, f, df, G0, line_search, log=False, numItermax=numItermax, stopThr=tol, stopThr2=0.0, **kwargs, ), type_as=C10, ) def partial_fused_gromov_wasserstein2( M, C1, C2, p=None, q=None, m=None, loss_fun="square_loss", alpha=0.5, nb_dummies=1, G0=None, thres=1, numItermax=1e4, tol=1e-7, symmetric=None, warn=False, log=False, verbose=False, **kwargs, ): r""" Returns the Partial Fused Gromov-Wasserstein discrepancy between :math:`(\mathbf{C_1}, \mathbf{F_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{F_2}, \mathbf{q})`, with pairwise distance matrix :math:`\mathbf{M}` between node feature matrices. The function solves the following optimization problem using Conditional Gradient: .. math:: \mathbf{PFGW}_{\alpha} = \mathop{\min}_\mathbf{T} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) T_{i,j} T_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 \mathbf{1}^T \mathbf{T}^T \mathbf{1} = m &\leq \min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\} where : - :math:`\mathbf{M}`: metric cost matrix between features across domains - :math:`\mathbf{C_1}`: Metric cost matrix in the source space. - :math:`\mathbf{C_2}`: Metric cost matrix in the target space. - :math:`\mathbf{p}`: Distribution in the source space. - :math:`\mathbf{q}`: Distribution in the target space. - `m` is the amount of mass to be transported - `L`: Loss function to account for the misfit between the similarity matrices. The formulation of the problem has been proposed in :ref:`[29] ` Note that when using backends, this loss function is differentiable wrt the matrices (M, C1, C2). .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. .. note:: All computations in the conjugate gradient solver are done with numpy to limit memory overhead. .. note:: This function will cast the computed transport plan to the data type of the provided input :math:`\mathbf{C}_1`. Casting to an integer tensor might result in a loss of precision. If this behaviour is unwanted, please make sure to provide a floating point input. Parameters ---------- M : array-like, shape (ns, nt) Metric cost matrix between features across domains C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space C2 : ndarray, shape (nt, nt) Metric cost matrix in the target space p : ndarray, shape (ns,) Distribution in the source space q : ndarray, shape (nt,) Distribution in the target space m : float, optional Amount of mass to be transported (default: :math:`\min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\}`) loss_fun : str, optional Loss function used for the solver either 'square_loss' or 'kl_loss'. alpha : float, optional Trade-off parameter (0 < alpha < 1) nb_dummies : int, optional Number of dummy points to add (avoid instabilities in the EMD solver) G0 : ndarray, shape (ns, nt), optional Initialization of the transportation matrix thres : float, optional quantile of the gradient matrix to populate the cost matrix when 0 (default: 1) numItermax : int, optional Max number of iterations tol : float, optional tolerance for stopping iterations symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). warn: bool, optional. Whether to raise a warning when EMD did not converge. log : bool, optional return log if True verbose : bool, optional Print information along iterations **kwargs : dict parameters can be directly passed to the emd solver .. warning:: When dealing with a large number of points, the EMD solver may face some instabilities, especially when the mass associated to the dummy point is large. To avoid them, increase the number of dummy points (allows a smoother repartition of the mass over the points). Returns ------- partial_fgw_dist : float partial FGW discrepancy log : dict log dictionary returned only if `log` is `True` .. _references-partial-gromov-wasserstein2: References ---------- .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. .. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs", International Conference on Machine Learning (ICML). 2019. """ # simple get_backend as the full one will be handled in gromov_wasserstein nx = get_backend(M, C1, C2) # init marginals if set as None if p is None: p = unif(C1.shape[0], type_as=C1) if q is None: q = unif(C2.shape[0], type_as=C1) T, log_pfgw = partial_fused_gromov_wasserstein( M, C1, C2, p, q, m, loss_fun, alpha, nb_dummies, G0, thres, numItermax, tol, symmetric, warn, True, verbose, **kwargs, ) log_pfgw["T"] = T pfgw = log_pfgw["partial_fgw_dist"] # compute separate terms for gradients and log lin_term = nx.sum(T * M) log_pfgw["quad_loss"] = pfgw - (1 - alpha) * lin_term log_pfgw["lin_loss"] = lin_term * (1 - alpha) pgw_term = log_pfgw["quad_loss"] / alpha if loss_fun == "square_loss": gC1 = 2 * C1 * nx.outer(p, p) - 2 * nx.dot(T, nx.dot(C2, T.T)) gC2 = 2 * C2 * nx.outer(q, q) - 2 * nx.dot(T.T, nx.dot(C1, T)) elif loss_fun == "kl_loss": gC1 = nx.log(C1 + 1e-15) * nx.outer(p, p) - nx.dot( T, nx.dot(nx.log(C2 + 1e-15), T.T) ) gC2 = -nx.dot(T.T, nx.dot(C1, T)) / (C2 + 1e-15) + nx.outer(q, q) if isinstance(alpha, int) or isinstance(alpha, float): pfgw = nx.set_gradients( pfgw, (M, C1, C2), ((1 - alpha) * T, alpha * gC1, alpha * gC2) ) else: pfgw = nx.set_gradients( pfgw, (M, C1, C2, alpha), ((1 - alpha) * T, alpha * gC1, alpha * gC2, pgw_term - lin_term), ) if log: return pfgw, log_pfgw else: return pfgw def solve_partial_gromov_linesearch( G, deltaG, cost_G, df_G, df_Gc, M, reg, alpha_min=None, alpha_max=None, nx=None, **kwargs, ): """ Solve the linesearch in the FW iterations of partial (F)GW following eq.5 of :ref:`[29]`. Parameters ---------- G : array-like, shape(ns, nt) The transport map at a given iteration of the FW deltaG : array-like, shape (ns, nt) Difference between the optimal map `Gc` found by linearization in the FW algorithm and the value at a given iteration cost_G : float Value of the cost at `G` df_G : array-like, shape (ns, nt) Gradient of the GW cost at `G` df_Gc : array-like, shape (ns, nt) Gradient of the GW cost at `Gc` M : array-like, shape (ns, nt) Cost matrix between the features. reg : float Regularization parameter. alpha_min : float, optional Minimum value for alpha alpha_max : float, optional Maximum value for alpha nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ------- alpha : float The optimal step size of the FW fc : int nb of function call. Useless here cost_G : float The value of the cost for the next iteration df_G : array-like, shape (ns, nt) Updated gradient of the GW cost References ---------- .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. """ if nx is None: if isinstance(M, int) or isinstance(M, float): nx = get_backend(G, deltaG, df_G, df_Gc) else: nx = get_backend(G, deltaG, df_G, df_Gc, M) df_deltaG = df_Gc - df_G cost_deltaG = 0.5 * nx.sum(df_deltaG * deltaG) a = reg * cost_deltaG # formula to check for partial FGW b = nx.sum(M * deltaG) + reg * nx.sum(df_G * deltaG) alpha = solve_1d_linesearch_quad(a, b) if alpha_min is not None or alpha_max is not None: alpha = np.clip(alpha, alpha_min, alpha_max) # the new cost is deduced from the line search quadratic function cost_G = cost_G + a * (alpha**2) + b * alpha # update the gradient for next cg iteration df_G = df_G + alpha * df_deltaG return alpha, 1, cost_G, df_G def entropic_partial_gromov_wasserstein( C1, C2, p=None, q=None, reg=1.0, m=None, loss_fun="square_loss", G0=None, numItermax=1000, tol=1e-7, symmetric=None, log=False, verbose=False, ): r""" Returns the partial Gromov-Wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})` The function solves the following optimization problem: .. math:: \mathbf{T} = \mathop{\arg \min}_{\mathbf{T}} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) T_{i,j} T_{k,l} + \mathrm{reg} \Omega(\mathbf{T}) .. math:: s.t. \ \mathbf{T} &\geq 0 \mathbf{T} \mathbf{1} &\leq \mathbf{a} \mathbf{T}^T \mathbf{1} &\leq \mathbf{b} \mathbf{1}^T \mathbf{T}^T \mathbf{1} = m &\leq \min\{\|\mathbf{a}\|_1, \|\mathbf{b}\|_1\} where : - :math:`\mathbf{C_1}` is the metric cost matrix in the source space - :math:`\mathbf{C_2}` is the metric cost matrix in the target space - :math:`\mathbf{p}` and :math:`\mathbf{q}` are the sample weights - `L`: quadratic loss function - :math:`\Omega` is the entropic regularization term, :math:`\Omega(\mathbf{T})=\sum_{i,j} T_{i,j}\log(T_{i,j})` - `m` is the amount of mass to be transported The formulation of the GW problem has been proposed in :ref:`[12] ` and the partial GW in :ref:`[29] ` Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. reg: float, optional. Default is 1. entropic regularization parameter m : float, optional Amount of mass to be transported (default: :math:`\min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\}`) loss_fun : str, optional Loss function used for the solver either 'square_loss' or 'kl_loss'. G0 : array-like, shape (ns, nt), optional Initialization of the transportation matrix numItermax : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). log : bool, optional return log if True verbose : bool, optional Print information along iterations Examples -------- >>> from ot.gromov import entropic_partial_gromov_wasserstein >>> import scipy as sp >>> a = np.array([0.25] * 4) >>> b = np.array([0.25] * 4) >>> x = np.array([1,2,100,200]).reshape((-1,1)) >>> y = np.array([3,2,98,199]).reshape((-1,1)) >>> C1 = sp.spatial.distance.cdist(x, x) >>> C2 = sp.spatial.distance.cdist(y, y) >>> np.round(entropic_partial_gromov_wasserstein(C1, C2, a, b, 1e2), 2) array([[0.12, 0.13, 0. , 0. ], [0.13, 0.12, 0. , 0. ], [0. , 0. , 0.25, 0. ], [0. , 0. , 0. , 0.25]]) >>> np.round(entropic_partial_gromov_wasserstein(C1, C2, a, b, 1e2,0.25), 2) array([[0.02, 0.03, 0. , 0.03], [0.03, 0.03, 0. , 0.03], [0. , 0. , 0.03, 0. ], [0.02, 0.02, 0. , 0.03]]) Returns ------- T : ndarray, shape (dim_a, dim_b) Optimal transportation matrix for the given parameters log : dict log dictionary returned only if `log` is `True` .. _references-entropic-partial-gromov-wasserstein: References ---------- .. [12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. See Also -------- ot.partial.partial_gromov_wasserstein: exact Partial Gromov-Wasserstein """ arr = [C1, C2, G0] if p is not None: p = list_to_array(p) arr.append(p) if q is not None: q = list_to_array(q) arr.append(q) nx = get_backend(*arr) if p is None: p = nx.ones(C1.shape[0], type_as=C1) / C1.shape[0] if q is None: q = nx.ones(C2.shape[0], type_as=C2) / C2.shape[0] if m is None: m = min(nx.sum(p), nx.sum(q)) elif m < 0: raise ValueError("Problem infeasible. Parameter m should be greater than 0.") elif m > min(nx.sum(p), nx.sum(q)): raise ValueError( "Problem infeasible. Parameter m should lower or" " equal than min(|a|_1, |b|_1)." ) if G0 is None: G0 = ( nx.outer(p, q) * m / (nx.sum(p) * nx.sum(q)) ) # make sure |G0|=m, G01_m\leq p, G0.T1_n\leq q. else: # Check marginals of G0 assert nx.any(nx.sum(G0, 1) <= p) assert nx.any(nx.sum(G0, 0) <= q) if symmetric is None: symmetric = np.allclose(C1, C1.T, atol=1e-10) and np.allclose( C2, C2.T, atol=1e-10 ) # Setup gradient computation fC1, fC2, hC1, hC2 = _transform_matrix(C1, C2, loss_fun, nx) fC2t = fC2.T if not symmetric: fC1t, hC1t, hC2t = fC1.T, hC1.T, hC2.T ones_p = nx.ones(p.shape[0], type_as=p) ones_q = nx.ones(q.shape[0], type_as=q) def f(G): pG = nx.sum(G, 1) qG = nx.sum(G, 0) constC1 = nx.outer(nx.dot(fC1, pG), ones_q) constC2 = nx.outer(ones_p, nx.dot(qG, fC2t)) return gwloss(constC1 + constC2, hC1, hC2, G, nx) if symmetric: def df(G): pG = nx.sum(G, 1) qG = nx.sum(G, 0) constC1 = nx.outer(nx.dot(fC1, pG), ones_q) constC2 = nx.outer(ones_p, nx.dot(qG, fC2t)) return gwggrad(constC1 + constC2, hC1, hC2, G, nx) else: def df(G): pG = nx.sum(G, 1) qG = nx.sum(G, 0) constC1 = nx.outer(nx.dot(fC1, pG), ones_q) constC2 = nx.outer(ones_p, nx.dot(qG, fC2t)) constC1t = nx.outer(nx.dot(fC1t, pG), ones_q) constC2t = nx.outer(ones_p, nx.dot(qG, fC2)) return 0.5 * ( gwggrad(constC1 + constC2, hC1, hC2, G, nx) + gwggrad(constC1t + constC2t, hC1t, hC2t, G, nx) ) cpt = 0 err = 1 loge = {"err": []} while err > tol and cpt < numItermax: Gprev = G0 M_entr = df(G0) G0 = entropic_partial_wasserstein(p, q, M_entr, reg, m) if cpt % 10 == 0: # to speed up the computations err = np.linalg.norm(G0 - Gprev) if log: loge["err"].append(err) if verbose: if cpt % 200 == 0: print( "{:5s}|{:12s}|{:12s}".format("It.", "Err", "Loss") + "\n" + "-" * 31 ) print("{:5d}|{:8e}|{:8e}".format(cpt, err, f(G0))) cpt += 1 if log: loge["partial_gw_dist"] = f(G0) return G0, loge else: return G0 def entropic_partial_gromov_wasserstein2( C1, C2, p=None, q=None, reg=1.0, m=None, loss_fun="square_loss", G0=None, numItermax=1000, tol=1e-7, symmetric=None, log=False, verbose=False, ): r""" Returns the partial Gromov-Wasserstein discrepancy between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})` The function solves the following optimization problem: .. math:: PGW = \min_{\mathbf{T}} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) T_{i,j}T_{k,l} + \mathrm{reg} \Omega(\mathbf{T}) .. math:: s.t. \ \mathbf{T} &\geq 0 \mathbf{T} \mathbf{1} &\leq \mathbf{a} \mathbf{T}^T \mathbf{1} &\leq \mathbf{b} \mathbf{1}^T \mathbf{T}^T \mathbf{1} = m &\leq \min\{\|\mathbf{a}\|_1, \|\mathbf{b}\|_1\} where : - :math:`\mathbf{C_1}` is the metric cost matrix in the source space - :math:`\mathbf{C_2}` is the metric cost matrix in the target space - :math:`\mathbf{p}` and :math:`\mathbf{q}` are the sample weights - `L`: Loss function to account for the misfit between the similarity matrices. - :math:`\Omega` is the entropic regularization term, :math:`\Omega(\mathbf{T})=\sum_{i,j} T_{i,j}\log(T_{i,j})` - `m` is the amount of mass to be transported The formulation of the GW problem has been proposed in :ref:`[12] ` and the partial GW in :ref:`[29] ` Parameters ---------- C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space C2 : ndarray, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. reg: float entropic regularization parameter m : float, optional Amount of mass to be transported (default: :math:`\min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\}`) loss_fun : str, optional Loss function used for the solver either 'square_loss' or 'kl_loss'. G0 : ndarray, shape (ns, nt), optional Initialization of the transportation matrix numItermax : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). log : bool, optional return log if True verbose : bool, optional Print information along iterations Returns ------- partial_gw_dist: float Partial Gromov-Wasserstein distance log : dict log dictionary returned only if `log` is `True` Examples -------- >>> from ot.gromov import entropic_partial_gromov_wasserstein2 >>> import scipy as sp >>> a = np.array([0.25] * 4) >>> b = np.array([0.25] * 4) >>> x = np.array([1,2,100,200]).reshape((-1,1)) >>> y = np.array([3,2,98,199]).reshape((-1,1)) >>> C1 = sp.spatial.distance.cdist(x, x) >>> C2 = sp.spatial.distance.cdist(y, y) >>> np.round(entropic_partial_gromov_wasserstein2(C1, C2, a, b, 1e2), 2) 3.75 .. _references-entropic-partial-gromov-wasserstein2: References ---------- .. [12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. """ partial_gw, log_gw = entropic_partial_gromov_wasserstein( C1, C2, p, q, reg, m, loss_fun, G0, numItermax, tol, symmetric, True, verbose ) log_gw["T"] = partial_gw if log: return log_gw["partial_gw_dist"], log_gw else: return log_gw["partial_gw_dist"] def entropic_partial_fused_gromov_wasserstein( M, C1, C2, p=None, q=None, reg=1.0, m=None, loss_fun="square_loss", alpha=0.5, G0=None, numItermax=1000, tol=1e-7, symmetric=None, log=False, verbose=False, ): r""" Returns the entropic partial Fused Gromov-Wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{F_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{F_2}, \mathbf{q})`, with pairwise distance matrix :math:`\mathbf{M}` between node feature matrices. The function solves the following optimization problem: .. math:: \mathbf{T} = \mathop{\arg \min}_{\mathbf{T}} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) T_{i,j} T_{k,l} + \mathrm{reg} \Omega(\mathbf{T}) .. math:: s.t. \ \mathbf{T} &\geq 0 \mathbf{T} \mathbf{1} &\leq \mathbf{a} \mathbf{T}^T \mathbf{1} &\leq \mathbf{b} \mathbf{1}^T \mathbf{T}^T \mathbf{1} = m &\leq \min\{\|\mathbf{a}\|_1, \|\mathbf{b}\|_1\} where : - :math:`\mathbf{M}`: metric cost matrix between features across domains - :math:`\mathbf{C_1}` is the metric cost matrix in the source space - :math:`\mathbf{C_2}` is the metric cost matrix in the target space - :math:`\mathbf{p}` and :math:`\mathbf{q}` are the sample weights - `L`: quadratic loss function - :math:`\Omega` is the entropic regularization term, :math:`\Omega(\mathbf{T})=\sum_{i,j} T_{i,j}\log(T_{i,j})` - `m` is the amount of mass to be transported The formulation of the FGW problem has been proposed in :ref:`[24] ` and the partial GW in :ref:`[29] ` Parameters ---------- M : array-like, shape (ns, nt) Metric cost matrix between features across domains C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. reg: float, optional. Default is 1. entropic regularization parameter m : float, optional Amount of mass to be transported (default: :math:`\min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\}`) loss_fun : str, optional Loss function used for the solver either 'square_loss' or 'kl_loss'. alpha : float, optional Trade-off parameter (0 < alpha < 1) G0 : array-like, shape (ns, nt), optional Initialization of the transportation matrix numItermax : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). log : bool, optional return log if True verbose : bool, optional Print information along iterations Returns ------- T : ndarray, shape (dim_a, dim_b) Optimal transportation matrix for the given parameters log : dict log dictionary returned only if `log` is `True` .. _references-entropic-partial-fused-gromov-wasserstein: References ---------- .. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs", International Conference on Machine Learning (ICML). 2019. .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. See Also -------- ot.gromov.partial_fused_gromov_wasserstein: exact Partial Fused Gromov-Wasserstein """ arr = [M, C1, C2, G0] if p is not None: p = list_to_array(p) arr.append(p) if q is not None: q = list_to_array(q) arr.append(q) nx = get_backend(*arr) if p is None: p = nx.ones(C1.shape[0], type_as=C1) / C1.shape[0] if q is None: q = nx.ones(C2.shape[0], type_as=C2) / C2.shape[0] if m is None: m = min(nx.sum(p), nx.sum(q)) elif m < 0: raise ValueError("Problem infeasible. Parameter m should be greater" " than 0.") elif m > min(nx.sum(p), nx.sum(q)): raise ValueError( "Problem infeasible. Parameter m should lower or" " equal than min(|a|_1, |b|_1)." ) if G0 is None: G0 = ( nx.outer(p, q) * m / (nx.sum(p) * nx.sum(q)) ) # make sure |G0|=m, G01_m\leq p, G0.T1_n\leq q. else: # Check marginals of G0 assert nx.any(nx.sum(G0, 1) <= p) assert nx.any(nx.sum(G0, 0) <= q) if symmetric is None: symmetric = np.allclose(C1, C1.T, atol=1e-10) and np.allclose( C2, C2.T, atol=1e-10 ) # Setup gradient computation fC1, fC2, hC1, hC2 = _transform_matrix(C1, C2, loss_fun, nx) fC2t = fC2.T if not symmetric: fC1t, hC1t, hC2t = fC1.T, hC1.T, hC2.T ones_p = nx.ones(p.shape[0], type_as=p) ones_q = nx.ones(q.shape[0], type_as=q) def f(G): pG = nx.sum(G, 1) qG = nx.sum(G, 0) constC1 = nx.outer(nx.dot(fC1, pG), ones_q) constC2 = nx.outer(ones_p, nx.dot(qG, fC2t)) return alpha * gwloss(constC1 + constC2, hC1, hC2, G, nx) + ( 1 - alpha ) * nx.sum(G * M) if symmetric: def df(G): pG = nx.sum(G, 1) qG = nx.sum(G, 0) constC1 = nx.outer(nx.dot(fC1, pG), ones_q) constC2 = nx.outer(ones_p, nx.dot(qG, fC2t)) return alpha * gwggrad(constC1 + constC2, hC1, hC2, G, nx) + ( 1 - alpha ) * nx.sum(G * M) else: def df(G): pG = nx.sum(G, 1) qG = nx.sum(G, 0) constC1 = nx.outer(nx.dot(fC1, pG), ones_q) constC2 = nx.outer(ones_p, nx.dot(qG, fC2t)) constC1t = nx.outer(nx.dot(fC1t, pG), ones_q) constC2t = nx.outer(ones_p, nx.dot(qG, fC2)) return 0.5 * alpha * ( gwggrad(constC1 + constC2, hC1, hC2, G, nx) + gwggrad(constC1t + constC2t, hC1t, hC2t, G, nx) ) + (1 - alpha) * nx.sum(G * M) cpt = 0 err = 1 loge = {"err": []} while err > tol and cpt < numItermax: Gprev = G0 M_entr = df(G0) G0 = entropic_partial_wasserstein(p, q, M_entr, reg, m) if cpt % 10 == 0: # to speed up the computations err = np.linalg.norm(G0 - Gprev) if log: loge["err"].append(err) if verbose: if cpt % 200 == 0: print( "{:5s}|{:12s}|{:12s}".format("It.", "Err", "Loss") + "\n" + "-" * 31 ) print("{:5d}|{:8e}|{:8e}".format(cpt, err, f(G0))) cpt += 1 if log: loge["partial_fgw_dist"] = f(G0) return G0, loge else: return G0 def entropic_partial_fused_gromov_wasserstein2( M, C1, C2, p=None, q=None, reg=1.0, m=None, loss_fun="square_loss", alpha=0.5, G0=None, numItermax=1000, tol=1e-7, symmetric=None, log=False, verbose=False, ): r""" Returns the entropic partial Fused Gromov-Wasserstein discrepancy between :math:`(\mathbf{C_1}, \mathbf{F_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{F_2}, \mathbf{q})`, with pairwise distance matrix :math:`\mathbf{M}` between node feature matrices. The function solves the following optimization problem: .. math:: PGW = \min_{\mathbf{T}} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) T_{i,j} T_{k,l} + \mathrm{reg} \cdot\Omega(\mathbf{T}) .. math:: s.t. \ \mathbf{T} &\geq 0 \mathbf{T} \mathbf{1} &\leq \mathbf{a} \mathbf{T}^T \mathbf{1} &\leq \mathbf{b} \mathbf{1}^T \mathbf{T}^T \mathbf{1} = m &\leq \min\{\|\mathbf{a}\|_1, \|\mathbf{b}\|_1\} where : - :math:`\mathbf{M}`: metric cost matrix between features across domains - :math:`\mathbf{C_1}` is the metric cost matrix in the source space - :math:`\mathbf{C_2}` is the metric cost matrix in the target space - :math:`\mathbf{p}` and :math:`\mathbf{q}` are the sample weights - `L`: Loss function to account for the misfit between the similarity matrices. - :math:`\Omega` is the entropic regularization term, :math:`\Omega(\mathbf{T})=\sum_{i,j} T_{i,j}\log(T_{i,j})` - `m` is the amount of mass to be transported The formulation of the FGW problem has been proposed in :ref:`[24] ` and the partial GW in :ref:`[29] ` Parameters ---------- M : array-like, shape (ns, nt) Metric cost matrix between features across domains C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space C2 : ndarray, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. reg: float entropic regularization parameter m : float, optional Amount of mass to be transported (default: :math:`\min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\}`) loss_fun : str, optional Loss function used for the solver either 'square_loss' or 'kl_loss'. alpha : float, optional Trade-off parameter (0 < alpha < 1) G0 : ndarray, shape (ns, nt), optional Initialization of the transportation matrix numItermax : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). log : bool, optional return log if True verbose : bool, optional Print information along iterations Returns ------- partial_fgw_dist: float Partial Entropic Fused Gromov-Wasserstein discrepancy log : dict log dictionary returned only if `log` is `True` .. _references-entropic-partial-fused-gromov-wasserstein2: References ---------- .. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs", International Conference on Machine Learning (ICML). 2019. .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. """ nx = get_backend(M, C1, C2) T, log_pfgw = entropic_partial_fused_gromov_wasserstein( M, C1, C2, p, q, reg, m, loss_fun, alpha, G0, numItermax, tol, symmetric, True, verbose, ) log_pfgw["T"] = T # setup for ot.solve_gromov lin_term = nx.sum(T * M) log_pfgw["quad_loss"] = log_pfgw["partial_fgw_dist"] - (1 - alpha) * lin_term log_pfgw["lin_loss"] = lin_term * (1 - alpha) if log: return log_pfgw["partial_fgw_dist"], log_pfgw else: return log_pfgw["partial_fgw_dist"] python-pot-0.9.7+dfsg/ot/gromov/_quantized.py000066400000000000000000001302061525521106200212400ustar00rootroot00000000000000""" Quantized (Fused) Gromov-Wasserstein solvers. """ # Author: Cédric Vincent-Cuaz # # License: MIT License import numpy as np import warnings try: from networkx.algorithms.community import asyn_fluidc, louvain_communities from networkx import from_numpy_array, pagerank networkx_import = True except ImportError: networkx_import = False try: from sklearn.cluster import SpectralClustering, KMeans sklearn_import = True except ImportError: sklearn_import = False import random from ..utils import list_to_array, unif, dist from ..backend import get_backend from ..lp import emd_1d from ._gw import gromov_wasserstein, fused_gromov_wasserstein from ._utils import init_matrix, gwloss def quantized_fused_gromov_wasserstein_partitioned( CR1, CR2, list_R1, list_R2, list_p1, list_p2, MR=None, alpha=1.0, build_OT=False, log=False, armijo=False, max_iter=1e4, tol_rel=1e-9, tol_abs=1e-9, nx=None, **kwargs, ): r""" Returns the quantized Fused Gromov-Wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{F_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{F_2}, \mathbf{q})`, whose samples are assigned to partitions and representants :math:`\mathcal{P_1} = \{(\mathbf{P_{1, i}}, \mathbf{r_{1, i}})\}_{i \leq npart1}` and :math:`\mathcal{P_2} = \{(\mathbf{P_{2, j}}, \mathbf{r_{2, j}})\}_{j \leq npart2}`. The latter must be precomputed and encoded e.g for the source as: :math:`\mathbf{CR_1}` structure matrix between representants; `list_R1` a list of relations between representants and their associated samples; `list_p1` a list of nodes distribution within each partition; :math:`\mathbf{FR_1}` feature matrix of representants. The function estimates the following optimization problem: .. math:: \mathbf{T}^* \in \mathop{\arg \min}_\mathbf{T} \quad \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} + (1-\alpha) \langle \mathbf{T}, M\rangle_F s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 \mathbf{T}_{|\mathbf{P_{1, i}}, \mathbf{P_{2, j}}} &= T^{g}_{ij} \mathbf{T}^{(i,j)} using a two-step strategy computing: i) a global alignment :math:`\mathbf{T}^{g}` between representants joint structure and feature spaces; ii) local alignments :math:`\mathbf{T}^{(i, j)}` between partitions :math:`\mathbf{P_{1, i}}` and :math:`\mathbf{P_{2, j}}` seen as 1D measures. Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{F_1}`: Feature matrix in the source space - :math:`\mathbf{F_2}`: Feature matrix in the target space - :math:`\mathbf{M}`: Pairwise similarity matrix between features - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - :math:`L`: quadratic loss function to account for the misfit between the similarity matrices .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. .. note:: All computations in the Gromov-Wasserstein conjugate gradient solver are done with numpy to limit memory overhead. Parameters ---------- CR1 : array-like, shape (npart1, npart1) Structure matrix between partition representants in the source space. CR2 : array-like, shape (npart2, npart2) Structure matrix between partition representants in the target space. list_R1 : list of npart1 arrays, List of relations between representants and their associated samples in the source space. list_R2 : list of npart2 arrays, List of relations between representants and their associated samples in the target space. list_p1 : list of npart1 arrays, List of node distributions within each partition of the source space. list_p : list of npart2 arrays, List of node distributions within each partition of the target space. MR : array-like, shape (npart1, npart2), optional. (Default is None) Metric cost matrix between features of representants across spaces. alpha: float, optional. Default is None. FGW trade-off parameter in :math:`]0, 1]` between structure and features. If `alpha = 1` features are ignored hence computing qGW. build_OT: bool, optional. Default is False Either to build or not the OT between non-partitioned structures. log : bool, optional. Default is False record log if True armijo : bool, optional If True the step of the line-search is found via an armijo research. Else closed form is used. If there are convergence issues use False. max_iter : int, optional Max number of iterations tol_rel : float, optional Stop threshold on relative error (>0) tol_abs : float, optional Stop threshold on absolute error (>0) nx : backend, optional POT backend **kwargs : dict parameters can be directly passed to the ot.optim.cg solver Returns ------- T_global: array-like, shape (`npart1`, `npart2`) Gromov-Wasserstein alignment :math:`\mathbf{T}^{g}` between representants. Ts_local: dict of local OT matrices. Dictionary with keys :math:`(i, j)` corresponding to 1D OT between :math:`\mathbf{P_{1, i}}` and :math:`\mathbf{P_{2, j}}` if :math:`T^{g}_{ij} \neq 0`. T: array-like, shape `(ns, nt)` Coupling between the two spaces if `build_OT=True` else None. log : dict, if `log=True`. Convergence information and losses of inner OT problems. References ---------- .. [68] Chowdhury, S., Miller, D., & Needham, T. (2021). Quantized gromov-wasserstein. ECML PKDD 2021. Springer International Publishing. """ if nx is None: arr = [CR1, CR2, *list_R1, *list_R2, *list_p1, *list_p2] if MR is not None: arr.append(MR) nx = get_backend(*arr) npart1 = len(list_R1) npart2 = len(list_R2) # compute marginals for global alignment pR1 = nx.from_numpy(list_to_array([nx.sum(p) for p in list_p1])) pR2 = nx.from_numpy(list_to_array([nx.sum(q) for q in list_p2])) # compute global alignment if alpha == 1.0: res_global = gromov_wasserstein( CR1, CR2, pR1, pR2, loss_fun="square_loss", log=log, armijo=armijo, max_iter=max_iter, tol_rel=tol_rel, tol_abs=tol_abs, ) if log: T_global, dist_global = res_global[0], res_global[1]["gw_dist"] else: T_global = res_global elif (alpha < 1.0) and (alpha > 0.0): res_global = fused_gromov_wasserstein( MR, CR1, CR2, pR1, pR2, "square_loss", alpha=alpha, log=log, armijo=armijo, max_iter=max_iter, tol_rel=tol_rel, tol_abs=tol_abs, ) if log: T_global, dist_global = res_global[0], res_global[1]["fgw_dist"] else: T_global = res_global else: raise ValueError( f""" `alpha='{alpha}'` should be in ]0, 1]. """ ) if log: log_ = {} log_["global dist"] = dist_global # compute local alignments Ts_local = {} list_p1_norm = [p / nx.sum(p) for p in list_p1] list_p2_norm = [q / nx.sum(q) for q in list_p2] for i in range(npart1): for j in range(npart2): if T_global[i, j] != 0.0: res_1d = emd_1d( list_R1[i], list_R2[j], list_p1_norm[i], list_p2_norm[j], metric="sqeuclidean", p=1.0, log=log, ) if log: T_local, log_local = res_1d Ts_local[(i, j)] = T_local log_[f"local dist ({i},{j})"] = log_local["cost"] else: Ts_local[(i, j)] = res_1d if build_OT: T_rows = [] for i in range(npart1): list_Ti = [] for j in range(npart2): if T_global[i, j] == 0.0: T_local = nx.zeros( (list_R1[i].shape[0], list_R2[j].shape[0]), type_as=T_global ) else: T_local = T_global[i, j] * Ts_local[(i, j)] list_Ti.append(T_local) Ti = nx.concatenate(list_Ti, axis=1) T_rows.append(Ti) T = nx.concatenate(T_rows, axis=0) else: T = None if log: return T_global, Ts_local, T, log_ else: return T_global, Ts_local, T def get_graph_partition( C, npart, part_method="random", F=None, alpha=1.0, random_state=0, nx=None ): r""" Partitioning a given graph with structure matrix :math:`\mathbf{C} \in R^{n \times n}` into `npart` partitions either 'random', or using one of {'louvain', 'fluid'} algorithms from networkx, or 'spectral' clustering from scikit-learn, or (Fused) Gromov-Wasserstein projections from POT. Parameters ---------- C : array-like, shape (n, n) Structure matrix. npart : int, number of partitions/clusters smaller than the number of nodes in :math:`\mathbf{C}`. part_method : str, optional. Default is 'random'. Partitioning algorithm to use among {'random', 'louvain', 'fluid', 'spectral', 'GW', 'FGW'}. 'random' for random sampling of points; 'louvain' and 'fluid' for graph partitioning algorithm that works well on adjacency matrix, If the louvain algorithm is used, `npart` is ignored; 'spectral' for spectral clustering; '(F)GW' for (F)GW projection using sr(F)GW solvers. F : array-like, shape (n, d), optional. (Default is None) Optional feature matrix aligned with the graph structure. Only used if `part_method="FGW"`. alpha : float, optional. (Default is 1.) Trade-off parameter between feature and structure matrices, taking values in [0, 1] and only used if `F != None` and `part_method="FGW"`. random_state: int, optional Random seed for the partitioning algorithm. nx : backend, optional POT backend. Returns ------- part : array-like, shape (npart,) Array of partition assignment for each node. References ---------- .. [68] Chowdhury, S., Miller, D., & Needham, T. (2021). Quantized gromov-wasserstein. ECML PKDD 2021. Springer International Publishing. """ if nx is None: nx = get_backend(C) n = C.shape[0] C0 = C if (alpha != 1.0) and (F is None): raise ValueError("`alpha != 1` but node features are not provided.") if npart >= n: warnings.warn( "Requested number of partitions higher than the number of nodes" "hence we enforce each node to be a partition.", stacklevel=2, ) part = np.arange(n) elif npart == 1: part = np.zeros(n) elif part_method == "random": # randomly partition the space random.seed(random_state) part = list_to_array(random.choices(np.arange(npart), k=C.shape[0])) elif part_method == "louvain": C = nx.to_numpy(C0) graph = from_numpy_array(C) part_sets = louvain_communities(graph, seed=random_state) part = np.zeros(n) for iset_, set_ in enumerate(part_sets): set_ = list(set_) part[set_] = iset_ elif part_method == "fluid": C = nx.to_numpy(C0) graph = from_numpy_array(C) part_sets = asyn_fluidc(graph, npart, seed=random_state) part = np.zeros(n) for iset_, set_ in enumerate(part_sets): set_ = list(set_) part[set_] = iset_ elif part_method == "spectral": C = nx.to_numpy(C0) sc = SpectralClustering( n_clusters=npart, random_state=random_state, affinity="precomputed" ).fit(C) part = sc.labels_ elif part_method in ["GW", "FGW"]: raise ValueError(f"`part_method == {part_method}` not implemented yet.") else: raise ValueError( f""" Unknown `part_method='{part_method}'`. Use one of: {"random", "louvain", "fluid", "spectral", "GW", "FGW"}. """ ) return nx.from_numpy(part, type_as=C0) def get_graph_representants(C, part, rep_method="pagerank", random_state=0, nx=None): r""" Get representative node for each partition given by :math:`\mathbf{part} \in R^{n}` of a graph with structure matrix :math:`\mathbf{C} \in R^{n \times n}`. Selection is either done randomly or using 'pagerank' algorithm from networkx. Parameters ---------- C : array-like, shape (n, n) structure matrix. part : array-like, shape (n,) Array of partition assignment for each node. rep_method : str, optional. Default is 'pagerank'. Selection method for representant in each partition. Can be either 'random' i.e random sampling within each partition, or 'pagerank' to select a node with maximal pagerank. random_state: int, optional Random seed for the partitioning algorithm nx : backend, optional POT backend Returns ------- rep_indices : list, shape (npart,) indices for representative node of each partition sorted according to partition identifiers. References ---------- .. [68] Chowdhury, S., Miller, D., & Needham, T. (2021). Quantized gromov-wasserstein. ECML PKDD 2021. Springer International Publishing. """ if nx is None: nx = get_backend(C, part) rep_indices = [] part_ids = nx.unique(part) n_part_ids = part_ids.shape[0] if n_part_ids == C.shape[0]: rep_indices = nx.arange(n_part_ids) elif rep_method == "random": random.seed(random_state) for id_, part_id in enumerate(part_ids): indices = nx.where(part == part_id)[0] rep_indices.append(random.choice(indices)) elif rep_method == "pagerank": C0, part0 = C, part C = nx.to_numpy(C0) part = nx.to_numpy(part0) part_ids = np.unique(part) for id_ in part_ids: indices = np.where(part == id_)[0] C_id = C[indices, :][:, indices] graph = from_numpy_array(C_id) pagerank_values = list(pagerank(graph).values()) rep_idx = np.argmax(pagerank_values) rep_indices.append(indices[rep_idx]) else: raise ValueError( f""" Unknown `rep_method='{rep_method}'`. Use one of: {"random", "pagerank"}. """ ) return rep_indices def format_partitioned_graph( C, p, part, rep_indices, F=None, M=None, alpha=1.0, nx=None ): r""" Format an attributed graph :math:`(\mathbf{C}, \mathbf{F}, \mathbf{p})` with structure matrix :math:`(\mathbf{C} \in R^{n \times n}`, feature matrix :math:`(\mathbf{F} \in R^{n \times d}` and node relative importance :math:`(\mathbf{p} \in \Sigma_n`, into a partitioned attributed graph taking into account partitions and representants :math:`\mathcal{P} = \left\{(\mathbf{P_{i}}, \mathbf{r_{i}})\right\}_i`. Parameters ---------- C : array-like, shape (n, n) Structure matrix. p : array-like, shape (n,), Node distribution. part : array-like, shape (n,) Array of partition assignment for each node. rep_indices : list of array-like of ints, shape (npart,) indices for representative node of each partition sorted according to partition identifiers. F : array-like, shape (n, d), optional. (Default is None) Optional feature matrix aligned with the graph structure. M : array-like, shape (n, n), optional. (Default is None) Optional pairwise similarity matrix between features. alpha: float, optional. Default is 1. Trade-off parameter in :math:`]0, 1]` between structure and features. If `alpha = 1` features are ignored. This trade-off is taken into account into the outputted relations between nodes and representants. nx : backend, optional POT backend Returns ------- CR : array-like, shape (npart, npart) Structure matrix between partition representants. list_R : list of npart arrays, List of relations between a representant and nodes in its partition, for each partition. list_p : list of npart arrays, List of node distributions within each partition. FR : array-like, shape (npart, d), if `F != None`. Feature matrix of representants. References ---------- .. [68] Chowdhury, S., Miller, D., & Needham, T. (2021). Quantized gromov-wasserstein. ECML PKDD 2021. Springer International Publishing. """ if nx is None: arr = [C, p, part] if F is not None: arr.append(F) if M is not None: arr.append(M) nx = get_backend(*arr) if alpha != 1.0: if (M is None) or (F is None): raise ValueError( f""" `alpha == {alpha} != 1` but features information is not properly provided. """ ) CR = C[rep_indices, :][:, rep_indices] if alpha != 1.0: C_new = alpha * C + (1 - alpha) * M else: C_new = C list_R, list_p = [], [] part_ids = nx.unique(part) for id_, part_id in enumerate(part_ids): indices = nx.where(part == part_id)[0] list_R.append(C_new[rep_indices[id_], indices]) list_p.append(p[indices]) if F is None: return CR, list_R, list_p else: FR = F[rep_indices, :] return CR, list_R, list_p, FR def quantized_fused_gromov_wasserstein( C1, C2, npart1, npart2, p=None, q=None, C1_aux=None, C2_aux=None, F1=None, F2=None, alpha=1.0, part_method="fluid", rep_method="random", log=False, armijo=False, max_iter=1e4, tol_rel=1e-9, tol_abs=1e-9, random_state=0, **kwargs, ): r""" Returns the quantized Fused Gromov-Wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{F_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{F_2}, \mathbf{q})`, whose samples are assigned to partitions and representants :math:`\mathcal{P_1} = \{(\mathbf{P_{1, i}}, \mathbf{r_{1, i}})\}_{i \leq npart1}` and :math:`\mathcal{P_2} = \{(\mathbf{P_{2, j}}, \mathbf{r_{2, j}})\}_{j \leq npart2}`. The function estimates the following optimization problem: .. math:: \mathbf{T}^* \in \mathop{\arg \min}_\mathbf{T} \quad \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} + (1-\alpha) \langle \mathbf{T}, \mathbf{D}(\mathbf{F_1}, \mathbf{F}_2) \rangle_F s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 \mathbf{T}_{|\mathbf{P_{1, i}}, \mathbf{P_{2, j}}} &= T^{g}_{ij} \mathbf{T}^{(i,j)} using a two-step strategy computing: i) a global alignment :math:`\mathbf{T}^{g}` between representants across joint structure and feature spaces; ii) local alignments :math:`\mathbf{T}^{(i, j)}` between partitions :math:`\mathbf{P_{1, i}}` and :math:`\mathbf{P_{2, j}}` seen as 1D measures. Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{F_1}`: Feature matrix in the source space - :math:`\mathbf{F_2}`: Feature matrix in the target space - :math:`\mathbf{D}(\mathbf{F_1}, \mathbf{F_2})`: Pairwise euclidean distance matrix between features - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - :math:`L`: quadratic loss function to account for the misfit between the similarity matrices .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. .. note:: All computations in the conjugate gradient solver are done with numpy to limit memory overhead. Parameters ---------- C1 : array-like, shape (ns, ns) Structure matrix in the source space. C2 : array-like, shape (nt, nt) Structure matrix in the target space. npart1 : int, number of partition in the source space. npart2 : int, number of partition in the target space. p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. C1_aux : array-like, shape (ns, ns), optional. Default is None. Auxiliary structure matrix in the source space to perform the partitioning and representant selection. C2_aux : array-like, shape (nt, nt), optional. Default is None. Auxiliary structure matrix in the target space to perform the partitioning and representant selection. F1 : array-like, shape (ns, d), optional. Default is None. Feature matrix in the source space. F2 : array-like, shape (nt, d), optional. Default is None. Feature matrix in the target space alpha: float, optional. Default is 1. FGW trade-off parameter in :math:`]0, 1]` between structure and features. If `alpha = 1` features are ignored hence computing qGW, if `alpha=0` structures are ignored and we compute the quantized Wasserstein transport. part_method : str, optional. Default is 'spectral'. Partitioning algorithm to use among {'random', 'louvain', 'fluid', 'spectral', 'louvain_fused', 'fluid_fused', 'spectral_fused', 'GW', 'FGW'}. If part_method in {'louvain_fused', 'fluid_fused', 'spectral_fused'}, corresponding graph partitioning algorithm {'louvain', 'fluid', 'spectral'} will be used on the modified structure matrix :math:`\alpha \mathbf{C} + (1 - \alpha) \mathbf{D}(\mathbf{F})` where :math:`\mathbf{D}(\mathbf{F})` is the pairwise euclidean matrix between features. If part_method in {'GW', 'FGW'}, a (F)GW projection is used. If the louvain algorithm is used, the requested number of partitions is ignored. rep_method : str, optional. Default is 'pagerank'. Selection method for node representant in each partition. Can be either 'random' i.e random sampling within each partition, {'pagerank', 'pagerank_fused'} to select a node with maximal pagerank w.r.t :math:`\mathbf{C}` or :math:`\alpha \mathbf{C} + (1 - \alpha) \mathbf{D}(\mathbf{F})`. verbose : bool, optional Print information along iterations log : bool, optional record log if True armijo : bool, optional If True the step of the line-search is found via an armijo research. Else closed form is used. If there are convergence issues use False. max_iter : int, optional Max number of iterations tol_rel : float, optional Stop threshold on relative error (>0) tol_abs : float, optional Stop threshold on absolute error (>0) random_state: int, optional Random seed for the partitioning algorithm **kwargs : dict parameters can be directly passed to the ot.optim.cg solver Returns ------- T_global: array-like, shape (`npart1`, `npart2`) Fused Gromov-Wasserstein alignment :math:`\mathbf{T}^{g}` between representants. Ts_local: dict of local OT matrices. Dictionary with keys :math:`(i, j)` corresponding to 1D OT between :math:`\mathbf{P_{1, i}}` and :math:`\mathbf{P_{2, j}}` if :math:`T^{g}_{ij} \neq 0`. T: array-like, shape `(ns, nt)` Coupling between the two spaces. log : dict Convergence information for inner problems and qGW loss. References ---------- .. [68] Chowdhury, S., Miller, D., & Needham, T. (2021). Quantized gromov-wasserstein. ECML PKDD 2021. Springer International Publishing. """ if part_method in ["fluid", "louvain", "fluid_fused", "louvain_fused"] or ( rep_method in ["pagerank", "pagerank_fused"] ): if not networkx_import: warnings.warn( f""" Networkx is not installed, so part_method={part_method} and/or rep_method={rep_method} cannot be used and are set to `random` default methods. Consider installing Networkx to fix this. """ ) part_method = "random" rep_method = "random" if (part_method in ["spectral", "spectral_fused"]) and (not sklearn_import): warnings.warn( f""" Scikit-learn is not installed, so part_method={part_method} and/or rep_method={rep_method} cannot be used and are set to `random` default methods. Consider installing Scikit-learn to fix this. """ ) part_method = "random" rep_method = "random" if ("fused" in part_method) or ("fused" in rep_method) or (part_method == "FGW"): if (F1 is None) or (F2 is None): raise ValueError( f""" `part_method='{part_method}'` and/or `rep_method='{rep_method}'` require feature matrices which are not provided as inputs. """ ) arr = [C1, C2] if C1_aux is not None: arr.append(C1_aux) else: C1_aux = C1 if C2_aux is not None: arr.append(C2_aux) else: C2_aux = C2 if p is not None: arr.append(list_to_array(p)) else: p = unif(C1.shape[0], type_as=C1) if q is not None: arr.append(list_to_array(q)) else: q = unif(C2.shape[0], type_as=C1) if F1 is not None: arr.append(F1) if F2 is not None: arr.append(F1) nx = get_backend(*arr) DF1 = None DF2 = None # compute attributed graph partitions potentially using the auxiliary structure if "fused" in part_method: DF1 = dist(F1, F1) DF2 = dist(F2, F2) C1_new = alpha * C1_aux + (1 - alpha) * DF1 C2_new = alpha * C2_aux + (1 - alpha) * DF2 part_method_ = part_method[:-6] part1 = get_graph_partition( C1_new, npart1, part_method_, random_state=random_state, nx=nx ) part2 = get_graph_partition( C2_new, npart2, part_method_, random_state=random_state, nx=nx ) else: part1 = get_graph_partition( C1_aux, npart1, part_method, F1, alpha, random_state, nx ) part2 = get_graph_partition( C2_aux, npart2, part_method, F2, alpha, random_state, nx ) if "fused" in rep_method: if DF1 is None: DF1 = dist(F1, F1) DF2 = dist(F2, F2) C1_new = alpha * C1_aux + (1 - alpha) * DF1 C2_new = alpha * C2_aux + (1 - alpha) * DF2 rep_method_ = rep_method[:-6] rep_indices1 = get_graph_representants( C1_new, part1, rep_method_, random_state, nx ) rep_indices2 = get_graph_representants( C2_new, part2, rep_method_, random_state, nx ) else: rep_indices1 = get_graph_representants( C1_aux, part1, rep_method, random_state, nx ) rep_indices2 = get_graph_representants( C2_aux, part2, rep_method, random_state, nx ) # format partitions over (C1, F1) and (C2, F2) if (F1 is None) and (F2 is None): CR1, list_R1, list_p1 = format_partitioned_graph( C1, p, part1, rep_indices1, nx=nx ) CR2, list_R2, list_p2 = format_partitioned_graph( C2, q, part2, rep_indices2, nx=nx ) MR = None else: if DF1 is None: DF1 = dist(F1, F1) DF2 = dist(F2, F2) CR1, list_R1, list_p1, FR1 = format_partitioned_graph( C1, p, part1, rep_indices1, F1, DF1, alpha, nx ) CR2, list_R2, list_p2, FR2 = format_partitioned_graph( C2, q, part2, rep_indices2, F2, DF2, alpha, nx ) MR = dist(FR1, FR2) # call to partitioned quantized fused gromov-wasserstein solver res = quantized_fused_gromov_wasserstein_partitioned( CR1, CR2, list_R1, list_R2, list_p1, list_p2, MR, alpha, build_OT=True, log=log, armijo=armijo, max_iter=max_iter, tol_rel=tol_rel, tol_abs=tol_abs, nx=nx, **kwargs, ) if log: T_global, Ts_local, T, log_ = res # compute the transport cost on structures constC, hC1, hC2 = init_matrix(C1, C2, p, q, "square_loss", nx) structure_cost = gwloss(constC, hC1, hC2, T, nx) if alpha != 1.0: M = dist(F1, F2) feature_cost = nx.sum(M * T) else: feature_cost = 0.0 log_["qFGW_dist"] = alpha * structure_cost + (1 - alpha) * feature_cost return T_global, Ts_local, T, log_ else: T_global, Ts_local, T = res return T_global, Ts_local, T def get_partition_and_representants_samples( X, npart, method="kmeans", random_state=0, nx=None ): r""" Compute `npart` partitions and representants over samples :math:`\mathbf{X} \in R^{n \times d}` using either a random or a kmeans algorithm. Parameters ---------- X : array-like, shape (n, d) Samples endowed with an euclidean geometry. npart : int, number of partitions smaller than the number of samples in :math:`\mathbf{X}`. method : str, optional. Default is 'kmeans'. Partitioning and representant selection algorithms to use among {'random', 'kmeans'}. 'random' for random sampling of points; 'kmeans' for k-means clustering using scikit-learn implementation where closest points to centroids are considered as representants. random_state: int, optional Random seed for the partitioning algorithm. nx : backend, optional POT backend. Returns ------- part : array-like, shape (npart,) Array of partition assignment for each node. rep_indices : list, shape (npart,) indices for representative node of each partition sorted according to partition identifiers. References ---------- .. [68] Chowdhury, S., Miller, D., & Needham, T. (2021). Quantized gromov-wasserstein. ECML PKDD 2021. Springer International Publishing. """ if nx is None: nx = get_backend(X) n = X.shape[0] X0 = X if npart >= n: warnings.warn( "Requested number of partitions higher than the number of nodes" "hence we enforce each node to be a partition.", stacklevel=2, ) part = nx.arange(n) rep_indices = nx.arange(n) elif npart == 1: random.seed(random_state) part = nx.zeros(n) rep_indices = [random.choice(nx.arange(n))] elif method == "random": # randomly partition the space random.seed(random_state) part = list_to_array(random.choices(np.arange(npart), k=X.shape[0])) part = nx.from_numpy(part, type_as=X0) # randomly select representant in each partition rep_indices = [] part_ids = nx.unique(part) for id_, part_id in enumerate(part_ids): indices = nx.where(part == part_id)[0] rep_indices.append(random.choice(indices)) elif method == "kmeans": X = nx.to_numpy(X0) km = KMeans(n_clusters=npart, random_state=random_state).fit(X) part = nx.from_numpy(km.labels_, type_as=X0) rep_indices = [] for part_id in range(npart): indices = nx.where(part == part_id)[0] dists = dist(X[indices], km.cluster_centers_[part_id][None, :]) best_idx = indices[dists.argmin()] rep_indices.append(best_idx) else: raise ValueError( f""" Unknown `method='{method}'`. Use one of: {"random", "kmeans"} """ ) return part, rep_indices def format_partitioned_samples(X, p, part, rep_indices, F=None, alpha=1.0, nx=None): r""" Format an attributed graph :math:`(\mathbf{D}(\mathbf{X}), \mathbf{F}, \mathbf{p})` with euclidean structure matrix :math:`(\mathbf{D}(\mathbf{X}) \in R^{n \times n}`, feature matrix :math:`(\mathbf{F} \in R^{n \times d}` and node relative importance :math:`(\mathbf{p} \in \Sigma_n`, into a partitioned attributed graph taking into account partitions and representants :math:`\mathcal{P} = \left\{(\mathbf{P_{i}}, \mathbf{r_{i}})\right\}_i`. Parameters ---------- X : array-like, shape (n, d) Structure matrix. p : array-like, shape (n,), Node distribution. part : array-like, shape (n,) Array of partition assignment for each node. rep_indices : list of array-like of ints, shape (npart,) indices for representative node of each partition sorted according to partition identifiers. F : array-like, shape (n, p), optional. (Default is None) Optional feature matrix aligned with the samples. alpha: float, optional. Default is 1. Trade-off parameter in :math:`]0, 1]` between structure and features. If `alpha = 1` features are ignored. This trade-off is taken into account into the outputted relations between nodes and representants. nx : backend, optional POT backend Returns ------- CR : array-like, shape (npart, npart) Structure matrix between partition representants. list_R : list of npart arrays, List of relations between a representant and nodes in its partition, for each partition. list_p : list of npart arrays, List of node distributions within each partition. FR : array-like, shape (npart, d), if `F != None`. Feature matrix of representants. References ---------- .. [68] Chowdhury, S., Miller, D., & Needham, T. (2021). Quantized gromov-wasserstein. ECML PKDD 2021. Springer International Publishing. """ if nx is None: arr = [X, p, part] if F is not None: arr.append(F) nx = get_backend(*arr) if alpha != 1.0: if F is None: raise ValueError( f""" `alpha == {alpha} != 1` but features information is not properly provided. """ ) XR = X[rep_indices, :] CR = dist(XR, XR) list_R, list_p = [], [] part_ids = nx.unique(part) for id_, part_id in enumerate(part_ids): indices = nx.where(part == part_id)[0] structure_R = dist(X[indices], X[rep_indices[id_]][None, :]) if alpha != 1: features_R = dist(F[indices], F[rep_indices[id_]][None, :]) else: features_R = 0.0 list_R.append(alpha * structure_R + (1 - alpha) * features_R) list_p.append(p[indices]) if F is None: return CR, list_R, list_p else: FR = F[rep_indices, :] return CR, list_R, list_p, FR def quantized_fused_gromov_wasserstein_samples( X1, X2, npart1, npart2, p=None, q=None, F1=None, F2=None, alpha=1.0, method="kmeans", log=False, armijo=False, max_iter=1e4, tol_rel=1e-9, tol_abs=1e-9, random_state=0, **kwargs, ): r""" Returns the quantized Fused Gromov-Wasserstein transport between samples endowed with their respective euclidean geometry :math:`(\mathbf{D}(\mathbf{X_1}), \mathbf{F_1}, \mathbf{p})` and :math:`(\mathbf{D}(\mathbf{X_1}), \mathbf{F_2}, \mathbf{q})`, whose samples are assigned to partitions and representants :math:`\mathcal{P_1} = \{(\mathbf{P_{1, i}}, \mathbf{r_{1, i}})\}_{i \leq npart1}` and :math:`\mathcal{P_2} = \{(\mathbf{P_{2, j}}, \mathbf{r_{2, j}})\}_{j \leq npart2}`. The function estimates the following optimization problem: .. math:: \mathbf{T}^* \in \mathop{\arg \min}_\mathbf{T} \quad \alpha \sum_{i,j,k,l} L(\mathbf{D}(\mathbf{X_1})_{i,k}, \mathbf{D}(\mathbf{X_2})_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} + (1-\alpha) \langle \mathbf{T}, \mathbf{D}(\mathbf{F_1}, \mathbf{F}_2) \rangle_F s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T}^T \mathbf{1} &= \mathbf{q} \mathbf{T} &\geq 0 \mathbf{T}_{|\mathbf{P_{1, i}}, \mathbf{P_{2, j}}} &= T^{g}_{ij} \mathbf{T}^{(i,j)} using a two-step strategy computing: i) a global alignment :math:`\mathbf{T}^{g}` between representants across joint structure and feature spaces; ii) local alignments :math:`\mathbf{T}^{(i, j)}` between partitions :math:`\mathbf{P_{1, i}}` and :math:`\mathbf{P_{2, j}}` seen as 1D measures. Where : - :math:`\mathbf{X_1}`: Samples in the source space - :math:`\mathbf{X_2}`: Samples in the target space - :math:`\mathbf{F_1}`: Feature matrix in the source space - :math:`\mathbf{F_2}`: Feature matrix in the target space - :math:`\mathbf{D}(\mathbf{F_1}, \mathbf{F_2})`: Pairwise euclidean distance matrix between features - :math:`\mathbf{p}`: distribution in the source space - :math:`\mathbf{q}`: distribution in the target space - :math:`L`: quadratic loss function to account for the misfit between the similarity matrices .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. .. note:: All computations in the conjugate gradient solver are done with numpy to limit memory overhead. Parameters ---------- X1 : array-like, shape (ns, ds) Samples in the source space. X2 : array-like, shape (nt, dt) Samples in the target space. npart1 : int, number of partition in the source space. npart2 : int, number of partition in the target space. p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. q : array-like, shape (nt,), optional Distribution in the target space. If let to its default value None, uniform distribution is taken. F1 : array-like, shape (ns, d), optional. Default is None. Feature matrix in the source space. F2 : array-like, shape (nt, d), optional. Default is None. Feature matrix in the target space alpha: float, optional. Default is 1. FGW trade-off parameter in :math:`]0, 1]` between structure and features. If `alpha = 1` features are ignored hence computing qGW, if `alpha=0` structures are ignored and we compute the quantized Wasserstein transport. method : str, optional. Default is 'kmeans'. Partitioning and representant selection algorithms to use among {'random', 'kmeans', 'kmeans_fused'}. If `part_method == 'kmeans_fused'`, kmeans is performed on augmented samples :math:`[\alpha \mathbf{X}; (1 - \alpha) \mathbf{F}]`. verbose : bool, optional Print information along iterations log : bool, optional record log if True armijo : bool, optional If True the step of the line-search is found via an armijo research. Else closed form is used. If there are convergence issues use False. max_iter : int, optional Max number of iterations tol_rel : float, optional Stop threshold on relative error (>0) tol_abs : float, optional Stop threshold on absolute error (>0) random_state: int, optional Random seed for the partitioning algorithm **kwargs : dict parameters can be directly passed to the ot.optim.cg solver Returns ------- T_global: array-like, shape (`npart1`, `npart2`) Fused Gromov-Wasserstein alignment :math:`\mathbf{T}^{g}` between representants. Ts_local: dict of local OT matrices. Dictionary with keys :math:`(i, j)` corresponding to 1D OT between :math:`\mathbf{P_{1, i}}` and :math:`\mathbf{P_{2, j}}` if :math:`T^{g}_{ij} \neq 0`. T: array-like, shape `(ns, nt)` Coupling between the two spaces. log : dict Convergence information for inner problems and qGW loss. References ---------- .. [68] Chowdhury, S., Miller, D., & Needham, T. (2021). Quantized gromov-wasserstein. ECML PKDD 2021. Springer International Publishing. """ if (method in ["kmeans", "kmeans_fused"]) and (not sklearn_import): warnings.warn( f""" Scikit-learn is not installed, so method={method} cannot be used and is set to `random` default methods. Consider installing Scikit-learn to fix this. """ ) method = "random" if ("fused" in method) and ((F1 is None) or (F2 is None)): raise ValueError( f""" `method='{method}'` requires feature matrices which are not provided as inputs. """ ) arr = [X1, X2] if p is not None: arr.append(list_to_array(p)) else: p = unif(X1.shape[0], type_as=X1) if q is not None: arr.append(list_to_array(q)) else: q = unif(X2.shape[0], type_as=X1) if F1 is not None: arr.append(F1) if F2 is not None: arr.append(F1) nx = get_backend(*arr) # compute attributed partitions and representants if ("fused" in method) and (alpha != 1.0): X1_new = nx.concatenate([alpha * X1, (1 - alpha) * F1], axis=1) X2_new = nx.concatenate([alpha * X2, (1 - alpha) * F2], axis=1) method_ = method[:-6] else: X1_new, X2_new = X1, X2 method_ = method part1, rep_indices1 = get_partition_and_representants_samples( X1_new, npart1, method_, random_state, nx ) part2, rep_indices2 = get_partition_and_representants_samples( X2_new, npart2, method_, random_state, nx ) # format partitions over (C1, F1) and (C2, F2) if (F1 is None) and (F2 is None): CR1, list_R1, list_p1 = format_partitioned_samples( X1, p, part1, rep_indices1, nx=nx ) CR2, list_R2, list_p2 = format_partitioned_samples( X2, q, part2, rep_indices2, nx=nx ) MR = None else: CR1, list_R1, list_p1, FR1 = format_partitioned_samples( X1, p, part1, rep_indices1, F1, alpha, nx ) CR2, list_R2, list_p2, FR2 = format_partitioned_samples( X2, q, part2, rep_indices2, F2, alpha, nx ) MR = dist(FR1, FR2) # call to partitioned quantized fused gromov-wasserstein solver res = quantized_fused_gromov_wasserstein_partitioned( CR1, CR2, list_R1, list_R2, list_p1, list_p2, MR, alpha, build_OT=True, log=log, armijo=armijo, max_iter=max_iter, tol_rel=tol_rel, tol_abs=tol_abs, nx=nx, **kwargs, ) if log: T_global, Ts_local, T, log_ = res C1 = dist(X1, X1) C2 = dist(X2, X2) # compute the transport cost on structures constC, hC1, hC2 = init_matrix(C1, C2, p, q, "square_loss", nx) structure_cost = gwloss(constC, hC1, hC2, T, nx) if alpha != 1.0: M = dist(F1, F2) feature_cost = nx.sum(M * T) else: feature_cost = 0.0 log_["qFGW_dist"] = alpha * structure_cost + (1 - alpha) * feature_cost return T_global, Ts_local, T, log_ else: T_global, Ts_local, T = res return T_global, Ts_local, T python-pot-0.9.7+dfsg/ot/gromov/_semirelaxed.py000066400000000000000000002213601525521106200215400ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Semi-relaxed Gromov-Wasserstein and Fused-Gromov-Wasserstein solvers. """ # Author: Rémi Flamary # Cédric Vincent-Cuaz # # License: MIT License import numpy as np from ..utils import list_to_array, unif, dist, UndefinedParameter from ..optim import semirelaxed_cg, solve_1d_linesearch_quad from ..backend import get_backend from ._utils import ( init_matrix_semirelaxed, gwloss, gwggrad, update_barycenter_structure, update_barycenter_feature, semirelaxed_init_plan, ) try: from sklearn.cluster import KMeans sklearn_import = True except ImportError: sklearn_import = False def semirelaxed_gromov_wasserstein( C1, C2, p=None, loss_fun="square_loss", symmetric=None, log=False, G0=None, max_iter=1e4, tol_rel=1e-9, tol_abs=1e-9, random_state=0, **kwargs, ): r""" Returns the semi-relaxed Gromov-Wasserstein divergence transport from :math:`(\mathbf{C_1}, \mathbf{p})` to :math:`\mathbf{C_2}` (see [48]). The function solves the following optimization problem using Conditional Gradient: .. math:: \mathbf{T}^* \in \mathop{\arg \min}_{\mathbf{T}} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - `L`: loss function to account for the misfit between the similarity matrices .. note:: This function is backend-compatible and will work on arrays from all compatible backends. However all the steps in the conditional gradient are not differentiable. Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. loss_fun : str loss function used for the solver either 'square_loss' or 'kl_loss'. symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). verbose : bool, optional Print information along iterations log : bool, optional record log if True G0: array-like of shape (ns,nt) or string, optional If `G0=None` the initial transport plan of the solver is :math:`\mathbf{p} \frac{\mathbf{1}_{nt}}{nt}^\top`. If G0 is a tensor it must satisfy marginal constraints and will be used as initial transport of the solver. if G0 is a string it will be interpreted as a method for :func:`ot.gromov.semirelaxed_init_plan` taking values in "product", "random_product", "random", "fluid", "fluid_soft", "spectral", "spectral_soft", "kmeans", "kmeans_soft". max_iter : int, optional Max number of iterations tol_rel : float, optional Stop threshold on relative error (>0) tol_abs : float, optional Stop threshold on absolute error (>0) random_state: int, optional Random seed used in stochastic initialization methods. **kwargs : dict parameters can be directly passed to the ot.optim.cg solver Returns ------- T : array-like, shape (`ns`, `nt`) Coupling between the two spaces that minimizes: :math:`\sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l}` log : dict Convergence information and loss. References ---------- .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2022. .. [62] H. Van Assel, C. Vincent-Cuaz, T. Vayer, R. Flamary, N. Courty. "Interpolating between Clustering and Dimensionality Reduction with Gromov-Wasserstein". NeurIPS 2023 Workshop OTML. """ arr = [C1, C2] if p is not None: arr.append(list_to_array(p)) else: p = unif(C1.shape[0], type_as=C1) if (G0 is not None) and (not isinstance(G0, str)): arr.append(G0) nx = get_backend(*arr) if symmetric is None: symmetric = nx.allclose(C1, C1.T, atol=1e-10) and nx.allclose( C2, C2.T, atol=1e-10 ) if G0 is None: q = unif(C2.shape[0], type_as=p) G0 = nx.outer(p, q) elif isinstance(G0, str): G0 = semirelaxed_init_plan( C1, C2, p, method=G0, random_state=random_state, nx=nx ) q = nx.sum(G0, 0) else: q = nx.sum(G0, 0) # Check first marginal of G0 assert nx.allclose(nx.sum(G0, 1), p, atol=1e-08) constC, hC1, hC2, fC2t = init_matrix_semirelaxed(C1, C2, p, loss_fun, nx) ones_p = nx.ones(p.shape[0], type_as=p) def f(G): qG = nx.sum(G, 0) marginal_product = nx.outer(ones_p, nx.dot(qG, fC2t)) return gwloss(constC + marginal_product, hC1, hC2, G, nx) if symmetric: def df(G): qG = nx.sum(G, 0) marginal_product = nx.outer(ones_p, nx.dot(qG, fC2t)) return gwggrad(constC + marginal_product, hC1, hC2, G, nx) else: constCt, hC1t, hC2t, fC2 = init_matrix_semirelaxed(C1.T, C2.T, p, loss_fun, nx) def df(G): qG = nx.sum(G, 0) marginal_product_1 = nx.outer(ones_p, nx.dot(qG, fC2t)) marginal_product_2 = nx.outer(ones_p, nx.dot(qG, fC2)) return 0.5 * ( gwggrad(constC + marginal_product_1, hC1, hC2, G, nx) + gwggrad(constCt + marginal_product_2, hC1t, hC2t, G, nx) ) def line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs): return solve_semirelaxed_gromov_linesearch( G, deltaG, cost_G, hC1, hC2, ones_p, M=0.0, reg=1.0, fC2t=fC2t, nx=nx, **kwargs, ) if log: res, log = semirelaxed_cg( p, q, 0.0, 1.0, f, df, G0, line_search, log=True, numItermax=max_iter, stopThr=tol_rel, stopThr2=tol_abs, **kwargs, ) log["srgw_dist"] = log["loss"][-1] return res, log else: return semirelaxed_cg( p, q, 0.0, 1.0, f, df, G0, line_search, log=False, numItermax=max_iter, stopThr=tol_rel, stopThr2=tol_abs, **kwargs, ) def semirelaxed_gromov_wasserstein2( C1, C2, p=None, loss_fun="square_loss", symmetric=None, log=False, G0=None, max_iter=1e4, tol_rel=1e-9, tol_abs=1e-9, random_state=0, **kwargs, ): r""" Returns the semi-relaxed Gromov-Wasserstein divergence from :math:`(\mathbf{C_1}, \mathbf{p})` to :math:`\mathbf{C_2}` (see [48]). The function solves the following optimization problem using Conditional Gradient: .. math:: \text{srGW} = \min_{\mathbf{T}} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - `L`: loss function to account for the misfit between the similarity matrices Note that when using backends, this loss function is differentiable wrt the matrices (C1, C2) but not yet for the weights p. .. note:: This function is backend-compatible and will work on arrays from all compatible backends. However all the steps in the conditional gradient are not differentiable. Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. loss_fun : str loss function used for the solver either 'square_loss' or 'kl_loss'. symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). verbose : bool, optional Print information along iterations log : bool, optional record log if True G0: array-like of shape (ns,nt) or string, optional If `G0=None` the initial transport plan of the solver is :math:`\mathbf{p} \frac{\mathbf{1}_{nt}}{nt}^\top`. If G0 is a tensor it must satisfy marginal constraints and will be used as initial transport of the solver. if G0 is a string it will be interpreted as a method for :func:`ot.gromov.semirelaxed_init_plan` taking values in "product", "random_product", "random", "fluid", "fluid_soft", "spectral", "spectral_soft", "kmeans", "kmeans_soft". max_iter : int, optional Max number of iterations tol_rel : float, optional Stop threshold on relative error (>0) tol_abs : float, optional Stop threshold on absolute error (>0) random_state: int, optional Random seed used in stochastic initialization methods. **kwargs : dict parameters can be directly passed to the ot.optim.cg solver Returns ------- srgw : float Semi-relaxed Gromov-Wasserstein divergence log : dict convergence information and Coupling matrix References ---------- .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2022. .. [62] H. Van Assel, C. Vincent-Cuaz, T. Vayer, R. Flamary, N. Courty. "Interpolating between Clustering and Dimensionality Reduction with Gromov-Wasserstein". NeurIPS 2023 Workshop OTML. """ # partial get_backend as the full one will be handled in gromov_wasserstein nx = get_backend(C1, C2) # init marginals if set as None if p is None: p = unif(C1.shape[0], type_as=C1) T, log_srgw = semirelaxed_gromov_wasserstein( C1, C2, p, loss_fun, symmetric, log=True, G0=G0, max_iter=max_iter, tol_rel=tol_rel, tol_abs=tol_abs, random_state=random_state, **kwargs, ) q = nx.sum(T, 0) log_srgw["T"] = T srgw = log_srgw["srgw_dist"] if loss_fun == "square_loss": gC1 = 2 * C1 * nx.outer(p, p) - 2 * nx.dot(T, nx.dot(C2, T.T)) gC2 = 2 * C2 * nx.outer(q, q) - 2 * nx.dot(T.T, nx.dot(C1, T)) elif loss_fun == "kl_loss": gC1 = nx.log(C1 + 1e-15) * nx.outer(p, p) - nx.dot( T, nx.dot(nx.log(C2 + 1e-15), T.T) ) gC2 = -nx.dot(T.T, nx.dot(C1, T)) / (C2 + 1e-15) + nx.outer(q, q) srgw = nx.set_gradients(srgw, (C1, C2), (gC1, gC2)) if log: return srgw, log_srgw else: return srgw def semirelaxed_fused_gromov_wasserstein( M, C1, C2, p=None, loss_fun="square_loss", symmetric=None, alpha=0.5, G0=None, log=False, max_iter=1e4, tol_rel=1e-9, tol_abs=1e-9, random_state=0, **kwargs, ): r""" Computes the semi-relaxed Fused Gromov-Wasserstein transport between two graphs (see [48]). .. math:: \mathbf{T}^* \in \mathop{\arg \min}_{\mathbf{T}} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) T_{i,j} T_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T} &\geq 0 where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`\mathbf{p}` source weights (sum to 1) - `L` is a loss function to account for the misfit between the similarity matrices .. note:: This function is backend-compatible and will work on arrays from all compatible backends. However all the steps in the conditional gradient are not differentiable. The algorithm used for solving the problem is conditional gradient as discussed in :ref:`[48] ` Parameters ---------- M : array-like, shape (ns, nt) Metric cost matrix between features across domains C1 : array-like, shape (ns, ns) Metric cost matrix representative of the structure in the source space C2 : array-like, shape (nt, nt) Metric cost matrix representative of the structure in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. loss_fun : str loss function used for the solver either 'square_loss' or 'kl_loss'. symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). alpha : float, optional Trade-off parameter (0 < alpha < 1) G0: array-like of shape (ns,nt) or string, optional If `G0=None` the initial transport plan of the solver is :math:`\mathbf{p} \frac{\mathbf{1}_{nt}}{nt}^\top`. If G0 is a tensor it must satisfy marginal constraints and will be used as initial transport of the solver. if G0 is a string it will be interpreted as a method for :func:`ot.gromov.semirelaxed_init_plan` taking values in "product", "random_product", "random", "fluid", "fluid_soft", "spectral", "spectral_soft", "kmeans", "kmeans_soft". log : bool, optional record log if True max_iter : int, optional Max number of iterations tol_rel : float, optional Stop threshold on relative error (>0) tol_abs : float, optional Stop threshold on absolute error (>0) random_state: int, optional Random seed used in stochastic initialization methods. **kwargs : dict parameters can be directly passed to the ot.optim.cg solver Returns ------- gamma : array-like, shape (`ns`, `nt`) Optimal transportation matrix for the given parameters. log : dict Log dictionary return only if log==True in parameters. .. _references-semirelaxed-fused-gromov-wasserstein: References ---------- .. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs", International Conference on Machine Learning (ICML). 2019. .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2022. .. [62] H. Van Assel, C. Vincent-Cuaz, T. Vayer, R. Flamary, N. Courty. "Interpolating between Clustering and Dimensionality Reduction with Gromov-Wasserstein". NeurIPS 2023 Workshop OTML. """ arr = [M, C1, C2] if p is not None: arr.append(list_to_array(p)) else: p = unif(C1.shape[0], type_as=C1) if (G0 is not None) and (not isinstance(G0, str)): arr.append(G0) nx = get_backend(*arr) if symmetric is None: symmetric = nx.allclose(C1, C1.T, atol=1e-10) and nx.allclose( C2, C2.T, atol=1e-10 ) if G0 is None: q = unif(C2.shape[0], type_as=p) G0 = nx.outer(p, q) elif isinstance(G0, str): G0 = semirelaxed_init_plan( C1, C2, p, M, alpha, G0, random_state=random_state, nx=nx ) q = nx.sum(G0, 0) else: q = nx.sum(G0, 0) # Check first marginal of G0 assert nx.allclose(nx.sum(G0, 1), p, atol=1e-08) constC, hC1, hC2, fC2t = init_matrix_semirelaxed(C1, C2, p, loss_fun, nx) ones_p = nx.ones(p.shape[0], type_as=p) def f(G): qG = nx.sum(G, 0) marginal_product = nx.outer(ones_p, nx.dot(qG, fC2t)) return gwloss(constC + marginal_product, hC1, hC2, G, nx) if symmetric: def df(G): qG = nx.sum(G, 0) marginal_product = nx.outer(ones_p, nx.dot(qG, fC2t)) return gwggrad(constC + marginal_product, hC1, hC2, G, nx) else: constCt, hC1t, hC2t, fC2 = init_matrix_semirelaxed(C1.T, C2.T, p, loss_fun, nx) def df(G): qG = nx.sum(G, 0) marginal_product_1 = nx.outer(ones_p, nx.dot(qG, fC2t)) marginal_product_2 = nx.outer(ones_p, nx.dot(qG, fC2)) return 0.5 * ( gwggrad(constC + marginal_product_1, hC1, hC2, G, nx) + gwggrad(constCt + marginal_product_2, hC1t, hC2t, G, nx) ) def line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs): return solve_semirelaxed_gromov_linesearch( G, deltaG, cost_G, hC1, hC2, ones_p, M=(1 - alpha) * M, reg=alpha, fC2t=fC2t, nx=nx, **kwargs, ) if log: res, log = semirelaxed_cg( p, q, (1 - alpha) * M, alpha, f, df, G0, line_search, log=True, numItermax=max_iter, stopThr=tol_rel, stopThr2=tol_abs, **kwargs, ) log["srfgw_dist"] = log["loss"][-1] return res, log else: return semirelaxed_cg( p, q, (1 - alpha) * M, alpha, f, df, G0, line_search, log=False, numItermax=max_iter, stopThr=tol_rel, stopThr2=tol_abs, **kwargs, ) def semirelaxed_fused_gromov_wasserstein2( M, C1, C2, p=None, loss_fun="square_loss", symmetric=None, alpha=0.5, G0=None, log=False, max_iter=1e4, tol_rel=1e-9, tol_abs=1e-9, random_state=0, **kwargs, ): r""" Computes the semi-relaxed FGW divergence between two graphs (see [48]). .. math:: \mathbf{srFGW}_{\alpha} = \min_{\mathbf{T}} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) T_{i,j} T_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T} &\geq 0 where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`\mathbf{p}` source weights (sum to 1) - `L` is a loss function to account for the misfit between the similarity matrices The algorithm used for solving the problem is conditional gradient as discussed in :ref:`[48] ` Note that when using backends, this loss function is differentiable wrt the matrices (C1, C2) but not yet for the weights p. .. note:: This function is backend-compatible and will work on arrays from all compatible backends. However all the steps in the conditional gradient are not differentiable. Parameters ---------- M : array-like, shape (ns, nt) Metric cost matrix between features across domains C1 : array-like, shape (ns, ns) Metric cost matrix representative of the structure in the source space. C2 : array-like, shape (nt, nt) Metric cost matrix representative of the structure in the target space. p : array-like, shape (ns,) Distribution in the source space. If let to its default value None, uniform distribution is taken. loss_fun : str, optional loss function used for the solver either 'square_loss' or 'kl_loss'. symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). alpha : float, optional Trade-off parameter (0 < alpha < 1) G0: array-like of shape (ns,nt) or string, optional If `G0=None` the initial transport plan of the solver is :math:`\mathbf{p} \frac{\mathbf{1}_{nt}}{nt}^\top`. If G0 is a tensor it must satisfy marginal constraints and will be used as initial transport of the solver. if G0 is a string it will be interpreted as a method for :func:`ot.gromov.semirelaxed_init_plan` taking values in "product", "random_product", "random", "fluid", "fluid_soft", "spectral", "spectral_soft", "kmeans", "kmeans_soft". log : bool, optional Record log if True. max_iter : int, optional Max number of iterations tol_rel : float, optional Stop threshold on relative error (>0) tol_abs : float, optional Stop threshold on absolute error (>0) random_state: int, optional Random seed used in stochastic initialization methods. **kwargs : dict Parameters can be directly passed to the ot.optim.cg solver. Returns ------- srfgw-divergence : float Semi-relaxed Fused Gromov-Wasserstein divergence for the given parameters. log : dict Log dictionary return only if log==True in parameters. .. _references-semirelaxed-fused-gromov-wasserstein2: References ---------- .. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs", International Conference on Machine Learning (ICML). 2019. .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2022. .. [62] H. Van Assel, C. Vincent-Cuaz, T. Vayer, R. Flamary, N. Courty. "Interpolating between Clustering and Dimensionality Reduction with Gromov-Wasserstein". NeurIPS 2023 Workshop OTML. """ # partial get_backend as the full one will be handled in gromov_wasserstein nx = get_backend(C1, C2) # init marginals if set as None if p is None: p = unif(C1.shape[0], type_as=C1) T, log_fgw = semirelaxed_fused_gromov_wasserstein( M, C1, C2, p, loss_fun, symmetric, alpha, G0, log=True, max_iter=max_iter, tol_rel=tol_rel, tol_abs=tol_abs, random_state=random_state, **kwargs, ) q = nx.sum(T, 0) srfgw_dist = log_fgw["srfgw_dist"] log_fgw["T"] = T log_fgw["lin_loss"] = nx.sum(M * T) * (1 - alpha) log_fgw["quad_loss"] = srfgw_dist - log_fgw["lin_loss"] if loss_fun == "square_loss": gC1 = 2 * C1 * nx.outer(p, p) - 2 * nx.dot(T, nx.dot(C2, T.T)) gC2 = 2 * C2 * nx.outer(q, q) - 2 * nx.dot(T.T, nx.dot(C1, T)) elif loss_fun == "kl_loss": gC1 = nx.log(C1 + 1e-15) * nx.outer(p, p) - nx.dot( T, nx.dot(nx.log(C2 + 1e-15), T.T) ) gC2 = -nx.dot(T.T, nx.dot(C1, T)) / (C2 + 1e-15) + nx.outer(q, q) if isinstance(alpha, int) or isinstance(alpha, float): srfgw_dist = nx.set_gradients( srfgw_dist, (C1, C2, M), (alpha * gC1, alpha * gC2, (1 - alpha) * T) ) else: lin_term = nx.sum(T * M) srgw_term = (srfgw_dist - (1 - alpha) * lin_term) / alpha srfgw_dist = nx.set_gradients( srfgw_dist, (C1, C2, M, alpha), (alpha * gC1, alpha * gC2, (1 - alpha) * T, srgw_term - lin_term), ) if log: return srfgw_dist, log_fgw else: return srfgw_dist def solve_semirelaxed_gromov_linesearch( G, deltaG, cost_G, C1, C2, ones_p, M, reg, fC2t=None, alpha_min=None, alpha_max=None, nx=None, **kwargs, ): """ Solve the linesearch in the Conditional Gradient iterations for the semi-relaxed Gromov-Wasserstein divergence. Parameters ---------- G : array-like, shape(ns,nt) The transport map at a given iteration of the FW deltaG : array-like (ns,nt) Difference between the optimal map found by linearization in the FW algorithm and the value at a given iteration cost_G : float Value of the cost at `G` C1 : array-like (ns,ns), optional Transformed Structure matrix in the source domain. Note that for the 'square_loss' and 'kl_loss', we provide hC1 from ot.gromov.init_matrix_semirelaxed C2 : array-like (nt,nt), optional Transformed Structure matrix in the source domain. Note that for the 'square_loss' and 'kl_loss', we provide hC2 from ot.gromov.init_matrix_semirelaxed ones_p: array-like (ns,1) Array of ones of size ns M : array-like (ns,nt) Cost matrix between the features. reg : float Regularization parameter. fC2t: array-like (nt,nt), optional Transformed Structure matrix in the source domain. Note that for the 'square_loss' and 'kl_loss', we provide fC2t from ot.gromov.init_matrix_semirelaxed. If fC2t is not provided, it is by default fC2t corresponding to the 'square_loss'. alpha_min : float, optional Minimum value for alpha alpha_max : float, optional Maximum value for alpha nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ------- alpha : float The optimal step size of the FW fc : int nb of function call. Useless here cost_G : float The value of the cost for the next iteration References ---------- .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2021. .. [62] H. Van Assel, C. Vincent-Cuaz, T. Vayer, R. Flamary, N. Courty. "Interpolating between Clustering and Dimensionality Reduction with Gromov-Wasserstein". NeurIPS 2023 Workshop OTML. """ if nx is None: if isinstance(M, int) or isinstance(M, float): nx = get_backend(G, deltaG, C1, C2) else: nx = get_backend(G, deltaG, C1, C2, M) qG, qdeltaG = nx.sum(G, 0), nx.sum(deltaG, 0) dot = nx.dot(nx.dot(C1, deltaG), C2.T) if fC2t is None: fC2t = C2.T**2 dot_qG = nx.dot(nx.outer(ones_p, qG), fC2t) dot_qdeltaG = nx.dot(nx.outer(ones_p, qdeltaG), fC2t) a = reg * nx.sum((dot_qdeltaG - dot) * deltaG) b = nx.sum(M * deltaG) + reg * ( nx.sum((dot_qdeltaG - dot) * G) + nx.sum((dot_qG - nx.dot(nx.dot(C1, G), C2.T)) * deltaG) ) alpha = solve_1d_linesearch_quad(a, b) if alpha_min is not None or alpha_max is not None: alpha = np.clip(alpha, alpha_min, alpha_max) # the new cost can be deduced from the line search quadratic function cost_G = cost_G + a * (alpha**2) + b * alpha return alpha, 1, cost_G def entropic_semirelaxed_gromov_wasserstein( C1, C2, p=None, loss_fun="square_loss", epsilon=0.1, symmetric=None, G0=None, max_iter=1e4, tol=1e-9, log=False, verbose=False, random_state=0, ): r""" Returns the entropic-regularized semi-relaxed gromov-wasserstein divergence transport plan from :math:`(\mathbf{C_1}, \mathbf{p})` to :math:`\mathbf{C_2}` estimated using a Mirror Descent algorithm following the KL geometry. The function solves the following optimization problem: .. math:: \mathbf{T}^* \in \mathop{\arg \min}_\mathbf{T} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - `L`: loss function to account for the misfit between the similarity matrices .. note:: This function is backend-compatible and will work on arrays from all compatible backends. However all the steps in the conditional gradient are not differentiable. Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. loss_fun : str loss function used for the solver either 'square_loss' or 'kl_loss'. epsilon : float Regularization term >0 symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). verbose : bool, optional Print information along iterations G0: array-like of shape (ns,nt) or string, optional If `G0=None` the initial transport plan of the solver is :math:`\mathbf{p} \frac{\mathbf{1}_{nt}}{nt}^\top`. If G0 is a tensor it must satisfy marginal constraints and will be used as initial transport of the solver. if G0 is a string it will be interpreted as a method for :func:`ot.gromov.semirelaxed_init_plan` taking values in "product", "random_product", "random", "fluid", "fluid_soft", "spectral", "spectral_soft", "kmeans", "kmeans_soft". max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on error computed on transport plans log : bool, optional record log if True verbose : bool, optional Print information along iterations random_state: int, optional Random seed used in stochastic initialization methods. Returns ------- G : array-like, shape (`ns`, `nt`) Coupling between the two spaces that minimizes: :math:`\sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l}` log : dict Convergence information and loss. References ---------- .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2022. """ arr = [C1, C2] if p is not None: arr.append(list_to_array(p)) else: p = unif(C1.shape[0], type_as=C1) if (G0 is not None) and (not isinstance(G0, str)): arr.append(G0) nx = get_backend(*arr) if symmetric is None: symmetric = nx.allclose(C1, C1.T, atol=1e-10) and nx.allclose( C2, C2.T, atol=1e-10 ) if G0 is None: q = unif(C2.shape[0], type_as=p) G0 = nx.outer(p, q) elif isinstance(G0, str): G0 = semirelaxed_init_plan( C1, C2, p, method=G0, random_state=random_state, nx=nx ) q = nx.sum(G0, 0) else: q = nx.sum(G0, 0) # Check first marginal of G0 assert nx.allclose(nx.sum(G0, 1), p, atol=1e-08) constC, hC1, hC2, fC2t = init_matrix_semirelaxed(C1, C2, p, loss_fun, nx) ones_p = nx.ones(p.shape[0], type_as=p) if symmetric: def df(G): qG = nx.sum(G, 0) marginal_product = nx.outer(ones_p, nx.dot(qG, fC2t)) return gwggrad(constC + marginal_product, hC1, hC2, G, nx) else: constCt, hC1t, hC2t, fC2 = init_matrix_semirelaxed(C1.T, C2.T, p, loss_fun, nx) def df(G): qG = nx.sum(G, 0) marginal_product_1 = nx.outer(ones_p, nx.dot(qG, fC2t)) marginal_product_2 = nx.outer(ones_p, nx.dot(qG, fC2)) return 0.5 * ( gwggrad(constC + marginal_product_1, hC1, hC2, G, nx) + gwggrad(constCt + marginal_product_2, hC1t, hC2t, G, nx) ) cpt = 0 err = 1e15 G = G0 if log: log = {"err": []} while err > tol and cpt < max_iter: Gprev = G # compute the kernel K = G * nx.exp(-df(G) / epsilon) scaling = p / nx.sum(K, 1) G = nx.reshape(scaling, (-1, 1)) * K if cpt % 10 == 0: # we can speed up the process by checking for the error only all # the 10th iterations err = nx.norm(G - Gprev) if log: log["err"].append(err) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err)) cpt += 1 if log: qG = nx.sum(G, 0) marginal_product = nx.outer(ones_p, nx.dot(qG, fC2t)) log["srgw_dist"] = gwloss(constC + marginal_product, hC1, hC2, G, nx) return G, log else: return G def entropic_semirelaxed_gromov_wasserstein2( C1, C2, p=None, loss_fun="square_loss", epsilon=0.1, symmetric=None, G0=None, max_iter=1e4, tol=1e-9, log=False, verbose=False, random_state=0, **kwargs, ): r""" Returns the entropic-regularized semi-relaxed gromov-wasserstein divergence from :math:`(\mathbf{C_1}, \mathbf{p})` to :math:`\mathbf{C_2}` estimated using a Mirror Descent algorithm following the KL geometry. The function solves the following optimization problem: .. math:: \mathbf{srGW} = \min_{\mathbf{T}} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T} &\geq 0 Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}`: distribution in the source space - `L`: loss function to account for the misfit between the similarity matrices Note that when using backends, this loss function is differentiable wrt the matrices (C1, C2) but not yet for the weights p. .. note:: This function is backend-compatible and will work on arrays from all compatible backends. However all the steps in the conditional gradient are not differentiable. Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. loss_fun : str loss function used for the solver either 'square_loss' or 'kl_loss'. epsilon : float Regularization term >0 symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). verbose : bool, optional Print information along iterations G0: array-like of shape (ns,nt) or string, optional If `G0=None` the initial transport plan of the solver is :math:`\mathbf{p} \frac{\mathbf{1}_{nt}}{nt}^\top`. If G0 is a tensor it must satisfy marginal constraints and will be used as initial transport of the solver. if G0 is a string it will be interpreted as a method for :func:`ot.gromov.semirelaxed_init_plan` taking values in "product", "random_product", "random", "fluid", "fluid_soft", "spectral", "spectral_soft", "kmeans", "kmeans_soft". max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on error computed on transport plans log : bool, optional record log if True verbose : bool, optional Print information along iterations random_state: int, optional Random seed used in stochastic initialization methods. Returns ------- srgw : float Semi-relaxed Gromov-Wasserstein divergence log : dict convergence information and Coupling matrix References ---------- .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2022. """ T, log_srgw = entropic_semirelaxed_gromov_wasserstein( C1, C2, p, loss_fun, epsilon, symmetric, G0, max_iter, tol, log=True, verbose=verbose, random_state=random_state, ) log_srgw["T"] = T if log: return log_srgw["srgw_dist"], log_srgw else: return log_srgw["srgw_dist"] def entropic_semirelaxed_fused_gromov_wasserstein( M, C1, C2, p=None, loss_fun="square_loss", symmetric=None, epsilon=0.1, alpha=0.5, G0=None, max_iter=1e4, tol=1e-9, log=False, verbose=False, random_state=0, ): r""" Computes the entropic-regularized semi-relaxed FGW transport between two graphs (see :ref:`[48] `) estimated using a Mirror Descent algorithm following the KL geometry. .. math:: \mathbf{T}^* \in \mathop{\arg \min}_{\mathbf{T}} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T} &\geq 0 where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix between features - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}` source weights (sum to 1) - `L` is a loss function to account for the misfit between the similarity matrices .. note:: This function is backend-compatible and will work on arrays from all compatible backends. However all the steps in the conditional gradient are not differentiable. The algorithm used for solving the problem is conditional gradient as discussed in :ref:`[48] ` Parameters ---------- M : array-like, shape (ns, nt) Metric cost matrix between features across domains C1 : array-like, shape (ns, ns) Metric cost matrix representative of the structure in the source space C2 : array-like, shape (nt, nt) Metric cost matrix representative of the structure in the target space p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. loss_fun : str loss function used for the solver either 'square_loss' or 'kl_loss'. epsilon : float Regularization term >0 symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). alpha : float, optional Trade-off parameter (0 < alpha < 1) G0: array-like of shape (ns,nt) or string, optional If `G0=None` the initial transport plan of the solver is :math:`\mathbf{p} \frac{\mathbf{1}_{nt}}{nt}^\top`. If G0 is a tensor it must satisfy marginal constraints and will be used as initial transport of the solver. if G0 is a string it will be interpreted as a method for :func:`ot.gromov.semirelaxed_init_plan` taking values in "product", "random_product", "random", "fluid", "fluid_soft", "spectral", "spectral_soft", "kmeans", "kmeans_soft". max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on error computed on transport plans log : bool, optional record log if True verbose : bool, optional Print information along iterations random_state: int, optional Random seed used in stochastic initialization methods. Returns ------- G : array-like, shape (`ns`, `nt`) Optimal transportation matrix for the given parameters. log : dict Log dictionary return only if log==True in parameters. .. _references-semirelaxed-fused-gromov-wasserstein: References ---------- .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2022. """ arr = [M, C1, C2] if p is not None: arr.append(list_to_array(p)) else: p = unif(C1.shape[0], type_as=C1) if (G0 is not None) and (not isinstance(G0, str)): arr.append(G0) nx = get_backend(*arr) if symmetric is None: symmetric = nx.allclose(C1, C1.T, atol=1e-10) and nx.allclose( C2, C2.T, atol=1e-10 ) if G0 is None: q = unif(C2.shape[0], type_as=p) G0 = nx.outer(p, q) elif isinstance(G0, str): G0 = semirelaxed_init_plan( C1, C2, p, M, alpha, G0, random_state=random_state, nx=nx ) q = nx.sum(G0, 0) else: q = nx.sum(G0, 0) # Check first marginal of G0 assert nx.allclose(nx.sum(G0, 1), p, atol=1e-08) constC, hC1, hC2, fC2t = init_matrix_semirelaxed(C1, C2, p, loss_fun, nx) ones_p = nx.ones(p.shape[0], type_as=p) dM = (1 - alpha) * M if symmetric: def df(G): qG = nx.sum(G, 0) marginal_product = nx.outer(ones_p, nx.dot(qG, fC2t)) return alpha * gwggrad(constC + marginal_product, hC1, hC2, G, nx) + dM else: constCt, hC1t, hC2t, fC2 = init_matrix_semirelaxed(C1.T, C2.T, p, loss_fun, nx) def df(G): qG = nx.sum(G, 0) marginal_product_1 = nx.outer(ones_p, nx.dot(qG, fC2t)) marginal_product_2 = nx.outer(ones_p, nx.dot(qG, fC2)) return ( 0.5 * alpha * ( gwggrad(constC + marginal_product_1, hC1, hC2, G, nx) + gwggrad(constCt + marginal_product_2, hC1t, hC2t, G, nx) ) + dM ) cpt = 0 err = 1e15 G = G0 if log: log = {"err": []} while err > tol and cpt < max_iter: Gprev = G # compute the kernel K = G * nx.exp(-df(G) / epsilon) scaling = p / nx.sum(K, 1) G = nx.reshape(scaling, (-1, 1)) * K if cpt % 10 == 0: # we can speed up the process by checking for the error only all # the 10th iterations err = nx.norm(G - Gprev) if log: log["err"].append(err) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err)) cpt += 1 if log: qG = nx.sum(G, 0) marginal_product = nx.outer(ones_p, nx.dot(qG, fC2t)) log["lin_loss"] = nx.sum(M * G) * (1 - alpha) log["quad_loss"] = alpha * gwloss(constC + marginal_product, hC1, hC2, G, nx) log["srfgw_dist"] = log["lin_loss"] + log["quad_loss"] return G, log else: return G def entropic_semirelaxed_fused_gromov_wasserstein2( M, C1, C2, p=None, loss_fun="square_loss", symmetric=None, epsilon=0.1, alpha=0.5, G0=None, max_iter=1e4, tol=1e-9, log=False, verbose=False, random_state=0, ): r""" Computes the entropic-regularized semi-relaxed FGW divergence between two graphs (see :ref:`[48] `) estimated using a Mirror Descent algorithm following the KL geometry. .. math:: \mathbf{srFGW}_{\alpha} = \min_{\mathbf{T}} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} &= \mathbf{p} \mathbf{T} &\geq 0 where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix between features - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{p}` source weights (sum to 1) - `L` is a loss function to account for the misfit between the similarity matrices .. note:: This function is backend-compatible and will work on arrays from all compatible backends. However all the steps in the conditional gradient are not differentiable. The algorithm used for solving the problem is conditional gradient as discussed in :ref:`[48] ` Parameters ---------- M : array-like, shape (ns, nt) Metric cost matrix between features across domains C1 : array-like, shape (ns, ns) Metric cost matrix representative of the structure in the source space. C2 : array-like, shape (nt, nt) Metric cost matrix representative of the structure in the target space. p : array-like, shape (ns,), optional Distribution in the source space. If let to its default value None, uniform distribution is taken. loss_fun : str, optional loss function used for the solver either 'square_loss' or 'kl_loss'. epsilon : float Regularization term >0 symmetric : bool, optional Either C1 and C2 are to be assumed symmetric or not. If let to its default None value, a symmetry test will be conducted. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). alpha : float, optional Trade-off parameter (0 < alpha < 1) G0: array-like of shape (ns,nt) or string, optional If `G0=None` the initial transport plan of the solver is :math:`\mathbf{p} \frac{\mathbf{1}_{nt}}{nt}^\top`. If G0 is a tensor it must satisfy marginal constraints and will be used as initial transport of the solver. if G0 is a string it will be interpreted as a method for :func:`ot.gromov.semirelaxed_init_plan` taking values in "product", "random_product", "random", "fluid", "fluid_soft", "spectral", "spectral_soft", "kmeans", "kmeans_soft". max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on error computed on transport plans log : bool, optional record log if True verbose : bool, optional Print information along iterations random_state: int, optional Random seed used in stochastic initialization methods. Returns ------- srfgw-divergence : float Semi-relaxed Fused gromov wasserstein divergence for the given parameters. log : dict Log dictionary return only if log==True in parameters. .. _references-semirelaxed-fused-gromov-wasserstein2: References ---------- .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2022. """ T, log_srfgw = entropic_semirelaxed_fused_gromov_wasserstein( M, C1, C2, p, loss_fun, symmetric, epsilon, alpha, G0, max_iter, tol, log=True, verbose=verbose, random_state=random_state, ) log_srfgw["T"] = T if log: return log_srfgw["srfgw_dist"], log_srfgw else: return log_srfgw["srfgw_dist"] def semirelaxed_gromov_barycenters( N, Cs, ps=None, lambdas=None, loss_fun="square_loss", symmetric=True, max_iter=1000, tol=1e-9, stop_criterion="barycenter", warmstartT=False, verbose=False, log=False, init_C=None, G0="product", random_state=None, **kwargs, ): r""" Returns the Semi-relaxed Gromov-Wasserstein barycenters of `S` measured similarity matrices :math:`(\mathbf{C}_s)_{1 \leq s \leq S}` The function solves the following optimization problem with block coordinate descent: .. math:: \mathbf{C}^* = \mathop{\arg \min}_{\mathbf{C}\in \mathbb{R}^{N \times N}} \quad \sum_s \lambda_s \mathrm{srGW}(\mathbf{C}_s, \mathbf{p}_s, \mathbf{C}) Where : - :math:`\mathbf{C}_s`: input metric cost matrix - :math:`\mathbf{p}_s`: distribution Parameters ---------- N : int Size of the targeted barycenter Cs : list of S array-like of shape (ns, ns) Metric cost matrices ps : list of S array-like of shape (ns,), optional Sample weights in the `S` spaces. If let to its default value None, uniform distributions are taken. lambdas : array-like of shape (S,) , optional List of the `S` spaces' weights. If let to its default value None, uniform weights are taken. loss_fun : callable, optional tensor-matrix multiplication function based on specific loss function symmetric : bool, optional. Either structures are to be assumed symmetric or not. Default value is True. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on relative error (>0) stop_criterion : str, optional. Default is 'barycenter'. Stop criterion taking values in ['barycenter', 'loss']. If set to 'barycenter' uses absolute norm variations of estimated barycenters. Else if set to 'loss' uses the relative variations of the loss. warmstartT: bool, optional Either to perform warmstart of transport plans in the successive fused gromov-wasserstein transport problems.s verbose : bool, optional Print information along iterations. log : bool, optional Record log if True. init_C : array-like of shape (N,N), optional. Random initial value for the :math:`\mathbf{C}` matrix provided by user. Default is None and relies `G0` to produce an initial structure. G0: str, optional. Default is 'product'. Initialization method for transport plans calling :func:`ot.gromov.semirelaxed_init_plan`, and taking values in "product", "random_product", "random", "fluid", "fluid_soft", "spectral", "spectral_soft", "kmeans", "kmeans_soft". Transport plans are used to deduce an initial barycenter structure if `init_C=None`. random_state : int or RandomState instance, optional Fix the seed for reproducibility Returns ------- C : array-like, shape (`N`, `N`) Barycenters' structure matrix log : dict Only returned when log=True. It contains the keys: - :math:`\mathbf{T}`: list of (`N`, `ns`) transport matrices - :math:`\mathbf{p}`: (`N`,) barycenter weights - values used in convergence evaluation. References ---------- .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2022. """ if stop_criterion not in ["barycenter", "loss"]: raise ValueError( f"Unknown `stop_criterion='{stop_criterion}'`. Use one of: {'barycenter', 'loss'}." ) arr = [*Cs] if ps is not None: arr += [*ps] else: ps = [unif(C.shape[0], type_as=C) for C in Cs] nx = get_backend(*arr) S = len(Cs) if lambdas is None: lambdas = nx.ones(S) / S else: lambdas = list_to_array(lambdas) lambdas = nx.from_numpy(lambdas) # Initialization of transport plans and C (if not provided by user) if init_C is None: init_C = nx.zeros((N, N), type_as=Cs[0]) if G0 in ["product", "random_product", "random"]: T = [ semirelaxed_init_plan( Cs[i], init_C, ps[i], method=G0, use_target=False, random_state=random_state, nx=nx, ) for i in range(S) ] C = update_barycenter_structure(T, Cs, lambdas, loss_fun=loss_fun, nx=nx) if G0 in ["product", "random_product"]: # initial structure is constant so we add a small random noise # to avoid getting stuck at init np.random.seed(random_state) noise = np.random.uniform(-0.01, 0.01, size=(N, N)) if symmetric: noise = (noise + noise.T) / 2.0 noise = nx.from_numpy(noise) C = C + noise else: # relies on partitioning of inputs shapes = np.array([C.shape[0] for C in Cs]) large_graphs_idx = np.where(shapes > N)[0] small_graphs_idx = np.where(shapes <= N)[0] T = [None] * S list_init_C = [] # store different barycenter structure to average # we first compute an initial informative barycenter structure # on graphs we can compress # then use it on graphs to expand for indices in [large_graphs_idx, small_graphs_idx]: if len(indices) > 0: sub_T = [ semirelaxed_init_plan( Cs[i], init_C, ps[i], method=G0, use_target=False, random_state=random_state, nx=nx, ) for i in indices ] sub_Cs = [Cs[i] for i in indices] sub_lambdas = lambdas[indices] / nx.sum(lambdas[indices]) init_C = update_barycenter_structure( sub_T, sub_Cs, sub_lambdas, loss_fun=loss_fun, nx=nx ) for i, idx in enumerate(indices): T[idx] = sub_T[i] list_init_C.append(init_C) if len(list_init_C) == 2: init_C = update_barycenter_structure( T, Cs, lambdas, loss_fun=loss_fun, nx=nx ) C = init_C else: C = init_C T = [ semirelaxed_init_plan( Cs[i], C, ps[i], method=G0, use_target=True, random_state=random_state, nx=nx, ) for i in range(S) ] if stop_criterion == "barycenter": inner_log = False else: inner_log = True curr_loss = 1e15 if log: log_ = {} log_["err"] = [] if stop_criterion == "loss": log_["loss"] = [] for cpt in range(max_iter): if stop_criterion == "barycenter": Cprev = C else: prev_loss = curr_loss # get transport plans if warmstartT: res = [ semirelaxed_gromov_wasserstein( Cs[s], C, ps[s], loss_fun, symmetric, G0=T[s], max_iter=max_iter, tol_rel=tol, tol_abs=0.0, log=inner_log, verbose=verbose, **kwargs, ) for s in range(S) ] else: res = [ semirelaxed_gromov_wasserstein( Cs[s], C, ps[s], loss_fun, symmetric, G0=G0, max_iter=max_iter, tol_rel=tol, tol_abs=0.0, log=inner_log, verbose=verbose, **kwargs, ) for s in range(S) ] if stop_criterion == "barycenter": T = res else: T = [output[0] for output in res] curr_loss = np.sum([output[1]["srgw_dist"] for output in res]) # update barycenters p = nx.concatenate([nx.sum(T[s], 0)[None, :] for s in range(S)], axis=0) C = update_barycenter_structure(T, Cs, lambdas, p, loss_fun, nx=nx) # update convergence criterion if stop_criterion == "barycenter": err = nx.norm(C - Cprev) if log: log_["err"].append(err) else: err = abs(curr_loss - prev_loss) / prev_loss if prev_loss != 0.0 else np.nan if log: log_["loss"].append(curr_loss) log_["err"].append(err) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err)) if err <= tol: break if log: log_["T"] = T log_["p"] = p return C, log_ else: return C def semirelaxed_fgw_barycenters( N, Ys, Cs, ps=None, lambdas=None, alpha=0.5, fixed_structure=False, fixed_features=False, p=None, loss_fun="square_loss", symmetric=True, max_iter=100, tol=1e-9, stop_criterion="barycenter", warmstartT=False, verbose=False, log=False, init_C=None, init_X=None, G0="product", random_state=None, **kwargs, ): r""" Returns the Semi-relaxed Fused Gromov-Wasserstein barycenters of `S` measurable networks with node features :math:`(\mathbf{C}_s, \mathbf{Y}_s, \mathbf{p}_s)_{1 \leq s \leq S}` (see eq (44) in :ref:`[48]`, estimated using the semi-relaxed FGW transports from Conditional Gradient solvers. The function solves the following optimization problem: .. math:: \mathbf{C}^*, \mathbf{Y}^* = \mathop{\arg \min}_{\mathbf{C}\in \mathbb{R}^{N \times N}, \mathbf{Y}\in \mathbb{Y}^{N \times d}} \quad \sum_s \lambda_s \mathrm{srFGW}_{\alpha}(\mathbf{C}_s, \mathbf{Y}_s, \mathbf{p}_s, \mathbf{C}, \mathbf{Y}) Where : - :math:`\mathbf{Y}_s`: input feature matrix - :math:`\mathbf{C}_s`: input metric cost matrix - :math:`\mathbf{p}_s`: input distribution Parameters ---------- N : int Desired number of samples of the target barycenter Ys: list of array-like, each element has shape (ns,d) Features of all samples Cs : list of array-like, each element has shape (ns,ns) Structure matrices of all samples ps : list of array-like, each element has shape (ns,), optional Masses of all samples. If let to its default value None, uniform distributions are taken. lambdas : array-like of shape (S,) , optional List of the `S` spaces' weights. If let to its default value None, uniform weights are taken. alpha : float, optional Alpha parameter for the srFGW divergence in :math:`]0, 1[`. fixed_structure : bool, optional Whether to fix the structure of the barycenter during the updates. fixed_features : bool, optional Whether to fix the feature of the barycenter during the updates loss_fun : str, optional Loss function used for the solver either 'square_loss' or 'kl_loss' symmetric : bool, optional Either structures are to be assumed symmetric or not. Default value is True. Else if set to True (resp. False), C1 and C2 will be assumed symmetric (resp. asymmetric). max_iter : int, optional Max number of iterations tol : float, optional Stop threshold on relative error (>0) stop_criterion : str, optional. Default is 'barycenter'. Stop criterion taking values in ['barycenter', 'loss']. If set to 'barycenter' uses absolute norm variations of estimated barycenters. Else if set to 'loss' uses the relative variations of the loss. warmstartT: bool, optional Either to perform warmstart of transport plans in the successive fused gromov-wasserstein transport problems. verbose : bool, optional Print information along iterations. log : bool, optional Record log if True. init_C : array-like, shape (N,N), optional Initialization for the barycenters' structure matrix. If not set a random init is used. init_X : array-like, shape (N,d), optional Initialization for the barycenters' features. If not set a random init is used. G0: str, optional. Default is "product". Initialization method for transport plans calling :func:`ot.gromov.semirelaxed_init_plan`, and taking values in "product", "random_product", "random", "fluid", "fluid_soft", "spectral", "spectral_soft", "kmeans", "kmeans_soft". Transport plans are used to deduce an initial barycenter structure if `init_C=None`. random_state : int or RandomState instance, optional Fix the seed for reproducibility Returns ------- X : array-like, shape (`N`, `d`) Barycenters' features C : array-like, shape (`N`, `N`) Barycenters' structure matrix log : dict Only returned when log=True. It contains the keys: - :math:`\mathbf{T}_s`: list of (`N`, `ns`) transport matrices from which target masses can be deduced. - :math:`(\mathbf{M}_s)_s`: all distance matrices between the feature of the barycenter and the other features :math:`(dist(\mathbf{X}, \mathbf{Y}_s))_s` shape (`N`, `ns`) - values used in convergence evaluation. References ---------- .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2022. """ if stop_criterion not in ["barycenter", "loss"]: raise ValueError( f"Unknown `stop_criterion='{stop_criterion}'`. Use one of: {'barycenter', 'loss'}." ) arr = [*Cs, *Ys] if ps is not None: arr += [*ps] else: ps = [unif(C.shape[0], type_as=C) for C in Cs] nx = get_backend(*arr) S = len(Cs) if lambdas is None: lambdas = nx.ones(S) / S else: lambdas = list_to_array(lambdas) lambdas = nx.from_numpy(lambdas) if fixed_structure: if init_C is None: raise UndefinedParameter("If C is fixed it must be provided in init_C") else: C = init_C if fixed_features: if init_X is None: raise UndefinedParameter("If X is fixed it must be provided in init_X") else: X = init_X # Initialization of transport plans, C and X (if not provided by user) if G0 in ["product", "random_product", "random"]: # both init_X and init_C are simply deduced from transport plans # if not initialized if init_C is None: init_C = nx.zeros((N, N), type_as=Cs[0]) # to know the barycenter shape T = [ semirelaxed_init_plan( Cs[i], init_C, ps[i], method=G0, use_target=False, random_state=random_state, nx=nx, ) for i in range(S) ] C = update_barycenter_structure(T, Cs, lambdas, loss_fun=loss_fun, nx=nx) if G0 in ["product", "random_product"]: # initial structure is constant so we add a small random noise # to avoid getting stuck at init np.random.seed(random_state) noise = np.random.uniform(-0.01, 0.01, size=(N, N)) if symmetric: noise = (noise + noise.T) / 2.0 noise = nx.from_numpy(noise) C = C + noise else: T = [ semirelaxed_init_plan( Cs[i], init_C, ps[i], method=G0, use_target=False, random_state=random_state, nx=nx, ) for i in range(S) ] C = init_C if init_X is None: X = update_barycenter_feature(T, Ys, lambdas, loss_fun=loss_fun, nx=nx) else: X = init_X Ms = [dist(Ys[s], X) for s in range(len(Ys))] else: # more computationally costly inits could be used on structures # so we assume affordable a Kmeans-like init for features # and use it by default. if init_X is None: stacked_features = nx.concatenate(Ys, axis=0) if sklearn_import: stacked_features = nx.to_numpy(stacked_features) km = KMeans(n_clusters=N, random_state=random_state, n_init=1).fit( stacked_features ) X = nx.from_numpy(km.cluster_centers_) else: raise ValueError( f"Scikit-learn must be installed to use G0={G0} while init_X=None." ) else: X = init_X Ms = [dist(Ys[s], X) for s in range(len(Ys))] if init_C is None: init_C = nx.zeros((N, N), type_as=Cs[0]) # relies on partitioning of inputs shapes = np.array([C.shape[0] for C in Cs]) large_graphs_idx = np.where(shapes > N)[0] small_graphs_idx = np.where(shapes <= N)[0] T = [None] * S list_init_C = [] # store different barycenter structure to average # we first compute an initial informative barycenter structure # on graphs we can compress # then use it on graphs to expand for indices in [large_graphs_idx, small_graphs_idx]: if len(indices) > 0: sub_T = [ semirelaxed_init_plan( Cs[i], init_C, ps[i], Ms[i], alpha, method=G0, use_target=False, random_state=random_state, nx=nx, ) for i in indices ] sub_Cs = [Cs[i] for i in indices] sub_lambdas = lambdas[indices] / nx.sum(lambdas[indices]) init_C = update_barycenter_structure( sub_T, sub_Cs, sub_lambdas, loss_fun=loss_fun, nx=nx ) for i, idx in enumerate(indices): T[idx] = sub_T[i] list_init_C.append(init_C) if len(list_init_C) == 2: init_C = update_barycenter_structure( T, Cs, lambdas, loss_fun=loss_fun, nx=nx ) C = init_C else: C = init_C T = [ semirelaxed_init_plan( Cs[i], C, ps[i], Ms[i], alpha, method=G0, use_target=True, random_state=random_state, nx=nx, ) for i in range(S) ] if stop_criterion == "barycenter": inner_log = False else: inner_log = True curr_loss = 1e15 if log: log_ = {} if stop_criterion == "barycenter": log_["err_feature"] = [] log_["err_structure"] = [] else: log_["loss"] = [] log_["err_rel_loss"] = [] for cpt in range(max_iter): # break if specified errors are below tol. if stop_criterion == "barycenter": Cprev = C Xprev = X else: prev_loss = curr_loss # get transport plans if warmstartT: res = [ semirelaxed_fused_gromov_wasserstein( Ms[s], Cs[s], C, ps[s], loss_fun, symmetric, alpha, T[s], inner_log, max_iter, tol_rel=tol, tol_abs=0.0, **kwargs, ) for s in range(S) ] else: res = [ semirelaxed_fused_gromov_wasserstein( Ms[s], Cs[s], C, ps[s], loss_fun, symmetric, alpha, G0, inner_log, max_iter, tol_rel=tol, tol_abs=0.0, **kwargs, ) for s in range(S) ] if stop_criterion == "barycenter": T = res else: T = [output[0] for output in res] curr_loss = np.sum([output[1]["srfgw_dist"] for output in res]) # update barycenters p = nx.concatenate([nx.sum(T[s], 0)[None, :] for s in range(S)], axis=0) if not fixed_features: X = update_barycenter_feature(T, Ys, lambdas, p, nx=nx) Ms = [dist(Ys[s], X) for s in range(len(Ys))] if not fixed_structure: C = update_barycenter_structure(T, Cs, lambdas, p, loss_fun, nx=nx) # update convergence criterion if stop_criterion == "barycenter": err_feature, err_structure = 0.0, 0.0 if not fixed_features: err_feature = nx.norm(X - Xprev) if not fixed_structure: err_structure = nx.norm(C - Cprev) if log: log_["err_feature"].append(err_feature) log_["err_structure"].append(err_structure) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err_structure)) print("{:5d}|{:8e}|".format(cpt, err_feature)) if (err_feature <= tol) or (err_structure <= tol): break else: err_rel_loss = ( abs(curr_loss - prev_loss) / prev_loss if prev_loss != 0.0 else np.nan ) if log: log_["loss"].append(curr_loss) log_["err_rel_loss"].append(err_rel_loss) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err_rel_loss)) if err_rel_loss <= tol: break if log: log_["T"] = T log_["p"] = p log_["Ms"] = Ms return X, C, log_ else: return X, C python-pot-0.9.7+dfsg/ot/gromov/_unbalanced.py000066400000000000000000001505211525521106200213320ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Unbalanced Co-Optimal Transport and Fused Unbalanced Gromov-Wasserstein solvers """ # Author: Quang Huy Tran # Alexis Thual # # License: MIT License import warnings from functools import partial import ot from ot.backend import get_backend from ot.utils import list_to_array, get_parameter_pair from ._utils import ( fused_unbalanced_across_spaces_cost, uot_cost_matrix, uot_parameters_and_measures, ) def fused_unbalanced_across_spaces_divergence( X, Y, wx_samp=None, wx_feat=None, wy_samp=None, wy_feat=None, reg_marginals=10, epsilon=0, reg_type="joint", divergence="kl", unbalanced_solver="sinkhorn", alpha=0, M_samp=None, M_feat=None, rescale_plan=True, init_pi=None, init_duals=None, max_iter=100, tol=1e-7, max_iter_ot=500, tol_ot=1e-7, log=False, verbose=False, **kwargs_solver, ): r"""Compute the fused unbalanced cross-spaces divergence between two matrices equipped with the distributions on rows and columns. We consider two cases of matrix: - (Squared) similarity matrix in Gromov-Wasserstein setting, whose rows and columns represent the samples. - Arbitrary-size matrix in Co-Optimal Transport setting, whose rows represent samples, and columns represent corresponding features/dimensions. More precisely, this function returns the sample and feature transport plans between :math:`(\mathbf{X}, \mathbf{w}_{xs}, \mathbf{w}_{xf})` and :math:`(\mathbf{Y}, \mathbf{w}_{ys}, \mathbf{w}_{yf})`, by solving the following problem using Block Coordinate Descent algorithm: .. math:: \mathop{\arg \min}_{\mathbf{P}, \mathbf{Q}} &\quad \sum_{i,j,k,l} (\mathbf{X}_{i,k} - \mathbf{Y}_{j,l})^2 \mathbf{P}_{i,j} \mathbf{Q}_{k,l} \\ &+ \rho_s \mathbf{Div}(\mathbf{P}_{\# 1} \mathbf{Q}_{\# 1}^T | \mathbf{w}_{xs} \mathbf{w}_{ys}^T) + \rho_f \mathbf{Div}(\mathbf{P}_{\# 2} \mathbf{Q}_{\# 2}^T | \mathbf{w}_{xf} \mathbf{w}_{yf}^T) \\ &+ \alpha_s \sum_{i,j} \mathbf{P}_{i,j} \mathbf{M^{(s)}}_{i, j} + \alpha_f \sum_{k, l} \mathbf{Q}_{k,l} \mathbf{M^{(f)}}_{k, l} + \mathbf{Reg}(\mathbf{P}, \mathbf{Q}) Where: - :math:`\mathbf{X}`: Source input (arbitrary-size) matrix - :math:`\mathbf{Y}`: Target input (arbitrary-size) matrix - :math:`\mathbf{M^{(s)}}`: Additional sample matrix - :math:`\mathbf{M^{(f)}}`: Additional feature matrix - :math:`\mathbf{w}_{xs}`: Distribution of the samples in the source space - :math:`\mathbf{w}_{xf}`: Distribution of the features in the source space - :math:`\mathbf{w}_{ys}`: Distribution of the samples in the target space - :math:`\mathbf{w}_{yf}`: Distribution of the features in the target space - :math:`\mathbf{Div}`: Either Kullback-Leibler divergence or half-squared L2 norm. - :math:`\mathbf{Reg}`: Regularizer for sample and feature couplings. We consider two types of regularizer: + Independent regularization used in unbalanced Co-Optimal Transport .. math:: \mathbf{Reg}(\mathbf{P}, \mathbf{Q}) = \varepsilon_s \mathbf{Div}(\mathbf{P} | \mathbf{w}_{xs} \mathbf{w}_{ys}^T) + \varepsilon_f \mathbf{Div}(\mathbf{Q} | \mathbf{w}_{xf} \mathbf{w}_{yf}^T) + Joint regularization used in fused unbalanced Gromov-Wasserstein .. math:: \mathbf{Reg}(\mathbf{P}, \mathbf{Q}) = \varepsilon \mathbf{Div}(\mathbf{P} \otimes \mathbf{Q} | (\mathbf{w}_{xs} \mathbf{w}_{ys}^T) \otimes (\mathbf{w}_{xf} \mathbf{w}_{yf}^T) ) .. note:: This function allows epsilon to be zero. In that case, `unbalanced_method` must be either "mm" or "lbfgsb". Parameters ---------- X : (n_sample_x, n_feature_x) array-like, float Source input matrix. Y : (n_sample_y, n_feature_y) array-like, float Target input matrix. wx_samp : (n_sample_x, ) array-like, float, optional (default = None) Histogram assigned on rows (samples) of matrix X. Uniform distribution by default. wx_feat : (n_feature_x, ) array-like, float, optional (default = None) Histogram assigned on columns (features) of matrix X. Uniform distribution by default. wy_samp : (n_sample_y, ) array-like, float, optional (default = None) Histogram assigned on rows (samples) of matrix Y. Uniform distribution by default. wy_feat : (n_feature_y, ) array-like, float, optional (default = None) Histogram assigned on columns (features) of matrix Y. Uniform distribution by default. reg_marginals: float or indexable object of length 1 or 2 Marginal relaxation terms for sample and feature couplings. If `reg_marginals` is a scalar or an indexable object of length 1, then the same value is applied to both marginal relaxations. epsilon : scalar or indexable object of length 2, float or int, optional (default = 0) Regularization parameters for entropic approximation of sample and feature couplings. Allow the case where `epsilon` contains 0. In that case, the MM solver is used by default instead of Sinkhorn solver. If `epsilon` is scalar, then the same value is applied to both regularization of sample and feature couplings. reg_type: string, optional - If `reg_type` = "joint": then use joint regularization for couplings. - If `reg_type` = "independent": then use independent regularization for couplings. divergence : string, optional (default = "kl") - If `divergence` = "kl", then Div is the Kullback-Leibler divergence. - If `divergence` = "l2", then Div is the half squared Euclidean norm. unbalanced_solver : string, optional (default = "sinkhorn") Solver for the unbalanced OT subroutine. - If `divergence` = "kl", then `unbalanced_solver` can be: "sinkhorn", "sinkhorn_log", "mm", "lbfgsb" - If `divergence` = "l2", then `unbalanced_solver` can be "mm", "lbfgsb" alpha : scalar or indexable object of length 2, float or int, optional (default = 0) Coeffficient parameter of linear terms with respect to the sample and feature couplings. If alpha is scalar, then the same alpha is applied to both linear terms. M_samp : (n_sample_x, n_sample_y), float, optional (default = None) Sample matrix associated to the Wasserstein linear term on sample coupling. M_feat : (n_feature_x, n_feature_y), float, optional (default = None) Feature matrix associated to the Wasserstein linear term on feature coupling. rescale_plan : boolean, optional (default = True) If True, then rescale the sample and feature transport plans within each BCD iteration, so that they always have equal mass. init_pi : tuple of two matrices of size (n_sample_x, n_sample_y) and (n_feature_x, n_feature_y), optional (default = None). Initialization of sample and feature couplings. Uniform distributions by default. init_duals : tuple of two tuples ((n_sample_x, ), (n_sample_y, )) and ((n_feature_x, ), (n_feature_y, )), optional (default = None). Initialization of sample and feature dual vectors if using Sinkhorn algorithm. Zero vectors by default. max_iter : int, optional (default = 100) Number of Block Coordinate Descent (BCD) iterations. tol : float, optional (default = 1e-7) Tolerance of BCD scheme. If the L1-norm between the current and previous sample couplings is under this threshold, then stop BCD scheme. max_iter_ot : int, optional (default = 100) Number of iterations to solve each of the two unbalanced optimal transport problems in each BCD iteration. tol_ot : float, optional (default = 1e-7) Tolerance of unbalanced solver for each of the two unbalanced optimal transport problems in each BCD iteration. log : bool, optional (default = False) If True then the cost and four dual vectors, including two from sample and two from feature couplings, are recorded. verbose : bool, optional (default = False) If True then print the COOT cost at every multiplier of `eval_bcd`-th iteration. Returns ------- pi_samp : (n_sample_x, n_sample_y) array-like, float Sample coupling matrix. pi_feat : (n_feature_x, n_feature_y) array-like, float Feature coupling matrix. log : dictionary, optional Returned if `log` is True. The keys are: error : array-like, float list of L1 norms between the current and previous sample coupling. duals_sample : (n_sample_x, n_sample_y) tuple, float Pair of dual vectors when solving OT problem w.r.t the sample coupling. duals_feature : (n_feature_x, n_feature_y) tuple, float Pair of dual vectors when solving OT problem w.r.t the feature coupling. linear : float Linear part of the cost. ucoot : float Total cost. backend The proper backend for all input arrays """ # MAIN FUNCTION if reg_type not in ["joint", "independent"]: raise (NotImplementedError('Unknown reg_type="{}"'.format(reg_type))) if divergence not in ["kl", "l2"]: raise (NotImplementedError('Unknown divergence="{}"'.format(divergence))) if unbalanced_solver not in ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"]: raise (NotImplementedError('Unknown method="{}"'.format(unbalanced_solver))) # hyperparameters alpha_samp, alpha_feat = get_parameter_pair(alpha) rho_x, rho_y = get_parameter_pair(reg_marginals) eps_samp, eps_feat = get_parameter_pair(epsilon) if reg_type == "joint": # same regularization eps_feat = eps_samp if unbalanced_solver in ["sinkhorn", "sinkhorn_log"] and divergence == "l2": warnings.warn( "Sinkhorn algorithm does not support L2 norm. \ Divergence is set to 'kl'." ) divergence = "kl" if unbalanced_solver in ["sinkhorn", "sinkhorn_log"] and ( eps_samp == 0 or eps_feat == 0 ): warnings.warn( "Sinkhorn algorithm does not support unregularized problem. \ Solver is set to 'mm'." ) unbalanced_solver = "mm" if init_pi is None: pi_samp, pi_feat = None, None else: pi_samp, pi_feat = init_pi if init_duals is None: init_duals = (None, None) duals_samp, duals_feat = init_duals arr = [X, Y] for tuple in [duals_samp, duals_feat]: if tuple is not None: d1, d2 = duals_feat if d1 is not None: arr.append(list_to_array(d1)) if d2 is not None: arr.append(list_to_array(d2)) nx = get_backend( *arr, wx_samp, wx_feat, wy_samp, wy_feat, M_samp, M_feat, pi_samp, pi_feat ) # constant input variables if M_samp is None: if alpha_samp > 0: warnings.warn( "M_samp is None but alpha_samp = {} > 0. \ The algo will treat as if alpha_samp = 0.".format(alpha_samp) ) else: M_samp = alpha_samp * M_samp if M_feat is None: if alpha_feat > 0: warnings.warn( "M_feat is None but alpha_feat = {} > 0. \ The algo will treat as if alpha_feat = 0.".format(alpha_feat) ) else: M_feat = alpha_feat * M_feat nx_samp, nx_feat = X.shape ny_samp, ny_feat = Y.shape # measures on rows and columns if wx_samp is None: wx_samp = nx.ones(nx_samp, type_as=X) / nx_samp if wx_feat is None: wx_feat = nx.ones(nx_feat, type_as=X) / nx_feat if wy_samp is None: wy_samp = nx.ones(ny_samp, type_as=Y) / ny_samp if wy_feat is None: wy_feat = nx.ones(ny_feat, type_as=Y) / ny_feat wxy_samp = wx_samp[:, None] * wy_samp[None, :] wxy_feat = wx_feat[:, None] * wy_feat[None, :] # initialize coupling and dual vectors pi_samp = wxy_samp if pi_samp is None else pi_samp pi_feat = wxy_feat if pi_feat is None else pi_feat if unbalanced_solver in ["sinkhorn", "sinkhorn_log"]: if duals_samp is None: duals_samp = (nx.zeros(nx_samp, type_as=X), nx.zeros(ny_samp, type_as=Y)) if duals_feat is None: duals_feat = (nx.zeros(nx_feat, type_as=X), nx.zeros(ny_feat, type_as=Y)) # shortcut functions X_sqr, Y_sqr = X**2, Y**2 local_cost_samp = partial( uot_cost_matrix, data=(X_sqr, Y_sqr, X, Y, M_samp), tuple_p=(wx_feat, wy_feat), hyperparams=(rho_x, rho_y, eps_feat), divergence=divergence, reg_type=reg_type, nx=nx, ) local_cost_feat = partial( uot_cost_matrix, data=(X_sqr.T, Y_sqr.T, X.T, Y.T, M_feat), tuple_p=(wx_samp, wy_samp), hyperparams=(rho_x, rho_y, eps_samp), divergence=divergence, reg_type=reg_type, nx=nx, ) parameters_uot_l2_samp = partial( uot_parameters_and_measures, tuple_weights=(wx_samp, wy_samp, wxy_samp), hyperparams=(rho_x, rho_y, eps_samp), reg_type=reg_type, divergence=divergence, nx=nx, ) parameters_uot_l2_feat = partial( uot_parameters_and_measures, tuple_weights=(wx_feat, wy_feat, wxy_feat), hyperparams=(rho_x, rho_y, eps_feat), reg_type=reg_type, divergence=divergence, nx=nx, ) solver = partial( ot.solve, reg_type=divergence, unbalanced_type=divergence, method=unbalanced_solver, max_iter=max_iter_ot, tol=tol_ot, verbose=False, ) # initialize log if log: dict_log = {"backend": nx, "error": []} for idx in range(max_iter): pi_samp_prev = nx.copy(pi_samp) # Update feature coupling mass = nx.sum(pi_samp) uot_cost = local_cost_feat(pi=pi_samp) if divergence == "kl": new_rho = (rho_x * mass, rho_y * mass) new_eps = mass * eps_feat if reg_type == "joint" else eps_feat new_wx, new_wy, new_wxy = wx_feat, wy_feat, wxy_feat else: # divergence == "l2" new_w, new_rho, new_eps = parameters_uot_l2_feat(pi_feat) new_wx, new_wy, new_wxy = new_w res = solver( M=uot_cost, a=new_wx, b=new_wy, reg=new_eps, c=new_wxy, unbalanced=new_rho, plan_init=pi_feat, potentials_init=duals_feat, ) pi_feat, duals_feat = res.plan, res.potentials if rescale_plan: pi_feat = nx.sqrt(mass / nx.sum(pi_feat)) * pi_feat # Update sample coupling mass = nx.sum(pi_feat) uot_cost = local_cost_samp(pi=pi_feat) if divergence == "kl": new_rho = (rho_x * mass, rho_y * mass) new_eps = mass * eps_feat if reg_type == "joint" else eps_feat new_wx, new_wy, new_wxy = wx_samp, wy_samp, wxy_samp else: # divergence == "l2" new_w, new_rho, new_eps = parameters_uot_l2_samp(pi_samp) new_wx, new_wy, new_wxy = new_w res = solver( M=uot_cost, a=new_wx, b=new_wy, reg=new_eps, c=new_wxy, unbalanced=new_rho, plan_init=pi_samp, potentials_init=duals_samp, ) pi_samp, duals_samp = res.plan, res.potentials if rescale_plan: pi_samp = nx.sqrt(mass / nx.sum(pi_samp)) * pi_samp # shape nx x ny # get L1 error err = nx.sum(nx.abs(pi_samp - pi_samp_prev)) if log: dict_log["error"].append(err) if verbose: print("{:5d}|{:8e}|".format(idx + 1, err)) if err < tol: break # sanity check if nx.sum(nx.isnan(pi_samp)) > 0 or nx.sum(nx.isnan(pi_feat)) > 0: raise ( ValueError( "There is NaN in coupling. \ Adjust the relaxation or regularization parameters." ) ) if log: linear_cost, ucoot_cost = fused_unbalanced_across_spaces_cost( M_linear=(M_samp, M_feat), data=(X_sqr, Y_sqr, X, Y), tuple_pxy_samp=(wx_samp, wy_samp, wxy_samp), tuple_pxy_feat=(wx_feat, wy_feat, wxy_feat), pi_samp=pi_samp, pi_feat=pi_feat, hyperparams=(rho_x, rho_y, eps_samp, eps_feat), divergence=divergence, reg_type=reg_type, nx=nx, ) dict_log["duals_sample"] = duals_samp dict_log["duals_feature"] = duals_feat dict_log["linear_cost"] = linear_cost dict_log["ucoot_cost"] = ucoot_cost return pi_samp, pi_feat, dict_log else: return pi_samp, pi_feat def unbalanced_co_optimal_transport( X, Y, wx_samp=None, wx_feat=None, wy_samp=None, wy_feat=None, reg_marginals=10, epsilon=0, divergence="kl", unbalanced_solver="mm", alpha=0, M_samp=None, M_feat=None, rescale_plan=True, init_pi=None, init_duals=None, max_iter=100, tol=1e-7, max_iter_ot=500, tol_ot=1e-7, log=False, verbose=False, **kwargs_solve, ): r"""Compute the unbalanced Co-Optimal Transport between two Euclidean point clouds (represented as matrices whose rows are samples and columns are the features/dimensions). More precisely, this function returns the sample and feature transport plans between :math:`(\mathbf{X}, \mathbf{w}_{xs}, \mathbf{w}_{xf})` and :math:`(\mathbf{Y}, \mathbf{w}_{ys}, \mathbf{w}_{yf})`, by solving the following problem using Block Coordinate Descent algorithm: .. math:: \mathop{\arg \min}_{\mathbf{P}, \mathbf{Q}} &\quad \sum_{i,j,k,l} (\mathbf{X}_{i,k} - \mathbf{Y}_{j,l})^2 \mathbf{P}_{i,j} \mathbf{Q}_{k,l} \\ &+ \rho_s \mathbf{Div}(\mathbf{P}_{\# 1} \mathbf{Q}_{\# 1}^T | \mathbf{w}_{xs} \mathbf{w}_{ys}^T) + \rho_f \mathbf{Div}(\mathbf{P}_{\# 2} \mathbf{Q}_{\# 2}^T | \mathbf{w}_{xf} \mathbf{w}_{yf}^T) \\ &+ \alpha_s \sum_{i,j} \mathbf{P}_{i,j} \mathbf{M^{(s)}}_{i, j} + \alpha_f \sum_{k, l} \mathbf{Q}_{k,l} \mathbf{M^{(f)}}_{k, l} \\ &+ \varepsilon_s \mathbf{Div}(\mathbf{P} | \mathbf{w}_{xs} \mathbf{w}_{ys}^T) + \varepsilon_f \mathbf{Div}(\mathbf{Q} | \mathbf{w}_{xf} \mathbf{w}_{yf}^T) Where: - :math:`\mathbf{X}`: Source input (arbitrary-size) matrix - :math:`\mathbf{Y}`: Target input (arbitrary-size) matrix - :math:`\mathbf{M^{(s)}}`: Additional sample matrix - :math:`\mathbf{M^{(f)}}`: Additional feature matrix - :math:`\mathbf{w}_{xs}`: Distribution of the samples in the source space - :math:`\mathbf{w}_{xf}`: Distribution of the features in the source space - :math:`\mathbf{w}_{ys}`: Distribution of the samples in the target space - :math:`\mathbf{w}_{yf}`: Distribution of the features in the target space - :math:`\mathbf{Div}`: Either Kullback-Leibler divergence or half-squared L2 norm. .. note:: This function allows `epsilon` to be zero. In that case, `unbalanced_method` must be either "mm" or "lbfgsb". Parameters ---------- X : (n_sample_x, n_feature_x) array-like, float Source input matrix. Y : (n_sample_y, n_feature_y) array-like, float Target input matrix. wx_samp : (n_sample_x, ) array-like, float, optional (default = None) Histogram assigned on rows (samples) of matrix X. Uniform distribution by default. wx_feat : (n_feature_x, ) array-like, float, optional (default = None) Histogram assigned on columns (features) of matrix X. Uniform distribution by default. wy_samp : (n_sample_y, ) array-like, float, optional (default = None) Histogram assigned on rows (samples) of matrix Y. Uniform distribution by default. wy_feat : (n_feature_y, ) array-like, float, optional (default = None) Histogram assigned on columns (features) of matrix Y. Uniform distribution by default. reg_marginals: float or indexable object of length 1 or 2 Marginal relaxation terms for sample and feature couplings. If `reg_marginals is a scalar` or an indexable object of length 1, then the same value is applied to both marginal relaxations. epsilon : scalar or indexable object of length 2, float or int, optional (default = 0) Regularization parameters for entropic approximation of sample and feature couplings. Allow the case where `epsilon` contains 0. In that case, the MM solver is used by default instead of Sinkhorn solver. If `epsilon` is scalar, then the same value is applied to both regularization of sample and feature couplings. divergence : string, optional (default = "kl") - If `divergence` = "kl", then Div is the Kullback-Leibler divergence. - If `divergence` = "l2", then Div is the half squared Euclidean norm. unbalanced_solver : string, optional (default = "sinkhorn") Solver for the unbalanced OT subroutine. - If `divergence` = "kl", then `unbalanced_solver` can be: "sinkhorn", "sinkhorn_log", "mm", "lbfgsb" - If `divergence` = "l2", then `unbalanced_solver` can be "mm", "lbfgsb" alpha : scalar or indexable object of length 2, float or int, optional (default = 0) Coeffficient parameter of linear terms with respect to the sample and feature couplings. If alpha is scalar, then the same alpha is applied to both linear terms. M_samp : (n_sample_x, n_sample_y), float, optional (default = None) Sample matrix associated to the Wasserstein linear term on sample coupling. M_feat : (n_feature_x, n_feature_y), float, optional (default = None) Feature matrix associated to the Wasserstein linear term on feature coupling. rescale_plan : boolean, optional (default = True) If True, then rescale the sample and feature transport plans within each BCD iteration, so that they always have equal mass. init_pi : tuple of two matrices of size (n_sample_x, n_sample_y) and (n_feature_x, n_feature_y), optional (default = None). Initialization of sample and feature couplings. Uniform distributions by default. init_duals : tuple of two tuples ((n_sample_x, ), (n_sample_y, )) and ((n_feature_x, ), (n_feature_y, )), optional (default = None). Initialization of sample and feature dual vectors if using Sinkhorn algorithm. Zero vectors by default. max_iter : int, optional (default = 100) Number of Block Coordinate Descent (BCD) iterations. tol : float, optional (default = 1e-7) Tolerance of BCD scheme. If the L1-norm between the current and previous sample couplings is under this threshold, then stop BCD scheme. max_iter_ot : int, optional (default = 100) Number of iterations to solve each of the two unbalanced optimal transport problems in each BCD iteration. tol_ot : float, optional (default = 1e-7) Tolerance of unbalanced solver for each of the two unbalanced optimal transport problems in each BCD iteration. log : bool, optional (default = False) If True then the cost and four dual vectors, including two from sample and two from feature couplings, are recorded. verbose : bool, optional (default = False) If True then print the COOT cost at every multiplier of `eval_bcd`-th iteration. Returns ------- pi_samp : (n_sample_x, n_sample_y) array-like, float Sample coupling matrix. pi_feat : (n_feature_x, n_feature_y) array-like, float Feature coupling matrix. log : dictionary, optional Returned if `log` is True. The keys are: error : array-like, float list of L1 norms between the current and previous sample coupling. duals_sample : (n_sample_x, n_sample_y)-tuple, float Pair of dual vectors when solving OT problem w.r.t the sample coupling. duals_feature : (n_feature_x, n_feature_y)-tuple, float Pair of dual vectors when solving OT problem w.r.t the feature coupling. linear : float Linear part of the cost. ucoot : float Total cost. References ---------- .. [71] Tran, H., Janati, H., Courty, N., Flamary, R., Redko, I., Demetci, P., & Singh, R. Unbalanced Co-Optimal Transport. AAAI Conference on Artificial Intelligence, 2023. """ return fused_unbalanced_across_spaces_divergence( X=X, Y=Y, wx_samp=wx_samp, wx_feat=wx_feat, wy_samp=wy_samp, wy_feat=wy_feat, reg_marginals=reg_marginals, epsilon=epsilon, reg_type="independent", divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=M_samp, M_feat=M_feat, rescale_plan=rescale_plan, init_pi=init_pi, init_duals=init_duals, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=log, verbose=verbose, **kwargs_solve, ) def unbalanced_co_optimal_transport2( X, Y, wx_samp=None, wx_feat=None, wy_samp=None, wy_feat=None, reg_marginals=10, epsilon=0, divergence="kl", unbalanced_solver="sinkhorn", alpha=0, M_samp=None, M_feat=None, rescale_plan=True, init_pi=None, init_duals=None, max_iter=100, tol=1e-7, max_iter_ot=500, tol_ot=1e-7, log=False, verbose=False, **kwargs_solve, ): r"""Compute the unbalanced Co-Optimal Transport between two Euclidean point clouds (represented as matrices whose rows are samples and columns are the features/dimensions). More precisely, this function returns the unbalanced Co-Optimal Transport cost between :math:`(\mathbf{X}, \mathbf{w}_{xs}, \mathbf{w}_{xf})` and :math:`(\mathbf{Y}, \mathbf{w}_{ys}, \mathbf{w}_{yf})`, by solving the following problem using Block Coordinate Descent algorithm: .. math:: \mathop{\min}_{\mathbf{P}, \mathbf{Q}} &\quad \sum_{i,j,k,l} (\mathbf{X}_{i,k} - \mathbf{Y}_{j,l})^2 \mathbf{P}_{i,j} \mathbf{Q}_{k,l} \\ &+ \rho_s \mathbf{Div}(\mathbf{P}_{\# 1} \mathbf{Q}_{\# 1}^T | \mathbf{w}_{xs} \mathbf{w}_{ys}^T) + \rho_f \mathbf{Div}(\mathbf{P}_{\# 2} \mathbf{Q}_{\# 2}^T | \mathbf{w}_{xf} \mathbf{w}_{yf}^T) \\ &+ \alpha_s \sum_{i,j} \mathbf{P}_{i,j} \mathbf{M^{(s)}}_{i, j} + \alpha_f \sum_{k, l} \mathbf{Q}_{k,l} \mathbf{M^{(f)}}_{k, l} \\ &+ \varepsilon_s \mathbf{Div}(\mathbf{P} | \mathbf{w}_{xs} \mathbf{w}_{ys}^T) + \varepsilon_f \mathbf{Div}(\mathbf{Q} | \mathbf{w}_{xf} \mathbf{w}_{yf}^T) Where: - :math:`\mathbf{X}`: Source input (arbitrary-size) matrix - :math:`\mathbf{Y}`: Target input (arbitrary-size) matrix - :math:`\mathbf{M^{(s)}}`: Additional sample matrix - :math:`\mathbf{M^{(f)}}`: Additional feature matrix - :math:`\mathbf{w}_{xs}`: Distribution of the samples in the source space - :math:`\mathbf{w}_{xf}`: Distribution of the features in the source space - :math:`\mathbf{w}_{ys}`: Distribution of the samples in the target space - :math:`\mathbf{w}_{yf}`: Distribution of the features in the target space - :math:`\mathbf{Div}`: Either Kullback-Leibler divergence or half-squared L2 norm. .. note:: This function allows `epsilon` to be zero. In that case, `unbalanced_method` must be either "mm" or "lbfgsb". Also the computation of gradients is only supported for KL divergence. The case of half squared-L2 norm uses those of KL divergence. Parameters ---------- X : (n_sample_x, n_feature_x) array-like, float Source input matrix. Y : (n_sample_y, n_feature_y) array-like, float Target input matrix. wx_samp : (n_sample_x, ) array-like, float, optional (default = None) Histogram assigned on rows (samples) of matrix X. Uniform distribution by default. wx_feat : (n_feature_x, ) array-like, float, optional (default = None) Histogram assigned on columns (features) of matrix X. Uniform distribution by default. wy_samp : (n_sample_y, ) array-like, float, optional (default = None) Histogram assigned on rows (samples) of matrix Y. Uniform distribution by default. wy_feat : (n_feature_y, ) array-like, float, optional (default = None) Histogram assigned on columns (features) of matrix Y. Uniform distribution by default. reg_marginals: float or indexable object of length 1 or 2 Marginal relaxation terms for sample and feature couplings. If `reg_marginals` is a scalar or an indexable object of length 1, then the same value is applied to both marginal relaxations. epsilon : scalar or indexable object of length 2, float or int, optional (default = 0) Regularization parameters for entropic approximation of sample and feature couplings. Allow the case where `epsilon` contains 0. In that case, the MM solver is used by default instead of Sinkhorn solver. If `epsilon` is scalar, then the same value is applied to both regularization of sample and feature couplings. divergence : string, optional (default = "kl") - If `divergence` = "kl", then Div is the Kullback-Leibler divergence. - If `divergence` = "l2", then Div is the half squared Euclidean norm. unbalanced_solver : string, optional (default = "sinkhorn") Solver for the unbalanced OT subroutine. - If `divergence` = "kl", then `unbalanced_solver` can be: "sinkhorn", "sinkhorn_log", "mm", "lbfgsb" - If `divergence` = "l2", then `unbalanced_solver` can be "mm", "lbfgsb" alpha : scalar or indexable object of length 2, float or int, optional (default = 0) Coeffficient parameter of linear terms with respect to the sample and feature couplings. If alpha is scalar, then the same alpha is applied to both linear terms. M_samp : (n_sample_x, n_sample_y), float, optional (default = None) Sample matrix associated to the Wasserstein linear term on sample coupling. M_feat : (n_feature_x, n_feature_y), float, optional (default = None) Feature matrix associated to the Wasserstein linear term on feature coupling. rescale_plan : boolean, optional (default = True) If True, then rescale the transport plans in each BCD iteration, so that they always have equal mass. init_pi : tuple of two matrices of size (n_sample_x, n_sample_y) and (n_feature_x, n_feature_y), optional (default = None). Initialization of sample and feature couplings. Uniform distributions by default. init_duals : tuple of two tuples ((n_sample_x, ), (n_sample_y, )) and ((n_feature_x, ), (n_feature_y, )), optional (default = None). Initialization of sample and feature dual vectors if using Sinkhorn algorithm. Zero vectors by default. max_iter : int, optional (default = 100) Number of Block Coordinate Descent (BCD) iterations. tol : float, optional (default = 1e-7) Tolerance of BCD scheme. If the L1-norm between the current and previous sample couplings is under this threshold, then stop BCD scheme. max_iter_ot : int, optional (default = 100) Number of iterations to solve each of the two unbalanced optimal transport problems in each BCD iteration. tol_ot : float, optional (default = 1e-7) Tolerance of unbalanced solver for each of the two unbalanced optimal transport problems in each BCD iteration. log : bool, optional (default = False) If True then the cost and four dual vectors, including two from sample and two from feature couplings, are recorded. verbose : bool, optional (default = False) If True then print the COOT cost at every multiplier of `eval_bcd`-th iteration. Returns ------- ucoot : float UCOOT cost. log : dictionary, optional Returned if `log` is True. The keys are: error : array-like, float list of L1 norms between the current and previous sample coupling. duals_sample : (n_sample_x, n_sample_y)-tuple, float Pair of dual vectors when solving OT problem w.r.t the sample coupling. duals_feature : (n_feature_x, n_feature_y)-tuple, float Pair of dual vectors when solving OT problem w.r.t the feature coupling. linear : float Linear part of UCOOT cost. ucoot : float UCOOT cost. backend The proper backend for all input arrays References ---------- .. [71] Tran, H., Janati, H., Courty, N., Flamary, R., Redko, I., Demetci, P., & Singh, R. Unbalanced Co-Optimal Transport. AAAI Conference on Artificial Intelligence, 2023. """ if divergence != "kl": warnings.warn( "The computation of gradients is only supported for KL divergence, not \ for {} divergence".format(divergence) ) pi_samp, pi_feat, log_ucoot = unbalanced_co_optimal_transport( X=X, Y=Y, wx_samp=wx_samp, wx_feat=wx_feat, wy_samp=wy_samp, wy_feat=wy_feat, reg_marginals=reg_marginals, epsilon=epsilon, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=M_samp, M_feat=M_feat, rescale_plan=rescale_plan, init_pi=init_pi, init_duals=init_duals, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=True, verbose=verbose, **kwargs_solve, ) nx = log_ucoot["backend"] nx_samp, nx_feat = X.shape ny_samp, ny_feat = Y.shape # measures on rows and columns if wx_samp is None: wx_samp = nx.ones(nx_samp, type_as=X) / nx_samp if wx_feat is None: wx_feat = nx.ones(nx_feat, type_as=X) / nx_feat if wy_samp is None: wy_samp = nx.ones(ny_samp, type_as=Y) / ny_samp if wy_feat is None: wy_feat = nx.ones(ny_feat, type_as=Y) / ny_feat # extract parameters rho_x, rho_y = get_parameter_pair(reg_marginals) eps_samp, eps_feat = get_parameter_pair(epsilon) # calculate marginals pi1_samp, pi2_samp = nx.sum(pi_samp, 1), nx.sum(pi_samp, 0) pi1_feat, pi2_feat = nx.sum(pi_feat, 1), nx.sum(pi_feat, 0) m_samp, m_feat = nx.sum(pi1_samp), nx.sum(pi1_feat) m_wx_feat, m_wx_samp = nx.sum(wx_feat), nx.sum(wx_samp) m_wy_feat, m_wy_samp = nx.sum(wy_feat), nx.sum(wy_samp) # calculate subgradients gradX = 2 * X * (pi1_samp[:, None] * pi1_feat[None, :]) - 2 * nx.dot( nx.dot(pi_samp, Y), pi_feat.T ) # shape (nx_samp, nx_feat) gradY = 2 * Y * (pi2_samp[:, None] * pi2_feat[None, :]) - 2 * nx.dot( nx.dot(pi_samp.T, X), pi_feat ) # shape (ny_samp, ny_feat) grad_wx_samp = rho_x * (m_wx_feat - m_feat * pi1_samp / wx_samp) + eps_samp * ( m_wy_samp - pi1_samp / wx_samp ) grad_wx_feat = rho_x * (m_wx_samp - m_samp * pi1_feat / wx_feat) + eps_feat * ( m_wy_feat - pi1_feat / wx_feat ) grad_wy_samp = rho_y * (m_wy_feat - m_feat * pi2_samp / wy_samp) + eps_samp * ( m_wx_samp - pi2_samp / wy_samp ) grad_wy_feat = rho_y * (m_wy_samp - m_samp * pi2_feat / wy_feat) + eps_feat * ( m_wx_feat - pi2_feat / wy_feat ) # set gradients ucoot = log_ucoot["ucoot_cost"] ucoot = nx.set_gradients( ucoot, (X, Y, wx_samp, wx_feat, wy_samp, wy_feat), (gradX, gradY, grad_wx_samp, grad_wx_feat, grad_wy_samp, grad_wy_feat), ) if log: return ucoot, log_ucoot else: return ucoot def fused_unbalanced_gromov_wasserstein( Cx, Cy, wx=None, wy=None, reg_marginals=10, epsilon=0, divergence="kl", unbalanced_solver="mm", alpha=0, M=None, init_duals=None, init_pi=None, max_iter=100, tol=1e-7, max_iter_ot=500, tol_ot=1e-7, log=False, verbose=False, **kwargs_solve, ): r"""Compute the lower bound of the fused unbalanced Gromov-Wasserstein (FUGW) between two similarity matrices. In practice, this lower bound is used interchangeably with the true FUGW. More precisely, this function returns the transport plan between :math:`(\mathbf{C^X}, \mathbf{w_X})` and :math:`(\mathbf{C^Y}, \mathbf{w_Y})`, by solving the following problem using Block Coordinate Descent algorithm: .. math:: \mathop{\arg \min}_{\substack{\mathbf{P}, \mathbf{Q}: \\ mass(P) = mass(Q)}} &\quad \sum_{i,j,k,l} (\mathbf{C^X}_{i,k} - \mathbf{C^Y}_{j,l})^2 \mathbf{P}_{i,j} \mathbf{Q}_{k,l} + \frac{\alpha}{2} \sum_{i,j} (\mathbf{P}_{i,j} + \mathbf{Q}_{i,j}) \mathbf{M}_{i, j} \\ &+ \rho_1 \mathbf{Div}(\mathbf{P}_{\# 1} \mathbf{Q}_{\# 1}^T | \mathbf{w_X} \mathbf{w_X}^T) + \rho_2 \mathbf{Div}(\mathbf{P}_{\# 2} \mathbf{Q}_{\# 2}^T | \mathbf{w_Y} \mathbf{w_Y}^T) \\ &+ \varepsilon \mathbf{Div}(\mathbf{P} \otimes \mathbf{Q} | (\mathbf{w_X} \mathbf{w_Y}^T) \otimes (\mathbf{w_X} \mathbf{w_Y}^T)) Where: - :math:`\mathbf{C^X}`: Source similarity matrix - :math:`\mathbf{C^Y}`: Target similarity matrix - :math:`\mathbf{M}`: Sample matrix corresponding to the Wasserstein term - :math:`\mathbf{w_X}`: Distribution of the samples in the source space - :math:`\mathbf{w_Y}`: Distribution of the samples in the target space - :math:`\mathbf{Div}`: Either Kullback-Leibler divergence or half-squared L2 norm. .. note:: This function allows epsilon to be zero. In that case, `unbalanced_method` must be either "mm" or "lbfgsb". Parameters ---------- Cx : (n_sample_x, n_feature_x) array-like, float Source similarity matrix. Cy : (n_sample_y, n_feature_y) array-like, float Target similarity matrix. wx : (n_sample_x, ) array-like, float, optional (default = None) Histogram assigned on rows (samples) of matrix Cx. Uniform distribution by default. wy : (n_sample_y, ) array-like, float, optional (default = None) Histogram assigned on rows (samples) of matrix Cy. Uniform distribution by default. reg_marginals: float or indexable object of length 1 or 2 Marginal relaxation terms for sample and feature couplings. If `reg_marginals` is a scalar or an indexable object of length 1, then the same value is applied to both marginal relaxations. epsilon : scalar, float or int, optional (default = 0) Regularization parameters for entropic approximation of sample and feature couplings. Allow the case where `epsilon` contains 0. In that case, the MM solver is used by default instead of Sinkhorn solver. If `epsilon` is scalar, then the same value is applied to both regularization of sample and feature couplings. divergence : string, optional (default = "kl") - If `divergence` = "kl", then Div is the Kullback-Leibler divergence. - If `divergence` = "l2", then Div is the half squared Euclidean norm. unbalanced_solver : string, optional (default = "sinkhorn") Solver for the unbalanced OT subroutine. - If `divergence` = "kl", then `unbalanced_solver` can be: "sinkhorn", "sinkhorn_log", "mm", "lbfgsb" - If `divergence` = "l2", then `unbalanced_solver` can be "mm", "lbfgsb" alpha : scalar, float or int, optional (default = 0) Coeffficient parameter of linear terms with respect to the sample and feature couplings. If alpha is scalar, then the same alpha is applied to both linear terms. M : (n_sample_x, n_sample_y), float, optional (default = None) Sample matrix associated to the Wasserstein linear term on sample coupling. init_pi :(n_sample_x, n_sample_y) array-like, optional (default = None) Initialization of sample coupling. By default = :math:`w_X w_Y^T`. init_duals : tuple of vectors ((n_sample_x, ), (n_sample_y, )), optional (default = None). Initialization of sample and feature dual vectors if using Sinkhorn algorithm. Zero vectors by default. max_iter : int, optional (default = 100) Number of Block Coordinate Descent (BCD) iterations. tol : float, optional (default = 1e-7) Tolerance of BCD scheme. If the L1-norm between the current and previous sample couplings is under this threshold, then stop BCD scheme. max_iter_ot : int, optional (default = 100) Number of iterations to solve each of the two unbalanced optimal transport problems in each BCD iteration. tol_ot : float, optional (default = 1e-7) Tolerance of unbalanced solver for each of the two unbalanced optimal transport problems in each BCD iteration. log : bool, optional (default = False) If True then the cost and four dual vectors, including two from sample and two from feature couplings, are recorded. verbose : bool, optional (default = False) If True then print the COOT cost at every multiplier of `eval_bcd`-th iteration. Returns ------- pi_samp : (n_sample_x, n_sample_y) array-like, float Sample coupling matrix. In practice, we use this matrix as solution of FUGW. pi_samp2 : (n_sample_x, n_sample_y) array-like, float Second sample coupling matrix. In practice, we usually ignore this output. log : dictionary, optional Returned if `log` is True. The keys are: error : array-like, float list of L1 norms between the current and previous sample couplings. duals : (n_sample_x, n_sample_y)-tuple, float Pair of dual vectors when solving OT problem w.r.t the sample coupling. linear : float Linear part of FUGW cost. fugw_cost : float Total FUGW cost. backend The proper backend for all input arrays References ---------- .. [70] Thual, A., Tran, H., Zemskova, T., Courty, N., Flamary, R., Dehaene, S., & Thirion, B. Aligning individual brains with Fused Unbalanced Gromov-Wasserstein. Advances in Neural Information Systems, 35 (2022). .. [72] Thibault Séjourné, François-Xavier Vialard, & Gabriel Peyré. The Unbalanced Gromov Wasserstein Distance: Conic Formulation and Relaxation. Neural Information Processing Systems, 34 (2021). """ alpha = (alpha / 2, alpha / 2) pi_samp, pi_feat, dict_log = fused_unbalanced_across_spaces_divergence( X=Cx, Y=Cy, wx_samp=wx, wx_feat=wx, wy_samp=wy, wy_feat=wy, reg_marginals=reg_marginals, epsilon=epsilon, reg_type="joint", divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=M, M_feat=M, rescale_plan=True, init_pi=(init_pi, init_pi), init_duals=(init_duals, init_duals), max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=True, verbose=verbose, **kwargs_solve, ) if log: log_fugw = { "error": dict_log["error"], "duals": dict_log["duals_sample"], "linear_cost": dict_log["linear_cost"], "fugw_cost": dict_log["ucoot_cost"], "backend": dict_log["backend"], } return pi_samp, pi_feat, log_fugw else: return pi_samp, pi_feat def fused_unbalanced_gromov_wasserstein2( Cx, Cy, wx=None, wy=None, reg_marginals=10, epsilon=0, divergence="kl", unbalanced_solver="mm", alpha=0, M=None, init_duals=None, init_pi=None, max_iter=100, tol=1e-7, max_iter_ot=500, tol_ot=1e-7, log=False, verbose=False, **kwargs_solve, ): r"""Compute the lower bound of the fused unbalanced Gromov-Wasserstein (FUGW) between two similarity matrices. In practice, this lower bound is used interchangeably with the true FUGW. More precisely, this function returns the lower bound of the fused unbalanced Gromov-Wasserstein cost between :math:`(\mathbf{C^X}, \mathbf{w_X})` and :math:`(\mathbf{C^Y}, \mathbf{w_Y})`, by solving the following problem using Block Coordinate Descent algorithm: .. math:: \mathop{\min}_{\substack{\mathbf{P}, \mathbf{Q}: \\ mass(P) = mass(Q)}} &\quad \sum_{i,j,k,l} (\mathbf{C^X}_{i,k} - \mathbf{C^Y}_{j,l})^2 \mathbf{P}_{i,j} \mathbf{Q}_{k,l} + \frac{\alpha}{2} \sum_{i,j} (\mathbf{P}_{i,j} + \mathbf{Q}_{i,j}) \mathbf{M}_{i, j} \\ &+ \rho_1 \mathbf{Div}(\mathbf{P}_{\# 1} \mathbf{Q}_{\# 1}^T | \mathbf{w_X} \mathbf{w_X}^T) + \rho_2 \mathbf{Div}(\mathbf{P}_{\# 2} \mathbf{Q}_{\# 2}^T | \mathbf{w_Y} \mathbf{w_Y}^T) \\ &+ \varepsilon \mathbf{Div}(\mathbf{P} \otimes \mathbf{Q} | (\mathbf{w_X} \mathbf{w_Y}^T) \otimes (\mathbf{w_X} \mathbf{w_Y}^T)) Where: - :math:`\mathbf{C^X}`: Source similarity matrix - :math:`\mathbf{C^Y}`: Target similarity matrix - :math:`\mathbf{M}`: Sample matrix corresponding to the Wasserstein term - :math:`\mathbf{w_X}`: Distribution of the samples in the source space - :math:`\mathbf{w_Y}`: Distribution of the samples in the target space - :math:`\mathbf{Div}`: Either Kullback-Leibler divergence or half-squared L2 norm. .. note:: This function allows `epsilon` to be zero. In that case, unbalanced_method must be either "mm" or "lbfgsb". Also the computation of gradients is only supported for KL divergence, but not for half squared-L2 norm. In case of half squared-L2 norm, the calculation of KL divergence will be used. Parameters ---------- Cx : (n_sample_x, n_feature_x) array-like, float Source similarity matrix. Cy : (n_sample_y, n_feature_y) array-like, float Target similarity matrix. wx : (n_sample_x, ) array-like, float, optional (default = None) Histogram assigned on rows (samples) of matrix Cx. Uniform distribution by default. wy : (n_sample_y, ) array-like, float, optional (default = None) Histogram assigned on rows (samples) of matrix Cy. Uniform distribution by default. reg_marginals: float or indexable object of length 1 or 2 Marginal relaxation terms for sample and feature couplings. If `reg_marginals` is a scalar or an indexable object of length 1, then the same value is applied to both marginal relaxations. epsilon : scalar, float or int, optional (default = 0) Regularization parameters for entropic approximation of sample and feature couplings. Allow the case where `epsilon` contains 0. In that case, the MM solver is used by default instead of Sinkhorn solver. If `epsilon` is scalar, then the same value is applied to both regularization of sample and feature couplings. divergence : string, optional (default = "kl") - If `divergence` = "kl", then Div is the Kullback-Leibler divergence. - If `divergence` = "l2", then Div is the half squared Euclidean norm. unbalanced_solver : string, optional (default = "sinkhorn") Solver for the unbalanced OT subroutine. - If `divergence` = "kl", then `unbalanced_solver` can be: "sinkhorn", "sinkhorn_log", "mm", "lbfgsb" - If `divergence` = "l2", then `unbalanced_solver` can be "mm", "lbfgsb" alpha : scalar, float or int, optional (default = 0) Coeffficient parameter of linear terms with respect to the sample and feature couplings. If alpha is scalar, then the same alpha is applied to both linear terms. M : (n_sample_x, n_sample_y), float, optional (default = None) Sample matrix associated to the Wasserstein linear term on sample coupling. init_pi :(n_sample_x, n_sample_y) array-like, optional (default = None) Initialization of sample coupling. By default = :math:`w_X w_Y^T`. init_duals : tuple of vectors ((n_sample_x, ), (n_sample_y, )), optional (default = None). Initialization of sample and feature dual vectors if using Sinkhorn algorithm. Zero vectors by default. max_iter : int, optional (default = 100) Number of Block Coordinate Descent (BCD) iterations. tol : float, optional (default = 1e-7) Tolerance of BCD scheme. If the L1-norm between the current and previous sample couplings is under this threshold, then stop BCD scheme. max_iter_ot : int, optional (default = 100) Number of iterations to solve each of the two unbalanced optimal transport problems in each BCD iteration. tol_ot : float, optional (default = 1e-7) Tolerance of unbalanced solver for each of the two unbalanced optimal transport problems in each BCD iteration. log : bool, optional (default = False) If True then the cost and four dual vectors, including two from sample and two from feature couplings, are recorded. verbose : bool, optional (default = False) If True then print the COOT cost at every multiplier of `eval_bcd`-th iteration. Returns ------- fugw : float Total FUGW cost log : dictionary, optional Returned if `log` is True. The keys are: error : array-like, float list of L1 norms between the current and previous sample couplings. duals : (n_sample_x, n_sample_y)-tuple, float Pair of dual vectors when solving OT problem w.r.t the sample coupling. linear : float Linear part of FUGW cost. fugw_cost : float Total FUGW cost. backend The proper backend for all input arrays References ---------- .. [70] Thual, A., Tran, H., Zemskova, T., Courty, N., Flamary, R., Dehaene, S., & Thirion, B. Aligning individual brains with Fused Unbalanced Gromov-Wasserstein. Advances in Neural Information Systems, 35 (2022). .. [72] Thibault Séjourné, François-Xavier Vialard, & Gabriel Peyré. The Unbalanced Gromov Wasserstein Distance: Conic Formulation and Relaxation. Neural Information Processing Systems, 34 (2021). """ if divergence != "kl": warnings.warn( "The computation of gradients is only supported for KL divergence, \ but not for {} divergence. The gradient of the KL case will be used.".format( divergence ) ) pi_samp, pi_feat, log_fugw = fused_unbalanced_gromov_wasserstein( Cx=Cx, Cy=Cy, wx=wx, wy=wy, reg_marginals=reg_marginals, epsilon=epsilon, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M, init_duals=init_duals, init_pi=init_pi, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=True, verbose=verbose, **kwargs_solve, ) nx = log_fugw["backend"] sx, sy = Cx.shape[0], Cy.shape[0] # measures on rows and columns if wx is None: wx = nx.ones(sx, type_as=Cx) / sx if wy is None: wy = nx.ones(sy, type_as=Cy) / sy # calculate marginals pi1_samp, pi2_samp = nx.sum(pi_samp, 1), nx.sum(pi_samp, 0) pi1_feat, pi2_feat = nx.sum(pi_feat, 1), nx.sum(pi_feat, 0) m_samp, m_feat = nx.sum(pi1_samp), nx.sum(pi1_feat) m_wx, m_wy = nx.sum(wx), nx.sum(wy) # calculate subgradients gradX = 2 * Cx * (pi1_samp[:, None] * pi1_feat[None, :]) - 2 * nx.dot( nx.dot(pi_samp, Cy), pi_feat.T ) # shape (nx_samp, nx_feat) gradY = 2 * Cy * (pi2_samp[:, None] * pi2_feat[None, :]) - 2 * nx.dot( nx.dot(pi_samp.T, Cx), pi_feat ) # shape (ny_samp, ny_feat) gradM = alpha / 2 * (pi_samp + pi_feat) rho_x, rho_y = get_parameter_pair(reg_marginals) grad_wx = ( 2 * m_wx * (rho_x + epsilon * m_wy**2) - (rho_x + epsilon) * (m_feat * pi1_samp + m_samp * pi1_feat) / wx ) grad_wy = ( 2 * m_wy * (rho_y + epsilon * m_wx**2) - (rho_y + epsilon) * (m_feat * pi2_samp + m_samp * pi2_feat) / wy ) # set gradients fugw = log_fugw["fugw_cost"] fugw = nx.set_gradients( fugw, (Cx, Cy, M, wx, wy), (gradX, gradY, gradM, grad_wx, grad_wy) ) if log: return fugw, log_fugw else: return fugw python-pot-0.9.7+dfsg/ot/gromov/_utils.py000066400000000000000000001251031525521106200203740ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Gromov-Wasserstein and Fused-Gromov-Wasserstein utils. """ # Author: Erwan Vautier # Nicolas Courty # Rémi Flamary # Titouan Vayer # Cédric Vincent-Cuaz # Quang Huy Tran # # License: MIT License from ..utils import list_to_array, euclidean_distances from ..backend import get_backend from ..lp import emd try: from networkx.algorithms.community import asyn_fluidc from networkx import from_numpy_array networkx_import = True except ImportError: networkx_import = False try: from sklearn.cluster import SpectralClustering, KMeans sklearn_import = True except ImportError: sklearn_import = False import numpy as np import warnings def _transform_matrix(C1, C2, loss_fun="square_loss", nx=None): r"""Return transformed structure matrices for Gromov-Wasserstein fast computation Returns the matrices involved in the computation of :math:`\mathcal{L}(\mathbf{C_1}, \mathbf{C_2})` with the selected loss function as the loss function of Gromov-Wasserstein discrepancy. The matrices are computed as described in Proposition 1 in :ref:`[12] ` Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space The square-loss function :math:`L(a, b) = |a - b|^2` is read as : .. math:: L(a, b) = f_1(a) + f_2(b) - h_1(a) h_2(b) \mathrm{with} \ f_1(a) &= a^2 f_2(b) &= b^2 h_1(a) &= a h_2(b) &= 2b The kl-loss function :math:`L(a, b) = a \log\left(\frac{a}{b}\right) - a + b` is read as : .. math:: L(a, b) = f_1(a) + f_2(b) - h_1(a) h_2(b) \mathrm{with} \ f_1(a) &= a \log(a) - a f_2(b) &= b h_1(a) &= a h_2(b) &= \log(b) Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space loss_fun : str, optional Name of loss function to use: either 'square_loss' or 'kl_loss' (default='square_loss') nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ------- fC1 : array-like, shape (ns, ns) :math:`\mathbf{f1}(\mathbf{C1})` matrix in Eq. (6) fC2 : array-like, shape (nt, nt) :math:`\mathbf{f2}(\mathbf{C2})` matrix in Eq. (6) hC1 : array-like, shape (ns, ns) :math:`\mathbf{h1}(\mathbf{C1})` matrix in Eq. (6) hC2 : array-like, shape (nt, nt) :math:`\mathbf{h2}(\mathbf{C2})` matrix in Eq. (6) .. _references-transform_matrix: References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. """ if nx is None: C1, C2 = list_to_array(C1, C2) nx = get_backend(C1, C2) if loss_fun == "square_loss": def f1(a): return a**2 def f2(b): return b**2 def h1(a): return a def h2(b): return 2 * b elif loss_fun == "kl_loss": def f1(a): return a * nx.log(a + 1e-18) - a def f2(b): return b def h1(a): return a def h2(b): return nx.log(b + 1e-18) else: raise ValueError( f"Unknown `loss_fun='{loss_fun}'`. Use one of: {'square_loss', 'kl_loss'}." ) fC1 = f1(C1) fC2 = f2(C2) hC1 = h1(C1) hC2 = h2(C2) return fC1, fC2, hC1, hC2 def init_matrix(C1, C2, p, q, loss_fun="square_loss", nx=None): r"""Return loss matrices and tensors for Gromov-Wasserstein fast computation Returns the value of :math:`\mathcal{L}(\mathbf{C_1}, \mathbf{C_2}) \otimes \mathbf{T}` with the selected loss function as the loss function of Gromov-Wasserstein discrepancy. The matrices are computed as described in Proposition 1 in :ref:`[12] ` Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{T}`: A coupling between those two spaces The square-loss function :math:`L(a, b) = |a - b|^2` is read as : .. math:: L(a, b) = f_1(a) + f_2(b) - h_1(a) h_2(b) \mathrm{with} \ f_1(a) &= a^2 f_2(b) &= b^2 h_1(a) &= a h_2(b) &= 2b The kl-loss function :math:`L(a, b) = a \log\left(\frac{a}{b}\right) - a + b` is read as : .. math:: L(a, b) = f_1(a) + f_2(b) - h_1(a) h_2(b) \mathrm{with} \ f_1(a) &= a \log(a) - a f_2(b) &= b h_1(a) &= a h_2(b) &= \log(b) Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,) Probability distribution in the source space q : array-like, shape (nt,) Probability distribution in the target space loss_fun : str, optional Name of loss function to use: either 'square_loss' or 'kl_loss' (default='square_loss') nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ------- constC : array-like, shape (ns, nt) Constant :math:`\mathbf{C}` matrix in Eq. (6) hC1 : array-like, shape (ns, ns) :math:`\mathbf{h1}(\mathbf{C1})` matrix in Eq. (6) hC2 : array-like, shape (nt, nt) :math:`\mathbf{h2}(\mathbf{C2})` matrix in Eq. (6) .. _references-init-matrix: References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. """ if nx is None: C1, C2, p, q = list_to_array(C1, C2, p, q) nx = get_backend(C1, C2, p, q) fC1, fC2, hC1, hC2 = _transform_matrix(C1, C2, loss_fun, nx) constC1 = nx.dot( nx.dot(fC1, nx.reshape(p, (-1, 1))), nx.ones((1, len(q)), type_as=q) ) constC2 = nx.dot( nx.ones((len(p), 1), type_as=p), nx.dot(nx.reshape(q, (1, -1)), fC2.T) ) constC = constC1 + constC2 return constC, hC1, hC2 def tensor_product(constC, hC1, hC2, T, nx=None): r"""Return the tensor for Gromov-Wasserstein fast computation The tensor is computed as described in Proposition 1 Eq. (6) in :ref:`[12] ` Parameters ---------- constC : array-like, shape (ns, nt) Constant :math:`\mathbf{C}` matrix in Eq. (6) hC1 : array-like, shape (ns, ns) :math:`\mathbf{h1}(\mathbf{C1})` matrix in Eq. (6) hC2 : array-like, shape (nt, nt) :math:`\mathbf{h2}(\mathbf{C2})` matrix in Eq. (6) nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ------- tens : array-like, shape (`ns`, `nt`) :math:`\mathcal{L}(\mathbf{C_1}, \mathbf{C_2}) \otimes \mathbf{T}` tensor-matrix multiplication result .. _references-tensor-product: References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. """ if nx is None: constC, hC1, hC2, T = list_to_array(constC, hC1, hC2, T) nx = get_backend(constC, hC1, hC2, T) A = -nx.dot(nx.dot(hC1, T), hC2.T) tens = constC + A # tens -= tens.min() return tens def gwloss(constC, hC1, hC2, T, nx=None): r"""Return the Loss for Gromov-Wasserstein The loss is computed as described in Proposition 1 Eq. (6) in :ref:`[12] ` Parameters ---------- constC : array-like, shape (ns, nt) Constant :math:`\mathbf{C}` matrix in Eq. (6) hC1 : array-like, shape (ns, ns) :math:`\mathbf{h1}(\mathbf{C1})` matrix in Eq. (6) hC2 : array-like, shape (nt, nt) :math:`\mathbf{h2}(\mathbf{C2})` matrix in Eq. (6) T : array-like, shape (ns, nt) Current value of transport matrix :math:`\mathbf{T}` nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ------- loss : float Gromov-Wasserstein loss .. _references-gwloss: References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. """ tens = tensor_product(constC, hC1, hC2, T, nx) if nx is None: tens, T = list_to_array(tens, T) nx = get_backend(tens, T) return nx.sum(tens * T) def gwggrad(constC, hC1, hC2, T, nx=None): r"""Return the gradient for Gromov-Wasserstein The gradient is computed as described in Proposition 2 in :ref:`[12] ` Parameters ---------- constC : array-like, shape (ns, nt) Constant :math:`\mathbf{C}` matrix in Eq. (6) hC1 : array-like, shape (ns, ns) :math:`\mathbf{h1}(\mathbf{C1})` matrix in Eq. (6) hC2 : array-like, shape (nt, nt) :math:`\mathbf{h2}(\mathbf{C2})` matrix in Eq. (6) T : array-like, shape (ns, nt) Current value of transport matrix :math:`\mathbf{T}` nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ------- grad : array-like, shape (`ns`, `nt`) Gromov-Wasserstein gradient .. _references-gwggrad: References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. """ return 2 * tensor_product(constC, hC1, hC2, T, nx) # [12] Prop. 2 misses a 2 factor def init_matrix_semirelaxed(C1, C2, p, loss_fun="square_loss", nx=None): r"""Return loss matrices and tensors for semi-relaxed Gromov-Wasserstein fast computation Returns the value of :math:`\mathcal{L}(\mathbf{C_1}, \mathbf{C_2}) \otimes \mathbf{T}` with the selected loss function as the loss function of semi-relaxed Gromov-Wasserstein discrepancy. The matrices are computed as described in Proposition 1 in :ref:`[12] ` and adapted to the semi-relaxed problem where the second marginal is not a constant anymore. Where : - :math:`\mathbf{C_1}`: Metric cost matrix in the source space - :math:`\mathbf{C_2}`: Metric cost matrix in the target space - :math:`\mathbf{T}`: A coupling between those two spaces The square-loss function :math:`L(a, b) = |a - b|^2` is read as : .. math:: L(a, b) = f_1(a) + f_2(b) - h_1(a) h_2(b) \mathrm{with} \ f_1(a) &= a^2 f_2(b) &= b^2 h_1(a) &= a h_2(b) &= 2b The kl-loss function :math:`L(a, b) = a \log\left(\frac{a}{b}\right) - a + b` is read as : .. math:: L(a, b) = f_1(a) + f_2(b) - h_1(a) h_2(b) \mathrm{with} \ f_1(a) &= a \log(a) - a f_2(b) &= b h_1(a) &= a h_2(b) &= \log(b) Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space C2 : array-like, shape (nt, nt) Metric cost matrix in the target space p : array-like, shape (ns,) Probability distribution in the source space loss_fun : str, optional Name of loss function to use: either 'square_loss' or 'kl_loss' (default='square_loss') nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ------- constC : array-like, shape (ns, nt) Constant :math:`\mathbf{C}` matrix in Eq. (6) adapted to srGW hC1 : array-like, shape (ns, ns) :math:`\mathbf{h1}(\mathbf{C1})` matrix in Eq. (6) hC2 : array-like, shape (nt, nt) :math:`\mathbf{h2}(\mathbf{C2})` matrix in Eq. (6) fC2t: array-like, shape (nt, nt) :math:`\mathbf{f2}(\mathbf{C2})^\top` matrix in Eq. (6) .. _references-init-matrix: References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2022. """ if nx is None: C1, C2, p = list_to_array(C1, C2, p) nx = get_backend(C1, C2, p) fC1, fC2, hC1, hC2 = _transform_matrix(C1, C2, loss_fun, nx) constC = nx.dot( nx.dot(fC1, nx.reshape(p, (-1, 1))), nx.ones((1, C2.shape[0]), type_as=p) ) fC2t = fC2.T return constC, hC1, hC2, fC2t def semirelaxed_init_plan( C1, C2, p, M=None, alpha=1.0, method="product", use_target=True, random_state=0, nx=None, ): r""" Heuristics to initialize the semi-relaxed (F)GW transport plan :math:`\mathbf{T} \in \mathcal{U}_{nt}(\mathbf{p})`, between a graph :math:`(\mathbf{C1}, \mathbf{p})` and a structure matrix :math:`\mathbf{C2}`, where :math:`\mathcal{U}_{nt}(\mathbf{p}) = \{\mathbf{T} \in \mathbb{R}_{+}^{ns * nt}, \mathbf{T} \mathbf{1}_{nt} = \mathbf{p} \}`. Available methods are: - "product" or "random_product": :math:`\mathbf{T} = \mathbf{pq}^{T}` with :math:`\mathbf{q}` uniform or randomly samples in the nt probability simplex. - "random": random sampling in :math:`\mathcal{U}_{nt}(\mathbf{p})`. - "fluid": Fluid algorithm from networkx for graph partitioning. - "spectral", "kmeans" : Spectral or Kmeans clustering from sklearn. - "fluid_soft", "spectral_soft", "kmeans_soft": :math:`\mathbf{T}_0` given by corresponding clustering with target marginal :math:`\mathbf{q}_0`, further centered as :math:`\mathbf{T} = (\mathbf{T}_0 + \mathbf{pq}_0^T) / 2` . If a metric cost matrix between features across domains :math:`\mathbf{M}` is a provided, it will be used as cost matrix in a semi-relaxed Wasserstein problem providing :math:`\mathbf{T}_M \in \mathcal{U}_{nt}(\mathbf{p})`. Then the outputted transport plan is :math:`\alpha \mathbf{T} + (1 - \alpha ) \mathbf{T}_{M}`. Parameters ---------- C1 : array-like, shape (ns, ns) Metric cost matrix in the source space. C2 : array-like, shape (nt, nt) Metric cost matrix in the target space. p : array-like, shape (ns,), optional. Probability distribution in the source space. If let to None, uniform weights are assumed on C1. M : array-like, shape (ns, nt), optional. Metric cost matrix between features across domains. alpha : float, optional Trade-off parameter (0 <= alpha <= 1) method : str, optional Method to initialize the transport plan. The default is 'product'. use_target : bool, optional. Whether or not to use the target structure/features to further align transport plan provided by the `method`. random_state: int, optional Random seed used for stochastic methods. nx : backend, optional POT backend. Returns ------- T : array-like, shape (ns, ns) Admissible transport plan for the sr(F)GW problems. References ---------- .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2022. """ list_partitioning_methods = [ "fluid", "spectral", "kmeans", "fluid_soft", "spectral_soft", "kmeans_soft", ] if method not in list_partitioning_methods + [ "product", "random_product", "random", ]: raise ValueError(f"Unsupported initialization method = {method}.") if (method in ["kmeans", "kmeans_soft"]) and (not sklearn_import): raise ValueError(f"Scikit-learn must be installed to use method = {method}") if (method in ["fluid", "fluid_soft"]) and (not networkx_import): raise ValueError(f"Networkx must be installed to use method = {method}") if nx is None: nx = get_backend(C1, C2, p, M) n = C1.shape[0] m = C2.shape[0] min_size = min(n, m) if method in list_partitioning_methods: if n > m: # partition C1 to deduce map from C1 to C2 C_to_partition = nx.to_numpy(C1) elif m > n: # partition C2 to deduce map from C1 to C2 C_to_partition = nx.to_numpy(C2) else: # equal size -> simple Wasserstein alignment C_to_partition = None warnings.warn( "Both structures have the same size so no partitioning is" "performed to initialize the transport plan even though" f"initialization method is {method}", stacklevel=2, ) def get_transport_from_partition(part): if n > m: # partition C1 to deduce map from C1 to C2 T_ = nx.eye(m, type_as=C1)[part] T_ = p[:, None] * T_ q = nx.sum(T_, 0) factored_C1 = nx.dot(nx.dot(T_.T, C1), T_) / nx.outer(q, q) # alignment of both structure seen as feature matrices if use_target: M_structure = euclidean_distances(factored_C1, C2) T_emd = emd(q, q, M_structure) inv_q = 1.0 / q T = nx.dot(T_, inv_q[:, None] * T_emd) else: T = T_ elif m > n: T_ = nx.eye(n, type_as=C1)[part] / m # assume uniform masses on C2 q = nx.sum(T_, 0) factored_C2 = nx.dot(nx.dot(T_.T, C2), T_) / nx.outer(q, q) # alignment of both structure seen as feature matrices M_structure = euclidean_distances(factored_C2, C1) T_emd = emd(q, p, M_structure) inv_q = 1.0 / q T = nx.dot(T_, inv_q[:, None] * T_emd).T q = nx.sum(T, 0) # uniform one else: # alignment of both structure seen as feature matrices M_structure = euclidean_distances(C1, C2) q = p T = emd(p, q, M_structure) return T, q # Handle initialization via structure information if method == "product": q = nx.ones(m, type_as=C1) / m T = nx.outer(p, q) elif method == "random_product": np.random.seed(random_state) q = np.random.uniform(0, m, size=(m,)) q = q / q.sum() q = nx.from_numpy(q, type_as=p) T = nx.outer(p, q) elif method == "random": np.random.seed(random_state) U = np.random.uniform(0, n * m, size=(n, m)) U = (p / U.sum(1))[:, None] * U T = nx.from_numpy(U, type_as=C1) elif method in ["fluid", "fluid_soft"]: # compute fluid partitioning on the biggest graph if C_to_partition is None: T, q = get_transport_from_partition(None) else: graph = from_numpy_array(C_to_partition) part_sets = asyn_fluidc(graph, min_size, seed=random_state) part = np.zeros(C_to_partition.shape[0], dtype=int) for iset_, set_ in enumerate(part_sets): set_ = list(set_) part[set_] = iset_ part = nx.from_numpy(part) T, q = get_transport_from_partition(part) if "soft" in method: T = (T + nx.outer(p, q)) / 2.0 elif method in ["spectral", "spectral_soft"]: # compute spectral partitioning on the biggest graph if C_to_partition is None: T, q = get_transport_from_partition(None) else: sc = SpectralClustering( n_clusters=min_size, random_state=random_state, affinity="precomputed" ).fit(C_to_partition) part = sc.labels_ T, q = get_transport_from_partition(part) if "soft" in method: T = (T + nx.outer(p, q)) / 2.0 elif method in ["kmeans", "kmeans_soft"]: # compute spectral partitioning on the biggest graph if C_to_partition is None: T, q = get_transport_from_partition(None) else: km = KMeans(n_clusters=min_size, random_state=random_state, n_init=1).fit( C_to_partition ) part = km.labels_ T, q = get_transport_from_partition(part) if "soft" in method: T = (T + nx.outer(p, q)) / 2.0 if M is not None: # Add feature information solving a semi-relaxed Wasserstein problem # get minimum by rows as binary mask TM = nx.ones(1, type_as=p) * (M == nx.reshape(nx.min(M, axis=1), (-1, 1))) TM *= nx.reshape((p / nx.sum(TM, axis=1)), (-1, 1)) T = alpha * T + (1 - alpha) * TM return T def update_barycenter_structure( Ts, Cs, lambdas, p=None, loss_fun="square_loss", target=True, check_zeros=True, nx=None, ): r""" Updates :math:`\mathbf{C}` according to the inner loss L with the `S` :math:`\mathbf{T}_s` couplings calculated at each iteration of variants of the GW barycenter problem (e.g GW :ref:`[12]`, srGW :ref:`[48]`). If `target=True` it solves for: .. math:: \mathbf{C}^* = \mathop{\arg \min}_{\mathbf{C}\in \mathbb{R}^{N \times N}} \quad \sum_s \lambda_s \sum_{i,j,k,l} L(\mathbf{C}^{(s)}_{i,k}, \mathbf{C}_{j,l}) \mathbf{T}^{(s)}_{i,j} \mathbf{T}^{(s)}_{k,l} Else it solves the symmetric problem: .. math:: \mathbf{C}^* = \mathop{\arg \min}_{\mathbf{C}\in \mathbb{R}^{N \times N}} \quad \sum_s \lambda_s \sum_{i,j,k,l} L(\mathbf{C}_{j,l}, \mathbf{C}^{(s)}_{i,k}) \mathbf{T}^{(s)}_{i,j} \mathbf{T}^{(s)}_{k,l} Where : - :math:`\mathbf{C}^{(s)}`: pairwise matrix in the s^{th} source space . - :math:`\mathbf{C}`: pairwise matrix in the target space. - :math:`L`: inner divergence for the GW loss Parameters ---------- Ts : list of S array-like of shape (ns, N) if `target=True` else (N, ns). The `S` :math:`\mathbf{T}_s` couplings calculated at each iteration. Cs : list of S array-like, shape(ns, ns) Metric cost matrices. lambdas : list of float, List of the `S` spaces' weights. p : array-like, shape (N,) or (S,N) Masses or list of masses in the targeted barycenter. loss_fun : str, optional. Default is 'square_loss' Name of loss function to use in ['square_loss', 'kl_loss']. target: bool, optional. Default is True. Whether the barycenter is positioned as target (True) or source (False). check_zeros: bool, optional. Default is True. Whether to check if marginals on the barycenter contains zeros or not. Can be set to False to gain time if marginals are known to be positive. nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ---------- C : array-like, shape (`nt`, `nt`) Updated :math:`\mathbf{C}` matrix. References ---------- .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2022. """ if nx is None: arr = [*Ts, *Cs, p] nx = get_backend(*arr) S = len(Ts) if p is None: p = nx.concatenate( [nx.sum(Ts[s], int(not target))[None, :] for s in range(S)], axis=0 ) # compute coefficients for the barycenter coming from marginals if len(p.shape) == 1: # shared target masses potentially with zeros if check_zeros: inv_p = nx.nan_to_num(1.0 / p, nan=1.0, posinf=1.0, neginf=1.0) else: inv_p = 1.0 / p prod = nx.outer(inv_p, inv_p) else: quotient = sum([lambdas[s] * nx.outer(p[s], p[s]) for s in range(S)]) if check_zeros: prod = nx.nan_to_num(1.0 / quotient, nan=1.0, posinf=1.0, neginf=1.0) else: prod = 1.0 / quotient # compute coefficients for the barycenter coming from Ts and Cs if loss_fun == "square_loss": if target: list_structures = [ lambdas[s] * nx.dot(nx.dot(Ts[s].T, Cs[s]), Ts[s]) for s in range(S) ] else: list_structures = [ lambdas[s] * nx.dot(nx.dot(Ts[s], Cs[s]), Ts[s].T) for s in range(S) ] return sum(list_structures) * prod elif loss_fun == "kl_loss": if target: list_structures = [ lambdas[s] * nx.dot(nx.dot(Ts[s].T, Cs[s]), Ts[s]) for s in range(S) ] return sum(list_structures) * prod else: list_structures = [ lambdas[s] * nx.dot(nx.dot(Ts[s], nx.log(nx.maximum(Cs[s], 1e-16))), Ts[s].T) for s in range(S) ] return nx.exp(sum(list_structures) * prod) else: raise ValueError(f"not supported loss_fun = {loss_fun}") def update_barycenter_feature( Ts, Ys, lambdas, p=None, loss_fun="square_loss", target=True, check_zeros=True, nx=None, ): r"""Updates the feature with respect to the `S` :math:`\mathbf{T}_s` couplings calculated at each iteration of variants of the FGW barycenter problem with inner wasserstein loss `loss_fun` (e.g FGW :ref:`[24]`, srFGW :ref:`[48]`). If `target=True` the barycenter is considered as the target else as the source. Parameters ---------- Ts : list of S array-like of shape (ns, N) if `target=True` else (N, ns). The `S` :math:`\mathbf{T}_s` couplings calculated at each iteration. Ys : list of S array-like, shape (ns, d) Feature matrices. lambdas : list of float List of the `S` spaces' weights p : array-like, shape (N,) or (S,N) Masses or list of masses in the targeted barycenter. loss_fun : str, optional. Default is 'square_loss' Name of loss function to use in ['square_loss']. target: bool, optional. Default is True. Whether the barycenter is positioned as target (True) or source (False). check_zeros: bool, optional. Default is True. Whether to check if marginals on the barycenter contains zeros or not. Can be set to False to gain time if marginals are known to be positive. nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ------- X : array-like, shape (N, d) References ---------- .. [24] Vayer Titouan, Chapel Laetitia, Flamary Rémi, Tavenard Romain and Courty Nicolas "Optimal Transport for structured data with application on graphs" International Conference on Machine Learning (ICML). 2019. .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2022. """ if nx is None: arr = [*Ts, *Ys, p] nx = get_backend(*arr) if loss_fun != "square_loss": raise ValueError(f"not supported loss_fun = {loss_fun}") S = len(Ts) if target: list_features = [lambdas[s] * nx.dot(Ts[s].T, Ys[s]) for s in range(S)] else: list_features = [lambdas[s] * nx.dot(Ts[s], Ys[s]) for s in range(S)] if p is None: p = nx.concatenate( [nx.sum(Ts[s], int(not target))[None, :] for s in range(S)], axis=0 ) if len(p.shape) == 1: # shared target masses potentially with zeros if check_zeros: inv_p = nx.nan_to_num(1.0 / p, nan=1.0, posinf=1.0, neginf=1.0) else: inv_p = 1.0 / p else: p_sum = sum([lambdas[s] * p[s] for s in range(S)]) if check_zeros: inv_p = nx.nan_to_num(1.0 / p_sum, nan=1.0, posinf=1.0, neginf=1.0) else: inv_p = 1.0 / p_sum return sum(list_features) * inv_p[:, None] ############################################################################ # Methods related to fused unbalanced GW and unbalanced Co-Optimal Transport. ############################################################################ def div_to_product(pi, a, b, pi1=None, pi2=None, divergence="kl", mass=True, nx=None): r"""Fast computation of the Bregman divergence between an arbitrary measure and a product measure. Only support for Kullback-Leibler and half-squared L2 divergences. - For half-squared L2 divergence: .. math:: \frac{1}{2} || \pi - a \otimes b ||^2 = \frac{1}{2} \Big[ \sum_{i, j} \pi_{ij}^2 + (\sum_i a_i^2) ( \sum_j b_j^2) - 2 \sum_{i, j} a_i \pi_{ij} b_j \Big] - For Kullback-Leibler divergence: .. math:: KL(\pi | a \otimes b) = \langle \pi, \log \pi \rangle - \langle \pi_1, \log a \rangle - \langle \pi_2, \log b \rangle - m(\pi) + m(a) m(b) where : - :math:`\pi` is the (`dim_a`, `dim_b`) transport plan - :math:`\pi_1` and :math:`\pi_2` are the marginal distributions - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target unbalanced distributions - :math:`m` denotes the mass of the measure Parameters ---------- pi : array-like (dim_a, dim_b) Transport plan a : array-like (dim_a,) Unnormalized histogram of dimension `dim_a` b : array-like (dim_b,) Unnormalized histogram of dimension `dim_b` pi1 : array-like (dim_a,), optional (default = None) Marginal distribution with respect to the first dimension of the transport plan Only used in case of Kullback-Leibler divergence. pi2 : array-like (dim_a,), optional (default = None) Marginal distribution with respect to the second dimension of the transport plan Only used in case of Kullback-Leibler divergence. divergence : string, default = "kl" Bregman divergence, either "kl" (Kullback-Leibler divergence) or "l2" (half-squared L2 divergence) mass : bool, optional. Default is False. Only used in case of Kullback-Leibler divergence. If False, calculate the relative entropy. If True, calculate the Kullback-Leibler divergence. nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ------- Bregman divergence between an arbitrary measure and a product measure. """ arr = [pi, a, b, pi1, pi2] if nx is None: nx = get_backend(*arr, pi1, pi2) if divergence == "kl": if pi1 is None: pi1 = nx.sum(pi, 1) if pi2 is None: pi2 = nx.sum(pi, 0) res = ( nx.sum(pi * nx.log(pi + 1.0 * (pi == 0))) - nx.sum(pi1 * nx.log(a)) - nx.sum(pi2 * nx.log(b)) ) if mass: res = res - nx.sum(pi1) + nx.sum(a) * nx.sum(b) elif divergence == "l2": res = ( nx.sum(pi**2) + nx.sum(a**2) * nx.sum(b**2) - 2 * nx.dot(a, nx.dot(pi, b)) ) / 2 return res def div_between_product(mu, nu, alpha, beta, divergence, nx=None): r"""Fast computation of the Bregman divergence between two product measures. Only support for Kullback-Leibler and half-squared L2 divergences. For half-squared L2 divergence: .. math:: \frac{1}{2} || \mu \otimes \nu, \alpha \otimes \beta ||^2 = \frac{1}{2} \Big[ ||\alpha||^2 ||\beta||^2 + ||\mu||^2 ||\nu||^2 - 2 \langle \alpha, \mu \rangle \langle \beta, \nu \rangle \Big] For Kullback-Leibler divergence: .. math:: KL(\mu \otimes \nu, \alpha \otimes \beta) = m(\mu) * KL(\nu, \beta) + m(\nu) * KL(\mu, \alpha) + (m(\mu) - m(\alpha)) * (m(\nu) - m(\beta)) where: - :math:`\mu` and :math:`\alpha` are two measures having the same shape. - :math:`\nu` and :math:`\beta` are two measures having the same shape. - :math:`m` denotes the mass of the measure Parameters ---------- mu : array-like vector or matrix nu : array-like vector or matrix alpha : array-like vector or matrix with the same shape as `\mu` beta : array-like vector or matrix with the same shape as `\nu` divergence : string, default = "kl" Bregman divergence, either "kl" (Kullback-Leibler divergence) or "l2" (half-squared L2 divergence) nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ---------- Bregman divergence between two product measures. """ if nx is None: nx = get_backend(mu, nu, alpha, beta) if divergence == "kl": m_mu, m_nu = nx.sum(mu), nx.sum(nu) m_alpha, m_beta = nx.sum(alpha), nx.sum(beta) const = (m_mu - m_alpha) * (m_nu - m_beta) res = ( m_nu * nx.kl_div(mu, alpha, mass=True) + m_mu * nx.kl_div(nu, beta, mass=True) + const ) elif divergence == "l2": res = ( nx.sum(alpha**2) * nx.sum(beta**2) - 2 * nx.sum(alpha * mu) * nx.sum(beta * nu) + nx.sum(mu**2) * nx.sum(nu**2) ) / 2 return res # Support functions for BCD schemes def uot_cost_matrix(data, pi, tuple_p, hyperparams, divergence, reg_type, nx=None): r"""The Block Coordinate Descent algorithm for FUGW and UCOOT requires solving an UOT problem in each iteration. In particular, we need to specify the following inputs: - Cost matrix - Hyperparameters (marginal-relaxations and regularization) - Reference measures in the marginal-relaxation and regularization terms This method returns the cost matrix. The method :any:`ot.gromov.uot_parameters_and_measures` returns the rest of the inputs. Parameters ---------- data : tuple of arrays vector or matrix pi : array-like vector or matrix tuple_p : tuple of arrays Tuple of reference measures in the marginal-relaxation terms w.r.t the (either sample or feature) coupling hyperparams : tuple of floats Hyperparameters of marginal-relaxation and regularization terms in the fused unbalanced across-domain divergence divergence : string, default = "kl" Bregman divergence, either "kl" (Kullback-Leibler divergence) or "l2" (half-squared L2 divergence) reg_type : string, Type of regularization term in the fused unbalanced across-domain divergence - `reg_type = "joint"` corresponds to FUGW - `reg_type = "independent"` corresponds to UCOOT nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ---------- Cost matrix of the UOT subroutine for UCOOT and FUGW """ X_sqr, Y_sqr, X, Y, M = data rho_x, rho_y, eps = hyperparams a, b = tuple_p if nx is None: nx = get_backend(X, Y, a, b) pi1, pi2 = nx.sum(pi, 1), nx.sum(pi, 0) A, B = nx.dot(X_sqr, pi1), nx.dot(Y_sqr, pi2) uot_cost = A[:, None] + B[None, :] - 2 * nx.dot(nx.dot(X, pi), Y.T) if M is not None: uot_cost = uot_cost + M if divergence == "kl": if rho_x != float("inf") and rho_x != 0: uot_cost = uot_cost + rho_x * nx.kl_div(pi1, a, mass=False) if rho_y != float("inf") and rho_y != 0: uot_cost = uot_cost + rho_y * nx.kl_div(pi2, b, mass=False) if reg_type == "joint" and eps > 0: uot_cost = uot_cost + eps * div_to_product( pi, a, b, pi1, pi2, divergence, mass=False, nx=nx ) return uot_cost def uot_parameters_and_measures( pi, tuple_weights, hyperparams, reg_type, divergence, nx ): r"""The Block Coordinate Descent algorithm for FUGW and UCOOT requires solving an UOT problem in each iteration. In particular, we need to specify the following inputs: - Cost matrix - Hyperparameters (marginal-relaxations and regularization) - Reference measures in the marginal-relaxation and regularization terms The method :any:`ot.gromov.uot_cost_matrix` returns the cost matrix. This method returns the rest of the inputs. Parameters ---------- pi : array-like vector or matrix tuple_weights : tuple of arrays Tuple of reference measures in the marginal-relaxation and regularization terms w.r.t the (either sample or feature) coupling hyperparams : tuple of floats Hyperparameters of marginal-relaxation and regularization terms in the fused unbalanced across-domain divergence reg_type : string, Type of regularization term in the fused unbalanced across-domain divergence - `reg_type = "joint"` corresponds to FUGW - `reg_type = "independent"` corresponds to UCOOT divergence : string, default = "kl" Bregman divergence, either "kl" (Kullback-Leibler divergence) or "l2" (half-squared L2 divergence) nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ---------- Tuple of hyperparameters and distributions (weights) """ rho_x, rho_y, eps = hyperparams wx, wy, wxy = tuple_weights if divergence == "l2": pi1, pi2 = nx.sum(pi, 1), nx.sum(pi, 0) l2_pi1, l2_pi2, l2_pi = nx.sum(pi1**2), nx.sum(pi2**2), nx.sum(pi**2) weighted_wx = wx * nx.sum(pi1 * wx) / l2_pi1 weighted_wy = wy * nx.sum(pi2 * wy) / l2_pi2 weighted_wxy = wxy * nx.sum(pi * wxy) / l2_pi if reg_type == "joint" else wxy weighted_w = (weighted_wx, weighted_wy, weighted_wxy) new_rho = (rho_x * l2_pi1, rho_y * l2_pi2) new_eps = eps * l2_pi if reg_type == "joint" else eps elif divergence == "kl": mass = nx.sum(pi) new_rho = (rho_x * mass, rho_y * mass) new_eps = mass * eps if reg_type == "joint" else eps weighted_w = tuple_weights return weighted_w, new_rho, new_eps def fused_unbalanced_across_spaces_cost( M_linear, data, tuple_pxy_samp, tuple_pxy_feat, pi_samp, pi_feat, hyperparams, divergence, reg_type, nx, ): r"""Return the fused unbalanced across-space divergence between two spaces Parameters ---------- M_linear : tuple of arrays Pair of cost matrices corresponding to the Wasserstein terms w.r.t sample and feature couplings data : tuple of arrays Tuple of input spaces represented as matrices tuple_pxy_samp : tuple of arrays Tuple of reference measures in the marginal-relaxation and regularization terms w.r.t the sample coupling tuple_pxy_feat : tuple of arrays Tuple of reference measures in the marginal-relaxation and regularization terms w.r.t the feature coupling pi_samp : array-like Sample coupling pi_feat : array-like Feature coupling hyperparams : tuple of floats Hyperparameters of marginal-relaxation and regularization terms in the fused unbalanced across-domain divergence divergence : string, default = "kl" Bregman divergence, either "kl" (Kullback-Leibler divergence) or "l2" (half-squared L2 divergence) reg_type : string, Type of regularization term in the fused unbalanced across-domain divergence - `reg_type = "joint"` corresponds to FUGW - `reg_type = "independent"` corresponds to UCOOT nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ---------- Fused unbalanced across-space divergence between two spaces """ rho_x, rho_y, eps_samp, eps_feat = hyperparams M_samp, M_feat = M_linear px_samp, py_samp, pxy_samp = tuple_pxy_samp px_feat, py_feat, pxy_feat = tuple_pxy_feat X_sqr, Y_sqr, X, Y = data pi1_samp, pi2_samp = nx.sum(pi_samp, 1), nx.sum(pi_samp, 0) pi1_feat, pi2_feat = nx.sum(pi_feat, 1), nx.sum(pi_feat, 0) A_sqr = nx.dot(nx.dot(X_sqr, pi1_feat), pi1_samp) B_sqr = nx.dot(nx.dot(Y_sqr, pi2_feat), pi2_samp) AB = nx.dot(nx.dot(X, pi_feat), Y.T) * pi_samp linear_cost = A_sqr + B_sqr - 2 * nx.sum(AB) ucoot_cost = linear_cost if M_samp is not None: ucoot_cost = ucoot_cost + nx.sum(pi_samp * M_samp) if M_feat is not None: ucoot_cost = ucoot_cost + nx.sum(pi_feat * M_feat) if rho_x != float("inf") and rho_x != 0: ucoot_cost = ucoot_cost + rho_x * div_between_product( pi1_samp, pi1_feat, px_samp, px_feat, divergence, nx ) if rho_y != float("inf") and rho_y != 0: ucoot_cost = ucoot_cost + rho_y * div_between_product( pi2_samp, pi2_feat, py_samp, py_feat, divergence, nx ) if reg_type == "joint" and eps_samp != 0: div_cost = div_between_product( pi_samp, pi_feat, pxy_samp, pxy_feat, divergence, nx ) ucoot_cost = ucoot_cost + eps_samp * div_cost elif reg_type == "independent": if eps_samp != 0: div_samp = div_to_product( pi_samp, pi1_samp, pi2_samp, px_samp, py_samp, divergence, mass=True, nx=nx, ) ucoot_cost = ucoot_cost + eps_samp * div_samp if eps_feat != 0: div_feat = div_to_product( pi_feat, pi1_feat, pi2_feat, px_feat, py_feat, divergence, mass=True, nx=nx, ) ucoot_cost = ucoot_cost + eps_feat * div_feat return linear_cost, ucoot_cost python-pot-0.9.7+dfsg/ot/helpers/000077500000000000000000000000001525521106200166525ustar00rootroot00000000000000python-pot-0.9.7+dfsg/ot/helpers/__init__.py000066400000000000000000000001101525521106200207530ustar00rootroot00000000000000# Author: Remi Flamary # # License: MIT License python-pot-0.9.7+dfsg/ot/helpers/openmp_helpers.py000066400000000000000000000117501525521106200222500ustar00rootroot00000000000000"""Helpers for OpenMP support during the build.""" # This code is adapted for a large part from the astropy openmp helpers, which # can be found at: https://github.com/astropy/extension-helpers/blob/master/extension_helpers/_openmp_helpers.py # noqa import os import sys import glob import tempfile import textwrap import subprocess from setuptools.errors import CompileError, LinkError from setuptools.command.build_ext import customize_compiler, new_compiler def _get_compiler(): ccompiler = new_compiler() customize_compiler(ccompiler) return ccompiler def compile_test_program(code, extra_preargs=[], extra_postargs=[]): """Check that some C code can be compiled and run""" ccompiler = _get_compiler() # extra_(pre/post)args can be a callable to make it possible to get its # value from the compiler if callable(extra_preargs): extra_preargs = extra_preargs(ccompiler) if callable(extra_postargs): extra_postargs = extra_postargs(ccompiler) start_dir = os.path.abspath(".") with tempfile.TemporaryDirectory() as tmp_dir: try: os.chdir(tmp_dir) # Write test program with open("test_program.c", "w") as f: f.write(code) os.mkdir("objects") # Compile, test program ccompiler.compile( ["test_program.c"], output_dir="objects", extra_postargs=extra_postargs ) # Link test program objects = glob.glob(os.path.join("objects", "*" + ccompiler.obj_extension)) ccompiler.link_executable( objects, "test_program", extra_preargs=extra_preargs, extra_postargs=extra_postargs, ) if "PYTHON_CROSSENV" not in os.environ: # Run test program if not cross compiling # will raise a CalledProcessError if return code was non-zero output = subprocess.check_output("./test_program") output = output.decode(sys.stdout.encoding or "utf-8").splitlines() else: # Return an empty output if we are cross compiling # as we cannot run the test_program output = [] except Exception: raise finally: os.chdir(start_dir) return output, extra_postargs def get_openmp_flag(compiler): """Get openmp flags for a given compiler""" if hasattr(compiler, "compiler"): compiler = compiler.compiler[0] else: compiler = compiler.__class__.__name__ if sys.platform == "win32" and ("icc" in compiler or "icl" in compiler): omp_flag = ["/Qopenmp"] elif sys.platform == "win32": omp_flag = ["/openmp"] elif sys.platform in ("darwin", "linux") and "icc" in compiler: omp_flag = ["-qopenmp"] elif sys.platform == "darwin" and "openmp" in os.getenv("CPPFLAGS", ""): omp_flag = [] else: # Default flag for GCC and clang: omp_flag = ["-fopenmp"] if sys.platform.startswith("darwin"): omp_flag = ["-Xpreprocessor", "-fopenmp"] # Assuming `brew install libomp` on recent macOS if os.path.isfile("/opt/homebrew/opt/libomp/lib/libomp.dylib"): omp_flag += ["-I/opt/homebrew/opt/libomp/include"] else: # Assuming `brew install libomp` on old macOS if os.path.isfile("/usr/local/opt/libomp/lib/libomp.dylib"): omp_flag += ["-I/usr/local/opt/libomp/include"] return omp_flag def check_openmp_support(): """Check whether OpenMP test code can be compiled and run""" code = textwrap.dedent( """\ #include #include int main(void) { #pragma omp parallel printf("nthreads=%d\\n", omp_get_num_threads()); return 0; } """ ) extra_preargs = os.getenv("LDFLAGS", None) if extra_preargs is not None: extra_preargs = extra_preargs.strip().split(" ") extra_preargs = [ flag for flag in extra_preargs if flag.startswith(("-L", "-Wl,-rpath", "-l")) ] extra_postargs = get_openmp_flag try: output, compile_flags = compile_test_program( code, extra_preargs=extra_preargs, extra_postargs=extra_postargs ) if output and "nthreads=" in output[0]: nthreads = int(output[0].strip().split("=")[1]) openmp_supported = len(output) == nthreads elif "PYTHON_CROSSENV" in os.environ: # Since we can't run the test program when cross-compiling # assume that openmp is supported if the program can be # compiled. openmp_supported = True else: openmp_supported = False except (CompileError, LinkError, subprocess.CalledProcessError): openmp_supported = False compile_flags = [] return openmp_supported, compile_flags python-pot-0.9.7+dfsg/ot/lowrank.py000066400000000000000000000601721525521106200172450ustar00rootroot00000000000000""" Low rank OT solvers """ # Author: Laurène David # Titouan Vayer # # License: MIT License import warnings from .utils import unif, dist, get_lowrank_lazytensor from .backend import get_backend from .bregman import sinkhorn # test if sklearn is installed for linux-minimal-deps try: import sklearn.cluster sklearn_import = True except ImportError: sklearn_import = False def _init_lr_sinkhorn(X_s, X_t, a, b, rank, init, reg_init, random_state, nx=None): """ Implementation of different initialization strategies for the low rank sinkhorn solver (Q ,R, g). This function is specific to lowrank_sinkhorn. Parameters ---------- X_s : array-like, shape (n_samples_a, dim) samples in the source domain X_t : array-like, shape (n_samples_b, dim) samples in the target domain a : array-like, shape (n_samples_a,) samples weights in the source domain b : array-like, shape (n_samples_b,) samples weights in the target domain rank : int Nonnegative rank of the OT plan. init : str Initialization strategy for Q, R and g. 'random', 'trivial' or 'kmeans' reg_init : float, optional. Regularization term for a 'kmeans' init. random_state : int, optional. Random state for a "random" or 'kmeans' init strategy nx : optional, Default is None POT backend Returns --------- Q : array-like, shape (n_samples_a, r) Init for the first low-rank matrix decomposition of the OT plan (Q) R: array-like, shape (n_samples_b, r) Init for the second low-rank matrix decomposition of the OT plan (R) g : array-like, shape (r, ) Init for the weight vector of the low-rank decomposition of the OT plan (g) References ----------- .. [65] Scetbon, M., Cuturi, M., & Peyré, G. (2021). "Low-rank Sinkhorn factorization". In International Conference on Machine Learning. """ if nx is None: nx = get_backend(X_s, X_t, a, b) ns = X_s.shape[0] nt = X_t.shape[0] r = rank if init == "random": nx.seed(seed=random_state) # Init g g = nx.abs(nx.randn(r, type_as=X_s)) + 1 g = g / nx.sum(g) # Init Q Q = nx.abs(nx.randn(ns, r, type_as=X_s)) + 1 Q = (Q.T * (a / nx.sum(Q, axis=1))).T # Init R R = nx.abs(nx.randn(nt, rank, type_as=X_s)) + 1 R = (R.T * (b / nx.sum(R, axis=1))).T if init == "deterministic": # Init g g = nx.ones(rank) / rank lambda_1 = min(nx.min(a), nx.min(g), nx.min(b)) / 2 a1 = nx.arange(start=1, stop=ns + 1, type_as=X_s) a1 = a1 / nx.sum(a1) a2 = (a - lambda_1 * a1) / (1 - lambda_1) b1 = nx.arange(start=1, stop=nt + 1, type_as=X_s) b1 = b1 / nx.sum(b1) b2 = (b - lambda_1 * b1) / (1 - lambda_1) g1 = nx.arange(start=1, stop=rank + 1, type_as=X_s) g1 = g1 / nx.sum(g1) g2 = (g - lambda_1 * g1) / (1 - lambda_1) # Init Q Q1 = lambda_1 * nx.dot(a1[:, None], nx.reshape(g1, (1, -1))) Q2 = (1 - lambda_1) * nx.dot(a2[:, None], nx.reshape(g2, (1, -1))) Q = Q1 + Q2 # Init R R1 = lambda_1 * nx.dot(b1[:, None], nx.reshape(g1, (1, -1))) R2 = (1 - lambda_1) * nx.dot(b2[:, None], nx.reshape(g2, (1, -1))) R = R1 + R2 if init == "kmeans": if sklearn_import: # Init g g = nx.ones(rank, type_as=X_s) / rank # Init Q kmeans_Xs = sklearn.cluster.KMeans( n_clusters=rank, random_state=random_state, n_init="auto" ) kmeans_Xs.fit(X_s) Z_Xs = nx.from_numpy(kmeans_Xs.cluster_centers_) C_Xs = dist(X_s, Z_Xs) # shape (ns, rank) C_Xs = C_Xs / nx.max(C_Xs) Q = sinkhorn(a, g, C_Xs, reg=reg_init, numItermax=10000, stopThr=1e-3) # Init R kmeans_Xt = sklearn.cluster.KMeans( n_clusters=rank, random_state=random_state, n_init="auto" ) kmeans_Xt.fit(X_t) Z_Xt = nx.from_numpy(kmeans_Xt.cluster_centers_) C_Xt = dist(X_t, Z_Xt) # shape (nt, rank) C_Xt = C_Xt / nx.max(C_Xt) R = sinkhorn(b, g, C_Xt, reg=reg_init, numItermax=10000, stopThr=1e-3) else: raise ImportError( "Scikit-learn should be installed to use the 'kmeans' init." ) return Q, R, g def compute_lr_sqeuclidean_matrix(X_s, X_t, rescale_cost, nx=None): """ Compute the low rank decomposition of a squared euclidean distance matrix. This function won't work for other distance metrics. See "Section 3.5, proposition 1" Parameters ---------- X_s : array-like, shape (n_samples_a, dim) samples in the source domain X_t : array-like, shape (n_samples_b, dim) samples in the target domain rescale_cost : bool Rescale the low rank factorization of the sqeuclidean cost matrix nx : default None POT backend Returns ---------- M1 : array-like, shape (n_samples_a, dim+2) First low rank decomposition of the distance matrix M2 : array-like, shape (n_samples_b, dim+2) Second low rank decomposition of the distance matrix References ----------- .. [65] Scetbon, M., Cuturi, M., & Peyré, G. (2021). "Low-rank Sinkhorn factorization". In International Conference on Machine Learning. """ if nx is None: nx = get_backend(X_s, X_t) ns = X_s.shape[0] nt = X_t.shape[0] # First low rank decomposition of the cost matrix (A) array1 = nx.reshape(nx.sum(X_s**2, 1), (-1, 1)) array2 = nx.ones((ns, 1), type_as=X_s) M1 = nx.concatenate((array1, array2, -2 * X_s), axis=1) # Second low rank decomposition of the cost matrix (B) array1 = nx.ones((nt, 1), type_as=X_s) array2 = nx.reshape(nx.sum(X_t**2, 1), (-1, 1)) M2 = nx.concatenate((array1, array2, X_t), axis=1) if rescale_cost is True: M1 = M1 / nx.sqrt(nx.max(M1)) M2 = M2 / nx.sqrt(nx.max(M2)) return M1, M2 def _LR_Dysktra(eps1, eps2, eps3, p1, p2, alpha, stopThr, numItermax, warn, nx=None): """ Implementation of the Dykstra algorithm for the Low Rank sinkhorn OT solver. This function is specific to lowrank_sinkhorn. Parameters ---------- eps1 : array-like, shape (n_samples_a, r) First input parameter of the Dykstra algorithm eps2 : array-like, shape (n_samples_b, r) Second input parameter of the Dykstra algorithm eps3 : array-like, shape (r,) Third input parameter of the Dykstra algorithm p1 : array-like, shape (n_samples_a,) Samples weights in the source domain (same as "a" in lowrank_sinkhorn) p2 : array-like, shape (n_samples_b,) Samples weights in the target domain (same as "b" in lowrank_sinkhorn) alpha: int Lower bound for the weight vector g (same as "alpha" in lowrank_sinkhorn) stopThr : float Stop threshold on error numItermax : int Max number of iterations warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. nx : default None POT backend Returns ---------- Q : array-like, shape (n_samples_a, r) Dykstra update of the first low-rank matrix decomposition Q R: array-like, shape (n_samples_b, r) Dykstra update of the Second low-rank matrix decomposition R g : array-like, shape (r, ) Dykstra update of the weight vector g References ---------- .. [65] Scetbon, M., Cuturi, M., & Peyré, G. (2021). "Low-rank Sinkhorn Factorization". In International Conference on Machine Learning. """ # POT backend if None if nx is None: nx = get_backend(eps1, eps2, eps3, p1, p2) # ----------------- Initialisation of Dykstra algorithm ----------------- r = len(eps3) # rank g_ = nx.copy(eps3) # \tilde{g} q3_1, q3_2 = nx.ones(r, type_as=p1), nx.ones(r, type_as=p1) # q^{(3)}_1, q^{(3)}_2 v1_, v2_ = ( nx.ones(r, type_as=p1), nx.ones(r, type_as=p1), ) # \tilde{v}^{(1)}, \tilde{v}^{(2)} q1, q2 = nx.ones(r, type_as=p1), nx.ones(r, type_as=p1) # q^{(1)}, q^{(2)} err = 1 # initial error # --------------------- Dykstra algorithm ------------------------- # See Section 3.3 - "Algorithm 2 LR-Dykstra" in paper for ii in range(numItermax): if err > stopThr: # Compute u^{(1)} and u^{(2)} u1 = p1 / nx.dot(eps1, v1_) u2 = p2 / nx.dot(eps2, v2_) # Compute g, g^{(3)}_1 and update \tilde{g} g = nx.maximum(alpha, g_ * q3_1) q3_1 = (g_ * q3_1) / g g_ = nx.copy(g) # Compute new value of g with \prod prod1 = (v1_ * q1) * nx.dot(eps1.T, u1) prod2 = (v2_ * q2) * nx.dot(eps2.T, u2) g = (g_ * q3_2 * prod1 * prod2) ** (1 / 3) # Compute v^{(1)} and v^{(2)} v1 = g / nx.dot(eps1.T, u1) v2 = g / nx.dot(eps2.T, u2) # Compute q^{(1)}, q^{(2)} and q^{(3)}_2 q1 = (v1_ * q1) / v1 q2 = (v2_ * q2) / v2 q3_2 = (g_ * q3_2) / g # Update values of \tilde{v}^{(1)}, \tilde{v}^{(2)} and \tilde{g} v1_, v2_ = nx.copy(v1), nx.copy(v2) g_ = nx.copy(g) # Compute error err1 = nx.sum(nx.abs(u1 * (eps1 @ v1) - p1)) err2 = nx.sum(nx.abs(u2 * (eps2 @ v2) - p2)) err = err1 + err2 else: break else: if warn: warnings.warn( "Dykstra did not converge. You might want to " "increase the number of iterations `numItermax` " ) # Compute low rank matrices Q, R Q = u1[:, None] * eps1 * v1[None, :] R = u2[:, None] * eps2 * v2[None, :] return Q, R, g def lowrank_sinkhorn( X_s, X_t, a=None, b=None, reg=0, rank=None, alpha=1e-10, rescale_cost=True, init="random", reg_init=1e-1, seed_init=49, gamma_init="rescale", numItermax=2000, stopThr=1e-7, warn=True, log=False, ): r""" Solve the entropic regularization optimal transport problem under low-nonnegative rank constraints on the couplings. The function solves the following optimization problem: .. math:: \mathop{\inf_{(\mathbf{Q},\mathbf{R},\mathbf{g}) \in \mathcal{C}(\mathbf{a},\mathbf{b},r)}} \langle \mathbf{C}, \mathbf{Q}\mathrm{diag}(1/\mathbf{g})\mathbf{R}^\top \rangle - \mathrm{reg} \cdot H((\mathbf{Q}, \mathbf{R}, \mathbf{g})) where : - :math:`\mathbf{C}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`H((\mathbf{Q}, \mathbf{R}, \mathbf{g}))` is the values of the three respective entropies evaluated for each term. - :math:`\mathbf{Q}` and :math:`\mathbf{R}` are the low-rank matrix decomposition of the OT plan - :math:`\mathbf{g}` is the weight vector for the low-rank decomposition of the OT plan - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (histograms, both sum to 1) - :math:`r` is the rank of the OT plan - :math:`\mathcal{C}(\mathbf{a}, \mathbf{b}, r)` are the low-rank couplings of the OT problem Parameters ---------- X_s : array-like, shape (n_samples_a, dim) samples in the source domain X_t : array-like, shape (n_samples_b, dim) samples in the target domain a : array-like, shape (n_samples_a,) samples weights in the source domain b : array-like, shape (n_samples_b,) samples weights in the target domain reg : float, optional Regularization term >0 rank : int, optional. Default is None. (>0) Nonnegative rank of the OT plan. If None, min(ns, nt) is considered. alpha : int, optional. Default is 1e-10. (>0 and <1/r) Lower bound for the weight vector g. rescale_cost : bool, optional. Default is True Rescale the low rank factorization of the sqeuclidean cost matrix init : str, optional. Default is 'random'. Initialization strategy for the low rank couplings. 'random', 'deterministic' or 'kmeans' reg_init : float, optional. Default is 1e-1. (>0) Regularization term for a 'kmeans' init. If None, 1 is considered. seed_init : int, optional. Default is 49. (>0) Random state for a 'random' or 'kmeans' init strategy. gamma_init : str, optional. Default is "rescale". Initialization strategy for gamma. 'rescale', or 'theory' Gamma is a constant that scales the convergence criterion of the Mirror Descent optimization scheme used to compute the low-rank couplings (Q, R and g) numItermax : int, optional. Default is 2000. Max number of iterations for the Dykstra algorithm stopThr : float, optional. Default is 1e-7. Stop threshold on error (>0) in Dykstra warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. log : bool, optional record log if True Returns --------- Q : array-like, shape (n_samples_a, r) First low-rank matrix decomposition of the OT plan R: array-like, shape (n_samples_b, r) Second low-rank matrix decomposition of the OT plan g : array-like, shape (r, ) Weight vector for the low-rank decomposition of the OT log : dict (lazy_plan, value and value_linear) log dictionary return only if log==True in parameters References ---------- .. [65] Scetbon, M., Cuturi, M., & Peyré, G. (2021). "Low-rank Sinkhorn Factorization". In International Conference on Machine Learning. """ # POT backend nx = get_backend(X_s, X_t) ns, nt = X_s.shape[0], X_t.shape[0] # Initialize weights a, b if a is None: a = unif(ns, type_as=X_s) if b is None: b = unif(nt, type_as=X_t) # Compute rank (see Section 3.1, def 1) r = rank if rank is None: r = min(ns, nt) else: r = min(ns, nt, rank) if r <= 0: raise ValueError("The rank parameter cannot have a negative value") # Dykstra won't converge if 1/rank < alpha (see Section 3.2) if 1 / r < alpha: raise ValueError( "alpha ({a}) should be smaller than 1/rank ({r}) for the Dykstra algorithm to converge.".format( a=alpha, r=1 / rank ) ) # Low rank decomposition of the sqeuclidean cost matrix M1, M2 = compute_lr_sqeuclidean_matrix(X_s, X_t, rescale_cost, nx) # Initialize the low rank matrices Q, R, g Q, R, g = _init_lr_sinkhorn(X_s, X_t, a, b, r, init, reg_init, seed_init, nx=nx) # Gamma initialization if gamma_init == "theory": L = nx.sqrt( 3 * (2 / (alpha**4)) * ((nx.norm(M1) * nx.norm(M2)) ** 2) + (reg + (2 / (alpha**3)) * (nx.norm(M1) * nx.norm(M2))) ** 2 ) gamma = 1 / (2 * L) if gamma_init not in ["rescale", "theory"]: raise ( NotImplementedError('Not implemented gamma_init="{}"'.format(gamma_init)) ) # -------------------------- Low rank algorithm ------------------------------ # see "Section 3.3, Algorithm 3 LOT" for ii in range(100): # Compute C*R dot using the lr decomposition of C CR = nx.dot(M2.T, R) CR_ = nx.dot(M1, CR) diag_g = (1 / g)[None, :] CR_g = CR_ * diag_g # Compute C.T * Q using the lr decomposition of C CQ = nx.dot(M1.T, Q) CQ_ = nx.dot(M2, CQ) CQ_g = CQ_ * diag_g # Compute omega omega = nx.diag(nx.dot(Q.T, CR_)) # Rescale gamma at each iteration if gamma_init == "rescale": norm_1 = nx.max(nx.abs(CR_ * diag_g + reg * nx.log(Q))) ** 2 norm_2 = nx.max(nx.abs(CQ_ * diag_g + reg * nx.log(R))) ** 2 norm_3 = nx.max(nx.abs(-omega * diag_g)) ** 2 gamma = 10 / max(norm_1, norm_2, norm_3) eps1 = nx.exp(-gamma * CR_g - ((gamma * reg) - 1) * nx.log(Q)) eps2 = nx.exp(-gamma * CQ_g - ((gamma * reg) - 1) * nx.log(R)) eps3 = nx.exp((gamma * omega / (g**2)) - (gamma * reg - 1) * nx.log(g)) # LR Dykstra algorithm Q, R, g = _LR_Dysktra( eps1, eps2, eps3, a, b, alpha, stopThr, numItermax, warn, nx ) Q = Q + 1e-16 R = R + 1e-16 g = g + 1e-16 # ----------------- Compute lazy_plan, value and value_linear ------------------ # see "Section 3.2: The Low-rank OT Problem" in the paper # Compute lazy plan (using LazyTensor class) lazy_plan = get_lowrank_lazytensor(Q, R, 1 / g) # Compute value_linear (using trace formula) A = nx.dot(M1.T, Q) B = nx.dot(M2.T, R * diag_g) value_linear = nx.sum(A * B) # Compute value with entropy reg (see "Section 3.2" in the paper) reg_Q = nx.sum(Q * nx.log(Q + 1e-16)) # entropy for Q reg_g = nx.sum(g * nx.log(g + 1e-16)) # entropy for g reg_R = nx.sum(R * nx.log(R + 1e-16)) # entropy for R value = value_linear + reg * (reg_Q + reg_g + reg_R) if log: dict_log = dict() dict_log["value"] = value dict_log["value_linear"] = value_linear dict_log["lazy_plan"] = lazy_plan return Q, R, g, dict_log return Q, R, g def kernel_nystroem(X_s, X_t, anchors=50, sigma=1.0, random_state=None): r""" Compute left and right factors corresponding to the Nystroem method on the Gaussian kernel :math:`K(x^s_i, x^t_j) = \exp(-\|x^s_i-x^t_j\|^2/2\sigma^2)`. The Nystroem approximation is computed by sampling :math:`\min(n, \lceil(c / 2))\rceil)` components in each distribution, where :math:`n` is the number of samples in the distribution and :math:`c` the total number of anchor points. Parameters ---------- X_s : array-like, shape (n_samples_a, dim) samples in the source domain X_t : array-like, shape (n_samples_b, dim) samples in the target domain anchors : int, optional The total number of anchors sampled for the Nystroem approximation (anchors/2 in each distribution), default 50. sigma : float, optional The standard deviation parameter for the Gaussian kernel. random_state : int, optional The random state for sampling the components in each distribution. Returns ------- left_factor : array-like, shape (n_samples_a, dim_r) Left factor of Nystroem right_factor : array-like, shape (n_samples_b, dim_r) Right factor of Nystroem """ nx = get_backend(X_s, X_t) if random_state is not None: nx.seed(random_state) n, m = X_s.shape[0], X_t.shape[0] if not isinstance(anchors, int) or anchors < 2: raise ValueError("anchors must be an integer >= 2") n_components_source = min(n, anchors // 2) n_components_target = min(m, anchors // 2) # draw n_components/2 points in each distribution inds_source = nx.randperm(n) # sample n_components_source uniformly basis_source = X_s[inds_source[:n_components_source]] inds_target = nx.randperm(m) basis_target = X_t[inds_target[:n_components_target]] basis = nx.concatenate((basis_source, basis_target)) Mzz = dist(basis, metric="sqeuclidean") # compute \|z_i - z_j\|_2^2 basis_kernel = nx.exp(-Mzz / (2.0 * sigma**2)) normalization = nx.pinv(basis_kernel, hermitian=True) Mxz = dist(X_s, basis, metric="sqeuclidean") Myz = dist(X_t, basis, metric="sqeuclidean") left_factor = nx.exp(-Mxz / (2.0 * sigma**2)) @ normalization right_factor = nx.exp(-Myz / (2.0 * sigma**2)) return left_factor, right_factor # left_factor @ right_factor.T approx K def sinkhorn_low_rank_kernel( K1, # left factor K2, # right factor a=None, b=None, numItermax=1000, stopThr=1e-9, verbose=False, log=False, warn=True, warmstart=None, ): r""" Compute the Sinkhorn algorithm for a kernel :math:`\mathbf{K}` that can be written as a low rank factorization :math:`\mathbf{K} = \mathbf{K}_1 \mathbf{K}_2^\top`. Does not implement multiple targets. Precisely : - :math:`\mathbf{K}_1, \mathbf{K}_2` are the (`dim_a`, `dim_r`), (`dim_b`, `dim_r`) kernel matrices - :math:`\Omega` is the entropic regularization term :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (histograms, both sum to 1) The algorithm used for solving the problem is the Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[2] ` Parameters ---------- K_1 : array-like, shape (n_samples_a, dim_r) Left factor K_2 : array-like, shape (n_samples_b, dim_r) Right factor a : array-like, shape (n_samples_a,) samples weights in the source domain b : array-like, shape (n_samples_b,) samples in the target domain numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True warn : bool, optional if True, raises a warning if the algorithm doesn't convergence. warmstart: tuple of arrays, shape (n_samples_a, n_samples_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the u,v sinkhorn scaling vectors) Returns --------- u : array-like, shape (n_samples_a, ) Left dual variable v: array-like, shape (n_samples_b, ) Right dual variable log : dict (lazy_plan) log dictionary return only if log==True in parameters """ nx = get_backend(K1, K2, a, b) if a is None: a = nx.full((K1.shape[0],), 1.0 / K1.shape[0], type_as=K1) if b is None: b = nx.full((K2.shape[0],), 1.0 / K2.shape[0], type_as=K2) # init data dim_a = len(a) dim_b = b.shape[0] if log: dict_log = {"err": []} # we assume that no distances are null except those of the diagonal of # distances if warmstart is None: u = nx.ones(dim_a, type_as=K1) / dim_a v = nx.ones(dim_b, type_as=K2) / dim_b else: u, v = nx.exp(warmstart[0]), nx.exp(warmstart[1]) err = 1 for ii in range(numItermax): uprev = u vprev = v KtransposeU = K2 @ (nx.transpose(K1) @ u) v = b / KtransposeU KV = K1 @ (nx.transpose(K2) @ v) u = a / KV if ( nx.any(KtransposeU == 0) or nx.any(nx.isnan(u)) or nx.any(nx.isnan(v)) or nx.any(nx.isinf(u)) or nx.any(nx.isinf(v)) ): # we have reached the machine precision # come back to previous solution and quit loop warnings.warn("Warning: numerical errors at iteration %d" % ii) u = uprev v = vprev break if ii % 10 == 0: # we can speed up the process by checking for the error only all # the 10th iterations tmp2 = ( v * (K2 @ (nx.transpose(K1) @ u)) ) # same as nx.einsum("ik, ir, jr, jk->jk", u, K1, K2, v) and nx.einsum("i, ir, jr, j->j", u, K1, K2, v) in the scalar case err = nx.norm(tmp2 - b) # violation of marginal if log: dict_log["err"].append(err) if err < stopThr: break if verbose: if ii % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(ii, err)) else: if warn: warnings.warn( "Sinkhorn did not converge. You might want to " "increase the number of iterations `numItermax` " "or the regularization parameter `reg`." ) if log: dict_log["niter"] = ii dict_log["u"] = u dict_log["v"] = v dict_log["lazy_plan"] = get_lowrank_lazytensor( u.reshape((-1, 1)) * K1, v.reshape((-1, 1)) * K2 ) return u, v, dict_log else: return u, v python-pot-0.9.7+dfsg/ot/lp/000077500000000000000000000000001525521106200156235ustar00rootroot00000000000000python-pot-0.9.7+dfsg/ot/lp/EMD.h000066400000000000000000000064271525521106200164120ustar00rootroot00000000000000/* This file is a c++ wrapper function for computing the transportation cost * between two vectors given a cost matrix. * * It was written by Antoine Rolet (2014) and mainly consists of a wrapper * of the code written by Nicolas Bonneel available on this page * http://people.seas.harvard.edu/~nbonneel/FastTransport/ * * It was then modified to make it more amenable to python inline calling * * Please give relevant credit to the original author (Nicolas Bonneel) if * you use this code for a publication. * */ #ifndef EMD_H #define EMD_H #include #include #include typedef unsigned int node_id_type; enum ProblemType { INFEASIBLE, OPTIMAL, UNBOUNDED, MAX_ITER_REACHED }; int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double* alpha, double* beta, double *cost, uint64_t maxIter, double* alpha_init, double* beta_init); int EMD_wrap_sparse( int n1, int n2, double *X, double *Y, uint64_t n_edges, // Number of edges in sparse graph uint64_t *edge_sources, // Source indices for each edge (n_edges) uint64_t *edge_targets, // Target indices for each edge (n_edges) double *edge_costs, // Cost for each edge (n_edges) uint64_t *flow_sources_out, // Output: source indices of non-zero flows uint64_t *flow_targets_out, // Output: target indices of non-zero flows double *flow_values_out, // Output: flow values uint64_t *n_flows_out, uint64_t max_flows_out, double *alpha, // Output: dual variables for sources (n1) double *beta, // Output: dual variables for targets (n2) double *cost, // Output: total transportation cost uint64_t maxIter, // Maximum iterations for solver double *alpha_init, // Initial dual variables for sources (warmstart) double *beta_init // Initial dual variables for targets (warmstart) ); int EMD_wrap_lazy( int n1, // Number of source points int n2, // Number of target points double *X, // Source weights (n1) double *Y, // Target weights (n2) double *coords_a, // Source coordinates (n1 x dim) double *coords_b, // Target coordinates (n2 x dim) int dim, // Dimension of coordinates int metric, // Distance metric: 0=sqeuclidean, 1=euclidean, 2=cityblock uint64_t *flow_sources_out, // Output: source indices of non-zero flows uint64_t *flow_targets_out, // Output: target indices of non-zero flows double *flow_values_out, // Output: flow values uint64_t *n_flows_out, uint64_t max_flows_out, double *alpha, // Output: dual variables for sources (n1) double *beta, // Output: dual variables for targets (n2) double *cost, // Output: total transportation cost uint64_t maxIter, // Maximum iterations for solver double *alpha_init, // Initial dual variables for sources (warmstart) double *beta_init // Initial dual variables for targets (warmstart) ); #endif python-pot-0.9.7+dfsg/ot/lp/EMD_wrapper.cpp000066400000000000000000000423661525521106200205070ustar00rootroot00000000000000/* This file is a c++ wrapper function for computing the transportation cost * between two vectors given a cost matrix. * * It was written by Antoine Rolet (2014) and mainly consists of a wrapper * of the code written by Nicolas Bonneel available on this page * http://people.seas.harvard.edu/~nbonneel/FastTransport/ * * It was then modified to make it more amenable to python inline calling * * Please give relevant credit to the original author (Nicolas Bonneel) if * you use this code for a publication. * */ #include "network_simplex_simple.h" #include "sparse_bipartitegraph.h" #include "EMD.h" #include #include #include namespace { struct SetupPolicy { bool full_support; bool use_arc_mixing; bool use_dense_cost_pointer; }; inline SetupPolicy make_setup_policy( uint64_t n, uint64_t m, int n1, int n2, bool dense_cost_pointer_supported ) { SetupPolicy policy; policy.full_support = (n == static_cast(n1)) && (m == static_cast(n2)); policy.use_arc_mixing = !policy.full_support; policy.use_dense_cost_pointer = dense_cost_pointer_supported && policy.full_support; return policy; } template inline void setup_explicit_arc_costs( NetType& net, DigraphType& di, const double* D, int n2, const std::vector& indI, const std::vector& indJ, uint64_t n, uint64_t m ) { int64_t idarc = 0; for (uint64_t i = 0; i < n; ++i) { for (uint64_t j = 0; j < m; ++j) { net.setCost(di.arcFromId(idarc), D[indI[i] * n2 + indJ[j]]); ++idarc; } } } template inline void setup_warmstart_potentials( NetType& net, const double* alpha_init, const double* beta_init, const std::vector& indI, const std::vector& indJ, uint64_t n, uint64_t m ) { if (alpha_init == nullptr || beta_init == nullptr) return; std::vector alpha_compressed(n); std::vector beta_compressed(m); for (uint64_t i = 0; i < n; ++i) alpha_compressed[i] = alpha_init[indI[i]]; for (uint64_t j = 0; j < m; ++j) beta_compressed[j] = beta_init[indJ[j]]; net.setWarmstartPotentials(&alpha_compressed[0], &beta_compressed[0], (int)n, (int)m); } template inline void extract_dense_full_support( const NetType& net, const double* D, double* G, double* alpha, double* beta, double* cost, uint64_t n, uint64_t m ) { const int node_total = net.nodeNum(); const int pi_base = node_total - 1; for (uint64_t ii = 0; ii < n; ++ii) { alpha[ii] = -net._pi[pi_base - static_cast(ii)]; } for (uint64_t jj = 0; jj < m; ++jj) { beta[jj] = net._pi[pi_base - static_cast(n + jj)]; } // Only write non-zero entries. G is already zero-initialized in Python. const int64_t arc_total = net.arcNum(); for (int64_t a = 0; a < arc_total; ++a) { const double flow = net._flow[a]; if (flow == 0.0) continue; const int64_t d_idx = arc_total - a - 1; // row-major index in D/G *cost += flow * D[d_idx]; G[d_idx] = flow; } } template inline void extract_compressed_support( const NetType& net, DigraphType& di, InvalidType invalid, const double* D, double* G, double* alpha, double* beta, double* cost, const std::vector& indI, const std::vector& indJ, uint64_t n, int n2 ) { for (uint64_t ii = 0; ii < n; ++ii) { alpha[indI[ii]] = -net.potential(ii); } for (uint64_t jj = 0; jj < indJ.size(); ++jj) { beta[indJ[jj]] = net.potential(jj + n); } uint64_t i, j; typename DigraphType::Arc a; di.first(a); for (; a != invalid; di.next(a)) { i = di.source(a); j = di.target(a); const double flow = net.flow(a); *cost += flow * D[indI[i] * n2 + indJ[j - n]]; G[indI[i] * n2 + indJ[j - n]] = flow; } } template < typename NetType, typename DigraphType, typename InvalidType, typename SourceIndexVector, typename TargetIndexVector, typename CostAccessor > inline bool extract_sparse_solution( const NetType& net, DigraphType& di, InvalidType invalid, const SourceIndexVector& idx_a, const TargetIndexVector& idx_b, double* alpha, double* beta, double* cost, uint64_t* flow_sources_out, uint64_t* flow_targets_out, double* flow_values_out, uint64_t* n_flows_out, uint64_t max_flows_out, CostAccessor cost_accessor, double min_output_flow ) { const int n = static_cast(idx_a.size()); for (int i = 0; i < n; i++) { alpha[static_cast(idx_a[i])] = -net.potential(i); } for (int j = 0; j < static_cast(idx_b.size()); j++) { beta[static_cast(idx_b[j])] = net.potential(j + n); } typename DigraphType::Arc a; di.first(a); for (; a != invalid; di.next(a)) { const int i = di.source(a); const int j = di.target(a) - n; const double flow = net.flow(a); if (flow != 0) { *cost += flow * cost_accessor(a, i, j); } if (flow > min_output_flow) { if (*n_flows_out >= max_flows_out) { return false; } flow_sources_out[*n_flows_out] = static_cast(idx_a[i]); flow_targets_out[*n_flows_out] = static_cast(idx_b[j]); flow_values_out[*n_flows_out] = flow; ++(*n_flows_out); } } return true; } } // namespace int EMD_wrap(int n1, int n2, double *X, double *Y, double *D, double *G, double* alpha, double* beta, double *cost, uint64_t maxIter, double* alpha_init, double* beta_init) { // beware M and C are stored in row major C style!!! using namespace lemon; uint64_t n, m, cur; typedef FullBipartiteDigraph Digraph; DIGRAPH_TYPEDEFS(Digraph); // Get the number of non zero coordinates for r and c n=0; for (int i=0; i0) { n++; }else if(val<0){ return INFEASIBLE; } } m=0; for (int i=0; i0) { m++; }else if(val<0){ return INFEASIBLE; } } // Define graph and solver std::vector indI(n), indJ(m); std::vector weights1(n), weights2(m); Digraph di(n, m); const SetupPolicy policy = make_setup_policy(n, m, n1, n2, true); typedef NetworkSimplexSimple Simplex; Simplex::SimplexOptions simplex_options(policy.use_arc_mixing); Simplex net(di, simplex_options, (int) (n + m), n * m, maxIter); // Set supply and demand, don't account for 0 values (faster) cur=0; for (uint64_t i=0; i0) { weights1[ cur ] = val; indI[cur++]=i; } } // Demand is actually negative supply... cur=0; for (uint64_t i=0; i0) { weights2[ cur ] = -val; indJ[cur++]=i; } } net.supplyMap(&weights1[0], (int) n, &weights2[0], (int) m); if (policy.use_dense_cost_pointer) { net.setDenseCostMatrix(D, n2); } else { setup_explicit_arc_costs(net, di, D, n2, indI, indJ, n, m); } setup_warmstart_potentials(net, alpha_init, beta_init, indI, indJ, n, m); // Solve the problem with the network simplex algorithm int ret=net.run(); if (ret==(int)net.OPTIMAL || ret==(int)net.MAX_ITER_REACHED) { *cost = 0; if (policy.full_support) { extract_dense_full_support(net, D, G, alpha, beta, cost, n, m); } else { extract_compressed_support( net, di, INVALID, D, G, alpha, beta, cost, indI, indJ, n, n2 ); } } return ret; } // ============================================================================ // SPARSE VERSION: Accepts edge list instead of dense cost matrix // ============================================================================ int EMD_wrap_sparse( int n1, int n2, double *X, double *Y, uint64_t n_edges, uint64_t *edge_sources, uint64_t *edge_targets, double *edge_costs, uint64_t *flow_sources_out, uint64_t *flow_targets_out, double *flow_values_out, uint64_t *n_flows_out, uint64_t max_flows_out, double *alpha, double *beta, double *cost, uint64_t maxIter, double *alpha_init, double *beta_init ) { using namespace lemon; uint64_t n = 0; for (int i = 0; i < n1; i++) { double val = *(X + i); if (val > 0) { n++; } else if (val < 0) { return INFEASIBLE; } } uint64_t m = 0; for (int i = 0; i < n2; i++) { double val = *(Y + i); if (val > 0) { m++; } else if (val < 0) { return INFEASIBLE; } } std::vector indI(n); // indI[graph_idx] = original_source_idx std::vector indJ(m); // indJ[graph_idx] = original_target_idx std::vector weights1(n); // Source masses (positive only) std::vector weights2(m); // Target masses (negative for demand) // Create reverse mapping: original_idx → graph_idx std::vector source_to_graph(n1, -1); std::vector target_to_graph(n2, -1); uint64_t cur = 0; for (int i = 0; i < n1; i++) { double val = *(X + i); if (val > 0) { weights1[cur] = val; // Store the mass indI[cur] = i; // Forward map: graph → original source_to_graph[i] = cur; // Reverse map: original → graph cur++; } } cur = 0; for (int i = 0; i < n2; i++) { double val = *(Y + i); if (val > 0) { weights2[cur] = -val; indJ[cur] = i; // Forward map: graph → original target_to_graph[i] = cur; // Reverse map: original → graph cur++; } } typedef SparseBipartiteDigraph Digraph; DIGRAPH_TYPEDEFS(Digraph); Digraph di(n, m); std::vector> edges; // (source, target) pairs std::vector edge_to_arc; // edge_to_arc[k] = arc ID for edge k std::vector arc_costs; // arc_costs[arc_id] = cost (for O(1) lookup) edges.reserve(n_edges); edge_to_arc.reserve(n_edges); uint64_t valid_edge_count = 0; for (uint64_t k = 0; k < n_edges; k++) { int64_t src_orig = edge_sources[k]; int64_t tgt_orig = edge_targets[k]; int64_t src = source_to_graph[src_orig]; int64_t tgt = target_to_graph[tgt_orig]; if (src >= 0 && tgt >= 0) { edges.emplace_back(src, tgt + n); edge_to_arc.push_back(valid_edge_count); arc_costs.push_back(edge_costs[k]); // Store cost indexed by arc ID valid_edge_count++; } else { edge_to_arc.push_back(UINT64_MAX); } } di.buildFromEdges(edges); typedef NetworkSimplexSimple Simplex; Simplex::SimplexOptions simplex_options(true); Simplex net(di, simplex_options, (int)(n + m), di.arcNum(), maxIter); net.supplyMap(&weights1[0], (int)n, &weights2[0], (int)m); for (uint64_t k = 0; k < n_edges; k++) { if (edge_to_arc[k] != UINT64_MAX) { net.setCost(edge_to_arc[k], edge_costs[k]); } } // Initialize warmstart if provided if (alpha_init != nullptr && beta_init != nullptr) { // Map original indices to graph indices for warmstart std::vector alpha_filtered(n); std::vector beta_filtered(m); for (uint64_t i = 0; i < n; i++) { uint64_t orig_i = indI[i]; alpha_filtered[i] = alpha_init[orig_i]; } for (uint64_t j = 0; j < m; j++) { uint64_t orig_j = indJ[j]; beta_filtered[j] = beta_init[orig_j]; } net.setWarmstartPotentials(&alpha_filtered[0], &beta_filtered[0], n, m); } int ret = net.run(); if (ret == (int)net.OPTIMAL || ret == (int)net.MAX_ITER_REACHED) { *cost = 0; *n_flows_out = 0; auto sparse_cost = [&arc_costs](Arc a, int, int) { return arc_costs[a]; }; if (!extract_sparse_solution( net, di, INVALID, indI, indJ, alpha, beta, cost, flow_sources_out, flow_targets_out, flow_values_out, n_flows_out, max_flows_out, sparse_cost, 1e-15)) { return (int)net.MAX_ITER_REACHED; } } return ret; } int EMD_wrap_lazy(int n1, int n2, double *X, double *Y, double *coords_a, double *coords_b, int dim, int metric, uint64_t *flow_sources_out, uint64_t *flow_targets_out, double *flow_values_out, uint64_t *n_flows_out, uint64_t max_flows_out, double *alpha, double *beta, double *cost, uint64_t maxIter, double *alpha_init, double *beta_init) { using namespace lemon; typedef FullBipartiteDigraph Digraph; DIGRAPH_TYPEDEFS(Digraph); // Filter source nodes with non-zero weights std::vector idx_a; std::vector weights_a_filtered; std::vector coords_a_filtered; // Reserve space to avoid reallocations idx_a.reserve(n1); weights_a_filtered.reserve(n1); coords_a_filtered.reserve(n1 * dim); for (int i = 0; i < n1; i++) { if (X[i] > 0) { idx_a.push_back(i); weights_a_filtered.push_back(X[i]); for (int d = 0; d < dim; d++) { coords_a_filtered.push_back(coords_a[i * dim + d]); } } } int n = idx_a.size(); // Filter target nodes with non-zero weights std::vector idx_b; std::vector weights_b_filtered; std::vector coords_b_filtered; // Reserve space to avoid reallocations idx_b.reserve(n2); weights_b_filtered.reserve(n2); coords_b_filtered.reserve(n2 * dim); for (int j = 0; j < n2; j++) { if (Y[j] > 0) { idx_b.push_back(j); weights_b_filtered.push_back(-Y[j]); // Demand is negative supply for (int d = 0; d < dim; d++) { coords_b_filtered.push_back(coords_b[j * dim + d]); } } } int m = idx_b.size(); if (n == 0 || m == 0) { *cost = 0.0; return 0; } // Create full bipartite graph Digraph di(n, m); typedef NetworkSimplexSimple Simplex; Simplex::SimplexOptions simplex_options(false); // Lazy mode does not store costs or endpoints for the real complete // bipartite arcs. Artificial root arcs are still explicit because the // simplex initialization assigns them costs 0 or ART_COST. simplex_options.cost_storage_mode = Simplex::CostStorageMode::ArtificialArcCosts; simplex_options.flow_storage_mode = Simplex::FlowStorageMode::SparseArcFlows; simplex_options.endpoint_storage_mode = Simplex::EndpointStorageMode::ArcEndpoints; simplex_options.state_storage_mode = Simplex::StateStorageMode::PackedArcStates; Simplex net( di, simplex_options, (int)(n + m), (uint64_t)(n) * (uint64_t)(m), maxIter ); // Set supplies net.supplyMap(&weights_a_filtered[0], n, &weights_b_filtered[0], m); // Enable lazy cost computation - costs will be computed on-the-fly net.setLazyCost(&coords_a_filtered[0], &coords_b_filtered[0], dim, metric, n, m); // Initialize warmstart if provided if (alpha_init != nullptr && beta_init != nullptr) { // Map original indices to graph indices for warmstart std::vector alpha_filtered(n); std::vector beta_filtered(m); for (int i = 0; i < n; i++) { int orig_i = idx_a[i]; alpha_filtered[i] = alpha_init[orig_i]; } for (int j = 0; j < m; j++) { int orig_j = idx_b[j]; beta_filtered[j] = beta_init[orig_j]; } net.setWarmstartPotentials(&alpha_filtered[0], &beta_filtered[0], n, m); } // Run solver int ret = net.run(); if (ret == (int)net.OPTIMAL || ret == (int)net.MAX_ITER_REACHED) { *cost = 0; *n_flows_out = 0; // Initialize output arrays for (int i = 0; i < n1; i++) alpha[i] = 0.0; for (int i = 0; i < n2; i++) beta[i] = 0.0; auto lazy_cost = [&net](Arc, int i, int j) { return net.computeLazyCost(i, j); }; if (!extract_sparse_solution( net, di, INVALID, idx_a, idx_b, alpha, beta, cost, flow_sources_out, flow_targets_out, flow_values_out, n_flows_out, max_flows_out, lazy_cost, 0.0)) { return (int)net.MAX_ITER_REACHED; } } return ret; } python-pot-0.9.7+dfsg/ot/lp/__init__.py000066400000000000000000000026361525521106200177430ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Solvers for the original linear program OT problem. """ # Author: Remi Flamary # # License: MIT License from .dmmot import dmmot_monge_1dgrid_loss, dmmot_monge_1dgrid_optimize from ._network_simplex import emd, emd2, emd2_lazy from ._barycenter_solvers import ( barycenter, free_support_barycenter, generalized_free_support_barycenter, free_support_barycenter_generic_costs, ot_barycenter_energy, NorthWestMMGluing, ) from ..utils import check_number_threads # import compiled emd from .emd_wrap import emd_1d_sorted from .solver_1d import ( emd_1d, emd2_1d, wasserstein_1d, emd_1d_dual_backprop, ) from .solver_circle import ( binary_search_circle, wasserstein_circle, semidiscrete_wasserstein2_unif_circle, linear_circular_ot, ) __all__ = [ "emd", "emd2", "emd2_lazy", "barycenter", "free_support_barycenter", "cvx", "emd_1d_sorted", "emd_1d", "emd2_1d", "wasserstein_1d", "emd_1d_dual", "emd_1d_dual_backprop", "generalized_free_support_barycenter", "binary_search_circle", "wasserstein_circle", "semidiscrete_wasserstein2_unif_circle", "linear_circular_ot", "dmmot_monge_1dgrid_loss", "dmmot_monge_1dgrid_optimize", "check_number_threads", "free_support_barycenter_generic_costs", "NorthWestMMGluing", "ot_barycenter_energy", ] python-pot-0.9.7+dfsg/ot/lp/_barycenter_solvers.py000066400000000000000000001101641525521106200222520ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ OT Barycenter Solvers """ # Author: Remi Flamary # Eloi Tanguy # # License: MIT License from ..backend import get_backend from ..utils import dist from ._network_simplex import emd, emd2 import numpy as np import scipy as sp import scipy.sparse as sps try: import cvxopt # for cvxopt barycenter solver from cvxopt import solvers, matrix, spmatrix except ImportError: cvxopt = False def scipy_sparse_to_spmatrix(A): """Efficient conversion from scipy sparse matrix to cvxopt sparse matrix""" coo = A.tocoo() SP = spmatrix(coo.data.tolist(), coo.row.tolist(), coo.col.tolist(), size=A.shape) return SP def barycenter(A, M, weights=None, verbose=False, log=False, solver="highs-ipm"): r"""Compute the Wasserstein barycenter of distributions A The function solves the following optimization problem [16]: .. math:: \mathbf{a} = arg\min_\mathbf{a} \sum_i W_{1}(\mathbf{a},\mathbf{a}_i) where : - :math:`W_1(\cdot,\cdot)` is the Wasserstein distance (see ot.emd.sinkhorn) - :math:`\mathbf{a}_i` are training distributions in the columns of matrix :math:`\mathbf{A}` The linear program is solved using the interior point solver from scipy.optimize. If cvxopt solver if installed it can use cvxopt Note that this problem do not scale well (both in memory and computational time). Parameters ---------- A : np.ndarray (d,n) n training distributions a_i of size d M : np.ndarray (d,d) loss matrix for OT reg : float Regularization term >0 weights : np.ndarray (n,) Weights of each histogram a_i on the simplex (barycentric coordinates) verbose : bool, optional Print information along iterations log : bool, optional record log if True solver : string, optional the solver used, default 'interior-point' use the lp solver from scipy.optimize. None, or 'glpk' or 'mosek' use the solver from cvxopt. Returns ------- a : (d,) ndarray Wasserstein barycenter log : dict log dictionary return only if log==True in parameters References ---------- .. [16] Agueh, M., & Carlier, G. (2011). Barycenters in the Wasserstein space. SIAM Journal on Mathematical Analysis, 43(2), 904-924. """ if weights is None: weights = np.ones(A.shape[1]) / A.shape[1] else: assert len(weights) == A.shape[1] n_distributions = A.shape[1] n = A.shape[0] n2 = n * n c = np.zeros((0)) b_eq1 = np.zeros((0)) for i in range(n_distributions): c = np.concatenate((c, M.ravel() * weights[i])) b_eq1 = np.concatenate((b_eq1, A[:, i])) c = np.concatenate((c, np.zeros(n))) lst_idiag1 = [sps.kron(sps.eye(n), np.ones((1, n))) for i in range(n_distributions)] # row constraints A_eq1 = sps.hstack( (sps.block_diag(lst_idiag1), sps.coo_matrix((n_distributions * n, n))) ) # columns constraints lst_idiag2 = [] lst_eye = [] for i in range(n_distributions): if i == 0: lst_idiag2.append(sps.kron(np.ones((1, n)), sps.eye(n))) lst_eye.append(-sps.eye(n)) else: lst_idiag2.append(sps.kron(np.ones((1, n)), sps.eye(n - 1, n))) lst_eye.append(-sps.eye(n - 1, n)) A_eq2 = sps.hstack((sps.block_diag(lst_idiag2), sps.vstack(lst_eye))) b_eq2 = np.zeros((A_eq2.shape[0])) # full problem A_eq = sps.vstack((A_eq1, A_eq2)) b_eq = np.concatenate((b_eq1, b_eq2)) if not cvxopt or solver in ["interior-point", "highs", "highs-ipm", "highs-ds"]: # cvxopt not installed or interior point if solver is None: solver = "interior-point" options = {"disp": verbose} sol = sp.optimize.linprog( c, A_eq=A_eq, b_eq=b_eq, method=solver, options=options ) x = sol.x b = x[-n:] else: h = np.zeros((n_distributions * n2 + n)) G = -sps.eye(n_distributions * n2 + n) sol = solvers.lp( matrix(c), scipy_sparse_to_spmatrix(G), matrix(h), A=scipy_sparse_to_spmatrix(A_eq), b=matrix(b_eq), solver=solver, ) x = np.array(sol["x"]) b = x[-n:].ravel() if log: return b, sol else: return b def free_support_barycenter( measures_locations, measures_weights, X_init, b=None, weights=None, numItermax=100, stopThr=1e-7, verbose=False, log=None, numThreads=1, ): r""" Solves the free support (locations of the barycenters are optimized, not the weights) Wasserstein barycenter problem (i.e. the weighted Frechet mean for the 2-Wasserstein distance), formally: .. math:: \min_\mathbf{X} \quad \sum_{i=1}^N w_i W_2^2(\mathbf{b}, \mathbf{X}, \mathbf{a}_i, \mathbf{X}_i) where : - :math:`w \in \mathbb{(0, 1)}^{N}`'s are the barycenter weights and sum to one - `measure_weights` denotes the :math:`\mathbf{a}_i \in \mathbb{R}^{k_i}`: empirical measures weights (on simplex) - `measures_locations` denotes the :math:`\mathbf{X}_i \in \mathbb{R}^{k_i, d}`: empirical measures atoms locations - :math:`\mathbf{b} \in \mathbb{R}^{k}` is the desired weights vector of the barycenter This problem is considered in :ref:`[20] ` (Algorithm 2). There are two differences with the following codes: - we do not optimize over the weights - we do not do line search for the locations updates, we use i.e. :math:`\theta = 1` in :ref:`[20] ` (Algorithm 2). This can be seen as a discrete implementation of the fixed-point algorithm of :ref:`[43] ` proposed in the continuous setting. Parameters ---------- measures_locations : list of N (k_i,d) array-like The discrete support of a measure supported on :math:`k_i` locations of a `d`-dimensional space (:math:`k_i` can be different for each element of the list) measures_weights : list of N (k_i,) array-like Numpy arrays where each numpy array has :math:`k_i` non-negatives values summing to one representing the weights of each discrete input measure X_init : (k,d) array-like Initialization of the support locations (on `k` atoms) of the barycenter b : (k,) array-like Initialization of the weights of the barycenter (non-negatives, sum to 1) weights : (N,) array-like Initialization of the coefficients of the barycenter (non-negatives, sum to 1) numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True numThreads: int or "max", optional (default=1) Deprecated compatibility parameter forwarded to EMD. The network simplex solver no longer uses OpenMP, so this parameter is ignored. Returns ------- X : (k,d) array-like Support locations (on k atoms) of the barycenter .. _references-free-support-barycenter: References ---------- .. [20] Cuturi, Marco, and Arnaud Doucet. "Fast computation of Wasserstein barycenters." International Conference on Machine Learning. 2014. .. [43] �lvarez-Esteban, Pedro C., et al. "A fixed-point approach to barycenters in Wasserstein space." Journal of Mathematical Analysis and Applications 441.2 (2016): 744-762. """ nx = get_backend(*measures_locations, *measures_weights, X_init) iter_count = 0 N = len(measures_locations) k = X_init.shape[0] d = X_init.shape[1] if b is None: b = nx.ones((k,), type_as=X_init) / k if weights is None: weights = nx.ones((N,), type_as=X_init) / N X = X_init log_dict = {} displacement_square_norms = [] displacement_square_norm = stopThr + 1.0 while displacement_square_norm > stopThr and iter_count < numItermax: T_sum = nx.zeros((k, d), type_as=X_init) for measure_locations_i, measure_weights_i, weight_i in zip( measures_locations, measures_weights, weights ): M_i = dist(X, measure_locations_i) T_i = emd(b, measure_weights_i, M_i, numThreads=numThreads) T_sum = T_sum + weight_i * 1.0 / b[:, None] * nx.dot( T_i, measure_locations_i ) displacement_square_norm = nx.sum((T_sum - X) ** 2) if log: displacement_square_norms.append(displacement_square_norm) X = T_sum if verbose: print( "iteration %d, displacement_square_norm=%f\n", iter_count, displacement_square_norm, ) iter_count += 1 if log: log_dict["displacement_square_norms"] = displacement_square_norms return X, log_dict else: return X def generalized_free_support_barycenter( X_list, a_list, P_list, n_samples_bary, Y_init=None, b=None, weights=None, numItermax=100, stopThr=1e-7, verbose=False, log=None, numThreads=1, eps=0, ): r""" Solves the free support generalized Wasserstein barycenter problem: finding a barycenter (a discrete measure with a fixed amount of points of uniform weights) whose respective projections fit the input measures. More formally: .. math:: \min_\gamma \quad \sum_{i=1}^p w_i W_2^2(\nu_i, \mathbf{P}_i\#\gamma) where : - :math:`\gamma = \sum_{l=1}^n b_l\delta_{y_l}` is the desired barycenter with each :math:`y_l \in \mathbb{R}^d` - :math:`\mathbf{b} \in \mathbb{R}^{n}` is the desired weights vector of the barycenter - The input measures are :math:`\nu_i = \sum_{j=1}^{k_i}a_{i,j}\delta_{x_{i,j}}` - The :math:`\mathbf{a}_i \in \mathbb{R}^{k_i}` are the respective empirical measures weights (on the simplex) - The :math:`\mathbf{X}_i \in \mathbb{R}^{k_i, d_i}` are the respective empirical measures atoms locations - :math:`w = (w_1, \cdots w_p)` are the barycenter coefficients (on the simplex) - Each :math:`\mathbf{P}_i \in \mathbb{R}^{d, d_i}`, and :math:`P_i\#\nu_i = \sum_{j=1}^{k_i}a_{i,j}\delta_{P_ix_{i,j}}` As show by :ref:`[42] `, this problem can be re-written as a Wasserstein Barycenter problem, which we solve using the free support method :ref:`[20] ` (Algorithm 2). Parameters ---------- X_list : list of p (k_i,d_i) array-like Discrete supports of the input measures: each consists of :math:`k_i` locations of a `d_i`-dimensional space (:math:`k_i` can be different for each element of the list) a_list : list of p (k_i,) array-like Measure weights: each element is a vector (k_i) on the simplex P_list : list of p (d_i,d) array-like Each :math:`P_i` is a linear map :math:`\mathbb{R}^{d} \rightarrow \mathbb{R}^{d_i}` n_samples_bary : int Number of barycenter points Y_init : (n_samples_bary,d) array-like Initialization of the support locations (on `k` atoms) of the barycenter b : (n_samples_bary,) array-like Initialization of the weights of the barycenter measure (on the simplex) weights : (p,) array-like Initialization of the coefficients of the barycenter (on the simplex) numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True numThreads: int or "max", optional (default=1) Deprecated compatibility parameter forwarded to EMD. The network simplex solver no longer uses OpenMP, so this parameter is ignored. eps: Stability coefficient for the change of variable matrix inversion If the :math:`\mathbf{P}_i^T` matrices don't span :math:`\mathbb{R}^d`, the problem is ill-defined and a matrix inversion will fail. In this case one may set eps=1e-8 and get a solution anyway (which may make little sense) Returns ------- Y : (n_samples_bary,d) array-like Support locations (on n_samples_bary atoms) of the barycenter .. _references-generalized-free-support-barycenter: References ---------- .. [20] Cuturi, M. and Doucet, A.. "Fast computation of Wasserstein barycenters." International Conference on Machine Learning. 2014. .. [42] Delon, J., Gozlan, N., and Saint-Dizier, A.. Generalized Wasserstein barycenters between probability measures living on different subspaces. arXiv preprint arXiv:2105.09755, 2021. """ nx = get_backend(*X_list, *a_list, *P_list) d = P_list[0].shape[1] p = len(X_list) if weights is None: weights = nx.ones(p, type_as=X_list[0]) / p # variable change matrix to reduce the problem to a Wasserstein Barycenter (WB) A = eps * nx.eye( d, type_as=X_list[0] ) # if eps nonzero: will force the invertibility of A for P_i, lambda_i in zip(P_list, weights): A = A + lambda_i * P_i.T @ P_i B = nx.inv(nx.sqrtm(A)) Z_list = [ x @ Pi @ B.T for (x, Pi) in zip(X_list, P_list) ] # change of variables -> (WB) problem on Z if Y_init is None: Y_init = nx.randn(n_samples_bary, d, type_as=X_list[0]) if b is None: b = nx.ones(n_samples_bary, type_as=X_list[0]) / n_samples_bary # not optimized out = free_support_barycenter( Z_list, a_list, Y_init, b, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, numThreads=numThreads, ) if log: # unpack Y, log_dict = out else: Y = out log_dict = None Y = Y @ B.T # return to the Generalized WB formulation if log: return Y, log_dict else: return Y def ot_barycenter_energy(measure_locations, measure_weights, X, a, cost_list, nx=None): r""" Computes the energy of the OT barycenter functional for a given barycenter support `X` and weights `a`: .. math:: V(X, a) = \sum_{k=1}^K w_k \mathcal{T}_{c_k}(X, a, Y_k, b_k), where: - :math:`X` (n, d) is the barycenter support, - :math:`a` (n) is the barycenter weights, - :math:`Y_k` (m_k, d_k) is the k-th measure support (`measure_locations[k]`), - :math:`b_k` (m_k) is the k-th measure weights (`measure_weights[k]`), - :math:`c_k: \mathbb{R}^{n\times d}\times\mathbb{R}^{m_k\times d_k} \rightarrow \mathbb{R}_+^{n\times m_k}` is the k-th cost function (which computes the pairwise cost matrix) - :math:`\mathcal{T}_{c_k}(X, a, Y_k, b)` is the OT cost between the barycenter measure and the k-th measure with respect to the cost :math:`c_k`. The function computes :math:`V(X, a)` as defined above. Parameters ---------- measure_locations : list of array-like List of K arrays of measure positions, each of shape (m_k, d_k). measure_weights : list of array-like List of K arrays of measure weights, each of shape (m_k). X : array-like Array of shape (n, d) representing barycenter points. a : array-like Array of shape (n,) representing barycenter weights. cost_list : list of callable or callable List of K cost functions :math:`c_k: \mathbb{R}^{n\times d}\times\mathbb{R}^{m_k\times d_k} \rightarrow \mathbb{R}_+^{n\times m_k}`. If cost_list is a single callable, the same cost is used K times. nx : backend, optional The backend to use. Returns ------- V : float The value of the OT barycenter functional :math:`V(X, a)`. References ---------- .. [77] Tanguy, Eloi and Delon, Julie and Gozlan, Nathaël (2024). Computing barycenters of Measures for Generic Transport Costs. arXiv preprint 2501.04016 (2024) See Also -------- ot.lp.free_support_barycenter_generic_costs : Free support solver for the associated barycenter problem. """ if nx is None: nx = get_backend(*measure_locations, *measure_weights, X, a) K = len(measure_locations) if callable(cost_list): cost_list = [cost_list] * K V = 0 for k in range(K): C_k = cost_list[k](X, measure_locations[k]) V += emd2(a, measure_weights[k], C_k) return V def free_support_barycenter_generic_costs( measure_locations, measure_weights, X_init, cost_list, ground_bary=None, a=None, numItermax=100, method="L2_barycentric_proj", stopThr=1e-5, log=False, ground_bary_lr=1e-2, ground_bary_numItermax=100, ground_bary_stopThr=1e-5, ground_bary_solver="SGD", clean_measure=False, ): r""" Solves the OT barycenter problem [77] for generic costs using the fixed point algorithm, iterating the ground barycenter function B on transport plans between the current barycenter and the measures. The problem finds an optimal barycenter support `X` of given size (n, d) (enforced by the initialisation), minimising a sum of pairwise transport costs for the costs :math:`c_k`: .. math:: \min_{X} \sum_{k=1}^K \mathcal{T}_{c_k}(X, a, Y_k, b_k), where: - :math:`X` (n, d) is the barycenter support, - :math:`a` (n) is the (fixed) barycenter weights, - :math:`Y_k` (m_k, d_k) is the k-th measure support (`measure_locations[k]`), - :math:`b_k` (m_k) is the k-th measure weights (`measure_weights[k]`), - :math:`c_k: \mathbb{R}^{n\times d}\times\mathbb{R}^{m_k\times d_k}\rightarrow \mathbb{R}_+^{n\times m_k}` is the k-th cost function (which computes the pairwise cost matrix) - :math:`\mathcal{T}_{c_k}(X, a, Y_k, b)` is the OT cost between the barycenter measure and the k-th measure with respect to the cost :math:`c_k`: .. math:: \mathcal{T}_{c_k}(X, a, Y_k, b_k) = \min_\pi \quad \langle \pi, c_k(X, Y_k) \rangle_F s.t. \ \pi \mathbf{1} = \mathbf{a} \pi^T \mathbf{1} = \mathbf{b_k} \pi \geq 0 in other words, :math:`\mathcal{T}_{c_k}(X, a, Y_k, b_k)` is `ot.emd2(a, b_k, c_k(X, Y_k))`. The function :math:`B:\mathbb{R}^{n\times d_1}\times \cdots\times\mathbb{R}^{n\times d_K} \longrightarrow \mathbb{R}^{n\times d}` is an input to the solver. `B` computes solutions of the following minimisation problem given :math:`(Y_1, \cdots, Y_K) \in \mathbb{R}^{n\times d_1}\times\cdots\times\mathbb{R}^{n\times d_K}` (broadcasted along `n`): .. math:: B(y_1, \cdots, y_K) = \mathrm{argmin}_{x \in \mathbb{R}^d} \sum_{k=1}^K c_k(x, y_k), The input function B takes a list of K arrays of shape (n, d_k) and returns an array of shape (n, d). For certain costs, :math:`B` can be computed explicitly, or through a numerical solver. This function implements two algorithms: - Algorithm 2 from [77] when `method=true_fixed_point` is used, which may increase the support size of the barycenter at each iteration, with a maximum final size of :math:`N_0 + T\sum_k n_k - TK` for T iterations and an initial support size of :math:`N_0`. The computation of the iterates is done using the North West Corner multi-marginal gluing method. This method has convergence guarantees [77]. - Algorithm 3 from [77] when `method=L2_barycentric_proj` is used, which is a heuristic simplification which fixes the weights and support size of the barycenter by performing barycentric projections of the pair-wise OT matrices. This method is substantially faster than the first one, but does not have convergence guarantees. (Default) The implemented methods ([77] Algorithms 2 and 3), generalises [20] and [43] to general costs and includes convergence guarantees, including for discrete measures. Parameters ---------- measure_locations : list of array-like List of K arrays of measure positions, each of shape (m_k, d_k). measure_weights : list of array-like List of K arrays of measure weights, each of shape (m_k). X_init : array-like Array of shape (n, d) representing initial barycenter points. cost_list : list of callable or callable List of K cost functions :math:`c_k: \mathbb{R}^{n\times d}\times\mathbb{R}^{m_k\times d_k} \rightarrow \mathbb{R}_+^{n\times m_k}`. If cost_list is a single callable, the same cost is used K times. ground_bary : callable or None, optional Function List(array(n, d_k)) -> array(n, d) accepting a list of K arrays of shape :math:`(n\times d_K)`, computing the ground barycenters (broadcasted over n). If not provided, done with Adam on PyTorch (requires PyTorch backend), inefficiently using the cost functions in `cost_list`. This function must be provided if `method="true_fixed_point"` is used. a : array-like, optional Array of shape (n,) representing weights of the barycenter measure.Defaults to uniform. numItermax : int, optional Maximum number of iterations (default is 100). method : str, optional Barycentre method: 'L2_barycentric_proj' (default) for Euclidean barycentric projection, or 'true_fixed_point' for iterates using the North West Corner multi-marginal gluing method. stopThr : float, optional If :math:`W_2^2(a_t, X_t, a_{t+1}, X_{t+1}) < \mathrm{stopThr} \times \frac{1}{n}\|X_t\|_2^2`, terminate (default is 1e-5). log : bool, optional Whether to return the log dictionary (default is False). ground_bary_lr : float, optional Learning rate for the ground barycenter solver (if auto is used). ground_bary_numItermax : int, optional Maximum number of iterations for the ground barycenter solver (if auto is used). ground_bary_stopThr : float, optional Stop threshold for the ground barycenter solver (if auto is used): stop if the energy decreases less than this value. ground_bary_solver : str, optional Solver for auto ground bary solver (torch SGD or Adam). Default is "SGD". clean_measure : bool, optional For method=='true_fixed_point', whether to clean the discrete measure (X, a) at each iteration to remove duplicate points and sum their weights (default is False). Returns ------- X : array-like Array of shape (n, d) representing barycenter points. log_dict : list of array-like, optional log containing the exit status, list of iterations and list of displacements if log is True. References ---------- .. [77] Tanguy, Eloi and Delon, Julie and Gozlan, Nathaël (2024). Computing barycenters of Measures for Generic Transport Costs. arXiv preprint 2501.04016 (2024) .. [20] Cuturi, Marco, and Arnaud Doucet. "Fast computation of Wasserstein barycenters." International Conference on Machine Learning. 2014. .. [43] �lvarez-Esteban, Pedro C., et al. "A fixed-point approach to barycenters in Wasserstein space." Journal of Mathematical Analysis and Applications 441.2 (2016): 744-762. .. note:: For the case of the L2 cost :math:`c_k(x, y) = \|x-y\|_2^2`, the ground barycenter is simply the Euclidean barycenter, i.e. :math:`B(y_1, \cdots, y_K) = \sum_k w_k y_k`. In this case, we recover the free-support algorithm from [20]. See Also -------- ot.lp.free_support_barycenter : Free support solver for the case where :math:`c_k(x,y) = \lambda_k\|x-y\|_2^2`. ot.lp.generalized_free_support_barycenter : Free support solver for the case where :math:`c_k(x,y) = \|P_kx-y\|_2^2` with :math:`P_k` linear. ot.lp.NorthWestMMGluing : gluing method used in the `true_fixed_point` method. """ assert method in [ "L2_barycentric_proj", "true_fixed_point", ], "Method must be 'L2_barycentric_proj' or 'true_fixed_point'" nx = get_backend(X_init, measure_locations[0]) K = len(measure_locations) n = X_init.shape[0] if a is None: a = nx.ones(n, type_as=X_init) / n if callable(cost_list): # use the given cost for all K pairs cost_list = [cost_list] * K auto_ground_bary = False if ground_bary is None: auto_ground_bary = True assert ( method == "L2_barycentric_proj" ), "ground_bary must be provided if method is 'true_fixed_point'" assert str(nx) == "torch", ( f"Backend {str(nx)} is not compatible with ground_bary=None, it " "must be provided if not using PyTorch backend" ) try: import torch from torch.optim import Adam, SGD def ground_bary(y, x_init): x = x_init.clone().detach().requires_grad_(True) solver = Adam if ground_bary_solver == "Adam" else SGD opt = solver([x], lr=ground_bary_lr) loss_prev = None for i in range(ground_bary_numItermax): opt.zero_grad() # inefficient cost computation but compatible # with the choice of cost_list[k] giving the cost matrix loss = torch.sum( torch.stack( [torch.diag(cost_list[k](x, y[k])) for k in range(K)] ) ) loss.backward() opt.step() if i == 0: diff = ground_bary_stopThr + 1.0 else: diff = loss_prev - loss.item() loss_prev = loss.item() if diff < ground_bary_stopThr: break return x.detach() except ImportError: raise ImportError("PyTorch is required to use ground_bary=None") X_list = [X_init] if log else [] # store the iterations a_list = [a] if log and method == "true_fixed_point" else [] X = X_init diff_list = [] # store energy differences V_list = [] # store energy values exit_status = "Max iterations reached" for i in range(numItermax): pi_list = [ # compute the pairwise transport plans emd(a, measure_weights[k], cost_list[k](X, measure_locations[k])) for k in range(K) ] if i == 0: # compute initial energy V = ot_barycenter_energy( measure_locations, measure_weights, X, a, cost_list, nx=nx ) if log: V_list.append(V) if method == "L2_barycentric_proj": a_next = a # barycentre weights are fixed Y_perm = [ (1 / a[:, None]) * pi_list[k] @ measure_locations[k] for k in range(K) ] # L2 barycentric projection of pi_k if auto_ground_bary: # use previous position as initialization X_next = ground_bary(Y_perm, X) else: X_next = ground_bary(Y_perm) elif method == "true_fixed_point": # North West Corner gluing of pi_k J, a_next = NorthWestMMGluing(pi_list, nx=nx) # J is a (N, K) array of indices, w is a (N,) array of weights # Each Y_perm[k] is a (N, d_k) array of some points in Y_list[k] Y_perm = [measure_locations[k][J[:, k]] for k in range(K)] # warm start impossible due to possible size mismatch X_next = ground_bary(Y_perm) if clean_measure and method == "true_fixed_point": # clean the discrete measure (X, a) to remove duplicates X_next, a_next = _clean_discrete_measure(X_next, a_next, nx=nx) V_next = ot_barycenter_energy( measure_locations, measure_weights, X_next, a_next, cost_list, nx=nx ) diff = V - V_next if log: X_list.append(X_next) V_list.append(V_next) if method == "true_fixed_point": a_list.append(a_next) diff_list.append(diff) X = X_next a = a_next V = V_next if diff < stopThr: exit_status = "Stationary Point" break if log: log_dict = { "X_list": X_list, "exit_status": exit_status, "a_list": a_list, "diff_list": diff_list, "V_list": V_list, } if method == "true_fixed_point": return X, a, log_dict else: return X, log_dict if method == "true_fixed_point": return X, a else: return X def _to_int_array(x, nx=None): """ Converts an array to an integer type array. """ if nx is None: nx = get_backend(x) if str(nx) == "numpy": return x.astype(int) if str(nx) == "torch": return x.to(int) if str(nx) == "jax": return x.astype(int) if str(nx) == "cupy": return x.astype(int) if str(nx) == "tf": import tensorflow as tf return tf.cast(x, tf.int32) def NorthWestMMGluing(pi_list, a=None, log=False, nx=None): r""" Glue transport plans :math:`(\pi_1, ..., \pi_K)` which have a common first marginal using the (multi-marginal) North-West Corner method. Writing the marginals of each :math:`\pi_k\in \mathbb{R}^{n\times n_l}` as :math:`a \in \mathbb{R}^n` and :math:`b_k \in \mathbb{R}^{n_k}`, the output represents a particular K-marginal transport plan :math:`\rho \in \mathbb{R}^{n_1\times\cdots\times n_K}` whose k-th marginal is :math:`b_k`. This K-plan is such that there exists a K+1-marginal transport plan :math:`\gamma \in \mathbb{R}^{n\times n_1 \times \cdots \times n_K}` such that :math:`\sum_i\gamma_{i,j_1,\cdots,j_K} = \rho_{j_1, \cdots, j_K}` and with Einstein summation convention, :math:`\gamma_{i, j_1, \cdots, j_K} = [\pi_k]_{i, j_k}` for all :math:`k=1,\cdots,K`. Instead of outputting the full K-multi-marginal plan :math:`\rho`, this function provides an array `J` of shape (N, K) where each `J[i]` is of the form `(J[i, 1], ..., J[i, K])` with each `J[i, k]` between 0 and :math:`n_k-1`, and a weight vector `w` of size N, such that the K-plan :math:`\rho` writes: .. math:: \rho_{j_1, \cdots, j_K} = \mathbb{1}\left(\exists i \text{ s.t. } (j_1, \cdots, j_K) = (J[i, 1], \cdots, J[i, K])\right)\ w_i. This representation is useful for its memory efficiency, as it avoids storing the full K-marginal plan. If `log=True`, the function computes the full K+1-marginal transport plan :math:`\gamma` and stores it in log_dict['gamma']. Note that this option is extremely costly in memory. Parameters ---------- pi_list : list of arrays (n, n_k) List of transport plans. log : bool, optional If True, return a log dictionary (computationally expensive). a : array (n,), optional The common first marginal of each transport plan. If None is provided, it is computed as the first marginal of the first transport plan. nx : backend, optional The backend to use. If None is provided, the backend of the first transport plan is used. Returns ------- J : array (N, K) The indices (J[i, 1], ..., J[i, K]) of the K-plan :math:`\rho`. w : array (N,) The weights w_i of the K-plan :math:`\rho`. log_dict : dict, optional If log=True, a dictionary containing the full K+1-marginal transport plan under the key 'gamma'. """ if nx is None: nx = get_backend(pi_list[0]) if a is None: a = nx.sum(pi_list[0], axis=1) # common first marginal a in Delta_n nk_list = [pi.shape[1] for pi in pi_list] # list of n_k K = len(pi_list) n = pi_list[0].shape[0] # number of points in the first marginal gamma = None log_dict = {} if log: # n x n_1 x ... x n_K tensor gamma = nx.zeros([n] + nk_list, type_as=pi_list[0]) gamma_weights = {} # dict of (j_1, ..., j_K) : weight P_list = [nx.copy(pi) for pi in pi_list] # copy of the transport plans # jjs is a list of K lists of size m_k # checks if each jj_idx[k] is < m_k # this is to avoid over-shooting the while loop due to numerical # imprecision in the conditions "x > 0" def jj_idx_in_range(jj_idx, jjs): out = True for k in range(K): out = out and jj_idx[k] < len(jjs[k]) return out for i in range(n): # jjs[k] is the list of indices j in [0, n_k - 1] such that Pk[i, j] >0 jjs = [nx.to_numpy(nx.where(P[i, :] > 0)[0]) for P in P_list] # list [0, ..., 0] of size K for use with jjs: current indices in jjs jj_idx = [0] * K u = a[i] # mass at i, will decrease to 0 as we fill gamma[i, :] # while there is mass to add to gamma[i, :] while u > 0 and jj_idx_in_range(jj_idx, jjs): # current multi-index j_1 ... j_K jj = tuple(jjs[k][jj_idx[k]] for k in range(K)) # min transport plan value: min_k pi_k[i, j_k] v = nx.min(nx.stack([P_list[k][i, jj[k]] for k in range(K)])) if log: # assign mass v to gamma[i, j_1, ..., j_K] gamma[(i,) + jj] = v if jj in gamma_weights: gamma_weights[jj] += v else: gamma_weights[jj] = v u -= v # at i, we u-v mass left to assign for k in range(K): # update plan copies Pk P_list[k][i, jj[k]] -= v # Pk[i, j_k] has v less mass left if P_list[k][i, jj[k]] == 0: # move to next index in jjs[k] if Pk[i, j_k] is empty jj_idx[k] += 1 log_dict["gamma"] = gamma J = list(gamma_weights.keys()) # list of multi-indices (j_1, ..., j_K) J = _to_int_array(nx.from_numpy(np.array(J), type_as=pi_list[0]), nx=nx) w = nx.stack(list(gamma_weights.values())) if log: return J, w, log_dict return J, w def _clean_discrete_measure(X, a, tol=1e-10, nx=None): r""" Simplifies a discrete measure by consolidating duplicate points and summing their weights. Given a discrete measure with support X (n, d) and weights a (n), returns a points Y (m, d) and weights b (m) such that Y is the set of unique points in X and b is the sum of weights in a for each point in Y Parameters ---------- X : array-like Array of shape (n, d) representing the support points of the discrete measure. a : array-like Array of shape (n,) representing the weights associated with the support points. tol : float, optional Tolerance for determining uniqueness of points in `X`. Points closer than `tol` are considered identical. Default is 1e-10. nx : backend, optional The backend to use. If None is provided, the backend of `X` is used Returns ------- Y : array-like Array of shape (m, d) representing the unique support points of the discrete measure. b : array-like Array of shape (m,) representing the summed weights for each unique point in `Y`. """ nx = get_backend(X, a) D = dist(X, X) # each D[I[k], J[k]] < tol so X[I[k]] = X[J[k]] idxI, idxJ = nx.where(D < tol) idxI = nx.to_numpy(idxI) idxJ = nx.to_numpy(idxJ) # keep only the cases I[k] <= J[k] to avoid pairs (i, j) (j, i) with i != j mask = idxI <= idxJ idxI, idxJ = idxI[mask], idxJ[mask] X_idx_to_Y_idx = {} # X[i] = Y[X_idx_to_Y_idx[i]] # indices of unique points in X, at the end, Y := X[unique_X_idx] unique_X_idx = [] b = [] for i, j in zip(idxI, idxJ): if i not in X_idx_to_Y_idx: # i is a new point unique_X_idx.append(i) X_idx_to_Y_idx[i] = len(unique_X_idx) - 1 b.append(a[i]) else: # i is not new, check if j is known if j not in X_idx_to_Y_idx: b[X_idx_to_Y_idx[i]] += a[j] X_idx_to_Y_idx[j] = X_idx_to_Y_idx[i] # create the unique points array Y Y = X[tuple(unique_X_idx), :] b = nx.from_numpy(np.array(b), type_as=X) return Y, b python-pot-0.9.7+dfsg/ot/lp/_network_simplex.py000066400000000000000000001125021525521106200215670ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Solvers for the original linear program OT problem. """ # Author: Remi Flamary # # License: MIT License import numpy as np import warnings from ..utils import list_to_array, check_number_threads from ..backend import get_backend from .emd_wrap import emd_c, emd_c_sparse, emd_c_lazy, check_result def center_ot_dual(alpha0, beta0, a=None, b=None): r"""Center dual OT potentials w.r.t. their weights The main idea of this function is to find unique dual potentials that ensure some kind of centering/fairness. The main idea is to find dual potentials that lead to the same final objective value for both source and targets (see below for more details). It will help having stability when multiple calling of the OT solver with small changes. Basically we add another constraint to the potential that will not change the objective value but will ensure unicity. The constraint is the following: .. math:: \alpha^T \mathbf{a} = \beta^T \mathbf{b} in addition to the OT problem constraints. since :math:`\sum_i a_i=\sum_j b_j` this can be solved by adding/removing a constant from both :math:`\alpha_0` and :math:`\beta_0`. .. math:: c &= \frac{\beta_0^T \mathbf{b} - \alpha_0^T \mathbf{a}}{\mathbf{1}^T \mathbf{b} + \mathbf{1}^T \mathbf{a}} \alpha &= \alpha_0 + c \beta &= \beta_0 + c Parameters ---------- alpha0 : (ns, ...) numpy.ndarray, float64 Source dual potential beta0 : (nt, ...) numpy.ndarray, float64 Target dual potential a : (ns, ...) numpy.ndarray, float64 Source histogram (uniform weight if empty list) b : (nt, ...) numpy.ndarray, float64 Target histogram (uniform weight if empty list) Returns ------- alpha : (ns, ...) numpy.ndarray, float64 Source centered dual potential beta : (nt, ...) numpy.ndarray, float64 Target centered dual potential """ nx = get_backend(alpha0, beta0, a, b) n = alpha0.shape[0] m = beta0.shape[0] # if no weights are provided, use uniform if a is None: a = nx.full(alpha0.shape, 1.0 / n, type_as=alpha0) elif a.ndim != alpha0.ndim: a = nx.repeat(a[..., None], alpha0.shape[-1], -1) if b is None: b = nx.full(beta0.shape, 1.0 / m, type_as=beta0) elif b.ndim != beta0.ndim: b = nx.repeat(b[..., None], beta0.shape[-1], -1) # compute constant that balances the weighted sums of the duals ips = nx.sum(b * beta0, axis=0) - nx.sum(a * alpha0, axis=0) denom = nx.sum(a, axis=0) + nx.sum(b, axis=0) c = ips / denom # update duals alpha = alpha0 + c beta = beta0 - c return alpha, beta def estimate_dual_null_weights(alpha0, beta0, a, b, M): r"""Estimate feasible values for 0-weighted dual potentials The feasible values are computed efficiently but rather coarsely. .. warning:: This function is necessary because the C++ solver in `emd_c` discards all samples in the distributions with zeros weights. This means that while the primal variable (transport matrix) is exact, the solver only returns feasible dual potentials on the samples with weights different from zero. First we compute the constraints violations: .. math:: \mathbf{V} = \alpha + \beta^T - \mathbf{M} Next we compute the max amount of violation per row (:math:`\alpha`) and columns (:math:`beta`) .. math:: \mathbf{v^a}_i = \max_j \mathbf{V}_{i,j} \mathbf{v^b}_j = \max_i \mathbf{V}_{i,j} Finally we update the dual potential with 0 weights if a constraint is violated .. math:: \alpha_i = \alpha_i - \mathbf{v^a}_i \quad \text{ if } \mathbf{a}_i=0 \text{ and } \mathbf{v^a}_i>0 \beta_j = \beta_j - \mathbf{v^b}_j \quad \text{ if } \mathbf{b}_j=0 \text{ and } \mathbf{v^b}_j > 0 In the end the dual potentials are centered using function :py:func:`ot.lp.center_ot_dual`. Note that all those updates do not change the objective value of the solution but provide dual potentials that do not violate the constraints. Parameters ---------- alpha0 : (ns,) numpy.ndarray, float64 Source dual potential beta0 : (nt,) numpy.ndarray, float64 Target dual potential alpha0 : (ns,) numpy.ndarray, float64 Source dual potential beta0 : (nt,) numpy.ndarray, float64 Target dual potential a : (ns,) numpy.ndarray, float64 Source distribution (uniform weights if empty list) b : (nt,) numpy.ndarray, float64 Target distribution (uniform weights if empty list) M : (ns,nt) numpy.ndarray, float64 Loss matrix (c-order array with type float64) Returns ------- alpha : (ns,) numpy.ndarray, float64 Source corrected dual potential beta : (nt,) numpy.ndarray, float64 Target corrected dual potential """ # binary indexing of non-zeros weights asel = a != 0 bsel = b != 0 # compute dual constraints violation constraint_violation = alpha0[:, None] + beta0[None, :] - M # Compute largest violation per line and columns aviol = np.max(constraint_violation, 1) bviol = np.max(constraint_violation, 0) # update corrects violation of alpha_up = -1 * ~asel * np.maximum(aviol, 0) beta_up = -1 * ~bsel * np.maximum(bviol, 0) alpha = alpha0 + alpha_up beta = beta0 + beta_up return center_ot_dual(alpha, beta, a, b) def _compute_active_subset(a, b, M, row_mask, col_mask): """Return filtered inputs restricted to the rows/columns with mass.""" need_filter = np.any(~row_mask) or np.any(~col_mask) if not need_filter: return need_filter, None, None, a, b, M row_idx = np.flatnonzero(row_mask) col_idx = np.flatnonzero(col_mask) M_solver = np.asarray(M[np.ix_(row_idx, col_idx)], dtype=np.float64, order="C") return need_filter, row_idx, col_idx, a[row_idx], b[col_idx], M_solver def _inflate_from_active_subset( G, u, v, need_filter, row_idx, col_idx, row_mask, col_mask, n_rows, n_cols ): """Embed the filtered dense solution back into the full support.""" if not need_filter: return G, u, v G_full = np.zeros((n_rows, n_cols), dtype=G.dtype) G_full[np.ix_(row_idx, col_idx)] = G u_full = np.zeros((n_rows,), dtype=u.dtype) v_full = np.zeros((n_cols,), dtype=v.dtype) u_full[row_mask] = u v_full[col_mask] = v return G_full, u_full, v_full def _prepare_warmstart(potentials_init, need_filter, row_mask, col_mask): """Return warm-start potentials filtered to the active support.""" if potentials_init is None: return None, None alpha_init, beta_init = potentials_init alpha_init = np.asarray(alpha_init, dtype=np.float64) beta_init = np.asarray(beta_init, dtype=np.float64) if need_filter: alpha_init = alpha_init[row_mask] beta_init = beta_init[col_mask] return alpha_init, beta_init def emd( a, b, M, numItermax=100000, log=False, center_dual=True, numThreads=1, check_marginals=True, potentials_init=None, ): r"""Solves the Earth Movers distance problem and returns the OT matrix .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F s.t. \ \gamma \mathbf{1} = \mathbf{a} \gamma^T \mathbf{1} = \mathbf{b} \gamma \geq 0 where : - :math:`\mathbf{M}` is the metric cost matrix - :math:`\mathbf{a}` and :math:`\mathbf{b}` are the sample weights .. warning:: Note that the :math:`\mathbf{M}` matrix in numpy needs to be a C-order numpy.array in float64 format. It will be converted if not in this format .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. .. note:: This function will cast the computed transport plan to the data type of the provided input with the following priority: :math:`\mathbf{a}`, then :math:`\mathbf{b}`, then :math:`\mathbf{M}` if marginals are not provided. Casting to an integer tensor might result in a loss of precision. If this behaviour is unwanted, please make sure to provide a floating point input. .. note:: An error will be raised if the vectors :math:`\mathbf{a}` and :math:`\mathbf{b}` do not sum to the same value. Uses the algorithm proposed in :ref:`[1] `. Parameters ---------- a : (ns,) array-like, float Source histogram (uniform weight if empty list) b : (nt,) array-like, float Target histogram (uniform weight if empty list) M : (ns,nt) array-like or sparse matrix, float Loss matrix. Can be: - Dense array (c-order array in numpy with type float64) - Sparse matrix in backend's format (scipy.sparse.coo_matrix for NumPy backend, torch.sparse_coo_tensor for PyTorch backend, etc.) numItermax : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. log: bool, optional (default=False) If True, returns a dictionary containing the cost and dual variables. Otherwise returns only the optimal transportation matrix. center_dual: boolean, optional (default=True) If True, centers the dual potential using function :py:func:`ot.lp.center_ot_dual`. numThreads: int or "max", optional (default=1) Deprecated compatibility parameter. The network simplex solver no longer uses OpenMP, so this parameter is ignored. check_marginals: bool, optional (default=True) If True, checks that the marginals mass are equal. If False, skips the check. potentials_init: tuple of two arrays (alpha, beta), optional (default=None) Warmstart dual potentials to accelerate convergence. Should be a tuple (alpha, beta) where alpha is shape (ns,) and beta is shape (nt,). These potentials are used to guide initial pivots in the network simplex. Typically obtained from a previous EMD solve or Sinkhorn approximation. .. note:: The solver automatically detects sparse format using the backend's :py:meth:`issparse` method. For sparse inputs: - Uses a memory-efficient sparse EMD algorithm - Returns the transport plan as a sparse matrix in the backend's format - Supports scipy.sparse (NumPy), torch.sparse (PyTorch), etc. - JAX and TensorFlow backends don't support sparse matrices Returns ------- gamma: array-like or sparse matrix, shape (ns, nt) Optimal transportation matrix for the given parameters. - For dense inputs: returns a dense array - For sparse inputs: returns a sparse matrix in the backend's format (e.g., scipy.sparse.coo_matrix for NumPy, torch.sparse_coo_tensor for PyTorch) log: dict, optional If input log is True, a dictionary containing the cost, dual variables, and exit status. Examples -------- Simple example with obvious solution. The function emd accepts lists and perform automatic conversion to numpy arrays >>> import ot >>> a=[.5,.5] >>> b=[.5,.5] >>> M=[[0.,1.],[1.,0.]] >>> ot.emd(a, b, M) array([[0.5, 0. ], [0. , 0.5]]) .. _references-emd: References ---------- .. [1] Bonneel, N., Van De Panne, M., Paris, S., & Heidrich, W. (2011, December). Displacement interpolation using Lagrangian mass transport. In ACM Transactions on Graphics (TOG) (Vol. 30, No. 6, p. 158). ACM. See Also -------- ot.bregman.sinkhorn : Entropic regularized OT ot.optim.cg : General regularized OT """ n1, n2 = None, None # Convert lists to arrays, using M to detect backend when a,b are empty a, b, M = list_to_array(a, b, M) nx = get_backend(a, b, M) # Check if M is sparse using backend's issparse method is_sparse = nx.issparse(M) if is_sparse: # Check if backend supports sparse matrices backend_name = nx.__class__.__name__ if backend_name in ["JaxBackend", "TensorflowBackend"]: raise NotImplementedError( f"Sparse optimal transport is not supported for {backend_name}. " "JAX does not have native sparse matrix support, and TensorFlow's " "sparse implementation is incomplete. Please convert your sparse " "matrix to dense format using M.toarray() or equivalent before calling emd()." ) # Extract COO data using backend method - returns numpy arrays edge_sources, edge_targets, edge_costs, (n1, n2) = nx.sparse_coo_data(M) # Ensure correct dtypes for C++ solver if edge_sources.dtype != np.uint64: edge_sources = edge_sources.astype(np.uint64) if edge_targets.dtype != np.uint64: edge_targets = edge_targets.astype(np.uint64) if edge_costs.dtype != np.float64: edge_costs = edge_costs.astype(np.float64) if a is not None and len(a) != 0: type_as = a elif b is not None and len(b) != 0: type_as = b else: type_as = a if a is not None else b # Set n1, n2 if not already set (dense case) if n1 is None: n1, n2 = M.shape if a is None or len(a) == 0: a = nx.ones((n1,), type_as=type_as) / n1 if b is None or len(b) == 0: b = nx.ones((n2,), type_as=type_as) / n2 if is_sparse: a, b = nx.to_numpy(a, b) else: M, a, b = nx.to_numpy(M, a, b) M = np.asarray(M, dtype=np.float64, order="C") a = np.asarray(a, dtype=np.float64) b = np.asarray(b, dtype=np.float64) assert ( a.shape[0] == n1 and b.shape[0] == n2 ), "Dimension mismatch, check dimensions of M with a and b" # ensure that same mass if check_marginals: np.testing.assert_almost_equal( a.sum(0), b.sum(0), err_msg="a and b vector must have the same sum", decimal=6, ) b = b * a.sum() / b.sum() asel = a != 0 bsel = b != 0 numThreads = check_number_threads(numThreads) if numThreads != 1: warnings.warn( "The 'numThreads' parameter is deprecated and will be removed in a " "future version. The network simplex solver no longer uses OpenMP, " "so this parameter is ignored.", DeprecationWarning, stacklevel=2, ) # ============================================================================ # CALL SOLVER (sparse or dense) # ============================================================================ if is_sparse: # Sparse solver - never build full matrix flow_sources, flow_targets, flow_values, cost, u, v, result_code = emd_c_sparse( a, b, edge_sources, edge_targets, edge_costs, numItermax ) else: row_mask = asel col_mask = bsel ( need_filter, row_idx, col_idx, a_solver, b_solver, M_solver, ) = _compute_active_subset(a, b, M, row_mask, col_mask) alpha_init, beta_init = _prepare_warmstart( potentials_init, need_filter, row_mask, col_mask ) # Dense solver G, cost, u, v, result_code = emd_c( a_solver, b_solver, M_solver, numItermax, numThreads, alpha_init, beta_init ) G, u, v = _inflate_from_active_subset( G, u, v, need_filter, row_idx, col_idx, row_mask, col_mask, n1, n2 ) # ============================================================================ # POST-PROCESS DUAL VARIABLES AND CREATE TRANSPORT PLAN # ============================================================================ # Center dual potentials if center_dual: u, v = center_ot_dual(u, v, a, b) # Handle null weights if np.any(~asel) or np.any(~bsel): if is_sparse: u, v = center_ot_dual(u, v, a, b) else: u, v = estimate_dual_null_weights(u, v, a, b, M) result_code_string = check_result(result_code) # Create transport plan in backend format if is_sparse: # Convert flow to sparse matrix using backend's coo_matrix method flow_values_backend = nx.from_numpy(flow_values, type_as=type_as) flow_sources_backend = nx.from_numpy( flow_sources.astype(np.int64), type_as=type_as ) flow_targets_backend = nx.from_numpy( flow_targets.astype(np.int64), type_as=type_as ) G = nx.coo_matrix( flow_values_backend, flow_sources_backend, flow_targets_backend, shape=(n1, n2), type_as=type_as, ) else: # Warn about integer casting for dense case if not nx.is_floating_point(type_as): warnings.warn( "Input histogram consists of integer. The transport plan will be " "casted accordingly, possibly resulting in a loss of precision. " "If this behaviour is unwanted, please make sure your input " "histogram consists of floating point elements.", stacklevel=2, ) G = nx.from_numpy(G, type_as=type_as) # Return results if log: log_dict = { "cost": cost, "u": nx.from_numpy(u, type_as=type_as), "v": nx.from_numpy(v, type_as=type_as), "warning": result_code_string, "result_code": result_code, } return G, log_dict else: return G def emd2( a, b, M, processes=1, numItermax=100000, log=False, return_matrix=False, center_dual=True, numThreads=1, check_marginals=True, potentials_init=None, ): r"""Solves the Earth Movers distance problem and returns the loss .. math:: \min_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F s.t. \ \gamma \mathbf{1} = \mathbf{a} \gamma^T \mathbf{1} = \mathbf{b} \gamma \geq 0 where : - :math:`\mathbf{M}` is the metric cost matrix - :math:`\mathbf{a}` and :math:`\mathbf{b}` are the sample weights .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. .. note:: This function will cast the computed transport plan and transportation loss to the data type of the provided input with the following priority : :math:`\mathbf{a}`, then :math:`\mathbf{b}`, then :math:`\mathbf{M}` if marginals are not provided. Casting to an integer tensor might result in a loss of precision. If this behaviour is unwanted, please make sure to provide a floating point input. .. note:: An error will be raised if the vectors :math:`\mathbf{a}` and :math:`\mathbf{b}` do not sum to the same value. Uses the algorithm proposed in :ref:`[1] `. Parameters ---------- a : (ns,) array-like, float64 Source histogram (uniform weight if empty list) b : (nt,) array-like, float64 Target histogram (uniform weight if empty list) M : (ns,nt) array-like or sparse matrix, float64 Loss matrix. Can be: - Dense array (c-order array in numpy with type float64) - Sparse matrix in backend's format (scipy.sparse.coo_matrix for NumPy backend, torch.sparse_coo_tensor for PyTorch backend, etc.) processes : int, optional (default=1) Nb of processes used for multiple emd computation (deprecated) numItermax : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. log: boolean, optional (default=False) If True, returns a dictionary containing the cost and dual variables. Otherwise returns only the optimal transportation cost. return_matrix: boolean, optional (default=False) If True, returns the optimal transportation matrix in the log. center_dual: boolean, optional (default=True) If True, centers the dual potential using function :py:func:`ot.lp.center_ot_dual`. numThreads: int or "max", optional (default=1) Deprecated compatibility parameter. The network simplex solver no longer uses OpenMP, so this parameter is ignored. check_marginals: bool, optional (default=True) If True, checks that the marginals mass are equal. If False, skips the check. potentials_init: tuple of two arrays (alpha, beta), optional (default=None) Warmstart dual potentials to accelerate convergence. Should be a tuple (alpha, beta) where alpha is shape (ns,) and beta is shape (nt,). These potentials are used to guide initial pivots in the network simplex. Typically obtained from a previous EMD solve or Sinkhorn approximation. .. note:: The solver automatically detects sparse format using the backend's :py:meth:`issparse` method. For sparse inputs: - Uses a memory-efficient sparse EMD algorithm - Edges not included are treated as having infinite cost (forbidden) - Supports scipy.sparse (NumPy), torch.sparse (PyTorch), etc. - JAX and TensorFlow backends don't support sparse matrices Returns ------- W: float, array-like Optimal transportation loss for the given parameters log: dict If input log is true, a dictionary containing the cost, dual variables (u, v), exit status, and optionally the optimal transportation matrix (G) if return_matrix is True Examples -------- Simple example with obvious solution. The function emd accepts lists and perform automatic conversion to numpy arrays >>> import ot >>> a=[.5,.5] >>> b=[.5,.5] >>> M=[[0.,1.],[1.,0.]] >>> ot.emd2(a,b,M) 0.0 .. _references-emd2: References ---------- .. [1] Bonneel, N., Van De Panne, M., Paris, S., & Heidrich, W. (2011, December). Displacement interpolation using Lagrangian mass transport. In ACM Transactions on Graphics (TOG) (Vol. 30, No. 6, p. 158). ACM. See Also -------- ot.bregman.sinkhorn : Entropic regularized OT ot.optim.cg : General regularized OT """ n1, n2 = None, None a, b, M = list_to_array(a, b, M) nx = get_backend(a, b, M) # Check if M is sparse using backend's issparse method is_sparse = nx.issparse(M) # Save original sparse tensor for gradient tracking (before conversion to numpy) M_original_sparse = None if is_sparse: # Check if backend supports sparse matrices backend_name = nx.__class__.__name__ if backend_name in ["JaxBackend", "TensorflowBackend"]: raise NotImplementedError() # Save original M for gradient tracking (before numpy conversion) M_original_sparse = M edge_sources, edge_targets, edge_costs, (n1, n2) = nx.sparse_coo_data(M) if edge_sources.dtype != np.uint64: edge_sources = edge_sources.astype(np.uint64) if edge_targets.dtype != np.uint64: edge_targets = edge_targets.astype(np.uint64) if edge_costs.dtype != np.float64: edge_costs = edge_costs.astype(np.float64) if a is not None and len(a) != 0: type_as = a elif b is not None and len(b) != 0: type_as = b else: type_as = a if a is not None else b # Set n1, n2 if not already set (dense case) if n1 is None: n1, n2 = M.shape # if empty array given then use uniform distributions if a is None or len(a) == 0: a = nx.ones((n1,), type_as=type_as) / n1 if b is None or len(b) == 0: b = nx.ones((n2,), type_as=type_as) / n2 a0, b0 = a, b M0 = None if is_sparse else M if is_sparse: # Use the original sparse tensor (preserves gradients for PyTorch) edge_costs_original = M_original_sparse else: edge_costs_original = None if is_sparse: a, b = nx.to_numpy(a, b) else: M, a, b = nx.to_numpy(M, a, b) M = np.asarray(M, dtype=np.float64, order="C") a = np.asarray(a, dtype=np.float64) b = np.asarray(b, dtype=np.float64) assert ( a.shape[0] == n1 and b.shape[0] == n2 ), "Dimension mismatch, check dimensions of M with a and b" # ensure that same mass if check_marginals: np.testing.assert_almost_equal( a.sum(0), b.sum(0, keepdims=True), err_msg="a and b vector must have the same sum", decimal=6, ) b = b * a.sum(0) / b.sum(0, keepdims=True) asel = a != 0 numThreads = check_number_threads(numThreads) if numThreads != 1: warnings.warn( "The 'numThreads' parameter is deprecated and will be removed in a " "future version. The network simplex solver no longer uses OpenMP, " "so this parameter is ignored.", DeprecationWarning, stacklevel=2, ) # ============================================================================ # DEFINE SOLVER FUNCTION # ============================================================================ def f(b): bsel = b != 0 if is_sparse: # Solve sparse EMD flow_sources, flow_targets, flow_values, cost, u, v, result_code = ( emd_c_sparse(a, b, edge_sources, edge_targets, edge_costs, numItermax) ) else: ( need_filter, row_idx, col_idx, a_solver, b_solver, M_solver, ) = _compute_active_subset(a, b, M, asel, bsel) alpha_init, beta_init = _prepare_warmstart( potentials_init, need_filter, asel, bsel ) # Solve dense EMD G, cost, u, v, result_code = emd_c( a_solver, b_solver, M_solver, numItermax, numThreads, alpha_init, beta_init, ) G, u, v = _inflate_from_active_subset( G, u, v, need_filter, row_idx, col_idx, asel, bsel, n1, n2 ) # Center dual potentials if center_dual: u, v = center_ot_dual(u, v, a, b) # Handle null weights if np.any(~asel) or np.any(~bsel): if is_sparse: u, v = center_ot_dual(u, v, a, b) else: u, v = estimate_dual_null_weights(u, v, a, b, M) # Prepare cost with gradients if is_sparse: # Build gradient mapping for sparse case edge_to_idx = { (edge_sources[k], edge_targets[k]): k for k in range(len(edge_sources)) } grad_edge_costs = np.zeros(len(edge_costs), dtype=np.float64) for idx in range(len(flow_sources)): src, tgt, flow = flow_sources[idx], flow_targets[idx], flow_values[idx] edge_idx = edge_to_idx.get((src, tgt), -1) if edge_idx >= 0: grad_edge_costs[edge_idx] = flow # Convert gradient to sparse format matching edge_costs_original grad_edge_costs_backend = nx.from_numpy(grad_edge_costs, type_as=type_as) if nx.issparse(edge_costs_original): # Reconstruct sparse gradient tensor with same structure as original grad_M_sparse = nx.coo_matrix( grad_edge_costs_backend, nx.from_numpy(edge_sources.astype(np.int64), type_as=type_as), nx.from_numpy(edge_targets.astype(np.int64), type_as=type_as), shape=(n1, n2), type_as=type_as, ) else: grad_M_sparse = grad_edge_costs_backend cost = nx.set_gradients( nx.from_numpy(cost, type_as=type_as), (a0, b0, edge_costs_original), ( nx.from_numpy(u - np.mean(u), type_as=type_as), nx.from_numpy(v - np.mean(v), type_as=type_as), grad_M_sparse, ), ) # Build transport plan in backend sparse format flow_values_backend = nx.from_numpy(flow_values, type_as=type_as) flow_sources_backend = nx.from_numpy( flow_sources.astype(np.int64), type_as=type_as ) flow_targets_backend = nx.from_numpy( flow_targets.astype(np.int64), type_as=type_as ) G_backend = nx.coo_matrix( flow_values_backend, flow_sources_backend, flow_targets_backend, shape=(n1, n2), type_as=type_as, ) else: # Dense case: warn about integer casting if not nx.is_floating_point(type_as): warnings.warn( "Input histogram consists of integer. The transport plan will be " "casted accordingly, possibly resulting in a loss of precision. " "If this behaviour is unwanted, please make sure your input " "histogram consists of floating point elements.", stacklevel=2, ) G_backend = nx.from_numpy(G, type_as=type_as) cost = nx.set_gradients( nx.from_numpy(cost, type_as=type_as), (a0, b0, M0), ( nx.from_numpy(u - np.mean(u), type_as=type_as), nx.from_numpy(v - np.mean(v), type_as=type_as), G_backend, ), ) check_result(result_code) # Return results if log or return_matrix: log_dict = { "cost": cost, "u": nx.from_numpy(u, type_as=type_as), "v": nx.from_numpy(v, type_as=type_as), "warning": check_result(result_code), "result_code": result_code, } if return_matrix: log_dict["G"] = G_backend return [cost, log_dict] else: return cost if len(b.shape) == 1: return f(b) nb = b.shape[1] if processes > 1: warnings.warn( "The 'processes' parameter has been deprecated. " "Multiprocessing should be done outside of POT." ) res = list(map(f, [b[:, i].copy() for i in range(nb)])) return res def emd2_lazy( X_a, X_b, a=None, b=None, metric="sqeuclidean", numItermax=100000, log=False, return_matrix=True, center_dual=True, check_marginals=True, potentials_init=None, ): r"""Solves the Earth Movers distance problem with lazy cost computation and returns the loss .. math:: \min_\gamma \quad \langle \gamma, \mathbf{M}(\mathbf{X}_a, \mathbf{X}_b) \rangle_F s.t. \ \gamma \mathbf{1} = \mathbf{a} \gamma^T \mathbf{1} = \mathbf{b} \gamma \geq 0 where : - :math:`\mathbf{M}(\mathbf{X}_a, \mathbf{X}_b)` is computed on-the-fly from coordinates - :math:`\mathbf{a}` and :math:`\mathbf{b}` are the sample weights .. note:: This function computes distances on-the-fly during the network simplex algorithm, avoiding the O(ns*nt) memory cost of pre-computing the full cost matrix. Memory usage is O(ns+nt) instead. .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. Parameters ---------- X_a : (ns, dim) array-like, float64 Source sample coordinates X_b : (nt, dim) array-like, float64 Target sample coordinates a : (ns,) array-like, float64, optional Source histogram (uniform weight if None) b : (nt,) array-like, float64, optional Target histogram (uniform weight if None) metric : str, optional (default='sqeuclidean') Distance metric for cost computation. Options: - 'sqeuclidean': Squared Euclidean distance - 'euclidean': Euclidean distance - 'cityblock': Manhattan/L1 distance numItermax : int, optional (default=100000) Maximum number of iterations before stopping if not converged log: boolean, optional (default=False) If True, returns a dictionary containing the cost, dual variables, and optionally the transport plan (sparse format) return_matrix: boolean, optional (default=False) If True, returns the optimal transportation matrix in the log (sparse format) center_dual: boolean, optional (default=True) If True, centers the dual potential using :py:func:`ot.lp.center_ot_dual` check_marginals: bool, optional (default=True) If True, checks that the marginals mass are equal potentials_init : tuple of (ns,) and (nt,) arrays, optional Initial dual potentials (u, v) to warmstart the solver. If provided, the solver starts from these potentials instead of a cold start. Returns ------- W: float Optimal transportation loss log: dict If input log is True, a dictionary containing: - cost: the optimal transportation cost - u, v: dual variables - warning: solver status message - result_code: solver return code - G: (optional) sparse transport plan if return_matrix=True See Also -------- ot.emd2 : EMD with pre-computed cost matrix ot.lp.emd_c_lazy : Low-level C++ lazy solver """ a, b, X_a, X_b = list_to_array(a, b, X_a, X_b) nx = get_backend(a, b, X_a, X_b) n1, n2 = X_a.shape[0], X_b.shape[0] if X_a.shape[1] != X_b.shape[1]: raise ValueError( f"X_a and X_b must have the same number of dimensions, " f"got {X_a.shape[1]} and {X_b.shape[1]}" ) if a is not None and len(a) != 0: type_as = a elif b is not None and len(b) != 0: type_as = b else: type_as = X_a if a is None or len(a) == 0: a = nx.ones((n1,), type_as=type_as) / n1 if b is None or len(b) == 0: b = nx.ones((n2,), type_as=type_as) / n2 a0, b0 = a, b # Convert to numpy for C++ backend X_a_np = nx.to_numpy(X_a) X_b_np = nx.to_numpy(X_b) a_np = nx.to_numpy(a) b_np = nx.to_numpy(b) X_a_np = np.asarray(X_a_np, dtype=np.float64, order="C") X_b_np = np.asarray(X_b_np, dtype=np.float64, order="C") a_np = np.asarray(a_np, dtype=np.float64) b_np = np.asarray(b_np, dtype=np.float64) assert ( a_np.shape[0] == n1 and b_np.shape[0] == n2 ), "Dimension mismatch, check dimensions of X_a/X_b with a and b" if check_marginals: np.testing.assert_almost_equal( a_np.sum(), b_np.sum(), err_msg="a and b vector must have the same sum", decimal=6, ) b_np = b_np * a_np.sum() / b_np.sum() # Handle warmstart potentials alpha_init_np = None beta_init_np = None if potentials_init is not None: alpha_init, beta_init = potentials_init alpha_init_np = nx.to_numpy(alpha_init) beta_init_np = nx.to_numpy(beta_init) alpha_init_np = np.asarray(alpha_init_np, dtype=np.float64, order="C") beta_init_np = np.asarray(beta_init_np, dtype=np.float64, order="C") flow_sources, flow_targets, flow_values, cost, u, v, result_code = emd_c_lazy( a_np, b_np, X_a_np, X_b_np, metric, numItermax, alpha_init_np, beta_init_np ) if center_dual: u, v = center_ot_dual(u, v, a_np, b_np) if not nx.is_floating_point(type_as): warnings.warn( "Input histogram consists of integer. The transport plan will be " "casted accordingly, possibly resulting in a loss of precision. " "If this behaviour is unwanted, please make sure your input " "histogram consists of floating point elements.", stacklevel=2, ) cost_backend = nx.set_gradients( nx.from_numpy(cost, type_as=type_as), (a0, b0), ( nx.from_numpy(u - np.mean(u), type_as=type_as), nx.from_numpy(v - np.mean(v), type_as=type_as), ), ) check_result(result_code) if log or return_matrix: log_dict = { "cost": cost_backend, "u": nx.from_numpy(u, type_as=type_as), "v": nx.from_numpy(v, type_as=type_as), "warning": check_result(result_code), "result_code": result_code, } if return_matrix: flow_values_backend = nx.from_numpy(flow_values, type_as=type_as) flow_sources_backend = nx.from_numpy(flow_sources.astype(np.int64)) flow_targets_backend = nx.from_numpy(flow_targets.astype(np.int64)) G_backend = nx.coo_matrix( flow_values_backend, flow_sources_backend, flow_targets_backend, shape=(n1, n2), type_as=type_as, ) log_dict["G"] = G_backend return cost_backend, log_dict else: return cost_backend python-pot-0.9.7+dfsg/ot/lp/core.h000066400000000000000000000066171525521106200167360ustar00rootroot00000000000000/* -*- mode: C++; indent-tabs-mode: nil; -*- * * This file has been adapted by Nicolas Bonneel (2013), * from full_graph.h from LEMON, a generic C++ optimization library, * to make the other files independant from the rest of * the original library. * * **** Original file Copyright Notice : * Copyright (C) 2003-2010 * Egervary Jeno Kombinatorikus Optimalizalasi Kutatocsoport * (Egervary Research Group on Combinatorial Optimization, EGRES). * * Permission to use, modify and distribute this software is granted * provided that this copyright notice appears in all copies. For * precise terms see the accompanying LICENSE file. * * This software is provided "AS IS" with no warranty of any kind, * express or implied, and with no claim as to its suitability for any * purpose. * */ #ifndef LEMON_CORE_H #define LEMON_CORE_H #include #include // Disable the following warnings when compiling with MSVC: // C4250: 'class1' : inherits 'class2::member' via dominance // C4355: 'this' : used in base member initializer list // C4503: 'function' : decorated name length exceeded, name was truncated // C4800: 'type' : forcing value to bool 'true' or 'false' (performance warning) // C4996: 'function': was declared deprecated #ifdef _MSC_VER #pragma warning( disable : 4250 4355 4503 4800 4996 ) #endif ///\file ///\brief LEMON core utilities. /// ///This header file contains core utilities for LEMON. ///It is automatically included by all graph types, therefore it usually ///do not have to be included directly. namespace lemon { /// \brief Dummy type to make it easier to create invalid iterators. /// /// Dummy type to make it easier to create invalid iterators. /// See \ref INVALID for the usage. struct Invalid { public: bool operator==(Invalid) { return true; } bool operator!=(Invalid) { return false; } bool operator< (Invalid) { return false; } }; /// \brief Invalid iterators. /// /// \ref Invalid is a global type that converts to each iterator /// in such a way that the value of the target iterator will be invalid. #ifdef LEMON_ONLY_TEMPLATES const Invalid INVALID = Invalid(); #else extern const Invalid INVALID; #endif /// \addtogroup gutils /// @{ ///Create convenience typedefs for the digraph types and iterators ///This \c \#define creates convenient type definitions for the following ///types of \c Digraph: \c Node, \c NodeIt, \c Arc, \c ArcIt, \c InArcIt, ///\c OutArcIt, \c BoolNodeMap, \c IntNodeMap, \c DoubleNodeMap, ///\c BoolArcMap, \c IntArcMap, \c DoubleArcMap. /// ///\note If the graph type is a dependent type, ie. the graph type depend ///on a template parameter, then use \c TEMPLATE_DIGRAPH_TYPEDEFS() ///macro. #define DIGRAPH_TYPEDEFS(Digraph) \ typedef Digraph::Node Node; \ typedef Digraph::Arc Arc; \ ///Create convenience typedefs for the digraph types and iterators ///\see DIGRAPH_TYPEDEFS /// ///\note Use this macro, if the graph type is a dependent type, ///ie. the graph type depend on a template parameter. #define TEMPLATE_DIGRAPH_TYPEDEFS(Digraph) \ typedef typename Digraph::Node Node; \ typedef typename Digraph::Arc Arc; \ } //namespace lemon #endif python-pot-0.9.7+dfsg/ot/lp/cvx.py000066400000000000000000000007141525521106200167770ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ (DEPRECATED) LP solvers for optimal transport using cvxopt """ # Author: Remi Flamary # # License: MIT License import warnings from ._barycenter_solvers import barycenter __all__ = ["barycenter"] warnings.warn( "The module ot.lp.cvx is deprecated and will be removed in future versions." "The function `barycenter` was moved to ot.lp._barycenter_solvers and can" "be importer via ot.lp." ) python-pot-0.9.7+dfsg/ot/lp/dmmot.py000066400000000000000000000270241525521106200173220ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ d-MMOT solvers for optimal transport """ # Author: Ronak Mehta # Xizheng Yu # # License: MIT License import numpy as np from ..backend import get_backend def dist_monge_max_min(i): r""" A tensor :math:c is Monge if for all valid :math:i_1, \ldots i_d and :math:j_1, \ldots, j_d, .. math:: c(s_1, \ldots, s_d) + c(t_1, \ldots t_d) \leq c(i_1, \ldots i_d) + c(j_1, \ldots, j_d) where :math:s_k = \min(i_k, j_k) and :math:t_k = \max(i_k, j_k). Our focus is on a specific cost, which is known to be Monge: .. math:: c(i_1,i_2,\ldots,i_d) = \max{i_k:k\in[d]} - \min{i_k:k\in[d]}. When :math:d=2, this cost reduces to :math:c(i_1,i_2)=|i_1-i_2|, which agrees with the classical EMD cost. This choice of :math:c is called the generalized EMD cost. Parameters ---------- i : list The list of integer indexes. Returns ------- cost : numeric value The ground cost (generalized EMD cost) of the tensor. References ---------- .. [56] Jeffery Kline. Properties of the d-dimensional earth mover's problem. Discrete Applied Mathematics, 265: 128-141, 2019. .. [57] Wolfgang W. Bein, Peter Brucker, James K. Park, and Pramod K. Pathak. A monge property for the d-dimensional transportation problem. Discrete Applied Mathematics, 58(2):97-109, 1995. ISSN 0166-218X. doi: https://doi.org/10.1016/0166-218X(93)E0121-E. URL https://www.sciencedirect.com/ science/article/pii/0166218X93E0121E. Workshop on Discrete Algorithms. """ return max(i) - min(i) def dmmot_monge_1dgrid_loss(A, verbose=False, log=False): r""" Compute the discrete multi-marginal optimal transport of distributions A. This function operates on distributions whose supports are real numbers on the real line. The algorithm solves both primal and dual d-MMOT programs concurrently to produce the optimal transport plan as well as the total (minimal) cost. The cost is a ground cost, and the solution is independent of which Monge cost is desired. The algorithm accepts :math:`d` distributions (i.e., histograms) :math:`a_{1}, \ldots, a_{d} \in \mathbb{R}_{+}^{n}` with :math:`e^{\prime} a_{j}=1` for all :math:`j \in[d]`. Although the algorithm states that all histograms have the same number of bins, the algorithm can be easily adapted to accept as inputs :math:`a_{i} \in \mathbb{R}_{+}^{n_{i}}` with :math:`n_{i} \neq n_{j}` [50]. The function solves the following optimization problem[51]: .. math:: \begin{align}\begin{aligned} \underset{\gamma\in\mathbb{R}^{n^{d}}_{+}} {\textrm{min}} \sum_{i_1,\ldots,i_d} c(i_1,\ldots, i_d)\, \gamma(i_1,\ldots,i_d) \quad \textrm{s.t.} \sum_{i_2,\ldots,i_d} \gamma(i_1,\ldots,i_d) &= a_1(i_i), (\forall i_1\in[n])\\ \qquad\vdots\\ \sum_{i_1,\ldots,i_{d-1}} \gamma(i_1,\ldots,i_d) &= a_{d}(i_{d}), (\forall i_d\in[n]). \end{aligned} \end{align} Parameters ---------- A : nx.ndarray, shape (dim, n_hists) The input ndarray containing distributions of n bins in d dimensions. verbose : bool, optional If True, print debugging information during execution. Default=False. log : bool, optional If True, record log. Default is False. Returns ------- obj : float the value of the primal objective function evaluated at the solution. log : dict A dictionary containing the log of the discrete mmot problem: - 'A': a dictionary that maps tuples of indices to the corresponding primal variables. The tuples are the indices of the entries that are set to their minimum value during the algorithm. - 'primal objective': a float, the value of the objective function evaluated at the solution. - 'dual': a list of arrays, the dual variables corresponding to the input arrays. The i-th element of the list is the dual variable corresponding to the i-th dimension of the input arrays. - 'dual objective': a float, the value of the dual objective function evaluated at the solution. References ---------- .. [55] Ronak Mehta, Jeffery Kline, Vishnu Suresh Lokhande, Glenn Fung, & Vikas Singh (2023). Efficient Discrete Multi Marginal Optimal Transport Regularization. In The Eleventh International Conference on Learning Representations. .. [56] Jeffery Kline. Properties of the d-dimensional earth mover's problem. Discrete Applied Mathematics, 265: 128-141, 2019. .. [58] Leonid V Kantorovich. On the translocation of masses. Dokl. Akad. Nauk SSSR, 37:227-229, 1942. See Also -------- ot.lp.dmmot_monge_1dgrid_optimize : Optimize the d-Dimensional Earth Mover's Distance (d-MMOT) """ nx = get_backend(A) A_copy = A A = nx.to_numpy(A) AA = [np.copy(A[:, j]) for j in range(A.shape[1])] dims = tuple([len(_) for _ in AA]) xx = {} dual = [np.zeros(d) for d in dims] idx = [ 0, ] * len(AA) obj = 0 if verbose: print("i minval oldidx\t\tobj\t\tvals") while all([i < _ for _, i in zip(dims, idx)]): vals = [v[i] for v, i in zip(AA, idx)] minval = min(vals) i = vals.index(minval) xx[tuple(idx)] = minval obj += (dist_monge_max_min(idx)) * minval for v, j in zip(AA, idx): v[j] -= minval # oldidx = nx.copy(idx) oldidx = idx.copy() idx[i] += 1 if idx[i] < dims[i]: temp = ( dist_monge_max_min(idx) - dist_monge_max_min(oldidx) + dual[i][idx[i] - 1] ) dual[i][idx[i]] += temp if verbose: print(i, minval, oldidx, obj, "\t", vals) # the above terminates when any entry in idx equals the corresponding # value in dims this leaves other dimensions incomplete; the remaining # terms of the dual solution must be filled-in for _, i in enumerate(idx): try: dual[_][i:] = dual[_][i] except Exception: pass dualobj = sum([np.dot(A[:, i], arr) for i, arr in enumerate(dual)]) obj = nx.from_numpy(obj) log_dict = { "A": xx, "primal objective": obj, "dual": dual, "dual objective": dualobj, } # define forward/backward relations for pytorch obj = nx.set_gradients(obj, (A_copy), (dual)) if log: return obj, log_dict else: return obj def dmmot_monge_1dgrid_optimize( A, niters=100, lr_init=1e-5, lr_decay=0.995, print_rate=100, verbose=False, log=False, ): r"""Minimize the d-dimensional EMD using gradient descent. Discrete Multi-Marginal Optimal Transport (d-MMOT): Let :math:`a_1, \ldots, a_d\in\mathbb{R}^n_{+}` be discrete probability distributions. Here, the d-MMOT is the LP, .. math:: \begin{align}\begin{aligned} \underset{x\in\mathbb{R}^{n^{d}}_{+}} {\textrm{min}} \sum_{i_1,\ldots,i_d} c(i_1,\ldots, i_d)\, x(i_1,\ldots,i_d) \quad \textrm{s.t.} \sum_{i_2,\ldots,i_d} x(i_1,\ldots,i_d) &= a_1(i_i), (\forall i_1\in[n])\\ \qquad\vdots\\ \sum_{i_1,\ldots,i_{d-1}} x(i_1,\ldots,i_d) &= a_{d}(i_{d}), (\forall i_d\in[n]). \end{aligned} \end{align} The dual linear program of the d-MMOT problem is: .. math:: \underset{z_j\in\mathbb{R}^n, j\in[d]}{\textrm{maximize}}\qquad\sum_{j} a_j'z_j\qquad \textrm{subject to}\qquad z_{1}(i_1)+\cdots+z_{d}(i_{d}) \leq c(i_1,\ldots,i_{d}), where the indices in the constraints include all :math:`i_j\in[n]`, :math: `j\in[d]`. Denote by :math:`\phi(a_1,\ldots,a_d)`, the optimal objective value of the LP in d-MMOT problem. Let :math:`z^*` be an optimal solution to the dual program. Then, .. math:: \begin{align} \nabla \phi(a_1,\ldots,a_{d}) &= z^*, ~~\text{and for any $t\in \mathbb{R}$,}~~ \phi(a_1,a_2,\ldots,a_{d}) = \sum_{j}a_j' (z_j^* + t\, \eta), \nonumber \\ \text{where } \eta &:= (z_1^{*}(n)\,e, z^*_1(n)\,e, \cdots, z^*_{d}(n)\,e) \end{align} Using these dual variables naturally provided by the algorithm in ot.lp.dmmot_monge_1dgrid_loss, gradient steps move each input distribution to minimize their d-mmot distance. Parameters ---------- A : nx.ndarray, shape (dim, n_hists) The input ndarray containing distributions of n bins in d dimensions. niters : int, optional (default=100) The maximum number of iterations for the optimization algorithm. lr_init : float, optional (default=1e-5) The initial learning rate (step size) for the optimization algorithm. lr_decay : float, optional (default=0.995) The learning rate decay rate in each iteration. print_rate : int, optional (default=100) The rate at which to print the objective value and gradient norm during the optimization algorithm. verbose : bool, optional If True, print debugging information during execution. Default=False. log : bool, optional If True, record log. Default is False. Returns ------- a : list of ndarrays, each of shape (n,) The optimal solution as a list of n approximate barycenters, each of length vecsize. log : dict log dictionary return only if log==True in parameters References ---------- .. [55] Ronak Mehta, Jeffery Kline, Vishnu Suresh Lokhande, Glenn Fung, & Vikas Singh (2023). Efficient Discrete Multi Marginal Optimal Transport Regularization. In The Eleventh International Conference on Learning Representations. .. [60] Olvi L Mangasarian and RR Meyer. Nonlinear perturbation of linear programs. SIAM Journal on Control and Optimization, 17(6):745-752, 1979 .. [59] Michael C Ferris and Olvi L Mangasarian. Finite perturbation of convex programs. Applied Mathematics and Optimization, 23(1):263-273, 1991. See Also -------- ot.lp.dmmot_monge_1dgrid_loss: d-Dimensional Earth Mover's Solver """ # function body here nx = get_backend(A) A = nx.to_numpy(A) n, d = A.shape # n is dim, d is n_hists def dualIter(A, lr): funcval, log_dict = dmmot_monge_1dgrid_loss(A, verbose=verbose, log=True) grad = np.column_stack(log_dict["dual"]) A_new = np.reshape(A, (n, d)) - grad * lr return funcval, A_new, grad, log_dict def renormalize(A): A = np.reshape(A, (n, d)) for i in range(A.shape[1]): if min(A[:, i]) < 0: A[:, i] -= min(A[:, i]) A[:, i] /= np.sum(A[:, i]) return A def listify(A): return [A[:, i] for i in range(A.shape[1])] lr = lr_init funcval, _, grad, log_dict = dualIter(A, lr) gn = np.linalg.norm(grad) print(f"Initial:\t\tObj:\t{funcval:.4f}\tGradNorm:\t{gn:.4f}") for i in range(niters): A = renormalize(A) funcval, A, grad, log_dict = dualIter(A, lr) gn = np.linalg.norm(grad) if i % print_rate == 0: print(f"Iter {i:2.0f}:\tObj:\t{funcval:.4f}\tGradNorm:\t{gn:.4f}") lr *= lr_decay A = renormalize(A) a = listify(A) if log: return a, log_dict else: return a python-pot-0.9.7+dfsg/ot/lp/emd_wrap.pyx000066400000000000000000000342071525521106200201710ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Cython linker with C solver """ # Author: Remi Flamary # # License: MIT License import numpy as np cimport numpy as np from ..utils import dist cimport cython cimport libc.math as math from libc.stdint cimport uint64_t, int64_t import warnings cdef extern from "EMD.h": int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double* alpha, double* beta, double *cost, uint64_t maxIter, double* alpha_init, double* beta_init) nogil int EMD_wrap_sparse(int n1, int n2, double *X, double *Y, uint64_t n_edges, uint64_t *edge_sources, uint64_t *edge_targets, double *edge_costs, uint64_t *flow_sources_out, uint64_t *flow_targets_out, double *flow_values_out, uint64_t *n_flows_out, uint64_t max_flows_out, double *alpha, double *beta, double *cost, uint64_t maxIter, double* alpha_init, double* beta_init) nogil int EMD_wrap_lazy(int n1, int n2, double *X, double *Y, double *coords_a, double *coords_b, int dim, int metric, uint64_t *flow_sources_out, uint64_t *flow_targets_out, double *flow_values_out, uint64_t *n_flows_out, uint64_t max_flows_out, double* alpha, double* beta, double *cost, uint64_t maxIter, double* alpha_init, double* beta_init) nogil cdef enum ProblemType: INFEASIBLE, OPTIMAL, UNBOUNDED, MAX_ITER_REACHED def check_result(result_code): if result_code == OPTIMAL: return None if result_code == INFEASIBLE: message = "Problem infeasible. Check that a and b are in the simplex" elif result_code == UNBOUNDED: message = "Problem unbounded" elif result_code == MAX_ITER_REACHED: message = "numItermax reached before optimality. Try to increase numItermax." warnings.warn(message) return message @cython.boundscheck(False) @cython.wraparound(False) def emd_c(np.ndarray[double, ndim=1, mode="c"] a, np.ndarray[double, ndim=1, mode="c"] b, np.ndarray[double, ndim=2, mode="c"] M, uint64_t max_iter, int numThreads, alpha_init=None, beta_init=None): """ Solves the Earth Movers distance problem and returns the optimal transport matrix gamm=emd(a,b,M) .. math:: \gamma = arg\min_\gamma <\gamma,M>_F s.t. \gamma 1 = a \gamma^T 1= b \gamma\geq 0 where : - M is the metric cost matrix - a and b are the sample weights .. warning:: Note that the M matrix needs to be a C-order :py.cls:`numpy.array` .. warning:: The C++ solver discards all samples in the distributions with zeros weights. This means that while the primal variable (transport matrix) is exact, the solver only returns feasible dual potentials on the samples with weights different from zero. Parameters ---------- a : (ns,) numpy.ndarray, float64 source histogram b : (nt,) numpy.ndarray, float64 target histogram M : (ns,nt) numpy.ndarray, float64 loss matrix max_iter : uint64_t The maximum number of iterations before stopping the optimization algorithm if it has not converged. alpha_init : (ns,) numpy.ndarray, float64, optional Initial dual potentials for sources (warmstart) beta_init : (nt,) numpy.ndarray, float64, optional Initial dual potentials for targets (warmstart) Returns ------- gamma: (ns x nt) numpy.ndarray Optimal transportation matrix for the given parameters """ cdef int n1= M.shape[0] cdef int n2= M.shape[1] cdef int nmax=n1+n2-1 cdef int result_code = 0 cdef int nG=0 cdef double cost=0 cdef np.ndarray[double, ndim=1, mode="c"] alpha=np.zeros(n1) cdef np.ndarray[double, ndim=1, mode="c"] beta=np.zeros(n2) cdef np.ndarray[double, ndim=2, mode="c"] G=np.zeros([0, 0]) cdef np.ndarray[double, ndim=1, mode="c"] Gv=np.zeros(0) # Warmstart potentials cdef np.ndarray[double, ndim=1, mode="c"] alpha_init_c cdef np.ndarray[double, ndim=1, mode="c"] beta_init_c cdef double* alpha_init_ptr = NULL cdef double* beta_init_ptr = NULL if not len(a): a=np.ones((n1,))/n1 if not len(b): b=np.ones((n2,))/n2 # init OT matrix G=np.zeros([n1, n2]) # Setup warmstart pointers if provided if alpha_init is not None and beta_init is not None: alpha_init_c = np.ascontiguousarray(alpha_init, dtype=np.float64) beta_init_c = np.ascontiguousarray(beta_init, dtype=np.float64) alpha_init_ptr = alpha_init_c.data beta_init_ptr = beta_init_c.data # calling the function with nogil: result_code = EMD_wrap(n1, n2, a.data, b.data, M.data, G.data, alpha.data, beta.data, &cost, max_iter, alpha_init_ptr, beta_init_ptr) return G, cost, alpha, beta, result_code @cython.boundscheck(False) @cython.wraparound(False) def emd_1d_sorted(np.ndarray[double, ndim=1, mode="c"] u_weights, np.ndarray[double, ndim=1, mode="c"] v_weights, np.ndarray[double, ndim=1, mode="c"] u, np.ndarray[double, ndim=1, mode="c"] v, str metric='sqeuclidean', double p=1.): r""" Solves the Earth Movers distance problem between sorted 1d measures and returns the OT matrix and the associated cost Parameters ---------- u_weights : (ns,) ndarray, float64 Source histogram v_weights : (nt,) ndarray, float64 Target histogram u : (ns,) ndarray, float64 Source dirac locations (on the real line) v : (nt,) ndarray, float64 Target dirac locations (on the real line) metric: str, optional (default='sqeuclidean') Metric to be used. Only works with either of the strings `'sqeuclidean'`, `'minkowski'`, `'cityblock'`, or `'euclidean'`. p: float, optional (default=1.0) The p-norm to apply for if metric='minkowski' Returns ------- gamma: (n, ) ndarray, float64 Values in the Optimal transportation matrix indices: (n, 2) ndarray, int64 Indices of the values stored in gamma for the Optimal transportation matrix cost cost associated to the optimal transportation """ cdef double cost = 0. cdef Py_ssize_t n = u_weights.shape[0] cdef Py_ssize_t m = v_weights.shape[0] cdef Py_ssize_t i = 0 cdef double w_i = u_weights[0] cdef Py_ssize_t j = 0 cdef double w_j = v_weights[0] cdef double m_ij = 0. cdef np.ndarray[double, ndim=1, mode="c"] G = np.zeros((n + m - 1, ), dtype=np.float64) cdef np.ndarray[long long, ndim=2, mode="c"] indices = np.zeros((n + m - 1, 2), dtype=np.int64) cdef Py_ssize_t cur_idx = 0 while True: if metric == 'sqeuclidean': m_ij = (u[i] - v[j]) * (u[i] - v[j]) elif metric == 'cityblock' or metric == 'euclidean': m_ij = math.fabs(u[i] - v[j]) elif metric == 'minkowski': m_ij = math.pow(math.fabs(u[i] - v[j]), p) else: raise ValueError("Solver for EMD in 1d only supports metrics " + "from the following list: " + "`['sqeuclidean', 'minkowski', 'cityblock', 'euclidean']`") if w_i < w_j or j == m - 1: cost += m_ij * w_i G[cur_idx] = w_i indices[cur_idx, 0] = i indices[cur_idx, 1] = j i += 1 if i == n: break w_j -= w_i w_i = u_weights[i] else: cost += m_ij * w_j G[cur_idx] = w_j indices[cur_idx, 0] = i indices[cur_idx, 1] = j j += 1 if j == m: break w_i -= w_j w_j = v_weights[j] cur_idx += 1 cur_idx += 1 return G[:cur_idx], indices[:cur_idx], cost @cython.boundscheck(False) @cython.wraparound(False) def emd_c_sparse(np.ndarray[double, ndim=1, mode="c"] a, np.ndarray[double, ndim=1, mode="c"] b, np.ndarray[uint64_t, ndim=1, mode="c"] edge_sources, np.ndarray[uint64_t, ndim=1, mode="c"] edge_targets, np.ndarray[double, ndim=1, mode="c"] edge_costs, uint64_t max_iter, np.ndarray[double, ndim=1, mode="c"] alpha_init=None, np.ndarray[double, ndim=1, mode="c"] beta_init=None): """ Sparse EMD solver using cost matrix in COO (Coordinate) sparse format. The cost matrix is passed as three parallel arrays representing non-zero entries in COO format: (edge_sources[i], edge_targets[i]) -> edge_costs[i]. Only edges explicitly provided will be considered by the solver. Parameters ---------- a : (n1,) array, float64 Source histogram b : (n2,) array, float64 Target histogram edge_sources : (k,) array, uint64 Source indices for each edge (row indices in COO format) edge_targets : (k,) array, uint64 Target indices for each edge (column indices in COO format) edge_costs : (k,) array, float64 Cost for each edge (non-zero values in COO format) max_iter : uint64_t Maximum number of iterations alpha_init : (n1,) array, float64, optional Initial dual variables for sources (warmstart) beta_init : (n2,) array, float64, optional Initial dual variables for targets (warmstart) Returns ------- flow_sources : (n_flows,) array, uint64 Source indices of non-zero flows flow_targets : (n_flows,) array, uint64 Target indices of non-zero flows flow_values : (n_flows,) array, float64 Flow values cost : float Total cost alpha : (n1,) array Dual variables for sources beta : (n2,) array Dual variables for targets result_code : int Result status """ cdef int n1 = a.shape[0] cdef int n2 = b.shape[0] cdef uint64_t n_edges = edge_sources.shape[0] cdef uint64_t n_flows_out = 0 cdef int result_code = 0 cdef double cost = 0 # Allocate output arrays (max size = n_edges) cdef np.ndarray[uint64_t, ndim=1, mode="c"] flow_sources = np.zeros(n_edges, dtype=np.uint64) cdef np.ndarray[uint64_t, ndim=1, mode="c"] flow_targets = np.zeros(n_edges, dtype=np.uint64) cdef np.ndarray[double, ndim=1, mode="c"] flow_values = np.zeros(n_edges, dtype=np.float64) cdef np.ndarray[double, ndim=1, mode="c"] alpha = np.zeros(n1) cdef np.ndarray[double, ndim=1, mode="c"] beta = np.zeros(n2) cdef double* alpha_init_ptr = NULL cdef double* beta_init_ptr = NULL if alpha_init is not None and beta_init is not None: alpha_init_ptr = alpha_init.data beta_init_ptr = beta_init.data with nogil: result_code = EMD_wrap_sparse( n1, n2, a.data, b.data, n_edges, edge_sources.data, edge_targets.data, edge_costs.data, flow_sources.data, flow_targets.data, flow_values.data, &n_flows_out, n_edges, alpha.data, beta.data, &cost, max_iter, alpha_init_ptr, beta_init_ptr ) # Trim to actual number of flows flow_sources = flow_sources[:n_flows_out] flow_targets = flow_targets[:n_flows_out] flow_values = flow_values[:n_flows_out] return flow_sources, flow_targets, flow_values, cost, alpha, beta, result_code @cython.boundscheck(False) @cython.wraparound(False) def emd_c_lazy(np.ndarray[double, ndim=1, mode="c"] a, np.ndarray[double, ndim=1, mode="c"] b, np.ndarray[double, ndim=2, mode="c"] coords_a, np.ndarray[double, ndim=2, mode="c"] coords_b, str metric='sqeuclidean', uint64_t max_iter=100000, np.ndarray[double, ndim=1, mode="c"] alpha_init=None, np.ndarray[double, ndim=1, mode="c"] beta_init=None): """Solves the Earth Movers distance problem with lazy cost computation from coordinates.""" cdef int n1 = coords_a.shape[0] cdef int n2 = coords_b.shape[0] cdef int dim = coords_a.shape[1] cdef int result_code = 0 cdef double cost = 0 cdef int metric_code cdef uint64_t n_flows_out = 0 cdef uint64_t max_flows_out = n1 + n2 # Validate dimension consistency if coords_b.shape[1] != dim: raise ValueError(f"Coordinate dimension mismatch: coords_a has {dim} dimensions but coords_b has {coords_b.shape[1]}") metric_map = { 'sqeuclidean': 0, 'euclidean': 1, 'cityblock': 2 } try: metric_code = metric_map[metric] except KeyError: raise ValueError(f"Unknown metric: '{metric}'. Supported metrics are: {list(metric_map.keys())}") cdef np.ndarray[uint64_t, ndim=1, mode="c"] flow_sources = np.zeros(max_flows_out, dtype=np.uint64) cdef np.ndarray[uint64_t, ndim=1, mode="c"] flow_targets = np.zeros(max_flows_out, dtype=np.uint64) cdef np.ndarray[double, ndim=1, mode="c"] flow_values = np.zeros(max_flows_out, dtype=np.float64) cdef np.ndarray[double, ndim=1, mode="c"] alpha = np.zeros(n1) cdef np.ndarray[double, ndim=1, mode="c"] beta = np.zeros(n2) if not len(a): a = np.ones((n1,)) / n1 if not len(b): b = np.ones((n2,)) / n2 cdef double* alpha_init_ptr = NULL cdef double* beta_init_ptr = NULL if alpha_init is not None and beta_init is not None: alpha_init_ptr = alpha_init.data beta_init_ptr = beta_init.data with nogil: result_code = EMD_wrap_lazy(n1, n2, a.data, b.data, coords_a.data, coords_b.data, dim, metric_code, flow_sources.data, flow_targets.data, flow_values.data, &n_flows_out, max_flows_out, alpha.data, beta.data, &cost, max_iter, alpha_init_ptr, beta_init_ptr) flow_sources = flow_sources[:n_flows_out] flow_targets = flow_targets[:n_flows_out] flow_values = flow_values[:n_flows_out] return flow_sources, flow_targets, flow_values, cost, alpha, beta, result_code python-pot-0.9.7+dfsg/ot/lp/full_bipartitegraph.h000066400000000000000000000135471525521106200220350ustar00rootroot00000000000000/* -*- mode: C++; indent-tabs-mode: nil; -*- * * This file has been adapted by Nicolas Bonneel (2013), * from full_graph.h from LEMON, a generic C++ optimization library, * to implement a lightweight fully connected bipartite graph. A previous * version of this file is used as part of the Displacement Interpolation * project, * Web: http://www.cs.ubc.ca/labs/imager/tr/2011/DisplacementInterpolation/ * * **** Original file Copyright Notice : * Copyright (C) 2003-2010 * Egervary Jeno Kombinatorikus Optimalizalasi Kutatocsoport * (Egervary Research Group on Combinatorial Optimization, EGRES). * * Permission to use, modify and distribute this software is granted * provided that this copyright notice appears in all copies. For * precise terms see the accompanying LICENSE file. * * This software is provided "AS IS" with no warranty of any kind, * express or implied, and with no claim as to its suitability for any * purpose. * */ #pragma once #include "core.h" #include ///\ingroup graphs ///\file ///\brief FullBipartiteDigraph and FullBipartiteGraph classes. namespace lemon { class FullBipartiteDigraphBase { public: typedef FullBipartiteDigraphBase Digraph; //class Node; typedef int Node; //class Arc; typedef int64_t Arc; protected: int _node_num; int64_t _arc_num; FullBipartiteDigraphBase() {} void construct(int n1, int n2) { _node_num = n1+n2; _arc_num = (int64_t)n1 * (int64_t)n2; _n1=n1; _n2=n2;} public: int _n1, _n2; Node operator()(int ix) const { return Node(ix); } static int index(const Node& node) { return node; } Arc arc(const Node& s, const Node& t) const { if (s<_n1 && t>=_n1) return Arc((int64_t)s * (int64_t)_n2 + (int64_t)(t-_n1) ); else return Arc(-1); } int nodeNum() const { return _node_num; } int64_t arcNum() const { return _arc_num; } int maxNodeId() const { return _node_num - 1; } int64_t maxArcId() const { return _arc_num - 1; } Node source(Arc arc) const { return arc / _n2; } Node target(Arc arc) const { return (arc % _n2) + _n1; } static int id(Node node) { return node; } static int64_t id(Arc arc) { return arc; } static Node nodeFromId(int id) { return Node(id);} static Arc arcFromId(int64_t id) { return Arc(id);} Arc findArc(Node s, Node t, Arc prev = -1) const { return prev == -1 ? arc(s, t) : -1; } void first(Node& node) const { node = _node_num - 1; } static void next(Node& node) { --node; } void first(Arc& arc) const { arc = _arc_num - 1; } static void next(Arc& arc) { --arc; } void firstOut(Arc& arc, const Node& node) const { if (node>=_n1) arc = -1; else arc = (node + 1) * _n2 - 1; } void nextOut(Arc& arc) const { if (arc % _n2 == 0) arc = 0; --arc; } void firstIn(Arc& arc, const Node& node) const { if (node<_n1) arc = -1; else arc = _arc_num + node - _node_num; } void nextIn(Arc& arc) const { arc -= _n2; if (arc < 0) arc = -1; } }; /// \ingroup graphs /// /// \brief A directed full graph class. /// /// FullBipartiteDigraph is a simple and fast implementation of directed full /// (complete) graphs. It contains an arc from each node to each node /// (including a loop for each node), therefore the number of arcs /// is the square of the number of nodes. /// This class is completely static and it needs constant memory space. /// Thus you can neither add nor delete nodes or arcs, however /// the structure can be resized using resize(). /// /// This type fully conforms to the \ref concepts::Digraph "Digraph concept". /// Most of its member functions and nested classes are documented /// only in the concept class. /// /// This class provides constant time counting for nodes and arcs. /// /// \note FullBipartiteDigraph and FullBipartiteGraph classes are very similar, /// but there are two differences. While this class conforms only /// to the \ref concepts::Digraph "Digraph" concept, FullBipartiteGraph /// conforms to the \ref concepts::Graph "Graph" concept, /// moreover FullBipartiteGraph does not contain a loop for each /// node as this class does. /// /// \sa FullBipartiteGraph class FullBipartiteDigraph : public FullBipartiteDigraphBase { typedef FullBipartiteDigraphBase Parent; public: /// \brief Default constructor. /// /// Default constructor. The number of nodes and arcs will be zero. FullBipartiteDigraph() { construct(0,0); } /// \brief Constructor /// /// Constructor. /// \param n The number of the nodes. FullBipartiteDigraph(int n1, int n2) { construct(n1, n2); } /// \brief Returns the node with the given index. /// /// Returns the node with the given index. Since this structure is /// completely static, the nodes can be indexed with integers from /// the range [0..nodeNum()-1]. /// The index of a node is the same as its ID. /// \sa index() Node operator()(int ix) const { return Parent::operator()(ix); } /// \brief Returns the index of the given node. /// /// Returns the index of the given node. Since this structure is /// completely static, the nodes can be indexed with integers from /// the range [0..nodeNum()-1]. /// The index of a node is the same as its ID. /// \sa operator()() static int index(const Node& node) { return Parent::index(node); } /// \brief Returns the arc connecting the given nodes. /// /// Returns the arc connecting the given nodes. /*Arc arc(Node u, Node v) const { return Parent::arc(u, v); }*/ /// \brief Number of nodes. int nodeNum() const { return Parent::nodeNum(); } /// \brief Number of arcs. int64_t arcNum() const { return Parent::arcNum(); } }; } //namespace lemon python-pot-0.9.7+dfsg/ot/lp/full_bipartitegraph_omp.h000066400000000000000000000156161525521106200227070ustar00rootroot00000000000000/* -*- mode: C++; indent-tabs-mode: nil; -*- * * This file has been adapted by Nicolas Bonneel (2013), * from full_graph.h from LEMON, a generic C++ optimization library, * to implement a lightweight fully connected bipartite graph. A previous * version of this file is used as part of the Displacement Interpolation * project, * Web: http://www.cs.ubc.ca/labs/imager/tr/2011/DisplacementInterpolation/ * * **** Original file Copyright Notice : * Copyright (C) 2003-2010 * Egervary Jeno Kombinatorikus Optimalizalasi Kutatocsoport * (Egervary Research Group on Combinatorial Optimization, EGRES). * * Permission to use, modify and distribute this software is granted * provided that this copyright notice appears in all copies. For * precise terms see the accompanying LICENSE file. * * This software is provided "AS IS" with no warranty of any kind, * express or implied, and with no claim as to its suitability for any * purpose. * */ #pragma once #include ///\ingroup graphs ///\file ///\brief FullBipartiteDigraph and FullBipartiteGraph classes. namespace lemon_omp { ///This \c \#define creates convenient type definitions for the following ///types of \c Digraph: \c Node, \c NodeIt, \c Arc, \c ArcIt, \c InArcIt, ///\c OutArcIt, \c BoolNodeMap, \c IntNodeMap, \c DoubleNodeMap, ///\c BoolArcMap, \c IntArcMap, \c DoubleArcMap. /// ///\note If the graph type is a dependent type, ie. the graph type depend ///on a template parameter, then use \c TEMPLATE_DIGRAPH_TYPEDEFS() ///macro. #define DIGRAPH_TYPEDEFS(Digraph) \ typedef Digraph::Node Node; \ typedef Digraph::Arc Arc; \ ///Create convenience typedefs for the digraph types and iterators ///\see DIGRAPH_TYPEDEFS /// ///\note Use this macro, if the graph type is a dependent type, ///ie. the graph type depend on a template parameter. #define TEMPLATE_DIGRAPH_TYPEDEFS(Digraph) \ typedef typename Digraph::Node Node; \ typedef typename Digraph::Arc Arc; \ class FullBipartiteDigraphBase { public: typedef FullBipartiteDigraphBase Digraph; //class Node; typedef int Node; //class Arc; typedef int64_t Arc; protected: int _node_num; int64_t _arc_num; FullBipartiteDigraphBase() {} void construct(int n1, int n2) { _node_num = n1+n2; _arc_num = (int64_t)n1 * (int64_t)n2; _n1=n1; _n2=n2;} public: int _n1, _n2; Node operator()(int ix) const { return Node(ix); } static int index(const Node& node) { return node; } Arc arc(const Node& s, const Node& t) const { if (s<_n1 && t>=_n1) return Arc((int64_t)s * (int64_t)_n2 + (int64_t)(t-_n1) ); else return Arc(-1); } int nodeNum() const { return _node_num; } int64_t arcNum() const { return _arc_num; } int maxNodeId() const { return _node_num - 1; } int64_t maxArcId() const { return _arc_num - 1; } Node source(Arc arc) const { return arc / _n2; } Node target(Arc arc) const { return (arc % _n2) + _n1; } static int id(Node node) { return node; } static int64_t id(Arc arc) { return arc; } static Node nodeFromId(int id) { return Node(id);} static Arc arcFromId(int64_t id) { return Arc(id);} Arc findArc(Node s, Node t, Arc prev = -1) const { return prev == -1 ? arc(s, t) : -1; } void first(Node& node) const { node = _node_num - 1; } static void next(Node& node) { --node; } void first(Arc& arc) const { arc = _arc_num - 1; } static void next(Arc& arc) { --arc; } void firstOut(Arc& arc, const Node& node) const { if (node>=_n1) arc = -1; else arc = (node + 1) * _n2 - 1; } void nextOut(Arc& arc) const { if (arc % _n2 == 0) arc = 0; --arc; } void firstIn(Arc& arc, const Node& node) const { if (node<_n1) arc = -1; else arc = _arc_num + node - _node_num; } void nextIn(Arc& arc) const { arc -= _n2; if (arc < 0) arc = -1; } }; /// \ingroup graphs /// /// \brief A directed full graph class. /// /// FullBipartiteDigraph is a simple and fast implmenetation of directed full /// (complete) graphs. It contains an arc from each node to each node /// (including a loop for each node), therefore the number of arcs /// is the square of the number of nodes. /// This class is completely static and it needs constant memory space. /// Thus you can neither add nor delete nodes or arcs, however /// the structure can be resized using resize(). /// /// This type fully conforms to the \ref concepts::Digraph "Digraph concept". /// Most of its member functions and nested classes are documented /// only in the concept class. /// /// This class provides constant time counting for nodes and arcs. /// /// \note FullBipartiteDigraph and FullBipartiteGraph classes are very similar, /// but there are two differences. While this class conforms only /// to the \ref concepts::Digraph "Digraph" concept, FullBipartiteGraph /// conforms to the \ref concepts::Graph "Graph" concept, /// moreover FullBipartiteGraph does not contain a loop for each /// node as this class does. /// /// \sa FullBipartiteGraph class FullBipartiteDigraph : public FullBipartiteDigraphBase { typedef FullBipartiteDigraphBase Parent; public: /// \brief Default constructor. /// /// Default constructor. The number of nodes and arcs will be zero. FullBipartiteDigraph() { construct(0,0); } /// \brief Constructor /// /// Constructor. /// \param n The number of the nodes. FullBipartiteDigraph(int n1, int n2) { construct(n1, n2); } /// \brief Returns the node with the given index. /// /// Returns the node with the given index. Since this structure is /// completely static, the nodes can be indexed with integers from /// the range [0..nodeNum()-1]. /// The index of a node is the same as its ID. /// \sa index() Node operator()(int ix) const { return Parent::operator()(ix); } /// \brief Returns the index of the given node. /// /// Returns the index of the given node. Since this structure is /// completely static, the nodes can be indexed with integers from /// the range [0..nodeNum()-1]. /// The index of a node is the same as its ID. /// \sa operator()() static int index(const Node& node) { return Parent::index(node); } /// \brief Returns the arc connecting the given nodes. /// /// Returns the arc connecting the given nodes. /*Arc arc(Node u, Node v) const { return Parent::arc(u, v); }*/ /// \brief Number of nodes. int nodeNum() const { return Parent::nodeNum(); } /// \brief Number of arcs. int64_t arcNum() const { return Parent::arcNum(); } }; } //namespace lemon_omp python-pot-0.9.7+dfsg/ot/lp/network_simplex_simple.h000066400000000000000000002524411525521106200226070ustar00rootroot00000000000000/* -*- mode: C++; indent-tabs-mode: nil; -*- * * * This file has been adapted by Nicolas Bonneel (2013), * from network_simplex.h from LEMON, a generic C++ optimization library, * to implement a lightweight network simplex for mass transport, more * memory efficient that the original file. A previous version of this file * is used as part of the Displacement Interpolation project, * Web: http://www.cs.ubc.ca/labs/imager/tr/2011/DisplacementInterpolation/ * * **** Original file Copyright Notice : * * Copyright (C) 2003-2010 * Egervary Jeno Kombinatorikus Optimalizalasi Kutatocsoport * (Egervary Research Group on Combinatorial Optimization, EGRES). * * Permission to use, modify and distribute this software is granted * provided that this copyright notice appears in all copies. For * precise terms see the accompanying LICENSE file. * * This software is provided "AS IS" with no warranty of any kind, * express or implied, and with no claim as to its suitability for any * purpose. * */ #pragma once #undef DEBUG_LVL #undef EPSILON #undef _EPSILON #define EPSILON 2.2204460492503131e-15 #define _EPSILON 1e-8 /// \ingroup min_cost_flow_algs /// /// \file /// \brief Network Simplex algorithm for finding a minimum cost flow. // if your compiler has troubles with stdext or hashmaps, just comment the following line to use a slower std::map instead //#define HASHMAP #include #include #include #include #include #include #include #include #ifdef HASHMAP #include #else #include #endif #include #include //#include "core.h" //#include "lmath.h" //#include "sparse_array_n.h" #include "full_bipartitegraph.h" #undef INVALIDNODE #undef INVALID #define INVALIDNODE -1 #define INVALID (-1) namespace lemon { template class ProxyObject; template class SparseValueVector { public: SparseValueVector(size_t n=0) { } void resize(size_t n=0){}; T operator[](const size_t id) const { #ifdef HASHMAP typename stdext::hash_map::const_iterator it = data.find(id); #else typename std::map::const_iterator it = data.find(id); #endif if (it==data.end()) return 0; else return it->second; } ProxyObject operator[](const size_t id) { return ProxyObject( this, id ); } //private: #ifdef HASHMAP stdext::hash_map data; #else std::map data; #endif }; template class ProxyObject { public: ProxyObject( SparseValueVector *v, size_t idx ){_v=v; _idx=idx;}; ProxyObject & operator=( const T &v ) { // If we get here, we know that operator[] was called to perform a write access, // so we can insert an item in the vector if needed if (v!=0) _v->data[_idx]=v; return *this; } operator T() { // If we get here, we know that operator[] was called to perform a read access, // so we can simply return the existing object #ifdef HASHMAP typename stdext::hash_map::iterator it = _v->data.find(_idx); #else typename std::map::iterator it = _v->data.find(_idx); #endif if (it==_v->data.end()) return 0; else return it->second; } void operator+=(T val) { if (val==0) return; #ifdef HASHMAP typename stdext::hash_map::iterator it = _v->data.find(_idx); #else typename std::map::iterator it = _v->data.find(_idx); #endif if (it==_v->data.end()) _v->data[_idx] = val; else { T sum = it->second + val; if (sum==0) _v->data.erase(it); else it->second = sum; } } void operator-=(T val) { if (val==0) return; #ifdef HASHMAP typename stdext::hash_map::iterator it = _v->data.find(_idx); #else typename std::map::iterator it = _v->data.find(_idx); #endif if (it==_v->data.end()) _v->data[_idx] = -val; else { T sum = it->second - val; if (sum==0) _v->data.erase(it); else it->second = sum; } } SparseValueVector *_v; size_t _idx; }; /// \addtogroup min_cost_flow_algs /// @{ /// \brief Implementation of the primal Network Simplex algorithm /// for finding a \ref min_cost_flow "minimum cost flow". /// /// \ref NetworkSimplexSimple implements the primal Network Simplex algorithm /// for finding a \ref min_cost_flow "minimum cost flow" /// \ref amo93networkflows, \ref dantzig63linearprog, /// \ref kellyoneill91netsimplex. /// This algorithm is a highly efficient specialized version of the /// linear programming simplex method directly for the minimum cost /// flow problem. /// /// In general, %NetworkSimplexSimple is the fastest implementation available /// in LEMON for this problem. /// Moreover, it supports both directions of the supply/demand inequality /// constraints. For more information, see \ref SupplyType. /// /// Most of the parameters of the problem (except for the digraph) /// can be given using separate functions, and the algorithm can be /// executed using the \ref run() function. If some parameters are not /// specified, then default values will be used. /// /// \tparam GR The digraph type the algorithm runs on. /// \tparam V The number type used for flow amounts, capacity bounds /// and supply values in the algorithm. By default, it is \c int64_t. /// \tparam C The number type used for costs and potentials in the /// algorithm. By default, it is the same as \c V. /// /// \warning Both number types must be signed and all input data must /// be integer. /// /// \note %NetworkSimplexSimple provides five different pivot rule /// implementations, from which the most efficient one is used /// by default. For more information, see \ref PivotRule. template class NetworkSimplexSimple { public: /// \brief Constructor. /// /// The constructor of the class. /// /// \param graph The digraph the algorithm runs on. /// \param arc_mixing Indicate if the arcs have to be stored in a /// mixed order in the internal data structure. /// In special cases, it could lead to better overall performance, /// but it is usually slower. Therefore it is disabled by default. enum class CostMode { StoredArray, DenseMatrix, LazyGeometry }; enum class CostStorageMode { AllArcCosts, ArtificialArcCosts }; enum class FlowStorageMode { Dense, SparseArcFlows }; enum class EndpointStorageMode { Dense, ArcEndpoints }; enum class StateStorageMode { Dense, PackedArcStates }; struct SimplexOptions { bool arc_mixing; CostStorageMode cost_storage_mode; FlowStorageMode flow_storage_mode; EndpointStorageMode endpoint_storage_mode; StateStorageMode state_storage_mode; explicit SimplexOptions(bool arc_mixing_ = false) : arc_mixing(arc_mixing_), cost_storage_mode(CostStorageMode::AllArcCosts), flow_storage_mode(FlowStorageMode::Dense), endpoint_storage_mode(EndpointStorageMode::Dense), state_storage_mode(StateStorageMode::Dense) {} }; NetworkSimplexSimple( const GR& graph, SimplexOptions options, int nbnodes, ArcsType nb_arcs, uint64_t maxiters) : _graph(graph), //_arc_id(graph), _arc_mixing(options.arc_mixing), _init_nb_nodes(nbnodes), _init_nb_arcs(nb_arcs), MAX(std::numeric_limits::max()), INF(std::numeric_limits::has_infinity ? std::numeric_limits::infinity() : MAX), _cost_mode(CostMode::StoredArray), _cost_storage_mode(options.cost_storage_mode), _flow_storage_mode(options.flow_storage_mode), _endpoint_storage_mode(options.endpoint_storage_mode), _state_storage_mode(options.state_storage_mode), _coords_a(nullptr), _coords_b(nullptr), _dim(0), _metric(0), _n1(0), _n2(0), _D_ptr(nullptr), _D_n2(0), _warmstart_provided(false), _warmstart_tree_built(false), _max_cost(0), _has_max_cost(false) { // Reset data structures reset(); max_iter = maxiters; } /// The type of the flow amounts, capacity bounds and supply values typedef V Value; /// The type of the arc costs typedef C Cost; public: /// \brief Problem type constants for the \c run() function. /// /// Enum type containing the problem type constants that can be /// returned by the \ref run() function of the algorithm. enum ProblemType { /// The problem has no feasible solution (flow). INFEASIBLE, /// The problem has optimal solution (i.e. it is feasible and /// bounded), and the algorithm has found optimal flow and node /// potentials (primal and dual solutions). OPTIMAL, /// The objective function of the problem is unbounded, i.e. /// there is a directed cycle having negative total cost and /// infinite upper bound. UNBOUNDED, /// The maximum number of iteration has been reached MAX_ITER_REACHED }; /// \brief Constants for selecting the type of the supply constraints. /// /// Enum type containing constants for selecting the supply type, /// i.e. the direction of the inequalities in the supply/demand /// constraints of the \ref min_cost_flow "minimum cost flow problem". /// /// The default supply type is \c GEQ, the \c LEQ type can be /// selected using \ref supplyType(). /// The equality form is a special case of both supply types. enum SupplyType { /// This option means that there are "greater or equal" /// supply/demand constraints in the definition of the problem. GEQ, /// This option means that there are "less or equal" /// supply/demand constraints in the definition of the problem. LEQ }; private: uint64_t max_iter; TEMPLATE_DIGRAPH_TYPEDEFS(GR); typedef std::vector IntVector; typedef std::vector ArcVector; typedef std::vector ValueVector; typedef std::vector CostVector; // typedef SparseValueVector CostVector; typedef std::vector BoolVector; // Note: vector is used instead of vector for efficiency reasons // State constants for arcs enum ArcState { STATE_UPPER = -1, STATE_TREE = 0, STATE_LOWER = 1 }; class PackedStateVector { public: void resize(ArcsType n) { _size = n; _data.assign((static_cast(n) + 3) / 4, 0); } void clear() { _size = 0; _data.clear(); } void fill(ArcsType count, signed char state) { for (ArcsType i = 0; i < count; ++i) { set(i, state); } } signed char get(ArcsType index) const { const uint8_t bits = (_data[static_cast(index) / 4] >> (2 * (static_cast(index) % 4))) & 0x03; if (bits == 0) return STATE_LOWER; if (bits == 1) return STATE_TREE; return STATE_UPPER; } void set(ArcsType index, signed char state) { const size_t byte_index = static_cast(index) / 4; const size_t shift = 2 * (static_cast(index) % 4); _data[byte_index] = static_cast( (_data[byte_index] & ~(uint8_t(0x03) << shift)) | (encode(state) << shift) ); } private: static uint8_t encode(signed char state) { if (state == STATE_LOWER) return 0; if (state == STATE_TREE) return 1; return 2; } ArcsType _size; std::vector _data; }; typedef std::vector StateVector; // Note: vector is used instead of vector for // efficiency reasons private: // Data related to the underlying digraph const GR &_graph; int _node_num; int _n1; // Number of source nodes (for lazy cost computation) int _n2; // Number of target nodes (for lazy cost computation) ArcsType _arc_num; ArcsType _all_arc_num; ArcsType _search_arc_num; // Parameters of the problem SupplyType _stype; Value _sum_supply; Cost _max_cost; bool _has_max_cost; inline int _node_id(int n) const {return _node_num-n-1;} ; // IntArcMap _arc_id; IntVector _source; // keep nodes as integers IntVector _target; IntVector _artificial_source; IntVector _artificial_target; bool _arc_mixing; public: // Node and arc data CostVector _cost; ValueVector _supply; ValueVector _flow; ValueVector _artificial_flow; CostVector _pi; // Cost access support CostMode _cost_mode; CostStorageMode _cost_storage_mode; FlowStorageMode _flow_storage_mode; EndpointStorageMode _endpoint_storage_mode; StateStorageMode _state_storage_mode; const double* _coords_a; const double* _coords_b; int _dim; int _metric; // 0: sqeuclidean, 1: euclidean, 2: cityblock // Dense cost matrix pointer (lazy access, no copy) const double* _D_ptr; // pointer to row-major cost matrix int _D_n2; // number of columns in D (original n2) private: // Sparse real-flow mode is used by lazy full-bipartite solves: // real arcs use _real_flow, artificial arcs use _artificial_flow. // Dense mode keeps the original all-arc _flow vector. std::map _real_flow; // Warmstart data bool _warmstart_provided; // Flag indicating warmstart is available bool _warmstart_tree_built; // Flag: tree was built by warmstartInit() // Data for storing the spanning tree structure IntVector _parent; ArcVector _pred; IntVector _thread; IntVector _rev_thread; IntVector _succ_num; IntVector _last_succ; IntVector _dirty_revs; BoolVector _forward; StateVector _state; PackedStateVector _packed_state; ArcsType _root; // Temporary data used in the current pivot iteration ArcsType in_arc, join, u_in, v_in, u_out, v_out; ArcsType first, second, right, last; ArcsType stem, par_stem, new_stem; Value delta; const Value MAX; ArcsType mixingCoeff; public: /// \brief Constant for infinite upper bounds (capacities). /// /// Constant for infinite upper bounds (capacities). /// It is \c std::numeric_limits::infinity() if available, /// \c std::numeric_limits::max() otherwise. const Value INF; private: // thank you to DVK and MizardX from StackOverflow for this function! inline ArcsType sequence(ArcsType k) const { ArcsType smallv = (k > num_total_big_subsequence_numbers) & 1; k -= num_total_big_subsequence_numbers * smallv; ArcsType subsequence_length2 = subsequence_length- smallv; ArcsType subsequence_num = (k / subsequence_length2) + num_big_subseqiences * smallv; ArcsType subsequence_offset = (k % subsequence_length2) * mixingCoeff; return subsequence_offset + subsequence_num; } ArcsType subsequence_length; ArcsType num_big_subseqiences; ArcsType num_total_big_subsequence_numbers; inline ArcsType getArcID(const Arc &arc) const { //int n = _arc_num-arc._id-1; ArcsType n = _arc_num-GR::id(arc)-1; //ArcsType a = mixingCoeff*(n%mixingCoeff) + n/mixingCoeff; //ArcsType b = _arc_id[arc]; if (_arc_mixing) return sequence(n); else return n; } inline bool usesStoredCost() const { return _cost_mode == CostMode::StoredArray; } inline bool usesDenseCost() const { return _cost_mode == CostMode::DenseMatrix; } inline bool usesLazyCost() const { return _cost_mode == CostMode::LazyGeometry; } inline bool storesArtificialArcCosts() const { return _cost_storage_mode == CostStorageMode::ArtificialArcCosts; } inline bool storesSparseArcFlows() const { return _flow_storage_mode == FlowStorageMode::SparseArcFlows; } inline bool usesArcEndpoints() const { return _endpoint_storage_mode == EndpointStorageMode::ArcEndpoints; } inline bool usesPackedArcStates() const { return _state_storage_mode == StateStorageMode::PackedArcStates; } Cost computeLazyCostUpperBound() const { Cost squared_range_sum = 0; Cost l1_range_sum = 0; for (int d = 0; d < _dim; ++d) { Cost min_value = _coords_a[d]; Cost max_value = _coords_a[d]; for (int i = 0; i < _n1; ++i) { const Cost value = _coords_a[i * _dim + d]; if (value < min_value) min_value = value; if (value > max_value) max_value = value; } for (int j = 0; j < _n2; ++j) { const Cost value = _coords_b[j * _dim + d]; if (value < min_value) min_value = value; if (value > max_value) max_value = value; } const Cost range = max_value - min_value; squared_range_sum += range * range; l1_range_sum += range; } if (_metric == 0) return squared_range_sum; if (_metric == 1) return std::sqrt(squared_range_sum); return l1_range_sum; } Cost maxRealArcCost() { if (_has_max_cost) { return _max_cost; } Cost max_cost = 0; for (ArcsType i = 0; i != _arc_num; ++i) { Cost cost = getCostForArc(i); if (i == 0 || cost > max_cost) { max_cost = cost; } } _max_cost = max_cost; _has_max_cost = true; return max_cost; } inline int arcSource(ArcsType arc_id) const { if (usesArcEndpoints()) { if (arc_id < _arc_num) { const ArcsType graph_arc = _arc_num - arc_id - 1; return _node_id(static_cast(graph_arc / _n2)); } return _artificial_source[arc_id - _arc_num]; } return _source[arc_id]; } inline int arcTarget(ArcsType arc_id) const { if (usesArcEndpoints()) { if (arc_id < _arc_num) { const ArcsType graph_arc = _arc_num - arc_id - 1; return _node_id(static_cast(graph_arc % _n2) + _n1); } return _artificial_target[arc_id - _arc_num]; } return _target[arc_id]; } inline void setArcEndpoints(ArcsType arc_id, int source, int target) { if (usesArcEndpoints()) { if (arc_id >= _arc_num) { _artificial_source[arc_id - _arc_num] = source; _artificial_target[arc_id - _arc_num] = target; } return; } _source[arc_id] = source; _target[arc_id] = target; } inline void setArcCost(ArcsType arc_id, Cost cost) { if (storesArtificialArcCosts()) { if (arc_id >= _arc_num) { _cost[arc_id - _arc_num] = cost; } } else { _cost[arc_id] = cost; } if (arc_id < _arc_num && (!_has_max_cost || cost > _max_cost)) { _max_cost = cost; _has_max_cost = true; } } inline signed char arcState(ArcsType arc_id) const { if (usesPackedArcStates()) { return _packed_state.get(arc_id); } return _state[arc_id]; } inline void setArcState(ArcsType arc_id, signed char state) { if (usesPackedArcStates()) { _packed_state.set(arc_id, state); return; } _state[arc_id] = state; } inline void flipArcState(ArcsType arc_id) { setArcState(arc_id, -arcState(arc_id)); } inline void fillArcStates(ArcsType count, signed char state) { if (usesPackedArcStates()) { _packed_state.fill(count, state); } else { std::fill_n(_state.begin(), count, state); } } inline ArcsType flowArcCount() const { return storesSparseArcFlows() ? _all_arc_num : static_cast(_flow.size()); } inline Value arcFlow(ArcsType arc_id) const { if (storesSparseArcFlows()) { if (arc_id < _arc_num) { typename std::map::const_iterator it = _real_flow.find(arc_id); return it == _real_flow.end() ? Value(0) : it->second; } return _artificial_flow[arc_id - _arc_num]; } return _flow[arc_id]; } inline void setArcFlow(ArcsType arc_id, Value flow) { if (storesSparseArcFlows()) { if (arc_id < _arc_num) { if (flow == 0) { _real_flow.erase(arc_id); } else { _real_flow[arc_id] = flow; } } else { _artificial_flow[arc_id - _arc_num] = flow; } return; } _flow[arc_id] = flow; } inline void addArcFlow(ArcsType arc_id, Value delta) { if (storesSparseArcFlows()) { if (arc_id < _arc_num) { setArcFlow(arc_id, arcFlow(arc_id) + delta); } else { _artificial_flow[arc_id - _arc_num] += delta; } return; } _flow[arc_id] += delta; } // finally unused because too slow inline ArcsType getSource(const ArcsType arc) const { //ArcsType a = _source[arc]; //return a; ArcsType n = _arc_num-arc-1; if (_arc_mixing) n = mixingCoeff*(n%mixingCoeff) + n/mixingCoeff; ArcsType b; if (n>=0) b = _node_id(_graph.source(GR::arcFromId( n ) )); else { n = arc+1-_arc_num; if ( n<=_node_num) b = _node_num; else if ( n>=_graph._n1) b = _graph._n1; else b = _graph._n1-n; } return b; } // Implementation of the Block Search pivot rule class BlockSearchPivotRule { private: // References to the NetworkSimplexSimple class const CostVector &_pi; ArcsType &_in_arc; ArcsType _search_arc_num; // Pivot rule data ArcsType _block_size; ArcsType _next_arc; NetworkSimplexSimple &_ns; public: // Constructor BlockSearchPivotRule(NetworkSimplexSimple &ns) : _pi(ns._pi), _in_arc(ns.in_arc), _search_arc_num(ns._search_arc_num), _next_arc(0),_ns(ns) { // The main parameters of the pivot rule const double BLOCK_SIZE_FACTOR = 1.0; const ArcsType MIN_BLOCK_SIZE = 10; _block_size = std::max(ArcsType(BLOCK_SIZE_FACTOR * std::sqrt(double(_search_arc_num))), MIN_BLOCK_SIZE); } // Get cost for an arc (either from pre-computed array or compute lazily) inline Cost getCost(ArcsType e) const { return _ns.getCostForArc(e); } // Find next entering arc bool findEnteringArc() { Cost c, min = 0; ArcsType e; ArcsType cnt = _block_size; double a; for (e = _next_arc; e != _search_arc_num; ++e) { c = _ns.arcState(e) * (getCost(e) + _pi[_ns.arcSource(e)] - _pi[_ns.arcTarget(e)]); if (c < min) { min = c; _in_arc = e; } if (--cnt == 0) { a=fabs(_pi[_ns.arcSource(_in_arc)])>fabs(_pi[_ns.arcTarget(_in_arc)]) ? fabs(_pi[_ns.arcSource(_in_arc)]):fabs(_pi[_ns.arcTarget(_in_arc)]); a=a>fabs(getCost(_in_arc))?a:fabs(getCost(_in_arc)); if (min < -EPSILON*a) goto search_end; cnt = _block_size; } } for (e = 0; e != _next_arc; ++e) { c = _ns.arcState(e) * (getCost(e) + _pi[_ns.arcSource(e)] - _pi[_ns.arcTarget(e)]); if (c < min) { min = c; _in_arc = e; } if (--cnt == 0) { a=fabs(_pi[_ns.arcSource(_in_arc)])>fabs(_pi[_ns.arcTarget(_in_arc)]) ? fabs(_pi[_ns.arcSource(_in_arc)]):fabs(_pi[_ns.arcTarget(_in_arc)]); a=a>fabs(getCost(_in_arc))?a:fabs(getCost(_in_arc)); if (min < -EPSILON*a) goto search_end; cnt = _block_size; } } a=fabs(_pi[_ns.arcSource(_in_arc)])>fabs(_pi[_ns.arcTarget(_in_arc)]) ? fabs(_pi[_ns.arcSource(_in_arc)]):fabs(_pi[_ns.arcTarget(_in_arc)]); a=a>fabs(getCost(_in_arc))?a:fabs(getCost(_in_arc)); if (min >= -EPSILON*a) return false; search_end: _next_arc = e; return true; } }; //class BlockSearchPivotRule public: // Public accessors for efficient result extraction ArcsType arcNum() const { return _arc_num; } int nodeNum() const { return _node_num; } int n1() const { return _n1; } int n2() const { return _n2; } Cost pi(int internal_node) const { return _pi[internal_node]; } int _init_nb_nodes; ArcsType _init_nb_arcs; /// \name Parameters /// The parameters of the algorithm can be specified using these /// functions. /// @{ /// \brief Set the costs of the arcs. /// /// This function sets the costs of the arcs. /// If it is not used before calling \ref run(), the costs /// will be set to \c 1 on all arcs. /// /// \param map An arc map storing the costs. /// Its \c Value type must be convertible to the \c Cost type /// of the algorithm. /// /// \return (*this) template NetworkSimplexSimple& costMap(const CostMap& map) { Arc a; _graph.first(a); for (; a != INVALID; _graph.next(a)) { setArcCost(getArcID(a), map[a]); } return *this; } /// \brief Set the costs of one arc. /// /// This function sets the costs of one arcs. /// Done for memory reasons /// /// \param arc An arc. /// \param arc A cost /// /// \return (*this) template NetworkSimplexSimple& setCost(const Arc& arc, const Value cost) { setArcCost(getArcID(arc), cost); return *this; } /// \brief Enable lazy cost computation from coordinates. /// /// This function enables lazy cost computation where distances are /// computed on-the-fly from point coordinates instead of using a /// pre-computed cost matrix. /// /// \param coords_a Pointer to source coordinates (n1 x dim array) /// \param coords_b Pointer to target coordinates (n2 x dim array) /// \param dim Dimension of the coordinates /// \param metric Distance metric: 0=sqeuclidean, 1=euclidean, 2=cityblock /// /// \return (*this) NetworkSimplexSimple& setLazyCost(const double* coords_a, const double* coords_b, int dim, int metric, int n1, int n2) { _cost_mode = CostMode::LazyGeometry; _coords_a = coords_a; _coords_b = coords_b; _dim = dim; _metric = metric; _n1 = n1; _n2 = n2; _max_cost = computeLazyCostUpperBound(); _has_max_cost = true; return *this; } /// \brief Set a dense cost matrix pointer for lazy access. /// /// This function stores a pointer to the cost matrix D (row-major) /// so that costs can be read directly without copying. /// Requires arc_mixing=false and n==n1, m==n2 (no zero-mass filtering). /// /// \param D Pointer to the n1 x n2 cost matrix (row-major) /// \param n2 Number of columns in D /// /// \return (*this) NetworkSimplexSimple& setDenseCostMatrix(const double* D, int n2) { _cost_mode = CostMode::DenseMatrix; _D_ptr = D; _D_n2 = n2; // Precompute max cost once for reuse in init() _has_max_cost = true; _max_cost = D[0]; for (ArcsType i = 1; i != _arc_num; ++i) { if (D[i] > _max_cost) _max_cost = D[i]; } return *this; } /// \brief Compute cost lazily from coordinates. /// /// Computes the distance between source node i and target node j /// based on the specified metric. /// /// \param i Source node index /// \param j Target node index (adjusted by subtracting n1) /// /// \return Cost (distance) between the two points inline Cost computeLazyCost(int i, int j_adjusted) const { const double* xa = _coords_a + i * _dim; const double* xb = _coords_b + j_adjusted * _dim; Cost cost = 0; if (_metric == 0) { // sqeuclidean for (int d = 0; d < _dim; ++d) { Cost diff = xa[d] - xb[d]; cost += diff * diff; } return cost; } else if (_metric == 1) { // euclidean for (int d = 0; d < _dim; ++d) { Cost diff = xa[d] - xb[d]; cost += diff * diff; } return std::sqrt(cost); } else { // cityblock (L1) for (int d = 0; d < _dim; ++d) { cost += std::abs(xa[d] - xb[d]); } return cost; } } /// \brief Get cost for an arc (either from array or compute lazily). /// /// This is the main cost accessor that works from anywhere in the class. /// In lazy mode, real arc costs are computed on-the-fly from coordinates. /// Artificial root arcs still use stored costs because simplex /// initialization assigns them either 0 or ART_COST. /// In normal mode, returns pre-computed cost from array. /// /// \param arc_id The arc ID /// \return Cost of the arc inline Cost getCostForArc(ArcsType arc_id) const { if (usesDenseCost()) { // Dense matrix mode: read directly from D pointer // For artificial arcs (>= _arc_num), read from _cost array if (arc_id >= _arc_num) { return storesArtificialArcCosts() ? _cost[arc_id - _arc_num] : _cost[arc_id]; } // Without arc mixing: internal arc_id maps to graph arc = _arc_num - arc_id - 1 // graph arc g encodes source i = g / m, target j = g % m // cost = D[i * _D_n2 + j] = D[g] (since m == _D_n2) return _D_ptr[_arc_num - arc_id - 1]; } else if (usesStoredCost()) { return storesArtificialArcCosts() ? _cost[arc_id - _arc_num] : _cost[arc_id]; } else { // Artificial root arcs are not coordinate pairs, so even in // lazy mode their costs are stored explicitly. if (arc_id >= _arc_num) { return storesArtificialArcCosts() ? _cost[arc_id - _arc_num] : _cost[arc_id]; } // Real arc: compute from coordinates. // Convert internal node IDs back to graph node IDs, then to coordinate indices int i = _node_num - arcSource(arc_id) - 1; // graph source in [0, _n1-1] int j = _node_num - arcTarget(arc_id) - 1 - _n1; // graph target in [_n1, _node_num-1] -> [0, _n2-1] return computeLazyCost(i, j); } } /// \brief Set the supply values of the nodes. /// /// This function sets the supply values of the nodes. /// If neither this function nor \ref stSupply() is used before /// calling \ref run(), the supply of each node will be set to zero. /// /// \param map A node map storing the supply values. /// Its \c Value type must be convertible to the \c Value type /// of the algorithm. /// /// \return (*this) template NetworkSimplexSimple& supplyMap(const SupplyMap& map) { Node n; _graph.first(n); for (; n != INVALIDNODE; _graph.next(n)) { _supply[_node_id(n)] = map[n]; } return *this; } template NetworkSimplexSimple& supplyMap(const SupplyMap* map1, int n1, const SupplyMap* map2, int n2) { Node n; _graph.first(n); for (; n != INVALIDNODE; _graph.next(n)) { if (n NetworkSimplexSimple& supplyMapAll(SupplyMap val1, int n1, SupplyMap val2, int n2) { Node n; _graph.first(n); for (; n != INVALIDNODE; _graph.next(n)) { if (n(*this) NetworkSimplexSimple& stSupply(const Node& s, const Node& t, Value k) { for (int i = 0; i != _node_num; ++i) { _supply[i] = 0; } _supply[_node_id(s)] = k; _supply[_node_id(t)] = -k; return *this; } /// \brief Set the type of the supply constraints. /// /// This function sets the type of the supply/demand constraints. /// If it is not used before calling \ref run(), the \ref GEQ supply /// type will be used. /// /// For more information, see \ref SupplyType. /// /// \return (*this) NetworkSimplexSimple& supplyType(SupplyType supply_type) { _stype = supply_type; return *this; } /// \brief Set initial dual potentials for warmstart. /// /// This function sets warmstart dual potentials that will be used /// to guide the initial pivots in the network simplex algorithm. /// The potentials should come from a previous solution (e.g., Sinkhorn or EMD). /// /// \param alpha Source node potentials (size n), where alpha[i] = -pi[source_i] /// \param beta Target node potentials (size m), where beta[j] = +pi[target_j] /// \param n Number of source nodes (compressed, non-zero supply) /// \param m Number of target nodes (compressed, non-zero supply) /// void setWarmstartPotentials(const Cost* alpha, const Cost* beta, int n, int m) { // Graph source nodes: 0..n-1, stored at internal index _node_id(i) // Graph target nodes: n..n+m-1, stored at internal index _node_id(n+j) // _node_id(k) = _node_num - k - 1 (reversal mapping) // Note: warmstartInit() will refine these by recomputing from the tree structure. for (int i = 0; i < n; ++i) { _pi[_node_id(i)] = -alpha[i]; // pi[source] = -alpha } for (int j = 0; j < m; ++j) { _pi[_node_id(n + j)] = beta[j]; // pi[target] = +beta } _warmstart_provided = true; } /// @} /// \name Execution Control /// The algorithm can be executed using \ref run(). /// @{ /// \brief Run the algorithm. /// /// This function runs the algorithm. /// The paramters can be specified using functions \ref lowerMap(), /// \ref upperMap(), \ref costMap(), \ref supplyMap(), \ref stSupply(), /// \ref supplyType(). /// For example, /// \code /// NetworkSimplexSimple ns(graph); /// ns.lowerMap(lower).upperMap(upper).costMap(cost) /// .supplyMap(sup).run(); /// \endcode /// /// This function can be called more than once. All the given parameters /// are kept for the next call, unless \ref resetParams() or \ref reset() /// is used, thus only the modified parameters have to be set again. /// If the underlying digraph was also modified after the construction /// of the class (or the last \ref reset() call), then the \ref reset() /// function must be called. /// /// \param pivot_rule The pivot rule that will be used during the /// algorithm. For more information, see \ref PivotRule. /// /// \return \c INFEASIBLE if no feasible flow exists, /// \n \c OPTIMAL if the problem has optimal solution /// (i.e. it is feasible and bounded), and the algorithm has found /// optimal flow and node potentials (primal and dual solutions), /// \n \c UNBOUNDED if the objective function of the problem is /// unbounded, i.e. there is a directed cycle having negative total /// cost and infinite upper bound. /// /// \see ProblemType, PivotRule /// \see resetParams(), reset() ProblemType run() { if (_warmstart_provided) { if (!warmstartInit()) return INFEASIBLE; _warmstart_tree_built = true; } else { if (!init()) return INFEASIBLE; _warmstart_tree_built = false; } return start(); } /// \brief Reset all the parameters that have been given before. /// /// This function resets all the paramaters that have been given /// before using functions \ref lowerMap(), \ref upperMap(), /// \ref costMap(), \ref supplyMap(), \ref stSupply(), \ref supplyType(). /// /// It is useful for multiple \ref run() calls. Basically, all the given /// parameters are kept for the next \ref run() call, unless /// \ref resetParams() or \ref reset() is used. /// If the underlying digraph was also modified after the construction /// of the class or the last \ref reset() call, then the \ref reset() /// function must be used, otherwise \ref resetParams() is sufficient. /// /// For example, /// \code /// NetworkSimplexSimple ns(graph); /// /// // First run /// ns.lowerMap(lower).upperMap(upper).costMap(cost) /// .supplyMap(sup).run(); /// /// // Run again with modified cost map (resetParams() is not called, /// // so only the cost map have to be set again) /// cost[e] += 100; /// ns.costMap(cost).run(); /// /// // Run again from scratch using resetParams() /// // (the lower bounds will be set to zero on all arcs) /// ns.resetParams(); /// ns.upperMap(capacity).costMap(cost) /// .supplyMap(sup).run(); /// \endcode /// /// \return (*this) /// /// \see reset(), run() NetworkSimplexSimple& resetParams() { // Fast fills over contiguous storage std::fill_n(_supply.begin(), _node_num, Value(0)); // In dense/lazy modes, real-arc costs are not read from _cost. // Keep the default fill for the regular explicit-cost mode only. if (usesStoredCost() && !storesArtificialArcCosts()) { std::fill_n(_cost.begin(), _arc_num, Cost(1)); } _stype = GEQ; _has_max_cost = false; _warmstart_provided = false; _warmstart_tree_built = false; // Reset warmstart flag return *this; } int64_t divid (int64_t x, int64_t y) { return (x-x%y)/y; } /// \brief Reset the internal data structures and all the parameters /// that have been given before. /// /// This function resets the internal data structures and all the /// paramaters that have been given before using functions \ref lowerMap(), /// \ref upperMap(), \ref costMap(), \ref supplyMap(), \ref stSupply(), /// \ref supplyType(). /// /// It is useful for multiple \ref run() calls. Basically, all the given /// parameters are kept for the next \ref run() call, unless /// \ref resetParams() or \ref reset() is used. /// If the underlying digraph was also modified after the construction /// of the class or the last \ref reset() call, then the \ref reset() /// function must be used, otherwise \ref resetParams() is sufficient. /// /// See \ref resetParams() for examples. /// /// \return (*this) /// /// \see resetParams(), run() NetworkSimplexSimple& reset() { // Resize vectors _node_num = _init_nb_nodes; _arc_num = _init_nb_arcs; int all_node_num = _node_num + 1; ArcsType max_arc_num = _arc_num + 2 * _node_num; _all_arc_num = max_arc_num; if (usesArcEndpoints()) { _source.clear(); _target.clear(); _artificial_source.resize(2 * _node_num); _artificial_target.resize(2 * _node_num); } else { _source.resize(max_arc_num); _target.resize(max_arc_num); _artificial_source.clear(); _artificial_target.clear(); } if (storesArtificialArcCosts()) { _cost.resize(2 * _node_num); } else { _cost.resize(max_arc_num); } _supply.resize(all_node_num); if (storesSparseArcFlows()) { _flow.clear(); _artificial_flow.assign(2 * _node_num, Value(0)); _real_flow.clear(); } else { _flow.resize(max_arc_num); _artificial_flow.clear(); _real_flow.clear(); } _pi.resize(all_node_num); _parent.resize(all_node_num); _pred.resize(all_node_num); _forward.resize(all_node_num); _thread.resize(all_node_num); _rev_thread.resize(all_node_num); _succ_num.resize(all_node_num); _last_succ.resize(all_node_num); if (usesPackedArcStates()) { _state.clear(); _packed_state.resize(max_arc_num); } else { _state.resize(max_arc_num); _packed_state.clear(); } if (usesArcEndpoints()) { // Real full-bipartite arc endpoints are computed from arc ids. } else if (_arc_mixing) { // Store the arcs in a mixed order const ArcsType k = std::max(ArcsType(std::sqrt(double(_arc_num))), ArcsType(10)); mixingCoeff = k; subsequence_length = _arc_num / mixingCoeff + 1; num_big_subseqiences = _arc_num % mixingCoeff; num_total_big_subsequence_numbers = subsequence_length * num_big_subseqiences; ArcsType i = 0, j = 0; Arc a; _graph.first(a); for (; a != INVALID; _graph.next(a)) { setArcEndpoints( i, _node_id(_graph.source(a)), _node_id(_graph.target(a)) ); //_arc_id[a] = i; if ((i += k) >= _arc_num) i = ++j; } } else { // Store the arcs in the original order without extra permutation work ArcsType i = 0; Arc a; _graph.first(a); for (; a != INVALID; _graph.next(a), ++i) { setArcEndpoints( i, _node_id(_graph.source(a)), _node_id(_graph.target(a)) ); //_arc_id[a] = i; } } // Reset parameters resetParams(); return *this; } /// @} /// \name Query Functions /// The results of the algorithm can be obtained using these /// functions.\n /// The \ref run() function must be called before using them. /// @{ /// \brief Return the total cost of the found flow. /// /// This function returns the total cost of the found flow. /// Its complexity is O(e). /// /// \note The return type of the function can be specified as a /// template parameter. For example, /// \code /// ns.totalCost(); /// \endcode /// It is useful if the total cost cannot be stored in the \c Cost /// type of the algorithm, which is the default return type of the /// function. /// /// \pre \ref run() must be called before using this function. /*template Number totalCost() const { Number c = 0; for (ArcIt a(_graph); a != INVALID; ++a) { int64_t i = getArcID(a); c += Number(_flow[i]) * Number(_cost[i]); } return c; }*/ template Number totalCost() const { Number c = 0; /*#ifdef HASHMAP typename stdext::hash_map::const_iterator it; #else typename std::map::const_iterator it; #endif for (it = _flow.data.begin(); it!=_flow.data.end(); ++it) c += Number(it->second) * Number(_cost[it->first]); return c;*/ for (ArcsType i=0; i(); } #endif /// \brief Return the flow on the given arc. /// /// This function returns the flow on the given arc. /// /// \pre \ref run() must be called before using this function. Value flow(const Arc& a) const { return arcFlow(getArcID(a)); } /// \brief Return the flow map (the primal solution). /// /// This function copies the flow value on each arc into the given /// map. The \c Value type of the algorithm must be convertible to /// the \c Value type of the map. /// /// \pre \ref run() must be called before using this function. template void flowMap(FlowMap &map) const { Arc a; _graph.first(a); for (; a != INVALID; _graph.next(a)) { map.set(a, arcFlow(getArcID(a))); } } /// \brief Return the potential (dual value) of the given node. /// /// This function returns the potential (dual value) of the /// given node. /// /// \pre \ref run() must be called before using this function. Cost potential(const Node& n) const { return _pi[_node_id(n)]; } /// \brief Return the potential map (the dual solution). /// /// This function copies the potential (dual value) of each node /// into the given map. /// The \c Cost type of the algorithm must be convertible to the /// \c Value type of the map. /// /// \pre \ref run() must be called before using this function. template void potentialMap(PotentialMap &map) const { Node n; _graph.first(n); for (; n != INVALID; _graph.next(n)) { map.set(n, _pi[_node_id(n)]); } } /// @} private: // WARMSTART: Build spanning tree from dual potentials bool warmstartInit() { if (_node_num == 0) return false; // Check supply balance _sum_supply = 0; for (int i = 0; i != _node_num; ++i) { _sum_supply += _supply[i]; } if (fabs(_sum_supply) > _EPSILON) return false; _sum_supply = 0; int tree_edges = 0; std::vector tree_arcs; tree_arcs.reserve(_node_num); Cost ART_COST = 0; { ArcsType K = std::min((ArcsType)(4 * _node_num), _arc_num); // Max-heap: (|reduced_cost|, arc_index). We keep the K smallest. typedef std::pair HeapEntry; std::priority_queue maxheap; for (ArcsType e = 0; e < _arc_num; ++e) { setArcState(e, STATE_LOWER); Cost c = getCostForArc(e); if (c > ART_COST) ART_COST = c; Cost rc = fabs(c + _pi[arcSource(e)] - _pi[arcTarget(e)]); if ((ArcsType)maxheap.size() < K) { maxheap.push({rc, e}); } else if (rc < maxheap.top().first) { maxheap.pop(); maxheap.push({rc, e}); } } if (std::numeric_limits::is_exact) { ART_COST = std::numeric_limits::max() / 2 + 1; } else { ART_COST = (ART_COST + 1) * _node_num; } std::vector candidates; candidates.reserve(maxheap.size()); while (!maxheap.empty()) { candidates.push_back(maxheap.top()); maxheap.pop(); } std::sort(candidates.begin(), candidates.end(), [](const HeapEntry& a, const HeapEntry& b) { return a.first < b.first; }); // Kruskal's MST with union-find std::vector uf_parent(_node_num); std::vector uf_rank(_node_num, 0); for (int i = 0; i < _node_num; ++i) uf_parent[i] = i; for (ArcsType idx = 0; idx < (ArcsType)candidates.size() && tree_edges < _node_num - 1; ++idx) { ArcsType e = candidates[idx].second; int s = arcSource(e); int t = arcTarget(e); int rs = s, rt = t; while (uf_parent[rs] != rs) { uf_parent[rs] = uf_parent[uf_parent[rs]]; rs = uf_parent[rs]; } while (uf_parent[rt] != rt) { uf_parent[rt] = uf_parent[uf_parent[rt]]; rt = uf_parent[rt]; } if (rs == rt) continue; if (uf_rank[rs] < uf_rank[rt]) std::swap(rs, rt); uf_parent[rt] = rs; if (uf_rank[rs] == uf_rank[rt]) uf_rank[rs]++; tree_arcs.push_back(e); tree_edges++; } // Fallback: if K best weren't enough to span, scan remaining arcs if (tree_edges < _node_num - 1) { std::vector considered(_arc_num, false); for (auto& c : candidates) considered[c.second] = true; for (ArcsType e = 0; e < _arc_num && tree_edges < _node_num - 1; ++e) { if (considered[e]) continue; int s = arcSource(e); int t = arcTarget(e); int rs = s, rt = t; while (uf_parent[rs] != rs) { uf_parent[rs] = uf_parent[uf_parent[rs]]; rs = uf_parent[rs]; } while (uf_parent[rt] != rt) { uf_parent[rt] = uf_parent[uf_parent[rt]]; rt = uf_parent[rt]; } if (rs == rt) continue; if (uf_rank[rs] < uf_rank[rt]) std::swap(rs, rt); uf_parent[rt] = rs; if (uf_rank[rs] == uf_rank[rt]) uf_rank[rs]++; tree_arcs.push_back(e); tree_edges++; } } } std::vector tree_adj_deg(_node_num, 0); for (int k = 0; k < tree_edges; ++k) { ArcsType e = tree_arcs[k]; tree_adj_deg[arcSource(e)]++; tree_adj_deg[arcTarget(e)]++; } std::vector tree_adj_start(_node_num + 1, 0); for (int i = 0; i < _node_num; ++i) { tree_adj_start[i + 1] = tree_adj_start[i] + tree_adj_deg[i]; } int total_adj = tree_adj_start[_node_num]; std::vector tree_adj_node(total_adj); std::vector tree_adj_arc(total_adj); std::vector tree_adj_pos(_node_num, 0); for (int k = 0; k < tree_edges; ++k) { ArcsType e = tree_arcs[k]; int s = arcSource(e), t = arcTarget(e); int ps = tree_adj_start[s] + tree_adj_pos[s]++; tree_adj_node[ps] = t; tree_adj_arc[ps] = e; int pt = tree_adj_start[t] + tree_adj_pos[t]++; tree_adj_node[pt] = s; tree_adj_arc[pt] = e; } // STEP 2: Set up artificial arcs _search_arc_num = _arc_num; _all_arc_num = _arc_num + _node_num; _root = _node_num; for (ArcsType u = 0, e = _arc_num; u != _node_num; ++u, ++e) { setArcState(e, STATE_TREE); if (_supply[u] >= 0) { setArcEndpoints(e, u, _root); setArcCost(e, 0); setArcFlow(e, _supply[u]); } else { setArcEndpoints(e, _root, u); setArcCost(e, ART_COST); setArcFlow(e, -_supply[u]); } } // Root node setup _parent[_root] = -1; _pred[_root] = -1; _supply[_root] = -_sum_supply; _pi[_root] = 0; // STEP 3: BFS from root to build tree structure std::vector is_rep(_node_num, false); std::vector visited(_node_num, false); for (int u = 0; u < _node_num; ++u) { if (visited[u]) continue; is_rep[u] = true; _parent[u] = _root; _pred[u] = _arc_num + u; _forward[u] = (_supply[u] >= 0); // same as init() setArcState(_arc_num + u, STATE_TREE); visited[u] = true; std::queue bfs_queue; bfs_queue.push(u); while (!bfs_queue.empty()) { int v = bfs_queue.front(); bfs_queue.pop(); for (int k = tree_adj_start[v]; k < tree_adj_start[v + 1]; ++k) { int w = tree_adj_node[k]; ArcsType arc_e = tree_adj_arc[k]; if (visited[w]) continue; visited[w] = true; _parent[w] = v; _pred[w] = arc_e; setArcState(arc_e, STATE_TREE); _forward[w] = (arcSource(arc_e) == w); setArcState(_arc_num + w, STATE_LOWER); setArcFlow(_arc_num + w, 0); bfs_queue.push(w); } } } // STEP 4: Build thread (preorder traversal) { std::vector> children(_node_num + 1); for (int u = 0; u < _node_num; ++u) { children[_parent[u]].push_back(u); } std::vector preorder; preorder.reserve(_node_num + 1); std::stack dfs_stack; dfs_stack.push(_root); while (!dfs_stack.empty()) { int v = dfs_stack.top(); dfs_stack.pop(); preorder.push_back(v); for (int i = (int)children[v].size() - 1; i >= 0; --i) { dfs_stack.push(children[v][i]); } } for (int i = 0; i < (int)preorder.size() - 1; ++i) { _thread[preorder[i]] = preorder[i + 1]; } _thread[preorder.back()] = preorder[0]; for (int u = 0; u <= _node_num; ++u) { _rev_thread[_thread[u]] = u; } for (int u = 0; u <= _node_num; ++u) { _succ_num[u] = 1; } for (int i = (int)preorder.size() - 1; i > 0; --i) { int u = preorder[i]; _succ_num[_parent[u]] += _succ_num[u]; } std::vector pos(_node_num + 1); for (int i = 0; i < (int)preorder.size(); ++i) { pos[preorder[i]] = i; } for (int i = 0; i < (int)preorder.size(); ++i) { int u = preorder[i]; _last_succ[u] = preorder[pos[u] + _succ_num[u] - 1]; } } // STEP 5: Compute flows on tree arcs { std::vector net(_node_num + 1); for (int u = 0; u <= _node_num; ++u) { net[u] = _supply[u]; } std::vector preorder; preorder.reserve(_node_num + 1); int cur = _root; for (int i = 0; i <= _node_num; ++i) { preorder.push_back(cur); cur = _thread[cur]; } int ejected = 0; for (int i = (int)preorder.size() - 1; i > 0; --i) { int u = preorder[i]; ArcsType e = _pred[u]; Value f = _forward[u] ? net[u] : -net[u]; if (f >= 0) { setArcFlow(e, f); net[_parent[u]] += net[u]; } else { if (e < _arc_num) { setArcState(e, STATE_LOWER); setArcFlow(e, 0); } // Reconnect u to root via artificial arc ArcsType art_e = _arc_num + u; _parent[u] = _root; _pred[u] = art_e; _forward[u] = (arcSource(art_e) == u); setArcState(art_e, STATE_TREE); Value art_f = _forward[u] ? net[u] : -net[u]; setArcFlow(art_e, art_f >= 0 ? art_f : -art_f); if (art_f < 0) { _forward[u] = !_forward[u]; setArcFlow(art_e, -art_f); } net[_root] += net[u]; ejected++; } } if (ejected > 0) { std::vector> children2(_node_num + 1); for (int u = 0; u < _node_num; ++u) { children2[_parent[u]].push_back(u); } // DFS preorder std::vector preorder2; preorder2.reserve(_node_num + 1); std::stack dfs2; dfs2.push(_root); while (!dfs2.empty()) { int v = dfs2.top(); dfs2.pop(); preorder2.push_back(v); for (int j = (int)children2[v].size() - 1; j >= 0; --j) { dfs2.push(children2[v][j]); } } for (int i = 0; i < (int)preorder2.size() - 1; ++i) { _thread[preorder2[i]] = preorder2[i + 1]; } _thread[preorder2.back()] = preorder2[0]; for (int u = 0; u <= _node_num; ++u) { _rev_thread[_thread[u]] = u; } for (int u = 0; u <= _node_num; ++u) _succ_num[u] = 1; for (int i = (int)preorder2.size() - 1; i > 0; --i) { _succ_num[_parent[preorder2[i]]] += _succ_num[preorder2[i]]; } std::vector pos2(_node_num + 1); for (int i = 0; i < (int)preorder2.size(); ++i) pos2[preorder2[i]] = i; for (int i = 0; i < (int)preorder2.size(); ++i) { int u = preorder2[i]; _last_succ[u] = preorder2[pos2[u] + _succ_num[u] - 1]; } } } // STEP 6: Compute potentials from the final tree { _pi[_root] = 0; int u = _thread[_root]; while (u != _root) { ArcsType e = _pred[u]; int v = _parent[u]; Cost c = getCostForArc(e); if (_forward[u]) { _pi[u] = _pi[v] - c; } else { _pi[u] = _pi[v] + c; } u = _thread[u]; } } // Initialize in_arc to a valid value in_arc = 0; return true; } // Initialize internal data structures bool init() { if (_node_num == 0) return false; // Check the sum of supply values _sum_supply = 0; for (int i = 0; i != _node_num; ++i) { _sum_supply += _supply[i]; } if ( fabs(_sum_supply) > _EPSILON ) return false; _sum_supply = 0; // Initialize artifical cost Cost ART_COST; if (std::numeric_limits::is_exact) { ART_COST = std::numeric_limits::max() / 2 + 1; } else { Cost max_cost = maxRealArcCost(); ART_COST = (max_cost + 1) * _node_num; } fillArcStates(_arc_num, STATE_LOWER); // Set data for the artificial root node _root = _node_num; _parent[_root] = -1; _pred[_root] = -1; _thread[_root] = 0; _rev_thread[0] = _root; _succ_num[_root] = _node_num + 1; _last_succ[_root] = _root - 1; _supply[_root] = -_sum_supply; _pi[_root] = 0; // Add artificial arcs and initialize the spanning tree data structure if (_sum_supply == 0) { // EQ supply constraints _search_arc_num = _arc_num; _all_arc_num = _arc_num + _node_num; for (ArcsType u = 0, e = _arc_num; u != _node_num; ++u, ++e) { _parent[u] = _root; _pred[u] = e; _thread[u] = u + 1; _rev_thread[u + 1] = u; _succ_num[u] = 1; _last_succ[u] = u; setArcState(e, STATE_TREE); if (_supply[u] >= 0) { _forward[u] = true; _pi[u] = 0; setArcEndpoints(e, u, _root); setArcFlow(e, _supply[u]); setArcCost(e, 0); } else { _forward[u] = false; _pi[u] = ART_COST; setArcEndpoints(e, _root, u); setArcFlow(e, -_supply[u]); setArcCost(e, ART_COST); } } } else if (_sum_supply > 0) { // LEQ supply constraints _search_arc_num = _arc_num + _node_num; ArcsType f = _arc_num + _node_num; for (ArcsType u = 0, e = _arc_num; u != _node_num; ++u, ++e) { _parent[u] = _root; _thread[u] = u + 1; _rev_thread[u + 1] = u; _succ_num[u] = 1; _last_succ[u] = u; if (_supply[u] >= 0) { _forward[u] = true; _pi[u] = 0; _pred[u] = e; setArcEndpoints(e, u, _root); setArcFlow(e, _supply[u]); setArcCost(e, 0); setArcState(e, STATE_TREE); } else { _forward[u] = false; _pi[u] = ART_COST; _pred[u] = f; setArcEndpoints(f, _root, u); setArcFlow(f, -_supply[u]); setArcCost(f, ART_COST); setArcState(f, STATE_TREE); setArcEndpoints(e, u, _root); // Flow is zero by default. setArcCost(e, 0); setArcState(e, STATE_LOWER); ++f; } } _all_arc_num = f; } else { // GEQ supply constraints _search_arc_num = _arc_num + _node_num; ArcsType f = _arc_num + _node_num; for (ArcsType u = 0, e = _arc_num; u != _node_num; ++u, ++e) { _parent[u] = _root; _thread[u] = u + 1; _rev_thread[u + 1] = u; _succ_num[u] = 1; _last_succ[u] = u; if (_supply[u] <= 0) { _forward[u] = false; _pi[u] = 0; _pred[u] = e; setArcEndpoints(e, _root, u); setArcFlow(e, -_supply[u]); setArcCost(e, 0); setArcState(e, STATE_TREE); } else { _forward[u] = true; _pi[u] = -ART_COST; _pred[u] = f; setArcEndpoints(f, u, _root); setArcFlow(f, _supply[u]); setArcState(f, STATE_TREE); setArcCost(f, ART_COST); setArcEndpoints(e, _root, u); // Flow is zero by default. setArcCost(e, 0); setArcState(e, STATE_LOWER); ++f; } } _all_arc_num = f; } return true; } // Find the join node void findJoinNode() { int u = arcSource(in_arc); int v = arcTarget(in_arc); while (u != v) { if (_succ_num[u] < _succ_num[v]) { u = _parent[u]; } else { v = _parent[v]; } } join = u; } // Find the leaving arc of the cycle and returns true if the // leaving arc is not the same as the entering arc bool findLeavingArc() { // Initialize first and second nodes according to the direction // of the cycle if (arcState(in_arc) == STATE_LOWER) { first = arcSource(in_arc); second = arcTarget(in_arc); } else { first = arcTarget(in_arc); second = arcSource(in_arc); } delta = INF; char result = 0; Value d; ArcsType e; // Search the cycle along the path form the first node to the root for (int u = first; u != join; u = _parent[u]) { e = _pred[u]; d = _forward[u] ? arcFlow(e) : INF ; if (d < delta) { delta = d; u_out = u; result = 1; } } // Search the cycle along the path form the second node to the root for (int u = second; u != join; u = _parent[u]) { e = _pred[u]; d = _forward[u] ? INF : arcFlow(e); if (d <= delta) { delta = d; u_out = u; result = 2; } } if (result == 1) { u_in = first; v_in = second; } else { u_in = second; v_in = first; } return result != 0; } // Change flow and state vectors void changeFlow(bool change) { // Augment along the cycle if (delta > 0) { Value val = arcState(in_arc) * delta; addArcFlow(in_arc, val); for (int u = arcSource(in_arc); u != join; u = _parent[u]) { addArcFlow(_pred[u], _forward[u] ? -val : val); } for (int u = arcTarget(in_arc); u != join; u = _parent[u]) { addArcFlow(_pred[u], _forward[u] ? val : -val); } } // Update the state of the entering and leaving arcs if (change) { setArcState(in_arc, STATE_TREE); setArcState( _pred[u_out], (arcFlow(_pred[u_out]) == 0) ? STATE_LOWER : STATE_UPPER ); } else { flipArcState(in_arc); } } // Update the tree structure void updateTreeStructure() { int u, w; int old_rev_thread = _rev_thread[u_out]; int old_succ_num = _succ_num[u_out]; int old_last_succ = _last_succ[u_out]; v_out = _parent[u_out]; u = _last_succ[u_in]; // the last successor of u_in right = _thread[u]; // the node after it // Handle the case when old_rev_thread equals to v_in // (it also means that join and v_out coincide) if (old_rev_thread == v_in) { last = _thread[_last_succ[u_out]]; } else { last = _thread[v_in]; } // Update _thread and _parent along the stem nodes (i.e. the nodes // between u_in and u_out, whose parent have to be changed) _thread[v_in] = stem = u_in; _dirty_revs.clear(); _dirty_revs.push_back(v_in); par_stem = v_in; while (stem != u_out) { // Insert the next stem node into the thread list new_stem = _parent[stem]; _thread[u] = new_stem; _dirty_revs.push_back(u); // Remove the subtree of stem from the thread list w = _rev_thread[stem]; _thread[w] = right; _rev_thread[right] = w; // Change the parent node and shift stem nodes _parent[stem] = par_stem; par_stem = stem; stem = new_stem; // Update u and right u = _last_succ[stem] == _last_succ[par_stem] ? _rev_thread[par_stem] : _last_succ[stem]; right = _thread[u]; } _parent[u_out] = par_stem; _thread[u] = last; _rev_thread[last] = u; _last_succ[u_out] = u; // Remove the subtree of u_out from the thread list except for // the case when old_rev_thread equals to v_in // (it also means that join and v_out coincide) if (old_rev_thread != v_in) { _thread[old_rev_thread] = right; _rev_thread[right] = old_rev_thread; } // Update _rev_thread using the new _thread values for (int i = 0; i != int(_dirty_revs.size()); ++i) { int u = _dirty_revs[i]; _rev_thread[_thread[u]] = u; } // Update _pred, _forward, _last_succ and _succ_num for the // stem nodes from u_out to u_in int tmp_sc = 0, tmp_ls = _last_succ[u_out]; u = u_out; while (u != u_in) { w = _parent[u]; _pred[u] = _pred[w]; _forward[u] = !_forward[w]; tmp_sc += _succ_num[u] - _succ_num[w]; _succ_num[u] = tmp_sc; _last_succ[w] = tmp_ls; u = w; } _pred[u_in] = in_arc; _forward[u_in] = (u_in == arcSource(in_arc)); _succ_num[u_in] = old_succ_num; // Set limits for updating _last_succ form v_in and v_out // towards the root int up_limit_in = -1; int up_limit_out = -1; if (_last_succ[join] == v_in) { up_limit_out = join; } else { up_limit_in = join; } // Update _last_succ from v_in towards the root for (u = v_in; u != up_limit_in && _last_succ[u] == v_in; u = _parent[u]) { _last_succ[u] = _last_succ[u_out]; } // Update _last_succ from v_out towards the root if (join != old_rev_thread && v_in != old_rev_thread) { for (u = v_out; u != up_limit_out && _last_succ[u] == old_last_succ; u = _parent[u]) { _last_succ[u] = old_rev_thread; } } else { for (u = v_out; u != up_limit_out && _last_succ[u] == old_last_succ; u = _parent[u]) { _last_succ[u] = _last_succ[u_out]; } } // Update _succ_num from v_in to join for (u = v_in; u != join; u = _parent[u]) { _succ_num[u] += old_succ_num; } // Update _succ_num from v_out to join for (u = v_out; u != join; u = _parent[u]) { _succ_num[u] -= old_succ_num; } } // Update potentials void updatePotential() { Cost sigma = _forward[u_in] ? _pi[v_in] - _pi[u_in] - getCostForArc(_pred[u_in]) : _pi[v_in] - _pi[u_in] + getCostForArc(_pred[u_in]); // Update potentials in the subtree, which has been moved int end = _thread[_last_succ[u_in]]; for (int u = u_in; u != end; u = _thread[u]) { _pi[u] += sigma; } } // Heuristic initial pivots bool initialPivots() { Value curr, total = 0; std::vector supply_nodes, demand_nodes; Node u; _graph.first(u); for (; u != INVALIDNODE; _graph.next(u)) { curr = _supply[_node_id(u)]; if (curr > 0) { total += curr; supply_nodes.push_back(u); } else if (curr < 0) { demand_nodes.push_back(u); } } if (_sum_supply > 0) total -= _sum_supply; if (total <= 0) return true; ArcVector arc_vector; if (_sum_supply >= 0) { if (supply_nodes.size() == 1 && demand_nodes.size() == 1) { // Perform a reverse graph search from the sink to the source BoolVector reached(_node_num, false); Node s = supply_nodes[0], t = demand_nodes[0]; std::vector stack; reached[t] = true; stack.push_back(t); while (!stack.empty()) { Node u, v = stack.back(); stack.pop_back(); if (v == s) break; Arc a; _graph.firstIn(a, v); for (; a != INVALID; _graph.nextIn(a)) { if (reached[u = _graph.source(a)]) continue; ArcsType j = getArcID(a); if (INF >= total) { arc_vector.push_back(j); reached[u] = true; stack.push_back(u); } } } } else { // Find the min. cost incomming arc for each demand node for (int i = 0; i != demand_nodes.size(); ++i) { Node v = demand_nodes[i]; Cost c, min_cost = std::numeric_limits::max(); Arc min_arc = INVALID; Arc a; _graph.firstIn(a, v); for (; a != INVALID; _graph.nextIn(a)) { c = getCostForArc(getArcID(a)); if (c < min_cost) { min_cost = c; min_arc = a; } } if (min_arc != INVALID) { arc_vector.push_back(getArcID(min_arc)); } } } } else { // Find the min. cost outgoing arc for each supply node for (int i = 0; i != int(supply_nodes.size()); ++i) { Node u = supply_nodes[i]; Cost c, min_cost = std::numeric_limits::max(); Arc min_arc = INVALID; Arc a; _graph.firstOut(a, u); for (; a != INVALID; _graph.nextOut(a)) { c = getCostForArc(getArcID(a)); if (c < min_cost) { min_cost = c; min_arc = a; } } if (min_arc != INVALID) { arc_vector.push_back(getArcID(min_arc)); } } } // Perform heuristic initial pivots for (ArcsType i = 0; i != arc_vector.size(); ++i) { in_arc = arc_vector[i]; // l'erreur est probablement ici... if (arcState(in_arc) * (getCostForArc(in_arc) + _pi[arcSource(in_arc)] - _pi[arcTarget(in_arc)]) >= 0) continue; findJoinNode(); bool change = findLeavingArc(); if (delta >= MAX) return false; changeFlow(change); if (change) { updateTreeStructure(); updatePotential(); } } return true; } // Execute the algorithm ProblemType start() { return start(); } template ProblemType start() { PivotRuleImpl pivot(*this); ProblemType retVal = OPTIMAL; // Perform heuristic initial pivots (skip if warmstart tree was built) if (!_warmstart_tree_built) { if (!initialPivots()) return UNBOUNDED; } uint64_t iter_number = 0; //pivot.setDantzig(true); // Execute the Network Simplex algorithm while (pivot.findEnteringArc()) { if(max_iter > 0 && ++iter_number>=max_iter&&max_iter>0){ // max iterations hit retVal = MAX_ITER_REACHED; break; } findJoinNode(); bool change = findLeavingArc(); if (delta >= MAX) return UNBOUNDED; changeFlow(change); if (change) { updateTreeStructure(); updatePotential(); } } // Check feasibility if( retVal == OPTIMAL){ for (ArcsType e = _search_arc_num; e != _all_arc_num; ++e) { if (arcFlow(e) != 0){ if (fabs(arcFlow(e)) > _EPSILON) // change of the original code following issue #126 return INFEASIBLE; else setArcFlow(e, 0); } } } // Shift potentials to meet the requirements of the GEQ/LEQ type // optimality conditions if (_sum_supply == 0) { if (_stype == GEQ) { Cost max_pot = -std::numeric_limits::max(); for (ArcsType i = 0; i != _node_num; ++i) { if (_pi[i] > max_pot) max_pot = _pi[i]; } if (max_pot > 0) { for (ArcsType i = 0; i != _node_num; ++i) _pi[i] -= max_pot; } } else { Cost min_pot = std::numeric_limits::max(); for (ArcsType i = 0; i != _node_num; ++i) { if (_pi[i] < min_pot) min_pot = _pi[i]; } if (min_pot < 0) { for (ArcsType i = 0; i != _node_num; ++i) _pi[i] -= min_pot; } } } return retVal; } }; //class NetworkSimplexSimple ///@} } //namespace lemon python-pot-0.9.7+dfsg/ot/lp/solver_1d.py000066400000000000000000000451171525521106200201030ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Exact solvers for the 1D Wasserstein distance using cvxopt """ # Author: Remi Flamary # Author: Nicolas Courty # Author: Clément Bonet # Author: Laetitia Chapel # # License: MIT License import numpy as np import warnings from .emd_wrap import emd_1d_sorted from ..backend import get_backend from ..utils import list_to_array from ._network_simplex import center_ot_dual from collections import namedtuple def quantile_function(qs, cws, xs, return_index=False): r"""Computes the quantile function of an empirical distribution Parameters ---------- qs: array-like, shape (n,) Quantiles at which the quantile function is evaluated cws: array-like, shape (m, ...) cumulative weights of the 1D empirical distribution, if batched, must be similar to xs xs: array-like, shape (n, ...) locations of the 1D empirical distribution, batched against the `xs.ndim - 1` first dimensions return_index: bool, optional if True, returns also the associated indices. If False, do not return them Returns ------- q: array-like, shape (..., n) The quantiles of the distribution """ nx = get_backend(qs, cws) n = xs.shape[0] if nx.__name__ == "torch": # this is to ensure the best performance for torch searchsorted # and avoid a warning related to non-contiguous arrays cws = cws.movedim(0, -1).contiguous() qs = qs.movedim(0, -1).contiguous() else: cws = cws.T qs = qs.T idx = nx.clip(nx.searchsorted(cws, qs).T, 0, n - 1) if return_index: return nx.take_along_axis(xs, idx, axis=0), idx else: return nx.take_along_axis(xs, idx, axis=0) def wasserstein_1d( u_values, v_values, u_weights=None, v_weights=None, p=1, require_sort=True, return_plans=False, ): r""" Computes the 1 dimensional OT loss [15] between two (batched) empirical distributions .. math: OT_{loss} = \int_0^1 |cdf_u^{-1}(q) - cdf_v^{-1}(q)|^p dq It is formally the p-Wasserstein distance raised to the power p. We do so in a vectorized way by first building the individual quantile functions then integrating them. This function should be preferred to `emd_1d` whenever the backend is different to numpy, and when gradients over either sample positions or weights are required. Parameters ---------- u_values: array-like, shape (n, ...) locations of the first empirical distributions v_values: array-like, shape (m, ...) locations of the second empirical distributions u_weights: array-like, shape (n, ...), optional weights of the first empirical distributions, if None then uniform weights are used v_weights: array-like, shape (m, ...), optional weights of the second empirical distributions, if None then uniform weights are used p: int, optional order of the ground metric used, should be at least 1 (see [2, Chap. 2], default is 1 require_sort: bool, optional sort the distributions atoms locations, if False we will consider they have been sorted prior to being passed to the function, default is True return_plans: True, False or "coo_tuple", optional if True, also returns the optimal transport plan between the two (batched) measures as a coo_matrix, default is False. if "coo_tuple", returns the optimal transport plans as a tuple of (data, rows, cols) of the non-zero elements of the transportation matrix. This is useful for backends that do not support well sparse matrices (e.g. JAX, Tensorflow). Returns ------- cost: float/array-like, shape (...) the batched EMD plans: list of coo_matrix or namedTuple, optional if return_plans is True, returns a list of coo_matrix containing the plans. if return_plans is "coo_tuple", returns the plans as a list of namedTuple containing the data, rows and cols of the non-zero elements of the transportation matrix. References ---------- .. [15] Peyré, G., & Cuturi, M. (2018). Computational Optimal Transport. """ assert p >= 1, "The OT loss is only valid for p>=1, {p} was given".format(p=p) nx = get_backend(u_values, v_values, u_weights, v_weights) n = u_values.shape[0] m = v_values.shape[0] if u_weights is None: u_weights = nx.full(u_values.shape, 1.0 / n, type_as=u_values) elif u_weights.ndim != u_values.ndim: u_weights = nx.repeat(u_weights[..., None], u_values.shape[-1], -1) if v_weights is None: v_weights = nx.full(v_values.shape, 1.0 / m, type_as=v_values) elif v_weights.ndim != v_values.ndim: v_weights = nx.repeat(v_weights[..., None], v_values.shape[-1], -1) if require_sort: u_sorter = nx.argsort(u_values, 0) u_values = nx.take_along_axis(u_values, u_sorter, 0) v_sorter = nx.argsort(v_values, 0) v_values = nx.take_along_axis(v_values, v_sorter, 0) u_weights = nx.take_along_axis(u_weights, u_sorter, 0) v_weights = nx.take_along_axis(v_weights, v_sorter, 0) else: u_sorter = nx.arange(n) v_sorter = nx.arange(m) u_cumweights = nx.cumsum(u_weights, 0) v_cumweights = nx.cumsum(v_weights, 0) qs = nx.sort(nx.concatenate((u_cumweights, v_cumweights), 0), 0) u_quantiles, idx_u = quantile_function( qs, u_cumweights, u_values, return_index=True ) v_quantiles, idx_v = quantile_function( qs, v_cumweights, v_values, return_index=True ) qs = nx.zero_pad(qs, pad_width=[(1, 0)] + (qs.ndim - 1) * [(0, 0)]) delta = qs[1:, ...] - qs[:-1, ...] diff_quantiles = nx.abs(u_quantiles - v_quantiles) if return_plans is not False: u_quantiles_idx = nx.take_along_axis(u_sorter, idx_u, axis=0) v_quantiles_idx = nx.take_along_axis(v_sorter, idx_v, axis=0) if return_plans == "coo_tuple": PlanTuple = namedtuple("PlanTuple", ["data", "rows", "cols"]) plans = [ PlanTuple( data=delta[:, k], rows=u_quantiles_idx[:, k], cols=v_quantiles_idx[:, k], ) for k in range(delta.shape[1]) ] else: plans = [ nx.coo_matrix( delta[:, k], u_quantiles_idx[:, k], v_quantiles_idx[:, k], shape=(n, m), type_as=u_values, ) for k in range(delta.shape[1]) ] if p == 1: w_1d = nx.sum(delta * diff_quantiles, axis=0) else: w_1d = nx.sum(delta * nx.power(diff_quantiles, p), axis=0) if return_plans: return w_1d, plans else: return w_1d def emd_1d( x_a, x_b, a=None, b=None, metric="sqeuclidean", p=1.0, dense=True, log=False, check_marginals=True, ): r"""Solves the Earth Movers distance problem between 1d measures and returns the OT matrix .. math:: \gamma = arg\min_\gamma \sum_i \sum_j \gamma_{ij} d(x_a[i], x_b[j]) s.t. \gamma 1 = a, \gamma^T 1= b, \gamma\geq 0 where : - d is the metric - :math:`x_a` and :math:`x_b` are the samples - a and b are the sample weights This implementation only supports metrics of the form :math:`d(x, y) = |x - y|^p`. Uses the algorithm detailed in [1]_ Parameters ---------- x_a : ndarray of float64, shape (ns,) or (ns, 1) Source dirac locations (on the real line) x_b : ndarray of float64, shape (nt,) or (ns, 1) Target dirac locations (on the real line) a : ndarray of float64, shape (ns,), optional Source histogram (default is uniform weight) b : ndarray of float64, shape (nt,), optional Target histogram (default is uniform weight) metric: str, optional (default='sqeuclidean') Metric to be used. Only works with either of the strings `'sqeuclidean'`, `'minkowski'`, `'cityblock'`, or `'euclidean'`. p: float, optional (default=1.0) The p-norm to apply for if metric='minkowski' dense: boolean, optional (default=True) If True, returns :math:`\gamma` as a dense ndarray of shape (ns, nt). Otherwise returns a sparse representation using scipy's `coo_matrix` format. Due to implementation details, this function runs faster when `'sqeuclidean'`, `'minkowski'`, `'cityblock'`, or `'euclidean'` metrics are used. log: boolean, optional (default=False) If True, returns a dictionary containing the cost. Otherwise returns only the optimal transportation matrix. check_marginals: bool, optional (default=True) If True, checks that the marginals mass are equal. If False, skips the check. Returns ------- gamma: ndarray, shape (ns, nt) Optimal transportation matrix for the given parameters log: dict If input log is True, a dictionary containing the cost and the indices of the non-zero elements of the transportation matrix Examples -------- Simple example with obvious solution. The function emd_1d accepts lists and performs automatic conversion to numpy arrays >>> import ot >>> a=[.5, .5] >>> b=[.5, .5] >>> x_a = [2., 0.] >>> x_b = [0., 3.] >>> ot.emd_1d(x_a, x_b, a, b) array([[0. , 0.5], [0.5, 0. ]]) >>> ot.emd_1d(x_a, x_b) array([[0. , 0.5], [0.5, 0. ]]) References ---------- .. [1] Peyré, G., & Cuturi, M. (2017). "Computational Optimal Transport", 2018. See Also -------- ot.lp.emd : EMD for multidimensional distributions ot.lp.emd2_1d : EMD for 1d distributions (returns cost instead of the transportation matrix) """ x_a, x_b = list_to_array(x_a, x_b) nx = get_backend(x_a, x_b) if a is not None: a = list_to_array(a, nx=nx) if b is not None: b = list_to_array(b, nx=nx) assert ( x_a.ndim == 1 or x_a.ndim == 2 and x_a.shape[1] == 1 ), "emd_1d should only be used with monodimensional data" assert ( x_b.ndim == 1 or x_b.ndim == 2 and x_b.shape[1] == 1 ), "emd_1d should only be used with monodimensional data" if metric not in ["sqeuclidean", "minkowski", "cityblock", "euclidean"]: raise ValueError( "Solver for EMD in 1d only supports metrics " + "from the following list: " + "`['sqeuclidean', 'minkowski', 'cityblock', 'euclidean']`" ) # if empty array given then use uniform distributions if a is None or a.ndim == 0 or len(a) == 0: a = nx.ones((x_a.shape[0],), type_as=x_a) / x_a.shape[0] if b is None or b.ndim == 0 or len(b) == 0: b = nx.ones((x_b.shape[0],), type_as=x_b) / x_b.shape[0] # ensure that same mass if check_marginals: np.testing.assert_almost_equal( nx.to_numpy(nx.sum(a, axis=0)), nx.to_numpy(nx.sum(b, axis=0)), err_msg="a and b vector must have the same sum", decimal=6, ) b = b * nx.sum(a) / nx.sum(b) x_a_1d = nx.reshape(x_a, (-1,)) x_b_1d = nx.reshape(x_b, (-1,)) perm_a = nx.argsort(x_a_1d) perm_b = nx.argsort(x_b_1d) G_sorted, indices, cost = emd_1d_sorted( nx.to_numpy(a[perm_a]).astype(np.float64), nx.to_numpy(b[perm_b]).astype(np.float64), nx.to_numpy(x_a_1d[perm_a]).astype(np.float64), nx.to_numpy(x_b_1d[perm_b]).astype(np.float64), metric=metric, p=p, ) G = nx.coo_matrix( G_sorted, perm_a[indices[:, 0]], perm_b[indices[:, 1]], shape=(a.shape[0], b.shape[0]), type_as=x_a, ) if dense: G = nx.todense(G) elif str(nx) == "jax": warnings.warn("JAX does not support sparse matrices, converting to dense") if log: log = {"cost": nx.from_numpy(cost, type_as=x_a)} log["perms_x_a"] = perm_a[indices[:, 0]] log["perms_x_b"] = perm_b[indices[:, 1]] return G, log return G def emd2_1d( x_a, x_b, a=None, b=None, metric="sqeuclidean", p=1.0, dense=True, log=False ): r"""Solves the Earth Movers distance problem between 1d measures and returns the loss .. math:: \gamma = arg\min_\gamma \sum_i \sum_j \gamma_{ij} d(x_a[i], x_b[j]) s.t. \gamma 1 = a, \gamma^T 1= b, \gamma\geq 0 where : - d is the metric - :math:`x_a` and :math:`x_b` are the samples - a and b are the sample weights This implementation only supports metrics of the form :math:`d(x, y) = |x - y|^p`. Uses the algorithm detailed in [1]_ Parameters ---------- x_a : ndarray of float64, shape (ns,) or (ns, 1) Source dirac locations (on the real line) x_b : ndarray of float64, shape (nt,) or (ns, 1) Target dirac locations (on the real line) a : ndarray of float64, shape (ns,), optional Source histogram (default is uniform weight) b : ndarray of float64, shape (nt,), optional Target histogram (default is uniform weight) metric: str, optional (default='sqeuclidean') Metric to be used. Only works with either of the strings `'sqeuclidean'`, `'minkowski'`, `'cityblock'`, or `'euclidean'`. p: float, optional (default=1.0) The p-norm to apply for if metric='minkowski' dense: boolean, optional (default=True) If True, returns :math:`\gamma` as a dense ndarray of shape (ns, nt). Otherwise returns a sparse representation using scipy's `coo_matrix` format. Only used if log is set to True. Due to implementation details, this function runs faster when dense is set to False. log: boolean, optional (default=False) If True, returns a dictionary containing the transportation matrix. Otherwise returns only the loss. Returns ------- loss: float Cost associated to the optimal transportation log: dict If input log is True, a dictionary containing the Optimal transportation matrix for the given parameters Examples -------- Simple example with obvious solution. The function emd2_1d accepts lists and performs automatic conversion to numpy arrays >>> import ot >>> a=[.5, .5] >>> b=[.5, .5] >>> x_a = [2., 0.] >>> x_b = [0., 3.] >>> ot.emd2_1d(x_a, x_b, a, b) 0.5 >>> ot.emd2_1d(x_a, x_b) 0.5 References ---------- .. [1] Peyré, G., & Cuturi, M. (2017). "Computational Optimal Transport", 2018. See Also -------- ot.lp.emd2 : EMD for multidimensional distributions ot.lp.emd_1d : EMD for 1d distributions (returns the transportation matrix instead of the cost) """ # If we do not return G (log==False), then we should not to cast it to dense # (useless overhead) G, log_emd = emd_1d( x_a=x_a, x_b=x_b, a=a, b=b, metric=metric, p=p, dense=dense and log, log=True ) cost = log_emd["cost"] if log: log_emd = {"G": G} return cost, log_emd return cost def emd_1d_dual_backprop( u_values, v_values, u_weights=None, v_weights=None, p=1, require_sort=True ): r""" Computes the 1 dimensional OT loss between two (batched) empirical distributions .. math:: OT_{loss} = \int_0^1 |cdf_u^{-1}(q) - cdf_v^{-1}(q)|^p dq and returns the dual potentials and the loss, i.e. such that .. math:: OT_{loss}(u,v) = \int f(x)\mathrm{d}u(x) + \int g(y)\mathrm{d}v(y). .. warning:: This function only works in pytorch or jax as it backpropagates through the `wasserstein_1d` function. Parameters ---------- u_values: array-like, shape (n, ...) locations of the first empirical distribution v_values: array-like, shape (m, ...) locations of the second empirical distribution u_weights: array-like, shape (n, ...), optional weights of the first empirical distribution, if None then uniform weights are used v_weights: array-like, shape (m, ...), optional weights of the second empirical distribution, if None then uniform weights are used p: int, optional order of the ground metric used, should be at least 1, default is 1 require_sort: bool, optional sort the distributions atoms locations, if False we will consider they have been sorted prior to being passed to the function, default is True Returns ------- f: array-like shape (n, ...) First dual potential g: array-like shape (m, ...) Second dual potential loss: float/array-like, shape (...) the batched EMD """ nx = get_backend(u_values, v_values, u_weights, v_weights) assert nx.__name__ in ["torch", "jax"], "Function only valid in torch and jax" n = u_values.shape[0] m = v_values.shape[0] # Init weights or broadcast if necessary if u_weights is None: u_weights = nx.full(u_values.shape, 1.0 / n, type_as=u_values) elif u_weights.ndim != u_values.ndim: u_weights = nx.repeat(u_weights[..., None], u_values.shape[-1], -1) if v_weights is None: v_weights = nx.full(v_values.shape, 1.0 / m, type_as=v_values) elif v_weights.ndim != v_values.ndim: v_weights = nx.repeat(v_weights[..., None], v_values.shape[-1], -1) if nx.__name__ == "torch": u_weights_diff = nx.copy(u_weights) v_weights_diff = nx.copy(v_weights) u_weights_diff.requires_grad_(True) v_weights_diff.requires_grad_(True) cost_output = wasserstein_1d( u_values, v_values, u_weights_diff, v_weights_diff, p=p, require_sort=require_sort, ) loss = cost_output.sum() loss.backward() f, g = center_ot_dual( u_weights_diff.grad.detach(), v_weights_diff.grad.detach(), u_weights, v_weights, ) return f, g, cost_output.detach() # value can not be backward anymore elif nx.__name__ == "jax": import jax def ot_1d(a, b): return wasserstein_1d( u_values, v_values, a, b, p=p, require_sort=require_sort ).sum() f, g = jax.grad(ot_1d, argnums=[0, 1])(u_weights, v_weights) cost_output = wasserstein_1d( u_values, v_values, u_weights, v_weights, p=p, require_sort=require_sort ) f, g = center_ot_dual(f, g, u_weights, v_weights) return f, g, cost_output python-pot-0.9.7+dfsg/ot/lp/solver_circle.py000066400000000000000000000700271525521106200210360ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Exact solvers for the 1D Wasserstein distance using cvxopt """ # Author: Clément Bonet # # License: MIT License import numpy as np import warnings from ..backend import get_backend from .solver_1d import quantile_function def roll_cols(M, shifts): r""" Utils functions which allow to shift the order of each row of a 2d matrix Parameters ---------- M : ndarray, shape (nr, nc) Matrix to shift shifts: int or ndarray, shape (nr,) Returns ------- Shifted array Examples -------- >>> M = np.array([[1,2,3],[4,5,6],[7,8,9]]) >>> roll_cols(M, 2) array([[2, 3, 1], [5, 6, 4], [8, 9, 7]]) >>> roll_cols(M, np.array([[1],[2],[1]])) array([[3, 1, 2], [5, 6, 4], [9, 7, 8]]) References ---------- https://stackoverflow.com/questions/66596699/how-to-shift-columns-or-rows-in-a-tensor-with-different-offsets-in-pytorch """ nx = get_backend(M) n_rows, n_cols = M.shape arange1 = nx.tile( nx.reshape(nx.arange(n_cols, type_as=shifts), (1, n_cols)), (n_rows, 1) ) arange2 = (arange1 - shifts) % n_cols return nx.take_along_axis(M, arange2, 1) def derivative_cost_on_circle(theta, u_values, v_values, u_cdf, v_cdf, p=2): r"""Computes the left and right derivative of the cost (Equation (6.3) and (6.4) of [1]) Parameters ---------- theta: array-like, shape (n_batch, n) Cuts on the circle u_values: array-like, shape (n_batch, n) locations of the first empirical distribution v_values: array-like, shape (n_batch, n) locations of the second empirical distribution u_cdf: array-like, shape (n_batch, n) cdf of the first empirical distribution v_cdf: array-like, shape (n_batch, n) cdf of the second empirical distribution p: float, optional = 2 Power p used for computing the Wasserstein distance Returns ------- dCp: array-like, shape (n_batch, 1) The batched right derivative dCm: array-like, shape (n_batch, 1) The batched left derivative References --------- .. [44] Delon, Julie, Julien Salomon, and Andrei Sobolevski. "Fast transport optimization for Monge costs on the circle." SIAM Journal on Applied Mathematics 70.7 (2010): 2239-2258. """ nx = get_backend(theta, u_values, v_values, u_cdf, v_cdf) v_values = nx.copy(v_values) n = u_values.shape[-1] m_batch, m = v_values.shape v_cdf_theta = v_cdf - (theta - nx.floor(theta)) mask_p = v_cdf_theta >= 0 mask_n = v_cdf_theta < 0 v_values[mask_n] += nx.floor(theta)[mask_n] + 1 v_values[mask_p] += nx.floor(theta)[mask_p] if nx.any(mask_n) and nx.any(mask_p): v_cdf_theta[mask_n] += 1 v_cdf_theta2 = nx.copy(v_cdf_theta) v_cdf_theta2[mask_n] = np.inf shift = -nx.argmin(v_cdf_theta2, axis=-1) v_cdf_theta = roll_cols(v_cdf_theta, nx.reshape(shift, (-1, 1))) v_values = roll_cols(v_values, nx.reshape(shift, (-1, 1))) v_values = nx.concatenate( [v_values, nx.reshape(v_values[:, 0], (-1, 1)) + 1], axis=1 ) if nx.__name__ == "torch": # this is to ensure the best performance for torch searchsorted # and avoid a warning related to non-contiguous arrays u_cdf = u_cdf.contiguous() v_cdf_theta = v_cdf_theta.contiguous() # quantiles of F_u evaluated in F_v^\theta u_index = nx.searchsorted(u_cdf, v_cdf_theta) u_icdf_theta = nx.take_along_axis(u_values, nx.clip(u_index, 0, n - 1), -1) # Deal with 1 u_cdfm = nx.concatenate([u_cdf, nx.reshape(u_cdf[:, 0], (-1, 1)) + 1], axis=1) u_valuesm = nx.concatenate( [u_values, nx.reshape(u_values[:, 0], (-1, 1)) + 1], axis=1 ) if nx.__name__ == "torch": # this is to ensure the best performance for torch searchsorted # and avoid a warning related to non-contiguous arrays u_cdfm = u_cdfm.contiguous() v_cdf_theta = v_cdf_theta.contiguous() u_indexm = nx.searchsorted(u_cdfm, v_cdf_theta, side="right") u_icdfm_theta = nx.take_along_axis(u_valuesm, nx.clip(u_indexm, 0, n), -1) dCp = nx.sum( nx.power(nx.abs(u_icdf_theta - v_values[:, 1:]), p) - nx.power(nx.abs(u_icdf_theta - v_values[:, :-1]), p), axis=-1, ) dCm = nx.sum( nx.power(nx.abs(u_icdfm_theta - v_values[:, 1:]), p) - nx.power(nx.abs(u_icdfm_theta - v_values[:, :-1]), p), axis=-1, ) return dCp.reshape(-1, 1), dCm.reshape(-1, 1) def ot_cost_on_circle(theta, u_values, v_values, u_cdf, v_cdf, p): r"""Computes the the cost (Equation (6.2) of [1]) Parameters ---------- theta: array-like, shape (n_batch, n) Cuts on the circle u_values: array-like, shape (n_batch, n) locations of the first empirical distribution v_values: array-like, shape (n_batch, n) locations of the second empirical distribution u_cdf: array-like, shape (n_batch, n) cdf of the first empirical distribution v_cdf: array-like, shape (n_batch, n) cdf of the second empirical distribution p: float, optional = 2 Power p used for computing the Wasserstein distance Returns ------- ot_cost: array-like, shape (n_batch,) OT cost evaluated at theta References --------- .. [44] Delon, Julie, Julien Salomon, and Andrei Sobolevski. "Fast transport optimization for Monge costs on the circle." SIAM Journal on Applied Mathematics 70.7 (2010): 2239-2258. """ nx = get_backend(theta, u_values, v_values, u_cdf, v_cdf) v_values = nx.copy(v_values) m_batch, m = v_values.shape n_batch, n = u_values.shape v_cdf_theta = v_cdf - (theta - nx.floor(theta)) mask_p = v_cdf_theta >= 0 mask_n = v_cdf_theta < 0 v_values[mask_n] += nx.floor(theta)[mask_n] + 1 v_values[mask_p] += nx.floor(theta)[mask_p] if nx.any(mask_n) and nx.any(mask_p): v_cdf_theta[mask_n] += 1 # Put negative values at the end v_cdf_theta2 = nx.copy(v_cdf_theta) v_cdf_theta2[mask_n] = np.inf shift = -nx.argmin(v_cdf_theta2, axis=-1) v_cdf_theta = roll_cols(v_cdf_theta, nx.reshape(shift, (-1, 1))) v_values = roll_cols(v_values, nx.reshape(shift, (-1, 1))) v_values = nx.concatenate( [v_values, nx.reshape(v_values[:, 0], (-1, 1)) + 1], axis=1 ) # Compute absciss cdf_axis = nx.sort(nx.concatenate((u_cdf, v_cdf_theta), -1), -1) cdf_axis_pad = nx.zero_pad(cdf_axis, pad_width=[(0, 0), (1, 0)]) delta = cdf_axis_pad[..., 1:] - cdf_axis_pad[..., :-1] if nx.__name__ == "torch": # this is to ensure the best performance for torch searchsorted # and avoid a warning related to non-contiguous arrays u_cdf = u_cdf.contiguous() v_cdf_theta = v_cdf_theta.contiguous() cdf_axis = cdf_axis.contiguous() # Compute icdf u_index = nx.searchsorted(u_cdf, cdf_axis) u_icdf = nx.take_along_axis(u_values, u_index.clip(0, n - 1), -1) v_values = nx.concatenate( [v_values, nx.reshape(v_values[:, 0], (-1, 1)) + 1], axis=1 ) v_index = nx.searchsorted(v_cdf_theta, cdf_axis) v_icdf = nx.take_along_axis(v_values, v_index.clip(0, m), -1) if p == 1: ot_cost = nx.sum(delta * nx.abs(u_icdf - v_icdf), axis=-1) else: ot_cost = nx.sum(delta * nx.power(nx.abs(u_icdf - v_icdf), p), axis=-1) return ot_cost def binary_search_circle( u_values, v_values, u_weights=None, v_weights=None, p=1, Lm=10, Lp=10, tm=-1, tp=1, eps=1e-6, require_sort=True, log=False, ): r"""Computes the Wasserstein distance on the circle using the Binary search algorithm proposed in [44]. Samples need to be in :math:`S^1\cong [0,1[`. If they are on :math:`\mathbb{R}`, takes the value modulo 1. If the values are on :math:`S^1\subset\mathbb{R}^2`, it is required to first find the coordinates using e.g. the atan2 function. .. math:: W_p^p(u,v) = \inf_{\theta\in\mathbb{R}}\int_0^1 |F_u^{-1}(q) - (F_v-\theta)^{-1}(q)|^p\ \mathrm{d}q where: - :math:`F_u` and :math:`F_v` are respectively the cdfs of :math:`u` and :math:`v` For values :math:`x=(x_1,x_2)\in S^1`, it is required to first get their coordinates with .. math:: u = \frac{\pi + \mathrm{atan2}(-x_2,-x_1)}{2\pi} using e.g. ot.utils.get_coordinate_circle(x) The function runs on backend but tensorflow and jax are not supported. Parameters ---------- u_values : ndarray, shape (n, ...) samples in the source domain (coordinates on [0,1[) v_values : ndarray, shape (n, ...) samples in the target domain (coordinates on [0,1[) u_weights : ndarray, shape (n, ...), optional samples weights in the source domain v_weights : ndarray, shape (n, ...), optional samples weights in the target domain p : float, optional (default=1) Power p used for computing the Wasserstein distance Lm : int, optional Lower bound dC Lp : int, optional Upper bound dC tm: float, optional Lower bound theta tp: float, optional Upper bound theta eps: float, optional Stopping condition require_sort: bool, optional If True, sort the values. log: bool, optional If True, returns also the optimal theta Returns ------- loss: float/array-like, shape (...) Batched cost associated to the optimal transportation log: dict, optional log dictionary returned only if log==True in parameters Examples -------- >>> u = np.array([[0.2,0.5,0.8]])%1 >>> v = np.array([[0.4,0.5,0.7]])%1 >>> binary_search_circle(u.T, v.T, p=1) array([0.1]) References ---------- .. [44] Delon, Julie, Julien Salomon, and Andrei Sobolevski. "Fast transport optimization for Monge costs on the circle." SIAM Journal on Applied Mathematics 70.7 (2010): 2239-2258. .. Matlab Code: https://users.mccme.ru/ansobol/otarie/software.html """ assert p >= 1, "The OT loss is only valid for p>=1, {p} was given".format(p=p) nx = get_backend(u_values, v_values, u_weights, v_weights) n = u_values.shape[0] m = v_values.shape[0] if len(u_values.shape) == 1: u_values = nx.reshape(u_values, (n, 1)) if len(v_values.shape) == 1: v_values = nx.reshape(v_values, (m, 1)) if u_values.shape[1] != v_values.shape[1]: raise ValueError( "u and v must have the same number of batches {} and {} respectively given".format( u_values.shape[1], v_values.shape[1] ) ) u_values = u_values % 1 v_values = v_values % 1 if u_weights is None: u_weights = nx.full(u_values.shape, 1.0 / n, type_as=u_values) elif u_weights.ndim != u_values.ndim: u_weights = nx.repeat(u_weights[..., None], u_values.shape[-1], -1) if v_weights is None: v_weights = nx.full(v_values.shape, 1.0 / m, type_as=v_values) elif v_weights.ndim != v_values.ndim: v_weights = nx.repeat(v_weights[..., None], v_values.shape[-1], -1) if require_sort: u_sorter = nx.argsort(u_values, 0) u_values = nx.take_along_axis(u_values, u_sorter, 0) v_sorter = nx.argsort(v_values, 0) v_values = nx.take_along_axis(v_values, v_sorter, 0) u_weights = nx.take_along_axis(u_weights, u_sorter, 0) v_weights = nx.take_along_axis(v_weights, v_sorter, 0) u_cdf = nx.cumsum(u_weights, 0).T v_cdf = nx.cumsum(v_weights, 0).T u_values = u_values.T v_values = v_values.T L = max(Lm, Lp) tm = tm * nx.reshape(nx.ones((u_values.shape[0],), type_as=u_values), (-1, 1)) tm = nx.tile(tm, (1, m)) tp = tp * nx.reshape(nx.ones((u_values.shape[0],), type_as=u_values), (-1, 1)) tp = nx.tile(tp, (1, m)) tc = (tm + tp) / 2 done = nx.zeros((u_values.shape[0], m)) cpt = 0 while nx.any(1 - done): cpt += 1 dCp, dCm = derivative_cost_on_circle(tc, u_values, v_values, u_cdf, v_cdf, p) done = ((dCp * dCm) <= 0) * 1 mask = ((tp - tm) < eps / L) * (1 - done) if nx.any(mask): # can probably be improved by computing only relevant values dCptp, dCmtp = derivative_cost_on_circle( tp, u_values, v_values, u_cdf, v_cdf, p ) dCptm, dCmtm = derivative_cost_on_circle( tm, u_values, v_values, u_cdf, v_cdf, p ) Ctm = ot_cost_on_circle(tm, u_values, v_values, u_cdf, v_cdf, p).reshape( -1, 1 ) Ctp = ot_cost_on_circle(tp, u_values, v_values, u_cdf, v_cdf, p).reshape( -1, 1 ) # Avoid warning raised when dCptm - dCmtp == 0, for which # tc is not updated as mask_end is False, # see Issue #738 with warnings.catch_warnings(): warnings.simplefilter("ignore", category=RuntimeWarning) mask_end = mask * (nx.abs(dCptm - dCmtp) > 0.001) tc[mask_end > 0] = ( (Ctp - Ctm + tm * dCptm - tp * dCmtp) / (dCptm - dCmtp) )[mask_end > 0] done[nx.prod(mask, axis=-1) > 0] = 1 elif nx.any(1 - done): tm[((1 - mask) * (dCp < 0)) > 0] = tc[((1 - mask) * (dCp < 0)) > 0] tp[((1 - mask) * (dCp >= 0)) > 0] = tc[((1 - mask) * (dCp >= 0)) > 0] tc[((1 - mask) * (1 - done)) > 0] = ( tm[((1 - mask) * (1 - done)) > 0] + tp[((1 - mask) * (1 - done)) > 0] ) / 2 w = ot_cost_on_circle(nx.detach(tc), u_values, v_values, u_cdf, v_cdf, p) if log: return w, {"optimal_theta": tc[:, 0]} return w def wasserstein1_circle( u_values, v_values, u_weights=None, v_weights=None, require_sort=True ): r"""Computes the 1-Wasserstein distance on the circle using the level median [45]. Samples need to be in :math:`S^1\cong [0,1[`. If they are on :math:`\mathbb{R}`, takes the value modulo 1. If the values are on :math:`S^1\subset\mathbb{R}^2`, first find the coordinates using e.g. the atan2 function. The function runs on backend but tensorflow and jax are not supported. .. math:: W_1(u,v) = \int_0^1 |F_u(t)-F_v(t)-LevMed(F_u-F_v)|\ \mathrm{d}t Parameters ---------- u_values : ndarray, shape (n, ...) samples in the source domain (coordinates on [0,1[) v_values : ndarray, shape (n, ...) samples in the target domain (coordinates on [0,1[) u_weights : ndarray, shape (n, ...), optional samples weights in the source domain v_weights : ndarray, shape (n, ...), optional samples weights in the target domain require_sort: bool, optional If True, sort the values. Returns ------- loss: float/array-like, shape (...) Batched cost associated to the optimal transportation Examples -------- >>> u = np.array([[0.2,0.5,0.8]])%1 >>> v = np.array([[0.4,0.5,0.7]])%1 >>> wasserstein1_circle(u.T, v.T) array([0.1]) References ---------- .. [45] Hundrieser, Shayan, Marcel Klatt, and Axel Munk. "The statistics of circular optimal transport." Directional Statistics for Innovative Applications: A Bicentennial Tribute to Florence Nightingale. Singapore: Springer Nature Singapore, 2022. 57-82. .. Code R: https://gitlab.gwdg.de/shundri/circularOT/-/tree/master/ """ nx = get_backend(u_values, v_values, u_weights, v_weights) n = u_values.shape[0] m = v_values.shape[0] if len(u_values.shape) == 1: u_values = nx.reshape(u_values, (n, 1)) if len(v_values.shape) == 1: v_values = nx.reshape(v_values, (m, 1)) if u_values.shape[1] != v_values.shape[1]: raise ValueError( "u and v must have the same number of batchs {} and {} respectively given".format( u_values.shape[1], v_values.shape[1] ) ) u_values = u_values % 1 v_values = v_values % 1 if u_weights is None: u_weights = nx.full(u_values.shape, 1.0 / n, type_as=u_values) elif u_weights.ndim != u_values.ndim: u_weights = nx.repeat(u_weights[..., None], u_values.shape[-1], -1) if v_weights is None: v_weights = nx.full(v_values.shape, 1.0 / m, type_as=v_values) elif v_weights.ndim != v_values.ndim: v_weights = nx.repeat(v_weights[..., None], v_values.shape[-1], -1) if require_sort: u_sorter = nx.argsort(u_values, 0) u_values = nx.take_along_axis(u_values, u_sorter, 0) v_sorter = nx.argsort(v_values, 0) v_values = nx.take_along_axis(v_values, v_sorter, 0) u_weights = nx.take_along_axis(u_weights, u_sorter, 0) v_weights = nx.take_along_axis(v_weights, v_sorter, 0) # Code inspired from https://gitlab.gwdg.de/shundri/circularOT/-/tree/master/ values_sorted, values_sorter = nx.sort2(nx.concatenate((u_values, v_values), 0), 0) cdf_diff = nx.cumsum( nx.take_along_axis( nx.concatenate((u_weights, -v_weights), 0), values_sorter, 0 ), 0, ) cdf_diff_sorted, cdf_diff_sorter = nx.sort2(cdf_diff, axis=0) values_sorted = nx.zero_pad(values_sorted, pad_width=[(0, 1), (0, 0)], value=1) delta = values_sorted[1:, ...] - values_sorted[:-1, ...] weight_sorted = nx.take_along_axis(delta, cdf_diff_sorter, 0) sum_weights = nx.cumsum(weight_sorted, axis=0) - 0.5 sum_weights[sum_weights < 0] = np.inf inds = nx.argmin(sum_weights, axis=0) levMed = nx.take_along_axis(cdf_diff_sorted, nx.reshape(inds, (1, -1)), 0) return nx.sum(delta * nx.abs(cdf_diff - levMed), axis=0) def wasserstein_circle( u_values, v_values, u_weights=None, v_weights=None, p=1, Lm=10, Lp=10, tm=-1, tp=1, eps=1e-6, require_sort=True, ): r"""Computes the Wasserstein distance on the circle using either :ref:`[45] ` for p=1 or the binary search algorithm proposed in :ref:`[44] ` otherwise. Samples need to be in :math:`S^1\cong [0,1[`. If they are on :math:`\mathbb{R}`, takes the value modulo 1. If the values are on :math:`S^1\subset\mathbb{R}^2`, it requires to first find the coordinates using e.g. the atan2 function. General loss returned: .. math:: OT_{loss} = \inf_{\theta\in\mathbb{R}}\int_0^1 |cdf_u^{-1}(q) - (cdf_v-\theta)^{-1}(q)|^p\ \mathrm{d}q For p=1, [45] .. math:: W_1(u,v) = \int_0^1 |F_u(t)-F_v(t)-LevMed(F_u-F_v)|\ \mathrm{d}t For values :math:`x=(x_1,x_2)\in S^1`, it is required to first get their coordinates with .. math:: u = \frac{\pi + \mathrm{atan2}(-x_2,-x_1)}{2\pi} using e.g. ot.utils.get_coordinate_circle(x) The function runs on backend but tensorflow and jax are not supported. Parameters ---------- u_values : ndarray, shape (n, ...) samples in the source domain (coordinates on [0,1[) v_values : ndarray, shape (n, ...) samples in the target domain (coordinates on [0,1[) u_weights : ndarray, shape (n, ...), optional samples weights in the source domain v_weights : ndarray, shape (n, ...), optional samples weights in the target domain p : float, optional (default=1) Power p used for computing the Wasserstein distance Lm : int, optional Lower bound dC. For p>1. Lp : int, optional Upper bound dC. For p>1. tm: float, optional Lower bound theta. For p>1. tp: float, optional Upper bound theta. For p>1. eps: float, optional Stopping condition. For p>1. require_sort: bool, optional If True, sort the values. Returns ------- loss: float/array-like, shape (...) Batched cost associated to the optimal transportation Examples -------- >>> u = np.array([[0.2,0.5,0.8]])%1 >>> v = np.array([[0.4,0.5,0.7]])%1 >>> wasserstein_circle(u.T, v.T) array([0.1]) .. _references-wasserstein-circle: References ---------- .. [44] Hundrieser, Shayan, Marcel Klatt, and Axel Munk. "The statistics of circular optimal transport." Directional Statistics for Innovative Applications: A Bicentennial Tribute to Florence Nightingale. Singapore: Springer Nature Singapore, 2022. 57-82. .. [45] Delon, Julie, Julien Salomon, and Andrei Sobolevski. "Fast transport optimization for Monge costs on the circle." SIAM Journal on Applied Mathematics 70.7 (2010): 2239-2258. """ assert p >= 1, "The OT loss is only valid for p>=1, {p} was given".format(p=p) return binary_search_circle( u_values, v_values, u_weights, v_weights, p=p, Lm=Lm, Lp=Lp, tm=tm, tp=tp, eps=eps, require_sort=require_sort, ) def semidiscrete_wasserstein2_unif_circle(u_values, u_weights=None): r"""Computes the closed-form for the 2-Wasserstein distance between samples and a uniform distribution on :math:`S^1` Samples need to be in :math:`S^1\cong [0,1[`. If they are on :math:`\mathbb{R}`, takes the value modulo 1. If the values are on :math:`S^1\subset\mathbb{R}^2`, it is required to first find the coordinates using e.g. the atan2 function. .. math:: W_2^2(\mu_n, \nu) = \sum_{i=1}^n \alpha_i x_i^2 - \left(\sum_{i=1}^n \alpha_i x_i\right)^2 + \sum_{i=1}^n \alpha_i x_i \left(1-\alpha_i-2\sum_{k=1}^{i-1}\alpha_k\right) + \frac{1}{12} where: - :math:`\nu=\mathrm{Unif}(S^1)` and :math:`\mu_n = \sum_{i=1}^n \alpha_i \delta_{x_i}` For values :math:`x=(x_1,x_2)\in S^1`, it is required to first get their coordinates with .. math:: u = \frac{\pi + \mathrm{atan2}(-x_2,-x_1)}{2\pi}, using e.g. ot.utils.get_coordinate_circle(x). Parameters ---------- u_values : ndarray, shape (n, ...) Samples u_weights : ndarray, shape (n, ...), optional samples weights in the source domain Returns ------- loss: float/array-like, shape (...) Batched cost associated to the optimal transportation Examples -------- >>> x0 = np.array([[0], [0.2], [0.4]]) >>> semidiscrete_wasserstein2_unif_circle(x0) array([0.02111111]) References ---------- .. [46] Bonet, C., Berg, P., Courty, N., Septier, F., Drumetz, L., & Pham, M. T. (2023). Spherical sliced-wasserstein. International Conference on Learning Representations. """ nx = get_backend(u_values, u_weights) n = u_values.shape[0] u_values = u_values % 1 if len(u_values.shape) == 1: u_values = nx.reshape(u_values, (n, 1)) if u_weights is None: u_weights = nx.full(u_values.shape, 1.0 / n, type_as=u_values) elif u_weights.ndim != u_values.ndim: u_weights = nx.repeat(u_weights[..., None], u_values.shape[-1], -1) u_values = nx.sort(u_values, 0) u_cdf = nx.cumsum(u_weights, 0) u_cdf = nx.zero_pad(u_cdf, [(1, 0), (0, 0)]) cpt1 = nx.sum(u_weights * u_values**2, axis=0) u_mean = nx.sum(u_weights * u_values, axis=0) ns = 1 - u_weights - 2 * u_cdf[:-1] cpt2 = nx.sum(u_values * u_weights * ns, axis=0) return cpt1 - u_mean**2 + cpt2 + 1 / 12 def linear_circular_embedding(x, u_values, u_weights=None, require_sort=True): r"""Returns the embedding :math:`\hat{\mu}(x)` of Linear Circular OT with reference :math:`\eta=\mathrm{Unif}(S^1)` evaluated in :math:`x`. For any :math:`x\in [0,1[`, the embedding is given by (see :ref:`[78] `) .. math`` \hat{\mu}(x) = F_{\mu}^{-1}\big(x - \int z\mathrm{d}\mu(z) + \frac12) - x. Parameters ---------- x : ndary, shape (m,) Points in [0,1[ where to evaluate the embedding u_values : ndarray, shape (n, ...) samples in the source domain (coordinates on [0,1[) u_weights : ndarray, shape (n, ...), optional samples weights in the source domain Returns ------- embedding: ndarray of shape (m, ...) Embedding evaluated at :math:`x` .. _references-lcot: References ---------- .. [78] Martin, R. D., Medri, I., Bai, Y., Liu, X., Yan, K., Rohde, G. K., & Kolouri, S. (2024). LCOT: Linear Circular Optimal Transport. International Conference on Learning Representations. """ nx = get_backend(u_values, u_weights) n = u_values.shape[0] u_values = u_values % 1 if len(u_values.shape) == 1: u_values = nx.reshape(u_values, (n, 1)) if u_weights is None: u_weights = nx.full(u_values.shape, 1.0 / n, type_as=u_values) elif u_weights.ndim != u_values.ndim: u_weights = nx.repeat(u_weights[..., None], u_values.shape[-1], -1) if require_sort: u_sorter = nx.argsort(u_values, 0) u_values = nx.take_along_axis(u_values, u_sorter, 0) u_weights = nx.take_along_axis(u_weights, u_sorter, 0) u_cdf = nx.cumsum(u_weights, 0) u_cdf = nx.zero_pad(u_cdf, [(1, 0), (0, 0)]) q_s = ( x[:, None] - nx.sum(u_values * u_weights, axis=0)[None] + 0.5 ) # shape (m, ...) u_quantiles = quantile_function(q_s % 1, u_cdf, u_values) return (u_quantiles - x[:, None]) % 1 def linear_circular_ot(u_values, v_values=None, u_weights=None, v_weights=None): r"""Computes the Linear Circular Optimal Transport distance from :ref:`[78] ` using :math:`\eta=\mathrm{Unif}(S^1)` as reference measure. Samples need to be in :math:`S^1\cong [0,1[`. If they are on :math:`\mathbb{R}`, takes the value modulo 1. If the values are on :math:`S^1\subset\mathbb{R}^2`, it is required to first find the coordinates using e.g. the atan2 function. General loss returned: .. math:: \mathrm{LCOT}_2^2(\mu, \nu) = \int_0^1 d_{S^1}\big(\hat{\mu}(t), \hat{\nu}(t)\big)^2\ \mathrm{d}t where :math:`\hat{\mu}(x)=F_{\mu}^{-1}(x-\int z\mathrm{d}\mu(z)+\frac12) - x` for all :math:`x\in [0,1[`, and :math:`d_{S^1}(x,y)=\min(|x-y|, 1-|x-y|)` for :math:`x,y\in [0,1[`. Parameters ---------- u_values : ndarray, shape (n, ...) samples in the source domain (coordinates on [0,1[) v_values : ndarray, shape (n, ...), optional samples in the target domain (coordinates on [0,1[), if None, compute distance against uniform distribution u_weights : ndarray, shape (n, ...), optional samples weights in the source domain v_weights : ndarray, shape (n, ...), optional samples weights in the target domain Returns ------- loss: float/array-like, shape (...) Batched cost associated to the linear optimal transportation Examples -------- >>> u = np.array([[0.2,0.5,0.8]])%1 >>> v = np.array([[0.4,0.5,0.7]])%1 >>> linear_circular_ot(u.T, v.T) array([0.0127]) .. _references-lcot: References ---------- .. [78] Martin, R. D., Medri, I., Bai, Y., Liu, X., Yan, K., Rohde, G. K., & Kolouri, S. (2024). LCOT: Linear Circular Optimal Transport. International Conference on Learning Representations. """ nx = get_backend(u_values, u_weights) n = u_values.shape[0] u_values = u_values % 1 if len(u_values.shape) == 1: u_values = nx.reshape(u_values, (n, 1)) if u_weights is None: u_weights = nx.full(u_values.shape, 1.0 / n, type_as=u_values) elif u_weights.ndim != u_values.ndim: u_weights = nx.repeat(u_weights[..., None], u_values.shape[-1], -1) unif_s1 = nx.linspace(0, 1, 101, type_as=u_values)[:-1] emb_u = linear_circular_embedding(unif_s1, u_values, u_weights) if v_values is None: dist_u = nx.minimum(nx.abs(emb_u), 1 - nx.abs(emb_u)) return nx.mean(dist_u**2, axis=0) else: m = v_values.shape[0] if len(v_values.shape) == 1: v_values = nx.reshape(v_values, (m, 1)) if u_values.shape[1] != v_values.shape[1]: raise ValueError( "u and v must have the same number of batchs {} and {} respectively given".format( u_values.shape[1], v_values.shape[1] ) ) emb_v = linear_circular_embedding(unif_s1, v_values, v_weights) dist_uv = nx.minimum(nx.abs(emb_u - emb_v), 1 - nx.abs(emb_u - emb_v)) return nx.mean(dist_uv**2, axis=0) python-pot-0.9.7+dfsg/ot/lp/sparse_bipartitegraph.h000066400000000000000000000210661525521106200223630ustar00rootroot00000000000000/* -*- mode: C++; indent-tabs-mode: nil; -*- * * Sparse bipartite graph for optimal transport * Only stores edges that are explicitly added (not all n1×n2 edges) * * Uses CSR (Compressed Sparse Row) format for better cache locality and performance * - Binary search for arc lookup: O(log k) where k = avg edges per node * - Compact memory layout for better cache performance * - Requires edges to be provided in sorted order during construction */ #pragma once #include "core.h" #include #include #include namespace lemon { class SparseBipartiteDigraphBase { public: typedef SparseBipartiteDigraphBase Digraph; typedef int Node; typedef int64_t Arc; protected: int _node_num; int64_t _arc_num; int _n1, _n2; std::vector _arc_sources; // _arc_sources[arc_id] = source node std::vector _arc_targets; // _arc_targets[arc_id] = target node // CSR format // _row_ptr[i] = start index in _col_indices for source node i // _row_ptr[i+1] - _row_ptr[i] = number of outgoing edges from node i std::vector _row_ptr; std::vector _col_indices; std::vector _arc_ids; mutable std::vector> _in_arcs; // _in_arcs[node] = incoming arc IDs mutable bool _in_arcs_built; // Position tracking for O(1) iteration mutable std::vector _arc_to_out_pos; // _arc_to_out_pos[arc_id] = position in _arc_ids mutable std::vector _arc_to_in_pos; // _arc_to_in_pos[arc_id] = position in _in_arcs[target] mutable bool _position_maps_built; SparseBipartiteDigraphBase() : _node_num(0), _arc_num(0), _n1(0), _n2(0), _in_arcs_built(false), _position_maps_built(false) {} void construct(int n1, int n2) { _node_num = n1 + n2; _n1 = n1; _n2 = n2; _arc_num = 0; _arc_sources.clear(); _arc_targets.clear(); _row_ptr.clear(); _col_indices.clear(); _arc_ids.clear(); _in_arcs.clear(); _in_arcs_built = false; _arc_to_out_pos.clear(); _arc_to_in_pos.clear(); _position_maps_built = false; } void build_in_arcs() const { if (_in_arcs_built) return; _in_arcs.resize(_node_num); for (Arc a = 0; a < _arc_num; ++a) { Node tgt = _arc_targets[a]; _in_arcs[tgt].push_back(a); } _in_arcs_built = true; } void build_position_maps() const { if (_position_maps_built) return; _arc_to_out_pos.resize(_arc_num); _arc_to_in_pos.resize(_arc_num); // Build outgoing arc position map from CSR structure for (int64_t pos = 0; pos < _arc_num; ++pos) { Arc arc_id = _arc_ids[pos]; _arc_to_out_pos[arc_id] = pos; } // Build incoming arc position map build_in_arcs(); for (Node node = 0; node < _node_num; ++node) { const std::vector& in = _in_arcs[node]; for (size_t pos = 0; pos < in.size(); ++pos) { Arc arc_id = in[pos]; _arc_to_in_pos[arc_id] = pos; } } _position_maps_built = true; } public: Node operator()(int ix) const { return Node(ix); } static int index(const Node& node) { return node; } void buildFromEdges(const std::vector>& edges) { _arc_num = edges.size(); if (_arc_num == 0) { _row_ptr.assign(_n1 + 1, 0); return; } // Create indexed edges: (source, target, original_arc_id) std::vector> indexed_edges; indexed_edges.reserve(_arc_num); for (Arc i = 0; i < _arc_num; ++i) { indexed_edges.emplace_back(edges[i].first, edges[i].second, i); } // Sort by source node, then by target node CSR requirement std::sort(indexed_edges.begin(), indexed_edges.end(), [](const auto& a, const auto& b) { if (std::get<0>(a) != std::get<0>(b)) return std::get<0>(a) < std::get<0>(b); return std::get<1>(a) < std::get<1>(b); }); _arc_sources.resize(_arc_num); _arc_targets.resize(_arc_num); _col_indices.resize(_arc_num); _arc_ids.resize(_arc_num); _row_ptr.resize(_n1 + 1); _row_ptr[0] = 0; int current_row = 0; for (int64_t i = 0; i < _arc_num; ++i) { Node src = std::get<0>(indexed_edges[i]); Node tgt = std::get<1>(indexed_edges[i]); Arc orig_arc_id = std::get<2>(indexed_edges[i]); // Fill out row_ptr for rows with no outgoing edges while (current_row < src) { _row_ptr[++current_row] = i; } _arc_sources[orig_arc_id] = src; _arc_targets[orig_arc_id] = tgt; _col_indices[i] = tgt; _arc_ids[i] = orig_arc_id; } // Fill remaining row_ptr entries while (current_row < _n1) { _row_ptr[++current_row] = _arc_num; } _in_arcs_built = false; } // Find arc from s to t using binary search (returns -1 if not found) Arc arc(const Node& s, const Node& t) const { if (s < 0 || s >= _n1 || t < _n1 || t >= _node_num) { return Arc(-1); } int64_t start = _row_ptr[s]; int64_t end = _row_ptr[s + 1]; // Binary search for target t in col_indices[start:end] auto it = std::lower_bound( _col_indices.begin() + start, _col_indices.begin() + end, t ); if (it != _col_indices.begin() + end && *it == t) { int64_t pos = it - _col_indices.begin(); return _arc_ids[pos]; } return Arc(-1); } int nodeNum() const { return _node_num; } int64_t arcNum() const { return _arc_num; } int maxNodeId() const { return _node_num - 1; } int64_t maxArcId() const { return _arc_num - 1; } Node source(Arc arc) const { return (arc >= 0 && arc < _arc_num) ? _arc_sources[arc] : Node(-1); } Node target(Arc arc) const { return (arc >= 0 && arc < _arc_num) ? _arc_targets[arc] : Node(-1); } static int id(Node node) { return node; } static int64_t id(Arc arc) { return arc; } static Node nodeFromId(int id) { return Node(id); } static Arc arcFromId(int64_t id) { return Arc(id); } Arc findArc(Node s, Node t, Arc prev = -1) const { return prev == -1 ? arc(s, t) : Arc(-1); } void first(Node& node) const { node = _node_num - 1; } static void next(Node& node) { --node; } void first(Arc& arc) const { arc = _arc_num - 1; } static void next(Arc& arc) { --arc; } void firstOut(Arc& arc, const Node& node) const { if (node < 0 || node >= _n1) { arc = -1; return; } int64_t start = _row_ptr[node]; int64_t end = _row_ptr[node + 1]; arc = (start < end) ? _arc_ids[start] : Arc(-1); } void nextOut(Arc& arc) const { if (arc < 0) return; build_position_maps(); int64_t pos = _arc_to_out_pos[arc]; Node src = _arc_sources[arc]; int64_t end = _row_ptr[src + 1]; arc = (pos + 1 < end) ? _arc_ids[pos + 1] : Arc(-1); } void firstIn(Arc& arc, const Node& node) const { build_in_arcs(); // Lazy build on first call if (node < 0 || node >= _node_num || node < _n1) { arc = -1; // Invalid node or source nodes have no incoming arcs return; } const std::vector& in = _in_arcs[node]; arc = in.empty() ? Arc(-1) : in[0]; } void nextIn(Arc& arc) const { if (arc < 0) return; build_position_maps(); int64_t pos = _arc_to_in_pos[arc]; Node tgt = _arc_targets[arc]; const std::vector& in = _in_arcs[tgt]; arc = (pos + 1 < in.size()) ? in[pos + 1] : Arc(-1); } }; /// Sparse bipartite digraph - only stores edges that are explicitly added class SparseBipartiteDigraph : public SparseBipartiteDigraphBase { typedef SparseBipartiteDigraphBase Parent; public: SparseBipartiteDigraph() { construct(0, 0); } SparseBipartiteDigraph(int n1, int n2) { construct(n1, n2); } Node operator()(int ix) const { return Parent::operator()(ix); } static int index(const Node& node) { return Parent::index(node); } void buildFromEdges(const std::vector>& edges) { Parent::buildFromEdges(edges); } Arc arc(Node s, Node t) const { return Parent::arc(s, t); } int nodeNum() const { return Parent::nodeNum(); } int64_t arcNum() const { return Parent::arcNum(); } }; } //namespace lemon python-pot-0.9.7+dfsg/ot/mapping.py000066400000000000000000000702541525521106200172250ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Optimal Transport maps and variants .. warning:: Note that by default the module is not imported in :mod:`ot`. In order to use it you need to explicitly import :mod:`ot.mapping` """ # Author: Eloi Tanguy # Remi Flamary # # License: MIT License from .backend import get_backend, to_numpy from .lp import emd import numpy as np from .optim import cg from .utils import dist, unif, list_to_array, kernel, dots def nearest_brenier_potential_fit( X, V, X_classes=None, a=None, b=None, strongly_convex_constant=0.6, gradient_lipschitz_constant=1.4, its=100, log=False, init_method="barycentric", solver=None, ): r""" Computes optimal values and gradients at X for a strongly convex potential :math:`\varphi` with Lipschitz gradients on the partitions defined by `X_classes`, where :math:`\varphi` is optimal such that :math:`\nabla \varphi \#\mu \approx \nu`, given samples :math:`X = x_1, \cdots, x_n \sim \mu` and :math:`V = v_1, \cdots, v_n \sim \nu`. Finding such a potential that has the desired regularity on the partition :math:`(E_k)_{k \in [K]}` (given by the classes `X_classes`) is equivalent to finding optimal values `phi` for the :math:`\varphi(x_i)` and its gradients :math:`\nabla \varphi(x_i)` (variable`G`). In practice, these optimal values are found by solving the following problem .. math:: :nowrap: \begin{gather*} \text{min} \sum_{i,j}\pi_{i,j}\|g_i - v_j\|_2^2 \\ g_1,\cdots, g_n \in \mathbb{R}^d,\; \varphi_1, \cdots, \varphi_n \in \mathbb{R},\; \pi \in \Pi(a, b) \\ \text{s.t.}\ \forall k \in [K],\; \forall i,j \in I_k: \\ \varphi_i-\varphi_j-\langle g_j, x_i-x_j\rangle \geq c_1\|g_i - g_j\|_2^2 + c_2\|x_i-x_j\|_2^2 - c_3\langle g_j-g_i, x_j -x_i \rangle. \end{gather*} The constants :math:`c_1, c_2, c_3` only depend on `strongly_convex_constant` and `gradient_lipschitz_constant`. The constraint :math:`\pi \in \Pi(a, b)` denotes the fact that the matrix :math:`\pi` belong to the OT polytope of marginals a and b. :math:`I_k` is the subset of :math:`[n]` of the i such that :math:`x_i` is in the partition (or class) :math:`E_k`, i.e. `X_classes[i] == k`. This problem is solved by alternating over the variable :math:`\pi` and the variables :math:`\varphi_i, g_i`. For :math:`\pi`, the problem is the standard discrete OT problem, and for :math:`\varphi_i, g_i`, the problem is a convex QCQP solved using :code:`cvxpy` (ECOS solver). Accepts any compatible backend, but will perform the QCQP optimisation on Numpy arrays, and convert back at the end. .. warning:: This function requires the CVXPY library .. warning:: Accepts any backend but will convert to Numpy then back to the backend. Parameters ---------- X : array-like (n, d) reference points used to compute the optimal values phi and G V : array-like (n, d) values of the gradients at the reference points X X_classes : array-like (n,), optional classes of the reference points, defaults to a single class a : array-like (n,), optional weights for the reference points X, defaults to uniform b : array-like (n,), optional weights for the target points V, defaults to uniform strongly_convex_constant : float, optional constant for the strong convexity of the input potential phi, defaults to 0.6 gradient_lipschitz_constant : float, optional constant for the Lipschitz property of the input gradient G, defaults to 1.4 its: int, optional number of iterations, defaults to 100 log : bool, optional record log if true init_method : str, optional 'target' initialises G=V, 'barycentric' initialises at the image of X by the barycentric projection solver : str, optional The CVXPY solver to use Returns ------- phi : array-like (n,) optimal values of the potential at the points X G : array-like (n, d) optimal values of the gradients at the points X log : dict, optional If input log is true, a dictionary containing the values of the variables at each iteration, as well as solver information References ---------- .. [58] François-Pierre Paty, Alexandre d’Aspremont, and Marco Cuturi. Regularity as regularization: Smooth and strongly convex brenier potentials in optimal transport. In International Conference on Artificial Intelligence and Statistics, pages 1222–1232. PMLR, 2020. See Also -------- ot.mapping.nearest_brenier_potential_predict_bounds : Predicting SSNB images on new source data ot.da.NearestBrenierPotential : BaseTransport wrapper for SSNB """ try: import cvxpy as cvx except ImportError: print("Please install CVXPY to use this function") return assert ( X.shape == V.shape ), f"point shape should be the same as value shape, yet {X.shape} != {V.shape}" nx = get_backend(X, V, X_classes, a, b) X, V = to_numpy(X), to_numpy(V) n, d = X.shape if X_classes is not None: X_classes = to_numpy(X_classes) assert X_classes.size == n, "incorrect number of class items" else: X_classes = np.zeros(n) a = unif(n) if a is None else nx.to_numpy(a) b = unif(n) if b is None else nx.to_numpy(b) assert a.shape[-1] == b.shape[-1] == n, "incorrect measure weight sizes" assert init_method in [ "target", "barycentric", ], f"Unsupported initialization method '{init_method}'" if init_method == "target": G_val = V else: # Init G_val with barycentric projection G_val = emd(a, b, dist(X, V)) @ V / a.reshape(n, 1) phi_val = None log_dict = {"G_list": [], "phi_list": [], "its": []} for _ in range(its): # alternate optimisation iterations cost_matrix = dist(G_val, V) # optimise the plan plan = emd(a, b, cost_matrix) # optimise the values phi and the gradients G phi = cvx.Variable(n) G = cvx.Variable((n, d)) constraints = [] cost = 0 for i in range(n): for j in range(n): cost += cvx.sum_squares(G[i, :] - V[j, :]) * plan[i, j] objective = cvx.Minimize(cost) # OT cost c1, c2, c3 = _ssnb_qcqp_constants( strongly_convex_constant, gradient_lipschitz_constant ) for k in np.unique(X_classes): # constraints for the convex interpolation for i in np.where(X_classes == k)[0]: for j in np.where(X_classes == k)[0]: constraints += [ phi[i] >= phi[j] + G[j].T @ (X[i] - X[j]) + c1 * cvx.sum_squares(G[i] - G[j]) + c2 * cvx.sum_squares(X[i] - X[j]) - c3 * (G[j] - G[i]).T @ (X[j] - X[i]) ] problem = cvx.Problem(objective, constraints) problem.solve(solver=solver) phi_val, G_val = phi.value, G.value it_log_dict = { "solve_time": problem.solver_stats.solve_time, "setup_time": problem.solver_stats.setup_time, "num_iters": problem.solver_stats.num_iters, "status": problem.status, "value": problem.value, } if log: log_dict["its"].append(it_log_dict) log_dict["G_list"].append(G_val) log_dict["phi_list"].append(phi_val) # convert back to backend phi_val = nx.from_numpy(phi_val) G_val = nx.from_numpy(G_val) if not log: return phi_val, G_val return phi_val, G_val, log_dict def _ssnb_qcqp_constants(strongly_convex_constant, gradient_lipschitz_constant): r""" Handy function computing the constants for the Nearest Brenier Potential QCQP problems Parameters ---------- strongly_convex_constant : float gradient_lipschitz_constant : float Returns ------- c1 : float c2 : float c3 : float """ assert ( 0 < strongly_convex_constant < gradient_lipschitz_constant ), "incompatible regularity assumption" c = 1 / (2 * (1 - strongly_convex_constant / gradient_lipschitz_constant)) c1 = c / gradient_lipschitz_constant c2 = strongly_convex_constant * c c3 = 2 * strongly_convex_constant * c / gradient_lipschitz_constant return c1, c2, c3 def nearest_brenier_potential_predict_bounds( X, phi, G, Y, X_classes=None, Y_classes=None, strongly_convex_constant=0.6, gradient_lipschitz_constant=1.4, log=False, solver=None, ): r""" Compute the values of the lower and upper bounding potentials at the input points Y, using the potential optimal values phi at X and their gradients G at X. The 'lower' potential corresponds to the method from :ref:`[58]`, Equation 2, while the bounding property and 'upper' potential come from :ref:`[59]`, Theorem 3.14 (taking into account the fact that this theorem's statement has a min instead of a max, which is a typo). Both potentials are optimal for the SSNB problem. If :math:`I_k` is the subset of :math:`[n]` of the i such that :math:`x_i` is in the partition (or class) :math:`E_k`, for each :math:`y \in E_k`, this function solves the convex QCQP problems, respectively for l: 'lower' and u: 'upper': .. math:: :nowrap: \begin{gather*} (\varphi_{l}(x), \nabla \varphi_l(x)) = \text{argmin}\ t, \\ t\in \mathbb{R},\; g\in \mathbb{R}^d, \\ \text{s.t.} \forall j \in I_k,\; t-\varphi_j - \langle g_j, y-x_j \rangle \geq c_1\|g - g_j\|_2^2 + c_2\|y-x_j\|_2^2 - c_3\langle g_j-g, x_j -y \rangle. \end{gather*} .. math:: :nowrap: \begin{gather*} (\varphi_{u}(x), \nabla \varphi_u(x)) = \text{argmax}\ t, \\ t\in \mathbb{R},\; g\in \mathbb{R}^d, \\ \text{s.t.} \forall i \in I_k,\; \varphi_i^* -t - \langle g, x_i-y \rangle \geq c_1\|g_i - g\|_2^2 + c_2\|x_i-y\|_2^2 - c_3\langle g-g_i, y -x_i \rangle. \end{gather*} The constants :math:`c_1, c_2, c_3` only depend on `strongly_convex_constant` and `gradient_lipschitz_constant`. .. warning:: This function requires the CVXPY library .. warning:: Accepts any backend but will convert to Numpy then back to the backend. Parameters ---------- X : array-like (n, d) reference points used to compute the optimal values phi and G X_classes : array-like (n,) classes of the reference points phi : array-like (n,) optimal values of the potential at the points X G : array-like (n, d) optimal values of the gradients at the points X Y : array-like (m, d) input points X_classes : array-like (n,), optional classes of the reference points, defaults to a single class Y_classes : array_like (m,), optional classes of the input points, defaults to a single class strongly_convex_constant : float, optional constant for the strong convexity of the input potential phi, defaults to 0.6 gradient_lipschitz_constant : float, optional constant for the Lipschitz property of the input gradient G, defaults to 1.4 log : bool, optional record log if true solver : str, optional The CVXPY solver to use Returns ------- phi_lu: array-like (2, m) values of the lower and upper bounding potentials at Y G_lu: array-like (2, m, d) gradients of the lower and upper bounding potentials at Y log : dict, optional If input log is true, a dictionary containing solver information References ---------- .. [58] François-Pierre Paty, Alexandre d’Aspremont, and Marco Cuturi. Regularity as regularization: Smooth and strongly convex brenier potentials in optimal transport. In International Conference on Artificial Intelligence and Statistics, pages 1222–1232. PMLR, 2020. .. [59] Adrien B Taylor. Convex interpolation and performance estimation of first-order methods for convex optimization. PhD thesis, Catholic University of Louvain, Louvain-la-Neuve, Belgium, 2017. See Also -------- ot.mapping.nearest_brenier_potential_fit : Fitting the SSNB on source and target data ot.da.NearestBrenierPotential : BaseTransport wrapper for SSNB """ try: import cvxpy as cvx except ImportError: print("Please install CVXPY to use this function") return nx = get_backend(X, phi, G, Y) X = to_numpy(X) phi = to_numpy(phi) G = to_numpy(G) Y = to_numpy(Y) m, d = Y.shape if Y_classes is not None: Y_classes = to_numpy(Y_classes) assert Y_classes.size == m, "wrong number of class items for Y" else: Y_classes = np.zeros(m) assert ( X.shape[1] == d ), f"incompatible dimensions between X: {X.shape} and Y: {Y.shape}" n, _ = X.shape if X_classes is not None: X_classes = to_numpy(X_classes) assert X_classes.size == n, "incorrect number of class items" else: X_classes = np.zeros(n) assert X_classes.size == n, "wrong number of class items for X" c1, c2, c3 = _ssnb_qcqp_constants( strongly_convex_constant, gradient_lipschitz_constant ) phi_lu = np.zeros((2, m)) G_lu = np.zeros((2, m, d)) log_dict = {} for y_idx in range(m): log_item = {} # lower bound phi_l_y = cvx.Variable(1) G_l_y = cvx.Variable(d) objective = cvx.Minimize(phi_l_y) constraints = [] k = Y_classes[y_idx] for j in np.where(X_classes == k)[0]: constraints += [ phi_l_y >= phi[j] + G[j].T @ (Y[y_idx] - X[j]) + c1 * cvx.sum_squares(G_l_y - G[j]) + c2 * cvx.sum_squares(Y[y_idx] - X[j]) - c3 * (G[j] - G_l_y).T @ (X[j] - Y[y_idx]) ] problem = cvx.Problem(objective, constraints) problem.solve(solver=solver) phi_lu[0, y_idx] = ( phi_l_y.value if np.isscalar(phi_l_y.value) else phi_l_y.value.item() ) G_lu[0, y_idx] = G_l_y.value if log: log_item["l"] = { "solve_time": problem.solver_stats.solve_time, "setup_time": problem.solver_stats.setup_time, "num_iters": problem.solver_stats.num_iters, "status": problem.status, "value": problem.value, } # upper bound phi_u_y = cvx.Variable(1) G_u_y = cvx.Variable(d) objective = cvx.Maximize(phi_u_y) constraints = [] for i in np.where(X_classes == k)[0]: constraints += [ phi[i] >= phi_u_y + G_u_y.T @ (X[i] - Y[y_idx]) + c1 * cvx.sum_squares(G[i] - G_u_y) + c2 * cvx.sum_squares(X[i] - Y[y_idx]) - c3 * (G_u_y - G[i]).T @ (Y[y_idx] - X[i]) ] problem = cvx.Problem(objective, constraints) problem.solve(solver=solver) phi_lu[1, y_idx] = ( phi_u_y.value if np.isscalar(phi_u_y.value) else phi_u_y.value.item() ) G_lu[1, y_idx] = G_u_y.value if log: log_item["u"] = { "solve_time": problem.solver_stats.solve_time, "setup_time": problem.solver_stats.setup_time, "num_iters": problem.solver_stats.num_iters, "status": problem.status, "value": problem.value, } log_dict[y_idx] = log_item phi_lu, G_lu = nx.from_numpy(phi_lu), nx.from_numpy(G_lu) if not log: return phi_lu, G_lu return phi_lu, G_lu, log_dict def joint_OT_mapping_linear( xs, xt, mu=1, eta=0.001, bias=False, verbose=False, verbose2=False, numItermax=100, numInnerItermax=10, stopInnerThr=1e-6, stopThr=1e-5, log=False, **kwargs, ): r"""Joint OT and linear mapping estimation as proposed in :ref:`[8] `. The function solves the following optimization problem: .. math:: \min_{\gamma,L}\quad \|L(\mathbf{X_s}) - n_s\gamma \mathbf{X_t} \|^2_F + \mu \langle \gamma, \mathbf{M} \rangle_F + \eta \|L - \mathbf{I}\|^2_F s.t. \ \gamma \mathbf{1} = \mathbf{a} \gamma^T \mathbf{1} = \mathbf{b} \gamma \geq 0 where : - :math:`\mathbf{M}` is the (`ns`, `nt`) squared euclidean cost matrix between samples in :math:`\mathbf{X_s}` and :math:`\mathbf{X_t}` (scaled by :math:`n_s`) - :math:`L` is a :math:`d\times d` linear operator that approximates the barycentric mapping - :math:`\mathbf{I}` is the identity matrix (neutral linear mapping) - :math:`\mathbf{a}` and :math:`\mathbf{b}` are uniform source and target weights The problem consist in solving jointly an optimal transport matrix :math:`\gamma` and a linear mapping that fits the barycentric mapping :math:`n_s\gamma \mathbf{X_t}`. One can also estimate a mapping with constant bias (see supplementary material of :ref:`[8] `) using the bias optional argument. The algorithm used for solving the problem is the block coordinate descent that alternates between updates of :math:`\mathbf{G}` (using conditional gradient) and the update of :math:`\mathbf{L}` using a classical least square solver. Parameters ---------- xs : array-like (ns,d) samples in the source domain xt : array-like (nt,d) samples in the target domain mu : float,optional Weight for the linear OT loss (>0) eta : float, optional Regularization term for the linear mapping L (>0) bias : bool,optional Estimate linear mapping with constant bias numItermax : int, optional Max number of BCD iterations stopThr : float, optional Stop threshold on relative loss decrease (>0) numInnerItermax : int, optional Max number of iterations (inner CG solver) stopInnerThr : float, optional Stop threshold on error (inner CG solver) (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- gamma : (ns, nt) array-like Optimal transportation matrix for the given parameters L : (d, d) array-like Linear mapping matrix ((:math:`d+1`, `d`) if bias) log : dict log dictionary return only if log==True in parameters .. _references-joint-OT-mapping-linear: References ---------- .. [8] M. Perrot, N. Courty, R. Flamary, A. Habrard, "Mapping estimation for discrete optimal transport", Neural Information Processing Systems (NIPS), 2016. See Also -------- ot.lp.emd : Unregularized OT ot.optim.cg : General regularized OT """ xs, xt = list_to_array(xs, xt) nx = get_backend(xs, xt) ns, nt, d = xs.shape[0], xt.shape[0], xt.shape[1] if bias: xs1 = nx.concatenate((xs, nx.ones((ns, 1), type_as=xs)), axis=1) xstxs = nx.dot(xs1.T, xs1) Id = nx.eye(d + 1, type_as=xs) Id[-1] = 0 I0 = Id[:, :-1] def sel(x): return x[:-1, :] else: xs1 = xs xstxs = nx.dot(xs1.T, xs1) Id = nx.eye(d, type_as=xs) I0 = Id def sel(x): return x if log: log = {"err": []} a = unif(ns, type_as=xs) b = unif(nt, type_as=xt) M = dist(xs, xt) * ns G = emd(a, b, M) vloss = [] def loss(L, G): """Compute full loss""" return ( nx.sum((nx.dot(xs1, L) - ns * nx.dot(G, xt)) ** 2) + mu * nx.sum(G * M) + eta * nx.sum(sel(L - I0) ** 2) ) def solve_L(G): """solve L problem with fixed G (least square)""" xst = ns * nx.dot(G, xt) return nx.solve(xstxs + eta * Id, nx.dot(xs1.T, xst) + eta * I0) def solve_G(L, G0): """Update G with CG algorithm""" xsi = nx.dot(xs1, L) def f(G): return nx.sum((xsi - ns * nx.dot(G, xt)) ** 2) def df(G): return -2 * ns * nx.dot(xsi - ns * nx.dot(G, xt), xt.T) G = cg( a, b, M, 1.0 / mu, f, df, G0=G0, numItermax=numInnerItermax, stopThr=stopInnerThr, ) return G L = solve_L(G) vloss.append(loss(L, G)) if verbose: print( "{:5s}|{:12s}|{:8s}".format("It.", "Loss", "Delta loss") + "\n" + "-" * 32 ) print("{:5d}|{:8e}|{:8e}".format(0, vloss[-1], 0)) # init loop if numItermax > 0: loop = 1 else: loop = 0 it = 0 while loop: it += 1 # update G G = solve_G(L, G) # update L L = solve_L(G) vloss.append(loss(L, G)) if it >= numItermax: loop = 0 if abs(vloss[-1] - vloss[-2]) / abs(vloss[-2]) < stopThr: loop = 0 if verbose: if it % 20 == 0: print( "{:5s}|{:12s}|{:8s}".format("It.", "Loss", "Delta loss") + "\n" + "-" * 32 ) print( "{:5d}|{:8e}|{:8e}".format( it, vloss[-1], (vloss[-1] - vloss[-2]) / abs(vloss[-2]) ) ) if log: log["loss"] = vloss return G, L, log else: return G, L def joint_OT_mapping_kernel( xs, xt, mu=1, eta=0.001, kerneltype="gaussian", sigma=1, bias=False, verbose=False, verbose2=False, numItermax=100, numInnerItermax=10, stopInnerThr=1e-6, stopThr=1e-5, log=False, **kwargs, ): r"""Joint OT and nonlinear mapping estimation with kernels as proposed in :ref:`[8] `. The function solves the following optimization problem: .. math:: \min_{\gamma, L\in\mathcal{H}}\quad \|L(\mathbf{X_s}) - n_s\gamma \mathbf{X_t}\|^2_F + \mu \langle \gamma, \mathbf{M} \rangle_F + \eta \|L\|^2_\mathcal{H} s.t. \ \gamma \mathbf{1} = \mathbf{a} \gamma^T \mathbf{1} = \mathbf{b} \gamma \geq 0 where : - :math:`\mathbf{M}` is the (`ns`, `nt`) squared euclidean cost matrix between samples in :math:`\mathbf{X_s}` and :math:`\mathbf{X_t}` (scaled by :math:`n_s`) - :math:`L` is a :math:`n_s \times d` linear operator on a kernel matrix that approximates the barycentric mapping - :math:`\mathbf{a}` and :math:`\mathbf{b}` are uniform source and target weights The problem consist in solving jointly an optimal transport matrix :math:`\gamma` and the nonlinear mapping that fits the barycentric mapping :math:`n_s\gamma \mathbf{X_t}`. One can also estimate a mapping with constant bias (see supplementary material of :ref:`[8] `) using the bias optional argument. The algorithm used for solving the problem is the block coordinate descent that alternates between updates of :math:`\mathbf{G}` (using conditional gradient) and the update of :math:`\mathbf{L}` using a classical kernel least square solver. Parameters ---------- xs : array-like (ns,d) samples in the source domain xt : array-like (nt,d) samples in the target domain mu : float,optional Weight for the linear OT loss (>0) eta : float, optional Regularization term for the linear mapping L (>0) kerneltype : str,optional kernel used by calling function :py:func:`ot.utils.kernel` (gaussian by default) sigma : float, optional Gaussian kernel bandwidth. bias : bool,optional Estimate linear mapping with constant bias verbose : bool, optional Print information along iterations verbose2 : bool, optional Print information along iterations numItermax : int, optional Max number of BCD iterations numInnerItermax : int, optional Max number of iterations (inner CG solver) stopInnerThr : float, optional Stop threshold on error (inner CG solver) (>0) stopThr : float, optional Stop threshold on relative loss decrease (>0) log : bool, optional record log if True Returns ------- gamma : (ns, nt) array-like Optimal transportation matrix for the given parameters L : (ns, d) array-like Nonlinear mapping matrix ((:math:`n_s+1`, `d`) if bias) log : dict log dictionary return only if log==True in parameters .. _references-joint-OT-mapping-kernel: References ---------- .. [8] M. Perrot, N. Courty, R. Flamary, A. Habrard, "Mapping estimation for discrete optimal transport", Neural Information Processing Systems (NIPS), 2016. See Also -------- ot.lp.emd : Unregularized OT ot.optim.cg : General regularized OT """ xs, xt = list_to_array(xs, xt) nx = get_backend(xs, xt) ns, nt = xs.shape[0], xt.shape[0] K = kernel(xs, xs, method=kerneltype, sigma=sigma) if bias: K1 = nx.concatenate((K, nx.ones((ns, 1), type_as=xs)), axis=1) Id = nx.eye(ns + 1, type_as=xs) Id[-1] = 0 Kp = nx.eye(ns + 1, type_as=xs) Kp[:ns, :ns] = K # ls regu # K0 = K1.T.dot(K1)+eta*I # Kreg=I # RKHS regul K0 = nx.dot(K1.T, K1) + eta * Kp Kreg = Kp else: K1 = K Id = nx.eye(ns, type_as=xs) # ls regul # K0 = K1.T.dot(K1)+eta*I # Kreg=I # proper kernel ridge K0 = K + eta * Id Kreg = K if log: log = {"err": []} a = unif(ns, type_as=xs) b = unif(nt, type_as=xt) M = dist(xs, xt) * ns G = emd(a, b, M) vloss = [] def loss(L, G): """Compute full loss""" return ( nx.sum((nx.dot(K1, L) - ns * nx.dot(G, xt)) ** 2) + mu * nx.sum(G * M) + eta * nx.trace(dots(L.T, Kreg, L)) ) def solve_L_nobias(G): """solve L problem with fixed G (least square)""" xst = ns * nx.dot(G, xt) return nx.solve(K0, xst) def solve_L_bias(G): """solve L problem with fixed G (least square)""" xst = ns * nx.dot(G, xt) return nx.solve(K0, nx.dot(K1.T, xst)) def solve_G(L, G0): """Update G with CG algorithm""" xsi = nx.dot(K1, L) def f(G): return nx.sum((xsi - ns * nx.dot(G, xt)) ** 2) def df(G): return -2 * ns * nx.dot(xsi - ns * nx.dot(G, xt), xt.T) G = cg( a, b, M, 1.0 / mu, f, df, G0=G0, numItermax=numInnerItermax, stopThr=stopInnerThr, ) return G if bias: solve_L = solve_L_bias else: solve_L = solve_L_nobias L = solve_L(G) vloss.append(loss(L, G)) if verbose: print( "{:5s}|{:12s}|{:8s}".format("It.", "Loss", "Delta loss") + "\n" + "-" * 32 ) print("{:5d}|{:8e}|{:8e}".format(0, vloss[-1], 0)) # init loop if numItermax > 0: loop = 1 else: loop = 0 it = 0 while loop: it += 1 # update G G = solve_G(L, G) # update L L = solve_L(G) vloss.append(loss(L, G)) if it >= numItermax: loop = 0 if abs(vloss[-1] - vloss[-2]) / abs(vloss[-2]) < stopThr: loop = 0 if verbose: if it % 20 == 0: print( "{:5s}|{:12s}|{:8s}".format("It.", "Loss", "Delta loss") + "\n" + "-" * 32 ) print( "{:5d}|{:8e}|{:8e}".format( it, vloss[-1], (vloss[-1] - vloss[-2]) / abs(vloss[-2]) ) ) if log: log["loss"] = vloss return G, L, log else: return G, L python-pot-0.9.7+dfsg/ot/optim.py000066400000000000000000000667201525521106200167250ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Generic solvers for regularized OT or its semi-relaxed version. """ # Author: Remi Flamary # Titouan Vayer # Cédric Vincent-Cuaz # License: MIT License import numpy as np import warnings from .lp import emd from .bregman import sinkhorn from .backend import get_backend with warnings.catch_warnings(): warnings.simplefilter("ignore") try: from scipy.optimize._linesearch import scalar_search_armijo except ModuleNotFoundError: # scipy<1.8.0 from scipy.optimize.linesearch import scalar_search_armijo # The corresponding scipy function does not work for matrices def line_search_armijo( f, xk, pk, gfk, old_fval, args=(), c1=1e-4, alpha0=0.99, alpha_min=0.0, alpha_max=None, nx=None, **kwargs, ): r""" Armijo linesearch function that works with matrices Find an approximate minimum of :math:`f(x_k + \alpha \cdot p_k)` that satisfies the armijo conditions. .. note:: If the loss function f returns a float (resp. a 1d array) then the returned alpha and fa are float (resp. 1d arrays). Parameters ---------- f : callable loss function xk : array-like initial position pk : array-like descent direction gfk : array-like gradient of `f` at :math:`x_k` old_fval : float or 1d array loss value at :math:`x_k` args : tuple, optional arguments given to `f` c1 : float, optional :math:`c_1` const in armijo rule (>0) alpha0 : float, optional initial step (>0) alpha_min : float, default=0. minimum value for alpha alpha_max : float, optional maximum value for alpha nx : backend, optional If let to its default value None, a backend test will be conducted. Returns ------- alpha : float or 1d array step that satisfy armijo conditions fc : int nb of function call fa : float or 1d array loss value at step alpha """ if nx is None: xk0, pk0 = xk, pk nx = get_backend(xk0, pk0) else: xk0, pk0 = xk, pk if len(xk.shape) == 0: xk = nx.reshape(xk, (-1,)) xk = nx.to_numpy(xk) pk = nx.to_numpy(pk) gfk = nx.to_numpy(gfk) fc = [0] def phi(alpha1): # it's necessary to check boundary condition here for the coefficient # as the callback could be evaluated for negative value of alpha by # `scalar_search_armijo` function here: # # https://github.com/scipy/scipy/blob/11509c4a98edded6c59423ac44ca1b7f28fba1fd/scipy/optimize/linesearch.py#L686 # # see more details https://github.com/PythonOT/POT/issues/502 alpha1 = np.clip(alpha1, alpha_min, alpha_max) # The callable function operates on nx backend fc[0] += 1 alpha10 = nx.from_numpy(alpha1) fval = f(xk0 + alpha10 * pk0, *args) if isinstance(fval, float): # prevent bug from nx.to_numpy that can look for .cpu or .gpu return fval else: return nx.to_numpy(fval) if old_fval is None: phi0 = phi(0.0) elif isinstance(old_fval, float): # prevent bug from nx.to_numpy that can look for .cpu or .gpu phi0 = old_fval else: phi0 = nx.to_numpy(old_fval) derphi0 = np.sum(pk * gfk) # Quickfix for matrices alpha, phi1 = scalar_search_armijo( phi, phi0, derphi0, c1=c1, alpha0=alpha0, amin=alpha_min ) if alpha is None: return 0.0, fc[0], nx.from_numpy(phi0, type_as=xk0) else: alpha = np.clip(alpha, alpha_min, alpha_max) return ( nx.from_numpy(alpha, type_as=xk0), fc[0], nx.from_numpy(phi1, type_as=xk0), ) def generic_conditional_gradient( a, b, M, f, df, reg1, reg2, lp_solver, line_search, G0=None, numItermax=200, stopThr=1e-9, stopThr2=1e-9, verbose=False, log=False, nx=None, **kwargs, ): r""" Solve the general regularized OT problem or its semi-relaxed version with conditional gradient or generalized conditional gradient depending on the provided linear program solver. The function solves the following optimization problem if set as a conditional gradient: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg_1} \cdot f(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma^T \mathbf{1} &= \mathbf{b} \ (\text{optional constraint}) \gamma &\geq 0 where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`f` is the regularization term (and `df` is its gradient) - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) The algorithm used for solving the problem is conditional gradient as discussed in :ref:`[1] ` The function solves the following optimization problem if set a generalized conditional gradient: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg_1}\cdot f(\gamma) + \mathrm{reg_2}\cdot\Omega(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma^T \mathbf{1} &= \mathbf{b} \gamma &\geq 0 where : - :math:`\Omega` is the entropic regularization term :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` The algorithm used for solving the problem is the generalized conditional gradient as discussed in :ref:`[5, 7] ` Parameters ---------- a : array-like, shape (ns,) samples weights in the source domain b : array-like, shape (nt,) samples weights in the target domain M : array-like, shape (ns, nt) loss matrix f : function Regularization function taking a transportation matrix as argument df: function Gradient of the regularization function taking a transportation matrix as argument reg1 : float Regularization term >0 reg2 : float, Entropic Regularization term >0. Ignored if set to None. lp_solver: function, linear program solver for direction finding of the (generalized) conditional gradient. This function must take the form `lp_solver(a, b, Mi, **kwargs)` with p: `a` and `b` are sample weights in both domains; `Mi` is the gradient of the regularized objective; optimal arguments via kwargs. It must output an admissible transport plan. For instance, for the general regularized OT problem with conditional gradient :ref:`[1] `: def lp_solver(a, b, M, **kwargs): return ot.emd(a, b, M) or with the generalized conditional gradient instead :ref:`[5, 7] `: def lp_solver(a, b, Mi, **kwargs): return ot.sinkhorn(a, b, Mi) line_search: function, Function to find the optimal step. This function must take the form `line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs)` with: `cost` the cost function, `G` the transport plan, `deltaG` the conditional gradient direction given by lp_solver, `Mi` the gradient of regularized objective, `cost_G` the cost at G, `df_G` the gradient of the regularizer at G. Two types of outputs are supported: Instances such as `ot.optim.line_search_armijo` (generic solver), `ot.gromov.solve_gromov_linesearch` (FGW problems), `solve_semirelaxed_gromov_linesearch` (srFGW problems) and `gcg_linesearch` (generalized cg), output : the line-search step alpha, the number of iterations used in the solver if applicable and the loss value at step alpha. These can be called e.g as: def line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs): return ot.optim.line_search_armijo(cost, G, deltaG, Mi, cost_G, **kwargs) Instances such as `ot.gromov.solve_partial_gromov_linesearch` for partial (F)GW problems add as finale output, the next step gradient reading as a convex combination of previously computed gradients, taking advantage of the regularizer quadratic form. G0 : array-like, shape (ns,nt), optional initial guess (default is indep joint density) numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on the relative variation (>0) stopThr2 : float, optional Stop threshold on the absolute variation (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True nx : backend, optional If let to its default value None, the backend will be deduced from other inputs. **kwargs : dict Parameters for linesearch Returns ------- gamma : (ns x nt) ndarray Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters .. _references-cg: .. _references_gcg: References ---------- .. [1] Ferradans, S., Papadakis, N., Peyré, G., & Aujol, J. F. (2014). Regularized discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. .. [5] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, "Optimal Transport for Domain Adaptation," in IEEE Transactions on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 .. [7] Rakotomamonjy, A., Flamary, R., & Courty, N. (2015). Generalized conditional gradient: analysis of convergence and applications. arXiv preprint arXiv:1510.06567. See Also -------- ot.lp.emd : Unregularized optimal transport ot.bregman.sinkhorn : Entropic regularized optimal transport """ if nx is None: if isinstance(M, int) or isinstance(M, float): nx = get_backend(a, b) else: nx = get_backend(a, b, M) loop = 1 if log: log = {"loss": []} if G0 is None: G = nx.outer(a, b) else: # to not change G0 in place. G = nx.copy(G0) if reg2 is None: def cost(G): return nx.sum(M * G) + reg1 * f(G) else: def cost(G): return nx.sum(M * G) + reg1 * f(G) + reg2 * nx.sum(G * nx.log(G)) cost_G = cost(G) if log: log["loss"].append(cost_G) df_G = None it = 0 if verbose: print( "{:5s}|{:12s}|{:8s}|{:8s}".format( "It.", "Loss", "Relative loss", "Absolute loss" ) + "\n" + "-" * 48 ) print("{:5d}|{:8e}|{:8e}|{:8e}".format(it, cost_G, 0, 0)) while loop: it += 1 old_cost_G = cost_G # problem linearization if df_G is None: df_G = df(G) Mi = M + reg1 * df_G if reg2 is not None: Mi = Mi + reg2 * (1 + nx.log(G)) # solve linear program Gc, innerlog_ = lp_solver(a, b, Mi, **kwargs) # line search deltaG = Gc - G res_line_search = line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs) if len(res_line_search) == 3: # the line-search does not allow to update the gradient alpha, fc, cost_G = res_line_search df_G = None else: # the line-search allows to update the gradient directly # e.g. while using quadratic losses as the gromov-wasserstein loss alpha, fc, cost_G, df_G = res_line_search G = G + alpha * deltaG # test convergence if it >= numItermax: loop = 0 abs_delta_cost_G = abs(cost_G - old_cost_G) relative_delta_cost_G = ( abs_delta_cost_G / abs(cost_G) if cost_G != 0.0 else np.nan ) if relative_delta_cost_G < stopThr or abs_delta_cost_G < stopThr2: loop = 0 if log: log["loss"].append(cost_G) if verbose: if it % 20 == 0: print( "{:5s}|{:12s}|{:8s}|{:8s}".format( "It.", "Loss", "Relative loss", "Absolute loss" ) + "\n" + "-" * 48 ) print( "{:5d}|{:8e}|{:8e}|{:8e}".format( it, cost_G, relative_delta_cost_G, abs_delta_cost_G ) ) if log: log.update(innerlog_) return G, log else: return G def cg( a, b, M, reg, f, df, G0=None, line_search=None, numItermax=200, numItermaxEmd=100000, stopThr=1e-9, stopThr2=1e-9, verbose=False, log=False, nx=None, **kwargs, ): r""" Solve the general regularized OT problem with conditional gradient The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot f(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma^T \mathbf{1} &= \mathbf{b} \gamma &\geq 0 where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`f` is the regularization term (and `df` is its gradient) - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) The algorithm used for solving the problem is conditional gradient as discussed in :ref:`[1] ` Parameters ---------- a : array-like, shape (ns,) samples weights in the source domain b : array-like, shape (nt,) samples in the target domain M : array-like, shape (ns, nt) loss matrix reg : float Regularization term >0 G0 : array-like, shape (ns,nt), optional initial guess (default is indep joint density) line_search: function, Function to find the optimal step. Default is None and calls a wrapper to line_search_armijo. numItermax : int, optional Max number of iterations numItermaxEmd : int, optional Max number of iterations for emd stopThr : float, optional Stop threshold on the relative variation (>0) stopThr2 : float, optional Stop threshold on the absolute variation (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True nx : backend, optional If let to its default value None, the backend will be deduced from other inputs. **kwargs : dict Parameters for linesearch Returns ------- gamma : (ns x nt) ndarray Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters .. _references-cg: References ---------- .. [1] Ferradans, S., Papadakis, N., Peyré, G., & Aujol, J. F. (2014). Regularized discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. See Also -------- ot.lp.emd : Unregularized optimal transport ot.bregman.sinkhorn : Entropic regularized optimal transport """ if nx is None: if isinstance(M, int) or isinstance(M, float): nx = get_backend(a, b) else: nx = get_backend(a, b, M) if line_search is None: def line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs): return line_search_armijo(cost, G, deltaG, Mi, cost_G, nx=nx, **kwargs) def lp_solver(a, b, M, **kwargs): return emd(a, b, M, numItermaxEmd, log=True) return generic_conditional_gradient( a, b, M, f, df, reg, None, lp_solver, line_search, G0=G0, numItermax=numItermax, stopThr=stopThr, stopThr2=stopThr2, verbose=verbose, log=log, nx=nx, **kwargs, ) def semirelaxed_cg( a, b, M, reg, f, df, G0=None, line_search=None, numItermax=200, stopThr=1e-9, stopThr2=1e-9, verbose=False, log=False, nx=None, **kwargs, ): r""" Solve the general regularized and semi-relaxed OT problem with conditional gradient The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot f(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma &\geq 0 where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`f` is the regularization term (and `df` is its gradient) - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) The algorithm used for solving the problem is conditional gradient as discussed in :ref:`[1] ` Parameters ---------- a : array-like, shape (ns,) samples weights in the source domain b : array-like, shape (nt,) currently estimated samples weights in the target domain M : array-like, shape (ns, nt) loss matrix reg : float Regularization term >0 G0 : array-like, shape (ns,nt), optional initial guess (default is indep joint density) line_search: function, Function to find the optimal step. Default is None and calls a wrapper to line_search_armijo. numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on the relative variation (>0) stopThr2 : float, optional Stop threshold on the absolute variation (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True nx : backend, optional If let to its default value None, the backend will be deduced from other inputs. **kwargs : dict Parameters for linesearch Returns ------- gamma : (ns x nt) ndarray Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters .. _references-cg: References ---------- .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty. "Semi-relaxed Gromov-Wasserstein divergence and applications on graphs" International Conference on Learning Representations (ICLR), 2021. """ if nx is None: if isinstance(M, int) or isinstance(M, float): nx = get_backend(a, b) else: nx = get_backend(a, b, M) if line_search is None: def line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs): return line_search_armijo(cost, G, deltaG, Mi, cost_G, nx=nx, **kwargs) def lp_solver(a, b, Mi, **kwargs): # get minimum by rows as binary mask min_ = nx.reshape(nx.min(Mi, axis=1), (-1, 1)) # instead of exact elements equal to min_ we consider a small margin (1e-15) # for float precision issues. Then the mass is split uniformly # between these elements. Gc = nx.ones(1, type_as=a) * (Mi <= min_ + 1e-15) Gc *= nx.reshape((a / nx.sum(Gc, axis=1)), (-1, 1)) # return by default an empty inner_log return Gc, {} return generic_conditional_gradient( a, b, M, f, df, reg, None, lp_solver, line_search, G0=G0, numItermax=numItermax, stopThr=stopThr, stopThr2=stopThr2, verbose=verbose, log=log, nx=nx, **kwargs, ) def partial_cg( a, b, a_extended, b_extended, M, reg, f, df, G0=None, line_search=line_search_armijo, numItermax=200, stopThr=1e-9, stopThr2=1e-9, warn=True, verbose=False, log=False, **kwargs, ): r""" Solve the general regularized partial OT problem with conditional gradient The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot f(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma \mathbf{1} &= \mathbf{b} \mathbf{1}^T \gamma^T \mathbf{1} = m &\leq \min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\} \gamma &\geq 0 where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`f` is the regularization term (and `df` is its gradient) - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights - `m` is the amount of mass to be transported The algorithm used for solving the problem is conditional gradient as discussed in :ref:`[1] ` Parameters ---------- a : array-like, shape (ns,) samples weights in the source domain b : array-like, shape (nt,) currently estimated samples weights in the target domain a_extended : array-like, shape (ns + nb_dummies,) samples weights in the source domain with added dummy nodes b_extended : array-like, shape (nt + nb_dummies,) currently estimated samples weights in the target domain with added dummy nodes M : array-like, shape (ns, nt) loss matrix reg : float Regularization term >0 G0 : array-like, shape (ns,nt), optional initial guess (default is indep joint density) line_search: function, Function to find the optimal step. Default is the armijo line-search. numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on the relative variation (>0) stopThr2 : float, optional Stop threshold on the absolute variation (>0) warn: bool, optional. Whether to raise a warning when EMD did not converge. verbose : bool, optional Print information along iterations log : bool, optional record log if True **kwargs : dict Parameters for linesearch Returns ------- gamma : (ns x nt) ndarray Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters .. _references-partial-cg: References ---------- .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. """ n, m = a.shape[0], b.shape[0] n_extended, m_extended = a_extended.shape[0], b_extended.shape[0] nb_dummies = n_extended - n def lp_solver(a, b, Mi, **kwargs): # add dummy nodes to Mi Mi_extended = np.zeros((n_extended, m_extended), dtype=Mi.dtype) Mi_extended[:n, :m] = Mi Mi_extended[-nb_dummies:, -nb_dummies:] = np.max(M) * 1e2 G_extended, log_ = emd( a_extended, b_extended, Mi_extended, numItermax, log=True ) Gc = G_extended[:n, :m] if warn: if log_["warning"] is not None: raise ValueError( "Error in the EMD resolution: try to increase the" " number of dummy points" ) return Gc, log_ return generic_conditional_gradient( a, b, M, f, df, reg, None, lp_solver, line_search, G0=G0, numItermax=numItermax, stopThr=stopThr, stopThr2=stopThr2, verbose=verbose, log=log, **kwargs, ) def gcg( a, b, M, reg1, reg2, f, df, G0=None, numItermax=10, numInnerItermax=200, stopThr=1e-9, stopThr2=1e-9, verbose=False, log=False, **kwargs, ): r""" Solve the general regularized OT problem with the generalized conditional gradient The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg_1}\cdot\Omega(\gamma) + \mathrm{reg_2}\cdot f(\gamma) s.t. \ \gamma \mathbf{1} &= \mathbf{a} \gamma^T \mathbf{1} &= \mathbf{b} \gamma &\geq 0 where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`\Omega` is the entropic regularization term :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`f` is the regularization term (and `df` is its gradient) - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) The algorithm used for solving the problem is the generalized conditional gradient as discussed in :ref:`[5, 7] ` Parameters ---------- a : array-like, shape (ns,) samples weights in the source domain b : array-like, (nt,) samples in the target domain M : array-like, shape (ns, nt) loss matrix reg1 : float Entropic Regularization term >0 reg2 : float Second Regularization term >0 G0 : array-like, shape (ns, nt), optional initial guess (default is indep joint density) numItermax : int, optional Max number of iterations numInnerItermax : int, optional Max number of iterations of Sinkhorn stopThr : float, optional Stop threshold on the relative variation (>0) stopThr2 : float, optional Stop threshold on the absolute variation (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- gamma : ndarray, shape (ns, nt) Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters .. _references-gcg: References ---------- .. [5] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, "Optimal Transport for Domain Adaptation," in IEEE Transactions on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 .. [7] Rakotomamonjy, A., Flamary, R., & Courty, N. (2015). Generalized conditional gradient: analysis of convergence and applications. arXiv preprint arXiv:1510.06567. See Also -------- ot.optim.cg : conditional gradient """ def lp_solver(a, b, Mi, **kwargs): return sinkhorn(a, b, Mi, reg1, numItermax=numInnerItermax, log=True, **kwargs) def line_search(cost, G, deltaG, Mi, cost_G, df_G, **kwargs): return line_search_armijo(cost, G, deltaG, Mi, cost_G, **kwargs) return generic_conditional_gradient( a, b, M, f, df, reg2, None, lp_solver, line_search, G0=G0, numItermax=numItermax, stopThr=stopThr, stopThr2=stopThr2, verbose=verbose, log=log, **kwargs, ) def solve_1d_linesearch_quad(a, b): r""" For any convex or non-convex 1d quadratic function `f`, solve the following problem: .. math:: \mathop{\arg \min}_{0 \leq x \leq 1} \quad f(x) = ax^{2} + bx + c Parameters ---------- a,b : float or tensors (1,) The coefficients of the quadratic function Returns ------- x : float The optimal value which leads to the minimal cost """ if a > 0: # convex minimum = min(1.0, max(0.0, -b / (2.0 * a))) return minimum else: # non convex if a + b < 0: return 1.0 else: return 0.0 python-pot-0.9.7+dfsg/ot/partial/000077500000000000000000000000001525521106200166445ustar00rootroot00000000000000python-pot-0.9.7+dfsg/ot/partial/__init__.py000066400000000000000000000020101525521106200207460ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Efficient 1D solver for the partial optimal transport problem. """ # Author: Romain Tavenard # # License: MIT License # import compiled emd from .partial_solvers import ( partial_wasserstein_lagrange, partial_wasserstein, partial_wasserstein2, entropic_partial_wasserstein, entropic_partial_wasserstein_logscale, gwgrad_partial, gwloss_partial, partial_gromov_wasserstein, partial_gromov_wasserstein2, entropic_partial_gromov_wasserstein, entropic_partial_gromov_wasserstein2, partial_wasserstein_1d, ) __all__ = [ "partial_wasserstein_1d", "partial_wasserstein_lagrange", "partial_wasserstein", "partial_wasserstein2", "entropic_partial_wasserstein", "entropic_partial_wasserstein_logscale", "gwgrad_partial", "gwloss_partial", "partial_gromov_wasserstein", "partial_gromov_wasserstein2", "entropic_partial_gromov_wasserstein", "entropic_partial_gromov_wasserstein2", ] python-pot-0.9.7+dfsg/ot/partial/partial_cython.pyx000066400000000000000000000546171525521106200224430ustar00rootroot00000000000000# distutils: language = c++ # cython: boundscheck=False # cython: wraparound=False # cython: cdivision=True import numpy as np cimport numpy as np cimport cython import heapq @cython.boundscheck(False) @cython.wraparound(False) def insert_new_chain(np.ndarray[np.int64_t, ndim=1] chains_starting_at, np.ndarray[np.int64_t, ndim=1] chains_ending_at, int i, int j): """Insert the `candidate_chain=(i,j)` into the already known chains stored in `chains_starting_at` and `chains_ending_at`. `chains_starting_at` and `chains_ending_at` are modified in-place and the chain in which the candidate is inserted is returned. """ cdef int n = chains_starting_at.shape[0] if i - 1 >= 0 and i - 1 < n and chains_ending_at[i - 1] != -1: i = chains_ending_at[i - 1] if j + 1 >= 0 and j + 1 < n and chains_starting_at[j + 1] != -1: j = chains_starting_at[j + 1] if i >= 0 and i < n: if chains_starting_at[i] != -1: chains_ending_at[chains_starting_at[i]] = -1 chains_starting_at[i] = j if j >= 0 and j < n: if chains_ending_at[j] != -1: chains_starting_at[chains_ending_at[j]] = -1 chains_ending_at[j] = i return i, j @cython.boundscheck(False) @cython.wraparound(False) def compute_cost_for_chain(int idx_start, int idx_end, np.ndarray[np.float64_t, ndim=1] chain_costs_cumsum): """Compute the associated cost for a chain (set of contiguous points included in the solution) ranging from `idx_start` to `idx_end` (both included). """ if idx_start == 0: return chain_costs_cumsum[idx_end] else: return chain_costs_cumsum[idx_end] - chain_costs_cumsum[idx_start - 1] @cython.boundscheck(False) @cython.wraparound(False) def precompute_chain_costs_cumsum(np.ndarray[np.int64_t, ndim=1] minimal_chain_ending_at_idx, np.ndarray[np.float64_t, ndim=1] minimal_chain_ending_at_cost, int n): """For each position `i` at which a chain could end, Compute (using dynamic programming and the costs of minimal chains that have been precomputed) the cost of the largest chain ending at `i`. This is useful because this can be used later, to compute the cost of any chain in O(1) (cf. `compute_cost_for_chain`). """ cdef np.ndarray[np.float64_t, ndim=1] chain_costs_cumsum = np.zeros((n, ), dtype=np.float64) cdef int i, start cdef double additional_cost for i in range(n): if minimal_chain_ending_at_idx[i] != -1: start = minimal_chain_ending_at_idx[i] assert start >= 0 and start < n, "start should be in [0, n-1]" additional_cost = minimal_chain_ending_at_cost[i] if start == 0: chain_costs_cumsum[i] = additional_cost else: chain_costs_cumsum[i] = chain_costs_cumsum[start - 1] + additional_cost return chain_costs_cumsum @cython.boundscheck(False) @cython.wraparound(False) def get_cost_w1(np.ndarray[np.float64_t, ndim=1] diff_cum_sum, int idx_start, int idx_end): """Compute L1 cost of a chain using the L1 specific trick (O(1) time).""" if idx_start == 0: return diff_cum_sum[idx_end] else: return diff_cum_sum[idx_end] - diff_cum_sum[idx_start - 1] @cython.boundscheck(False) @cython.wraparound(False) def get_cost_wp(np.ndarray[np.float64_t, ndim=1] sorted_z, np.ndarray[np.int64_t, ndim=1] sorted_distrib_indicator, int idx_start, int idx_end, int p): """Compute Lp^p cost of a chain using no specific trick (O(N) time).""" cdef np.ndarray[np.float64_t, ndim=1] subset_z = sorted_z[idx_start:idx_end+1] cdef np.ndarray[np.int64_t, ndim=1] subset_indicator = sorted_distrib_indicator[idx_start:idx_end+1] cdef np.ndarray[np.float64_t, ndim=1] subset_x = subset_z[subset_indicator == 0] cdef np.ndarray[np.float64_t, ndim=1] subset_y = subset_z[subset_indicator == 1] return np.sum(np.abs(subset_x - subset_y) ** p) @cython.boundscheck(False) @cython.wraparound(True) def compute_costs(np.ndarray[np.float64_t, ndim=1] sorted_z, np.ndarray[np.float64_t, ndim=1] diff_cum_sum, np.ndarray[np.int64_t, ndim=1] diff_ranks, np.ndarray[np.int64_t, ndim=1] sorted_distrib_indicator, int p=1): """For each element in sorted `z`, compute its minimal chain (cf note below). Then compute the cost for each minimal chain and sort all minimal chains in increasing cost order. Note: the "minimal chain" of a point x_i in x is the minimal set of adjacent points (starting at x_i and extending to the right) that one should take to get a balanced set (ie. a set in which we have as many elements from x as elements from y) """ cdef list l_costs = [] cdef np.ndarray[np.int64_t, ndim=1] minimal_chain_ending_at_idx = np.full((sorted_z.shape[0], ), -1, dtype=np.int64) cdef np.ndarray[np.float64_t, ndim=1] minimal_chain_ending_at_cost = np.full((sorted_z.shape[0], ), -1.0, dtype=np.float64) cdef np.ndarray[np.int64_t, ndim=1] last_pos_for_rank_x = np.full((2 * sorted_z.shape[0], ), -1, dtype=np.int64) cdef np.ndarray[np.int64_t, ndim=1] last_pos_for_rank_y = np.full((2 * sorted_z.shape[0], ), -1, dtype=np.int64) cdef int n = diff_ranks.shape[0] cdef int idx_end, cur_rank, idx_start, target_rank cdef double cost assert sorted_z.shape[0] == diff_ranks.shape[0] == sorted_distrib_indicator.shape[0], \ "All input arrays must have the same length" for idx_end in range(n): # For each item in either distrib, find the scope of the smallest # "minimal chain" that would end at that point and extend on the left, # if one exists, and store the cost of this "minimal chain" by relying # on differences of cumulative sums cur_rank = diff_ranks[idx_end] idx_start = -1 if sorted_distrib_indicator[idx_end] == 0: target_rank = cur_rank - 1 if last_pos_for_rank_y[target_rank] != -1: idx_start = last_pos_for_rank_y[target_rank] last_pos_for_rank_x[cur_rank] = idx_end else: target_rank = cur_rank + 1 if last_pos_for_rank_x[target_rank] != -1: idx_start = last_pos_for_rank_x[target_rank] last_pos_for_rank_y[cur_rank] = idx_end if idx_start != -1: if p == 1: cost = get_cost_w1(diff_cum_sum, idx_start, idx_end) else: cost = get_cost_wp(sorted_z, sorted_distrib_indicator, idx_start, idx_end, p) if idx_end == idx_start + 1: heapq.heappush(l_costs, (abs(cost), idx_start, idx_end)) minimal_chain_ending_at_idx[idx_end] = idx_start minimal_chain_ending_at_cost[idx_end] = abs(cost) return l_costs, precompute_chain_costs_cumsum(minimal_chain_ending_at_idx, minimal_chain_ending_at_cost, n) @cython.boundscheck(False) @cython.wraparound(False) def arg_insert_in_sorted(np.ndarray[np.float64_t, ndim=1] x, np.ndarray[np.float64_t, ndim=1] y): """ Returns the index (similar to argsort) to be used to sort the concatenation of `x` and `y` (supposed to be 1d arrays). `x` and `y` are supposed to be sorted and their order cannot be changed in the resulting array (which is important for our ranking based algo in case of ex-aequos). """ assert x.ndim == 1 and y.ndim == 1 cdef Py_ssize_t n_x = x.shape[0] cdef Py_ssize_t n_y = y.shape[0] cdef np.ndarray[np.int64_t, ndim=1] arr_out = np.zeros(n_x + n_y, dtype=np.int64) cdef Py_ssize_t idx_x = 0 cdef Py_ssize_t idx_y = 0 while idx_x + idx_y < n_x + n_y: if idx_x == n_x: arr_out[idx_x + idx_y] = n_x + idx_y idx_y += 1 elif idx_y == n_y: arr_out[idx_x + idx_y] = idx_x idx_x += 1 else: if x[idx_x] <= y[idx_y]: arr_out[idx_x + idx_y] = idx_x idx_x += 1 else: arr_out[idx_x + idx_y] = n_x + idx_y idx_y += 1 return arr_out @cython.boundscheck(False) @cython.wraparound(False) def preprocess(np.ndarray[np.float64_t, ndim=1] x, np.ndarray[np.float64_t, ndim=1] y): """Given two 1d distributions `x` and `y`: 1. `indices_sort_x` sorts `x` (ie. `x[indices_sort_x]` is sorted) and `indices_sort_y` sorts `y` (ie. `y[indices_sort_y]` is sorted) 2. stack them into a single distrib such that: * the new distrib is sorted with sort indices (wrt a stack of sorted x and sorted y) `indices_sort_xy` * `sorted_distrib_indicator` is a vector of zeros and ones where 0 means "this point comes from x" and 1 means "this point comes from y" """ cdef np.ndarray[np.int64_t, ndim=1] indices_sort_x = np.argsort(x) cdef np.ndarray[np.int64_t, ndim=1] indices_sort_y = np.argsort(y) cdef np.ndarray[np.int64_t, ndim=1] indices_sort_xy = arg_insert_in_sorted(x[indices_sort_x], y[indices_sort_y]) cdef np.ndarray[np.int64_t, ndim=1] idx = np.concatenate((np.zeros(x.shape[0], dtype=np.int64), np.ones(y.shape[0], dtype=np.int64))) cdef np.ndarray[np.int64_t, ndim=1] sorted_distrib_indicator = idx[indices_sort_xy] return indices_sort_x, indices_sort_y, indices_sort_xy, sorted_distrib_indicator @cython.boundscheck(False) @cython.wraparound(False) def generate_solution_using_marginal_costs( list costs, np.ndarray[np.int64_t, ndim=1] ranks_xy, np.ndarray[np.float64_t, ndim=1] chain_costs_cumsum, int max_iter, np.ndarray[np.int64_t, ndim=1] sorted_distrib_indicator ): """Generate a solution from a sorted list of minimal chain costs. See the note in `compute_costs` docs for a definition of minimal chains. The solution is a pair of lists. The first list contains the indices from `sorted_x` that are in the active set, and the second one contains the indices from `sorted_y` that are in the active set. The third returned element is a list of marginal costs induced by each step: `arr_marginal_costs[i]` is the marginal cost induced by the `i`-th step of the algorithm, such that `np.cumsum(arr_marginal_costs[:i])` gives all intermediate costs up to step `i`. """ cdef np.ndarray[np.int64_t, ndim=1] active_set = np.zeros((sorted_distrib_indicator.shape[0], ), dtype=np.int64) cdef np.ndarray[np.int64_t, ndim=1] chains_starting_at = np.full((sorted_distrib_indicator.shape[0], ), -1, dtype=np.int64) cdef np.ndarray[np.int64_t, ndim=1] chains_ending_at = np.full((sorted_distrib_indicator.shape[0], ), -1, dtype=np.int64) cdef np.ndarray[np.int64_t, ndim=2] active_set_inserts = np.zeros((max_iter, 2), dtype=np.int64) cdef int n = sorted_distrib_indicator.shape[0] cdef int i, j, p_s, p_e cdef int n_pairs_in_active_set = 0 cdef double c cdef double marginal_cost cdef np.ndarray[np.float64_t, ndim=1] arr_marginal_costs = np.zeros((max_iter, ), dtype=np.float64) while len(costs) > 0 and max_iter > n_pairs_in_active_set: c, i, j = heapq.heappop(costs) if active_set[i] == 1 or active_set[j] == 1: continue # Case 1: j == i + 1 => "Simple" insert if j == i + 1: p_s, p_e = insert_new_chain(chains_starting_at, chains_ending_at, i, j) # Case 2: insert a chain that contains a chain elif chains_ending_at[j - 1] != -1: if i + 1 >= 0 and i + 1 < n: chains_starting_at[i + 1] = -1 if j - 1 >= 0 and j - 1 < n: chains_ending_at[j - 1] = -1 p_s, p_e = insert_new_chain(chains_starting_at, chains_ending_at, i, j) # There should be no "Case 3" else: raise ValueError active_set_inserts[n_pairs_in_active_set, 0] = i active_set_inserts[n_pairs_in_active_set, 1] = j arr_marginal_costs[n_pairs_in_active_set] = c active_set[i] = 1 active_set[j] = 1 n_pairs_in_active_set += 1 # We now need to update the candidate chains wrt the chain we have just created if p_s == 0 or p_e == n - 1: continue if sorted_distrib_indicator[p_s - 1] != sorted_distrib_indicator[p_e + 1]: # Insert (p_s - 1, p_e + 1) as a new candidate chain with marginal cost marginal_cost = (compute_cost_for_chain(p_s - 1, p_e + 1, chain_costs_cumsum) - compute_cost_for_chain(p_s, p_e, chain_costs_cumsum)) heapq.heappush(costs, (marginal_cost, p_s - 1, p_e + 1)) # Generate index arrays in the order of insertion in the active set cdef np.ndarray[np.int64_t, ndim=1] indices_sorted_x = np.zeros((max_iter, ), dtype=np.int64) - 1 cdef np.ndarray[np.int64_t, ndim=1] indices_sorted_y = np.zeros((max_iter, ), dtype=np.int64) - 1 cdef int a, b for i in range(max_iter): a = active_set_inserts[i, 0] b = active_set_inserts[i, 1] if sorted_distrib_indicator[a] == 0: indices_sorted_x[i] = ranks_xy[a] else: indices_sorted_y[i] = ranks_xy[a] if sorted_distrib_indicator[b] == 0: indices_sorted_x[i] = ranks_xy[b] else: indices_sorted_y[i] = ranks_xy[b] return ( indices_sorted_x, indices_sorted_y, arr_marginal_costs ) @cython.boundscheck(False) @cython.wraparound(False) def compute_cumulative_sum_differences( np.ndarray[np.float64_t, ndim=1] x_sorted, np.ndarray[np.float64_t, ndim=1] y_sorted, np.ndarray[np.int64_t, ndim=1] indices_sort_xy, np.ndarray[np.int64_t, ndim=1] sorted_distrib_indicator ): """Computes difference between cumulative sums for both distribs. The cumulative sum vector for a sorted x is: cumsum_x = [x_0, x_0 + x_1, ..., x_0 + ... + x_n] This vector is then extend to reach a length of 2*n by repeating values at places that correspond to an y item. In other words, if the order of x and y elements on the real line is something like x-y-y-x..., then the extended vector is (note the repetitions): cumsum_x = [x_0, x_0, x_0, x_0 + x_1, ..., x_0 + ... + x_n] Overall, this function returns `cumsum_x - cumsum_y` where `cumsum_x` and `cumsum_y` are the extended versions. """ cdef np.ndarray[np.float64_t, ndim=1] cum_sum_xs = np.cumsum(x_sorted) cdef np.ndarray[np.float64_t, ndim=1] cum_sum_ys = np.cumsum(y_sorted) cdef np.ndarray[np.float64_t, ndim=1] cum_sum = np.concatenate((cum_sum_xs, cum_sum_ys)) cdef np.ndarray[np.float64_t, ndim=1] cum_sum_ordered = cum_sum[indices_sort_xy] cdef np.ndarray[np.float64_t, ndim=1] cum_sum_x = insert_constant_values_float(cum_sum_ordered, 0, sorted_distrib_indicator) cdef np.ndarray[np.float64_t, ndim=1] cum_sum_y = insert_constant_values_float(cum_sum_ordered, 1, sorted_distrib_indicator) return cum_sum_x - cum_sum_y @cython.boundscheck(False) @cython.wraparound(False) def insert_constant_values_float( np.ndarray[np.float64_t, ndim=1] arr, np.int64_t distrib_index, np.ndarray[np.int64_t, ndim=1] sorted_distrib_indicator ): """Takes `arr` as input. For each position `i` in `arr`, if `sorted_distrib_indicator[i]==distrib_index`, the value from `arr` is copied, otherwise, the previous value that was copied from `arr` is repeated. """ cdef np.ndarray[np.float64_t, ndim=1] arr_insert = np.copy(arr) for i in range(len(arr)): if sorted_distrib_indicator[i]!=distrib_index: if i == 0: arr_insert[i] = 0 else: arr_insert[i] = arr_insert[i-1] return arr_insert @cython.boundscheck(False) @cython.wraparound(False) def insert_constant_values_int( np.ndarray[np.int64_t, ndim=1] arr, np.int64_t distrib_index, np.ndarray[np.int64_t, ndim=1] sorted_distrib_indicator ): """Takes `arr` as input. For each position `i` in `arr`, if `sorted_distrib_indicator[i]==distrib_index`, the value from `arr` is copied, otherwise, the previous value that was copied from `arr` is repeated. """ cdef np.ndarray[np.int64_t, ndim=1] arr_insert = np.copy(arr) for i in range(len(arr)): if sorted_distrib_indicator[i]!=distrib_index: if i == 0: arr_insert[i] = 0 else: arr_insert[i] = arr_insert[i-1] return arr_insert @cython.boundscheck(False) @cython.wraparound(False) def compute_rank_differences( np.ndarray[np.int64_t, ndim=1] indices_sort_xy, np.ndarray[np.int64_t, ndim=1] sorted_distrib_indicator ): """Precompute important rank-related quantities for better minimal chain extraction. Two quantities are returned: * `ranks_xy` is an array that gathers ranks of the elements in their original distrib, eg. if the distrib indicator is [0, 1, 1, 0, 0, 1], then `rank_xy` will be: [0, 0, 1, 1, 2, 2] * `diff_ranks` is computed from `ranks_xy_x_cum` and `ranks_xy_y_cum`. For the example above, we would have: ranks_xy_x_cum = [1, 1, 1, 2, 3, 3] ranks_xy_y_cum = [0, 1, 2, 2, 2, 3] And `diff_ranks` is just `ranks_xy_x_cum - ranks_xy_y_cum`. """ cdef int n_x = np.sum(sorted_distrib_indicator == 0) cdef int n_y = np.sum(sorted_distrib_indicator == 1) cdef np.ndarray[np.int64_t, ndim=1] ranks_x = np.arange(n_x) cdef np.ndarray[np.int64_t, ndim=1] ranks_y = np.arange(n_y) cdef np.ndarray[np.int64_t, ndim=1]ranks_xy = np.concatenate((ranks_x, ranks_y)) cdef np.ndarray[np.int64_t, ndim=1] ranks_xy_ordered = ranks_xy[indices_sort_xy] cdef np.ndarray[np.int64_t, ndim=1] ranks_xy_x = ranks_xy_ordered.copy() ranks_xy_x[sorted_distrib_indicator==1] = 0 ranks_xy_x[sorted_distrib_indicator==0] += 1 cdef np.ndarray[np.int64_t, ndim=1] ranks_xy_x_cum = insert_constant_values_int(ranks_xy_x, 0, sorted_distrib_indicator) cdef np.ndarray[np.int64_t, ndim=1] ranks_xy_y = ranks_xy_ordered.copy() ranks_xy_y[sorted_distrib_indicator==0] = 0 ranks_xy_y[sorted_distrib_indicator==1] += 1 cdef np.ndarray[np.int64_t, ndim=1] ranks_xy_y_cum = insert_constant_values_int(ranks_xy_y, 1, sorted_distrib_indicator) cdef np.ndarray[np.int64_t, ndim=1] diff_ranks = ranks_xy_x_cum - ranks_xy_y_cum return ranks_xy_ordered, diff_ranks @cython.boundscheck(False) @cython.wraparound(False) def partial_wasserstein_1d_cy( np.ndarray[np.float64_t, ndim=1] x, np.ndarray[np.float64_t, ndim=1] y, int max_iter, int p ): """Main routine for the partial Wasserstein problem in 1D. Does: 1. Preprocessing of the distribs (sorted & co) 2. Precomputations (ranks, cumulative sums) 3. Extraction of minimal chains 4. Generate and return solution Note that the indices in `indices_x` and `indices_y` are ordered wrt their order of appearance in the solution such that `indices_x[:10]` (resp y) is the set of indices from x (resp. y) for the partial problem of size 10. Arguments --------- x : np.ndarray of shape (n, ) First distrib to be considered (weights are considered uniform) y : np.ndarray of shape (m, ) Second distrib to be considered (weights are considered uniform) max_iter : int Number of iterations of the algorithm, which is equal to the number of pairs in the returned solution. p : int (default: 1) Order of the partial Wasserstein distance to be computed (p-Wasserstein, or $W_p^p$) Returns ------- indices_x : np.ndarray of shape (min(n, m, max_iter), ) Indices of elements from the x distribution to be included in the partial solutions Order of appearance in this array indicates order of inclusion in the solution indices_y : np.ndarray of shape (min(n, m, max_iter), ) Indices of elements from the x distribution to be included in the partial solutions Order of appearance in this array indicates order of inclusion in the solution list_marginal_costs : list of length min(n, m, max_iter) List of marginal costs associated to the intermediate partial problems `np.cumsum(list_marginal_costs)` gives the corresponding total costs for intermediate partial problems """ # Sort distribs and keep track of their original indices cdef np.ndarray[np.int64_t, ndim=1] indices_sort_x cdef np.ndarray[np.int64_t, ndim=1] indices_sort_y cdef np.ndarray[np.int64_t, ndim=1] indices_sort_xy cdef np.ndarray[np.int64_t, ndim=1] sorted_distrib_indicator cdef np.ndarray[np.float64_t, ndim=1] sorted_z cdef np.ndarray[np.float64_t, ndim=1] diff_cum_sum cdef np.ndarray[np.int64_t, ndim=1] ranks_xy cdef np.ndarray[np.int64_t, ndim=1] diff_ranks cdef list costs cdef np.ndarray[np.float64_t, ndim=1] chain_costs_cumsum cdef np.ndarray[np.int64_t, ndim=1] indices_x cdef np.ndarray[np.int64_t, ndim=1] indices_y cdef np.ndarray[np.int64_t, ndim=1] sol_indices_x_sorted cdef np.ndarray[np.int64_t, ndim=1] sol_indices_y_sorted cdef np.ndarray[np.float64_t, ndim=1] marginal_costs indices_sort_x, indices_sort_y, indices_sort_xy, sorted_distrib_indicator = preprocess(x, y) sorted_z = np.concatenate((x[indices_sort_x], y[indices_sort_y]))[indices_sort_xy] # Precompute useful quantities diff_cum_sum = compute_cumulative_sum_differences(x[indices_sort_x], y[indices_sort_y], indices_sort_xy, sorted_distrib_indicator) ranks_xy, diff_ranks = compute_rank_differences(indices_sort_xy, sorted_distrib_indicator) # Compute costs for "minimal chains" costs, chain_costs_cumsum = compute_costs(sorted_z, diff_cum_sum, diff_ranks, sorted_distrib_indicator, p=p) # Generate solution from sorted costs sol_indices_x_sorted, sol_indices_y_sorted, marginal_costs = generate_solution_using_marginal_costs(costs, ranks_xy, chain_costs_cumsum, max_iter, sorted_distrib_indicator) # Convert back into indices in original `x` and `y` distribs indices_x = indices_sort_x[sol_indices_x_sorted] indices_y = indices_sort_y[sol_indices_y_sorted] return indices_x, indices_y, marginal_costs python-pot-0.9.7+dfsg/ot/partial/partial_solvers.py000077500000000000000000001433301525521106200224360ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Partial OT solvers """ # Author: Laetitia Chapel # Yikun Bai < yikun.bai@vanderbilt.edu > # Cédric Vincent-Cuaz from ..utils import list_to_array from ..backend import get_backend from ..lp import emd from .partial_cython import partial_wasserstein_1d_cy import numpy as np import warnings # License: MIT License def partial_wasserstein_lagrange( a, b, M, reg_m=None, nb_dummies=1, log=False, **kwargs ): r""" Solves the partial optimal transport problem for the quadratic cost and returns the OT plan The function considers the following problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, (\mathbf{M} - \lambda) \rangle_F .. math:: s.t. \ \gamma \mathbf{1} &\leq \mathbf{a} \gamma^T \mathbf{1} &\leq \mathbf{b} \gamma &\geq 0 \mathbf{1}^T \gamma^T \mathbf{1} = m & \leq \min\{\|\mathbf{a}\|_1, \|\mathbf{b}\|_1\} or equivalently (see Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2018). An interpolating distance between optimal transport and Fisher–Rao metrics. Foundations of Computational Mathematics, 18(1), 1-44.) .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \sqrt{\frac{\lambda}{2} (\|\gamma \mathbf{1} - \mathbf{a}\|_1 + \|\gamma^T \mathbf{1} - \mathbf{b}\|_1)} s.t. \ \gamma \geq 0 where : - :math:`\mathbf{M}` is the metric cost matrix - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target unbalanced distributions - :math:`\lambda` is the lagrangian cost. Tuning its value allows attaining a given mass to be transported `m` The formulation of the problem has been proposed in :ref:`[28] ` Parameters ---------- a : np.ndarray (dim_a,) Unnormalized histogram of dimension `dim_a` b : np.ndarray (dim_b,) Unnormalized histograms of dimension `dim_b` M : np.ndarray (dim_a, dim_b) cost matrix for the quadratic cost reg_m : float, optional Lagrangian cost nb_dummies : int, optional, default:1 number of reservoir points to be added (to avoid numerical instabilities, increase its value if an error is raised) log : bool, optional record log if True **kwargs : dict parameters can be directly passed to the emd solver .. warning:: When dealing with a large number of points, the EMD solver may face some instabilities, especially when the mass associated to the dummy point is large. To avoid them, increase the number of dummy points (allows a smoother repartition of the mass over the points). Returns ------- gamma : (dim_a, dim_b) ndarray Optimal transportation matrix for the given parameters log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> a = [.1, .2] >>> b = [.1, .1] >>> M = [[0., 1.], [2., 3.]] >>> np.round(partial_wasserstein_lagrange(a,b,M), 2) array([[0.1, 0. ], [0. , 0.1]]) >>> np.round(partial_wasserstein_lagrange(a,b,M,reg_m=2), 2) array([[0.1, 0. ], [0. , 0. ]]) .. _references-partial-wasserstein-lagrange: References ---------- .. [28] Caffarelli, L. A., & McCann, R. J. (2010) Free boundaries in optimal transport and Monge-Ampere obstacle problems. Annals of mathematics, 673-730. See Also -------- ot.partial.partial_wasserstein : Partial Wasserstein with fixed mass """ a, b, M = list_to_array(a, b, M) nx = get_backend(a, b, M) if nx.sum(a) > 1 + 1e-15 or nx.sum(b) > 1 + 1e-15: # 1e-15 for numerical errors raise ValueError("Problem infeasible. Check that a and b are in the simplex") if reg_m is None: reg_m = float(nx.max(M)) + 1 if reg_m < -nx.max(M): return nx.zeros((len(a), len(b)), type_as=M) a0, b0, M0 = a, b, M # convert to humpy a, b, M = nx.to_numpy(a, b, M) eps = 1e-20 M = np.asarray(M, dtype=np.float64) b = np.asarray(b, dtype=np.float64) a = np.asarray(a, dtype=np.float64) M_star = M - reg_m # modified cost matrix # trick to fasten the computation: select only the subset of columns/lines # that can have marginals greater than 0 (that is to say M < 0) idx_x = np.where(np.min(M_star, axis=1) < eps)[0] idx_y = np.where(np.min(M_star, axis=0) < eps)[0] # extend a, b, M with "reservoir" or "dummy" points M_extended = np.zeros((len(idx_x) + nb_dummies, len(idx_y) + nb_dummies)) M_extended[: len(idx_x), : len(idx_y)] = M_star[np.ix_(idx_x, idx_y)] a_extended = np.append( a[idx_x], [(np.sum(a) - np.sum(a[idx_x]) + np.sum(b)) / nb_dummies] * nb_dummies ) b_extended = np.append( b[idx_y], [(np.sum(b) - np.sum(b[idx_y]) + np.sum(a)) / nb_dummies] * nb_dummies ) gamma_extended, log_emd = emd( a_extended, b_extended, M_extended, log=True, **kwargs ) gamma = np.zeros((len(a), len(b))) gamma[np.ix_(idx_x, idx_y)] = gamma_extended[:-nb_dummies, :-nb_dummies] # convert back to backend gamma = nx.from_numpy(gamma, type_as=M0) if log_emd["warning"] is not None: raise ValueError( "Error in the EMD resolution: try to increase the number of dummy points" ) log_emd["cost"] = nx.sum(gamma * M0) log_emd["u"] = nx.from_numpy(log_emd["u"], type_as=a0) log_emd["v"] = nx.from_numpy(log_emd["v"], type_as=b0) if log: return gamma, log_emd else: return gamma def partial_wasserstein(a, b, M, m=None, nb_dummies=1, log=False, **kwargs): r""" Solves the partial optimal transport problem for the quadratic cost and returns the OT plan The function considers the following problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F .. math:: s.t. \ \gamma \mathbf{1} &\leq \mathbf{a} \gamma^T \mathbf{1} &\leq \mathbf{b} \gamma &\geq 0 \mathbf{1}^T \gamma^T \mathbf{1} = m &\leq \min\{\|\mathbf{a}\|_1, \|\mathbf{b}\|_1\} where : - :math:`\mathbf{M}` is the metric cost matrix - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target unbalanced distributions - `m` is the amount of mass to be transported Parameters ---------- a : np.ndarray (dim_a,) Unnormalized histogram of dimension `dim_a` b : np.ndarray (dim_b,) Unnormalized histograms of dimension `dim_b` M : np.ndarray (dim_a, dim_b) cost matrix for the quadratic cost m : float, optional amount of mass to be transported nb_dummies : int, optional, default:1 number of reservoir points to be added (to avoid numerical instabilities, increase its value if an error is raised) log : bool, optional record log if True **kwargs : dict parameters can be directly passed to the emd solver .. warning:: When dealing with a large number of points, the EMD solver may face some instabilities, especially when the mass associated to the dummy point is large. To avoid them, increase the number of dummy points (allows a smoother repartition of the mass over the points). Returns ------- gamma : (dim_a, dim_b) ndarray Optimal transportation matrix for the given parameters log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> a = [.1, .2] >>> b = [.1, .1] >>> M = [[0., 1.], [2., 3.]] >>> np.round(partial_wasserstein(a,b,M), 2) array([[0.1, 0. ], [0. , 0.1]]) >>> np.round(partial_wasserstein(a,b,M,m=0.1), 2) array([[0.1, 0. ], [0. , 0. ]]) References ---------- .. [28] Caffarelli, L. A., & McCann, R. J. (2010) Free boundaries in optimal transport and Monge-Ampere obstacle problems. Annals of mathematics, 673-730. .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. See Also -------- ot.partial.partial_wasserstein_lagrange: Partial Wasserstein with regularization on the marginals ot.partial.entropic_partial_wasserstein: Partial Wasserstein with a entropic regularization parameter """ a, b, M = list_to_array(a, b, M) nx = get_backend(a, b, M) dim_a, dim_b = M.shape if len(a) == 0: a = nx.ones(dim_a, type_as=a) / dim_a if len(b) == 0: b = nx.ones(dim_b, type_as=b) / dim_b if m is None: return partial_wasserstein_lagrange(a, b, M, log=log, **kwargs) elif m < 0: raise ValueError("Problem infeasible. Parameter m should be greater than 0.") elif m > nx.min(nx.stack((nx.sum(a), nx.sum(b)))): raise ValueError( "Problem infeasible. Parameter m should lower or" " equal than min(|a|_1, |b|_1)." ) b_extension = nx.ones(nb_dummies, type_as=b) * (nx.sum(a) - m) / nb_dummies b_extended = nx.concatenate((b, b_extension)) a_extension = nx.ones(nb_dummies, type_as=a) * (nx.sum(b) - m) / nb_dummies a_extended = nx.concatenate((a, a_extension)) M_extension = nx.ones((nb_dummies, nb_dummies), type_as=M) * nx.max(M) * 2 M_extended = nx.concatenate( ( nx.concatenate((M, nx.zeros((M.shape[0], M_extension.shape[1]))), axis=1), nx.concatenate( (nx.zeros((M_extension.shape[0], M.shape[1])), M_extension), axis=1 ), ), axis=0, ) gamma, log_emd = emd(a_extended, b_extended, M_extended, log=True, **kwargs) gamma = gamma[: len(a), : len(b)] if log_emd["warning"] is not None: raise ValueError( "Error in the EMD resolution: try to increase the number of dummy points" ) log_emd["partial_w_dist"] = nx.sum(M * gamma) log_emd["u"] = log_emd["u"][: len(a)] log_emd["v"] = log_emd["v"][: len(b)] if log: return gamma, log_emd else: return gamma def partial_wasserstein2(a, b, M, m=None, nb_dummies=1, log=False, **kwargs): r""" Solves the partial optimal transport problem for the quadratic cost and returns the partial GW discrepancy The function considers the following problem: .. math:: \gamma = \min_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F .. math:: s.t. \ \gamma \mathbf{1} &\leq \mathbf{a} \gamma^T \mathbf{1} &\leq \mathbf{b} \gamma &\geq 0 \mathbf{1}^T \gamma^T \mathbf{1} = m &\leq \min\{\|\mathbf{a}\|_1, \|\mathbf{b}\|_1\} where : - :math:`\mathbf{M}` is the metric cost matrix - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target unbalanced distributions - `m` is the amount of mass to be transported Parameters ---------- a : np.ndarray (dim_a,) Unnormalized histogram of dimension `dim_a` b : np.ndarray (dim_b,) Unnormalized histograms of dimension `dim_b` M : np.ndarray (dim_a, dim_b) cost matrix for the quadratic cost m : float, optional amount of mass to be transported nb_dummies : int, optional, default:1 number of reservoir points to be added (to avoid numerical instabilities, increase its value if an error is raised) log : bool, optional record log if True **kwargs : dict parameters can be directly passed to the emd solver .. warning:: When dealing with a large number of points, the EMD solver may face some instabilities, especially when the mass associated to the dummy point is large. To avoid them, increase the number of dummy points (allows a smoother repartition of the mass over the points). Returns ------- GW: float partial GW discrepancy log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> a=[.1, .2] >>> b=[.1, .1] >>> M=[[0., 1.], [2., 3.]] >>> np.round(partial_wasserstein2(a, b, M), 1) 0.3 >>> np.round(partial_wasserstein2(a,b,M,m=0.1), 1) 0.0 References ---------- .. [28] Caffarelli, L. A., & McCann, R. J. (2010) Free boundaries in optimal transport and Monge-Ampere obstacle problems. Annals of mathematics, 673-730. .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. """ a, b, M = list_to_array(a, b, M) nx = get_backend(a, b, M) partial_gw, log_w = partial_wasserstein(a, b, M, m, nb_dummies, log=True, **kwargs) log_w["T"] = partial_gw if log: return nx.sum(partial_gw * M), log_w else: return nx.sum(partial_gw * M) def entropic_partial_wasserstein( a, b, M, reg, m=None, method="sinkhorn", numItermax=1000, stopThr=1e-100, verbose=False, log=False, ): r""" Solves the partial optimal transport problem and returns the OT plan The function considers the following problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot\Omega(\gamma) s.t. \gamma \mathbf{1} &\leq \mathbf{a} \\ \gamma^T \mathbf{1} &\leq \mathbf{b} \\ \gamma &\geq 0 \\ \mathbf{1}^T \gamma^T \mathbf{1} = m &\leq \min\{\|\mathbf{a}\|_1, \|\mathbf{b}\|_1\} \\ where : - :math:`\mathbf{M}` is the metric cost matrix - :math:`\Omega` is the entropic regularization term, :math:`\Omega=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are the sample weights - `m` is the amount of mass to be transported The formulation of the problem has been proposed in :ref:`[3] ` (prop. 5) **Choosing a solver** By default (``method='sinkhorn'``) the problem is solved with the classical Sinkhorn-like iterations in the multiplicative (exponential) domain. These are fast, but the kernel :math:`K = e^{-\mathbf{M}/\mathrm{reg}}` underflows to zero for small ``reg`` (or large cost values), making the plan collapse to ``NaN``/``Inf`` (see Issue #723). When this happens, set ``method='sinkhorn_log'`` to switch to the mathematically equivalent log-domain solver :any:`entropic_partial_wasserstein_logscale`, which relies on the log-sum-exp trick and stays finite at the price of being slower. Parameters ---------- a : np.ndarray (dim_a,) Unnormalized histogram of dimension `dim_a` b : np.ndarray (dim_b,) Unnormalized histograms of dimension `dim_b` M : np.ndarray (dim_a, dim_b) cost matrix reg : float Regularization term > 0 m : float, optional Amount of mass to be transported method : str, optional Solver used, either ``'sinkhorn'`` (classical multiplicative-domain solver, default) or ``'sinkhorn_log'`` (numerically stable log-domain solver). See the *Choosing a solver* note above. numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- gamma : (dim_a, dim_b) ndarray Optimal transportation matrix for the given parameters log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> a = [.1, .2] >>> b = [.1, .1] >>> M = [[0., 1.], [2., 3.]] >>> np.round(entropic_partial_wasserstein(a, b, M, 1, 0.1), 2) array([[0.06, 0.02], [0.01, 0. ]]) .. _references-entropic-partial-wasserstein: References ---------- .. [3] Benamou, J. D., Carlier, G., Cuturi, M., Nenna, L., & Peyré, G. (2015). Iterative Bregman projections for regularized transportation problems. SIAM Journal on Scientific Computing, 37(2), A1111-A1138. See Also -------- ot.partial.partial_wasserstein: exact Partial Wasserstein ot.partial.entropic_partial_wasserstein_logscale: stable log-domain solver """ if method.lower() == "sinkhorn": return _entropic_partial_wasserstein_sinkhorn( a, b, M, reg, m=m, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, ) elif method.lower() == "sinkhorn_log": return entropic_partial_wasserstein_logscale( a, b, M, reg, m=m, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, ) else: raise ValueError("Unknown method '%s'." % method) def _entropic_partial_wasserstein_sinkhorn( a, b, M, reg, m=None, numItermax=1000, stopThr=1e-100, verbose=False, log=False ): r"""Classical multiplicative-domain solver for the entropic partial OT problem. This is the backend used by ``entropic_partial_wasserstein(..., method="sinkhorn")``; see that function for the problem definition, parameters and references. It is kept as a separate solver so that the public ``entropic_partial_wasserstein`` is a thin wrapper dispatching between this solver and :any:`entropic_partial_wasserstein_logscale`. """ a, b, M = list_to_array(a, b, M) nx = get_backend(a, b, M) dim_a, dim_b = M.shape dx = nx.ones(dim_a, type_as=a) dy = nx.ones(dim_b, type_as=b) if len(a) == 0: a = nx.ones(dim_a, type_as=a) / dim_a if len(b) == 0: b = nx.ones(dim_b, type_as=b) / dim_b if m is None: m = nx.min(nx.stack((nx.sum(a), nx.sum(b)))) * 1.0 if m < 0: raise ValueError("Problem infeasible. Parameter m should be greater than 0.") if m > nx.min(nx.stack((nx.sum(a), nx.sum(b)))): raise ValueError( "Problem infeasible. Parameter m should lower or" " equal than min(|a|_1, |b|_1)." ) log_e = {"err": []} if nx.__name__ == "numpy": # Next 3 lines equivalent to K=nx.exp(-M/reg), but faster to compute K = np.empty(M.shape, dtype=M.dtype) np.divide(M, -reg, out=K) np.exp(K, out=K) np.multiply(K, m / np.sum(K), out=K) else: K = nx.exp(-M / reg) K = K * m / nx.sum(K) err, cpt = 1, 0 q1 = nx.ones(K.shape, type_as=K) q2 = nx.ones(K.shape, type_as=K) q3 = nx.ones(K.shape, type_as=K) while err > stopThr and cpt < numItermax: Kprev = K K = K * q1 K1 = nx.dot(nx.diag(nx.minimum(a / nx.sum(K, axis=1), dx)), K) q1 = q1 * Kprev / K1 K1prev = K1 K1 = K1 * q2 K2 = nx.dot(K1, nx.diag(nx.minimum(b / nx.sum(K1, axis=0), dy))) q2 = q2 * K1prev / K2 K2prev = K2 K2 = K2 * q3 K = K2 * (m / nx.sum(K2)) q3 = q3 * K2prev / K if nx.any(nx.isnan(K)) or nx.any(nx.isinf(K)): print("Warning: numerical errors at iteration", cpt) break if cpt % 10 == 0: err = nx.norm(Kprev - K) if log: log_e["err"].append(err) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 11) print("{:5d}|{:8e}|".format(cpt, err)) cpt = cpt + 1 log_e["partial_w_dist"] = nx.sum(M * K) if log: return K, log_e else: return K def entropic_partial_wasserstein_logscale( a, b, M, reg, m=None, numItermax=1000, stopThr=1e-100, verbose=False, log=False ): r""" Solves the partial optimal transport problem and returns the OT plan This function solves exactly the same problem as :any:`entropic_partial_wasserstein`, but it is in log-scale, i.e., the log-sum-exp trick is used, so it is more stable but slower. The input and output format is the same as :any:`entropic_partial_wasserstein`. The function considers the following problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot\Omega(\gamma) s.t. \gamma \mathbf{1} &\leq \mathbf{a} \\ \gamma^T \mathbf{1} &\leq \mathbf{b} \\ \gamma &\geq 0 \\ \mathbf{1}^T \gamma^T \mathbf{1} = m &\leq \min\{\|\mathbf{a}\|_1, \|\mathbf{b}\|_1\} \\ where : - :math:`\mathbf{M}` is the metric cost matrix - :math:`\Omega` is the entropic regularization term, :math:`\Omega=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are the sample weights - `m` is the amount of mass to be transported The formulation of the problem has been proposed in :ref:`[3] ` (prop. 5) Parameters ---------- a : np.ndarray (dim_a,) Unnormalized histogram of dimension `dim_a` b : np.ndarray (dim_b,) Unnormalized histograms of dimension `dim_b` M : np.ndarray (dim_a, dim_b) cost matrix reg : float Regularization term > 0 m : float, optional Amount of mass to be transported numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- gamma : (dim_a, dim_b) ndarray Optimal transportation matrix for the given parameters log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> a = [.1, .2] >>> b = [.1, .1] >>> M = [[0., 1.], [2., 3.]] >>> np.round(entropic_partial_wasserstein_logscale(a, b, M, 1, 0.1), 2) array([[0.06, 0.02], [0.01, 0. ]]) References ---------- .. [3] Benamou, J. D., Carlier, G., Cuturi, M., Nenna, L., & Peyré, G. (2015). Iterative Bregman projections for regularized transportation problems. SIAM Journal on Scientific Computing, 37(2), A1111-A1138. See Also -------- ot.partial.partial_wasserstein: exact Partial Wasserstein ot.partial.entropic_partial_wasserstein: classical entropic solver and dispatcher (this solver is also reachable through its ``method='sinkhorn_log'`` argument) """ a, b, M = list_to_array(a, b, M) nx = get_backend(a, b, M) dim_a, dim_b = M.shape Ldx = nx.zeros(dim_a, type_as=a) Ldy = nx.zeros(dim_b, type_as=b) if len(a) == 0: a = nx.ones(dim_a, type_as=a) / dim_a if len(b) == 0: b = nx.ones(dim_b, type_as=b) / dim_b La = nx.log(a) Lb = nx.log(b) if m is None: m = nx.min(nx.stack((nx.sum(a), nx.sum(b)))) * 1.0 if m < 0: raise ValueError("Problem infeasible. Parameter m should be greater than 0.") if m > nx.min(nx.stack((nx.sum(a), nx.sum(b)))): raise ValueError( "Problem infeasible. Parameter m should lower or" " equal than min(|a|_1, |b|_1)." ) log_e = {"err": []} LK = -M / reg LK = LK + nx.log(m) - nx.logsumexp(LK) err, cpt = 1, 0 Lq1 = nx.zeros(M.shape, type_as=M) Lq2 = nx.zeros(M.shape, type_as=M) Lq3 = nx.zeros(M.shape, type_as=M) while err > stopThr and cpt < numItermax: LKprev = LK LK = LK + Lq1 LK1 = nx.reshape(nx.minimum(La - nx.logsumexp(LK, 1), Ldx), (-1, 1)) + LK Lq1 = Lq1 + LKprev - LK1 LK1prev = LK1 LK1 = LK1 + Lq2 LK2 = LK1 + nx.reshape(nx.minimum(Lb - nx.logsumexp(LK1, 0), Ldy), (1, -1)) Lq2 = Lq2 + LK1prev - LK2 LK2prev = LK2 LK2 = LK2 + Lq3 LK = LK2 + nx.log(m) - nx.logsumexp(LK2) Lq3 = Lq3 + LK2prev - LK if nx.any(nx.isnan(LK)) or nx.any(nx.isinf(LK)): warnings.warn( f"Numerical errors at iteration {cpt} of entropic_partial_wasserstein_logscale", stacklevel=2, ) break if cpt % 10 == 0: err = nx.norm(LKprev - LK) if log: log_e["err"].append(err) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 11) print("{:5d}|{:8e}|".format(cpt, err)) cpt = cpt + 1 log_e["partial_w_dist"] = nx.sum(M * nx.exp(LK)) if log: return nx.exp(LK), log_e else: return nx.exp(LK) def gwgrad_partial(C1, C2, T): """Compute the GW gradient. Note: we can not use the trick in :ref:`[12] ` as the marginals may not sum to 1. .. note:: This function will be deprecated in a near future, please use `ot.gromov.gwggrad` instead. Parameters ---------- C1: array of shape (n_p,n_p) intra-source (P) cost matrix C2: array of shape (n_u,n_u) intra-target (U) cost matrix T : array of shape(n_p+nb_dummies, n_u) (default: None) Transport matrix Returns ------- numpy.array of shape (n_p+nb_dummies, n_u) gradient .. _references-gwgrad-partial: References ---------- .. [12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. """ warnings.warn( "This function will be deprecated in a near future, please use " "ot.gromov.gwggrad` instead.", stacklevel=2, ) cC1 = np.dot(C1**2 / 2, np.dot(T, np.ones(C2.shape[0]).reshape(-1, 1))) cC2 = np.dot(np.dot(np.ones(C1.shape[0]).reshape(1, -1), T), C2**2 / 2) constC = cC1 + cC2 A = -np.dot(C1, T).dot(C2.T) tens = constC + A return tens * 2 def gwloss_partial(C1, C2, T): """Compute the GW loss. .. note:: This function will be deprecated in a near future, please use `ot.gromov.gwloss` instead. Parameters ---------- C1: array of shape (n_p,n_p) intra-source (P) cost matrix C2: array of shape (n_u,n_u) intra-target (U) cost matrix T : array of shape(n_p+nb_dummies, n_u) (default: None) Transport matrix Returns ------- GW loss """ warnings.warn( "This function will be deprecated in a near future, please use " "ot.gromov.gwloss` instead.", stacklevel=2, ) g = gwgrad_partial(C1, C2, T) * 0.5 return np.sum(g * T) def partial_gromov_wasserstein( C1, C2, p, q, m=None, nb_dummies=1, G0=None, thres=1, numItermax=1000, tol=1e-7, log=False, verbose=False, **kwargs, ): r""" Solves the partial optimal transport problem and returns the OT plan The function considers the following problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F .. math:: s.t. \ \gamma \mathbf{1} &\leq \mathbf{a} \gamma^T \mathbf{1} &\leq \mathbf{b} \gamma &\geq 0 \mathbf{1}^T \gamma^T \mathbf{1} = m &\leq \min\{\|\mathbf{a}\|_1, \|\mathbf{b}\|_1\} where : - :math:`\mathbf{M}` is the metric cost matrix - :math:`\Omega` is the entropic regularization term, :math:`\Omega(\gamma) = \sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are the sample weights - `m` is the amount of mass to be transported The formulation of the problem has been proposed in :ref:`[29] ` .. note:: This function will be deprecated in a near future, please use `ot.gromov.partial_gromov_wasserstein` instead. Parameters ---------- C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space C2 : ndarray, shape (nt, nt) Metric costfr matrix in the target space p : ndarray, shape (ns,) Distribution in the source space q : ndarray, shape (nt,) Distribution in the target space m : float, optional Amount of mass to be transported (default: :math:`\min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\}`) nb_dummies : int, optional Number of dummy points to add (avoid instabilities in the EMD solver) G0 : ndarray, shape (ns, nt), optional Initialization of the transportation matrix thres : float, optional quantile of the gradient matrix to populate the cost matrix when 0 (default: 1) numItermax : int, optional Max number of iterations tol : float, optional tolerance for stopping iterations log : bool, optional return log if True verbose : bool, optional Print information along iterations **kwargs : dict parameters can be directly passed to the emd solver Returns ------- gamma : (dim_a, dim_b) ndarray Optimal transportation matrix for the given parameters log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> import scipy as sp >>> a = np.array([0.25] * 4) >>> b = np.array([0.25] * 4) >>> x = np.array([1,2,100,200]).reshape((-1,1)) >>> y = np.array([3,2,98,199]).reshape((-1,1)) >>> C1 = sp.spatial.distance.cdist(x, x) >>> C2 = sp.spatial.distance.cdist(y, y) >>> np.round(partial_gromov_wasserstein(C1, C2, a, b),2) array([[0. , 0.25, 0. , 0. ], [0.25, 0. , 0. , 0. ], [0. , 0. , 0.25, 0. ], [0. , 0. , 0. , 0.25]]) >>> np.round(partial_gromov_wasserstein(C1, C2, a, b, m=0.25),2) array([[0. , 0. , 0. , 0. ], [0. , 0. , 0. , 0. ], [0. , 0. , 0.25, 0. ], [0. , 0. , 0. , 0. ]]) .. _references-partial-gromov-wasserstein: References ---------- .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. """ warnings.warn( "This function will be deprecated in a near future, please use " "ot.gromov.partial_gromov_wasserstein` instead.", stacklevel=2, ) if m is None: m = np.min((np.sum(p), np.sum(q))) elif m < 0: raise ValueError("Problem infeasible. Parameter m should be greater than 0.") elif m > np.min((np.sum(p), np.sum(q))): raise ValueError( "Problem infeasible. Parameter m should lower or" " equal than min(|a|_1, |b|_1)." ) if G0 is None: G0 = ( np.outer(p, q) * m / (np.sum(p) * np.sum(q)) ) # make sure |G0|=m, G01_m\leq p, G0.T1_n\leq q. dim_G_extended = (len(p) + nb_dummies, len(q) + nb_dummies) q_extended = np.append(q, [(np.sum(p) - m) / nb_dummies] * nb_dummies) p_extended = np.append(p, [(np.sum(q) - m) / nb_dummies] * nb_dummies) cpt = 0 err = 1 if log: log = {"err": []} while err > tol and cpt < numItermax: Gprev = np.copy(G0) M = 0.5 * gwgrad_partial( C1, C2, G0 ) # rescaling the gradient with 0.5 for line-search while not changing Gc M_emd = np.zeros(dim_G_extended) M_emd[: len(p), : len(q)] = M M_emd[-nb_dummies:, -nb_dummies:] = np.max(M) * 1e2 M_emd = np.asarray(M_emd, dtype=np.float64) Gc, logemd = emd(p_extended, q_extended, M_emd, log=True, **kwargs) if logemd["warning"] is not None: raise ValueError( "Error in the EMD resolution: try to increase the" " number of dummy points" ) G0 = Gc[: len(p), : len(q)] if cpt % 10 == 0: # to speed up the computations err = np.linalg.norm(G0 - Gprev) if log: log["err"].append(err) if verbose: if cpt % 200 == 0: print( "{:5s}|{:12s}|{:12s}".format("It.", "Err", "Loss") + "\n" + "-" * 31 ) print("{:5d}|{:8e}|{:8e}".format(cpt, err, gwloss_partial(C1, C2, G0))) deltaG = G0 - Gprev a = gwloss_partial(C1, C2, deltaG) b = 2 * np.sum(M * deltaG) if b > 0: # due to numerical precision gamma = 0 cpt = numItermax elif a > 0: gamma = min(1, np.divide(-b, 2.0 * a)) else: if (a + b) < 0: gamma = 1 else: gamma = 0 cpt = numItermax G0 = Gprev + gamma * deltaG cpt += 1 if log: log["partial_gw_dist"] = gwloss_partial(C1, C2, G0) return G0[: len(p), : len(q)], log else: return G0[: len(p), : len(q)] def partial_gromov_wasserstein2( C1, C2, p, q, m=None, nb_dummies=1, G0=None, thres=1, numItermax=1000, tol=1e-7, log=False, verbose=False, **kwargs, ): r""" Solves the partial optimal transport problem and returns the partial Gromov-Wasserstein discrepancy The function considers the following problem: .. math:: GW = \min_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F .. math:: s.t. \ \gamma \mathbf{1} &\leq \mathbf{a} \gamma^T \mathbf{1} &\leq \mathbf{b} \gamma &\geq 0 \mathbf{1}^T \gamma^T \mathbf{1} = m &\leq \min\{\|\mathbf{a}\|_1, \|\mathbf{b}\|_1\} where : - :math:`\mathbf{M}` is the metric cost matrix - :math:`\Omega` is the entropic regularization term, :math:`\Omega(\gamma) = \sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are the sample weights - `m` is the amount of mass to be transported The formulation of the problem has been proposed in :ref:`[29] ` .. note:: This function will be deprecated in a near future, please use `ot.gromov.partial_gromov_wasserstein2` instead. Parameters ---------- C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space C2 : ndarray, shape (nt, nt) Metric cost matrix in the target space p : ndarray, shape (ns,) Distribution in the source space q : ndarray, shape (nt,) Distribution in the target space m : float, optional Amount of mass to be transported (default: :math:`\min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\}`) nb_dummies : int, optional Number of dummy points to add (avoid instabilities in the EMD solver) G0 : ndarray, shape (ns, nt), optional Initialization of the transportation matrix thres : float, optional quantile of the gradient matrix to populate the cost matrix when 0 (default: 1) numItermax : int, optional Max number of iterations tol : float, optional tolerance for stopping iterations log : bool, optional return log if True verbose : bool, optional Print information along iterations **kwargs : dict parameters can be directly passed to the emd solver .. warning:: When dealing with a large number of points, the EMD solver may face some instabilities, especially when the mass associated to the dummy point is large. To avoid them, increase the number of dummy points (allows a smoother repartition of the mass over the points). Returns ------- partial_gw_dist : float partial GW discrepancy log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> import scipy as sp >>> a = np.array([0.25] * 4) >>> b = np.array([0.25] * 4) >>> x = np.array([1,2,100,200]).reshape((-1,1)) >>> y = np.array([3,2,98,199]).reshape((-1,1)) >>> C1 = sp.spatial.distance.cdist(x, x) >>> C2 = sp.spatial.distance.cdist(y, y) >>> np.round(partial_gromov_wasserstein2(C1, C2, a, b),2) 1.69 >>> np.round(partial_gromov_wasserstein2(C1, C2, a, b, m=0.25),2) 0.0 .. _references-partial-gromov-wasserstein2: References ---------- .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. """ warnings.warn( "This function will be deprecated in a near future, please use " "ot.gromov.partial_gromov_wasserstein2` instead.", stacklevel=2, ) partial_gw, log_gw = partial_gromov_wasserstein( C1, C2, p, q, m, nb_dummies, G0, thres, numItermax, tol, True, verbose, **kwargs ) log_gw["T"] = partial_gw if log: return log_gw["partial_gw_dist"], log_gw else: return log_gw["partial_gw_dist"] def entropic_partial_gromov_wasserstein( C1, C2, p, q, reg, m=None, G0=None, numItermax=1000, tol=1e-7, log=False, verbose=False, ): r""" Returns the partial Gromov-Wasserstein transport between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})` The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_{\gamma} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l})\cdot \gamma_{i,j}\cdot\gamma_{k,l} + \mathrm{reg} \cdot\Omega(\gamma) .. math:: s.t. \ \gamma &\geq 0 \gamma \mathbf{1} &\leq \mathbf{a} \gamma^T \mathbf{1} &\leq \mathbf{b} \mathbf{1}^T \gamma^T \mathbf{1} = m &\leq \min\{\|\mathbf{a}\|_1, \|\mathbf{b}\|_1\} where : - :math:`\mathbf{C_1}` is the metric cost matrix in the source space - :math:`\mathbf{C_2}` is the metric cost matrix in the target space - :math:`\mathbf{p}` and :math:`\mathbf{q}` are the sample weights - `L`: quadratic loss function - :math:`\Omega` is the entropic regularization term, :math:`\Omega=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - `m` is the amount of mass to be transported The formulation of the GW problem has been proposed in :ref:`[12] ` and the partial GW in :ref:`[29] ` .. note:: This function will be deprecated in a near future, please use `ot.gromov.entropic_partial_gromov_wasserstein` instead. Parameters ---------- C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space C2 : ndarray, shape (nt, nt) Metric cost matrix in the target space p : ndarray, shape (ns,) Distribution in the source space q : ndarray, shape (nt,) Distribution in the target space reg: float entropic regularization parameter m : float, optional Amount of mass to be transported (default: :math:`\min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\}`) G0 : ndarray, shape (ns, nt), optional Initialization of the transportation matrix numItermax : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) log : bool, optional return log if True verbose : bool, optional Print information along iterations Examples -------- >>> import ot >>> import scipy as sp >>> a = np.array([0.25] * 4) >>> b = np.array([0.25] * 4) >>> x = np.array([1,2,100,200]).reshape((-1,1)) >>> y = np.array([3,2,98,199]).reshape((-1,1)) >>> C1 = sp.spatial.distance.cdist(x, x) >>> C2 = sp.spatial.distance.cdist(y, y) >>> np.round(entropic_partial_gromov_wasserstein(C1, C2, a, b, 50), 2) array([[0.12, 0.13, 0. , 0. ], [0.13, 0.12, 0. , 0. ], [0. , 0. , 0.25, 0. ], [0. , 0. , 0. , 0.25]]) >>> np.round(entropic_partial_gromov_wasserstein(C1, C2, a, b, 50,0.25), 2) array([[0.02, 0.03, 0. , 0.03], [0.03, 0.03, 0. , 0.03], [0. , 0. , 0.03, 0. ], [0.02, 0.02, 0. , 0.03]]) Returns ------- gamma : ndarray, shape (dim_a, dim_b) Optimal transportation matrix for the given parameters log : dict log dictionary returned only if `log` is `True` .. _references-entropic-partial-gromov-wasserstein: References ---------- .. [12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. See Also -------- ot.partial.partial_gromov_wasserstein: exact Partial Gromov-Wasserstein """ warnings.warn( "This function will be deprecated in a near future, please use " "ot.gromov.entropic_partial_gromov_wasserstein` instead.", stacklevel=2, ) if G0 is None: G0 = np.outer(p, q) if m is None: m = np.min((np.sum(p), np.sum(q))) elif m < 0: raise ValueError("Problem infeasible. Parameter m should be greater than 0.") elif m > np.min((np.sum(p), np.sum(q))): raise ValueError( "Problem infeasible. Parameter m should lower or" " equal than min(|a|_1, |b|_1)." ) cpt = 0 err = 1 loge = {"err": []} while err > tol and cpt < numItermax: Gprev = G0 M_entr = gwgrad_partial(C1, C2, G0) G0 = entropic_partial_wasserstein(p, q, M_entr, reg, m) if cpt % 10 == 0: # to speed up the computations err = np.linalg.norm(G0 - Gprev) if log: loge["err"].append(err) if verbose: if cpt % 200 == 0: print( "{:5s}|{:12s}|{:12s}".format("It.", "Err", "Loss") + "\n" + "-" * 31 ) print("{:5d}|{:8e}|{:8e}".format(cpt, err, gwloss_partial(C1, C2, G0))) cpt += 1 if log: loge["partial_gw_dist"] = gwloss_partial(C1, C2, G0) return G0, loge else: return G0 def entropic_partial_gromov_wasserstein2( C1, C2, p, q, reg, m=None, G0=None, numItermax=1000, tol=1e-7, log=False, verbose=False, ): r""" Returns the partial Gromov-Wasserstein discrepancy between :math:`(\mathbf{C_1}, \mathbf{p})` and :math:`(\mathbf{C_2}, \mathbf{q})` The function solves the following optimization problem: .. math:: GW = \min_{\gamma} \quad \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l})\cdot \gamma_{i,j}\cdot\gamma_{k,l} + \mathrm{reg} \cdot\Omega(\gamma) .. math:: s.t. \ \gamma &\geq 0 \gamma \mathbf{1} &\leq \mathbf{a} \gamma^T \mathbf{1} &\leq \mathbf{b} \mathbf{1}^T \gamma^T \mathbf{1} = m &\leq \min\{\|\mathbf{a}\|_1, \|\mathbf{b}\|_1\} where : - :math:`\mathbf{C_1}` is the metric cost matrix in the source space - :math:`\mathbf{C_2}` is the metric cost matrix in the target space - :math:`\mathbf{p}` and :math:`\mathbf{q}` are the sample weights - `L` : quadratic loss function - :math:`\Omega` is the entropic regularization term, :math:`\Omega=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - `m` is the amount of mass to be transported The formulation of the GW problem has been proposed in :ref:`[12] ` and the partial GW in :ref:`[29] ` .. note:: This function will be deprecated in a near future, please use `ot.gromov.entropic_partial_gromov_wasserstein2` instead. Parameters ---------- C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space C2 : ndarray, shape (nt, nt) Metric cost matrix in the target space p : ndarray, shape (ns,) Distribution in the source space q : ndarray, shape (nt,) Distribution in the target space reg: float entropic regularization parameter m : float, optional Amount of mass to be transported (default: :math:`\min\{\|\mathbf{p}\|_1, \|\mathbf{q}\|_1\}`) G0 : ndarray, shape (ns, nt), optional Initialization of the transportation matrix numItermax : int, optional Max number of iterations tol : float, optional Stop threshold on error (>0) log : bool, optional return log if True verbose : bool, optional Print information along iterations Returns ------- partial_gw_dist: float Gromov-Wasserstein distance log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> import scipy as sp >>> a = np.array([0.25] * 4) >>> b = np.array([0.25] * 4) >>> x = np.array([1,2,100,200]).reshape((-1,1)) >>> y = np.array([3,2,98,199]).reshape((-1,1)) >>> C1 = sp.spatial.distance.cdist(x, x) >>> C2 = sp.spatial.distance.cdist(y, y) >>> np.round(entropic_partial_gromov_wasserstein2(C1, C2, a, b,50), 2) 1.87 .. _references-entropic-partial-gromov-wasserstein2: References ---------- .. [12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). "Partial Optimal Transport with Applications on Positive-Unlabeled Learning". NeurIPS. """ warnings.warn( "This function will be deprecated in a near future, please use " "ot.gromov.entropic_partial_gromov_wasserstein2` instead.", stacklevel=2, ) partial_gw, log_gw = entropic_partial_gromov_wasserstein( C1, C2, p, q, reg, m, G0, numItermax, tol, True, verbose ) log_gw["T"] = partial_gw if log: return log_gw["partial_gw_dist"], log_gw else: return log_gw["partial_gw_dist"] def partial_wasserstein_1d(x_a, x_b, n_transported_samples=None, p=1): r"""Solves the partial Wasserstein distance problem between 1d measures and returns all required information to reconstruct the solutions to all partial problems for any amount of transported samples up to `n_transported_samples`. The function considers the following problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \sum_{ij} \gamma_{ij} \|x_a[i] - x_b[j]\|^p_p .. math:: s.t. \ \gamma \mathbf{1} &\leq \mathbf{1} \gamma^T \mathbf{1} &\leq \mathbf{1} \gamma &\geq 0 \mathbf{1}^T \gamma^T \mathbf{1} = n_\text{transported samples} Parameters ---------- x_a : ndarray of float64, shape (n,) or (n, 1) Source dirac locations (on the real line) x_b : ndarray of float64, shape (m,) or (m, 1) Target dirac locations (on the real line) n_transported_samples : int, optional (default: min(n, m)) number of samples to be transported p : float, optional power of the metric (default: 1) log : bool, optional record log if True Returns ------- indices_x : np.ndarray of shape (min(n, m, n_transported_samples), ) Indices of elements from the x distribution to be included in the partial solutions Order of appearance in this array indicates order of inclusion in the solution indices_y : np.ndarray of shape (min(n, m, n_transported_samples), ) Indices of elements from the y distribution to be included in the partial solutions Order of appearance in this array indicates order of inclusion in the solution list_marginal_costs : list of length min(n, m, n_transported_samples) List of marginal costs associated to the intermediate partial problems `np.cumsum(list_marginal_costs)` gives the corresponding total costs for intermediate partial problems Examples -------- >>> x = np.array([5., -2., 4.]) >>> y = np.array([-1., 1., 3.]) >>> ind_x, ind_y, marginal_costs = partial_wasserstein_1d(x, y, n_transported_samples=2) >>> ind_x array([1, 2]) >>> ind_y array([0, 2]) >>> np.sum(marginal_costs) 2.0 >>> np.sum(np.abs(np.sort(x[ind_x]) - np.sort(y[ind_y]))) 2.0 References ---------- .. [76] Chapel, L., Tavenard, R. (2025). "One for all and all for one: Efficient computation of partial Wasserstein distances on the line". ICLR. See Also -------- ot.partial.partial_wasserstein: Partial Wasserstein in dimension higher than 1 """ x_a, x_b = list_to_array(x_a, x_b) nx = get_backend(x_a, x_b) if n_transported_samples is None: n_transported_samples = min(x_a.shape[0], x_b.shape[0]) assert ( x_a.ndim == 1 or x_a.ndim == 2 and x_a.shape[1] == 1 ), "partial_wasserstein_1d should only be used with monodimensional data" assert ( x_b.ndim == 1 or x_b.ndim == 2 and x_b.shape[1] == 1 ), "partial_wasserstein_1d should only be used with monodimensional data" x_a_1d = nx.reshape(x_a, (-1,)) x_b_1d = nx.reshape(x_b, (-1,)) ind_x, ind_y, marginal_costs = partial_wasserstein_1d_cy( nx.to_numpy(x_a_1d).astype(np.float64), nx.to_numpy(x_b_1d).astype(np.float64), max_iter=n_transported_samples, p=p, ) return ind_x, ind_y, marginal_costs python-pot-0.9.7+dfsg/ot/plot.py000066400000000000000000000217061525521106200165460ustar00rootroot00000000000000""" Functions for plotting OT matrices .. warning:: Note that by default the module is not import in :mod:`ot`. In order to use it you need to explicitly import :mod:`ot.plot` """ # Author: Remi Flamary # # License: MIT License import numpy as np import matplotlib.pylab as pl from matplotlib import gridspec from . import backend from scipy.sparse import issparse, coo_array def plot1D_mat( a, b, M, title="", plot_style="yx", a_label="", b_label="", color_source="b", color_target="r", coupling_cmap="gray_r", ): r"""Plot matrix :math:`\mathbf{M}` with the source and target 1D distributions. Creates a subplot with the source distribution :math:`\mathbf{a}` and target distribution :math:`\mathbf{b}`. In 'yx' mode (default), the source is on the left and the target on the top, and in 'xy' mode, source on the bottom (upside down) and the target on the left. The matrix :math:`\mathbf{M}` is shown in between. Parameters ---------- a : ndarray, shape (na,) Source distribution b : ndarray, shape (nb,) Target distribution M : ndarray, shape (na, nb) Matrix to plot title : str, optional Title of the plot plot_style : str, optional Style of the plot, 'yx' or 'xy'. 'yx' places the source on the left and the target on the top, 'xy' places the source on the bottom (upside down) and the target on the left. a_label : str, optional Label for source distribution b_label : str, optional Label for target distribution color_source : str, optional Color of the source distribution color_target : str, optional Color of the target distribution coupling_cmap : str, optional Colormap for the coupling matrix Returns ------- ax1 : source plot ax ax2 : target plot ax ax3 : coupling plot ax See Also -------- :func:`rescale_for_imshow_plot` """ assert plot_style in ["yx", "xy"], "plot_style should be 'yx' or 'xy'" na, nb = M.shape gs = gridspec.GridSpec( 3, 3, height_ratios=[1, 1, 1], width_ratios=[1, 1, 1], hspace=0, wspace=0 ) xa = np.arange(na) xb = np.arange(nb) # helper function for code factorisation def _set_ticks_and_spines(ax, empty_ticks=True, visible_spines=False): if empty_ticks: ax.set_xticks(()) ax.set_yticks(()) ax.spines["top"].set_visible(visible_spines) ax.spines["right"].set_visible(visible_spines) ax.spines["bottom"].set_visible(visible_spines) ax.spines["left"].set_visible(visible_spines) if plot_style == "xy": # horizontal source on the bottom, flipped vertically ax1 = pl.subplot(gs[2, 1:]) ax1.plot(xa, np.max(a) - a, color=color_source, linewidth=2) ax1.fill( xa, np.max(a) - a, np.max(a) * np.ones_like(a), color=color_source, alpha=0.5, ) ax1.set_title(a_label, y=-0.15) # vertical target on the left ax2 = pl.subplot(gs[0:2, 0]) ax2.plot(b, xb, color=color_target, linewidth=2) ax2.fill(b, xb, color=color_target, alpha=0.5) ax2.invert_xaxis() ax2.invert_yaxis() ax2.set_title(b_label) _set_ticks_and_spines(ax1, empty_ticks=True, visible_spines=False) _set_ticks_and_spines(ax2, empty_ticks=True, visible_spines=False) # coupling matrix in the middle ax3 = pl.subplot(gs[0:2, 1:], sharey=ax2, sharex=ax1) ax3.imshow(M.T, interpolation="nearest", origin="lower", cmap=coupling_cmap) ax3.set_title(title) _set_ticks_and_spines(ax3, empty_ticks=False, visible_spines=True) pl.subplots_adjust(hspace=0, wspace=0) return ax1, ax2, ax3 else: # plot_style == 'yx' # vertical source on the left ax1 = pl.subplot(gs[1:, 0]) ax1.plot(a, xa, color=color_source, linewidth=2) ax1.fill(a, xa, color=color_source, alpha=0.5) ax1.invert_xaxis() ax1.set_title(a_label) # horizontal target on the top ax2 = pl.subplot(gs[0, 1:]) ax2.plot(xb, b, color=color_target, linewidth=2) ax2.fill(xb, b, color=color_target, alpha=0.5) ax2.set_title(b_label) _set_ticks_and_spines(ax1, empty_ticks=True, visible_spines=False) _set_ticks_and_spines(ax2, empty_ticks=True, visible_spines=False) # coupling matrix in the middle ax3 = pl.subplot(gs[1:, 1:], sharey=ax1, sharex=ax2) ax3.imshow(M, interpolation="nearest", cmap=coupling_cmap) # Set title below matrix plot ax3.text( 0.5, -0.025, title, ha="center", va="top", transform=ax3.transAxes, fontsize="large", ) _set_ticks_and_spines(ax3, empty_ticks=False, visible_spines=True) pl.subplots_adjust(hspace=0, wspace=0) return ax1, ax2, ax3 def rescale_for_imshow_plot(x, y, n, m=None, a_y=None, b_y=None): r""" Gives arrays xr, yr that can be plotted over an (n, m) imshow plot (in 'xy' coordinates). If `a_y` or `b_y` is provided, y is sliced over its indices such that y stays in [ay, by]. Parameters ---------- x : ndarray, shape (nx,) y : ndarray, shape (nx,) n : int x-axis size of the imshow plot on which to plot (x, y) m : int, optional y-axis size of the imshow plot, defaults to n a_y : float, optional Lower bound for y b_y : float, optional Upper bound for y Returns ------- xr : ndarray, shape (nx,) Rescaled x values (due to slicing, may have less elements than x) yr : ndarray, shape (nx,) Rescaled y values (due to slicing, may have less elements than y) See Also -------- :func:`plot1D_mat` """ # slice over the y values that are in the y range a_x, b_x = np.min(x), np.max(x) assert x.shape[0] == y.shape[0], "x and y arrays should have the same size" if a_y is None: a_y = np.min(y) if b_y is None: b_y = np.max(y) if m is None: m = n idx = (y >= a_y) & (y <= b_y) x_rescaled = (x[idx] - a_x) * (n - 1) / (b_x - a_x) y_rescaled = (y[idx] - a_y) * (m - 1) / (b_y - a_y) return x_rescaled, y_rescaled def plot2D_samples_mat(xs, xt, G, thr=1e-8, **kwargs): r"""Plot matrix :math:`\mathbf{G}` in 2D with lines using alpha values Plot lines between source and target 2D samples with a color proportional to the value of the matrix :math:`\mathbf{G}` between samples. Supports both dense and sparse matrices. For sparse matrices, automatically detects the format and efficiently iterates only over non-zero entries. Parameters ---------- xs : ndarray, shape (ns,2) Source samples positions xt : ndarray, shape (nt,2) Target samples positions G : ndarray or sparse matrix, shape (ns,nt) OT matrix (dense array, scipy.sparse matrix, or backend array) thr : float, optional threshold above which the line is drawn **kwargs : dict parameters given to the plot functions (default color is black if nothing given) """ if ("color" not in kwargs) and ("c" not in kwargs): kwargs["color"] = "k" scale = kwargs.pop("alpha", 1) # Convert to numpy/scipy format for plotting try: nx = backend.get_backend(G) if nx.issparse(G): # Backend sparse -> extract as numpy arrays for COO format rows, cols, data = nx.sparse_coo_data(G) rows = nx.to_numpy(rows).astype(int) cols = nx.to_numpy(cols).astype(int) data = nx.to_numpy(data) is_sparse = True else: # Backend dense -> convert to numpy G = nx.to_numpy(G) is_sparse = False except (ValueError, AttributeError): # Not a backend array, check if scipy.sparse is_sparse = issparse(G) if is_sparse: G_coo = G if isinstance(G, coo_array) else G.tocoo() rows, cols, data = G_coo.row, G_coo.col, G_coo.data if is_sparse: # Sparse: iterate over non-zero entries only mx = data.max() if len(data) > 0 else 1.0 for i, j, val in zip(rows, cols, data): if val / mx > thr: pl.plot( [xs[i, 0], xt[j, 0]], [xs[i, 1], xt[j, 1]], alpha=val / mx * scale, **kwargs, ) else: # Dense: iterate over all entries mx = np.max(G) for i in range(xs.shape[0]): for j in range(xt.shape[0]): if G[i, j] / mx > thr: pl.plot( [xs[i, 0], xt[j, 0]], [xs[i, 1], xt[j, 1]], alpha=G[i, j] / mx * scale, **kwargs, ) python-pot-0.9.7+dfsg/ot/regpath.py000066400000000000000000000773111525521106200172250ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Regularization path OT solvers """ # Author: Haoran Wu # License: MIT License import numpy as np import scipy.sparse as sp def recast_ot_as_lasso(a, b, C): r"""This function recasts the l2-penalized UOT problem as a Lasso problem. Recall the l2-penalized UOT problem defined in :ref:`[41] ` .. math:: \text{UOT}_{\lambda} = \min_T + \lambda \|T 1_m - \mathbf{a}\|_2^2 + \lambda \|T^T 1_n - \mathbf{b}\|_2^2 s.t. T \geq 0 where : - :math:`C` is the cost matrix - :math:`\lambda` is the l2-regularization parameter - :math:`\mathbf{a}` and :math:`\mathbf{b}` are the source and target \ distributions - :math:`T` is the transport plan to optimize The problem above can be reformulated as a non-negative penalized linear regression problem, particularly Lasso .. math:: \text{UOT2}_{\lambda} = \min_{\mathbf{t}} \gamma \mathbf{c}^T \mathbf{t} + 0.5 * \|H \mathbf{t} - \mathbf{y}\|_2^2 s.t. \mathbf{t} \geq 0 where : - :math:`\mathbf{c}` is the flattened version of the cost matrix :math:`C` - :math:`\mathbf{y}` is the concatenation of vectors :math:`\mathbf{a}` \ and :math:`\mathbf{b}` - :math:`H` is a metric matrix, see :ref:`[41] ` for \ the design of :math:`H`. The matrix product :math:`H\mathbf{t}` \ computes both the source marginal and the target marginals. - :math:`\mathbf{t}` is the flattened version of the transport plan \ :math:`T` Parameters ---------- a : np.ndarray (dim_a,) Histogram of dimension dim_a b : np.ndarray (dim_b,) Histogram of dimension dim_b C : np.ndarray, shape (dim_a, dim_b) Cost matrix Returns ------- H : np.ndarray (dim_a+dim_b, dim_a*dim_b) Design matrix that contains only 0 and 1 y : np.ndarray (ns + nt, ) Concatenation of histograms :math:`\mathbf{a}` and :math:`\mathbf{b}` c : np.ndarray (ns * nt, ) Flattened array of the cost matrix Examples -------- >>> import ot >>> a = np.array([0.2, 0.3, 0.5]) >>> b = np.array([0.1, 0.9]) >>> C = np.array([[16., 25.], [28., 16.], [40., 36.]]) >>> H, y, c = ot.regpath.recast_ot_as_lasso(a, b, C) >>> H.toarray() array([[1., 1., 0., 0., 0., 0.], [0., 0., 1., 1., 0., 0.], [0., 0., 0., 0., 1., 1.], [1., 0., 1., 0., 1., 0.], [0., 1., 0., 1., 0., 1.]]) >>> y array([0.2, 0.3, 0.5, 0.1, 0.9]) >>> c array([16., 25., 28., 16., 40., 36.]) References ---------- .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. """ dim_a = np.shape(a)[0] dim_b = np.shape(b)[0] y = np.concatenate((a, b)) c = C.flatten() jHa = np.arange(dim_a * dim_b) iHa = np.repeat(np.arange(dim_a), dim_b) jHb = np.arange(dim_a * dim_b) iHb = np.tile(np.arange(dim_b), dim_a) + dim_a j = np.concatenate((jHa, jHb)) i = np.concatenate((iHa, iHb)) H = sp.csc_matrix( (np.ones(dim_a * dim_b * 2), (i, j)), shape=(dim_a + dim_b, dim_a * dim_b) ) return H, y, c def recast_semi_relaxed_as_lasso(a, b, C): r"""This function recasts the semi-relaxed l2-UOT problem as Lasso problem. .. math:: \text{semi-relaxed UOT} = \min_T + \lambda \|T 1_m - \mathbf{a}\|_2^2 s.t. T^T 1_n = \mathbf{b} \mathbf{t} \geq 0 where : - :math:`C` is the metric cost matrix - :math:`\lambda` is the l2-regularization parameter - :math:`\mathbf{a}` and :math:`\mathbf{b}` are the source and target \ distributions - :math:`T` is the transport plan to optimize The problem above can be reformulated as follows .. math:: \text{semi-relaxed UOT2} = \min_t \gamma \mathbf{c}^T t + 0.5 * \|H_r \mathbf{t} - \mathbf{a}\|_2^2 s.t. H_c \mathbf{t} = \mathbf{b} \mathbf{t} \geq 0 where : - :math:`\mathbf{c}` is flattened version of the cost matrix :math:`C` - :math:`\gamma = 1/\lambda` is the l2-regularization parameter - :math:`H_r` is a metric matrix which computes the sum along the \ rows of the transport plan :math:`T` - :math:`H_c` is a metric matrix which computes the sum along the \ columns of the transport plan :math:`T` - :math:`\mathbf{t}` is the flattened version of :math:`T` Parameters ---------- a : np.ndarray (dim_a,) Histogram of dimension dim_a b : np.ndarray (dim_b,) Histogram of dimension dim_b C : np.ndarray, shape (dim_a, dim_b) Cost matrix Returns ------- Hr : np.ndarray (dim_a, dim_a * dim_b) Auxiliary matrix constituted by 0 and 1, which computes the sum along the rows of transport plan :math:`T` Hc : np.ndarray (dim_b, dim_a * dim_b) Auxiliary matrix constituted by 0 and 1, which computes the sum along the columns of transport plan :math:`T` c : np.ndarray (ns * nt, ) Flattened array of the cost matrix Examples -------- >>> import ot >>> a = np.array([0.2, 0.3, 0.5]) >>> b = np.array([0.1, 0.9]) >>> C = np.array([[16., 25.], [28., 16.], [40., 36.]]) >>> Hr,Hc,c = ot.regpath.recast_semi_relaxed_as_lasso(a, b, C) >>> Hr.toarray() array([[1., 1., 0., 0., 0., 0.], [0., 0., 1., 1., 0., 0.], [0., 0., 0., 0., 1., 1.]]) >>> Hc.toarray() array([[1., 0., 1., 0., 1., 0.], [0., 1., 0., 1., 0., 1.]]) >>> c array([16., 25., 28., 16., 40., 36.]) """ dim_a = np.shape(a)[0] dim_b = np.shape(b)[0] c = C.flatten() jHr = np.arange(dim_a * dim_b) iHr = np.repeat(np.arange(dim_a), dim_b) jHc = np.arange(dim_a * dim_b) iHc = np.tile(np.arange(dim_b), dim_a) Hr = sp.csc_matrix( (np.ones(dim_a * dim_b), (iHr, jHr)), shape=(dim_a, dim_a * dim_b) ) Hc = sp.csc_matrix( (np.ones(dim_a * dim_b), (iHc, jHc)), shape=(dim_b, dim_a * dim_b) ) return Hr, Hc, c def ot_next_gamma(phi, delta, HtH, Hty, c, active_index, current_gamma): r""" This function computes the next value of gamma if a variable is added in the next iteration of the regularization path. We look for the largest value of gamma such that the gradient of an inactive variable vanishes .. math:: \max_{i \in \bar{A}} \frac{\mathbf{h}_i^T(H_A \phi - \mathbf{y})} {\mathbf{h}_i^T H_A \delta - \mathbf{c}_i} where : - A is the current active set - :math:`\mathbf{h}_i` is the :math:`i` th column of the design \ matrix :math:`{H}` - :math:`{H}_A` is the sub-matrix constructed by the columns of \ :math:`{H}` whose indices belong to the active set A - :math:`\mathbf{c}_i` is the :math:`i` th element of the cost vector \ :math:`\mathbf{c}` - :math:`\mathbf{y}` is the concatenation of the source and target \ distributions - :math:`\phi` is the intercept of the solutions at the current iteration - :math:`\delta` is the slope of the solutions at the current iteration Parameters ---------- phi : np.ndarray (size(A), ) Intercept of the solutions at the current iteration delta : np.ndarray (size(A), ) Slope of the solutions at the current iteration HtH : np.ndarray (dim_a * dim_b, dim_a * dim_b) Matrix product of :math:`{H}^T {H}` Hty : np.ndarray (dim_a + dim_b, ) Matrix product of :math:`{H}^T \mathbf{y}` c: np.ndarray (dim_a * dim_b, ) Flattened array of the cost matrix :math:`{C}` active_index : list Indices of active variables current_gamma : float Value of the regularization parameter at the beginning of the current \ iteration Returns ------- next_gamma : float Value of gamma if a variable is added to active set in next iteration next_active_index : int Index of variable to be activated References ---------- .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. """ M = (HtH[:, active_index].dot(phi) - Hty) / ( HtH[:, active_index].dot(delta) - c + 1e-16 ) M[active_index] = 0 M[M > (current_gamma - 1e-10 * current_gamma)] = 0 return np.max(M), np.argmax(M) def semi_relaxed_next_gamma( phi, delta, phi_u, delta_u, HrHr, Hc, Hra, c, active_index, current_gamma ): r""" This function computes the next value of gamma when a variable is active in the regularization path of semi-relaxed UOT. By taking the Lagrangian form of the problem, we obtain a similar update as the two-sided relaxed UOT .. math:: \max_{i \in \bar{A}} \frac{\mathbf{h}_{ri}^T(H_{rA} \phi - \mathbf{a}) + \mathbf{h}_{c i}^T\phi_u}{\mathbf{h}_{r i}^T H_{r A} \delta + \ \mathbf{h}_{c i} \delta_u - \mathbf{c}_i} where : - A is the current active set - :math:`\mathbf{h}_{r i}` is the ith column of the matrix :math:`H_r` - :math:`\mathbf{h}_{c i}` is the ith column of the matrix :math:`H_c` - :math:`H_{r A}` is the sub-matrix constructed by the columns of \ :math:`H_r` whose indices belong to the active set A - :math:`\mathbf{c}_i` is the :math:`i` th element of cost vector \ :math:`\mathbf{c}` - :math:`\phi` is the intercept of the solutions in current iteration - :math:`\delta` is the slope of the solutions in current iteration - :math:`\phi_u` is the intercept of Lagrange parameter at the \ current iteration - :math:`\delta_u` is the slope of Lagrange parameter at the \ current iteration Parameters ---------- phi : np.ndarray (size(A), ) Intercept of the solutions at the current iteration delta : np.ndarray (size(A), ) Slope of the solutions at the current iteration phi_u : np.ndarray (dim_b, ) Intercept of the Lagrange parameter at the current iteration delta_u : np.ndarray (dim_b, ) Slope of the Lagrange parameter at the current iteration HrHr : np.ndarray (dim_a * dim_b, dim_a * dim_b) Matrix product of :math:`H_r^T H_r` Hc : np.ndarray (dim_b, dim_a * dim_b) Matrix that computes the sum along the columns of the transport plan \ :math:`T` Hra : np.ndarray (dim_a * dim_b, ) Matrix product of :math:`H_r^T \mathbf{a}` c: np.ndarray (dim_a * dim_b, ) Flattened array of cost matrix :math:`C` active_index : list Indices of active variables current_gamma : float Value of regularization coefficient at the start of current iteration Returns ------- next_gamma : float Value of gamma if a variable is added to active set in next iteration next_active_index : int Index of variable to be activated References ---------- .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. """ M = (HrHr[:, active_index].dot(phi) - Hra + Hc.T.dot(phi_u)) / ( HrHr[:, active_index].dot(delta) - c + Hc.T.dot(delta_u) + 1e-16 ) M[active_index] = 0 M[M > (current_gamma - 1e-10 * current_gamma)] = 0 return np.max(M), np.argmax(M) def compute_next_removal(phi, delta, current_gamma): r""" This function computes the next gamma value if a variable is removed at the next iteration of the regularization path. We look for the largest value of the regularization parameter such that an element of the current solution vanishes .. math:: \max_{j \in A} \frac{\phi_j}{\delta_j} where : - A is the current active set - :math:`\phi_j` is the :math:`j` th element of the intercept of the \ current solution - :math:`\delta_j` is the :math:`j` th element of the slope of the \ current solution Parameters ---------- phi : ndarray, shape (size(A), ) Intercept of the solution at the current iteration delta : ndarray, shape (size(A), ) Slope of the solution at the current iteration current_gamma : float Value of the regularization parameter at the beginning of the \ current iteration Returns ------- next_removal_gamma : float Gamma value if a variable is removed at the next iteration next_removal_index : int Index of the variable to be removed at the next iteration .. _references-regpath: References ---------- .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. """ r_candidate = phi / (delta - 1e-16) r_candidate[r_candidate >= (1 - 1e-8) * current_gamma] = 0 return np.max(r_candidate), np.argmax(r_candidate) def complement_schur(M_current, b, d, id_pop): r""" This function computes the inverse of the design matrix in the \ regularization path using the Schur complement. Two cases may arise: Case 1: one variable is added to the active set .. math:: M_{k+1}^{-1} = \begin{bmatrix} M_{k}^{-1} + s^{-1} M_{k}^{-1} \mathbf{b} \mathbf{b}^T M_{k}^{-1} \ & - M_{k}^{-1} \mathbf{b} s^{-1} \\ - s^{-1} \mathbf{b}^T M_{k}^{-1} & s^{-1} \end{bmatrix} where : - :math:`M_k^{-1}` is the inverse of the design matrix :math:`H_A^tH_A` \ of the previous iteration - :math:`\mathbf{b}` is the last column of :math:`M_{k}` - :math:`s` is the Schur complement, given by \ :math:`s = \mathbf{d} - \mathbf{b}^T M_{k}^{-1} \mathbf{b}` Case 2: one variable is removed from the active set. .. math:: M_{k+1}^{-1} = M^{-1}_{k \backslash q} - \frac{r_{-q,q} r^{T}_{-q,q}}{r_{q,q}} where : - :math:`q` is the index of column and row to delete - :math:`M^{-1}_{k \backslash q}` is the previous inverse matrix deprived \ of the :math:`q` th column and :math:`q` th row - :math:`r_{-q,q}` is the :math:`q` th column of :math:`M^{-1}_{k}` \ without the :math:`q` th element - :math:`r_{q, q}` is the element of :math:`q` th column and :math:`q` th \ row in :math:`M^{-1}_{k}` Parameters ---------- M_current : ndarray, shape (size(A)-1, size(A)-1) Inverse matrix of :math:`H_A^tH_A` at the previous iteration, with \ size(A) the size of the active set b : ndarray, shape (size(A)-1, ) None for case 2 (removal), last column of :math:`M_{k}` for case 1 \ (addition) d : float should be equal to 2 when UOT and 1 for the semi-relaxed OT id_pop : int Index of the variable to be removed, equal to -1 if no variable is deleted at the current iteration Returns ------- M : ndarray, shape (size(A), size(A)) Inverse matrix of :math:`H_A^tH_A` of the current iteration References ---------- .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. """ if b is None: b = M_current[id_pop, :] b = np.delete(b, id_pop) M_del = np.delete(M_current, id_pop, 0) a = M_del[:, id_pop] M_del = np.delete(M_del, id_pop, 1) M = M_del - np.outer(a, b) / M_current[id_pop, id_pop] else: n = b.shape[0] + 1 if np.shape(b)[0] == 0: M = np.array([[0.5]]) else: X = M_current.dot(b) s = d - b.T.dot(X) # Ensure s is a scalar (extract from array if needed) if np.ndim(s) > 0: s = s.item() M = np.zeros((n, n)) M[:-1, :-1] = M_current + X.dot(X.T) / s X_ravel = X.ravel() M[-1, :-1] = -X_ravel / s M[:-1, -1] = -X_ravel / s M[-1, -1] = 1 / s return M def construct_augmented_H(active_index, m, Hc, HrHr): r""" This function constructs an augmented matrix for the first iteration of the semi-relaxed regularization path .. math:: \text{Augmented}_H = \begin{bmatrix} 0 & H_{c A} \\ H_{c A}^T & H_{r A}^T H_{r A} \end{bmatrix} where : - :math:`H_{r A}` is the sub-matrix constructed by the columns of \ :math:`H_r` whose indices belong to the active set A - :math:`H_{c A}` is the sub-matrix constructed by the columns of \ :math:`H_c` whose indices belong to the active set A Parameters ---------- active_index : list Indices of the active variables m : int Length of the target distribution Hc : np.ndarray (dim_b, dim_a * dim_b) Matrix that computes the sum along the columns of the transport plan \ :math:`T` HrHr : np.ndarray (dim_a * dim_b, dim_a * dim_b) Matrix product of :math:`H_r^T H_r` Returns ------- H_augmented : np.ndarray (dim_b + size(A), dim_b + size(A)) Augmented matrix for the first iteration of the semi-relaxed regularization path """ Hc_sub = Hc[:, active_index].toarray() HrHr_sub = HrHr[:, active_index] HrHr_sub = HrHr_sub[active_index, :].toarray() H_augmented = np.block([[np.zeros((m, m)), Hc_sub], [Hc_sub.T, HrHr_sub]]) return H_augmented def fully_relaxed_path(a: np.array, b: np.array, C: np.array, reg=1e-4, itmax=50000): r"""This function gives the regularization path of l2-penalized UOT problem The problem to optimize is the Lasso reformulation of the l2-penalized UOT: .. math:: \min_t \gamma \mathbf{c}^T \mathbf{t} + 0.5 * \|{H} \mathbf{t} - \mathbf{y}\|_2^2 s.t. \mathbf{t} \geq 0 where : - :math:`\mathbf{c}` is the flattened version of the cost matrix \ :math:`{C}` - :math:`\gamma = 1/\lambda` is the l2-regularization coefficient - :math:`\mathbf{y}` is the concatenation of vectors :math:`\mathbf{a}` \ and :math:`\mathbf{b}`, defined as \ :math:`\mathbf{y}^T = [\mathbf{a}^T \mathbf{b}^T]` - :math:`{H}` is a design matrix, see :ref:`[41] ` \ for the design of :math:`{H}`. The matrix product :math:`H\mathbf{t}` \ computes both the source marginal and the target marginals. - :math:`\mathbf{t}` is the flattened version of the transport matrix Parameters ---------- a : np.ndarray (dim_a,) Histogram of dimension dim_a b : np.ndarray (dim_b,) Histogram of dimension dim_b C : np.ndarray, shape (dim_a, dim_b) Cost matrix reg: float l2-regularization coefficient itmax: int Maximum number of iteration Returns ------- t : np.ndarray (dim_a*dim_b, ) Flattened vector of the optimal transport matrix t_list : list List of solutions in the regularization path gamma_list : list List of regularization coefficients in the regularization path Examples -------- >>> import ot >>> import numpy as np >>> n = 3 >>> xs = np.array([1., 2., 3.]).reshape((n, 1)) >>> xt = np.array([5., 6., 7.]).reshape((n, 1)) >>> C = ot.dist(xs, xt) >>> C /= C.max() >>> a = np.array([0.2, 0.5, 0.3]) >>> b = np.array([0.2, 0.5, 0.3]) >>> t, _, _ = ot.regpath.fully_relaxed_path(a, b, C, 1e-4) >>> t array([1.99958333e-01, 0.00000000e+00, 0.00000000e+00, 3.88888889e-05, 4.99938889e-01, 0.00000000e+00, 0.00000000e+00, 3.88888889e-05, 2.99958333e-01]) References ---------- .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. """ n = np.shape(a)[0] m = np.shape(b)[0] H, y, c = recast_ot_as_lasso(a, b, C) HtH = H.T.dot(H) Hty = H.T.dot(y) n_iter = 1 # initialization M0 = Hty / c gamma_list = [np.max(M0)] active_index = [np.argmax(M0)] t_list = [np.zeros((n * m,))] H_inv = np.array([[]]) add_col = np.array([]) id_pop = -1 while n_iter < itmax and gamma_list[-1] > reg: H_inv = complement_schur(H_inv, add_col, 2.0, id_pop) current_gamma = gamma_list[-1] # compute the intercept and slope of solutions in current iteration # t = phi - gamma * delta phi = H_inv.dot(Hty[active_index]) delta = H_inv.dot(c[active_index]) gamma, ik = ot_next_gamma(phi, delta, HtH, Hty, c, active_index, current_gamma) # compute the next lambda when removing a point from the active set alt_gamma, id_pop = compute_next_removal(phi, delta, current_gamma) # if the positivity constraint is violated, we remove id_pop # from active set, otherwise we add ik to active set if alt_gamma > gamma: gamma = alt_gamma else: id_pop = -1 # compute the solution of current segment tA = phi - gamma * delta sol = np.zeros((n * m,)) sol[active_index] = tA if id_pop != -1: active_index.pop(id_pop) add_col = None else: active_index.append(ik) add_col = HtH[active_index[:-1], ik].toarray() gamma_list.append(gamma) t_list.append(sol) n_iter += 1 if itmax <= n_iter: print("maximum iteration has been reached !") # correct the last solution and gamma if len(t_list) > 1: t_final = t_list[-2] + (t_list[-1] - t_list[-2]) * (reg - gamma_list[-2]) / ( gamma_list[-1] - gamma_list[-2] ) t_list[-1] = t_final gamma_list[-1] = reg else: gamma_list[-1] = reg print("Regularization path does not exist !") return t_list[-1], t_list, gamma_list def semi_relaxed_path(a: np.array, b: np.array, C: np.array, reg=1e-4, itmax=50000): r"""This function gives the regularization path of semi-relaxed l2-UOT problem. The problem to optimize is the Lasso reformulation of the l2-penalized UOT: .. math:: \min_t \gamma \mathbf{c}^T t + 0.5 * \|H_r \mathbf{t} - \mathbf{a}\|_2^2 s.t. H_c \mathbf{t} = \mathbf{b} \mathbf{t} \geq 0 where : - :math:`\mathbf{c}` is the flattened version of the cost matrix \ :math:`C` - :math:`\gamma = 1/\lambda` is the l2-regularization parameter - :math:`H_r` is a matrix that computes the sum along the rows of \ the transport plan :math:`T` - :math:`H_c` is a matrix that computes the sum along the columns of \ the transport plan :math:`T` - :math:`\mathbf{t}` is the flattened version of the transport plan \ :math:`T` Parameters ---------- a : np.ndarray (dim_a,) Histogram of dimension dim_a b : np.ndarray (dim_b,) Histogram of dimension dim_b C : np.ndarray, shape (dim_a, dim_b) Cost matrix reg: float (optional) l2-regularization coefficient itmax: int (optional) Maximum number of iteration Returns ------- t : np.ndarray (dim_a*dim_b, ) Flattened vector of the (unregularized) optimal transport matrix t_list : list List of all the optimal transport vectors of the regularization path gamma_list : list List of the regularization parameters in the path Examples -------- >>> import ot >>> import numpy as np >>> n = 3 >>> xs = np.array([1., 2., 3.]).reshape((n, 1)) >>> xt = np.array([5., 6., 7.]).reshape((n, 1)) >>> C = ot.dist(xs, xt) >>> C /= C.max() >>> a = np.array([0.2, 0.5, 0.3]) >>> b = np.array([0.2, 0.5, 0.3]) >>> t, _, _ = ot.regpath.semi_relaxed_path(a, b, C, 1e-4) >>> t array([1.99980556e-01, 0.00000000e+00, 0.00000000e+00, 1.94444444e-05, 4.99980556e-01, 0.00000000e+00, 0.00000000e+00, 1.94444444e-05, 3.00000000e-01]) References ---------- .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. """ n = np.shape(a)[0] m = np.shape(b)[0] Hr, Hc, c = recast_semi_relaxed_as_lasso(a, b, C) Hra = Hr.T.dot(a) HrHr = Hr.T.dot(Hr) n_iter = 1 active_index = [] # initialization for j in range(np.shape(C)[1]): i = np.argmin(C[:, j]) active_index.append(i * m + j) gamma_list = [] t_list = [] current_gamma = np.inf augmented_H0 = construct_augmented_H(active_index, m, Hc, HrHr) add_col = np.array([]) id_pop = -1 while n_iter < itmax and current_gamma > reg: if n_iter == 1: H_inv = np.linalg.inv(augmented_H0) else: H_inv = complement_schur(H_inv, add_col, 1.0, id_pop + m) # compute the intercept and slope of solutions in current iteration augmented_phi = H_inv.dot(np.concatenate((b, Hra[active_index]))) augmented_delta = H_inv[:, m:].dot(c[active_index]) phi = augmented_phi[m:] delta = augmented_delta[m:] phi_u = augmented_phi[0:m] delta_u = augmented_delta[0:m] gamma, ik = semi_relaxed_next_gamma( phi, delta, phi_u, delta_u, HrHr, Hc, Hra, c, active_index, current_gamma ) # compute the next lambda when removing a point from the active set alt_gamma, id_pop = compute_next_removal(phi, delta, current_gamma) # if the positivity constraint is violated, we remove id_pop # from active set, otherwise we add ik to active set if alt_gamma > gamma: gamma = alt_gamma else: id_pop = -1 # compute the solution of current segment tA = phi - gamma * delta sol = np.zeros((n * m,)) sol[active_index] = tA if id_pop != -1: active_index.pop(id_pop) add_col = None else: active_index.append(ik) add_col = np.concatenate( (Hc.toarray()[:, ik], HrHr.toarray()[active_index[:-1], ik]) ) add_col = add_col[:, np.newaxis] gamma_list.append(gamma) t_list.append(sol) current_gamma = gamma n_iter += 1 if itmax <= n_iter: print("maximum iteration has been reached !") # correct the last solution and gamma if len(t_list) > 1: t_final = t_list[-2] + (t_list[-1] - t_list[-2]) * (reg - gamma_list[-2]) / ( gamma_list[-1] - gamma_list[-2] ) t_list[-1] = t_final gamma_list[-1] = reg else: gamma_list[-1] = reg print("Regularization path does not exist !") return t_list[-1], t_list, gamma_list def regularization_path( a: np.array, b: np.array, C: np.array, reg=1e-4, semi_relaxed=False, itmax=50000 ): r"""This function provides all the solutions of the regularization path \ of the l2-UOT problem :ref:`[41] `. The problem to optimize is the Lasso reformulation of the l2-penalized UOT: .. math:: \min_t \gamma \mathbf{c}^T \mathbf{t} + 0.5 * \|{H} \mathbf{t} - \mathbf{y}\|_2^2 s.t. \mathbf{t} \geq 0 where : - :math:`\mathbf{c}` is the flattened version of the cost matrix \ :math:`{C}` - :math:`\gamma = 1/\lambda` is the l2-regularization coefficient - :math:`\mathbf{y}` is the concatenation of vectors :math:`\mathbf{a}` \ and :math:`\mathbf{b}`, defined as \ :math:`\mathbf{y}^T = [\mathbf{a}^T \mathbf{b}^T]` - :math:`{H}` is a design matrix, see :ref:`[41] ` \ for the design of :math:`{H}`. The matrix product :math:`H\mathbf{t}` \ computes both the source marginal and the target marginals. - :math:`\mathbf{t}` is the flattened version of the transport matrix For the semi-relaxed problem, it optimizes the Lasso reformulation of the l2-penalized UOT: .. math:: \min_t \gamma \mathbf{c}^T \mathbf{t} + 0.5 * \|H_r \mathbf{t} - \mathbf{a}\|_2^2 s.t. H_c \mathbf{t} = \mathbf{b} \mathbf{t} \geq 0 Parameters ---------- a : np.ndarray (dim_a,) Histogram of dimension dim_a b : np.ndarray (dim_b,) Histogram of dimension dim_b C : np.ndarray, shape (dim_a, dim_b) Cost matrix reg: float (optional) l2-regularization coefficient semi_relaxed : bool (optional) Give the semi-relaxed path if True itmax: int (optional) Maximum number of iteration Returns ------- t : np.ndarray (dim_a*dim_b, ) Flattened vector of the (unregularized) optimal transport matrix t_list : list List of all the optimal transport vectors of the regularization path gamma_list : list List of the regularization parameters in the path References ---------- .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. """ if semi_relaxed: t, t_list, gamma_list = semi_relaxed_path(a, b, C, reg=reg, itmax=itmax) else: t, t_list, gamma_list = fully_relaxed_path(a, b, C, reg=reg, itmax=itmax) return t, t_list, gamma_list def compute_transport_plan(gamma, gamma_list, Pi_list): r"""Given the regularization path, this function computes the transport plan for any value of gamma thanks to the piecewise linearity of the path. .. math:: t(\gamma) = \phi(\gamma) - \gamma \delta(\gamma) where: - :math:`\gamma` is the regularization parameter - :math:`\phi(\gamma)` is the corresponding intercept - :math:`\delta(\gamma)` is the corresponding slope - :math:`\mathbf{t}` is the flattened version of the transport matrix Parameters ---------- gamma : float Regularization coefficient gamma_list : list List of regularization parameters of the regularization path Pi_list : list List of all the solutions of the regularization path Returns ------- t : np.ndarray (dim_a*dim_b, ) Vectorization of the transport plan corresponding to the given value of gamma Examples -------- >>> import ot >>> import numpy as np >>> n = 3 >>> xs = np.array([1., 2., 3.]).reshape((n, 1)) >>> xt = np.array([5., 6., 7.]).reshape((n, 1)) >>> C = ot.dist(xs, xt) >>> C /= C.max() >>> a = np.array([0.2, 0.5, 0.3]) >>> b = np.array([0.2, 0.5, 0.3]) >>> t, pi_list, g_list = ot.regpath.regularization_path(a, b, C, reg=1e-4) >>> gamma = 1 >>> t2 = ot.regpath.compute_transport_plan(gamma, g_list, pi_list) >>> t2 array([0. , 0. , 0. , 0.19722222, 0.05555556, 0. , 0. , 0.24722222, 0. ]) .. _references-regpath: References ---------- .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. """ if gamma >= gamma_list[0]: Pi = Pi_list[0] elif gamma <= gamma_list[-1]: Pi = Pi_list[-1] else: idx = np.where(gamma <= np.array(gamma_list))[0][-1] gamma_k0 = gamma_list[idx] gamma_k1 = gamma_list[idx + 1] pi_k0 = Pi_list[idx] pi_k1 = Pi_list[idx + 1] Pi = pi_k0 + (pi_k1 - pi_k0) * (gamma - gamma_k0) / (gamma_k1 - gamma_k0) return Pi python-pot-0.9.7+dfsg/ot/semidiscrete.py000066400000000000000000000426111525521106200202460ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Semi-discrete optimal transport: continuous source, discrete target. Backend-agnostic semi-dual solver based on the Projected Averaged SGD of [1]_, with an optional decreasing entropic regularization schedule (DRAG, [2]_). Works with any backend supported by :mod:`ot.backend` (NumPy, PyTorch, JAX, CuPy, TensorFlow). References ---------- .. [1] Genans, F., Godichon-Baggioni, A., Vialard, F.-X., Wintenberger, O. (2025). "Stochastic Optimization in Semi-Discrete Optimal Transport: Convergence Analysis and Minimax Rate." NeurIPS 2025. .. [2] Genans, F., Godichon-Baggioni, A., Vialard, F.-X., Wintenberger, O. (2025). "Decreasing Entropic Regularization Averaged Gradient for Semi-Discrete Optimal Transport." NeurIPS 2025. """ # Author: Ferdinand Genans # # License: MIT License import math import numpy as np from .backend import get_backend from .utils import dist def _resolve_metric(metric): r"""Turn ``metric`` into a callable ``(x, y) -> (n_samples, n_atoms)`` matrix. ``None`` defaults to ``'sqeuclidean'``. A string is forwarded to :func:`ot.dist`; a callable is returned unchanged. """ if metric is None: metric = "sqeuclidean" if callable(metric): return metric return lambda x, y: dist(x, y, metric=metric) def _setup(X_target, a_target, metric): """Resolve backend, default weights and metric callable.""" nx = get_backend(X_target) m = X_target.shape[0] if a_target is None: a_target = nx.full((m,), 1.0 / m, type_as=X_target) return nx, m, a_target, nx.log(a_target), _resolve_metric(metric) def _resolve_sampler(sampler_source, X_target, nx): r"""Turn ``sampler_source`` into a callable ``batch_size -> (batch_size, d)``. A callable is returned unchanged. A string selects a built-in sampler drawing in the same backend as ``X_target``: - ``'unif'`` / ``'unif_cube'``: uniform on the unit cube :math:`[0, 1]^d`; - ``'ball'`` / ``'unif_ball'``: uniform on the unit ball; - ``'normal'``: standard Gaussian :math:`\mathcal{N}(0, I_d)`. """ if callable(sampler_source): return sampler_source d = X_target.shape[1] if sampler_source in ("unif", "unif_cube"): return lambda b: nx.rand(b, d, type_as=X_target) if sampler_source in ("ball", "unif_ball"): def sampler(b): z = nx.randn(b, d, type_as=X_target) r = nx.rand(b, 1, type_as=X_target) ** (1.0 / d) return r * z / nx.sqrt(nx.sum(z**2, axis=1))[:, None] return sampler if sampler_source == "normal": return lambda b: nx.randn(b, d, type_as=X_target) raise ValueError( f"Unknown sampler_source {sampler_source!r}. Expected a callable or one " "of 'unif', 'unif_cube', 'ball', 'unif_ball', 'normal'." ) def _atom_weights(score, reg, log_b, nx): """Row-stochastic weights ``(batch, m)`` from ``score = g - C``. Softmax of ``score / reg + log_b`` when ``reg > 0``, one-hot of ``argmax(score, axis=1)`` when ``reg == 0``. """ if reg > 0: log_w = score / reg + log_b[None, :] log_w = log_w - nx.logsumexp(log_w, axis=1)[:, None] return nx.exp(log_w) m = score.shape[1] idx = nx.argmax(score, axis=1) arange_m = nx.from_numpy(np.arange(m), type_as=score) mask = idx[:, None] == arange_m[None, :] one = nx.full((1,), 1.0, type_as=score) zero = nx.full((1,), 0.0, type_as=score) return nx.where(mask, one, zero) def semidiscrete_atom_weights( X_target, X_source, semi_dual_potential, a_target=None, metric=None, reg=0.0, ): r"""(Entropic) assignment weights of source samples to target atoms. For target atoms :math:`(y_j)_{j=1}^M` (``X_target``) with weights :math:`(w_j)_j` (``a_target``), a ground cost :math:`c` (``metric``) and a semi-dual potential :math:`g` (``semi_dual_potential``), this returns, for each source sample :math:`x` (a row of ``X_source``), the (entropic) assignment weights :math:`\chi^\varepsilon(x, g)` ([2]_, Sec. 2.2): .. math:: \chi^{\mathrm{reg}}_j(x, g) = \begin{cases} \displaystyle \frac{w_j \exp\!\big((g_j - c(x, y_j))/\mathrm{reg}\big)} {\sum_{k=1}^M w_k \exp\!\big((g_k - c(x, y_k))/\mathrm{reg}\big)}, & \mathrm{reg} > 0, \\[1.2em] \mathbf{1}\big[\, j = \arg\min_k\, c(x, y_k) - g_k \,\big] & \mathrm{reg} = 0. \end{cases} :math:`\mathbb{E}_{X}[\chi^\varepsilon(X, g)]` is the atom marginal that the semi-dual gradient matches to :math:`w` ([1]_); cf. :func:`solve_semidiscrete`. Parameters ---------- X_target : array-like, shape (n_atoms, d) Target atom positions :math:`y_j`. X_source : array-like, shape (n_samples, d) Source samples :math:`x` to assign. semi_dual_potential : array-like, shape (n_atoms,) Semi-dual potential :math:`g`, e.g. from :func:`solve_semidiscrete`. a_target : array-like, shape (n_atoms,), optional Atom weights :math:`w_j`. Defaults to uniform. metric : str or callable, optional Ground cost. A string is passed to :func:`ot.dist` (e.g. ``'sqeuclidean'``, ``'euclidean'``); a callable ``metric(x, y)`` must return the ``(n_samples, n_atoms)`` cost matrix. Defaults to ``'sqeuclidean'`` (:math:`\|x - y\|^2`). reg : float, default=0.0 Entropic regularization :math:`\varepsilon`. ``0`` gives hard, one-hot assignments; ``> 0`` the softmax above. Returns ------- w : array, shape (n_samples, n_atoms) Row-stochastic assignment weights: ``w[i, j]`` is the (entropic) probability that sample :math:`x_i` is sent to atom :math:`y_j`. Each row sums to 1. References ---------- .. [1] Genans, F., Godichon-Baggioni, A., Vialard, F.-X., Wintenberger, O. (2025). "Stochastic Optimization in Semi-Discrete Optimal Transport: Convergence Analysis and Minimax Rate." NeurIPS 2025. .. [2] Genans, F., Godichon-Baggioni, A., Vialard, F.-X., Wintenberger, O. (2025). "Decreasing Entropic Regularization Averaged Gradient for Semi-Discrete Optimal Transport." NeurIPS 2025. """ nx, _, _, log_b, metric_fn = _setup(X_target, a_target, metric) score = semi_dual_potential[None, :] - metric_fn(X_source, X_target) return _atom_weights(score, reg, log_b, nx) def semidiscrete_ot_map( X_target, X_source, semi_dual_potential, a_target=None, metric=None, reg=0.0, ): r"""Semi-discrete OT map (barycentric projection) induced by a potential. For each source sample :math:`x` (a row of ``X_source``), the transported position uses the (entropic) assignment weights :math:`\chi^\varepsilon(x, g)` of :func:`semidiscrete_atom_weights`: .. math:: T(x) = \begin{cases} \displaystyle \sum_{j=1}^M \chi^\varepsilon_j(x, g)\, y_j & \varepsilon = \mathrm{reg} > 0, \\[1em] y_j \ \text{ for } x \in \mathbb{L}_j(g) & \varepsilon = 0. \end{cases} For :math:`\varepsilon > 0` this is the smoothed barycentric projection; for :math:`\varepsilon = 0` the weights are one-hot and :math:`T` is the Monge map of the (generalized) Brenier theorem ([1]_), sending :math:`x` to the atom of its Laguerre cell .. math:: \mathbb{L}_j(g) = \big\{\, x : g^{c}(x) = c(x, y_j) - g_j \,\big\}, cf. [2]_, Sec. 2.2. Parameters ---------- X_target : array-like, shape (n_atoms, d) Target atom positions :math:`y_j`. X_source : array-like, shape (n_samples, d) Source samples :math:`x` to transport. semi_dual_potential : array-like, shape (n_atoms,) Semi-dual potential :math:`g`, e.g. from :func:`solve_semidiscrete`. a_target : array-like, shape (n_atoms,), optional Atom weights :math:`w_j`. Defaults to uniform. metric : str or callable, optional Ground cost. A string is passed to :func:`ot.dist` (e.g. ``'sqeuclidean'``, ``'euclidean'``); a callable ``metric(x, y)`` must return the ``(n_samples, n_atoms)`` cost matrix. Defaults to ``'sqeuclidean'`` (:math:`\|x - y\|^2`). reg : float, default=0.0 Entropic regularization :math:`\varepsilon`. ``0`` gives the hard Monge map; ``> 0`` the smoothed barycentric map. Returns ------- T : array, shape (n_samples, d) Transported source positions :math:`T(x_i)`. References ---------- .. [1] Genans, F., Godichon-Baggioni, A., Vialard, F.-X., Wintenberger, O. (2025). "Stochastic Optimization in Semi-Discrete Optimal Transport: Convergence Analysis and Minimax Rate." NeurIPS 2025. .. [2] Genans, F., Godichon-Baggioni, A., Vialard, F.-X., Wintenberger, O. (2025). "Decreasing Entropic Regularization Averaged Gradient for Semi-Discrete Optimal Transport." NeurIPS 2025. """ w = semidiscrete_atom_weights( X_target, X_source, semi_dual_potential, a_target=a_target, metric=metric, reg=reg, ) return w @ X_target def semidiscrete_c_transform( X_target, X_source, semi_dual_potential, a_target=None, metric=None, reg=0.0, ): r"""(Entropic) :math:`c`-transform of a semi-dual potential. The vectorial :math:`(c, \varepsilon)`-transform :math:`g^{c,\varepsilon}` of the potential :math:`g` (``semi_dual_potential``), evaluated at the source samples ([1]_, Eq. (3); entropic form in [2]_, Eq. (4)): .. math:: g^{c,\varepsilon}(x) = \begin{cases} \min_{j}\, \big(c(x, y_j) - g_j\big) & \varepsilon = 0, \\[4pt] -\varepsilon \log \sum_{j=1}^M w_j \exp\!\big((g_j - c(x, y_j))/\varepsilon\big) & \varepsilon > 0. \end{cases} For :math:`\varepsilon = 0` this is the standard :math:`c`-transform :math:`g^{c}(x) = \min_j (c(x, y_j) - g_j)` whose expectation gives the concave semi-dual :math:`H(g) = \mathbb{E}_X[g^{c}(X)] + \langle g, w\rangle` maximized by :func:`solve_semidiscrete` ([1]_, Eq. (2)). Parameters ---------- X_target : array-like, shape (n_atoms, d) Target atom positions :math:`y_j`. X_source : array-like, shape (n_samples, d) Source samples :math:`x` at which to evaluate the transform. semi_dual_potential : array-like, shape (n_atoms,) Semi-dual potential :math:`g`, e.g. from :func:`solve_semidiscrete`. a_target : array-like, shape (n_atoms,), optional Atom weights :math:`w_j`. Defaults to uniform. metric : str or callable, optional Ground cost. A string is passed to :func:`ot.dist` (e.g. ``'sqeuclidean'``, ``'euclidean'``); a callable ``metric(x, y)`` must return the ``(n_samples, n_atoms)`` cost matrix. Defaults to ``'sqeuclidean'`` (:math:`\|x - y\|^2`). reg : float, default=0.0 Entropic regularization :math:`\varepsilon`. ``0`` gives the hard :math:`\min`; ``> 0`` the soft log-sum-exp. Returns ------- phi : array, shape (n_samples,) The :math:`c`-transform :math:`g^{c,\varepsilon}(x_i)` at each sample. References ---------- .. [1] Genans, F., Godichon-Baggioni, A., Vialard, F.-X., Wintenberger, O. (2025). "Stochastic Optimization in Semi-Discrete Optimal Transport: Convergence Analysis and Minimax Rate." NeurIPS 2025. .. [2] Genans, F., Godichon-Baggioni, A., Vialard, F.-X., Wintenberger, O. (2025). "Decreasing Entropic Regularization Averaged Gradient for Semi-Discrete Optimal Transport." NeurIPS 2025. """ nx, _, _, log_b, metric_fn = _setup(X_target, a_target, metric) score = semi_dual_potential[None, :] - metric_fn(X_source, X_target) if reg == 0: return -nx.max(score, axis=1) return -reg * nx.logsumexp(score / reg + log_b[None, :], axis=1) def solve_semidiscrete( X_target, sampler_source="unif", a_target=None, metric=None, reg=0.0, max_iter=10_000, batch_size=32, lr0=None, lr_exponent=2.0 / 3.0, init_potential=None, decreasing_reg=True, decreasing_reg_initial_eps=0.1, decreasing_reg_exponent=0.5, max_cost=None, polyak_average=True, log=False, ): r"""Solve semi-discrete OT by (projected) averaged SGD on the semi-dual. Maximizes the concave semi-dual objective ([1]_, Eq. (4), in its negative convex form) .. math:: H(g) = \mathbb{E}_{X}[g^{c}(X)] + \langle g, w\rangle , over the potential :math:`g \in \mathbb{R}^M`, where :math:`g^{c}` is the (entropic) :math:`c`-transform of :math:`g` (see :func:`semidiscrete_c_transform`). The base solver is the projected averaged SGD of [1]_ (Algorithm 1); the projection ``max_cost`` clips each iterate to the localizing set :math:`\{|g_j| \le \texttt{max\_cost}\}` ([1]_, Sec. 3.1). When ``decreasing_reg=True``, the entropic regularization is annealed along the iterations following the DRAG schedule of [2]_ (Algorithm 1), which accelerates convergence. With ``decreasing_reg=True`` the regularization at iteration ``t`` is :math:`\varepsilon_t = \max(\text{reg},\, \varepsilon_0 / t^\alpha)` — large at first for smoothness, then annealed towards ``reg``. This is the DRAG schedule of [2]_. Parameters ---------- X_target : array-like, shape (n_atoms, d) Positions of the target atoms. The backend of this array drives all subsequent computations. sampler_source : str or callable, default='unif' Source distribution to sample from: either a callable ``sampler_source(batch_size)`` returning a ``(batch_size, d)`` batch in the backend of ``X_target``, or a built-in name -- one of ``'unif'`` (``'unif_cube'``), ``'ball'`` (``'unif_ball'``) or ``'normal'``. a_target : array-like, shape (n_atoms,), optional Atom weights. Defaults to uniform. metric : str or callable, optional Ground cost, see ``metric`` in :func:`semidiscrete_atom_weights`. Defaults to ``'sqeuclidean'``. reg : float, default=0.0 Entropic regularization (target value when ``decreasing_reg=True``). max_iter : int, default=10000 batch_size : int, default=32 lr0 : float, optional Initial learning rate. Defaults to ``sqrt(n_atoms * batch_size)``. lr_exponent : float, default=2/3 Step size decays as ``lr0 / t**lr_exponent``. init_potential : array-like, shape (n_atoms,), optional Starting iterate; defaults to zero. Not mutated. decreasing_reg : bool, default=True Enable the DRAG decreasing-regularization schedule. decreasing_reg_initial_eps : float, default=0.1 Initial regularization in the DRAG schedule. decreasing_reg_exponent : float, default=0.5 Decay exponent of the DRAG schedule. max_cost : float, optional If given, clip each iterate to ``[-max_cost, max_cost]``. polyak_average : bool, default=True If True, return the uniform average of the iterates; else the last. log : bool, default=False If True, also return a small ``dict`` with the last iterate. Returns ------- semi_dual_potential : array, shape (n_atoms,) info : dict, optional Returned only when ``log=True``. References ---------- .. [1] Genans, F., Godichon-Baggioni, A., Vialard, F.-X., Wintenberger, O. (2025). "Stochastic Optimization in Semi-Discrete Optimal Transport: Convergence Analysis and Minimax Rate." NeurIPS 2025. .. [2] Genans, F., Godichon-Baggioni, A., Vialard, F.-X., Wintenberger, O. (2025). "Decreasing Entropic Regularization Averaged Gradient for Semi-Discrete Optimal Transport." NeurIPS 2025. Examples -------- >>> import numpy as np >>> from ot.semidiscrete import solve_semidiscrete >>> rng = np.random.default_rng(0) >>> target = np.linspace(0.0, 1.0, 10).reshape(-1, 1) >>> g = solve_semidiscrete( ... target, lambda b: rng.random((b, 1)), ... max_iter=500, batch_size=32, max_cost=1.0, ... ) """ nx, m, b, log_b, metric_fn = _setup(X_target, a_target, metric) sampler_source = _resolve_sampler(sampler_source, X_target, nx) if init_potential is None: g = nx.zeros((m,), type_as=X_target) else: g = init_potential + nx.zeros((m,), type_as=X_target) if lr0 is None: lr0 = math.sqrt(m * batch_size) g_avg = nx.zeros((m,), type_as=X_target) if polyak_average else None for t in range(1, max_iter + 1): if decreasing_reg: reg_t = max(reg, decreasing_reg_initial_eps / (t**decreasing_reg_exponent)) else: reg_t = reg x = sampler_source(batch_size) score = g[None, :] - metric_fn(x, X_target) w = _atom_weights(score, reg_t, log_b, nx) grad = nx.mean(w, axis=0) - b lr_t = lr0 / (t**lr_exponent) g = g - lr_t * grad if max_cost is not None: g = nx.clip(g, -max_cost, max_cost) if polyak_average: g_avg = g_avg + (g - g_avg) / t result = g_avg if polyak_average else g if log: return result, { "max_iter": max_iter, "batch_size": batch_size, "max_cost": max_cost, "polyak_average": polyak_average, "last_potential": g, } return result __all__ = [ "solve_semidiscrete", "semidiscrete_atom_weights", "semidiscrete_ot_map", "semidiscrete_c_transform", ] python-pot-0.9.7+dfsg/ot/sgot.py000066400000000000000000000372441525521106200165500ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Spectral-Grassmann optimal transport for linear operators. This module implements the Spectral-Grassmann Wasserstein framework for comparing dynamical systems via their learned operator representations. It provides tools to extract spectral "atoms" (eigenvalues and associated eigenspaces) from linear operators and to compute an optimal transport metric that combines a spectral term on eigenvalues with a Grassmannian term on eigenspaces. """ # Author: Sienna O'Shea # Thibaut Germain # License: MIT License import ot from ot.backend import get_backend ##################################################################################################################################### ##################################################################################################################################### ### NORMALIZATION AND OPERATOR ATOMS ### ##################################################################################################################################### ##################################################################################################################################### def eigenvalue_cost_matrix(Ds, Dt, q=1, eigen_scaling=None, nx=None): """Compute pairwise eigenvalue distances for source and target domains. Parameters ---------- Ds: array-like, shape (n_s,) Source eigenvalues. Dt: array-like, shape (n_t,) Target eigenvalues. q: int or float, optional Exponent applied to the eigenvalue distance, default 1. eigen_scaling: None or array-like of length 2, optional Scaling (real_scale, imag_scale) applied to eigenvalues before computing distances. If None, defaults to (1.0, 1.0). Accepts tuple/list or array/tensor with two entries. nx: module, optional Backend (NumPy-compatible). If None, inferred from inputs. Returns ---------- C: np.ndarray, shape (n_s, n_t) Eigenvalue cost matrix. """ if nx is None: nx = get_backend(Ds, Dt) if eigen_scaling is None: real_scale, imag_scale = 1.0, 1.0 else: if isinstance(eigen_scaling, (tuple, list)): real_scale, imag_scale = eigen_scaling else: real_scale, imag_scale = eigen_scaling[0], eigen_scaling[1] Dsn = nx.real(Ds) * real_scale + 1j * nx.imag(Ds) * imag_scale Dtn = nx.real(Dt) * real_scale + 1j * nx.imag(Dt) * imag_scale C_real = nx.real(Dsn)[:, None] - nx.real(Dtn)[None, :] C_real = C_real**2 C_imag = nx.imag(Dsn)[:, None] - nx.imag(Dtn)[None, :] C_imag = C_imag**2 prod = C_real + C_imag return prod ** (q / 2) def _normalize_columns(A, nx, eps=1e-12): """Normalize the columns of an array with a backend-aware norm. Parameters ---------- A: array-like, shape (d, n) Input array whose columns are normalized. nx: module Backend (NumPy-compatible) used for math operations. eps: float, optional Minimum norm value to avoid division by zero, default 1e-12. Returns ---------- A_norm: array-like, shape (d, n) Column-normalized array. """ nrm = nx.norm(A, axis=0, keepdims=True) nrm = nx.real(nrm) # norm is real; avoid complex dtype for maximum (e.g. torch) nrm = nx.maximum(nrm, eps) return A / nrm def _delta_matrix_1d(Rs, Ls, Rt, Lt, nx=None, eps=1e-12): """Compute the normalized inner-product delta matrix for eigenspaces. Parameters ---------- Rs: array-like, shape (L, n_s) Source right eigenvectors. Ls: array-like, shape (L, n_s) Source left eigenvectors. Rt: array-like, shape (L, n_t) Target right eigenvectors. Lt: array-like, shape (L, n_t) Target left eigenvectors. nx: module, optional Backend (NumPy-compatible). If None, inferred from inputs. eps: float, optional Minimum norm value used in normalization, default 1e-12. Returns ---------- delta: array-like, shape (n_s, n_t) Delta matrix with entries in [0, 1]. """ if nx is None: nx = get_backend(Rs, Ls, Rt, Lt) Rsn = _normalize_columns(Rs, nx=nx, eps=eps) Lsn = _normalize_columns(Ls, nx=nx, eps=eps) Rtn = _normalize_columns(Rt, nx=nx, eps=eps) Ltn = _normalize_columns(Lt, nx=nx, eps=eps) Cr = nx.dot(nx.conj(Rsn).T, Rtn) Cl = nx.dot(Lsn.T, nx.conj(Ltn)) delta = nx.abs(Cr * Cl) delta = nx.clip(delta, 0.0, 1.0) return delta def _grassmann_distance_squared( delta, grassmann_metric="chordal", q=1, nx=None, eps=1e-12 ): """Compute Grassmannian distances from delta similarities. Parameters ---------- delta: array-like Similarity values in [0, 1]. grassmann_metric: str, optional Metric type: "geodesic", "chordal", "procrustes", or "martin". q: int or float, optional Exponent applied to the Grassmann distance, in the same spirit as the eigenvalue cost exponent. Default is 1. nx: module, optional Backend (NumPy-compatible). If None, inferred from inputs. eps: float, optional Minimum value used for numerical stability in the Martin metric. Returns ------- dist_q: array-like Grassmannian distances raised to the power q. """ if nx is None: nx = get_backend(delta) if nx.any(delta < 0) or nx.any(delta > 1.0): raise ValueError( "delta must be in [0, 1]; found values outside this range " f"(min={nx.min(delta)}, max={nx.max(delta)})" ) if grassmann_metric == "geodesic": dist2 = nx.arccos(delta) ** 2 elif grassmann_metric == "chordal": dist2 = 1.0 - delta**2 elif grassmann_metric == "procrustes": dist2 = 2.0 * (1.0 - delta) elif grassmann_metric == "martin": delta2 = nx.maximum(delta**2, eps) dist2 = -nx.log(delta2) else: raise ValueError(f"Unknown grassmann_metric: {grassmann_metric}") return nx.real(dist2) ** (q / 2.0) ##################################################################################################################################### ##################################################################################################################################### ### SPECTRAL-GRASSMANNIAN WASSERSTEIN METRIC ### ##################################################################################################################################### ##################################################################################################################################### def sgot_cost_matrix( Ds, Rs, Ls, Dt, Rt, Lt, eta=0.5, p=2, q=1, grassmann_metric="chordal", eigen_scaling=None, nx=None, eps=1e-12, ): r"""Compute the SGOT cost matrix between two spectral decompositions. This returns the discrete ground cost matrix used in the SGOT optimal transport objective. Each spectral atom is :math:`z_i=(\lambda_i, V_i)` where :math:`\lambda_i \in \mathbb{C}` is an eigenvalue and :math:`V_i` is the associated (bi-orthogonal) eigenspace point. .. math:: C_2(i,j) \;=\; \eta\,C_\lambda(i,j) \;+\; (1-\eta)\,C_G(i,j), with spectral term .. math:: C_\lambda(i,j) \;=\; \big|\lambda_i - \lambda'_j\big|^{q}, and Grassmann term computed from a similarity score :math:`\delta_{ij}\in[0,1]` built from left/right eigenvectors .. math:: \delta_{ij} \;=\; \left|\langle r_i, r'_j\rangle\,\langle \ell_i, \ell'_j\rangle\right|. Depending on ``grassmann_metric``, the Grassmann contribution is: - ``"chordal"``: :math:`C_G(i,j) = 1 - \delta_{ij}^2` - ``"geodesic"``: :math:`C_G(i,j) = \arccos(\delta_{ij})^2` - ``"procrustes"``: :math:`C_G(i,j) = 2(1-\delta_{ij})` - ``"martin"``: :math:`C_G(i,j) = -\log\!\left(\max(\delta_{ij}^2,\varepsilon)\right)` Finally, we return a matrix suited for a :math:`p`-Wasserstein objective by treating :math:`C_2 \approx d^2` and outputting .. math:: C(i,j) \;=\; \big(\operatorname{Re}(C_2(i,j))\big)^{p/2}. Parameters ---------- Ds: array-like, shape (n_s,) Eigenvalues of operator T1. Rs: array-like, shape (L, n_s) Right eigenvectors of operator T1. Ls: array-like, shape (L, n_s) Left eigenvectors of operator T1. Dt: array-like, shape (n_t,) Eigenvalues of operator T2. Rt: array-like, shape (L, n_t) Right eigenvectors of operator T2. Lt: array-like, shape (L, n_t) Left eigenvectors of operator T2. eta: float, optional Weighting between spectral and Grassmann terms, default 0.5. p: int, optional Exponent defining the OT ground cost. The returned cost is :math:`d^p` with :math:`d^2 \approx C_2`. Default is 2. q: int, optional Exponent applied to the eigenvalue distance in the spectral term. Default is 1. grassmann_metric: str, optional Metric type: "geodesic", "chordal", "procrustes", or "martin". eigen_scaling: None or array-like of length 2, optional Scaling ``(real_scale, imag_scale)`` applied to eigenvalues before computing :math:`C_\lambda`. If provided, eigenvalues are transformed as :math:`\lambda \mapsto \alpha\operatorname{Re}(\lambda) + i\,\beta\operatorname{Im}(\lambda)`. If None, defaults to ``(1.0, 1.0)``. Accepts tuple/list or array/tensor with two entries. nx: module, optional Backend (NumPy-compatible). If None, inferred from inputs. eps: float, optional Minimum value used for numerical stability in Grassmann distances and Martin metric. Default is 1e-12. Returns ---------- C: array-like, shape (n_s, n_t) SGOT cost matrix :math:`C = d^p`. References ---------- .. [83] Germain, T., Flamary, R., Kostic, V. R., & Lounici, K. (2025). `A Spectral-Grassmann Wasserstein Metric for Operator Representations of Dynamical Systems `_. """ if nx is None: nx = get_backend(Ds, Rs, Ls, Dt, Rt, Lt) _validate_sgot_inputs(Ds, Rs, Ls, Dt, Rt, Lt) C_lambda = eigenvalue_cost_matrix(Ds, Dt, q=q, eigen_scaling=eigen_scaling, nx=nx) delta = _delta_matrix_1d(Rs, Ls, Rt, Lt, nx=nx) C_grass = _grassmann_distance_squared( delta, grassmann_metric=grassmann_metric, q=q, nx=nx, eps=eps, ) C2 = eta * C_lambda + (1.0 - eta) * C_grass C = C2 ** (p / 2.0) return C def _validate_sgot_inputs(Ds, Rs, Ls, Dt, Rt, Lt): """Validate shapes of spectral atoms for SGOT cost/metric.""" Ds_shape = getattr(Ds, "shape", None) Dt_shape = getattr(Dt, "shape", None) Ds_ndim = getattr(Ds, "ndim", None) Dt_ndim = getattr(Dt, "ndim", None) if Ds_ndim != 1 or Dt_ndim != 1: raise ValueError( "SGOT expects Ds and Dt to be 1D (n,), " f"got Ds shape {Ds_shape} and Dt shape {Dt_shape}" ) if Rs.shape != Ls.shape or Rt.shape != Lt.shape: raise ValueError( "Right/left eigenvector shapes must match; got " f"(Rs,Ls)=({Rs.shape},{Ls.shape}), (Rt,Lt)=({Rt.shape},{Lt.shape})" ) if Rs.shape[1] != Ds.shape[0] or Rt.shape[1] != Dt.shape[0]: raise ValueError( "Eigenvector columns must match eigenvalues: " f"Rs {Rs.shape[1]} vs Ds {Ds.shape[0]}, " f"Rt {Rt.shape[1]} vs Dt {Dt.shape[0]}" ) def sgot_metric( Ds, Rs, Ls, Dt, Rt, Lt, eta=0.5, p=2, q=1, r=2, grassmann_metric="chordal", eigen_scaling=None, Ws=None, Wt=None, nx=None, eps=1e-12, ): r"""Compute the SGOT metric between two spectral decompositions. This function computes a discrete optimal transport problem between two measures over spectral atoms :math:`z_i=(\lambda_i, V_i)` and :math:`z'_j=(\lambda'_j, V'_j)`. Using the ground cost matrix :math:`C = d^p` returned by :func:`sgot_cost_matrix`, we solve: .. math:: P^\star \in \arg\min_{P\in\Pi(W_s, W_t)} \langle C, P\rangle, where :math:`C(i,j) = d(i,j)^p` and :math:`d(i,j)` is the SGOT ground distance combining spectral and Grassmann terms with exponent :math:`q`: .. math:: d(i,j)^2 \;=\; \eta\,\big|\lambda_i - \lambda'_j\big|^{q} \;+\; (1-\eta)\,d_G(i,j)^{q}, and :math:`d_G(i,j)` is the Grassmann distance associated with the chosen ``grassmann_metric``. From the optimal plan :math:`P^\star`, we first form the :math:`p`-Wasserstein objective: .. math:: \mathrm{obj} \;=\; \left(\sum_{i,j} C(i,j)\,P^\star_{ij}\right)^{1/p}, and then apply an outer root :math:`r`: .. math:: \mathrm{SGOT} \;=\; \mathrm{obj}^{1/r}. In summary: - :math:`q` controls how strongly spectral and Grassmann distances are curved (via :math:`|\lambda_i - \lambda'_j|^{q}` and :math:`d_G(i,j)^{q}`), - :math:`p` is the exponent used in the OT ground cost and the inner Wasserstein root, - :math:`r` is an additional outer root applied to the Wasserstein objective. Parameters ---------- Ds: array-like, shape (n_s,) Eigenvalues of operator T1. Rs: array-like, shape (L, n_s) Right eigenvectors of operator T1. Ls: array-like, shape (L, n_s) Left eigenvectors of operator T1. Dt: array-like, shape (n_t,) Eigenvalues of operator T2. Rt: array-like, shape (L, n_t) Right eigenvectors of operator T2. Lt: array-like, shape (L, n_t) Left eigenvectors of operator T2. eta: float, optional Weighting between spectral and Grassmann terms, default 0.5. p: int, optional Exponent defining the OT ground cost, default 2. q: int, optional Exponent applied to the eigenvalue and Grassmann distances, default 1. r: int or float, optional Outer root applied to the Wasserstein objective, default 2. grassmann_metric: str, optional Metric type: "geodesic", "chordal", "procrustes", or "martin". eigen_scaling: None or array-like of length 2, optional Scaling ``(real_scale, imag_scale)`` applied to eigenvalues before computing the spectral cost. If None, defaults to ``(1.0, 1.0)``. Ws: array-like, shape (n_s,), optional Source weights. If None, uniform weights are used. Wt: array-like, shape (n_t,), optional Target weights. If None, uniform weights are used. nx: module, optional Backend (NumPy-compatible). If None, inferred from inputs. eps: float, optional Numerical stability constant, default 1e-12. Returns ------- dist: float SGOT distance between the two spectral decompositions. References ---------- .. [83] Germain, T., Flamary, R., Kostic, V. R., & Lounici, K. (2025). `A Spectral-Grassmann Wasserstein Metric for Operator Representations of Dynamical Systems `_. """ if nx is None: nx = get_backend(Ds, Rs, Ls, Dt, Rt, Lt) _validate_sgot_inputs(Ds, Rs, Ls, Dt, Rt, Lt) C = sgot_cost_matrix( Ds, Rs, Ls, Dt, Rt, Lt, eta=eta, p=p, q=q, grassmann_metric=grassmann_metric, eigen_scaling=eigen_scaling, nx=nx, eps=eps, ) if Ws is None: Ws = nx.ones((C.shape[0],), type_as=C) / float(C.shape[0]) if Wt is None: Wt = nx.ones((C.shape[1],), type_as=C) / float(C.shape[1]) Ws = Ws / nx.sum(Ws) Wt = Wt / nx.sum(Wt) obj = ot.emd2(Ws, Wt, nx.real(C)) obj = obj ** (1.0 / p) return obj ** (1.0 / float(r)) python-pot-0.9.7+dfsg/ot/sliced/000077500000000000000000000000001525521106200164535ustar00rootroot00000000000000python-pot-0.9.7+dfsg/ot/sliced/__init__.py000066400000000000000000000017671525521106200205770ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Solvers related to (balanced) sliced transport. """ # Author: Laetitia Chapel # # License: MIT License # All submodules and packages from ._utils import ( get_random_projections, get_projections_sphere, projection_sphere_to_circle, ) from ._sliced_distances import ( sliced_wasserstein_distance, max_sliced_wasserstein_distance, ) from ._spherical_sliced import ( sliced_wasserstein_sphere, sliced_wasserstein_sphere_unif, linear_sliced_wasserstein_sphere, ) from ._sliced_plans import min_sliced_transport_plan, expected_sliced_plan, sliced_plans __all__ = [ "get_random_projections", "get_projections_sphere", "projection_sphere_to_circle", "min_sliced_transport_plan", "expected_sliced_plan", "sliced_plans", "sliced_wasserstein_distance", "max_sliced_wasserstein_distance", "sliced_wasserstein_sphere", "sliced_wasserstein_sphere_unif", "linear_sliced_wasserstein_sphere", ] python-pot-0.9.7+dfsg/ot/sliced/_sliced_distances.py000066400000000000000000000205701525521106200224700ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Sliced Wasserstein distances solvers: sliced and max-sliced. """ # Author: Adrien Corenflos # Nicolas Courty # Rémi Flamary # # License: MIT License from ..backend import get_backend from ..utils import list_to_array, apply_scaler from ._utils import get_random_projections from ..lp import wasserstein_1d def sliced_wasserstein_distance( X_s, X_t, a=None, b=None, n_projections=50, p=2, projections=None, seed=None, log=False, scaler=None, ): r""" Computes a Monte-Carlo approximation of the p-Sliced Wasserstein distance .. math:: \mathcal{SWD}_p(\mu, \nu) = \underset{\theta \sim \mathcal{U}(\mathbb{S}^{d-1})}{\mathbb{E}}\left(\mathcal{W}_p^p(\theta_\# \mu, \theta_\# \nu)\right)^{\frac{1}{p}} where : - :math:`\theta_\# \mu` stands for the pushforwards of the projection :math:`X \in \mathbb{R}^d \mapsto \langle \theta, X \rangle` Parameters ---------- X_s : ndarray, shape (n_samples_a, dim) samples in the source domain X_t : ndarray, shape (n_samples_b, dim) samples in the target domain a : ndarray, shape (n_samples_a,), optional samples weights in the source domain b : ndarray, shape (n_samples_b,), optional samples weights in the target domain n_projections : int, optional Number of projections used for the Monte-Carlo approximation p: float, optional Power p used for computing the sliced Wasserstein projections: shape (dim, n_projections), optional Projection matrix (n_projections and seed are not used in this case) seed: int or RandomState or None, optional Seed used for random number generator log: bool, optional if True, sliced_wasserstein_distance returns the projections used and their associated EMD. scaler: None, object with .transform(), or callable, optional Preprocessing applied to X_s and X_t before computing the distance. Useful for normalizing inputs when features have very different scales. - ``None`` : no preprocessing (default) - Object with ``.transform()`` method : e.g. an :class:`ot.utils.DataScaler` fitted on a representative sample. This is the recommended way to get stable, consistent normalization across multiple calls (e.g. when using SWD as a loss in mini-batch training). - Callable : any function, lambda, or PyTorch transform applied directly as ``scaler(X_s)`` and ``scaler(X_t)``. See :class:`ot.utils.DataScaler` for a backend-aware scaler that supports joint fitting on multiple distributions. Returns ------- cost: float Sliced Wasserstein Cost log : dict, optional log dictionary return only if log==True in parameters Examples -------- >>> import numpy as np >>> n_samples_a = 20 >>> X = np.random.normal(0., 1., (n_samples_a, 5)) >>> sliced_wasserstein_distance(X, X, seed=0) # doctest: +NORMALIZE_WHITESPACE 0.0 References ---------- .. [31] Bonneel, Nicolas, et al. "Sliced and radon wasserstein barycenters of measures." Journal of Mathematical Imaging and Vision 51.1 (2015): 22-45 """ X_s, X_t = list_to_array(X_s, X_t) nx = get_backend(X_s, X_t, a, b, projections) X_s, X_t = apply_scaler(X_s, X_t, scaler) n = X_s.shape[0] m = X_t.shape[0] if X_s.shape[1] != X_t.shape[1]: raise ValueError( "X_s and X_t must have the same number of dimensions {} and {} respectively given".format( X_s.shape[1], X_t.shape[1] ) ) if a is None: a = nx.full(n, 1 / n, type_as=X_s) if b is None: b = nx.full(m, 1 / m, type_as=X_s) d = X_s.shape[1] if projections is None: projections = get_random_projections( d, n_projections, seed, backend=nx, type_as=X_s ) else: n_projections = projections.shape[1] X_s_projections = nx.dot(X_s, projections) X_t_projections = nx.dot(X_t, projections) projected_emd = wasserstein_1d(X_s_projections, X_t_projections, a, b, p=p) res = (nx.sum(projected_emd) / n_projections) ** (1.0 / p) if log: return res, {"projections": projections, "projected_emds": projected_emd} return res def max_sliced_wasserstein_distance( X_s, X_t, a=None, b=None, n_projections=50, p=2, projections=None, seed=None, log=False, scaler=None, ): r""" Computes a Monte-Carlo approximation of the max p-Sliced Wasserstein distance .. math:: \mathcal{Max-SWD}_p(\mu, \nu) = \underset{\theta \in \mathcal{U}(\mathbb{S}^{d-1})}{\max} [\mathcal{W}_p^p(\theta_\# \mu, \theta_\# \nu)]^{\frac{1}{p}} where : - :math:`\theta_\# \mu` stands for the pushforwards of the projection :math:`\mathbb{R}^d \ni X \mapsto \langle \theta, X \rangle` Parameters ---------- X_s : ndarray, shape (n_samples_a, dim) samples in the source domain X_t : ndarray, shape (n_samples_b, dim) samples in the target domain a : ndarray, shape (n_samples_a,), optional samples weights in the source domain b : ndarray, shape (n_samples_b,), optional samples weights in the target domain n_projections : int, optional Number of projections used for the Monte-Carlo approximation p: float, optional = Power p used for computing the sliced Wasserstein projections: shape (dim, n_projections), optional Projection matrix (n_projections and seed are not used in this case) seed: int or RandomState or None, optional Seed used for random number generator log: bool, optional if True, sliced_wasserstein_distance returns the projections used and their associated EMD. scaler : None, object with .transform(), or callable, optional Preprocessing applied to X_s and X_t before computing the distance. Useful for normalizing inputs when features have very different scales. - ``None`` : no preprocessing (default) - Object with ``.transform()`` method : e.g. an :class:`ot.utils.DataScaler` fitted on a representative sample. This is the recommended way to get stable, consistent normalization across multiple calls (e.g. when using SWD as a loss in mini-batch training). - Callable : any function, lambda, or PyTorch transform applied directly as ``scaler(X_s)`` and ``scaler(X_t)``. See :class:`ot.utils.DataScaler` for a backend-aware scaler that supports joint fitting on multiple distributions. Returns ------- cost: float Sliced Wasserstein Cost log : dict, optional log dictionary return only if log==True in parameters Examples -------- >>> import numpy as np >>> n_samples_a = 20 >>> X = np.random.normal(0., 1., (n_samples_a, 5)) >>> sliced_wasserstein_distance(X, X, seed=0) # doctest: +NORMALIZE_WHITESPACE 0.0 References ---------- .. [35] Deshpande, I., Hu, Y. T., Sun, R., Pyrros, A., Siddiqui, N., Koyejo, S., ... & Schwing, A. G. (2019). Max-sliced wasserstein distance and its use for gans. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (pp. 10648-10656). """ X_s, X_t = list_to_array(X_s, X_t) nx = get_backend(X_s, X_t, a, b, projections) X_s, X_t = apply_scaler(X_s, X_t, scaler) n = X_s.shape[0] m = X_t.shape[0] if X_s.shape[1] != X_t.shape[1]: raise ValueError( "X_s and X_t must have the same number of dimensions {} and {} respectively given".format( X_s.shape[1], X_t.shape[1] ) ) if a is None: a = nx.full(n, 1 / n, type_as=X_s) if b is None: b = nx.full(m, 1 / m, type_as=X_s) d = X_s.shape[1] if projections is None: projections = get_random_projections( d, n_projections, seed, backend=nx, type_as=X_s ) X_s_projections = nx.dot(X_s, projections) X_t_projections = nx.dot(X_t, projections) projected_emd = wasserstein_1d(X_s_projections, X_t_projections, a, b, p=p) res = nx.max(projected_emd) ** (1.0 / p) if log: return res, {"projections": projections, "projected_emds": projected_emd} return res python-pot-0.9.7+dfsg/ot/sliced/_sliced_plans.py000066400000000000000000000430441525521106200216310ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Sliced Wasserstein plans approximation solvers: min-pivot sliced (=sliced Wasserstein Generalized Geodesic) and expected sliced. """ # Author: Eloi Tanguy # Laetitia Chapel # # License: MIT License import warnings from ..backend import get_backend from ..utils import list_to_array, sparse_ot_dist, dist from ._utils import get_random_projections from ..lp import wasserstein_1d from collections import namedtuple def sliced_plans( X_s, X_t, a=None, b=None, metric="sqeuclidean", p=1, projections=None, n_projections=None, seed=None, batch_size=None, log=False, ): r""" Computes all the permutations that sort the projections of two `(ns, nt)` datasets `X_s` and `X_t` on the directions `projections`. Each permutation `perm[:, k]` is such that each :math:`X_s[i, :]` is matched to `X_t[perm[i, k], :]` when projected on `projections[k, :]`. Parameters ---------- X_s : array-like, shape (ns, d) The first set of vectors. X_t : array-like, shape (nt, d) The second set of vectors. a : ndarray of float64, shape (ns,), optional Source histogram (default is uniform weight) b : ndarray of float64, shape (nt,), optional Target histogram (default is uniform weight) metric: str, optional (default='sqeuclidean') Metric to be used. Only works with either of the strings `'sqeuclidean'`, `'minkowski'`, `'cityblock'`, or `'euclidean'`. p: float, optional (default=1.0) The p-norm to apply for if metric='minkowski' projections: shape (dim, n_projections), optional Projection matrix (n_projections and seed are not used in this case) n_projections : int, optional The number of projection directions. Required if projections is None. seed : int, optional The seed for the random number generator for sampling projections, in case projections is None. Default is None. batch_size : int, optional If specified, compute the distance in batches of size `batch_size` to avoid memory issues for large datasets. Default is None (no batching). log : bool, optional If True, returns additional logging information. Default is False. Returns ------- plan: list of dictionaries List of the optimal transport plans as a list of dictionaries containing the rows, cols and data of the non-zero elements of the transportation matrix. costs : list of float The cost associated to each projection. log_dict : dict, optional A dictionary containing intermediate computations for logging purposes. Returned only if `log` is True. """ nx = get_backend(X_s, X_t, a, b, projections) assert X_s.ndim == 2, f"X_s must be a 2d array, got {X_s.ndim}d array instead" assert X_t.ndim == 2, f"X_t must be a 2d array, got {X_t.ndim}d array instead" assert metric in ("minkowski", "euclidean", "cityblock", "sqeuclidean"), ( "Sliced plans work only with metrics " + "from the following list: " + "`['sqeuclidean', 'minkowski', 'cityblock', 'euclidean']`" ) assert ( X_s.shape[1] == X_t.shape[1] ), f"X_s ({X_s.shape}) and X_t ({X_t.shape}) must have the same number of columns" if metric == "euclidean": p = 2 elif metric == "cityblock": p = 1 d = X_s.shape[1] n = X_s.shape[0] m = X_t.shape[0] if a is None: a = nx.ones(n) / n if b is None: b = nx.ones(m) / m is_perm = (n == m) and (a == a.sum() / n).all() and (b == a.sum() / n).all() if projections is None: # create projections (n_projections, d) assert ( n_projections is not None ), "n_projections must be specified if projections is None" projections = get_random_projections( d, n_projections, seed, backend=nx, type_as=X_s ) else: n_projections = projections.shape[1] # projections on the line X_s_projections = nx.dot(X_s, projections) X_t_projections = nx.dot(X_t, projections) if is_perm: # we compute maps (permutations) # sigma[:, i_projections] is a permutation sorting Xs_projections[:, i_projections] sigma = nx.argsort(X_s_projections, axis=0) # (n, n_projections) tau = nx.argsort(X_t_projections, axis=0) # (m, n_projections) costs = [ sparse_ot_dist( X_s, X_t, sigma[:, k], tau[:, k], metric=metric, p=p, batch_size=batch_size, ) for k in range(n_projections) ] PlanTuple = namedtuple("PlanTuple", ["data", "rows", "cols"]) plans = [ PlanTuple( data=a, rows=sigma[:, k], cols=tau[:, k], ) for k in range(n_projections) ] else: # we compute plans _, plans = wasserstein_1d( X_s_projections, X_t_projections, a, b, p, require_sort=True, return_plans="coo_tuple", ) costs = [ sparse_ot_dist( X_s, X_t, plans[k].rows, plans[k].cols, plans[k].data, metric=metric, p=p, batch_size=batch_size, ) for k in range(n_projections) ] if log: log_dict = { "X_projection": X_s_projections, "Y_projection": X_t_projections, "projections": projections, } return plans, nx.stack(costs), log_dict else: return plans, nx.stack(costs) def min_sliced_transport_plan( X_s, X_t, a=None, b=None, projections=None, metric="sqeuclidean", p=2, n_projections=None, seed=None, batch_size=None, dense=True, log=False, ): r""" Computes the cost and permutation associated to the min-Pivot Sliced Discrepancy (introduced as min-SWGG in [85] and studied further in [86]). Given the supports `X_s` and `X_t` of two discrete uniform measures with `ns` and `nt` atoms in dimension `d`, the min-Pivot Sliced Discrepancy goes through `n_projections` different projections of the measures on random directions, and retains the couplings that yields the lowest cost between `X_s` and `X_t` (compared in :math:`\mathbb{R}^d`). When `ns=nt`, it gives .. math:: \mathrm{min\text{-}PS}_p^p(X_s, X_t) \approx \min_{k \in [1, n_{\mathrm{proj}}]} \left( \frac{1}{n_s} \sum_{i=1}^{n_s} \|X_{s,i} - X_{t,\sigma_k(i)}\|_2^p \right), where :math:`\sigma_k` is a permutation such that ordering the projections on the axis `projections[k, :]` matches :math:`X_s[i, :]` to :math:`X_t[\sigma_k(i), :]`. .. note:: The computation ignores potential ambiguities in the projections: if two points from a same measure have the same projection on a direction, then multiple sorting permutations are possible. To avoid combinatorial explosion, only one permutation is retained: this strays from theory in pathological cases. .. warning:: Tensorflow and jax only returns dense plans, as they do not support well sparse matrices. Parameters ---------- X_s : array-like, shape (ns, d) The first set of vectors. X_t : array-like, shape (nt, d) The second set of vectors. a : ndarray of float64, shape (ns,), optional Source histogram (default is uniform weight) b : ndarray of float64, shape (nt,), optional Target histogram (default is uniform weight) projections: shape (dim, n_projections), optional Projection matrix (n_projections and seed are not used in this case). Default is None metric: str, optional (default='sqeuclidean') Metric to be used. Only works with either of the strings `'sqeuclidean'`, `'minkowski'`, `'cityblock'`, or `'euclidean'`. p: float, optional (default=1.0) The p-norm to apply for if metric='minkowski' n_projections : int, optional The number of projection directions. Required if projections is None. seed : int, optional The seed for the random number generator for sampling projections, in case projections is None. Default is None. batch_size : int, optional If specified, compute the distance in batches of size `batch_size` to avoid memory issues for large datasets. Default is None (no batching). dense: boolean, optional (default=True) If True, returns :math:`\gamma` as a dense ndarray of shape (ns, nt). Otherwise returns a sparse representation using scipy's `coo_matrix` format. log : bool, optional If True, returns additional logging information. Default is False. Returns ------- plan : ndarray, shape (ns, nt) or coo_matrix if dense is False Optimal transportation matrix for the given parameters. cost : float The cost associated to the optimal permutation. log_dict : dict, optional A dictionary containing intermediate computations for logging purposes. Returned only if `log` is True. References ---------- .. [85] Mahey, G., Chapel, L., Gasso, G., Bonet, C., & Courty, N. (2023). Fast Optimal Transport through Sliced Generalized Wasserstein Geodesics. Advances in Neural Information Processing Systems, 36, 35350–35385. .. [86] Tanguy, E., Chapel, L., Delon, J. (2025). Sliced Transport Plans. arXiv preprint 2506.03661. Examples -------- >>> import ot >>> import numpy as np >>> x=np.array([[3.,3.], [1.,1.]]) >>> y=np.array([[2.,2.5], [3.,2.]]) >>> projections=np.array([[1, 0], [0, 1]]) >>> plan, cost = ot.min_sliced_transport_plan(x, y, projections=projections) >>> plan array([[0. , 0.5], [0.5, 0. ]]) >>> cost 2.125 """ nx = get_backend(X_s, X_t, a, b, projections) assert X_s.ndim == 2, f"X_s must be a 2d array, got {X_s.ndim}d array instead" assert X_t.ndim == 2, f"X_t must be a 2d array, got {X_t.ndim}d array instead" assert ( X_s.shape[1] == X_t.shape[1] ), f"X_s ({X_s.shape}) and X_t ({X_t.shape}) must have the same number of columns" if str(nx) in ["tf", "jax"] and not dense: dense = True warnings.warn("JAX and TF do not support sparse matrices, converting to dense") log_dict = {} plans, costs, log_dict_plans = sliced_plans( X_s, X_t, a, b, metric, p, projections, n_projections=n_projections, seed=seed, batch_size=batch_size, log=True, ) pos_min = nx.argmin(costs) cost = costs[pos_min] plan = plans[pos_min] if log: log_dict = { "projections": log_dict_plans["projections"], "costs": costs, "min_projection": log_dict_plans["projections"][:, pos_min], "X_min_projection": log_dict_plans["X_projection"][:, pos_min], "Y_min_projection": log_dict_plans["Y_projection"][:, pos_min], } # get the plan from the indices of the non-zero entries of the sparse plan plan = nx.coo_matrix( plan.data, plan.rows, plan.cols, shape=(X_s.shape[0], X_t.shape[0]), type_as=X_s, ) if dense: plan = nx.todense(plan) if log: return plan, cost, log_dict else: return plan, cost def expected_sliced_plan( X_s, X_t, a=None, b=None, projections=None, metric="sqeuclidean", p=2, n_projections=None, beta=0.0, seed=None, dense=True, batch_size=None, log=False, ): r""" Computes the Expected Sliced cost and plan between two datasets `X_s` and `X_t` of shapes `(ns, d)` and `(nt, d)`. Given a set of `n_projections` projection directions, the expected sliced plan is obtained by averaging the `n_projections` 1d optimal transport plans between the projections of `X_s` and `X_t` on each direction. Expected Sliced was introduced in [87] and further studied in [86]. .. note:: The computation ignores potential ambiguities in the projections: if two points from a same measure have the same projection on a direction, then multiple sorting permutations are possible. To avoid combinatorial explosion, only one permutation is retained: this strays from theory in pathological cases. .. warning:: Tensorflow and jax only returns dense plans, as they do not support well sparse matrices. Parameters ---------- X_s : array-like, shape (ns, d) The first set of vectors. X_t : array-like, shape (nt, d) The second set of vectors. a : ndarray of float64, shape (ns,), optional Source histogram (default is uniform weight) b : ndarray of float64, shape (nt,), optional Target histogram (default is uniform weight) projections: shape (dim, n_projections), optional Projection matrix (n_projections and seed are not used in this case). Default is None metric: str, optional (default='sqeuclidean') Metric to be used. Only works with either of the strings `'sqeuclidean'`, `'minkowski'`, `'cityblock'`, or `'euclidean'`. p: float, optional (default=2) The p-norm to apply for if metric='minkowski' n_projections : int, optional The number of projection directions. Required if projections is None. seed : int, optional The seed for the random number generator for sampling projections, in case projections is None. Default is None. beta : float, optional Inverse-temperature parameter which weights each projection's contribution to the expected plan. Default is 0 (uniform weighting). dense: boolean, optional (default=True) If True, returns :math:`\gamma` as a dense ndarray of shape (ns, nt). Otherwise returns a sparse representation using scipy's `coo_matrix` format. batch_size : int, optional If specified, compute the distance in batches of size `batch_size` to avoid memory issues for large datasets. Default is None (no batching). log : bool, optional If True, returns additional logging information. Default is False. Returns ------- plan : ndarray, shape (ns, nt) or coo_matrix if dense is False Optimal transportation matrix for the given parameters. cost : float The cost associated to the optimal permutation. log_dict : dict, optional A dictionary containing intermediate computations for logging purposes. Returned only if `log` is True. References ---------- .. [86] Tanguy, E., Chapel, L., Delon, J. (2025). Sliced Transport Plans. arXiv preprint 2506.03661. .. [87] Liu, X., Diaz Martin, R., Bai Y., Shahbazi A., Thorpe M., Aldroubi A., Kolouri, S. (2024). Expected Sliced Transport Plans. International Conference on Learning Representations. Examples -------- >>> import ot >>> import numpy as np >>> x=np.array([[3.,3.], [1.,1.]]) >>> y=np.array([[2.,2.5], [3.,2.]]) >>> projections=np.array([[1, 0], [0, 1]]) >>> plan, cost = ot.expected_sliced_plan(x, y, projections=projections) >>> plan array([[0.25, 0.25], [0.25, 0.25]]) >>> cost 2.625 """ nx = get_backend(X_s, X_t, a, b, projections) assert X_s.ndim == 2, f"X_s must be a 2d array, got {X_s.ndim}d array instead" assert X_t.ndim == 2, f"X_t must be a 2d array, got {X_t.ndim}d array instead" assert ( X_s.shape[1] == X_t.shape[1] ), f"X_s ({X_s.shape}) and X_t ({X_t.shape}) must have the same number of columns" if str(nx) in ["tf", "jax"] and not dense: dense = True warnings.warn("JAX and TF do not support sparse matrices, converting to dense") n = X_s.shape[0] m = X_t.shape[0] log_dict = {} plans, costs, log_dict_plans = sliced_plans( X_s, X_t, a, b, metric, p, projections, n_projections=n_projections, seed=seed, log=True, batch_size=batch_size, ) if beta != 0.0: # computing the temperature weighting log_factors = -beta * list_to_array(costs) weights = nx.exp(log_factors - nx.logsumexp(log_factors)) cost = nx.sum(list_to_array(costs) * weights) else: # uniform weights if n_projections is None: n_projections = projections.shape[1] weights = nx.ones(n_projections) / n_projections weights_e = nx.concatenate([plans[i].data * weights[i] for i in range(len(plans))]) Xs_idx = nx.concatenate([plans[i].rows for i in range(len(plans))]) Xt_idx = nx.concatenate([plans[i].cols for i in range(len(plans))]) plan = nx.coo_matrix(weights_e, Xs_idx, Xt_idx, shape=(n, m), type_as=weights) if dense: plan = nx.todense(plan) if beta == 0.0: if dense: cost = nx.sum(plan * dist(X_s, X_t, metric=metric, p=p)) else: cost = plan.multiply(dist(X_s, X_t, metric=metric, p=p)).sum() if log: log_dict = { "projections": log_dict_plans["projections"], "costs": costs, "G": plans, "weights": weights, } return plan, cost, log_dict else: return plan, cost python-pot-0.9.7+dfsg/ot/sliced/_spherical_sliced.py000066400000000000000000000237071525521106200224720ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Sliced Wasserstein distances on the Sphere solvers. """ # Author: Nicolas Courty # Author: Clément Bonet # # License: MIT License from ..backend import get_backend from ._utils import get_projections_sphere, projection_sphere_to_circle from ..lp import ( wasserstein_circle, semidiscrete_wasserstein2_unif_circle, linear_circular_ot, ) def sliced_wasserstein_sphere( X_s, X_t, a=None, b=None, n_projections=50, p=2, projections=None, seed=None, log=False, ): r""" Compute the spherical sliced-Wasserstein discrepancy. .. math:: SSW_p(\mu,\nu) = \left(\int_{\mathbb{V}_{d,2}} W_p^p(P^U_\#\mu, P^U_\#\nu)\ \mathrm{d}\sigma(U)\right)^{\frac{1}{p}} where: - :math:`P^U_\# \mu` stands for the pushforwards of the projection :math:`\forall x\in S^{d-1},\ P^U(x) = \frac{U^Tx}{\|U^Tx\|_2}` The function runs on backend but tensorflow and jax are not supported. Parameters ---------- X_s: ndarray, shape (n_samples_a, dim) Samples in the source domain X_t: ndarray, shape (n_samples_b, dim) Samples in the target domain a : ndarray, shape (n_samples_a,), optional samples weights in the source domain b : ndarray, shape (n_samples_b,), optional samples weights in the target domain n_projections : int, optional Number of projections used for the Monte-Carlo approximation p: float, optional (default=2) Power p used for computing the spherical sliced Wasserstein projections: shape (n_projections, dim, 2), optional Projection matrix (n_projections and seed are not used in this case) seed: int or RandomState or None, optional Seed used for random number generator log: bool, optional if True, sliced_wasserstein_sphere returns the projections used and their associated EMD. Returns ------- cost: float Spherical Sliced Wasserstein Cost log: dict, optional log dictionary return only if log==True in parameters Examples -------- >>> import ot >>> import numpy as np >>> n_samples_a = 20 >>> X = np.random.normal(0., 1., (n_samples_a, 5)) >>> X = X / np.sqrt(np.sum(X**2, -1, keepdims=True)) >>> ot.sliced_wasserstein_sphere(X, X, seed=0) # doctest: +NORMALIZE_WHITESPACE 0.0 References ---------- .. [46] Bonet, C., Berg, P., Courty, N., Septier, F., Drumetz, L., & Pham, M. T. (2023). Spherical sliced-wasserstein. International Conference on Learning Representations. """ d = X_s.shape[-1] nx = get_backend(X_s, X_t, a, b) if X_s.shape[1] != X_t.shape[1]: raise ValueError( "X_s and X_t must have the same number of dimensions {} and {} respectively given".format( X_s.shape[1], X_t.shape[1] ) ) if nx.any(nx.abs(nx.sum(X_s**2, axis=-1) - 1) > 10 ** (-4)): raise ValueError("X_s is not on the sphere.") if nx.any(nx.abs(nx.sum(X_t**2, axis=-1) - 1) > 10 ** (-4)): raise ValueError("X_t is not on the sphere.") if projections is None: projections = get_projections_sphere( d, n_projections, seed=seed, backend=nx, type_as=X_s ) Xps_coords, _ = projection_sphere_to_circle( X_s, n_projections=n_projections, projections=projections, seed=seed, backend=nx ) Xpt_coords, _ = projection_sphere_to_circle( X_t, n_projections=n_projections, projections=projections, seed=seed, backend=nx ) projected_emd = wasserstein_circle( Xps_coords.T, Xpt_coords.T, u_weights=a, v_weights=b, p=p ) res = nx.mean(projected_emd) ** (1 / p) if log: return res, {"projections": projections, "projected_emds": projected_emd} return res def sliced_wasserstein_sphere_unif( X_s, a=None, n_projections=50, projections=None, seed=None, log=False ): r"""Compute the 2-spherical sliced wasserstein w.r.t. a uniform distribution. .. math:: SSW_2(\mu_n, \nu) where - :math:`\mu_n=\sum_{i=1}^n \alpha_i \delta_{x_i}` - :math:`\nu=\mathrm{Unif}(S^{d-1})` Parameters ---------- X_s: ndarray, shape (n_samples_a, dim) Samples in the source domain a : ndarray, shape (n_samples_a,), optional samples weights in the source domain n_projections : int, optional Number of projections used for the Monte-Carlo approximation projections: shape (n_projections, dim, 2), optional Projection matrix (n_projections and seed are not used in this case) seed: int or RandomState or None, optional Seed used for random number generator log: bool, optional if True, sliced_wasserstein_distance returns the projections used and their associated EMD. Returns ------- cost: float Spherical Sliced Wasserstein Cost log: dict, optional log dictionary return only if log==True in parameters Examples --------- >>> import ot >>> import numpy as np >>> np.random.seed(42) >>> x0 = np.random.randn(500,3) >>> x0 = x0 / np.sqrt(np.sum(x0**2, -1, keepdims=True)) >>> ssw = ot.sliced_wasserstein_sphere_unif(x0, seed=42) >>> np.allclose(ot.sliced_wasserstein_sphere_unif(x0, seed=42), 0.01734, atol=1e-3) True References: ----------- .. [46] Bonet, C., Berg, P., Courty, N., Septier, F., Drumetz, L., & Pham, M. T. (2023). Spherical sliced-wasserstein. International Conference on Learning Representations. """ d = X_s.shape[-1] nx = get_backend(X_s, a) if nx.any(nx.abs(nx.sum(X_s**2, axis=-1) - 1) > 10 ** (-4)): raise ValueError("X_s is not on the sphere.") if projections is None: projections = get_projections_sphere( d, n_projections, seed=seed, backend=nx, type_as=X_s ) Xps_coords, _ = projection_sphere_to_circle( X_s, n_projections=n_projections, projections=projections, seed=seed, backend=nx ) projected_emd = semidiscrete_wasserstein2_unif_circle(Xps_coords.T, u_weights=a) res = nx.mean(projected_emd) ** (1 / 2) if log: return res, {"projections": projections, "projected_emds": projected_emd} return res def linear_sliced_wasserstein_sphere( X_s, X_t=None, a=None, b=None, n_projections=50, projections=None, seed=None, log=False, ): r"""Computes the linear spherical sliced wasserstein distance from :ref:`[79] `. General loss returned: .. math:: \mathrm{LSSOT}_2(\mu, \nu) = \left(\int_{\mathbb{V}_{d,2}} \mathrm{LCOT}_2^2(P^U_\#\mu, P^U_\#\nu)\ \mathrm{d}\sigma(U)\right)^{\frac12}, where :math:`\mu,\nu\in\mathcal{P}(S^{d-1})` are two probability measures on the sphere, :math:`\mathrm{LCOT}_2` is the linear circular optimal transport distance, and :math:`P^U_\# \mu` stands for the pushforwards of the projection :math:`\forall x\in S^{d-1},\ P^U(x) = \frac{U^Tx}{\|U^Tx\|_2}`. Parameters ---------- X_s: ndarray, shape (n_samples_a, dim) Samples in the source domain X_t: ndarray, shape (n_samples_b, dim), optional Samples in the target domain. If None, computes the distance against the uniform distribution on the sphere. a : ndarray, shape (n_samples_a,), optional samples weights in the source domain b : ndarray, shape (n_samples_b,), optional samples weights in the target domain n_projections : int, optional Number of projections used for the Monte-Carlo approximation projections: shape (n_projections, dim, 2), optional Projection matrix (n_projections and seed are not used in this case) seed: int or RandomState or None, optional Seed used for random number generator log: bool, optional if True, linear_sliced_wasserstein_sphere returns the projections used and their associated LCOT. Returns ------- cost: float Linear Spherical Sliced Wasserstein Cost log: dict, optional log dictionary return only if log==True in parameters Examples --------- >>> import ot >>> import numpy as np >>> n_samples_a = 20 >>> X = np.random.normal(0., 1., (n_samples_a, 5)) >>> X = X / np.sqrt(np.sum(X**2, -1, keepdims=True)) >>> ot.linear_sliced_wasserstein_sphere(X, X, seed=0) # doctest: +NORMALIZE_WHITESPACE 0.0 .. _references-lssot: References ---------- .. [79] Liu, X., Bai, Y., Martín, R. D., Shi, K., Shahbazi, A., Landman, B. A., Chang, C., & Kolouri, S. (2025). Linear Spherical Sliced Optimal Transport: A Fast Metric for Comparing Spherical Data. International Conference on Learning Representations. """ d = X_s.shape[-1] nx = get_backend(X_s, X_t, a, b) if X_s.shape[1] != X_t.shape[1]: raise ValueError( "X_s and X_t must have the same number of dimensions {} and {} \ respectively given".format(X_s.shape[1], X_t.shape[1]) ) if nx.any(nx.abs(nx.sum(X_s**2, axis=-1) - 1) > 10 ** (-4)): raise ValueError("X_s is not on the sphere.") if nx.any(nx.abs(nx.sum(X_t**2, axis=-1) - 1) > 10 ** (-4)): raise ValueError("X_t is not on the sphere.") if projections is None: projections = get_projections_sphere( d, n_projections, seed=seed, backend=nx, type_as=X_s ) Xps_coords, _ = projection_sphere_to_circle( X_s, n_projections=n_projections, projections=projections, seed=seed, backend=nx ) if X_t is not None: Xpt_coords, _ = projection_sphere_to_circle( X_t, n_projections=n_projections, projections=projections, seed=seed, backend=nx, ) projected_lcot = linear_circular_ot( Xps_coords.T, Xpt_coords.T, u_weights=a, v_weights=b ) res = nx.mean(projected_lcot) ** (1 / 2) if log: return res, {"projections": projections, "projected_emds": projected_lcot} return res python-pot-0.9.7+dfsg/ot/sliced/_utils.py000066400000000000000000000120641525521106200203270ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Useful functions for solvers for the (balanced) sliced transport problem. """ # Author: Adrien Corenflos # Nicolas Courty # Rémi Flamary # Eloi Tanguy # Laetitia Chapel # Clément Bonet # # License: MIT License import numpy as np from ..backend import get_backend, NumpyBackend from ..utils import get_coordinate_circle def get_random_projections(d, n_projections, seed=None, backend=None, type_as=None): r""" Generates n_projections samples from the uniform on the unit sphere of dimension :math:`d-1`: :math:`\mathcal{U}(\mathcal{S}^{d-1})` Parameters ---------- d : int dimension of the space n_projections : int number of samples requested seed: int or RandomState, optional Seed used for numpy random number generator backend: Backend to use for random generation type_as: type, optional Type of the returned array Returns ------- out: ndarray, shape (d, n_projections) The uniform unit vectors on the sphere Examples -------- >>> n_projections = 100 >>> d = 5 >>> projs = get_random_projections(d, n_projections) >>> np.allclose(np.sum(np.square(projs), 0), 1.) # doctest: +NORMALIZE_WHITESPACE True """ if backend is None: nx = NumpyBackend() else: nx = backend if isinstance(seed, np.random.RandomState) and str(nx) == "numpy": projections = seed.randn(d, n_projections) else: if seed is not None: nx.seed(seed) projections = nx.randn(d, n_projections, type_as=type_as) projections = projections / nx.sqrt(nx.sum(projections**2, 0, keepdims=True)) return projections def get_projections_sphere(d, n_projections, seed=None, backend=None, type_as=None): r""" Generates n_projections samples from the uniform distribution on the Stiefel manifold of dimension :math:`d\times 2`: :math:`\mathbb{V}_{d,2}=\{X \in \mathbb{R}^{d\times 2}, X^TX=I_2\}` Parameters ---------- d : int dimension of the space n_projections : int number of samples requested seed: int or RandomState, optional Seed used for numpy random number generator backend: Backend to use for random generation type_as: optional Type to use for random generation Returns ------- out: ndarray, shape (n_projections, d, 2) Examples -------- >>> n_projections = 100 >>> d = 5 >>> projs = get_projections_sphere(d, n_projections) >>> np.allclose(np.einsum("nij, nik -> njk", projs, projs), np.eye(2)) # doctest: +NORMALIZE_WHITESPACE True """ if backend is None: nx = NumpyBackend() else: nx = backend if isinstance(seed, np.random.RandomState) and str(nx) == "numpy": Z = seed.randn(n_projections, d, 2) else: if seed is not None: nx.seed(seed) Z = nx.randn(n_projections, d, 2, type_as=type_as) projections, _ = nx.qr(Z) return projections def projection_sphere_to_circle( x, n_projections=50, projections=None, seed=None, backend=None ): r""" Projection of :math:`x\in S^{d-1}` on circles using coordinates on [0,1[. To get the projection on the circle, we use the following formula: .. math:: P^U(x) = \frac{U^Tx}{\|U^Tx\|_2} where :math:`U` is a random matrix sampled from the uniform distribution on the Stiefel manifold of dimension :math:`d\times 2`: :math:`\mathbb{V}_{d,2}=\{X \in \mathbb{R}^{d\times 2}, X^TX=I_2\}` and :math:`x` is a point on the sphere. Then, we apply the function get_coordinate_circle to get the coordinates on :math:`[0,1[`. Parameters ---------- x : ndarray, shape (n_samples, dim) samples on the sphere n_projections : int, optional Number of projections used for the Monte-Carlo approximation projections: shape (n_projections, dim, 2), optional Projection matrix (n_projections and seed are not used in this case) seed: int or RandomState or None, optional Seed used for random number generator backend: Backend to use for random generation Returns ------- Xp_coords: ndarray, shape (n_projections, n_samples) Coordinates of the projections on the circle """ if backend is None: nx = get_backend(x) else: nx = backend n, d = x.shape if projections is None: projections = get_projections_sphere( d, n_projections, seed=seed, backend=nx, type_as=x ) # Projection on S^1 # Projection on plane Xp = nx.einsum("ikj, lk -> ilj", projections, x) # Projection on sphere Xp = Xp / nx.sqrt(nx.sum(Xp**2, -1, keepdims=True)) # Get coordinates on [0,1[ Xp_coords = nx.reshape( get_coordinate_circle(nx.reshape(Xp, (-1, 2))), (n_projections, n) ) return Xp_coords, projections python-pot-0.9.7+dfsg/ot/smooth.py000066400000000000000000000552441525521106200171050ustar00rootroot00000000000000# Copyright (c) 2018, Mathieu Blondel # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # 2. 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. # Author: Mathieu Blondel # Remi Flamary # Tianlin Liu """ Smooth and Sparse (KL an L2 reg.) and sparsity-constrained OT solvers. Implementation of : Smooth and Sparse Optimal Transport. Mathieu Blondel, Vivien Seguy, Antoine Rolet. In Proc. of AISTATS 2018. https://arxiv.org/abs/1710.06276 (Original code from https://github.com/mblondel/smooth-ot/) Sparsity-Constrained Optimal Transport. Liu, T., Puigcerver, J., & Blondel, M. (2023). Sparsity-constrained optimal transport. Proceedings of the Eleventh International Conference on Learning Representations (ICLR). https://arxiv.org/abs/2209.15466 [17] Blondel, M., Seguy, V., & Rolet, A. (2018). Smooth and Sparse Optimal Transport. Proceedings of the Twenty-First International Conference on Artificial Intelligence and Statistics (AISTATS). [50] Liu, T., Puigcerver, J., & Blondel, M. (2023). Sparsity-constrained optimal transport. Proceedings of the Eleventh International Conference on Learning Representations (ICLR). """ import numpy as np from scipy.optimize import minimize from .backend import get_backend import ot def projection_simplex(V, z=1, axis=None): r"""Projection of :math:`\mathbf{V}` onto the simplex, scaled by `z` .. math:: P\left(\mathbf{V}, z\right) = \mathop{\arg \min}_{\substack{\mathbf{y} >= 0 \\ \sum_i \mathbf{y}_i = z}} \quad \|\mathbf{y} - \mathbf{V}\|^2 Parameters ---------- V: ndarray, rank 2 z: float or array If array, len(z) must be compatible with :math:`\mathbf{V}` axis: None or int - axis=None: project :math:`\mathbf{V}` by :math:`P(\mathbf{V}.\mathrm{ravel}(), z)` - axis=1: project each :math:`\mathbf{V}_i` by :math:`P(\mathbf{V}_i, z_i)` - axis=0: project each :math:`\mathbf{V}_{:, j}` by :math:`P(\mathbf{V}_{:, j}, z_j)` Returns ------- projection: ndarray, shape :math:`\mathbf{V}`.shape """ if axis == 1: n_features = V.shape[1] U = np.sort(V, axis=1)[:, ::-1] z = np.ones(len(V)) * z cssv = np.cumsum(U, axis=1) - z[:, np.newaxis] ind = np.arange(n_features) + 1 cond = U - cssv / ind > 0 rho = np.count_nonzero(cond, axis=1) theta = cssv[np.arange(len(V)), rho - 1] / rho return np.maximum(V - theta[:, np.newaxis], 0) elif axis == 0: return projection_simplex(V.T, z, axis=1).T else: V = V.ravel().reshape(1, -1) return projection_simplex(V, z, axis=1).ravel() class Regularization(object): r"""Base class for Regularization objects Notes ----- This class is not intended for direct use but as apparent for true regularization implementation. """ def __init__(self, gamma=1.0): """ Parameters ---------- gamma: float Regularization parameter. We recover unregularized OT when gamma -> 0. """ self.gamma = gamma def delta_Omega(X): r""" Compute :math:`\delta_\Omega(\mathbf{X}_{:, j})` for each :math:`\mathbf{X}_{:, j}`. .. math:: \delta_\Omega(\mathbf{x}) = \sup_{\mathbf{y} >= 0} \ \mathbf{y}^T \mathbf{x} - \Omega(\mathbf{y}) Parameters ---------- X: array, shape = (len(a), len(b)) Input array. Returns ------- v: array, (len(b), ) Values: :math:`\mathbf{v}_j = \delta_\Omega(\mathbf{X}_{:, j})` G: array, (len(a), len(b)) Gradients: :math:`\mathbf{G}_{:, j} = \nabla \delta_\Omega(\mathbf{X}_{:, j})` """ raise NotImplementedError def max_Omega(X, b): r""" Compute :math:`\mathrm{max}_{\Omega, j}(\mathbf{X}_{:, j})` for each :math:`\mathbf{X}_{:, j}`. .. math:: \mathrm{max}_{\Omega, j}(\mathbf{x}) = \sup_{\substack{\mathbf{y} >= 0 \ \sum_i \mathbf{y}_i = 1}} \mathbf{y}^T \mathbf{x} - \frac{1}{\mathbf{b}_j} \Omega(\mathbf{b}_j \mathbf{y}) Parameters ---------- X: array, shape = (len(a), len(b)) Input array. b: array, shape = (len(b), ) Returns ------- v: array, (len(b), ) Values: :math:`\mathbf{v}_j = \mathrm{max}_{\Omega, j}(\mathbf{X}_{:, j})` G: array, (len(a), len(b)) Gradients: :math:`\mathbf{G}_{:, j} = \nabla \mathrm{max}_{\Omega, j}(\mathbf{X}_{:, j})` """ raise NotImplementedError def Omega(T): """ Compute regularization term. Parameters ---------- T: array, shape = len(a) x len(b) Input array. Returns ------- value: float Regularization term. """ raise NotImplementedError class NegEntropy(Regularization): """NegEntropy regularization""" def delta_Omega(self, X): G = np.exp(X / self.gamma - 1) val = self.gamma * np.sum(G, axis=0) return val, G def max_Omega(self, X, b): max_X = np.max(X, axis=0) / self.gamma exp_X = np.exp(X / self.gamma - max_X) val = self.gamma * (np.log(np.sum(exp_X, axis=0)) + max_X) val -= self.gamma * np.log(b) G = exp_X / np.sum(exp_X, axis=0) return val, G def Omega(self, T): return self.gamma * np.sum(T * np.log(T)) class SquaredL2(Regularization): """Squared L2 regularization""" def delta_Omega(self, X): max_X = np.maximum(X, 0) val = np.sum(max_X**2, axis=0) / (2 * self.gamma) G = max_X / self.gamma return val, G def max_Omega(self, X, b): G = projection_simplex(X / (b * self.gamma), axis=0) val = np.sum(X * G, axis=0) val -= 0.5 * self.gamma * b * np.sum(G * G, axis=0) return val, G def Omega(self, T): return 0.5 * self.gamma * np.sum(T**2) class SparsityConstrained(Regularization): """Squared L2 regularization with sparsity constraints""" def __init__(self, max_nz, gamma=1.0): self.max_nz = max_nz self.gamma = gamma def delta_Omega(self, X): # For each column of X, find entries that are not among the top max_nz. non_top_indices = np.argpartition(-X, self.max_nz, axis=0)[self.max_nz :] # Set these entries to -inf. if X.ndim == 1: X[non_top_indices] = 0.0 else: X[non_top_indices, np.arange(X.shape[1])] = 0.0 max_X = np.maximum(X, 0) val = np.sum(max_X**2, axis=0) / (2 * self.gamma) G = max_X / self.gamma return val, G def max_Omega(self, X, b): # Project the scaled X onto the simplex with sparsity constraint. G = ot.utils.projection_sparse_simplex( X / (b * self.gamma), self.max_nz, axis=0 ) val = np.sum(X * G, axis=0) val -= 0.5 * self.gamma * b * np.sum(G * G, axis=0) return val, G def Omega(self, T): return 0.5 * self.gamma * np.sum(T**2) def dual_obj_grad(alpha, beta, a, b, C, regul): r""" Compute objective value and gradients of dual objective. Parameters ---------- alpha: array, shape = len(a) beta: array, shape = len(b) Current iterate of dual potentials. a: array, shape = len(a) b: array, shape = len(b) Input histograms (should be non-negative and sum to 1). C: array, shape = (len(a), len(b)) Ground cost matrix. regul: Regularization object Should implement a `delta_Omega(X)` method. Returns ------- obj: float Objective value (higher is better). grad_alpha: array, shape = len(a) Gradient w.r.t. `alpha`. grad_beta: array, shape = len(b) Gradient w.r.t. `beta`. """ obj = np.dot(alpha, a) + np.dot(beta, b) grad_alpha = a.copy() grad_beta = b.copy() # X[:, j] = alpha + beta[j] - C[:, j] X = alpha[:, np.newaxis] + beta - C # val.shape = len(b) # G.shape = len(a) x len(b) val, G = regul.delta_Omega(X) obj -= np.sum(val) grad_alpha -= G.sum(axis=1) grad_beta -= G.sum(axis=0) return obj, grad_alpha, grad_beta def solve_dual( a, b, C, regul, method="L-BFGS-B", tol=1e-3, max_iter=500, verbose=False ): """ Solve the "smoothed" dual objective. Parameters ---------- a: array, shape = (len(a), ) b: array, shape = (len(b), ) Input histograms (should be non-negative and sum to 1). C: array, shape = (len(a), len(b)) Ground cost matrix. regul: Regularization object Should implement a `delta_Omega(X)` method. method: str Solver to be used (passed to `scipy.optimize.minimize`). tol: float Tolerance parameter. max_iter: int Maximum number of iterations. Returns ------- alpha: array, shape = (len(a), ) beta: array, shape = (len(b), ) Dual potentials. """ def _func(params): # Unpack alpha and beta. alpha = params[: len(a)] beta = params[len(a) :] obj, grad_alpha, grad_beta = dual_obj_grad(alpha, beta, a, b, C, regul) # Pack grad_alpha and grad_beta. grad = np.concatenate((grad_alpha, grad_beta)) # We need to maximize the dual. return -obj, -grad # Unfortunately, `minimize` only supports functions whose argument is a # vector. So, we need to concatenate alpha and beta. alpha_init = np.zeros(len(a)) beta_init = np.zeros(len(b)) params_init = np.concatenate((alpha_init, beta_init)) res = minimize( _func, params_init, method=method, jac=True, tol=tol, options=dict(maxiter=max_iter, disp=verbose), ) alpha = res.x[: len(a)] beta = res.x[len(a) :] return alpha, beta, res def semi_dual_obj_grad(alpha, a, b, C, regul): """ Compute objective value and gradient of semi-dual objective. Parameters ---------- alpha: array, shape = len(a) Current iterate of semi-dual potentials. a: array, shape = len(a) b: array, shape = len(b) Input histograms (should be non-negative and sum to 1). C: array, shape = (len(a), len(b)) Ground cost matrix. regul: Regularization object Should implement a `max_Omega(X)` method. Returns ------- obj: float Objective value (higher is better). grad: array, shape = len(a) Gradient w.r.t. alpha. """ obj = np.dot(alpha, a) grad = a.copy() # X[:, j] = alpha - C[:, j] X = alpha[:, np.newaxis] - C # val.shape = len(b) # G.shape = len(a) x len(b) val, G = regul.max_Omega(X, b) obj -= np.dot(b, val) grad -= np.dot(G, b) return obj, grad def solve_semi_dual( a, b, C, regul, method="L-BFGS-B", tol=1e-3, max_iter=500, verbose=False ): """ Solve the "smoothed" semi-dual objective. Parameters ---------- a: array, shape = (len(a), ) b: array, shape = (len(b), ) Input histograms (should be non-negative and sum to 1). C: array, shape = (len(a), len(b)) Ground cost matrix. regul: Regularization object Should implement a `max_Omega(X)` method. method: str Solver to be used (passed to `scipy.optimize.minimize`). tol: float Tolerance parameter. max_iter: int Maximum number of iterations. Returns ------- alpha: array, shape = (len(a), ) Semi-dual potentials. """ def _func(alpha): obj, grad = semi_dual_obj_grad(alpha, a, b, C, regul) # We need to maximize the semi-dual. return -obj, -grad alpha_init = np.zeros(len(a)) res = minimize( _func, alpha_init, method=method, jac=True, tol=tol, options=dict(maxiter=max_iter, disp=verbose), ) return res.x, res def get_plan_from_dual(alpha, beta, C, regul): r""" Retrieve optimal transportation plan from optimal dual potentials. Parameters ---------- alpha: array, shape = len(a) beta: array, shape = len(b) Optimal dual potentials. C: array, shape = (len(a), len(b)) Ground cost matrix. regul: Regularization object Should implement a `delta_Omega(X)` method. Returns ------- T: array, shape = (len(a), len(b)) Optimal transportation plan. """ X = alpha[:, np.newaxis] + beta - C return regul.delta_Omega(X)[1] def get_plan_from_semi_dual(alpha, b, C, regul): r""" Retrieve optimal transportation plan from optimal semi-dual potentials. Parameters ---------- alpha: array, shape = len(a) Optimal semi-dual potentials. b: array, shape = len(b) Second input histogram (should be non-negative and sum to 1). C: array, shape = (len(a), len(b)) Ground cost matrix. regul: Regularization object Should implement a `delta_Omega(X)` method. Returns ------- T: array, shape = (len(a), len(b)) Optimal transportation plan. """ X = alpha[:, np.newaxis] - C return regul.max_Omega(X, b)[1] * b def smooth_ot_dual( a, b, M, reg, reg_type="l2", method="L-BFGS-B", stopThr=1e-9, numItermax=500, verbose=False, log=False, max_nz=None, ): r""" Solve the regularized OT problem in the dual and return the OT matrix The function solves the smooth relaxed dual formulation (7) in :ref:`[17] `: .. math:: \max_{\alpha,\beta}\quad \mathbf{a}^T\alpha + \mathbf{b}^T\beta - \sum_j \delta_\Omega \left(\alpha+\beta_j-\mathbf{m}_j \right) where : - :math:`\mathbf{m}_j` is the j-th column of the cost matrix - :math:`\delta_\Omega` is the convex conjugate of the regularization term :math:`\Omega` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) The OT matrix can is reconstructed from the gradient of :math:`\delta_\Omega` (See :ref:`[17] ` Proposition 1). The optimization algorithm is using gradient decent (L-BFGS by default). Parameters ---------- a : np.ndarray (ns,) samples weights in the source domain b : np.ndarray (nt,) or np.ndarray (nt,nbb) samples in the target domain, compute sinkhorn with multiple targets and fixed :math:`\mathbf{M}` if :math:`\mathbf{b}` is a matrix (return OT loss + dual variables in log) M : np.ndarray (ns,nt) loss matrix reg : float Regularization term >0 reg_type : str Regularization type, can be the following (default ='l2'): - 'kl' : Kullback Leibler (~ Neg-entropy used in sinkhorn :ref:`[2] `) - 'l2' : Squared Euclidean regularization - 'sparsity_constrained' : Sparsity-constrained regularization [50] max_nz : int or None, optional. Used only in the case of reg_type = 'sparsity_constrained' to specify the maximum number of nonzeros per column of the optimal plan; not used for other regularization types. method : str Solver to use for scipy.optimize.minimize numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- gamma : (ns, nt) ndarray Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters .. _references-smooth-ot-dual: References ---------- .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [17] Blondel, M., Seguy, V., & Rolet, A. (2018). Smooth and Sparse Optimal Transport. Proceedings of the Twenty-First International Conference on Artificial Intelligence and Statistics (AISTATS). .. [50] Liu, T., Puigcerver, J., & Blondel, M. (2023). Sparsity-constrained optimal transport. Proceedings of the Eleventh International Conference on Learning Representations (ICLR). See Also -------- ot.lp.emd : Unregularized OT ot.sinhorn : Entropic regularized OT ot.optim.cg : General regularized OT """ nx = get_backend(a, b, M) if reg_type.lower() in ["l2", "squaredl2"]: regul = SquaredL2(gamma=reg) elif reg_type.lower() in ["entropic", "negentropy", "kl"]: regul = NegEntropy(gamma=reg) elif reg_type.lower() in ["sparsity_constrained", "sparsity-constrained"]: if not isinstance(max_nz, int): raise ValueError(f"max_nz {max_nz} must be an integer") regul = SparsityConstrained(gamma=reg, max_nz=max_nz) else: raise NotImplementedError("Unknown regularization") a0, b0, M0 = a, b, M # convert to humpy a, b, M = nx.to_numpy(a, b, M) # solve dual alpha, beta, res = solve_dual( a, b, M, regul, max_iter=numItermax, tol=stopThr, verbose=verbose ) # reconstruct transport matrix G = nx.from_numpy(get_plan_from_dual(alpha, beta, M, regul), type_as=M0) if log: log = { "alpha": nx.from_numpy(alpha, type_as=a0), "beta": nx.from_numpy(beta, type_as=b0), "res": res, } return G, log else: return G def smooth_ot_semi_dual( a, b, M, reg, reg_type="l2", max_nz=None, method="L-BFGS-B", stopThr=1e-9, numItermax=500, verbose=False, log=False, ): r""" Solve the regularized OT problem in the semi-dual and return the OT matrix The function solves the smooth relaxed dual formulation (10) in :ref:`[17] `: .. math:: \max_{\alpha}\quad \mathbf{a}^T\alpha- \mathrm{OT}_\Omega^*(\alpha, \mathbf{b}) where : .. math:: \mathrm{OT}_\Omega^*(\alpha,b)=\sum_j \mathbf{b}_j - :math:`\mathbf{m}_j` is the j-th column of the cost matrix - :math:`\mathrm{OT}_\Omega^*(\alpha,b)` is defined in Eq. (9) in :ref:`[17] ` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) The OT matrix can is reconstructed using :ref:`[17] ` Proposition 2. The optimization algorithm is using gradient decent (L-BFGS by default). Parameters ---------- a : np.ndarray (ns,) samples weights in the source domain b : np.ndarray (nt,) or np.ndarray (nt,nbb) samples in the target domain, compute sinkhorn with multiple targets and fixed:math:`\mathbf{M}` if :math:`\mathbf{b}` is a matrix (return OT loss + dual variables in log) M : np.ndarray (ns,nt) loss matrix reg : float Regularization term >0 reg_type : str Regularization type, can be the following (default ='l2'): - 'kl' : Kullback Leibler (~ Neg-entropy used in sinkhorn :ref:`[2] `) - 'l2' : Squared Euclidean regularization - 'sparsity_constrained' : Sparsity-constrained regularization [50] max_nz : int or None, optional. Used only in the case of reg_type = 'sparsity_constrained' to specify the maximum number of nonzeros per column of the optimal plan; not used for other regularization types. method : str Solver to use for scipy.optimize.minimize numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- gamma : (ns, nt) ndarray Optimal transportation matrix for the given parameters log : dict log dictionary return only if log==True in parameters .. _references-smooth-ot-semi-dual: References ---------- .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [17] Blondel, M., Seguy, V., & Rolet, A. (2018). Smooth and Sparse Optimal Transport. Proceedings of the Twenty-First International Conference on Artificial Intelligence and Statistics (AISTATS). .. [50] Liu, T., Puigcerver, J., & Blondel, M. (2023). Sparsity-constrained optimal transport. Proceedings of the Eleventh International Conference on Learning Representations (ICLR). See Also -------- ot.lp.emd : Unregularized OT ot.sinhorn : Entropic regularized OT ot.optim.cg : General regularized OT """ if reg_type.lower() in ["l2", "squaredl2"]: regul = SquaredL2(gamma=reg) elif reg_type.lower() in ["entropic", "negentropy", "kl"]: regul = NegEntropy(gamma=reg) elif reg_type.lower() in ["sparsity_constrained", "sparsity-constrained"]: if not isinstance(max_nz, int): raise ValueError(f"max_nz {max_nz} must be an integer") regul = SparsityConstrained(gamma=reg, max_nz=max_nz) else: raise NotImplementedError("Unknown regularization") # solve dual alpha, res = solve_semi_dual( a, b, M, regul, max_iter=numItermax, tol=stopThr, verbose=verbose ) # reconstruct transport matrix G = get_plan_from_semi_dual(alpha, b, M, regul) if log: log = {"alpha": alpha, "res": res} return G, log else: return G python-pot-0.9.7+dfsg/ot/solvers/000077500000000000000000000000001525521106200167055ustar00rootroot00000000000000python-pot-0.9.7+dfsg/ot/solvers/__init__.py000066400000000000000000000007211525521106200210160ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ General OT solvers with unified API """ # Author: Remi Flamary # Cédric Vincent-Cuaz # # License: MIT License # All submodules and packages from ._linear import solve, solve_sample from ._gromov import ( solve_gromov, ) from ._bary import ( solve_bary_sample, ) __all__ = [ "solve", "solve_sample", "solve_gromov", "solve_bary_sample", ] python-pot-0.9.7+dfsg/ot/solvers/_bary.py000066400000000000000000000610311525521106200203540ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ General OT solvers with unified API """ # Author: Remi Flamary # Cédric Vincent-Cuaz # # License: MIT License from ..utils import BaryResult from ..lp import free_support_barycenter_generic_costs from ..backend import get_backend from ._linear import solve, solve_sample, lst_method_solve_sample def _bary_sample_bcd( X_a_list, X_b_init, a_list, b, w, metric, inner_solver, update_masses, warmstart_plan, warmstart_potentials, stopping_criterion, max_iter_bary, tol_bary, verbose, log, nx, ): """Compute the barycenter using BCD. Parameters ---------- X_a_list : list of array-like, shape (n_samples_k, dim) List of samples in each source distribution X_b_init : array-like, shape (n_samples_b, dim), Initialization of the barycenter samples. a_list : list of array-like, shape (dim_k,) List of samples weights in each source distribution b : array-like, shape (n_samples_b,) Weights of the barycenter samples. w : list of array-like, shape (N,) Samples barycentric weights metric : str Metric to use for the cost matrix, by default "sqeuclidean" inner_solver : callable with parameters (X_a, X_b, a, b, plan_init, potentials_init) Function to solve the inner OT problem with inputs: source and target samples (`X_a`, `X_b`), their respective masses (`a`, `b`), optional initial transport plan `plan_init`, optional initial dual potentials `potentials_init` used for instance with sinkhorn-like inner solvers. update_masses : bool Update the masses of the barycenter, depending on whether balanced or unbalanced OT is used. warmstart_plan : bool Use the previous plan as initialization for the inner solver. Set based on inner solver type in ot.bary_sample warmstart_potentials : bool Use the previous potentials as initialization for the inner solver. Set based on inner solver type in ot.bary_sample stopping_criterion : str Stopping criterion for the BCD algorithm. Can be "loss" or "bary". max_iter_bary : int Maximum number of iterations for the barycenter tol_bary : float Tolerance for the barycenter convergence verbose : bool Print information in the solver log : bool Log the loss during the iterations nx: backend, optional Backend to use for the computation. If provided it must match the backend of the input arrays. Returns ------- res : BaryResult() Result of the optimization problem. The information can be obtained as follows: - res.X : Barycenter samples - res.b : Barycenter weights - res.value : Optimal value of the optimization problem - res.value_linear : Linear OT loss with the optimal OT plan - res.list_res: List of OTResult for each inner OT problem (one per source distribution) - res.log: log of the optimization process (if log=True) See :any:`BaryResult` for more information. """ if nx is None: nx = get_backend(*X_a_list, X_b_init, *a_list, b, w) X_b = X_b_init # inv_b is used to compute the barycenter samples in closed-form # inv_b is fixed in balanced OT, or updated via estimated b in unbalanced settings. inv_b = nx.nan_to_num(1.0 / b, nan=1.0, posinf=1.0, neginf=1.0) prev_criterion = float("inf") n_samples = len(X_a_list) log_ = None if log: log_ = {"stopping_criterion": []} # Compute the barycenter using BCD for it in range(max_iter_bary): # Solve the inner OT problem for each source distribution if it == 0: # no pre-defined warmstart used at iteration 0. list_res = [ inner_solver(X_a_list[k], X_b, a_list[k], b, None, None) for k in range(n_samples) ] elif warmstart_plan: list_res = [ inner_solver(X_a_list[k], X_b, a_list[k], b, list_res[k].plan, None) for k in range(n_samples) ] elif warmstart_potentials: list_res = [ inner_solver( X_a_list[k], X_b, a_list[k], b, None, list_res[k].potentials ) for k in range(n_samples) ] else: list_res = [ inner_solver(X_a_list[k], X_b, a_list[k], b, None, None) for k in range(n_samples) ] # Update the estimated barycenter weights in unbalanced cases if update_masses: b_estimated = sum( [w[k] * list_res[k].plan.sum(axis=0) for k in range(n_samples)] ) inv_b = nx.nan_to_num(1.0 / b_estimated, nan=1.0, posinf=1.0, neginf=1.0) # Update the barycenter samples if metric in ["sqeuclidean", "euclidean"]: X_b_new = ( sum([w[k] * list_res[k].plan.T @ X_a_list[k] for k in range(n_samples)]) * inv_b[:, None] ) else: raise NotImplementedError('Not implemented metric="{}"'.format(metric)) # compute criterion if stopping_criterion == "loss": new_criterion = sum([w[k] * list_res[k].value for k in range(n_samples)]) else: # stopping_criterion = "bary" new_criterion = nx.sum((X_b_new - X_b) ** 2) if verbose: if it % 1 == 0: print( f"BCD iteration {it}: criterion {stopping_criterion} = {new_criterion:.4f}" ) if log: log_["stopping_criterion"].append(new_criterion) # Check convergence if abs(new_criterion - prev_criterion) / abs(prev_criterion) < tol_bary: print(f"BCD converged in {it} iterations") break X_b = X_b_new prev_criterion = new_criterion # compute loss values value_linear = sum([w[k] * list_res[k].value_linear for k in range(n_samples)]) if stopping_criterion == "loss": value = new_criterion else: value = sum([w[k] * list_res[k].value for k in range(n_samples)]) # update BaryResult bary_res = BaryResult( X=X_b, b=b, value=value, value_linear=value_linear, log=log_, list_res=list_res, backend=nx, ) return bary_res def solve_bary_sample( X_a_list, n, a_list=None, w=None, X_b_init=None, b=None, metric="sqeuclidean", reg=None, c=None, reg_type="KL", unbalanced=None, unbalanced_type="KL", lazy=False, method=None, auto_bary_method="L2_barycentric_proj", warmstart=True, stopping_criterion="loss", max_iter_bary=1000, tol_bary=1e-5, random_state=0, verbose=False, **kwargs, ): r"""Solve the discrete OT barycenter problem over source distributions optimizing the barycenter support using Block-Coordinate Descent. The function solves the following general OT barycenter problem .. math:: \min_{\mathbf{X} \in \mathbb{R}^{n \times d}} \min_{\{ \mathbf{T}^{(k)} \}_k \in \mathbb{R}_+^{n_i \times n}} \quad \sum_k w_k \{ \langle \mathbf{T}^{(k)}, \mathbf{M}^{(k)} \rangle_F + \lambda_r R(\mathbf{T}^{(k)}) + \lambda_u U(\mathbf{T^{(k)}}\mathbf{1},\mathbf{a}^{(k)}) + \lambda_u U(\mathbf{T}^{(k)T}\mathbf{1},\mathbf{b}) \} where the cost matrices :math:`\mathbf{M}^{(k)}` from each input distribution :math:`(\mathbf{X}^{(k)}, \mathbf{a}^{(k)})` to the barycenter domain are computed as :math:`M^{(k)}_{i,j} = d(x^{(k)}_i,x_j)` where :math:`d` is a metric (by default the squared Euclidean distance). For common metrics the barycenter is computed in closed-form. For balanced OT, the `metric` parameter can also be any callable function, or list of functions, that computes the distance from an input to the barycenter. In which case, the barycenter is updated by gradient descent using the provided metric(s) and the optimal transport plan(s) at each iteration. The barycenter probability weights are fixed to :math:`\mathbf{b}`. The regularization is selected with `reg` (:math:`\lambda_r`) and `reg_type`. By default ``reg=None`` and there is no regularization. The unbalanced marginal penalization can be selected with `unbalanced` (:math:`\lambda_u`) and `unbalanced_type`. By default ``unbalanced=None`` and the function solves the exact optimal transport problem (respecting the marginals). Parameters ---------- X_a_list : list of array-like, shape (n_samples_k, dim) List of N samples in each source distribution n : int number of samples in the barycenter domain a_list : list of array-like, shape (n_samples_k,), optional List of samples weights in each source distribution (default is uniform) w : list of array-like, shape (N,), optional Samples barycentric weights (default is uniform) X_b_init : array-like, shape (n, dim), optional Initialization of the barycenter samples (default is gaussian random sampling) b : array-like, shape (n,), optional Barycenter weights (default is uniform) metric : str, callable or list of callables optional Metric to use for the computation of the cost matrix, by default "sqeuclidean". It can be a list of callables (bary, source) of length N (number of source distributions) to use different metrics for each source distribution. In this case, the barycenter is updated by gradient descent using the provided metric(s) and the optimal transport plan(s) at each iteration. If only callable is provided the same cost function is used for all source distributions. reg : float, optional Regularization weight :math:`\lambda_r`, by default None (no reg., exact OT) c : array-like, shape (dim_a, dim_b), optional (default=None) Reference measure for the regularization. If None, then use :math:`\mathbf{c} = \mathbf{a}^{(k)} \mathbf{b}^T`. If :math:`\texttt{reg_type}=`'entropy', then :math:`\mathbf{c} = 1_{|a^{(k)}|} 1_{|b|}^T`. reg_type : str, optional Type of regularization :math:`R` either "KL", "L2", "entropy", by default "KL" unbalanced : float or indexable object of length 1 or 2 Marginal relaxation term. If it is a scalar or an indexable object of length 1, then the same relaxation is applied to both marginal relaxations. The balanced OT can be recovered using :math:`unbalanced=float("inf")`. For semi-relaxed case, use either :math:`unbalanced=(float("inf"), scalar)` or :math:`unbalanced=(scalar, float("inf"))`. If unbalanced is an array, it must have the same backend as input arrays `(a, b, M)`. unbalanced_type : str, optional Type of unbalanced penalization function :math:`U` either "KL", "L2", "TV", by default "KL" lazy : bool, optional Return :any:`OTResultlazy` object to reduce memory cost when True, by default False method : str, optional Method for solving the problem, this can be used to select the solver for unbalanced problems (see :any:`ot.solve`), or to select a specific large scale solver. auto_bary_method: str, optional For balanced OT with callable metric functions, the barycenter method to use in 'L2_barycentric_proj' (default) for Euclidean barycentric projection, or 'true_fixed_point' for iterates using the North West Corner multi-marginal gluing method. warmstart : bool, optional Use the previous OT or potentials as initialization for the next inner solver iteration, by default False. stopping_criterion : str, optional Stopping criterion for the outer loop of the BCD solver, by default 'loss'. Either 'loss' to use the optimize objective or 'bary' for variations of the barycenter w.r.t the Frobenius norm. max_iter_bary : int, optional Maximum number of iteration for the outer loop of the BCD solver, by default 1000. tol_bary : float, optional Tolerance for solution precision of the barycenter problem, by default 1e-5. random_state : int, optional Random seed for the initialization of the barycenter samples, by default 0. Only used if `X_init` is None. verbose : bool, optional Print information in the solver, by default False kwargs : optional Additional parameters for the inner solver (see :any:`ot.solve_sample` and :any:`ot.lp.free_support_barycenter_generic_costs`) Returns ------- res : BaryResult() Result of the optimization problem. The information can be obtained as follows: - res.X : Barycenter samples - res.b : Barycenter weights - res.value : Optimal value of the optimization problem - res.value_linear : Linear OT loss with the optimal OT plan - res.list_res: List of OTResult for each inner OT problem (one per source distribution) - res.log: log of the optimization process (if log=True) See :any:`BaryResult` for more information. Notes ----- The following methods are available for solving barycenter problems with respect to these inner OT problems: - **Classical exact OT problem [1]** (default parameters) : .. math:: \forall k, \quad \min_{\mathbf{T}^{(k)}} \quad \langle \mathbf{T}^{(k)}, \mathbf{M}^{(k)} \rangle_F s.t. \ \mathbf{T}^{(k)} \mathbf{1} = \mathbf{a}^{(k)} \mathbf{T}^{(k)^T} \mathbf{1} = \mathbf{b} \mathbf{T}^{(k)} \geq 0, M^{(k)}_{i,j} = d(x^{(k)}_i,x_j) can be solved with the following code for various cost metrics between the source distributions and the barycenter: .. code-block:: python # for squared Euclidean cost, where closed-form solutions are used to update the barycenter res = ot.solve_bary_sample([x1, x2], n , [a1, a2], w, metric='sqeuclidean') # for uniform sample weights and barycentric weights, res = ot.solve_bary_sample([x1, x2], n, [a1, a2], w, metric='sqeuclidean') # for other cost functions, where the barycenter is updated with gradient descent using Pytorch # refer to the documentation and examples for more details. - **Entropic regularized OT [2]** (when ``reg!=None``): .. math:: \min_{\mathbf{T}^{(k)}} \quad \langle \mathbf{T}^{(k)}, \mathbf{M}^{(k)} \rangle_F + \lambda R(\mathbf{T}^{(k)}) s.t. \ \mathbf{T}^{(k)} \mathbf{1} = \mathbf{a}^{(k)} \mathbf{T}^{(k)^T} \mathbf{1} = \mathbf{b} \mathbf{T}^{(k)} \geq 0, M^{(k)}_{i,j} = d(x^{(k)}_i,x_j) can be solved with the following code: .. code-block:: python # default is ``"KL"`` regularization (``reg_type="KL"``) res = ot.solve_bary_sample([x1, x2], n , [a1, a2], w, reg=1.0) # or for original Sinkhorn paper formulation [2] res = ot.solve_bary_sample([x1, x2], n , [a1, a2], w, reg=1.0, reg_type='entropy') - **Quadratic regularized OT [17]** (when ``reg!=None`` and ``reg_type="L2"``): .. math:: \min_{\mathbf{T}^{(k)}} \quad \langle \mathbf{T}^{(k)}, \mathbf{M}^{(k)} \rangle_F + \lambda R(\mathbf{T}^{(k))}) s.t. \ \mathbf{T}^{(k)} \mathbf{1} = \mathbf{a}^{(k)} \mathbf{T}^{(k)^T} \mathbf{1} = \mathbf{b} \mathbf{T}^{(k)} \geq 0, M^{(k)}_{i,j} = d(x^{(k)}_i,x_j) can be solved with the following code: .. code-block:: python res = ot.solve_bary_sample([x1, x2], n , [a1, a2], w, reg=1.0, reg_type='L2') - **Unbalanced OT [41]** (when ``unbalanced!=None``): .. math:: \min_{\mathbf{T}^{(k)}\geq 0} \quad \langle \mathbf{T}^{(k)}, \mathbf{M}^{(k)} \rangle_F + \lambda_u U(\mathbf{T}^{(k)}\mathbf{1},\mathbf{a}^{(k)}) + \lambda_u U(\mathbf{T}^{(k)^T}\mathbf{1},\mathbf{b}) can be solved with the following code: .. code-block:: python # default is ``"KL"`` res = ot.solve_bary_sample([x1, x2], n , [a1, a2], w, unbalanced=1.0) # quadratic unbalanced OT res = ot.solve_bary_sample([x1, x2], n , [a1, a2], w, unbalanced=1.0, unbalanced_type='L2') # TV = partial OT res = ot.solve_bary_sample([x1, x2], n , [a1, a2], w, unbalanced=1.0, unbalanced_type='TV') - **Regularized unbalanced regularized OT [34]** (when ``unbalanced!=None`` and ``reg!=None``): .. math:: \min_{\mathbf{T}^{(k)} \geq 0} \quad \langle \mathbf{T}^{(k)}, \mathbf{M}^{(k)} \rangle_F + \lambda_r R(\mathbf{T}^{(k)}) + \lambda_u U(\mathbf{T}^{(k)}\mathbf{1},\mathbf{a}^{(k)}) + \lambda_u U(\mathbf{T}^{(k)^T}\mathbf{1},\mathbf{b}) can be solved with the following code: .. code-block:: python # default is ``"KL"`` for both res = ot.solve_bary_sample([x1, x2], n , [a1, a2], w, reg=1.0, unbalanced=1.0) # quadratic unbalanced OT with KL regularization res = ot.solve_bary_sample([x1, x2], n , [a1, a2], w, reg=1.0, unbalanced=1.0, unbalanced_type='L2') # both quadratic res = ot.solve_bary_sample([x1, x2], n , [a1, a2], w, reg=1.0, reg_type='L2', unbalanced=1.0, unbalanced_type='L2') .. _references-solve_bary_sample: References ---------- .. [20] Cuturi, Marco, and Arnaud Doucet. "Fast computation of Wasserstein barycenters." International Conference on Machine Learning. 2014. .. [43] �lvarez-Esteban, Pedro C., et al. "A fixed-point approach to barycenters in Wasserstein space." Journal of Mathematical Analysis and Applications 441.2 (2016): 744-762. .. [77] Tanguy, Eloi and Delon, Julie and Gozlan, Nathaël (2024). Computing barycenters of Measures for Generic Transport Costs. arXiv preprint 2501.04016 (2024) """ if method is not None and method.lower() in lst_method_solve_sample: raise NotImplementedError( f"method {method} operating on lazy tensors is not implemented yet" ) if stopping_criterion not in ["loss", "bary"]: raise ValueError( "stopping_criterion must be either 'loss' or 'bary', got {}".format( stopping_criterion ) ) n_samples = len(X_a_list) if lazy: raise (NotImplementedError("Barycenter solver with lazy=True not implemented")) else: # default non lazy solver calls ot.solve_sample within _bary_sample_bcd # Detect backend nx = get_backend(*X_a_list, X_b_init, b, w) # check sample weights if a_list is None: a_list = [ nx.ones((X_a_list[k].shape[0],), type_as=X_a_list[k]) / X_a_list[k].shape[0] for k in range(n_samples) ] # check samples barycentric weights if w is None: w = nx.ones(n_samples, type_as=X_a_list[0]) / n_samples # check X_b_init if X_b_init is None: nx.seed(random_state) mean_ = nx.concatenate( [nx.mean(X_a_list[k], axis=0) for k in range(n_samples)], axis=0, ) mean_ = nx.mean(mean_, axis=0) std_ = nx.concatenate( [nx.std(X_a_list[k], axis=0) for k in range(n_samples)], axis=0, ) std_ = nx.mean(std_, axis=0) X_b_init = ( std_ * nx.randn(n, X_a_list[0].shape[1], type_as=X_a_list[0]) + mean_ ) else: if (X_b_init.shape[0] != n) or (X_b_init.shape[1] != X_a_list[0].shape[1]): raise ValueError("X_b_init must have shape (n, dim)") # check b if b is None: b = nx.ones((n,), type_as=X_a_list[0]) / n if callable(metric) or ( isinstance(metric, list) and all(callable(m) for m in metric) ): if reg is not None or unbalanced is not None: raise NotImplementedError( "Custom callable metric only available for balanced OT (reg=None and unbalanced=None)" ) else: if auto_bary_method == "true_fixed_point": ground_bary = kwargs.get("ground_bary", None) if ground_bary is None: raise ValueError( "ground_bary must be provided in kwargs for true_fixed_point method with callable metrics" ) outputs = free_support_barycenter_generic_costs( X_a_list, a_list, X_b_init, metric, ground_bary=None, a=b, numItermax=max_iter_bary, method=auto_bary_method, stopThr=tol_bary, log=True, **kwargs, ) if auto_bary_method == "L2_barycentric_proj": X_b, log_ = outputs elif auto_bary_method == "true_fixed_point": X_b, b, log_ = ( outputs # potentially modify the masses of the barycenter with the true fixed point method ) # compute the pairwise transport plans and losses metric_list = ( metric if isinstance(metric, list) else [metric] * n_samples ) list_res = [ solve( M=metric_list[k]( X_b, X_a_list[k] ).T, # in the free support setting, the cost matrix is computed from the barycenter to the source distribution, so we transpose it here to be consistent with the inner_solver interface (X_a, X_b, a=a_list[k], b=b, reg=None, unbalanced=None, ) for k in range(n_samples) ] value_linear = sum( w[k] * list_res[k].value_linear for k in range(n_samples) ) res = BaryResult( X=X_b, b=b, value=value_linear, value_linear=value_linear, log=log_, list_res=list_res, backend=nx, ) return res else: # check metric if metric not in ["sqeuclidean", "euclidean"]: raise NotImplementedError( 'Not implemented BCD with closed-form on the barycenter samples with metric="{}"'.format( metric ) ) if warmstart: # covers exact OT, regularized OT, unbalanced and regularized OT, but not unbalanced OT without regularization (not implemented) warmstart_plan = False warmstart_potentials = True if reg is not None: if ( not isinstance(reg_type, tuple) and reg_type.lower() not in ["kl"] and unbalanced_type.lower() != "kl" ) or isinstance(reg_type, tuple): warmstart_plan = True warmstart_potentials = False else: warmstart_plan = False warmstart_potentials = False def inner_solver(X_a, X_b, a, b, plan_init, potentials_init): return solve_sample( X_a=X_a, X_b=X_b, a=a, b=b, metric=metric, reg=reg, c=c, reg_type=reg_type, unbalanced=unbalanced, unbalanced_type=unbalanced_type, method=method, plan_init=plan_init, potentials_init=potentials_init, verbose=False, **kwargs, ) # compute the barycenter using BCD update_masses = unbalanced is not None res = _bary_sample_bcd( X_a_list, X_b_init, a_list, b, w, metric, inner_solver, update_masses, warmstart_plan, warmstart_potentials, stopping_criterion, max_iter_bary, tol_bary, verbose, True, # log set to True by default nx, ) return res python-pot-0.9.7+dfsg/ot/solvers/_gromov.py000066400000000000000000000751401525521106200207360ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ General OT solvers with unified API """ # Author: Remi Flamary # Cédric Vincent-Cuaz # # License: MIT License from ..utils import OTResult from ..lp import emd2 from ..backend import get_backend from ..bregman import ( sinkhorn_log, ) from ..gromov import ( gromov_wasserstein2, fused_gromov_wasserstein2, entropic_gromov_wasserstein2, entropic_fused_gromov_wasserstein2, semirelaxed_gromov_wasserstein2, semirelaxed_fused_gromov_wasserstein2, entropic_semirelaxed_fused_gromov_wasserstein2, entropic_semirelaxed_gromov_wasserstein2, partial_gromov_wasserstein2, partial_fused_gromov_wasserstein2, entropic_partial_gromov_wasserstein2, entropic_partial_fused_gromov_wasserstein2, fused_unbalanced_gromov_wasserstein, ) from ._linear import solve import warnings def solve_gromov( Ca, Cb, M=None, a=None, b=None, loss="L2", symmetric=None, alpha=0.5, reg=None, reg_type="entropy", unbalanced=None, unbalanced_type="KL", n_threads=1, method=None, max_iter=None, plan_init=None, tol=None, verbose=False, ): r"""Solve the discrete (Fused) Gromov-Wasserstein and return :any:`OTResult` object The function solves the following optimization problem: .. math:: \min_{\mathbf{T}\geq 0} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j} \mathbf{T}_{k,l} + \lambda_r R(\mathbf{T}) + \lambda_u U(\mathbf{T}\mathbf{1},\mathbf{a}) + \lambda_u U(\mathbf{T}^T\mathbf{1},\mathbf{b}) The regularization is selected with `reg` (:math:`\lambda_r`) and `reg_type`. By default ``reg=None`` and there is no regularization. The unbalanced marginal penalization can be selected with `unbalanced` (:math:`\lambda_u`) and `unbalanced_type`. By default ``unbalanced=None`` and the function solves the exact optimal transport problem (respecting the marginals). Parameters ---------- Ca : array-like, shape (dim_a, dim_a) Cost matrix in the source domain Cb : array-like, shape (dim_b, dim_b) Cost matrix in the target domain M : array-like, shape (dim_a, dim_b), optional Linear cost matrix for Fused Gromov-Wasserstein (default is None). a : array-like, shape (dim_a,), optional Samples weights in the source domain (default is uniform) b : array-like, shape (dim_b,), optional Samples weights in the source domain (default is uniform) loss : str, optional Type of loss function, either ``"L2"`` or ``"KL"``, by default ``"L2"`` symmetric : bool, optional Use symmetric version of the Gromov-Wasserstein problem, by default None tests whether the matrices are symmetric or True/False to avoid the test. reg : float, optional Regularization weight :math:`\lambda_r`, by default None (no reg., exact OT) reg_type : str, optional Type of regularization :math:`R`, by default "entropy" (only used when ``reg!=None``) alpha : float, optional Weight the quadratic term (alpha*Gromov) and the linear term ((1-alpha)*Wass) in the Fused Gromov-Wasserstein problem. Not used for Gromov problem (when M is not provided). By default ``alpha=None`` corresponds to ``alpha=1`` for Gromov problem (``M==None``) and ``alpha=0.5`` for Fused Gromov-Wasserstein problem (``M!=None``) unbalanced : float, optional Unbalanced penalization weight :math:`\lambda_u`, by default None (balanced OT). Not implemented yet for "KL" unbalanced penalization function :math:`U`. Corresponds to the total transport mass for partial OT. unbalanced_type : str, optional Type of unbalanced penalization function :math:`U` either "KL", "semirelaxed", "partial", by default "KL". n_threads : int, optional Number of OMP threads for exact OT solver, by default 1 method : str, optional Method for solving the problem when multiple algorithms are available, default None for automatic selection. max_iter : int, optional Maximum number of iterations, by default None (default values in each solvers) plan_init : array-like, shape (dim_a, dim_b), optional Initialization of the OT plan for iterative methods, by default None tol : float, optional Tolerance for solution precision, by default None (default values in each solvers) verbose : bool, optional Print information in the solver, by default False Returns ------- res : OTResult() Result of the optimization problem. The information can be obtained as follows: - res.plan : OT plan :math:`\mathbf{T}` - res.potentials : OT dual potentials - res.value : Optimal value of the optimization problem - res.value_linear : Linear OT loss with the optimal OT plan - res.value_quad : Quadratic (GW) part of the OT loss with the optimal OT plan See :any:`OTResult` for more information. Notes ----- The following methods are available for solving the Gromov-Wasserstein problem: - **Classical Gromov-Wasserstein (GW) problem [3]** (default parameters): .. math:: \min_{\mathbf{T}\geq 0} \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j}\mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} = \mathbf{a} \mathbf{T}^T \mathbf{1} = \mathbf{b} \mathbf{T} \geq 0 can be solved with the following code: .. code-block:: python res = ot.solve_gromov(Ca, Cb) # uniform weights res = ot.solve_gromov(Ca, Cb, a=a, b=b) # given weights res = ot.solve_gromov(Ca, Cb, loss='KL') # KL loss plan = res.plan # GW plan value = res.value # GW value - **Fused Gromov-Wasserstein (FGW) problem [24]** (when ``M!=None``): .. math:: \min_{\mathbf{T}\geq 0} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j}\mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} = \mathbf{a} \mathbf{T}^T \mathbf{1} = \mathbf{b} \mathbf{T} \geq 0 can be solved with the following code: .. code-block:: python res = ot.solve_gromov(Ca, Cb, M) # uniform weights, alpha=0.5 (default) res = ot.solve_gromov(Ca, Cb, M, a=a, b=b, alpha=0.1) # given weights and alpha plan = res.plan # FGW plan loss_linear_term = res.value_linear # Wasserstein part of the loss loss_quad_term = res.value_quad # Gromov part of the loss loss = res.value # FGW value - **Regularized (Fused) Gromov-Wasserstein (GW) problem [12]** (when ``reg!=None``): .. math:: \min_{\mathbf{T}\geq 0} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j}\mathbf{T}_{k,l} + \lambda_r R(\mathbf{T}) s.t. \ \mathbf{T} \mathbf{1} = \mathbf{a} \mathbf{T}^T \mathbf{1} = \mathbf{b} \mathbf{T} \geq 0 can be solved with the following code: .. code-block:: python res = ot.solve_gromov(Ca, Cb, reg=1.0) # GW entropy regularization (default) res = ot.solve_gromov(Ca, Cb, M, a=a, b=b, reg=10, alpha=0.1) # FGW with entropy plan = res.plan # FGW plan loss_linear_term = res.value_linear # Wasserstein part of the loss loss_quad_term = res.value_quad # Gromov part of the loss loss = res.value # FGW value (including regularization) - **Semi-relaxed (Fused) Gromov-Wasserstein (GW) [48]** (when ``unbalanced='semirelaxed'``): .. math:: \min_{\mathbf{T}\geq 0} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j}\mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} = \mathbf{a} \mathbf{T} \geq 0 can be solved with the following code: .. code-block:: python res = ot.solve_gromov(Ca, Cb, unbalanced='semirelaxed') # semirelaxed GW res = ot.solve_gromov(Ca, Cb, unbalanced='semirelaxed', reg=1) # entropic semirelaxed GW res = ot.solve_gromov(Ca, Cb, M, unbalanced='semirelaxed', alpha=0.1) # semirelaxed FGW plan = res.plan # FGW plan right_marginal = res.marginal_b # right marginal of the plan - **Partial (Fused) Gromov-Wasserstein (GW) problem [29]** (when ``unbalanced='partial'``): .. math:: \min_{\mathbf{T}\geq 0} \quad (1 - \alpha) \langle \mathbf{T}, \mathbf{M} \rangle_F + \alpha \sum_{i,j,k,l} L(\mathbf{C_1}_{i,k}, \mathbf{C_2}_{j,l}) \mathbf{T}_{i,j}\mathbf{T}_{k,l} s.t. \ \mathbf{T} \mathbf{1} \leq \mathbf{a} \mathbf{T}^T \mathbf{1} \leq \mathbf{b} \mathbf{T} \geq 0 \mathbf{1}^T\mathbf{T}\mathbf{1} = m can be solved with the following code: .. code-block:: python res = ot.solve_gromov(Ca, Cb, unbalanced_type='partial', unbalanced=0.8) # partial GW with m=0.8 res = ot.solve_gromov(Ca, Cb, M, unbalanced_type='partial', unbalanced=0.8, alpha=0.5) # partial FGW with m=0.8 .. _references-solve-gromov: References ---------- .. [3] Mémoli, F. (2011). Gromov–Wasserstein distances and the metric approach to object matching. Foundations of computational mathematics, 11(4), 417-487. .. [12] Gabriel Peyré, Marco Cuturi, and Justin Solomon (2016), Gromov-Wasserstein averaging of kernel and distance matrices International Conference on Machine Learning (ICML). .. [24] Vayer, T., Chapel, L., Flamary, R., Tavenard, R. and Courty, N. (2019). Optimal Transport for structured data with application on graphs Proceedings of the 36th International Conference on Machine Learning (ICML). .. [48] Cédric Vincent-Cuaz, Rémi Flamary, Marco Corneli, Titouan Vayer, Nicolas Courty (2022). Semi-relaxed Gromov-Wasserstein divergence and applications on graphs. International Conference on Learning Representations (ICLR), 2022. .. [29] Chapel, L., Alaya, M., Gasso, G. (2020). Partial Optimal Transport with Applications on Positive-Unlabeled Learning, Advances in Neural Information Processing Systems (NeurIPS), 2020. """ # detect backend nx = get_backend(Ca, Cb, M, a, b) # create uniform weights if not given if a is None: a = nx.ones(Ca.shape[0], type_as=Ca) / Ca.shape[0] if b is None: b = nx.ones(Cb.shape[1], type_as=Cb) / Cb.shape[1] # default values for solutions potentials = None value = None value_linear = None value_quad = None plan = None status = None log = None loss_dict = {"l2": "square_loss", "kl": "kl_loss"} if loss.lower() not in loss_dict.keys(): raise (NotImplementedError('Not implemented GW loss="{}"'.format(loss))) loss_fun = loss_dict[loss.lower()] if reg is None or reg == 0: # exact OT if unbalanced is None and unbalanced_type.lower() not in [ "semirelaxed", ]: # Exact balanced OT if unbalanced_type.lower() in ["partial"]: warnings.warn( "Exact balanced OT is computed as `unbalanced=None` even though " f"unbalanced_type = {unbalanced_type}.", stacklevel=2, ) if M is None or alpha == 1: # Gromov-Wasserstein problem # default values for solver if max_iter is None: max_iter = 10000 if tol is None: tol = 1e-9 value, log = gromov_wasserstein2( Ca, Cb, a, b, loss_fun=loss_fun, log=True, symmetric=symmetric, max_iter=max_iter, G0=plan_init, tol_rel=tol, tol_abs=tol, verbose=verbose, ) value_quad = value if alpha == 1: # set to 0 for FGW with alpha=1 value_linear = 0 plan = log["T"] potentials = (log["u"], log["v"]) elif alpha == 0: # Wasserstein problem # default values for EMD solver if max_iter is None: max_iter = 1000000 value_linear, log = emd2( a, b, M, numItermax=max_iter, log=True, return_matrix=True, numThreads=n_threads, ) value = value_linear potentials = (log["u"], log["v"]) plan = log["G"] status = log["warning"] if log["warning"] is not None else "Converged" value_quad = 0 else: # Fused Gromov-Wasserstein problem # default values for solver if max_iter is None: max_iter = 10000 if tol is None: tol = 1e-9 value, log = fused_gromov_wasserstein2( M, Ca, Cb, a, b, loss_fun=loss_fun, alpha=alpha, log=True, symmetric=symmetric, max_iter=max_iter, G0=plan_init, tol_rel=tol, tol_abs=tol, verbose=verbose, ) value_linear = log["lin_loss"] value_quad = log["quad_loss"] plan = log["T"] potentials = (log["u"], log["v"]) elif unbalanced_type.lower() in ["semirelaxed"]: # Semi-relaxed OT if M is None or alpha == 1: # Semi relaxed Gromov-Wasserstein problem # default values for solver if max_iter is None: max_iter = 10000 if tol is None: tol = 1e-9 value, log = semirelaxed_gromov_wasserstein2( Ca, Cb, a, loss_fun=loss_fun, log=True, symmetric=symmetric, max_iter=max_iter, G0=plan_init, tol_rel=tol, tol_abs=tol, verbose=verbose, ) value_quad = value if alpha == 1: # set to 0 for FGW with alpha=1 value_linear = 0 plan = log["T"] # potentials = (log['u'], log['v']) TODO else: # Semi relaxed Fused Gromov-Wasserstein problem # default values for solver if max_iter is None: max_iter = 10000 if tol is None: tol = 1e-9 value, log = semirelaxed_fused_gromov_wasserstein2( M, Ca, Cb, a, loss_fun=loss_fun, alpha=alpha, log=True, symmetric=symmetric, max_iter=max_iter, G0=plan_init, tol_rel=tol, tol_abs=tol, verbose=verbose, ) value_linear = log["lin_loss"] value_quad = log["quad_loss"] plan = log["T"] # potentials = (log['u'], log['v']) TODO elif unbalanced_type.lower() in ["partial"]: # Partial OT if M is None or alpha == 1.0: # Partial Gromov-Wasserstein problem if unbalanced > nx.sum(a) or unbalanced > nx.sum(b): raise ( ValueError("Partial GW mass given in `unbalanced` is too large") ) # default values for solver if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-7 value, log = partial_gromov_wasserstein2( Ca, Cb, a, b, m=unbalanced, loss_fun=loss_fun, log=True, numItermax=max_iter, G0=plan_init, tol=tol, symmetric=symmetric, verbose=verbose, ) value_quad = value plan = log["T"] # potentials = (log['u'], log['v']) TODO else: # partial FGW if unbalanced > nx.sum(a) or unbalanced > nx.sum(b): raise ( ValueError("Partial GW mass given in `unbalanced` is too large") ) # default values for solver if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-7 value, log = partial_fused_gromov_wasserstein2( M, Ca, Cb, a, b, m=unbalanced, loss_fun=loss_fun, alpha=alpha, log=True, numItermax=max_iter, G0=plan_init, tol=tol, symmetric=symmetric, verbose=verbose, ) value_linear = log["lin_loss"] value_quad = log["quad_loss"] plan = log["T"] # potentials = (log['u'], log['v']) TODO elif unbalanced_type.lower() in ["kl", "l2"]: # unbalanced exact OT if alpha == 0: # unbalanced Wasserstein problem res = solve( M, a=a, b=b, reg=None, reg_type=reg_type, unbalanced=unbalanced, unbalanced_type=unbalanced_type, method=method, max_iter=max_iter, plan_init=plan_init, tol=tol, verbose=verbose, ) plan = res.plan potentials = res.potentials value_linear = res.value_linear value = res.value value_quad = 0 status = res.status else: if max_iter is None: max_iter = 100 if tol is None: tol = 1e-7 # in this function alpha weights the linear and quadratic terms : alpha * quadratic + (1 - alpha) * linear # while fused_unbalanced_gromov_wasserstein uses alpha as the coefficient of the linear term. alpha_fugw = (1 - alpha) / alpha reg_fugw = unbalanced / (2 * alpha) plan, _, log = fused_unbalanced_gromov_wasserstein( Ca, Cb, a, b, reg_marginals=reg_fugw, divergence=unbalanced_type.lower(), alpha=alpha_fugw, M=M, max_iter=max_iter, tol=tol, log=True, epsilon=0, ) value_linear = log["linear_cost"] * alpha value = log["fugw_cost"] * alpha else: raise ( NotImplementedError( 'Unknown unbalanced_type="{}"'.format(unbalanced_type) ) ) else: # regularized OT if unbalanced is None and unbalanced_type.lower() not in [ "semirelaxed", ]: # Balanced regularized OT if unbalanced_type.lower() in ["partial"]: warnings.warn( "Exact balanced OT is computed as `unbalanced=None` even though " f"unbalanced_type = {unbalanced_type}.", stacklevel=2, ) if reg_type.lower() in ["entropy"] and ( M is None or alpha == 1 ): # Entropic Gromov-Wasserstein problem # default values for solver if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-9 if method is None: method = "PGD" value_quad, log = entropic_gromov_wasserstein2( Ca, Cb, a, b, epsilon=reg, loss_fun=loss_fun, log=True, symmetric=symmetric, solver=method, max_iter=max_iter, G0=plan_init, tol_rel=tol, tol_abs=tol, verbose=verbose, ) plan = log["T"] value_linear = 0 value = value_quad + reg * nx.sum(plan * nx.log(plan + 1e-16)) # potentials = (log['log_u'], log['log_v']) #TODO elif ( reg_type.lower() in ["entropy"] and M is not None and alpha == 0 ): # Entropic Wasserstein problem # default values for solver if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-9 plan, log = sinkhorn_log( a, b, M, reg=reg, numItermax=max_iter, stopThr=tol, log=True, verbose=verbose, ) value_linear = nx.sum(M * plan) value = value_linear + reg * nx.sum(plan * nx.log(plan + 1e-16)) potentials = (log["log_u"], log["log_v"]) elif ( reg_type.lower() in ["entropy"] and M is not None ): # Entropic Fused Gromov-Wasserstein problem # default values for solver if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-9 if method is None: method = "PGD" value_noreg, log = entropic_fused_gromov_wasserstein2( M, Ca, Cb, a, b, loss_fun=loss_fun, alpha=alpha, log=True, symmetric=symmetric, solver=method, max_iter=max_iter, G0=plan_init, tol_rel=tol, tol_abs=tol, verbose=verbose, ) value_linear = log["lin_loss"] value_quad = log["quad_loss"] plan = log["T"] # potentials = (log['u'], log['v']) value = value_noreg + reg * nx.sum(plan * nx.log(plan + 1e-16)) else: raise ( NotImplementedError( 'Not implemented reg_type="{}"'.format(reg_type) ) ) elif unbalanced_type.lower() in ["semirelaxed"]: # Semi-relaxed OT if reg_type.lower() in ["entropy"] and ( M is None or alpha == 1 ): # Entropic Semi-relaxed Gromov-Wasserstein problem # default values for solver if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-9 value_quad, log = entropic_semirelaxed_gromov_wasserstein2( Ca, Cb, a, epsilon=reg, loss_fun=loss_fun, log=True, symmetric=symmetric, max_iter=max_iter, G0=plan_init, tol=tol, verbose=verbose, ) plan = log["T"] value_linear = 0 value = value_quad + reg * nx.sum(plan * nx.log(plan + 1e-16)) else: # Entropic Semi-relaxed FGW problem # default values for solver if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-9 value_noreg, log = entropic_semirelaxed_fused_gromov_wasserstein2( M, Ca, Cb, a, loss_fun=loss_fun, alpha=alpha, log=True, symmetric=symmetric, max_iter=max_iter, G0=plan_init, tol=tol, verbose=verbose, ) value_linear = log["lin_loss"] value_quad = log["quad_loss"] plan = log["T"] value = value_noreg + reg * nx.sum(plan * nx.log(plan + 1e-16)) elif unbalanced_type.lower() in ["partial"]: # Partial OT if M is None or alpha == 1.0: # Partial Gromov-Wasserstein problem if unbalanced > nx.sum(a) or unbalanced > nx.sum(b): raise ( ValueError("Partial GW mass given in `unbalanced` is too large") ) # default values for solver if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-7 value_noreg, log = entropic_partial_gromov_wasserstein2( Ca, Cb, a, b, reg=reg, loss_fun=loss_fun, m=unbalanced, log=True, numItermax=max_iter, G0=plan_init, tol=tol, symmetric=symmetric, verbose=verbose, ) value_quad = value_noreg plan = log["T"] # potentials = (log['u'], log['v']) TODO value = value_noreg + reg * nx.sum(plan * nx.log(plan + 1e-16)) else: # partial FGW if unbalanced > nx.sum(a) or unbalanced > nx.sum(b): raise ( ValueError("Partial GW mass given in `unbalanced` is too large") ) # default values for solver if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-7 value_noreg, log = entropic_partial_fused_gromov_wasserstein2( M, Ca, Cb, a, b, reg=reg, loss_fun=loss_fun, alpha=alpha, m=unbalanced, log=True, numItermax=max_iter, G0=plan_init, tol=tol, symmetric=symmetric, verbose=verbose, ) value_linear = log["lin_loss"] value_quad = log["quad_loss"] plan = log["T"] # potentials = (log['u'], log['v']) TODO value = value_noreg + reg * nx.sum(plan * nx.log(plan + 1e-16)) elif unbalanced_type.lower() in ["kl", "l2"]: if alpha == 0: # regularized unbalanced Wasserstein problem res = solve( M, a=a, b=b, reg=reg, reg_type=reg_type, unbalanced=unbalanced, unbalanced_type=unbalanced_type, method=method, max_iter=max_iter, plan_init=plan_init, tol=tol, verbose=verbose, ) plan = res.plan potentials = res.potentials value_linear = res.value_linear value = res.value value_quad = 0 status = res.status else: if max_iter is None: max_iter = 100 if tol is None: tol = 1e-7 # in this function alpha weights the linear and quadratic terms : alpha * quadratic + (1 - alpha) * linear # while fused_unbalanced_gromov_wasserstein uses alpha as the coefficient of the linear term. alpha_fugw = (1 - alpha) / alpha reg_fugw = unbalanced / (2 * alpha) epsilon_fugw = reg / (2 * alpha) plan, _, log = fused_unbalanced_gromov_wasserstein( Ca, Cb, a, b, reg_marginals=reg_fugw, divergence=unbalanced_type.lower(), alpha=alpha_fugw, M=M, max_iter=max_iter, tol=tol, log=True, epsilon=epsilon_fugw, ) value_linear = log["linear_cost"] * alpha value = log["fugw_cost"] * alpha else: # unbalanced AND regularized OT raise ( NotImplementedError( 'Not implemented reg_type="{}" and unbalanced_type="{}"'.format( reg_type, unbalanced_type ) ) ) res = OTResult( potentials=potentials, value=value, value_linear=value_linear, value_quad=value_quad, plan=plan, status=status, backend=nx, log=log, ) return res python-pot-0.9.7+dfsg/ot/solvers/_linear.py000066400000000000000000001576241525521106200207070ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ General OT solvers with unified API """ # Author: Remi Flamary # Cédric Vincent-Cuaz # # License: MIT License from ..utils import OTResult, dist, split_sample_ratio, apply_scaler from ..lp import emd2, emd2_lazy, wasserstein_1d from ..backend import get_backend from ..unbalanced import mm_unbalanced, sinkhorn_knopp_unbalanced, lbfgsb_unbalanced from ..bregman import ( sinkhorn_log, empirical_sinkhorn2, empirical_sinkhorn_nystroem2, old_geomloss, ) from ..sliced import sliced_wasserstein_distance, max_sliced_wasserstein_distance from ..bsp import compute_bspot_bijection from ..smooth import smooth_ot_dual from ..gaussian import ( empirical_bures_wasserstein_distance, empirical_bures_wasserstein_distance_hd, ) from ..factored import factored_optimal_transport from ..lowrank import lowrank_sinkhorn from ..optim import cg from warnings import warn lst_valid_methods_solve = [ "sinkhorn", # sinkhorn for unbalanced "sinkhorn_log", # sinkhorn for unbalanced "mm", # unbalanced "lbfgsb", # unbalanced ] lst_method_solve_sample = [ "1d", "gaussian", "gaussian_hd", "lowrank", "nystroem", "factored", "geomloss", "geomloss_auto", "geomloss_tensorized", "geomloss_online", "geomloss_multiscale", "sliced", "max_sliced", "bsp", ] all_valid_methods_solve_sample = lst_valid_methods_solve + lst_method_solve_sample def solve( M, a=None, b=None, reg=None, c=None, reg_type="KL", unbalanced=None, unbalanced_type="KL", method=None, n_threads=1, max_iter=None, plan_init=None, potentials_init=None, tol=None, verbose=False, grad="autodiff", ): r"""Solve the discrete optimal transport problem and return :any:`OTResult` object The function solves the following general optimal transport problem .. math:: \min_{\mathbf{T}\geq 0} \quad \sum_{i,j} T_{i,j}M_{i,j} + \lambda_r R(\mathbf{T}) + \lambda_1 U(\mathbf{T}\mathbf{1},\mathbf{a}) + \lambda_2 U(\mathbf{T}^T\mathbf{1},\mathbf{b}) The regularization is selected with `reg` (:math:`\lambda_r`) and `reg_type`. By default ``reg=None`` and there is no regularization. The unbalanced marginal penalization can be selected with `unbalanced` (:math:`(\lambda_1, \lambda_2)`) and `unbalanced_type`. By default ``unbalanced=None`` and the function solves the exact optimal transport problem (respecting the marginals). Parameters ---------- M : array-like, shape (dim_a, dim_b) Loss matrix a : array-like, shape (dim_a,), optional Samples weights in the source domain (default is uniform) b : array-like, shape (dim_b,), optional Samples weights in the source domain (default is uniform) reg : float, optional Regularization weight :math:`\lambda_r`, by default None (no reg., exact OT) c : array-like, shape (dim_a, dim_b), optional (default=None) Reference measure for the regularization. If None, then use :math:`\mathbf{c} = \mathbf{a} \mathbf{b}^T`. If :math:`\texttt{reg_type}=`'entropy', then :math:`\mathbf{c} = 1_{dim_a} 1_{dim_b}^T`. reg_type : str, optional Type of regularization :math:`R` either "KL", "L2", "entropy", by default "KL". a tuple of functions can be provided for general solver (see :any:`cg`). This is only used when ``reg!=None``. unbalanced : float or indexable object of length 1 or 2 Marginal relaxation term. If it is a scalar or an indexable object of length 1, then the same relaxation is applied to both marginal relaxations. The balanced OT can be recovered using :math:`unbalanced=float("inf")`. For semi-relaxed case, use either :math:`unbalanced=(float("inf"), scalar)` or :math:`unbalanced=(scalar, float("inf"))`. If unbalanced is an array, it must have the same backend as input arrays `(a, b, M)`. unbalanced_type : str, optional Type of unbalanced penalization function :math:`U` either "KL", "L2", "TV", by default "KL". method : str, optional Method for solving the problem when multiple algorithms are available, default None for automatic selection. n_threads : int, optional Number of OMP threads for exact OT solver, by default 1 max_iter : int, optional Maximum number of iterations, by default None (default values in each solvers) plan_init : array-like, shape (dim_a, dim_b), optional Initialization of the OT plan for iterative methods, by default None potentials_init : (array-like(dim_a,),array-like(dim_b,)), optional Initialization of the OT dual potentials for iterative methods (Sinkhorn, unbalanced) or warmstart for exact EMD solver. For EMD, these potentials are used to guide initial pivots in the network simplex. By default None tol : _type_, optional Tolerance for solution precision, by default None (default values in each solvers) verbose : bool, optional Print information in the solver, by default False grad : str, optional Type of gradient computation, either or 'autodiff', 'envelope' or 'last_step' used only for Sinkhorn solver. By default 'autodiff' provides gradients wrt all outputs (`plan, value, value_linear`) but with important memory cost. 'envelope' provides gradients only for `value` and and other outputs are detached. This is useful for memory saving when only the value is needed. 'last_step' provides gradients only for the last iteration of the Sinkhorn solver, but provides gradient for both the OT plan and the objective values. 'detach' does not compute the gradients for the Sinkhorn solver. Returns ------- res : OTResult() Result of the optimization problem. The information can be obtained as follows: - res.plan : OT plan :math:`\mathbf{T}` - res.potentials : OT dual potentials - res.value : Optimal value of the optimization problem - res.value_linear : Linear OT loss with the optimal OT plan See :any:`OTResult` for more information. Notes ----- The following methods are available for solving the OT problems: - **Classical exact OT problem [1]** (default parameters) : .. math:: \min_\mathbf{T} \quad \langle \mathbf{T}, \mathbf{M} \rangle_F s.t. \ \mathbf{T} \mathbf{1} = \mathbf{a} \mathbf{T}^T \mathbf{1} = \mathbf{b} \mathbf{T} \geq 0 can be solved with the following code: .. code-block:: python res = ot.solve(M, a, b) - **Entropic regularized OT [2]** (when ``reg!=None``): .. math:: \min_\mathbf{T} \quad \langle \mathbf{T}, \mathbf{M} \rangle_F + \lambda R(\mathbf{T}) s.t. \ \mathbf{T} \mathbf{1} = \mathbf{a} \mathbf{T}^T \mathbf{1} = \mathbf{b} \mathbf{T} \geq 0 can be solved with the following code: .. code-block:: python # default is ``"KL"`` regularization (``reg_type="KL"``) res = ot.solve(M, a, b, reg=1.0) # or for original Sinkhorn paper formulation [2] res = ot.solve(M, a, b, reg=1.0, reg_type='entropy') # Use envelope theorem differentiation for memory saving res = ot.solve(M, a, b, reg=1.0, grad='envelope') # M, a, b are torch tensors res.value.backward() # only the value is differentiable Note that by default the Sinkhorn solver uses automatic differentiation to compute the gradients of the values and plan. This can be changed with the `grad` parameter. The `envelope` mode computes the gradients only for the value and the other outputs are detached. This is useful for memory saving when only the gradient of value is needed. - **Quadratic regularized OT [17]** (when ``reg!=None`` and ``reg_type="L2"``): .. math:: \min_\mathbf{T} \quad \langle \mathbf{T}, \mathbf{M} \rangle_F + \lambda R(\mathbf{T}) s.t. \ \mathbf{T} \mathbf{1} = \mathbf{a} \mathbf{T}^T \mathbf{1} = \mathbf{b} \mathbf{T} \geq 0 can be solved with the following code: .. code-block:: python res = ot.solve(M,a,b,reg=1.0,reg_type='L2') - **Unbalanced OT [41]** (when ``unbalanced!=None``): .. math:: \min_{\mathbf{T}\geq 0} \quad \sum_{i,j} T_{i,j}M_{i,j} + \lambda_1 U(\mathbf{T}\mathbf{1},\mathbf{a}) + \lambda_2 U(\mathbf{T}^T\mathbf{1},\mathbf{b}) can be solved with the following code: .. code-block:: python # default is ``"KL"`` res = ot.solve(M,a,b,unbalanced=1.0) # quadratic unbalanced OT res = ot.solve(M,a,b,unbalanced=1.0,unbalanced_type='L2') # TV = partial OT res = ot.solve(M,a,b,unbalanced=1.0,unbalanced_type='TV') - **Regularized unbalanced regularized OT [34]** (when ``unbalanced!=None`` and ``reg!=None``): .. math:: \min_{\mathbf{T}\geq 0} \quad \sum_{i,j} T_{i,j}M_{i,j} + \lambda_r R(\mathbf{T}) + \lambda_1 U(\mathbf{T}\mathbf{1},\mathbf{a}) + \lambda_2 U(\mathbf{T}^T\mathbf{1},\mathbf{b}) can be solved with the following code: .. code-block:: python # default is ``"KL"`` for both res = ot.solve(M,a,b,reg=1.0,unbalanced=1.0) # quadratic unbalanced OT with KL regularization res = ot.solve(M,a,b,reg=1.0,unbalanced=1.0,unbalanced_type='L2') # both quadratic res = ot.solve(M,a,b,reg=1.0, reg_type='L2',unbalanced=1.0,unbalanced_type='L2') .. _references-solve: References ---------- .. [1] Bonneel, N., Van De Panne, M., Paris, S., & Heidrich, W. (2011, December). Displacement interpolation using Lagrangian mass transport. In ACM Transactions on Graphics (TOG) (Vol. 30, No. 6, p. 158). ACM. .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. .. [17] Blondel, M., Seguy, V., & Rolet, A. (2018). Smooth and Sparse Optimal Transport. Proceedings of the Twenty-First International Conference on Artificial Intelligence and Statistics (AISTATS). .. [34] Feydy, J., Séjourné, T., Vialard, F. X., Amari, S. I., Trouvé, A., & Peyré, G. (2019, April). Interpolating between optimal transport and MMD using Sinkhorn divergences. In The 22nd International Conference on Artificial Intelligence and Statistics (pp. 2681-2690). PMLR. .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. """ # detect backend nx = get_backend(M, a, b, c) # create uniform weights if not given if a is None: a = nx.ones(M.shape[0], type_as=M) / M.shape[0] if b is None: b = nx.ones(M.shape[1], type_as=M) / M.shape[1] if c is None: c = a[:, None] * b[None, :] if reg is None: reg = 0 if method is not None and method not in lst_valid_methods_solve: raise ValueError( "Unknown method={} for solve, must be in {}".format( method, lst_valid_methods_solve ) ) # default values for solutions potentials = None value = None value_linear = None plan = None status = None if reg == 0: # exact OT if unbalanced is None: # Exact balanced OT # default values for EMD solver if max_iter is None: max_iter = 1000000 value_linear, log = emd2( a, b, M, numItermax=max_iter, log=True, return_matrix=True, numThreads=n_threads, potentials_init=potentials_init, ) value = value_linear potentials = (log["u"], log["v"]) plan = log["G"] status = log["warning"] if log["warning"] is not None else "Converged" elif unbalanced_type.lower() in ["kl", "l2"]: # unbalanced exact OT # default values for exact unbalanced OT if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-12 plan, log = mm_unbalanced( a, b, M, reg_m=unbalanced, c=c, reg=reg, div=unbalanced_type, numItermax=max_iter, stopThr=tol, log=True, verbose=verbose, G0=plan_init, ) value_linear = log["cost"] value = log["total_cost"] elif unbalanced_type.lower() == "tv": if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-12 if isinstance(reg_type, str): reg_type = reg_type.lower() plan, log = lbfgsb_unbalanced( a, b, M, reg=reg, reg_m=unbalanced, c=c, reg_div=reg_type, regm_div=unbalanced_type, numItermax=max_iter, stopThr=tol, verbose=verbose, log=True, G0=plan_init, ) value_linear = log["cost"] value = log["total_cost"] else: raise ( NotImplementedError( 'Unknown unbalanced_type="{}"'.format(unbalanced_type) ) ) else: # regularized OT if unbalanced is None: # Balanced regularized OT if isinstance(reg_type, tuple): # general solver f, df = reg_type if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-9 plan, log = cg( a, b, M, reg=reg, f=f, df=df, numItermax=max_iter, stopThr=tol, log=True, verbose=verbose, G0=plan_init, ) value_linear = nx.sum(M * plan) value = log["loss"][-1] potentials = (log["u"], log["v"]) elif reg_type.lower() in ["entropy", "kl"]: if grad in [ "envelope", "last_step", "detach", ]: # if envelope, last_step or detach then detach the input M0, a0, b0 = M, a, b M, a, b = nx.detach(M, a, b) # default values for sinkhorn if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-9 if grad == "last_step": if max_iter == 0: raise ValueError( "The maximum number of iterations must be greater than 0 when using grad=last_step." ) max_iter = max_iter - 1 plan, log = sinkhorn_log( a, b, M, reg=reg, numItermax=max_iter, stopThr=tol, log=True, verbose=verbose, ) potentials = (log["log_u"], log["log_v"]) # if last_step, compute the last step of the Sinkhorn algorithm with the non-detached inputs if grad == "last_step": loga = nx.log(a0) logb = nx.log(b0) v = logb - nx.logsumexp(-M0 / reg + potentials[0][:, None], 0) u = loga - nx.logsumexp(-M0 / reg + potentials[1][None, :], 1) plan = nx.exp(-M0 / reg + u[:, None] + v[None, :]) potentials = (u, v) log["niter"] = max_iter + 1 log["log_u"] = u log["log_v"] = v log["u"] = nx.exp(u) log["v"] = nx.exp(v) value_linear = nx.sum(M * plan) if reg_type.lower() == "entropy": value = value_linear + reg * nx.sum(plan * nx.log(plan + 1e-16)) else: # stabilize kl of 0 mass if nx.any(a == 0) or nx.any(b == 0): a = a + 1e-15 * nx.min(a) b = b + 1e-15 * nx.min(b) print( "Warning: a or b has 0 mass, adding small mass to avoid NaN in KL divergence." ) value = value_linear + reg * nx.kl_div( plan, a[:, None] * b[None, :] + 1e-15 ) if grad == "envelope": # set the gradient at convergence value = nx.set_gradients( value, (M0, a0, b0), ( plan, reg * (potentials[0] - potentials[0].mean()), reg * (potentials[1] - potentials[1].mean()), ), ) elif reg_type.lower() == "l2": if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-9 plan, log = smooth_ot_dual( a, b, M, reg=reg, numItermax=max_iter, stopThr=tol, log=True, verbose=verbose, ) value_linear = nx.sum(M * plan) value = value_linear + reg * nx.sum(plan**2) potentials = (log["alpha"], log["beta"]) else: raise ( NotImplementedError( 'Not implemented reg_type="{}"'.format(reg_type) ) ) else: # unbalanced AND regularized OT if ( not isinstance(reg_type, tuple) and reg_type.lower() in ["kl"] and unbalanced_type.lower() == "kl" ): if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-9 plan, log = sinkhorn_knopp_unbalanced( a, b, M, reg=reg, reg_m=unbalanced, method=method, reg_type=reg_type, c=c, warmstart=potentials_init, numItermax=max_iter, stopThr=tol, verbose=verbose, log=True, ) value_linear = log["cost"] value = log["total_cost"] potentials = (log["logu"], log["logv"]) elif ( isinstance(reg_type, tuple) or reg_type.lower() in ["kl", "l2", "entropy"] ) and unbalanced_type.lower() in ["kl", "l2", "tv"]: if isinstance(reg_type, tuple) and nx.__name__ == "tf": raise NotImplementedError( "Custom regularization is not implemented for TensorFlow backend." ) if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-12 if isinstance(reg_type, str): reg_type = reg_type.lower() plan, log = lbfgsb_unbalanced( a, b, M, reg=reg, reg_m=unbalanced, c=c, reg_div=reg_type, regm_div=unbalanced_type, numItermax=max_iter, stopThr=tol, verbose=verbose, log=True, G0=plan_init, ) value_linear = log["cost"] value = log["total_cost"] else: raise ( NotImplementedError( 'Not implemented reg_type="{}" and unbalanced_type="{}"'.format( reg_type, unbalanced_type ) ) ) res = OTResult( potentials=potentials, value=value, value_linear=value_linear, plan=plan, status=status, backend=nx, ) return res def solve_sample( X_a, X_b, a=None, b=None, metric="sqeuclidean", reg=None, c=None, reg_type="KL", unbalanced=None, unbalanced_type="KL", lazy=False, batch_size=None, method=None, n_threads=1, max_iter=None, plan_init=None, rank=100, scaling=0.95, potentials_init=None, X_init=None, tol=None, verbose=False, grad="autodiff", random_state=None, debias=False, n_projections=50, projections=None, scaler=None, ): r"""Solve the discrete optimal transport problem using the samples in the source and target domains. The function solves the following general optimal transport problem .. math:: \min_{\mathbf{T}\geq 0} \quad \sum_{i,j} T_{i,j}M_{i,j} + \lambda_r R(\mathbf{T}) + \lambda_u U(\mathbf{T}\mathbf{1},\mathbf{a}) + \lambda_u U(\mathbf{T}^T\mathbf{1},\mathbf{b}) where the cost matrix :math:`\mathbf{M}` is computed from the samples in the source and target domains such that :math:`M_{i,j} = d(x_i,y_j)` where :math:`d` is a metric (by default the squared Euclidean distance). The regularization is selected with `reg` (:math:`\lambda_r`) and `reg_type`. By default ``reg=None`` and there is no regularization. The unbalanced marginal penalization can be selected with `unbalanced` (:math:`\lambda_u`) and `unbalanced_type`. By default ``unbalanced=None`` and the function solves the exact optimal transport problem (respecting the marginals). Parameters ---------- X_a : array-like, shape (n_samples_a, dim) samples in the source domain X_b : array-like, shape (n_samples_b, dim) samples in the target domain a : array-like, shape (dim_a,), optional Samples weights in the source domain (default is uniform) b : array-like, shape (dim_b,), optional Samples weights in the source domain (default is uniform) reg : float, optional Regularization weight :math:`\lambda_r`, by default None (no reg., exact OT) c : array-like, shape (dim_a, dim_b), optional (default=None) Reference measure for the regularization. If None, then use :math:`\mathbf{c} = \mathbf{a} \mathbf{b}^T`. If `reg_type='entropy'`, then :math:`\mathbf{c} = 1_{dim_a} 1_{dim_b}^T`. reg_type : str, optional Type of regularization :math:`R` either "KL", "L2", "entropy", by default "KL" unbalanced : float or indexable object of length 1 or 2 Marginal relaxation term. If it is a scalar or an indexable object of length 1, then the same relaxation is applied to both marginal relaxations. The balanced OT can be recovered using `unbalanced=float("inf")`. For semi-relaxed case, use either `unbalanced=(float("inf"), scalar)` or `unbalanced=(scalar, float("inf"))`. If unbalanced is an array, it must have the same backend as input arrays `(a, b, M)`. unbalanced_type : str, optional Type of unbalanced penalization function :math:`U` either "KL", "L2", "TV", by default "KL" lazy : bool, optional Return :any:`OTResultlazy` object to reduce memory cost when True, by default False batch_size : int, optional Batch size for lazy solver, by default None (default values in each solvers) method : str, optional Method for solving the problem, this can be used to select the solver for unbalanced problems (see :any:`ot.solve`), or to select a specific large scale solver. Available methods are: - "1d" for 1D OT solver done in parallel for each dimension. - "gaussian" for Gaussian OT solver that estimates mean and covariance and solve the closed form solution) - "gaussian_hd" for high-dimensional Gaussian OT solver with rank given by `rank` parameter . - "lowrank" for low-rank Sinkhorn solver (see :any:`ot.lowrank`) - "nystroem" for Nystroem Sinkhorn solver - "factored" for factored OT solver - "geomloss" for GeomLoss Sinkhorn solver - "sliced" for sliced wasserstein distance (see :any:`ot.sliced`) - "max_sliced" for max sliced wasserstein distance (see :any:`ot.sliced`) - "bsp" for BSP-OT solver (only for distribution with same number of samples and uniform weights) n_threads : int, optional Number of OMP threads for exact OT solver, by default 1 max_iter : int, optional Maximum number of iteration, by default None (default values in each solvers) plan_init : array-like, shape (dim_a, dim_b), optional Initialization of the OT plan for iterative methods, by default None rank : int, optional Rank of the OT matrix for lazy solvers (method='factored') or (method='nystroem'), by default 100 scaling : float, optional Scaling factor for the epsilon scaling lazy solvers (method='geomloss'), by default 0.95 potentials_init : (array-like(dim_a,),array-like(dim_b,)), optional Initialization of the OT dual potentials for iterative methods (Sinkhorn, unbalanced) or warmstart for exact EMD solver. For EMD, these potentials are used to guide initial pivots in the network simplex. By default None tol : _type_, optional Tolerance for solution precision, by default None (default values in each solvers) verbose : bool, optional Print information in the solver, by default False grad : str, optional Type of gradient computation, either or 'autodiff' or 'envelope' used only for Sinkhorn solver. By default 'autodiff' provides gradients wrt all outputs (`plan, value, value_linear`) but with important memory cost. 'envelope' provides gradients only for `value` and and other outputs are detached. This is useful for memory saving when only the value is needed. random_state : int, optional The random state for sampling the components in each distribution for method='nystroem'. debias : bool, optional Whether to use the debiased version of the OT problem, by default False if True, the value returned is the Sinkhorn divergence [23] but the plan is still the Sinkhorn plan. The results for all pairs of problems and provided in the log dictionary. if debiased='split', then X_a and X_b are split into two halves and the debiased value is computed using the two halves as proposed in minibatch OT [91]. n_projections : int, optional Number of projections for sliced OT, by default 50 projections : array-like, shape (n_projections, dim), optional Projections for sliced OT, by default None (random projections are used) scaler : callable, optional Function to scale the input data, following :any:`ot.utils.apply_scaler`. by default None (no scaling). Returns ------- res : OTResult() Result of the optimization problem. The information can be obtained as follows: - res.plan : OT plan :math:`\mathbf{T}` - res.potentials : OT dual potentials - res.value : Optimal value of the optimization problem - res.value_linear : Linear OT loss with the optimal OT plan - res.lazy_plan : Lazy OT plan (when ``lazy=True`` or lazy method) See :any:`OTResult` for more information. Notes ----- The following methods are available for solving the OT problems: - **Classical exact OT problem [1]** (default parameters) : .. math:: \min_\mathbf{T} \quad \langle \mathbf{T}, \mathbf{M} \rangle_F s.t. \ \mathbf{T} \mathbf{1} = \mathbf{a} \mathbf{T}^T \mathbf{1} = \mathbf{b} \mathbf{T} \geq 0, M_{i,j} = d(x_i,y_j) can be solved with the following code: .. code-block:: python res = ot.solve_sample(xa, xb, a, b) # for uniform weights res = ot.solve_sample(xa, xb) - **Entropic regularized OT [2]** (when ``reg!=None``): .. math:: \min_\mathbf{T} \quad \langle \mathbf{T}, \mathbf{M} \rangle_F + \lambda R(\mathbf{T}) s.t. \ \mathbf{T} \mathbf{1} = \mathbf{a} \mathbf{T}^T \mathbf{1} = \mathbf{b} \mathbf{T} \geq 0, M_{i,j} = d(x_i,y_j) can be solved with the following code: .. code-block:: python # default is ``"KL"`` regularization (``reg_type="KL"``) res = ot.solve_sample(xa, xb, a, b, reg=1.0) # or for original Sinkhorn paper formulation [2] res = ot.solve_sample(xa, xb, a, b, reg=1.0, reg_type='entropy') # lazy solver of memory complexity O(n) res = ot.solve_sample(xa, xb, a, b, reg=1.0, lazy=True, batch_size=100) # lazy OT plan lazy_plan = res.lazy_plan # Use envelope theorem differentiation for memory saving res = ot.solve_sample(xa, xb, a, b, reg=1.0, grad='envelope') res.value.backward() # only the value is differentiable Note that by default the Sinkhorn solver uses automatic differentiation to compute the gradients of the values and plan. This can be changed with the `grad` parameter. The `envelope` mode computes the gradients only for the value and the other outputs are detached. This is useful for memory saving when only the gradient of value is needed. We also have a very efficient solver with compiled CPU/CUDA code using geomloss/PyKeOps that can be used with the following code: .. code-block:: python # automatic solver res = ot.solve_sample(xa, xb, a, b, reg=1.0, method='geomloss') .. warning:: The geomloss solver is a thin wrapper around the `geomloss.ot.solve_sample` function. The API is still under development and some features might be missing. Please refer to the `geomloss` documentation for more information. - **Quadratic regularized OT [17]** (when ``reg!=None`` and ``reg_type="L2"``): .. math:: \min_\mathbf{T} \quad \langle \mathbf{T}, \mathbf{M} \rangle_F + \lambda R(\mathbf{T}) s.t. \ \mathbf{T} \mathbf{1} = \mathbf{a} \mathbf{T}^T \mathbf{1} = \mathbf{b} \mathbf{T} \geq 0, M_{i,j} = d(x_i,y_j) can be solved with the following code: .. code-block:: python res = ot.solve_sample(xa, xb, a, b, reg=1.0, reg_type='L2') - **Debiased OT and Sinkhorn divergence** (when ``debias=True``): One can compute the Sinkhorn divergence between two distributions using the following code: .. code-block:: python div = ot.solve_sample(xa, xb, a, b, reg=1.0, debias=True).value Debiasing for all existing solvers is implemented and in particular one can recover the minibatch exact OT debiasing [91] using the following code: .. code-block:: python div = ot.solve_sample(xa, xb, a, b, debias='split').value - **Unbalanced OT [41]** (when ``unbalanced!=None``): .. math:: \min_{\mathbf{T}\geq 0} \quad \sum_{i,j} T_{i,j}M_{i,j} + \lambda_u U(\mathbf{T}\mathbf{1},\mathbf{a}) + \lambda_u U(\mathbf{T}^T\mathbf{1},\mathbf{b}) \text{with} \ M_{i,j} = d(x_i,y_j) can be solved with the following code: .. code-block:: python # default is ``"KL"`` res = ot.solve_sample(xa, xb, a, b, unbalanced=1.0) # quadratic unbalanced OT res = ot.solve_sample(xa, xb, a, b, unbalanced=1.0,unbalanced_type='L2') # TV = partial OT res = ot.solve_sample(xa, xb, a, b, unbalanced=1.0,unbalanced_type='TV') - **Regularized unbalanced regularized OT [34]** (when ``unbalanced!=None`` and ``reg!=None``): .. math:: \min_{\mathbf{T}\geq 0} \quad \sum_{i,j} T_{i,j}M_{i,j} + \lambda_r R(\mathbf{T}) + \lambda_u U(\mathbf{T}\mathbf{1},\mathbf{a}) + \lambda_u U(\mathbf{T}^T\mathbf{1},\mathbf{b}) \text{with} \ M_{i,j} = d(x_i,y_j) can be solved with the following code: .. code-block:: python # default is ``"KL"`` for both res = ot.solve_sample(xa, xb, a, b, reg=1.0, unbalanced=1.0) # quadratic unbalanced OT with KL regularization res = ot.solve_sample(xa, xb, a, b, reg=1.0, unbalanced=1.0,unbalanced_type='L2') # both quadratic res = ot.solve_sample(xa, xb, a, b, reg=1.0, reg_type='L2', unbalanced=1.0, unbalanced_type='L2') - **Factored OT [2]** (when ``method='factored'``): This method solve the following OT problem [40]_ .. math:: \mathop{\arg \min}_\mu \quad W_2^2(\mu_a,\mu)+ W_2^2(\mu,\mu_b) where $\mu$ is a uniform weighted empirical distribution of :math:`\mu_a` and :math:`\mu_b` are the empirical measures associated to the samples in the source and target domains, and :math:`W_2` is the Wasserstein distance. This problem is solved using exact OT solvers for `reg=None` and the Sinkhorn solver for `reg!=None`. The solution provides two transport plans that can be used to recover a low rank OT plan between the two distributions. .. code-block:: python res = ot.solve_sample(xa, xb, method='factored', rank=10) # recover the lazy low rank plan factored_solution_lazy = res.lazy_plan # recover the full low rank plan factored_solution = factored_solution_lazy[:] - ** Nystroem OT [76] ** (when ``method='nystroem'``): Corresponds to a low rank approximation of entropic OT (for a squared Euclidean cost) that runs in linear time. - **Gaussian Bures-Wasserstein** (when ``method='gaussian'``): This method computes the Gaussian Bures-Wasserstein distance between two Gaussian distributions estimated from the empirical distributions .. math:: \mathcal{W}(\mu_s, \mu_t)_2^2= \left\lVert \mathbf{m}_s - \mathbf{m}_t \right\rVert^2 + \mathcal{B}(\Sigma_s, \Sigma_t)^{2} where : .. math:: \mathbf{B}(\Sigma_s, \Sigma_t)^{2} = \text{Tr}\left(\Sigma_s + \Sigma_t - 2 \sqrt{\Sigma_s^{1/2}\Sigma_t\Sigma_s^{1/2}} \right) The covariances and means are estimated from the data. .. code-block:: python res = ot.solve_sample(xa, xb, method='gaussian') # recover the squared Gaussian Bures-Wasserstein distance BW_dist = res.value The Gaussian-HD variant of this method can be used for high-dimensional data with the following code: .. code-block:: python res = ot.solve_sample(xa, xb, method='gaussian_hd', rank=10) where the rank parameter controls the rank of the covariance matrices used in the computation of the Bures-Wasserstein distance. - **Wasserstein 1d [1]** (when ``method='1D'``): This method computes the Wasserstein distance between two 1d distributions estimated from the empirical distributions. For multivariate data the distances are computed independently for each dimension. .. code-block:: python res = ot.solve_sample(xa, xb, method='1D') # recover the squared Wasserstein distances W_dists = res.value - **Sliced Wasserstein distance** (when ``method='sliced'``): This method computes the sliced Wasserstein distance between two distributions. The sliced Wasserstein distance is defined as the average of the Wasserstein distances between the projected distributions on random directions. .. code-block:: python swd = ot.solve_sample(xa, xb, method='sliced', n_projections=50).value - **Max Sliced Wasserstein [34]** (when ``method='max_sliced'``): This method computes the max sliced Wasserstein distance between two distributions. .. code-block:: python mswd = ot.solve_sample(xa, xb, method='max_sliced', n_projections=50).value - **Binary Space Partitioning OT (BSP)** (when ``method='bsp'``): This method computes the BSP-OT distance between two distributions and alignments. The number of partitioning directions can be set with the `n_projections` parameter. .. code-block:: python res = ot.solve_sample(xa, xb, method='bsp', n_projections=50) bsp_ot_distance = res.value bsp_plan = res.plan .. _references-solve-sample: References ---------- .. [1] Bonneel, N., Van De Panne, M., Paris, S., & Heidrich, W. (2011, December). Displacement interpolation using Lagrangian mass transport. In ACM Transactions on Graphics (TOG) (Vol. 30, No. 6, p. 158). ACM. .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. .. [17] Blondel, M., Seguy, V., & Rolet, A. (2018). Smooth and Sparse Optimal Transport. Proceedings of the Twenty-First International Conference on Artificial Intelligence and Statistics (AISTATS). .. [23] Aude, G., Peyré, G., Cuturi, M., Learning Generative Models with Sinkhorn Divergences, Proceedings of the Twenty-First International Conference on Artificial Intelligence and Statistics, (AISTATS) 21, 2018 .. [34] Feydy, J., Séjourné, T., Vialard, F. X., Amari, S. I., Trouvé, A., & Peyré, G. (2019, April). Interpolating between optimal transport and MMD using Sinkhorn divergences. In The 22nd International Conference on Artificial Intelligence and Statistics (pp. 2681-2690). PMLR. .. [40] Forrow, A., Hütter, J. C., Nitzan, M., Rigollet, P., Schiebinger, G., & Weed, J. (2019, April). Statistical optimal transport via factored couplings. In The 22nd International Conference on Artificial Intelligence and Statistics (pp. 2454-2465). PMLR. .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. .. [65] Scetbon, M., Cuturi, M., & Peyré, G. (2021). Low-rank Sinkhorn Factorization. In International Conference on Machine Learning. .. [80] Altschuler, J., Bach, F., Rudi, A., Niles-Weed, J. (2019). Massively scalable Sinkhorn distances via the Nyström method. NeurIPS. .. [91] Fatras, K., Zine, Y., Majewski, S., Flamary, R., Gribonval, R., & Courty, N. (2021). [Minibatch optimal transport distances; analysis and applications](https://arxiv.org/pdf/2101.01792). arXiv preprint arXiv:2101.01792. """ if method is not None and method.lower() not in all_valid_methods_solve_sample: raise ValueError( "Unknown method={} for solve_sample. Available methods are: {}".format( method, all_valid_methods_solve_sample ) ) if scaler is not None: X_a, Xb = apply_scaler(X_a, X_b, scaler=scaler) if debias: dict_params = dict( metric=metric, reg=reg, c=c, reg_type=reg_type, unbalanced=unbalanced, unbalanced_type=unbalanced_type, lazy=lazy, batch_size=batch_size, method=method, n_threads=n_threads, max_iter=max_iter, plan_init=plan_init, rank=rank, scaling=scaling, potentials_init=potentials_init, X_init=X_init, tol=tol, verbose=verbose, grad=grad, random_state=random_state, debias=False, ) nx = get_backend(X_a, X_b, a, b) if nx.__name__ == "tf": raise NotImplementedError( "Debiasing is not implemented for TensorFlow backend." ) if isinstance(debias, str) and debias == "split": # split the samples into two halves with each half the mass X_a1, X_a2, a1, a2, sel_a1, sel_a2 = split_sample_ratio(X_a, a, nx=nx) X_b1, X_b2, b1, b2, sel_b1, sel_b2 = split_sample_ratio(X_b, b, nx=nx) # compute the four OT problems resaa = solve_sample(X_a1, X_a2, a=a1, b=a2, **dict_params) resbb = solve_sample(X_b1, X_b2, a=b1, b=b2, **dict_params) resab1 = solve_sample(X_a1, X_b1, a=a1, b=b1, **dict_params) resab2 = solve_sample(X_a2, X_b2, a=a2, b=b2, **dict_params) # compute debiased values value = (resab1.value + resab2.value) - (resaa.value + resbb.value) value_linear = value log = { "res_aa": resaa, "res_bb": resbb, "res_ab1": resab1, "res_ab2": resab2, "sel_a1": sel_a1, "sel_a2": sel_a2, "sel_b1": sel_b1, "sel_b2": sel_b2, "a1": a1, "a2": a2, "b1": b1, "b2": b2, } res = OTResult( value=value, value_linear=value_linear, plan=None, # no plan for debiased version potentials=None, # no potentials for debiased version sparse_plan=None, # no sparse plan for debiased version lazy_plan=None, # no lazy plan for debiased version status="Debiased", log=log, backend=nx, ) else: # standard debiasing à la sinkhorn divergence resaa = solve_sample(X_a, X_a, a=a, b=a, **dict_params) resbb = solve_sample(X_b, X_b, a=b, b=b, **dict_params) resab = solve_sample(X_a, X_b, a=a, b=b, **dict_params) # compute debiased values value = resab.value - 0.5 * (resaa.value + resbb.value) value_linear = resab.value_linear - 0.5 * ( resaa.value_linear + resbb.value_linear ) log = {"res_aa": resaa, "res_bb": resbb, "res_ab": resab} res = OTResult( value=value, value_linear=value_linear, plan=resab.plan, potentials=resab.potentials, sparse_plan=resab.sparse_plan, lazy_plan=resab.lazy_plan, status=resab.status, log=log, backend=nx, ) # return debiased result return res if method is not None and method.lower() in all_valid_methods_solve_sample: lazy0 = lazy lazy = True if not lazy: # default non lazy solver calls ot.solve # compute cost matrix M and use solve function M = dist(X_a, X_b, metric) res = solve( M, a, b, reg, c, reg_type, unbalanced, unbalanced_type, method, n_threads, max_iter, plan_init, potentials_init, tol, verbose, grad, ) return res elif lazy and method is None and (reg is None or reg == 0) and unbalanced is None: # Use lazy EMD solver with coordinates (no regularization, balanced) nx = get_backend(X_a, X_b, a, b) value_linear, log = emd2_lazy( X_a, X_b, a, b, metric=metric, numItermax=max_iter if max_iter is not None else 100000, log=True, return_matrix=True, potentials_init=potentials_init, ) res = OTResult( potentials=(log["u"], log["v"]), value=value_linear, value_linear=value_linear, sparse_plan=log["G"], backend=nx, status=log["warning"] if log["warning"] is not None else "Converged", ) return res else: # Detect backend nx = get_backend(X_a, X_b, a, b) # default values for solutions potentials = None value = None value_linear = None plan = None lazy_plan = None sparse_plan = None status = None log = None method = method.lower() if method is not None else "" if method == "1d": # Wasserstein 1d (parallel on all dimensions) if metric == "sqeuclidean": p = 2 elif metric in ["euclidean", "cityblock"]: p = 1 else: raise ( NotImplementedError('Not implemented metric="{}"'.format(metric)) ) value = wasserstein_1d(X_a, X_b, a, b, p=p) value_linear = value elif method == "sliced": # Sliced Wasserstein distance if metric == "sqeuclidean": p = 2 elif metric in ["euclidean", "cityblock"]: p = 1 else: raise ( NotImplementedError('Not implemented metric="{}"'.format(metric)) ) value, log = sliced_wasserstein_distance( X_a, X_b, a=a, b=b, p=p, n_projections=n_projections, projections=projections, seed=random_state, log=True, ) value_linear = value elif method == "max_sliced": # Max Sliced Wasserstein distance if metric == "sqeuclidean": p = 2 elif metric in ["euclidean", "cityblock"]: p = 1 else: raise ( NotImplementedError('Not implemented metric="{}"'.format(metric)) ) value, log = max_sliced_wasserstein_distance( X_a, X_b, a=a, b=b, p=p, n_projections=n_projections, projections=projections, seed=random_state, log=True, ) value_linear = value elif method == "bsp": # BSP-OT solver if metric == "sqeuclidean": p = 2 elif metric in ["euclidean", "cityblock"]: p = 1 else: raise ( NotImplementedError('Not implemented metric="{}"'.format(metric)) ) if (X_a.shape[0] != X_b.shape[0]) or a is not None or b is not None: raise ValueError( "BSP-OT solver requires the same number of samples in both distributions and uniform weights (provided as None)." ) if random_state is None: random_state = 0 n = X_a.shape[0] value, perm, perms = compute_bspot_bijection( X_a, X_b, p=p, seed=random_state, n_plans=n_projections ) value_linear = value sparse_plan = nx.coo_matrix( nx.ones(n, type_as=X_a) / n, nx.arange(n), perm, shape=(n, n), type_as=X_a, ) log = { "perm": perm, "perms": perms, } elif method == "gaussian": # Gaussian Bures-Wasserstein if metric.lower() not in ["sqeuclidean"]: raise ( NotImplementedError('Not implemented metric="{}"'.format(metric)) ) if reg is None: reg = 1e-6 if a is not None and len(a.shape) == 1: a = a.reshape(-1, 1) if b is not None and len(b.shape) == 1: b = b.reshape(-1, 1) value, log = empirical_bures_wasserstein_distance( X_a, X_b, reg=reg, ws=a, wt=b, log=True ) value = value**2 # return the value (squared bures distance) value_linear = value # return the value elif method == "gaussian_hd": # Gaussian BW for high-dimensional data if metric.lower() not in ["sqeuclidean"]: raise ( NotImplementedError('Not implemented metric="{}"'.format(metric)) ) if reg is None: reg = 1e-5 if a is not None and len(a.shape) == 1: a = a.reshape(-1, 1) if b is not None and len(b.shape) == 1: b = b.reshape(-1, 1) if rank > X_a.shape[1] or rank > X_b.shape[1]: warn( "Rank is larger than the number of features. Using full rank for Gaussian Bures-Wasserstein distance." ) rank = min(X_a.shape[1], X_b.shape[1]) value, log = empirical_bures_wasserstein_distance_hd( X_a, X_b, d_intrinsic=rank, ws=a, wt=b, reg=reg, log=True ) value = value**2 # return the value (squared bures distance) value_linear = value # return the value elif method == "factored": # Factored OT if metric.lower() not in ["sqeuclidean"]: raise ( NotImplementedError('Not implemented metric="{}"'.format(metric)) ) if max_iter is None: max_iter = 100 if tol is None: tol = 1e-7 if reg is None: reg = 0 Q, R, X, log = factored_optimal_transport( X_a, X_b, reg=reg, r=rank, log=True, stopThr=tol, numItermax=max_iter, verbose=verbose, ) log["X"] = X value_linear = log["costa"] + log["costb"] value = value_linear # TODO add reg term lazy_plan = log["lazy_plan"] if not lazy0: # store plan if not lazy plan = lazy_plan[:] elif method == "lowrank": if metric.lower() not in ["sqeuclidean"]: raise ( NotImplementedError('Not implemented metric="{}"'.format(metric)) ) if max_iter is None: max_iter = 2000 if tol is None: tol = 1e-7 if reg is None: reg = 0 Q, R, g, log = lowrank_sinkhorn( X_a, X_b, rank=rank, reg=reg, a=a, b=b, numItermax=max_iter, stopThr=tol, log=True, ) value = log["value"] value_linear = log["value_linear"] lazy_plan = log["lazy_plan"] if not lazy0: # store plan if not lazy plan = lazy_plan[:] elif method == "nystroem": if metric.lower() not in ["sqeuclidean"]: raise ( NotImplementedError('Not implemented metric="{}"'.format(metric)) ) if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-7 if reg is None: reg = 1.0 value, log = empirical_sinkhorn_nystroem2( X_a, X_b, reg=reg, anchors=rank, a=a, b=b, numItermax=max_iter, verbose=verbose, stopThr=tol, random_state=random_state, log=True, ) lazy_plan = log["lazy_plan"] if not lazy0: # store plan if not lazy plan = lazy_plan[:] elif method.startswith("geomloss"): # Geomloss solver for entropic OT split_method = method.split("_") if len(split_method) == 2: backend = split_method[1] else: if lazy0 is None: backend = "auto" elif lazy0: backend = "online" else: backend = "tensorized" if lazy0 is None: warn( f"geomloss backend is set to '{backend}' but is not yet supported by unified geomloss API yet." ) if old_geomloss: # old wrapper for old geomloss versions raise NotImplementedError( "geomloss version >= 0.3.1 required for ot.solve_sample() geomloss backend." ) else: # new geomloss wrapper import geomloss.ot as got if max_iter is None: max_iter = 1000 res = got.solve_sample( X_a, X_b, a=a, b=b, reg=reg, cost=metric, unbalanced=unbalanced, unbalanced_type=unbalanced_type, max_iter=max_iter, tol=tol, ) res.value_linear = None return res elif reg is None or reg == 0: # exact OT if unbalanced is None: # balanced EMD solver not available for lazy raise ( NotImplementedError( "Exact OT solver with lazy=True not implemented" ) ) else: raise ( NotImplementedError( 'Non regularized lazy solver with unbalanced_type="{}" not implemented'.format( unbalanced_type ) ) ) else: if unbalanced is None: if max_iter is None: max_iter = 1000 if tol is None: tol = 1e-9 if batch_size is None: batch_size = 100 value_linear, log = empirical_sinkhorn2( X_a, X_b, reg, a, b, metric=metric, numIterMax=max_iter, stopThr=tol, isLazy=True, batchSize=batch_size, verbose=verbose, log=True, ) # compute potentials potentials = (log["u"], log["v"]) lazy_plan = log["lazy_plan"] else: raise ( NotImplementedError( 'Not implemented unbalanced_type="{}" with regularization'.format( unbalanced_type ) ) ) res = OTResult( potentials=potentials, value=value, lazy_plan=lazy_plan, value_linear=value_linear, sparse_plan=sparse_plan, plan=plan, status=status, backend=nx, log=log, ) return res python-pot-0.9.7+dfsg/ot/stochastic.py000066400000000000000000000631211525521106200177310ustar00rootroot00000000000000""" Stochastic solvers for regularized OT. """ # Authors: Kilian Fatras # Rémi Flamary # # License: MIT License import numpy as np from .utils import dist, check_random_state from .backend import get_backend ############################################################################## # Optimization toolbox for SEMI - DUAL problems ############################################################################## def coordinate_grad_semi_dual(b, M, reg, beta, i): r""" Compute the coordinate gradient update for regularized discrete distributions for :math:`(i, :)` The function computes the gradient of the semi dual problem: .. math:: \max_\mathbf{v} \ \sum_i \mathbf{a}_i \left[ \sum_j \mathbf{v}_j \mathbf{b}_j - \mathrm{reg} \cdot \log \left( \sum_j \mathbf{b}_j \exp \left( \frac{\mathbf{v}_j - \mathbf{M}_{i,j}}{\mathrm{reg}} \right) \right) \right] Where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`\mathbf{v}` is a dual variable in :math:`\mathbb{R}^{nt}` - reg is the regularization term - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) The algorithm used for solving the problem is the ASGD & SAG algorithms as proposed in :ref:`[18] ` [alg.1 & alg.2] Parameters ---------- b : ndarray, shape (nt,) Target measure. M : ndarray, shape (ns, nt) Cost matrix. reg : float Regularization term > 0. v : ndarray, shape (nt,) Dual variable. i : int Picked number `i`. Returns ------- coordinate gradient : ndarray, shape (nt,) .. _references-coordinate-grad-semi-dual: References ---------- .. [18] Genevay, A., Cuturi, M., Peyré, G. & Bach, F. (2016) Stochastic Optimization for Large-scale Optimal Transport. Advances in Neural Information Processing Systems (2016). """ r = M[i, :] - beta exp_beta = np.exp(-r / reg) * b khi = exp_beta / (np.sum(exp_beta)) return b - khi def sag_entropic_transport(a, b, M, reg, numItermax=10000, lr=None, random_state=None): r""" Compute the SAG algorithm to solve the regularized discrete measures optimal transport max problem The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot\Omega(\gamma) s.t. \ \gamma \mathbf{1} = \mathbf{a} \gamma^T \mathbf{1} = \mathbf{b} \gamma \geq 0 Where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`\Omega` is the entropic regularization term with :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) The algorithm used for solving the problem is the SAG algorithm as proposed in :ref:`[18] ` [alg.1] Parameters ---------- a : ndarray, shape (ns,), Source measure. b : ndarray, shape (nt,), Target measure. M : ndarray, shape (ns, nt), Cost matrix. reg : float Regularization term > 0 numItermax : int Number of iteration. lr : float Learning rate. random_state : int, RandomState instance or None, default=None Determines random number generation. Pass an int for reproducible output across multiple function calls. Returns ------- v : ndarray, shape (`nt`,) Dual variable. .. _references-sag-entropic-transport: References ---------- .. [18] Genevay, A., Cuturi, M., Peyré, G. & Bach, F. (2016) Stochastic Optimization for Large-scale Optimal Transport. Advances in Neural Information Processing Systems (2016). """ if lr is None: lr = 1.0 / max(a / reg) n_source = np.shape(M)[0] n_target = np.shape(M)[1] cur_beta = np.zeros(n_target) stored_gradient = np.zeros((n_source, n_target)) sum_stored_gradient = np.zeros(n_target) rng = check_random_state(random_state) for _ in range(numItermax): i = rng.randint(n_source) cur_coord_grad = a[i] * coordinate_grad_semi_dual(b, M, reg, cur_beta, i) sum_stored_gradient += cur_coord_grad - stored_gradient[i] stored_gradient[i] = cur_coord_grad cur_beta += lr * (1.0 / n_source) * sum_stored_gradient return cur_beta def averaged_sgd_entropic_transport( a, b, M, reg, numItermax=300000, lr=None, random_state=None ): r""" Compute the ASGD algorithm to solve the regularized semi continuous measures optimal transport max problem The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg}\cdot\Omega(\gamma) s.t. \gamma \mathbf{1} = \mathbf{a} \gamma^T \mathbf{1} = \mathbf{b} \gamma \geq 0 Where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`\Omega` is the entropic regularization term with :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) The algorithm used for solving the problem is the ASGD algorithm as proposed in :ref:`[18] ` [alg.2] Parameters ---------- a : ndarray, shape (ns,) source measure b : ndarray, shape (nt,) target measure M : ndarray, shape (ns, nt) cost matrix reg : float Regularization term > 0 numItermax : int Number of iteration. lr : float Learning rate. random_state : int, RandomState instance or None, default=None Determines random number generation. Pass an int for reproducible output across multiple function calls. Returns ------- ave_v : ndarray, shape (`nt`,) dual variable .. _references-averaged-sgd-entropic-transport: References ---------- .. [18] Genevay, A., Cuturi, M., Peyré, G. & Bach, F. (2016) Stochastic Optimization for Large-scale Optimal Transport. Advances in Neural Information Processing Systems (2016). """ if lr is None: lr = 1.0 / max(a / reg) n_source = np.shape(M)[0] n_target = np.shape(M)[1] cur_beta = np.zeros(n_target) ave_beta = np.zeros(n_target) rng = check_random_state(random_state) for cur_iter in range(numItermax): k = cur_iter + 1 i = rng.randint(n_source) cur_coord_grad = coordinate_grad_semi_dual(b, M, reg, cur_beta, i) cur_beta += (lr / np.sqrt(k)) * cur_coord_grad ave_beta = (1.0 / k) * cur_beta + (1 - 1.0 / k) * ave_beta return ave_beta def c_transform_entropic(b, M, reg, beta): r""" The goal is to recover u from the c-transform. The function computes the c-transform of a dual variable from the other dual variable: .. math:: \mathbf{u} = \mathbf{v}^{c,reg} = - \mathrm{reg} \sum_j \mathbf{b}_j \exp\left( \frac{\mathbf{v} - \mathbf{M}}{\mathrm{reg}} \right) Where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`\mathbf{u}`, :math:`\mathbf{v}` are dual variables in :math:`\mathbb{R}^{ns} \times \mathbb{R}^{nt}` - reg is the regularization term It is used to recover an optimal u from optimal v solving the semi dual problem, see Proposition 2.1 of :ref:`[18] ` Parameters ---------- b : ndarray, shape (nt,) Target measure M : ndarray, shape (ns, nt) Cost matrix reg : float Regularization term > 0 v : ndarray, shape (nt,) Dual variable. Returns ------- u : ndarray, shape (`ns`,) Dual variable. .. _references-c-transform-entropic: References ---------- .. [18] Genevay, A., Cuturi, M., Peyré, G. & Bach, F. (2016) Stochastic Optimization for Large-scale Optimal Transport. Advances in Neural Information Processing Systems (2016). """ n_source = np.shape(M)[0] alpha = np.zeros(n_source) for i in range(n_source): r = M[i, :] - beta min_r = np.min(r) exp_beta = np.exp(-(r - min_r) / reg) * b alpha[i] = min_r - reg * np.log(np.sum(exp_beta)) return alpha def solve_semi_dual_entropic( a, b, M, reg, method, numItermax=10000, lr=None, log=False ): r""" Compute the transportation matrix to solve the regularized discrete measures optimal transport max problem The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot\Omega(\gamma) s.t. \ \gamma \mathbf{1} = \mathbf{a} \gamma^T \mathbf{1} = \mathbf{b} \gamma \geq 0 Where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`\Omega` is the entropic regularization term with :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) The algorithm used for solving the problem is the SAG or ASGD algorithms as proposed in :ref:`[18] ` Parameters ---------- a : ndarray, shape (ns,) source measure b : ndarray, shape (nt,) target measure M : ndarray, shape (ns, nt) cost matrix reg : float Regularization term > 0 method : str used method (SAG or ASGD) numItermax : int number of iteration lr : float learning rate n_source : int size of the source measure n_target : int size of the target measure log : bool, optional record log if True Returns ------- pi : ndarray, shape (ns, nt) transportation matrix log : dict log dictionary return only if log==True in parameters .. _references-solve-semi-dual-entropic: References ---------- .. [18] Genevay, A., Cuturi, M., Peyré, G. & Bach, F. (2016) Stochastic Optimization for Large-scale Optimal Transport. Advances in Neural Information Processing Systems (2016). """ if method.lower() == "sag": opt_beta = sag_entropic_transport(a, b, M, reg, numItermax, lr) elif method.lower() == "asgd": opt_beta = averaged_sgd_entropic_transport(a, b, M, reg, numItermax, lr) else: print("Please, select your method between SAG and ASGD") return None opt_alpha = c_transform_entropic(b, M, reg, opt_beta) pi = ( np.exp((opt_alpha[:, None] + opt_beta[None, :] - M[:, :]) / reg) * a[:, None] * b[None, :] ) if log: log = {} log["alpha"] = opt_alpha log["beta"] = opt_beta return pi, log else: return pi ############################################################################## # Optimization toolbox for DUAL problems ############################################################################## def batch_grad_dual(a, b, M, reg, alpha, beta, batch_size, batch_alpha, batch_beta): r""" Computes the partial gradient of the dual optimal transport problem. For each :math:`(i,j)` in a batch of coordinates, the partial gradients are : .. math:: \partial_{\mathbf{u}_i} F = \frac{b_s}{l_v} \mathbf{u}_i - \sum_{j \in B_v} \mathbf{a}_i \mathbf{b}_j \exp\left( \frac{\mathbf{u}_i + \mathbf{v}_j - \mathbf{M}_{i,j}}{\mathrm{reg}} \right) \partial_{\mathbf{v}_j} F = \frac{b_s}{l_u} \mathbf{v}_j - \sum_{i \in B_u} \mathbf{a}_i \mathbf{b}_j \exp\left( \frac{\mathbf{u}_i + \mathbf{v}_j - \mathbf{M}_{i,j}}{\mathrm{reg}} \right) Where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`\mathbf{u}`, :math:`\mathbf{v}` are dual variables in :math:`\mathbb{R}^{ns} \times \mathbb{R}^{nt}` - reg is the regularization term - :math:`B_u` and :math:`B_v` are lists of index - :math:`b_s` is the size of the batches :math:`B_u` and :math:`B_v` - :math:`l_u` and :math:`l_v` are the lengths of :math:`B_u` and :math:`B_v` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) The algorithm used for solving the dual problem is the SGD algorithm as proposed in :ref:`[19] ` [alg.1] Parameters ---------- a : ndarray, shape (ns,) source measure b : ndarray, shape (nt,) target measure M : ndarray, shape (ns, nt) cost matrix reg : float Regularization term > 0 alpha : ndarray, shape (ns,) dual variable beta : ndarray, shape (nt,) dual variable batch_size : int size of the batch batch_alpha : ndarray, shape (bs,) batch of index of alpha batch_beta : ndarray, shape (bs,) batch of index of beta Returns ------- grad : ndarray, shape (`ns`,) partial grad F .. _references-batch-grad-dual: References ---------- .. [19] Seguy, V., Bhushan Damodaran, B., Flamary, R., Courty, N., Rolet, A.& Blondel, M. Large-scale Optimal Transport and Mapping Estimation. International Conference on Learning Representation (2018) """ G = -( np.exp( ( alpha[batch_alpha, None] + beta[None, batch_beta] - M[batch_alpha, :][:, batch_beta] ) / reg ) * a[batch_alpha, None] * b[None, batch_beta] ) grad_beta = np.zeros(np.shape(M)[1]) grad_alpha = np.zeros(np.shape(M)[0]) grad_beta[batch_beta] = b[batch_beta] * len(batch_alpha) / np.shape(M)[0] + G.sum(0) grad_alpha[batch_alpha] = a[batch_alpha] * len(batch_beta) / np.shape(M)[1] + G.sum( 1 ) return grad_alpha, grad_beta def sgd_entropic_regularization( a, b, M, reg, batch_size, numItermax, lr, random_state=None ): r""" Compute the sgd algorithm to solve the regularized discrete measures optimal transport dual problem The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot\Omega(\gamma) s.t. \ \gamma \mathbf{1} = \mathbf{a} \gamma^T \mathbf{1} = \mathbf{b} \gamma \geq 0 Where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`\Omega` is the entropic regularization term with :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) Parameters ---------- a : ndarray, shape (ns,) source measure b : ndarray, shape (nt,) target measure M : ndarray, shape (ns, nt) cost matrix reg : float Regularization term > 0 batch_size : int size of the batch numItermax : int number of iteration lr : float learning rate random_state : int, RandomState instance or None, default=None Determines random number generation. Pass an int for reproducible output across multiple function calls. Returns ------- alpha : ndarray, shape (ns,) dual variable beta : ndarray, shape (nt,) dual variable References ---------- .. [19] Seguy, V., Bhushan Damodaran, B., Flamary, R., Courty, N., Rolet, A.& Blondel, M. Large-scale Optimal Transport and Mapping Estimation. International Conference on Learning Representation (2018) """ n_source = np.shape(M)[0] n_target = np.shape(M)[1] cur_alpha = np.zeros(n_source) cur_beta = np.zeros(n_target) rng = check_random_state(random_state) for cur_iter in range(numItermax): k = np.sqrt(cur_iter + 1) batch_alpha = rng.choice(n_source, batch_size, replace=False) batch_beta = rng.choice(n_target, batch_size, replace=False) update_alpha, update_beta = batch_grad_dual( a, b, M, reg, cur_alpha, cur_beta, batch_size, batch_alpha, batch_beta ) cur_alpha[batch_alpha] += (lr / k) * update_alpha[batch_alpha] cur_beta[batch_beta] += (lr / k) * update_beta[batch_beta] return cur_alpha, cur_beta def solve_dual_entropic(a, b, M, reg, batch_size, numItermax=10000, lr=1, log=False): r""" Compute the transportation matrix to solve the regularized discrete measures optimal transport dual problem The function solves the following optimization problem: .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot\Omega(\gamma) s.t. \ \gamma \mathbf{1} = \mathbf{a} \gamma^T \mathbf{1} = \mathbf{b} \gamma \geq 0 Where : - :math:`\mathbf{M}` is the (`ns`, `nt`) metric cost matrix - :math:`\Omega` is the entropic regularization term with :math:`\Omega(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target weights (sum to 1) Parameters ---------- a : ndarray, shape (ns,) source measure b : ndarray, shape (nt,) target measure M : ndarray, shape (ns, nt) cost matrix reg : float Regularization term > 0 batch_size : int size of the batch numItermax : int number of iteration lr : float learning rate log : bool, optional record log if True Returns ------- pi : ndarray, shape (ns, nt) transportation matrix log : dict log dictionary return only if log==True in parameters References ---------- .. [19] Seguy, V., Bhushan Damodaran, B., Flamary, R., Courty, N., Rolet, A.& Blondel, M. Large-scale Optimal Transport and Mapping Estimation. International Conference on Learning Representation (2018) """ opt_alpha, opt_beta = sgd_entropic_regularization( a, b, M, reg, batch_size, numItermax, lr ) pi = ( np.exp((opt_alpha[:, None] + opt_beta[None, :] - M[:, :]) / reg) * a[:, None] * b[None, :] ) if log: log = {} log["alpha"] = opt_alpha log["beta"] = opt_beta return pi, log else: return pi ################################################################################ # Losses for stochastic optimization ################################################################################ def loss_dual_entropic(u, v, xs, xt, reg=1, ws=None, wt=None, metric="sqeuclidean"): r""" Compute the dual loss of the entropic OT as in equation (6)-(7) of [19] This loss is backend compatible and can be used for stochastic optimization of the dual potentials. It can be used on the full dataset (beware of memory) or on minibatches. Parameters ---------- u : array-like, shape (ns,) Source dual potential v : array-like, shape (nt,) Target dual potential xs : array-like, shape (ns,d) Source samples xt : array-like, shape (ns,d) Target samples reg : float Regularization term > 0 (default=1) ws : array-like, shape (ns,), optional Source sample weights (default unif) wt : array-like, shape (ns,), optional Target sample weights (default unif) metric : string, callable Ground metric for OT (default quadratic). Can be given as a callable function taking (xs,xt) as parameters. Returns ------- dual_loss : array-like Dual loss (to maximize) References ---------- .. [19] Seguy, V., Bhushan Damodaran, B., Flamary, R., Courty, N., Rolet, A.& Blondel, M. Large-scale Optimal Transport and Mapping Estimation. International Conference on Learning Representation (2018) """ nx = get_backend(u, v, xs, xt) if ws is None: ws = nx.ones(xs.shape[0], type_as=xs) / xs.shape[0] if wt is None: wt = nx.ones(xt.shape[0], type_as=xt) / xt.shape[0] if callable(metric): M = metric(xs, xt) else: M = dist(xs, xt, metric=metric) F = -reg * nx.exp((u[:, None] + v[None, :] - M) / reg) return nx.sum(u * ws) + nx.sum(v * wt) + nx.sum(ws[:, None] * F * wt[None, :]) def plan_dual_entropic(u, v, xs, xt, reg=1, ws=None, wt=None, metric="sqeuclidean"): r""" Compute the primal OT plan the entropic OT as in equation (8) of [19] This loss is backend compatible and can be used for stochastic optimization of the dual potentials. It can be used on the full dataset (beware of memory) or on minibatches. Parameters ---------- u : array-like, shape (ns,) Source dual potential v : array-like, shape (nt,) Target dual potential xs : array-like, shape (ns,d) Source samples xt : array-like, shape (ns,d) Target samples reg : float Regularization term > 0 (default=1) ws : array-like, shape (ns,), optional Source sample weights (default unif) wt : array-like, shape (ns,), optional Target sample weights (default unif) metric : string, callable Ground metric for OT (default quadratic). Can be given as a callable function taking (xs,xt) as parameters. Returns ------- G : array-like Primal OT plan References ---------- .. [19] Seguy, V., Bhushan Damodaran, B., Flamary, R., Courty, N., Rolet, A.& Blondel, M. Large-scale Optimal Transport and Mapping Estimation. International Conference on Learning Representation (2018) """ nx = get_backend(u, v, xs, xt) if ws is None: ws = nx.ones(xs.shape[0], type_as=xs) / xs.shape[0] if wt is None: wt = nx.ones(xt.shape[0], type_as=xt) / xt.shape[0] if callable(metric): M = metric(xs, xt) else: M = dist(xs, xt, metric=metric) H = nx.exp((u[:, None] + v[None, :] - M) / reg) return ws[:, None] * H * wt[None, :] def loss_dual_quadratic(u, v, xs, xt, reg=1, ws=None, wt=None, metric="sqeuclidean"): r""" Compute the dual loss of the quadratic regularized OT as in equation (6)-(7) of [19] This loss is backend compatible and can be used for stochastic optimization of the dual potentials. It can be used on the full dataset (beware of memory) or on minibatches. Parameters ---------- u : array-like, shape (ns,) Source dual potential v : array-like, shape (nt,) Target dual potential xs : array-like, shape (ns,d) Source samples xt : array-like, shape (ns,d) Target samples reg : float Regularization term > 0 (default=1) ws : array-like, shape (ns,), optional Source sample weights (default unif) wt : array-like, shape (ns,), optional Target sample weights (default unif) metric : string, callable Ground metric for OT (default quadratic). Can be given as a callable function taking (xs,xt) as parameters. Returns ------- dual_loss : array-like Dual loss (to maximize) References ---------- .. [19] Seguy, V., Bhushan Damodaran, B., Flamary, R., Courty, N., Rolet, A.& Blondel, M. Large-scale Optimal Transport and Mapping Estimation. International Conference on Learning Representation (2018) """ nx = get_backend(u, v, xs, xt) if ws is None: ws = nx.ones(xs.shape[0], type_as=xs) / xs.shape[0] if wt is None: wt = nx.ones(xt.shape[0], type_as=xt) / xt.shape[0] if callable(metric): M = metric(xs, xt) else: M = dist(xs, xt, metric=metric) F = -1.0 / (4 * reg) * nx.maximum(u[:, None] + v[None, :] - M, 0.0) ** 2 return nx.sum(u * ws) + nx.sum(v * wt) + nx.sum(ws[:, None] * F * wt[None, :]) def plan_dual_quadratic(u, v, xs, xt, reg=1, ws=None, wt=None, metric="sqeuclidean"): r""" Compute the primal OT plan the quadratic regularized OT as in equation (8) of [19] This loss is backend compatible and can be used for stochastic optimization of the dual potentials. It can be used on the full dataset (beware of memory) or on minibatches. Parameters ---------- u : array-like, shape (ns,) Source dual potential v : array-like, shape (nt,) Target dual potential xs : array-like, shape (ns,d) Source samples xt : array-like, shape (ns,d) Target samples reg : float Regularization term > 0 (default=1) ws : array-like, shape (ns,), optional Source sample weights (default unif) wt : array-like, shape (ns,), optional Target sample weights (default unif) metric : string, callable Ground metric for OT (default quadratic). Can be given as a callable function taking (xs,xt) as parameters. Returns ------- G : array-like Primal OT plan References ---------- .. [19] Seguy, V., Bhushan Damodaran, B., Flamary, R., Courty, N., Rolet, A.& Blondel, M. Large-scale Optimal Transport and Mapping Estimation. International Conference on Learning Representation (2018) """ nx = get_backend(u, v, xs, xt) if ws is None: ws = nx.ones(xs.shape[0], type_as=xs) / xs.shape[0] if wt is None: wt = nx.ones(xt.shape[0], type_as=xt) / xt.shape[0] if callable(metric): M = metric(xs, xt) else: M = dist(xs, xt, metric=metric) H = 1.0 / (2 * reg) * nx.maximum(u[:, None] + v[None, :] - M, 0.0) return ws[:, None] * H * wt[None, :] python-pot-0.9.7+dfsg/ot/unbalanced/000077500000000000000000000000001525521106200173045ustar00rootroot00000000000000python-pot-0.9.7+dfsg/ot/unbalanced/__init__.py000066400000000000000000000022441525521106200214170ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Solvers related to Unbalanced Optimal Transport problems. """ # Author: Quang Huy Tran # # License: MIT License # All submodules and packages from ._sinkhorn import ( sinkhorn_knopp_unbalanced, sinkhorn_unbalanced, sinkhorn_stabilized_unbalanced, sinkhorn_unbalanced_translation_invariant, sinkhorn_unbalanced2, barycenter_unbalanced_sinkhorn, barycenter_unbalanced_stabilized, barycenter_unbalanced, ) from ._mm import mm_unbalanced, mm_unbalanced2 from ._lbfgs import lbfgsb_unbalanced, lbfgsb_unbalanced2 from ._solver_1d import uot_1d from ._sliced import sliced_unbalanced_ot, unbalanced_sliced_ot __all__ = [ "sinkhorn_knopp_unbalanced", "sinkhorn_unbalanced", "sinkhorn_stabilized_unbalanced", "sinkhorn_unbalanced_translation_invariant", "sinkhorn_unbalanced2", "barycenter_unbalanced_sinkhorn", "barycenter_unbalanced_stabilized", "barycenter_unbalanced", "mm_unbalanced", "mm_unbalanced2", "_get_loss_unbalanced", "lbfgsb_unbalanced", "lbfgsb_unbalanced2", "uot_1d", "sliced_unbalanced_ot", "unbalanced_sliced_ot", ] python-pot-0.9.7+dfsg/ot/unbalanced/_lbfgs.py000066400000000000000000000406451525521106200211230ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Regularized Unbalanced OT solvers """ # Author: Hicham Janati # Laetitia Chapel # Quang Huy Tran # # License: MIT License import numpy as np from scipy.optimize import minimize, Bounds from ..backend import get_backend from ..utils import list_to_array, get_parameter_pair, fun_to_numpy def _get_loss_unbalanced(a, b, c, M, reg, reg_m1, reg_m2, reg_div="kl", regm_div="kl"): r""" Return loss function for the L-BFGS-B solver .. note:: This function will be fed into scipy.optimize, so all input arrays must be Numpy arrays. Parameters ---------- a : array-like (dim_a,) Unnormalized histogram of dimension `dim_a` b : array-like (dim_b,) Unnormalized histogram of dimension `dim_b` M : array-like (dim_a, dim_b) loss matrix reg: float regularization term >=0 c : array-like (dim_a, dim_b), optional (default = None) Reference measure for the regularization. If None, then use :math:`\mathbf{c} = \mathbf{a} \mathbf{b}^T`. reg_m1: float Marginal relaxation term with respect to the first marginal: nonnegative (including 0) but cannot be infinity. reg_m2: float Marginal relaxation term with respect to the second marginal: nonnegative (including 0) but cannot be infinity. reg_div: string, optional Divergence used for regularization. Can take three values: 'entropy' (negative entropy), or 'kl' (Kullback-Leibler) or 'l2' (half-squared) or a tuple of two callable functions returning the reg term and its derivative. Note that the callable functions should be able to handle Numpy arrays and not tensors from the backend regm_div: string, optional Divergence to quantify the difference between the marginals. Can take three values: 'kl' (Kullback-Leibler) or 'l2' (half-squared) or 'tv' (Total Variation) Returns ------- Loss function (scipy.optimize compatible) for regularized unbalanced OT """ m, n = M.shape nx_numpy = get_backend(M, a, b) def reg_l2(G): return np.sum((G - c) ** 2) / 2 def grad_l2(G): return G - c def reg_kl(G): return nx_numpy.kl_div(G, c, mass=True) def grad_kl(G): return np.log(G / c + 1e-16) def reg_entropy(G): return np.sum(G * np.log(G + 1e-16)) - np.sum(G) def grad_entropy(G): return np.log(G + 1e-16) if reg_div == "kl": reg_fun = reg_kl grad_reg_fun = grad_kl elif reg_div == "entropy": reg_fun = reg_entropy grad_reg_fun = grad_entropy elif isinstance(reg_div, tuple): reg_fun = reg_div[0] grad_reg_fun = reg_div[1] else: reg_fun = reg_l2 grad_reg_fun = grad_l2 def marg_l2(G): return reg_m1 * 0.5 * np.sum((G.sum(1) - a) ** 2) + reg_m2 * 0.5 * np.sum( (G.sum(0) - b) ** 2 ) def grad_marg_l2(G): return reg_m1 * np.outer((G.sum(1) - a), np.ones(n)) + reg_m2 * np.outer( np.ones(m), (G.sum(0) - b) ) def marg_kl(G): return reg_m1 * nx_numpy.kl_div( G.sum(1), a, mass=True ) + reg_m2 * nx_numpy.kl_div(G.sum(0), b, mass=True) def grad_marg_kl(G): return reg_m1 * np.outer( np.log(G.sum(1) / a + 1e-16), np.ones(n) ) + reg_m2 * np.outer(np.ones(m), np.log(G.sum(0) / b + 1e-16)) def marg_tv(G): return reg_m1 * np.sum(np.abs(G.sum(1) - a)) + reg_m2 * np.sum( np.abs(G.sum(0) - b) ) def grad_marg_tv(G): return reg_m1 * np.outer(np.sign(G.sum(1) - a), np.ones(n)) + reg_m2 * np.outer( np.ones(m), np.sign(G.sum(0) - b) ) if regm_div == "kl": regm_fun = marg_kl grad_regm_fun = grad_marg_kl elif regm_div == "tv": regm_fun = marg_tv grad_regm_fun = grad_marg_tv else: regm_fun = marg_l2 grad_regm_fun = grad_marg_l2 def _func(G): G = G.reshape((m, n)) # compute loss val = np.sum(G * M) + regm_fun(G) if reg > 0: val = val + reg * reg_fun(G) # compute gradient grad = M + grad_regm_fun(G) if reg > 0: grad = grad + reg * grad_reg_fun(G) return val, grad.ravel() return _func def lbfgsb_unbalanced( a, b, M, reg, reg_m, c=None, reg_div="kl", regm_div="kl", G0=None, numItermax=1000, stopThr=1e-15, method="L-BFGS-B", verbose=False, log=False, ): r""" Solve the unbalanced optimal transport problem and return the OT plan using L-BFGS-B algorithm. The function solves the following optimization problem: .. math:: W = \arg \min_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \mathrm{div}(\gamma, \mathbf{c}) + \mathrm{reg_{m1}} \cdot \mathrm{div_m}(\gamma \mathbf{1}, \mathbf{a}) + \mathrm{reg_{m2}} \cdot \mathrm{div_m}(\gamma^T \mathbf{1}, \mathbf{b}) s.t. \gamma \geq 0 where: - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target unbalanced distributions - :math:`\mathbf{c}` is a reference distribution for the regularization - :math:`\mathrm{div_m}` is a divergence, either Kullback-Leibler divergence, or half-squared :math:`\ell_2` divergence, or Total variation - :math:`\mathrm{div}` is a divergence, either Kullback-Leibler divergence, or half-squared :math:`\ell_2` divergence .. note:: This function is backend-compatible and will work on arrays from all compatible backends. First, it converts all arrays into Numpy arrays, then uses the L-BFGS-B algorithm from scipy.optimize to solve the optimization problem. Parameters ---------- a : array-like (dim_a,) Unnormalized histogram of dimension `dim_a` If `a` is an empty list or array ([]), then `a` is set to uniform distribution. b : array-like (dim_b,) Unnormalized histogram of dimension `dim_b` If `b` is an empty list or array ([]), then `b` is set to uniform distribution. M : array-like (dim_a, dim_b) loss matrix reg: float regularization term >=0 reg_m: float or indexable object of length 1 or 2 Marginal relaxation term: nonnegative (including 0) but cannot be infinity. If :math:`\mathrm{reg_{m}}` is a scalar or an indexable object of length 1, then the same :math:`\mathrm{reg_{m}}` is applied to both marginal relaxations. If :math:`\mathrm{reg_{m}}` is an array, it must be a Numpy array. c : array-like (dim_a, dim_b), optional (default = None) Reference measure for the regularization. If None, then use :math:`\mathbf{c} = \mathbf{a} \mathbf{b}^T`. reg_div: string or pair of callable functions, optional (default = 'kl') Divergence used for regularization. Can take three values: 'entropy' (negative entropy), or 'kl' (Kullback-Leibler) or 'l2' (half-squared) or a tuple of two callable functions returning the reg term and its derivative. Note that the callable functions should be able to handle Numpy arrays and not tensors from the backend, otherwise functions will be converted to Numpy leading to a computational overhead. regm_div: string, optional (default = 'kl') Divergence to quantify the difference between the marginals. Can take three values: 'kl' (Kullback-Leibler) or 'l2' (half-squared) or 'tv' (Total Variation) G0: array-like (dim_a, dim_b), optional (default = None) Initialization of the transport matrix. None corresponds to uniform product. numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (> 0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- gamma : (dim_a, dim_b) array-like Optimal transportation matrix for the given parameters log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> import numpy as np >>> a=[.5, .5] >>> b=[.5, .5] >>> M=[[1., 36.],[9., 4.]] >>> np.round(ot.unbalanced.lbfgsb_unbalanced(a, b, M, reg=0, reg_m=5, reg_div='kl', regm_div='kl'), 2) array([[0.45, 0. ], [0. , 0.34]]) >>> np.round(ot.unbalanced.lbfgsb_unbalanced(a, b, M, reg=0, reg_m=5, reg_div='l2', regm_div='l2'), 2) array([[0.4, 0. ], [0. , 0.1]]) References ---------- .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. See Also -------- ot.lp.emd2 : Unregularized OT loss ot.unbalanced.sinkhorn_unbalanced2 : Entropic regularized OT loss """ # test settings regm_div = regm_div.lower() if regm_div not in ["kl", "l2", "tv"]: raise ValueError( "Unknown regm_div = {}. Must be either 'kl', 'l2' or 'tv'".format(regm_div) ) if isinstance(reg_div, str): reg_div = reg_div.lower() if reg_div not in ["entropy", "kl", "l2"]: raise ValueError( "Unknown reg_div = {}. Must be either 'entropy', 'kl' or 'l2', or a tuple".format( reg_div ) ) # convert all inputs to numpy arrays reg_m1, reg_m2 = get_parameter_pair(reg_m) M, a, b = list_to_array(M, a, b) nx = get_backend(M, a, b, G0) M0 = M dim_a, dim_b = M.shape if len(a) == 0: a = nx.ones(dim_a, type_as=M) / dim_a if len(b) == 0: b = nx.ones(dim_b, type_as=M) / dim_b # convert to numpy if nx.__name__ == "numpy": # remaining parameters which can be arrays reg_m1, reg_m2, reg = nx.to_numpy(reg_m1, reg_m2, reg) else: a, b, M, reg_m1, reg_m2, reg = nx.to_numpy(a, b, M, reg_m1, reg_m2, reg) G0 = a[:, None] * b[None, :] if G0 is None else nx.to_numpy(G0) c = a[:, None] * b[None, :] if c is None else nx.to_numpy(c) # potentially convert the callable function to handle numpy arrays if isinstance(reg_div, tuple): f0, df0 = reg_div f = fun_to_numpy(f0, G0, nx, warn=True) df = fun_to_numpy(df0, G0, nx, warn=True) reg_div = (f, df) _func = _get_loss_unbalanced(a, b, c, M, reg, reg_m1, reg_m2, reg_div, regm_div) res = minimize( _func, G0.ravel(), method=method, jac=True, bounds=Bounds(0, np.inf), tol=stopThr, options=dict(maxiter=numItermax, disp=verbose), ) G = nx.from_numpy(res.x.reshape(M.shape), type_as=M0) if log: log = {"cost": nx.sum(G * M), "res": res} log["total_cost"] = nx.from_numpy(res.fun, type_as=M0) return G, log else: return G def lbfgsb_unbalanced2( a, b, M, reg, reg_m, c=None, reg_div="kl", regm_div="kl", G0=None, returnCost="linear", numItermax=1000, stopThr=1e-15, method="L-BFGS-B", verbose=False, log=False, ): r""" Solve the unbalanced optimal transport problem and return the OT cost using L-BFGS-B. The function solves the following optimization problem: .. math:: \min_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \mathrm{div}(\gamma, \mathbf{c}) + \mathrm{reg_{m1}} \cdot \mathrm{div_m}(\gamma \mathbf{1}, \mathbf{a}) + \mathrm{reg_{m2}} \cdot \mathrm{div_m}(\gamma^T \mathbf{1}, \mathbf{b}) s.t. \gamma \geq 0 where: - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target unbalanced distributions - :math:`\mathbf{c}` is a reference distribution for the regularization - :math:`\mathrm{div_m}` is a divergence, either Kullback-Leibler divergence, or half-squared :math:`\ell_2` divergence, or Total variation - :math:`\mathrm{div}` is a divergence, either Kullback-Leibler divergence, or half-squared :math:`\ell_2` divergence .. note:: This function is backend-compatible and will work on arrays from all compatible backends. First, it converts all arrays into Numpy arrays, then uses the L-BFGS-B algorithm from scipy.optimize to solve the optimization problem. Parameters ---------- a : array-like (dim_a,) Unnormalized histogram of dimension `dim_a` If `a` is an empty list or array ([]), then `a` is set to uniform distribution. b : array-like (dim_b,) Unnormalized histogram of dimension `dim_b` If `b` is an empty list or array ([]), then `b` is set to uniform distribution. M : array-like (dim_a, dim_b) loss matrix reg: float regularization term >=0 reg_m: float or indexable object of length 1 or 2 Marginal relaxation term: nonnegative (including 0) but cannot be infinity. If :math:`\mathrm{reg_{m}}` is a scalar or an indexable object of length 1, then the same :math:`\mathrm{reg_{m}}` is applied to both marginal relaxations. If :math:`\mathrm{reg_{m}}` is an array, it must be a Numpy array. c : array-like (dim_a, dim_b), optional (default = None) Reference measure for the regularization. If None, then use :math:`\mathbf{c} = \mathbf{a} \mathbf{b}^T`. reg_div: string or pair of callable functions, optional (default = 'kl') Divergence used for regularization. Can take three values: 'entropy' (negative entropy), or 'kl' (Kullback-Leibler) or 'l2' (half-squared) or a tuple of two callable functions returning the reg term and its derivative. Note that the callable functions should be able to handle Numpy arrays and not tensors from the backend, otherwise functions will be converted to Numpy leading to a computational overhead. regm_div: string, optional (default = 'kl') Divergence to quantify the difference between the marginals. Can take three values: 'kl' (Kullback-Leibler) or 'l2' (half-squared) or 'tv' (Total Variation) G0: array-like (dim_a, dim_b), optional (default = None) Initialization of the transport matrix. None corresponds to uniform product. returnCost: string, optional (default = "linear") If `returnCost` = "linear", then return the linear part of the unbalanced OT loss. If `returnCost` = "total", then return the total unbalanced OT loss. numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (> 0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- ot_cost : array-like the OT cost between :math:`\mathbf{a}` and :math:`\mathbf{b}` log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> import numpy as np >>> a=[.5, .5] >>> b=[.5, .5] >>> M=[[1., 36.],[9., 4.]] >>> np.round(ot.unbalanced.lbfgsb_unbalanced2(a, b, M, reg=0, reg_m=5, reg_div='kl', regm_div='kl'), 2) 1.79 >>> np.round(ot.unbalanced.lbfgsb_unbalanced2(a, b, M, reg=0, reg_m=5, reg_div='l2', regm_div='l2'), 2) 0.8 References ---------- .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. See Also -------- ot.lp.emd2 : Unregularized OT loss ot.unbalanced.sinkhorn_unbalanced2 : Entropic regularized OT loss """ _, log_lbfgs = lbfgsb_unbalanced( a=a, b=b, M=M, reg=reg, reg_m=reg_m, c=c, reg_div=reg_div, regm_div=regm_div, G0=G0, numItermax=numItermax, stopThr=stopThr, method=method, verbose=verbose, log=True, ) if returnCost == "linear": cost = log_lbfgs["cost"] elif returnCost == "total": cost = log_lbfgs["total_cost"] else: raise ValueError("Unknown returnCost = {}".format(returnCost)) if log: return cost, log_lbfgs else: return cost python-pot-0.9.7+dfsg/ot/unbalanced/_mm.py000066400000000000000000000255521525521106200204370ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Regularized Unbalanced OT solvers """ # Author: Hicham Janati # Laetitia Chapel # Quang Huy Tran # # License: MIT License from ..backend import get_backend from ..utils import list_to_array, get_parameter_pair def mm_unbalanced( a, b, M, reg_m, c=None, reg=0, div="kl", G0=None, numItermax=1000, stopThr=1e-15, verbose=False, log=False, ): r""" Solve the unbalanced optimal transport problem and return the OT plan. The function solves the following optimization problem: .. math:: W = \arg \min_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg_{m1}} \cdot \mathrm{div}(\gamma \mathbf{1}, \mathbf{a}) + \mathrm{reg_{m2}} \cdot \mathrm{div}(\gamma^T \mathbf{1}, \mathbf{b}) + \mathrm{reg} \cdot \mathrm{div}(\gamma, \mathbf{c}) s.t. \gamma \geq 0 where: - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target unbalanced distributions - :math:`\mathbf{c}` is a reference distribution for the regularization - div is a divergence, either Kullback-Leibler or half-squared :math:`\ell_2` divergence The algorithm used for solving the problem is a maximization- minimization algorithm as proposed in :ref:`[41] ` Parameters ---------- a : array-like (dim_a,) Unnormalized histogram of dimension `dim_a` If `a` is an empty list or array ([]), then `a` is set to uniform distribution. b : array-like (dim_b,) Unnormalized histogram of dimension `dim_b` If `b` is an empty list or array ([]), then `b` is set to uniform distribution. M : array-like (dim_a, dim_b) loss matrix reg_m: float or indexable object of length 1 or 2 Marginal relaxation term: nonnegative but cannot be infinity. If :math:`\mathrm{reg_{m}}` is a scalar or an indexable object of length 1, then the same :math:`\mathrm{reg_{m}}` is applied to both marginal relaxations. If :math:`\mathrm{reg_{m}}` is an array, it must have the same backend as input arrays `(a, b, M)`. reg : float, optional (default = 0) Regularization term >= 0. By default, solve the unregularized problem c : array-like (dim_a, dim_b), optional (default = None) Reference measure for the regularization. If None, then use :math:`\mathbf{c} = \mathbf{a} \mathbf{b}^T`. div: string, optional Divergence to quantify the difference between the marginals. Can take two values: 'kl' (Kullback-Leibler) or 'l2' (half-squared) G0: array-like (dim_a, dim_b) Initialization of the transport matrix numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (> 0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- gamma : (dim_a, dim_b) array-like Optimal transportation matrix for the given parameters log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> import numpy as np >>> a=[.5, .5] >>> b=[.5, .5] >>> M=[[1., 36.],[9., 4.]] >>> np.round(ot.unbalanced.mm_unbalanced(a, b, M, 5, div='kl'), 2) array([[0.45, 0. ], [0. , 0.34]]) >>> np.round(ot.unbalanced.mm_unbalanced(a, b, M, 5, div='l2'), 2) array([[0.4, 0. ], [0. , 0.1]]) .. _references-regpath: References ---------- .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. See Also -------- ot.lp.emd : Unregularized OT ot.unbalanced.sinkhorn_unbalanced : Entropic regularized OT """ M, a, b = list_to_array(M, a, b) nx = get_backend(M, a, b) dim_a, dim_b = M.shape if len(a) == 0: a = nx.ones(dim_a, type_as=M) / dim_a if len(b) == 0: b = nx.ones(dim_b, type_as=M) / dim_b G = a[:, None] * b[None, :] if G0 is None else G0 if reg > 0: # regularized case c = a[:, None] * b[None, :] if c is None else c else: # unregularized case c = 0 reg_m1, reg_m2 = get_parameter_pair(reg_m) if log: log = {"err": [], "G": []} div = div.lower() if div == "kl": sum_r = reg + reg_m1 + reg_m2 r1, r2, r = reg_m1 / sum_r, reg_m2 / sum_r, reg / sum_r K = (a[:, None] ** r1) * (b[None, :] ** r2) * (c**r) * nx.exp(-M / sum_r) elif div == "l2": K = (reg_m1 * a[:, None]) + (reg_m2 * b[None, :]) + reg * c - M K = nx.maximum(K, nx.zeros((dim_a, dim_b), type_as=M)) else: raise ValueError("Unknown div = {}. Must be either 'kl' or 'l2'".format(div)) for i in range(numItermax): Gprev = G if div == "kl": Gd = (nx.sum(G, 1, keepdims=True) ** r1) * ( nx.sum(G, 0, keepdims=True) ** r2 ) + 1e-16 G = K * G ** (r1 + r2) / Gd elif div == "l2": Gd = ( reg_m1 * nx.sum(G, 1, keepdims=True) + reg_m2 * nx.sum(G, 0, keepdims=True) + reg * G + 1e-16 ) G = K * G / Gd err = nx.sqrt(nx.sum((G - Gprev) ** 2)) if log: log["err"].append(err) log["G"].append(G) if verbose: print("{:5d}|{:8e}|".format(i, err)) if err < stopThr: break if log: linear_cost = nx.sum(G * M) log["cost"] = linear_cost m1, m2 = nx.sum(G, 1), nx.sum(G, 0) if div == "kl": cost = ( linear_cost + reg_m1 * nx.kl_div(m1, a, mass=True) + reg_m2 * nx.kl_div(m2, b, mass=True) ) if reg > 0: cost = cost + reg * nx.kl_div(G, c, mass=True) else: cost = ( linear_cost + reg_m1 * 0.5 * nx.sum((m1 - a) ** 2) + reg_m2 * 0.5 * nx.sum((m2 - b) ** 2) ) if reg > 0: cost = cost + reg * 0.5 * nx.sum((G - c) ** 2) log["total_cost"] = cost return G, log else: return G def mm_unbalanced2( a, b, M, reg_m, c=None, reg=0, div="kl", G0=None, returnCost="linear", numItermax=1000, stopThr=1e-15, verbose=False, log=False, ): r""" Solve the unbalanced optimal transport problem and return the OT cost. The function solves the following optimization problem: .. math:: \min_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg_{m1}} \cdot \mathrm{div}(\gamma \mathbf{1}, \mathbf{a}) + \mathrm{reg_{m2}} \cdot \mathrm{div}(\gamma^T \mathbf{1}, \mathbf{b}) + \mathrm{reg} \cdot \mathrm{div}(\gamma, \mathbf{c}) s.t. \gamma \geq 0 where: - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target unbalanced distributions - :math:`\mathbf{c}` is a reference distribution for the regularization - :math:`\mathrm{div}` is a divergence, either Kullback-Leibler or half-squared :math:`\ell_2` divergence The algorithm used for solving the problem is a maximization- minimization algorithm as proposed in :ref:`[41] ` Parameters ---------- a : array-like (dim_a,) Unnormalized histogram of dimension `dim_a` If `a` is an empty list or array ([]), then `a` is set to uniform distribution. b : array-like (dim_b,) Unnormalized histogram of dimension `dim_b` If `b` is an empty list or array ([]), then `b` is set to uniform distribution. M : array-like (dim_a, dim_b) loss matrix reg_m: float or indexable object of length 1 or 2 Marginal relaxation term: nonnegative but cannot be infinity. If :math:`\mathrm{reg_{m}}` is a scalar or an indexable object of length 1, then the same :math:`\mathrm{reg_{m}}` is applied to both marginal relaxations. If :math:`\mathrm{reg_{m}}` is an array, it must have the same backend as input arrays `(a, b, M)`. reg : float, optional (default = 0) Entropy regularization term >= 0. By default, solve the unregularized problem c : array-like (dim_a, dim_b), optional (default = None) Reference measure for the regularization. If None, then use :math:`\mathbf{c} = \mathbf{a} \mathbf{b}^T`. div: string, optional Divergence to quantify the difference between the marginals. Can take two values: 'kl' (Kullback-Leibler) or 'l2' (half-squared) G0: array-like (dim_a, dim_b) Initialization of the transport matrix returnCost: string, optional (default = "linear") If `returnCost` = "linear", then return the linear part of the unbalanced OT loss. If `returnCost` = "total", then return the total unbalanced OT loss. numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (> 0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- ot_cost : array-like the OT cost between :math:`\mathbf{a}` and :math:`\mathbf{b}` log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> import numpy as np >>> a=[.5, .5] >>> b=[.5, .5] >>> M=[[1., 36.],[9., 4.]] >>> np.round(ot.unbalanced.mm_unbalanced2(a, b, M, 5, div='l2'), 2) 0.8 >>> np.round(ot.unbalanced.mm_unbalanced2(a, b, M, 5, div='kl'), 2) 1.79 References ---------- .. [41] Chapel, L., Flamary, R., Wu, H., Févotte, C., and Gasso, G. (2021). Unbalanced optimal transport through non-negative penalized linear regression. NeurIPS. See Also -------- ot.lp.emd2 : Unregularized OT loss ot.unbalanced.sinkhorn_unbalanced2 : Entropic regularized OT loss """ _, log_mm = mm_unbalanced( a, b, M, reg_m, c=c, reg=reg, div=div, G0=G0, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=True, ) if returnCost == "linear": cost = log_mm["cost"] elif returnCost == "total": cost = log_mm["total_cost"] else: raise ValueError("Unknown returnCost = {}".format(returnCost)) if log: return cost, log_mm else: return cost python-pot-0.9.7+dfsg/ot/unbalanced/_sinkhorn.py000066400000000000000000001710601525521106200216550ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Regularized Unbalanced OT solvers """ # Author: Hicham Janati # Laetitia Chapel # Quang Huy Tran # # License: MIT License from __future__ import division import warnings from ..backend import get_backend from ..utils import list_to_array, get_parameter_pair def sinkhorn_unbalanced( a, b, M, reg, reg_m, method="sinkhorn", reg_type="kl", c=None, warmstart=None, numItermax=1000, stopThr=1e-6, verbose=False, log=False, **kwargs, ): r""" Solve the unbalanced entropic regularization optimal transport problem and return the OT plan The function solves the following optimization problem: .. math:: W = \arg \min_\gamma \ \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot \mathrm{KL}(\gamma, \mathbf{c}) + \mathrm{reg_{m1}} \cdot \mathrm{KL}(\gamma \mathbf{1}, \mathbf{a}) + \mathrm{reg_{m2}} \cdot \mathrm{KL}(\gamma^T \mathbf{1}, \mathbf{b}) s.t. \gamma \geq 0 where : - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target unbalanced distributions - :math:`\mathbf{c}` is a reference distribution for the regularization - KL is the Kullback-Leibler divergence The algorithm used for solving the problem is the generalized Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[10, 25] ` .. warning:: Starting from version 0.9.5, the default value has been changed to `reg_type='kl'` instead of `reg_type='entropy'`. This makes the function more consistent with the literature and the other solvers. If you want to use the entropy regularization, please set `reg_type='entropy'` explicitly. Parameters ---------- a : array-like, shape (dim_a,) Unnormalized histogram of dimension `dim_a` If `a` is an empty list or array ([]), then `a` is set to uniform distribution. b : array-like, shape (dim_b,) One or multiple unnormalized histograms of dimension `dim_b`. If `b` is an empty list or array ([]), then `b` is set to uniform distribution. If many, compute all the OT costs :math:`(\mathbf{a}, \mathbf{b}_i)_i` M : array-like, shape (dim_a, dim_b) loss matrix reg : float Entropy regularization term > 0 reg_m: float or indexable object of length 1 or 2 Marginal relaxation term. If :math:`\mathrm{reg_{m}}` is a scalar or an indexable object of length 1, then the same :math:`\mathrm{reg_{m}}` is applied to both marginal relaxations. The entropic balanced OT can be recovered using :math:`\mathrm{reg_{m}}=float("inf")`. For semi-relaxed case, use either :math:`\mathrm{reg_{m}}=(float("inf"), scalar)` or :math:`\mathrm{reg_{m}}=(scalar, float("inf"))`. If :math:`\mathrm{reg_{m}}` is an array, it must have the same backend as input arrays `(a, b, M)`. method : str method used for the solver either 'sinkhorn', 'sinkhorn_stabilized', 'sinkhorn_translation_invariant' or 'sinkhorn_reg_scaling', see those function for specific parameters reg_type : string, optional Regularizer term. Can take two values: - Negative entropy: 'entropy': :math:`\Omega(\gamma) = \sum_{i,j} \gamma_{i,j} \log(\gamma_{i,j}) - \sum_{i,j} \gamma_{i,j}`. This is equivalent (up to a constant) to :math:`\Omega(\gamma) = \text{KL}(\gamma, 1_{dim_a} 1_{dim_b}^T)`. - Kullback-Leibler divergence (default): 'kl': :math:`\Omega(\gamma) = \text{KL}(\gamma, \mathbf{a} \mathbf{b}^T)`. c : array-like, shape (dim_a, dim_b), optional (default=None) Reference measure for the regularization. If None, then use :math:`\mathbf{c} = \mathbf{a} \mathbf{b}^T`. If :math:`\texttt{reg_type}=`'entropy', then :math:`\mathbf{c} = 1_{dim_a} 1_{dim_b}^T`. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the `u`, `v` sinkhorn scaling vectors). numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record `log` if `True` Returns ------- if n_hists == 1: - gamma : array-like, shape(dim_a, dim_b) Optimal transportation matrix for the given parameters - log : dict log dictionary returned only if `log` is `True` else: - ot_distance : array-like, shape (n_hists,) the OT distance between :math:`\mathbf{a}` and each of the histograms :math:`\mathbf{b}_i` - log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> import numpy as np >>> a=[.5, .5] >>> b=[.5, .5] >>> M=[[0., 1.], [1., 0.]] >>> np.round(ot.sinkhorn_unbalanced(a, b, M, 1, 1), 7) array([[0.3220536, 0.1184769], [0.1184769, 0.3220536]]) .. _references-sinkhorn-unbalanced: References ---------- .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [9] Schmitzer, B. (2016). Stabilized Sparse Scaling Algorithms for Entropy Regularized Transport Problems. arXiv preprint arXiv:1610.06519. .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. .. [25] Frogner C., Zhang C., Mobahi H., Araya-Polo M., Poggio T. : Learning with a Wasserstein Loss, Advances in Neural Information Processing Systems (NIPS) 2015 .. [73] Séjourné, T., Vialard, F. X., & Peyré, G. (2022). Faster unbalanced optimal transport: Translation invariant sinkhorn and 1-d frank-wolfe. In International Conference on Artificial Intelligence and Statistics (pp. 4995-5021). PMLR. See Also -------- ot.unbalanced.sinkhorn_knopp_unbalanced: Unbalanced Classic Sinkhorn :ref:`[10] ` ot.unbalanced.sinkhorn_stabilized_unbalanced: Unbalanced Stabilized sinkhorn :ref:`[9, 10] ` ot.unbalanced.sinkhorn_reg_scaling_unbalanced: Unbalanced Sinkhorn with epsilon scaling :ref:`[9, 10] ` ot.unbalanced.sinkhorn_unbalanced_translation_invariant: Translation Invariant Unbalanced Sinkhorn :ref:`[73] ` """ if method.lower() == "sinkhorn": return sinkhorn_knopp_unbalanced( a, b, M, reg, reg_m, reg_type, c, warmstart, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, **kwargs, ) elif method.lower() == "sinkhorn_stabilized": return sinkhorn_stabilized_unbalanced( a, b, M, reg, reg_m, reg_type, c, warmstart, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, **kwargs, ) elif method.lower() == "sinkhorn_translation_invariant": return sinkhorn_unbalanced_translation_invariant( a, b, M, reg, reg_m, reg_type, c, warmstart, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, **kwargs, ) elif method.lower() in ["sinkhorn_reg_scaling"]: warnings.warn("Method not implemented yet. Using classic Sinkhorn-Knopp") return sinkhorn_knopp_unbalanced( a, b, M, reg, reg_m, reg_type, c, warmstart, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, **kwargs, ) else: raise ValueError("Unknown method '%s'." % method) def sinkhorn_unbalanced2( a, b, M, reg, reg_m, method="sinkhorn", reg_type="kl", c=None, warmstart=None, returnCost="linear", numItermax=1000, stopThr=1e-6, verbose=False, log=False, **kwargs, ): r""" Solve the entropic regularization unbalanced optimal transport problem and return the cost The function solves the following optimization problem: .. math:: \min_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot \mathrm{KL}(\gamma, \mathbf{c}) + \mathrm{reg_{m1}} \cdot \mathrm{KL}(\gamma \mathbf{1}, \mathbf{a}) + \mathrm{reg_{m2}} \cdot \mathrm{KL}(\gamma^T \mathbf{1}, \mathbf{b}) s.t. \gamma\geq 0 where : - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target unbalanced distributions - :math:`\mathbf{c}` is a reference distribution for the regularization - KL is the Kullback-Leibler divergence The algorithm used for solving the problem is the generalized Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[10, 25] ` .. warning:: Starting from version 0.9.5, the default value has been changed to `reg_type='kl'` instead of `reg_type='entropy'`. This makes the function more consistent with the literature and the other solvers. If you want to use the entropy regularization, please set `reg_type='entropy'` explicitly. Parameters ---------- a : array-like, shape (dim_a,) Unnormalized histogram of dimension `dim_a` If `a` is an empty list or array ([]), then `a` is set to uniform distribution. b : array-like, shape (dim_b,) One or multiple unnormalized histograms of dimension `dim_b`. If `b` is an empty list or array ([]), then `b` is set to uniform distribution. If many, compute all the OT costs :math:`(\mathbf{a}, \mathbf{b}_i)_i` M : array-like, shape (dim_a, dim_b) loss matrix reg : float Entropy regularization term > 0 reg_m: float or indexable object of length 1 or 2 Marginal relaxation term. If :math:`\mathrm{reg_{m}}` is a scalar or an indexable object of length 1, then the same :math:`\mathrm{reg_{m}}` is applied to both marginal relaxations. The entropic balanced OT can be recovered using :math:`\mathrm{reg_{m}}=float("inf")`. For semi-relaxed case, use either :math:`\mathrm{reg_{m}}=(float("inf"), scalar)` or :math:`\mathrm{reg_{m}}=(scalar, float("inf"))`. If :math:`\mathrm{reg_{m}}` is an array, it must have the same backend as input arrays `(a, b, M)`. method : str method used for the solver either 'sinkhorn', 'sinkhorn_stabilized', 'sinkhorn_translation_invariant' or 'sinkhorn_reg_scaling', see those function for specific parameters reg_type : string, optional Regularizer term. Can take two values: - Negative entropy: 'entropy': :math:`\Omega(\gamma) = \sum_{i,j} \gamma_{i,j} \log(\gamma_{i,j}) - \sum_{i,j} \gamma_{i,j}`. This is equivalent (up to a constant) to :math:`\Omega(\gamma) = \text{KL}(\gamma, 1_{dim_a} 1_{dim_b}^T)`. - Kullback-Leibler divergence: 'kl': :math:`\Omega(\gamma) = \text{KL}(\gamma, \mathbf{a} \mathbf{b}^T)`. c : array-like, shape (dim_a, dim_b), optional (default=None) Reference measure for the regularization. If None, then use :math:`\mathbf{c} = \mathbf{a} \mathbf{b}^T`. If :math:`\texttt{reg_type}=`'entropy', then :math:`\mathbf{c} = 1_{dim_a} 1_{dim_b}^T`. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the u,v sinkhorn scaling vectors). returnCost: string, optional (default = "linear") If `returnCost` = "linear", then return the linear part of the unbalanced OT loss. If `returnCost` = "total", then return the total unbalanced OT loss. numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record `log` if `True` Returns ------- ot_cost : array-like, shape (n_hists,) the OT cost between :math:`\mathbf{a}` and each of the histograms :math:`\mathbf{b}_i` log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> import numpy as np >>> a=[.5, .10] >>> b=[.5, .5] >>> M=[[0., 1.],[1., 0.]] >>> np.round(ot.unbalanced.sinkhorn_unbalanced2(a, b, M, 1., 1.), 8) 0.19600125 .. _references-sinkhorn-unbalanced2: References ---------- .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal Transport, Advances in Neural Information Processing Systems (NIPS) 26, 2013 .. [9] Schmitzer, B. (2016). Stabilized Sparse Scaling Algorithms for Entropy Regularized Transport Problems. arXiv preprint arXiv:1610.06519. .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. .. [25] Frogner C., Zhang C., Mobahi H., Araya-Polo M., Poggio T. : Learning with a Wasserstein Loss, Advances in Neural Information Processing Systems (NIPS) 2015 .. [73] Séjourné, T., Vialard, F. X., & Peyré, G. (2022). Faster unbalanced optimal transport: Translation invariant sinkhorn and 1-d frank-wolfe. In International Conference on Artificial Intelligence and Statistics (pp. 4995-5021). PMLR. See Also -------- ot.unbalanced.sinkhorn_knopp: Unbalanced Classic Sinkhorn :ref:`[10] ` ot.unbalanced.sinkhorn_stabilized: Unbalanced Stabilized sinkhorn :ref:`[9, 10] ` ot.unbalanced.sinkhorn_reg_scaling: Unbalanced Sinkhorn with epsilon scaling :ref:`[9, 10] ` ot.unbalanced.sinkhorn_unbalanced_translation_invariant: Translation Invariant Unbalanced Sinkhorn :ref:`[73] ` """ M, a, b = list_to_array(M, a, b) if len(b.shape) < 2: if method.lower() == "sinkhorn": res = sinkhorn_knopp_unbalanced( a, b, M, reg, reg_m, reg_type, c, warmstart, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=True, **kwargs, ) elif method.lower() == "sinkhorn_stabilized": res = sinkhorn_stabilized_unbalanced( a, b, M, reg, reg_m, reg_type, c, warmstart, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=True, **kwargs, ) elif method.lower() == "sinkhorn_translation_invariant": res = sinkhorn_unbalanced_translation_invariant( a, b, M, reg, reg_m, reg_type, c, warmstart, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=True, **kwargs, ) elif method.lower() in ["sinkhorn_reg_scaling"]: warnings.warn("Method not implemented yet. Using classic Sinkhorn-Knopp") res = sinkhorn_knopp_unbalanced( a, b, M, reg, reg_m, reg_type, c, warmstart, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=True, **kwargs, ) else: raise ValueError("Unknown method %s." % method) if returnCost == "linear": cost = res[1]["cost"] elif returnCost == "total": cost = res[1]["total_cost"] else: raise ValueError("Unknown returnCost = {}".format(returnCost)) if log: return cost, res[1] else: return cost else: if reg_type == "kl": warnings.warn("Reg_type not implemented yet. Use entropy.") if method.lower() == "sinkhorn": return sinkhorn_knopp_unbalanced( a, b, M, reg, reg_m, reg_type, c, warmstart, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, **kwargs, ) elif method.lower() == "sinkhorn_stabilized": return sinkhorn_stabilized_unbalanced( a, b, M, reg, reg_m, reg_type, c, warmstart, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, **kwargs, ) elif method.lower() == "sinkhorn_translation_invariant": return sinkhorn_unbalanced_translation_invariant( a, b, M, reg, reg_m, reg_type, c, warmstart, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, **kwargs, ) elif method.lower() in ["sinkhorn_reg_scaling"]: warnings.warn("Method not implemented yet. Using classic Sinkhorn-Knopp") return sinkhorn_knopp_unbalanced( a, b, M, reg, reg_m, reg_type, c, warmstart, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, **kwargs, ) else: raise ValueError("Unknown method %s." % method) def sinkhorn_knopp_unbalanced( a, b, M, reg, reg_m, reg_type="kl", c=None, warmstart=None, numItermax=1000, stopThr=1e-6, verbose=False, log=False, **kwargs, ): r""" Solve the entropic regularization unbalanced optimal transport problem and return the OT plan The function solves the following optimization problem: .. math:: W = \arg \min_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot \mathrm{KL}(\gamma, \mathbf{c}) + \mathrm{reg_{m1}} \cdot \mathrm{KL}(\gamma \mathbf{1}, \mathbf{a}) + \mathrm{reg_{m2}} \cdot \mathrm{KL}(\gamma^T \mathbf{1}, \mathbf{b}) s.t. \gamma \geq 0 where : - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target unbalanced distributions - :math:`\mathbf{c}` is a reference distribution for the regularization - KL is the Kullback-Leibler divergence The algorithm used for solving the problem is the generalized Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[10, 25] ` .. warning:: Starting from version 0.9.5, the default value has been changed to `reg_type='kl'` instead of `reg_type='entropy'`. This makes the function more consistent with the literature and the other solvers. If you want to use the entropy regularization, please set `reg_type='entropy'` explicitly. Parameters ---------- a : array-like, shape (dim_a,) Unnormalized histogram of dimension `dim_a` If `a` is an empty list or array ([]), then `a` is set to uniform distribution. b : array-like, shape (dim_b,) One or multiple unnormalized histograms of dimension `dim_b`. If `b` is an empty list or array ([]), then `b` is set to uniform distribution. If many, compute all the OT costs :math:`(\mathbf{a}, \mathbf{b}_i)_i` M : array-like, shape (dim_a, dim_b) loss matrix reg : float Entropy regularization term > 0 reg_m: float or indexable object of length 1 or 2 Marginal relaxation term. If :math:`\mathrm{reg_{m}}` is a scalar or an indexable object of length 1, then the same :math:`\mathrm{reg_{m}}` is applied to both marginal relaxations. The entropic balanced OT can be recovered using :math:`\mathrm{reg_{m}}=float("inf")`. For semi-relaxed case, use either :math:`\mathrm{reg_{m}}=(float("inf"), scalar)` or :math:`\mathrm{reg_{m}}=(scalar, float("inf"))`. If :math:`\mathrm{reg_{m}}` is an array, it must have the same backend as input arrays `(a, b, M)`. reg_type : string, optional Regularizer term. Can take two values: - Negative entropy: 'entropy': :math:`\Omega(\gamma) = \sum_{i,j} \gamma_{i,j} \log(\gamma_{i,j}) - \sum_{i,j} \gamma_{i,j}`. This is equivalent (up to a constant) to :math:`\Omega(\gamma) = \text{KL}(\gamma, 1_{dim_a} 1_{dim_b}^T)`. - Kullback-Leibler divergence: 'kl': :math:`\Omega(\gamma) = \text{KL}(\gamma, \mathbf{a} \mathbf{b}^T)`. c : array-like, shape (dim_a, dim_b), optional (default=None) Reference measure for the regularization. If None, then use :math:`\mathbf{c} = \mathbf{a} \mathbf{b}^T`. If :math:`\texttt{reg_type}=`'entropy', then :math:`\mathbf{c} = 1_{dim_a} 1_{dim_b}^T`. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the `u`, `v` sinkhorn scaling vectors). numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (> 0) verbose : bool, optional Print information along iterations log : bool, optional record `log` if `True` Returns ------- if n_hists == 1: - gamma : array-like, shape (dim_a, dim_b) Optimal transportation matrix for the given parameters - log : dict log dictionary returned only if `log` is `True` else: - ot_cost : array-like, shape (n_hists,) the OT cost between :math:`\mathbf{a}` and each of the histograms :math:`\mathbf{b}_i` - log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> import numpy as np >>> a=[.5, .5] >>> b=[.5, .5] >>> M=[[0., 1.],[1., 0.]] >>> np.round(ot.unbalanced.sinkhorn_knopp_unbalanced(a, b, M, 1., 1.), 7) array([[0.3220536, 0.1184769], [0.1184769, 0.3220536]]) .. _references-sinkhorn-knopp-unbalanced: References ---------- .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. .. [25] Frogner C., Zhang C., Mobahi H., Araya-Polo M., Poggio T. : Learning with a Wasserstein Loss, Advances in Neural Information Processing Systems (NIPS) 2015 See Also -------- ot.lp.emd : Unregularized OT ot.optim.cg : General regularized OT """ M, a, b = list_to_array(M, a, b) nx = get_backend(M, a, b) dim_a, dim_b = M.shape if len(a) == 0: a = nx.ones(dim_a, type_as=M) / dim_a if len(b) == 0: b = nx.ones(dim_b, type_as=M) / dim_b if len(b.shape) > 1: n_hists = b.shape[1] else: n_hists = 0 reg_m1, reg_m2 = get_parameter_pair(reg_m) if log: dict_log = {"err": []} # we assume that no distances are null except those of the diagonal of # distances if warmstart is None: if n_hists: u = nx.ones((dim_a, 1), type_as=M) v = nx.ones((dim_b, n_hists), type_as=M) a = a.reshape(dim_a, 1) else: u = nx.ones(dim_a, type_as=M) v = nx.ones(dim_b, type_as=M) else: u, v = nx.exp(warmstart[0]), nx.exp(warmstart[1]) if reg_type.lower() == "entropy": warnings.warn( "If reg_type = entropy, then the matrix c is overwritten by the one matrix." ) c = nx.ones((dim_a, dim_b), type_as=M) if n_hists: K = nx.exp(-M / reg) else: c = a[:, None] * b[None, :] if c is None else c K = nx.exp(-M / reg) * c fi_1 = reg_m1 / (reg_m1 + reg) if reg_m1 != float("inf") else 1 fi_2 = reg_m2 / (reg_m2 + reg) if reg_m2 != float("inf") else 1 err = 1.0 for i in range(numItermax): uprev = u vprev = v Kv = nx.dot(K, v) u = (a / Kv) ** fi_1 Ktu = nx.dot(K.T, u) v = (b / Ktu) ** fi_2 if ( nx.any(Ktu == 0.0) or nx.any(nx.isnan(u)) or nx.any(nx.isnan(v)) or nx.any(nx.isinf(u)) or nx.any(nx.isinf(v)) ): # we have reached the machine precision # come back to previous solution and quit loop warnings.warn("Numerical errors at iteration %s" % i) u = uprev v = vprev break err_u = nx.max(nx.abs(u - uprev)) / max( nx.max(nx.abs(u)), nx.max(nx.abs(uprev)), 1.0 ) err_v = nx.max(nx.abs(v - vprev)) / max( nx.max(nx.abs(v)), nx.max(nx.abs(vprev)), 1.0 ) err = 0.5 * (err_u + err_v) if log: dict_log["err"].append(err) if verbose: if i % 50 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(i, err)) if err < stopThr: break if log: dict_log["logu"] = nx.log(u + 1e-300) dict_log["logv"] = nx.log(v + 1e-300) if n_hists: # return only loss res = nx.einsum("ik,ij,jk,ij->k", u, K, v, M) if log: return res, dict_log else: return res else: # return OT matrix plan = u[:, None] * K * v[None, :] if log: linear_cost = nx.sum(plan * M) dict_log["cost"] = linear_cost total_cost = linear_cost + reg * nx.kl_div(plan, c) if reg_m1 != float("inf"): total_cost = total_cost + reg_m1 * nx.kl_div(nx.sum(plan, 1), a) if reg_m2 != float("inf"): total_cost = total_cost + reg_m2 * nx.kl_div(nx.sum(plan, 0), b) dict_log["total_cost"] = total_cost return plan, dict_log else: return plan def sinkhorn_stabilized_unbalanced( a, b, M, reg, reg_m, reg_type="kl", c=None, warmstart=None, tau=1e5, numItermax=1000, stopThr=1e-6, verbose=False, log=False, **kwargs, ): r""" Solve the entropic regularization unbalanced optimal transport problem and return the loss The function solves the following optimization problem using log-domain stabilization as proposed in :ref:`[10] `: .. math:: W = \arg \min_\gamma \quad \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot \mathrm{KL}(\gamma, \mathbf{c}) + \mathrm{reg_{m1}} \cdot \mathrm{KL}(\gamma \mathbf{1}, \mathbf{a}) + \mathrm{reg_{m2}} \cdot \mathrm{KL}(\gamma^T \mathbf{1}, \mathbf{b}) s.t. \gamma \geq 0 where : - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target unbalanced distributions - :math:`\mathbf{c}` is a reference distribution for the regularization - KL is the Kullback-Leibler divergence The algorithm used for solving the problem is the generalized Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[10, 25] ` Parameters ---------- a : array-like, shape (dim_a,) Unnormalized histogram of dimension `dim_a` If `a` is an empty list or array ([]), then `a` is set to uniform distribution. b : array-like, shape (dim_b,) One or multiple unnormalized histograms of dimension `dim_b`. If `b` is an empty list or array ([]), then `b` is set to uniform distribution. If many, compute all the OT costs :math:`(\mathbf{a}, \mathbf{b}_i)_i` M : array-like, shape (dim_a, dim_b) loss matrix reg : float Entropy regularization term > 0 reg_m: float or indexable object of length 1 or 2 Marginal relaxation term. If :math:`\mathrm{reg_{m}}` is a scalar or an indexable object of length 1, then the same :math:`\mathrm{reg_{m}}` is applied to both marginal relaxations. The entropic balanced OT can be recovered using :math:`\mathrm{reg_{m}}=float("inf")`. For semi-relaxed case, use either :math:`\mathrm{reg_{m}}=(float("inf"), scalar)` or :math:`\mathrm{reg_{m}}=(scalar, float("inf"))`. If :math:`\mathrm{reg_{m}}` is an array, it must have the same backend as input arrays `(a, b, M)`. method : str method used for the solver either 'sinkhorn', 'sinkhorn_stabilized' or 'sinkhorn_reg_scaling', see those function for specific parameters reg_type : string, optional Regularizer term. Can take two values: - Negative entropy: 'entropy': :math:`\Omega(\gamma) = \sum_{i,j} \gamma_{i,j} \log(\gamma_{i,j}) - \sum_{i,j} \gamma_{i,j}`. This is equivalent (up to a constant) to :math:`\Omega(\gamma) = \text{KL}(\gamma, 1_{dim_a} 1_{dim_b}^T)`. - Kullback-Leibler divergence: 'kl': :math:`\Omega(\gamma) = \text{KL}(\gamma, \mathbf{a} \mathbf{b}^T)`. c : array-like, shape (dim_a, dim_b), optional (default=None) Reference measure for the regularization. If None, then use :math:`\mathbf{c} = \mathbf{a} \mathbf{b}^T`. If :math:`\texttt{reg_type}=`'entropy', then :math:`\mathbf{c} = 1_{dim_a} 1_{dim_b}^T`. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the `u`, `v` sinkhorn scaling vectors). tau : float threshold for max value in `u` or `v` for log scaling numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record `log` if `True` .. warning:: Starting from version 0.9.5, the default value has been changed to `reg_type='kl'` instead of `reg_type='entropy'`. This makes the function more consistent with the literature and the other solvers. If you want to use the entropy regularization, please set `reg_type='entropy'` explicitly. Returns ------- if n_hists == 1: - gamma : array-like, shape (dim_a, dim_b) Optimal transportation matrix for the given parameters - log : dict log dictionary returned only if `log` is `True` else: - ot_cost : array-like, shape (n_hists,) the OT cost between :math:`\mathbf{a}` and each of the histograms :math:`\mathbf{b}_i` - log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> import numpy as np >>> a=[.5, .5] >>> b=[.5, .5] >>> M=[[0., 1.],[1., 0.]] >>> np.round(ot.unbalanced.sinkhorn_stabilized_unbalanced(a, b, M, 1., 1.), 7) array([[0.3220536, 0.1184769], [0.1184769, 0.3220536]]) .. _references-sinkhorn-stabilized-unbalanced: References ---------- .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. .. [25] Frogner C., Zhang C., Mobahi H., Araya-Polo M., Poggio T. : Learning with a Wasserstein Loss, Advances in Neural Information Processing Systems (NIPS) 2015 See Also -------- ot.lp.emd : Unregularized OT ot.optim.cg : General regularized OT """ a, b, M = list_to_array(a, b, M) nx = get_backend(M, a, b) dim_a, dim_b = M.shape if len(a) == 0: a = nx.ones(dim_a, type_as=M) / dim_a if len(b) == 0: b = nx.ones(dim_b, type_as=M) / dim_b if len(b.shape) > 1: n_hists = b.shape[1] else: n_hists = 0 reg_m1, reg_m2 = get_parameter_pair(reg_m) if log: dict_log = {"err": []} # we assume that no distances are null except those of the diagonal of # distances if warmstart is None: if n_hists: u = nx.ones((dim_a, n_hists), type_as=M) v = nx.ones((dim_b, n_hists), type_as=M) a = a.reshape(dim_a, 1) else: u = nx.ones(dim_a, type_as=M) v = nx.ones(dim_b, type_as=M) else: u, v = nx.exp(warmstart[0]), nx.exp(warmstart[1]) if reg_type == "entropy": warnings.warn( "If reg_type = entropy, then the matrix c is overwritten by the one matrix." ) c = nx.ones((dim_a, dim_b), type_as=M) if n_hists: M0 = M else: c = a[:, None] * b[None, :] if c is None else c M0 = M - reg * nx.log(c) K = nx.exp(-M0 / reg) fi_1 = reg_m1 / (reg_m1 + reg) if reg_m1 != float("inf") else 1 fi_2 = reg_m2 / (reg_m2 + reg) if reg_m2 != float("inf") else 1 cpt = 0 err = 1.0 alpha = nx.zeros(dim_a, type_as=M) beta = nx.zeros(dim_b, type_as=M) ones_a = nx.ones(dim_a, type_as=M) ones_b = nx.ones(dim_b, type_as=M) while err > stopThr and cpt < numItermax: uprev = u vprev = v Kv = nx.dot(K, v) f_alpha = nx.exp(-alpha / (reg + reg_m1)) if reg_m1 != float("inf") else ones_a f_beta = nx.exp(-beta / (reg + reg_m2)) if reg_m2 != float("inf") else ones_b if n_hists: f_alpha = f_alpha[:, None] f_beta = f_beta[:, None] u = ((a / (Kv + 1e-16)) ** fi_1) * f_alpha Ktu = nx.dot(K.T, u) v = ((b / (Ktu + 1e-16)) ** fi_2) * f_beta absorbing = False if nx.any(u > tau) or nx.any(v > tau): absorbing = True if n_hists: alpha = alpha + reg * nx.log(nx.max(u, 1)) beta = beta + reg * nx.log(nx.max(v, 1)) else: alpha = alpha + reg * nx.log(nx.max(u)) beta = beta + reg * nx.log(nx.max(v)) K = nx.exp((alpha[:, None] + beta[None, :] - M0) / reg) v = nx.ones(v.shape, type_as=v) Kv = nx.dot(K, v) if ( nx.any(Ktu == 0.0) or nx.any(nx.isnan(u)) or nx.any(nx.isnan(v)) or nx.any(nx.isinf(u)) or nx.any(nx.isinf(v)) ): # we have reached the machine precision # come back to previous solution and quit loop warnings.warn("Numerical errors at iteration %s" % cpt) u = uprev v = vprev break if (cpt % 10 == 0 and not absorbing) or cpt == 0: # we can speed up the process by checking for the error only all # the 10th iterations err = nx.max(nx.abs(u - uprev)) / max( nx.max(nx.abs(u)), nx.max(nx.abs(uprev)), 1.0 ) if log: dict_log["err"].append(err) if verbose: if cpt % 200 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(cpt, err)) cpt = cpt + 1 if err > stopThr: warnings.warn( "Stabilized Unbalanced Sinkhorn did not converge." + "Try a larger entropy `reg` or a lower mass `reg_m`." + "Or a larger absorption threshold `tau`." ) if n_hists: logu = alpha[:, None] / reg + nx.log(u) logv = beta[:, None] / reg + nx.log(v) else: logu = alpha / reg + nx.log(u) logv = beta / reg + nx.log(v) if log: dict_log["logu"] = logu dict_log["logv"] = logv if n_hists: # return only loss res = nx.logsumexp( nx.log(M + 1e-100)[:, :, None] + logu[:, None, :] + logv[None, :, :] - M0[:, :, None] / reg, axis=(0, 1), ) res = nx.exp(res) if log: return res, dict_log else: return res else: # return OT matrix plan = nx.exp(logu[:, None] + logv[None, :] - M0 / reg) if log: linear_cost = nx.sum(plan * M) dict_log["cost"] = linear_cost total_cost = linear_cost + reg * nx.kl_div(plan, c) if reg_m1 != float("inf"): total_cost = total_cost + reg_m1 * nx.kl_div(nx.sum(plan, 1), a) if reg_m2 != float("inf"): total_cost = total_cost + reg_m2 * nx.kl_div(nx.sum(plan, 0), b) dict_log["total_cost"] = total_cost return plan, dict_log else: return plan def sinkhorn_unbalanced_translation_invariant( a, b, M, reg, reg_m, reg_type="kl", c=None, warmstart=None, numItermax=1000, stopThr=1e-6, verbose=False, log=False, **kwargs, ): r""" Solve the entropic regularization unbalanced optimal transport problem and return the OT plan The function solves the following optimization problem: .. math:: W = \arg \min_\gamma \ \langle \gamma, \mathbf{M} \rangle_F + \mathrm{reg} \cdot \mathrm{KL}(\gamma, \mathbf{c}) + \mathrm{reg_{m1}} \cdot \mathrm{KL}(\gamma \mathbf{1}, \mathbf{a}) + \mathrm{reg_{m2}} \cdot \mathrm{KL}(\gamma^T \mathbf{1}, \mathbf{b}) s.t. \gamma \geq 0 where : - :math:`\mathbf{M}` is the (`dim_a`, `dim_b`) metric cost matrix - :math:`\Omega` is the entropic regularization term,KL divergence - :math:`\mathbf{a}` and :math:`\mathbf{b}` are source and target unbalanced distributions - KL is the Kullback-Leibler divergence The algorithm used for solving the problem is the translation invariant Sinkhorn algorithm as proposed in :ref:`[73] ` Parameters ---------- a : array-like, shape (dim_a,) Unnormalized histogram of dimension `dim_a` b : array-like, shape (dim_b,) or (dim_b, n_hists) One or multiple unnormalized histograms of dimension `dim_b` If many, compute all the OT distances (a, b_i) M : array-like, shape (dim_a, dim_b) loss matrix reg : float Entropy regularization term > 0 reg_m: float or indexable object of length 1 or 2 Marginal relaxation term. If reg_m is a scalar or an indexable object of length 1, then the same reg_m is applied to both marginal relaxations. The entropic balanced OT can be recovered using `reg_m=float("inf")`. For semi-relaxed case, use either `reg_m=(float("inf"), scalar)` or `reg_m=(scalar, float("inf"))`. If reg_m is an array, it must have the same backend as input arrays (a, b, M). reg_type : string, optional Regularizer term. Can take two values: 'entropy' (negative entropy) :math:`\Omega(\gamma) = \sum_{i,j} \gamma_{i,j} \log(\gamma_{i,j}) - \sum_{i,j} \gamma_{i,j}`, or 'kl' (Kullback-Leibler) :math:`\Omega(\gamma) = \text{KL}(\gamma, \mathbf{a} \mathbf{b}^T)`. c : array-like, shape (dim_a, dim_b), optional (default=None) Reference measure for the regularization. If None, then use :math:`\mathbf{c} = \mathbf{a} \mathbf{b}^T`. If :math:`\texttt{reg_type}=`'entropy', then :math:`\mathbf{c} = 1_{dim_a} 1_{dim_b}^T`. warmstart: tuple of arrays, shape (dim_a, dim_b), optional Initialization of dual potentials. If provided, the dual potentials should be given (that is the logarithm of the u,v sinkhorn scaling vectors). numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (> 0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- if n_hists == 1: - gamma : array-like, shape (dim_a, dim_b) Optimal transportation matrix for the given parameters - log : dict log dictionary returned only if `log` is `True` else: - ot_distance : array-like, shape (n_hists,) the OT distance between :math:`\mathbf{a}` and each of the histograms :math:`\mathbf{b}_i` - log : dict log dictionary returned only if `log` is `True` Examples -------- >>> import ot >>> a=[.5, .5] >>> b=[.5, .5] >>> M=[[0., 1.],[1., 0.]] >>> ot.unbalanced.sinkhorn_unbalanced_translation_invariant(a, b, M, 1., 1.) array([[0.32205357, 0.11847689], [0.11847689, 0.32205357]]) .. _references-sinkhorn-unbalanced-translation-invariant: References ---------- .. [73] Séjourné, T., Vialard, F. X., & Peyré, G. (2022). Faster unbalanced optimal transport: Translation invariant sinkhorn and 1-d frank-wolfe. In International Conference on Artificial Intelligence and Statistics (pp. 4995-5021). PMLR. """ M, a, b = list_to_array(M, a, b) nx = get_backend(M, a, b) dim_a, dim_b = M.shape if len(a) == 0: a = nx.ones(dim_a, type_as=M) / dim_a if len(b) == 0: b = nx.ones(dim_b, type_as=M) / dim_b if len(b.shape) > 1: n_hists = b.shape[1] else: n_hists = 0 reg_m1, reg_m2 = get_parameter_pair(reg_m) if log: dict_log = {"err": []} # we assume that no distances are null except those of the diagonal of # distances if warmstart is None: if n_hists: u = nx.ones((dim_a, 1), type_as=M) v = nx.ones((dim_b, n_hists), type_as=M) a = a.reshape(dim_a, 1) else: u = nx.ones(dim_a, type_as=M) v = nx.ones(dim_b, type_as=M) else: u, v = nx.exp(warmstart[0]), nx.exp(warmstart[1]) u_, v_ = u, v if reg_type == "entropy": warnings.warn( "If reg_type = entropy, then the matrix c is overwritten by the one matrix." ) c = nx.ones((dim_a, dim_b), type_as=M) if n_hists: M0 = M else: c = a[:, None] * b[None, :] if c is None else c M0 = M - reg * nx.log(c) K = nx.exp(-M0 / reg) fi_1 = reg_m1 / (reg_m1 + reg) if reg_m1 != float("inf") else 1 fi_2 = reg_m2 / (reg_m2 + reg) if reg_m2 != float("inf") else 1 k1 = ( reg * reg_m1 / ((reg + reg_m1) * (reg_m1 + reg_m2)) if reg_m1 != float("inf") else 0 ) k2 = ( reg * reg_m2 / ((reg + reg_m2) * (reg_m1 + reg_m2)) if reg_m2 != float("inf") else 0 ) k_rho1 = k1 * reg_m1 / reg if reg_m1 != float("inf") else 0 k_rho2 = k2 * reg_m2 / reg if reg_m2 != float("inf") else 0 if reg_m1 == float("inf") and reg_m2 == float("inf"): xi1, xi2 = 0, 0 fi_12 = 1 elif reg_m1 == float("inf"): xi1 = 0 xi2 = reg / reg_m2 fi_12 = reg_m2 elif reg_m2 == float("inf"): xi1 = reg / reg_m1 xi2 = 0 fi_12 = reg_m1 else: xi1 = (reg_m2 * reg) / (reg_m1 * (reg + reg_m1 + reg_m2)) xi2 = (reg_m1 * reg) / (reg_m2 * (reg + reg_m1 + reg_m2)) fi_12 = reg_m1 * reg_m2 / (reg_m1 + reg_m2) xi_rho1 = xi1 * reg_m1 / reg if reg_m1 != float("inf") else 0 xi_rho2 = xi2 * reg_m2 / reg if reg_m2 != float("inf") else 0 reg_ratio1 = reg / reg_m1 if reg_m1 != float("inf") else 0 reg_ratio2 = reg / reg_m2 if reg_m2 != float("inf") else 0 err = 1.0 for i in range(numItermax): uprev = u vprev = v Kv = nx.dot(K, v_) u_hat = (a / Kv) ** fi_1 * nx.sum(b * v_**reg_ratio2) ** k_rho2 u_ = u_hat * nx.sum(a * u_hat ** (-reg_ratio1)) ** (-xi_rho1) Ktu = nx.dot(K.T, u_) v_hat = (b / Ktu) ** fi_2 * nx.sum(a * u_ ** (-reg_ratio1)) ** k_rho1 v_ = v_hat * nx.sum(b * v_hat ** (-reg_ratio2)) ** (-xi_rho2) if ( nx.any(Ktu == 0.0) or nx.any(nx.isnan(u_)) or nx.any(nx.isnan(v_)) or nx.any(nx.isinf(u_)) or nx.any(nx.isinf(v_)) ): # we have reached the machine precision # come back to previous solution and quit loop warnings.warn("Numerical errors at iteration %s" % i) u = uprev v = vprev break t = (nx.sum(a * u_ ** (-reg_ratio1)) / nx.sum(b * v_ ** (-reg_ratio2))) ** ( fi_12 / reg ) u = u_ * t v = v_ / t err_u = nx.max(nx.abs(u - uprev)) / max( nx.max(nx.abs(u)), nx.max(nx.abs(uprev)), 1.0 ) err_v = nx.max(nx.abs(v - vprev)) / max( nx.max(nx.abs(v)), nx.max(nx.abs(vprev)), 1.0 ) err = 0.5 * (err_u + err_v) if log: dict_log["err"].append(err) if verbose: if i % 50 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(i, err)) if err < stopThr: break if log: dict_log["logu"] = nx.log(u + 1e-300) dict_log["logv"] = nx.log(v + 1e-300) if n_hists: # return only loss res = nx.einsum("ik,ij,jk,ij->k", u, K, v, M) if log: return res, dict_log else: return res else: # return OT matrix plan = u[:, None] * K * v[None, :] if log: linear_cost = nx.sum(plan * M) dict_log["cost"] = linear_cost total_cost = linear_cost + reg * nx.kl_div(plan, c) if reg_m1 != float("inf"): total_cost = total_cost + reg_m1 * nx.kl_div(nx.sum(plan, 1), a) if reg_m2 != float("inf"): total_cost = total_cost + reg_m2 * nx.kl_div(nx.sum(plan, 0), b) dict_log["total_cost"] = total_cost return plan, dict_log else: return plan def barycenter_unbalanced_stabilized( A, M, reg, reg_m, weights=None, tau=1e3, numItermax=1000, stopThr=1e-6, verbose=False, log=False, ): r"""Compute the entropic unbalanced wasserstein barycenter of :math:`\mathbf{A}` with stabilization. The function solves the following optimization problem: .. math:: \mathbf{a} = \mathop{\arg \min}_\mathbf{a} \quad \sum_i W_{u_{reg}}(\mathbf{a},\mathbf{a}_i) where : - :math:`W_{u_{reg}}(\cdot,\cdot)` is the unbalanced entropic regularized Wasserstein distance (see :py:func:`ot.unbalanced.sinkhorn_unbalanced`) - :math:`\mathbf{a}_i` are training distributions in the columns of matrix :math:`\mathbf{A}` - reg and :math:`\mathbf{M}` are respectively the regularization term and the cost matrix for OT - reg_mis the marginal relaxation hyperparameter The algorithm used for solving the problem is the generalized Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[10] ` Parameters ---------- A : array-like, shape (dim, n_hists) `n_hists` training distributions :math:`\mathbf{a}_i` of dimension `dim` M : array-like, shape (dim, dim) ground metric matrix for OT. reg : float Entropy regularization term > 0 reg_m : float Marginal relaxation term > 0 tau : float Stabilization threshold for log domain absorption. weights : array-like, shape (n_hists,) optional Weight of each distribution (barycentric coordinates) If None, uniform weights are used. numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (> 0) verbose : bool, optional Print information along iterations log : bool, optional record `log` if `True` Returns ------- a : array-like, shape (dim,) Unbalanced Wasserstein barycenter log : dict log dictionary return only if :math:`log==True` in parameters .. _references-barycenter-unbalanced-stabilized: References ---------- .. [3] Benamou, J. D., Carlier, G., Cuturi, M., Nenna, L., & Peyré, G. (2015). Iterative Bregman projections for regularized transportation problems. SIAM Journal on Scientific Computing, 37(2), A1111-A1138. .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprint arXiv:1607.05816. """ A, M = list_to_array(A, M) nx = get_backend(A, M) dim, n_hists = A.shape if weights is None: weights = nx.ones(n_hists, type_as=A) / n_hists else: assert len(weights) == A.shape[1] if log: log = {"err": []} fi = reg_m / (reg_m + reg) u = nx.ones((dim, n_hists), type_as=A) / dim v = nx.ones((dim, n_hists), type_as=A) / dim # print(reg) K = nx.exp(-M / reg) fi = reg_m / (reg_m + reg) cpt = 0 err = 1.0 alpha = nx.zeros(dim, type_as=A) beta = nx.zeros(dim, type_as=A) q = nx.ones(dim, type_as=A) / dim for i in range(numItermax): qprev = nx.copy(q) Kv = nx.dot(K, v) f_alpha = nx.exp(-alpha / (reg + reg_m)) f_beta = nx.exp(-beta / (reg + reg_m)) f_alpha = f_alpha[:, None] f_beta = f_beta[:, None] u = ((A / (Kv + 1e-16)) ** fi) * f_alpha Ktu = nx.dot(K.T, u) q = (Ktu ** (1 - fi)) * f_beta q = nx.dot(q, weights) ** (1 / (1 - fi)) Q = q[:, None] v = ((Q / (Ktu + 1e-16)) ** fi) * f_beta absorbing = False if nx.any(u > tau) or nx.any(v > tau): absorbing = True alpha = alpha + reg * nx.log(nx.max(u, 1)) beta = beta + reg * nx.log(nx.max(v, 1)) K = nx.exp((alpha[:, None] + beta[None, :] - M) / reg) v = nx.ones(v.shape, type_as=v) Kv = nx.dot(K, v) if ( nx.any(Ktu == 0.0) or nx.any(nx.isnan(u)) or nx.any(nx.isnan(v)) or nx.any(nx.isinf(u)) or nx.any(nx.isinf(v)) ): # we have reached the machine precision # come back to previous solution and quit loop warnings.warn("Numerical errors at iteration %s" % cpt) q = qprev break if (i % 10 == 0 and not absorbing) or i == 0: # we can speed up the process by checking for the error only all # the 10th iterations err = nx.max(nx.abs(q - qprev)) / max( nx.max(nx.abs(q)), nx.max(nx.abs(qprev)), 1.0 ) if log: log["err"].append(err) if verbose: if i % 50 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(i, err)) if err < stopThr: break if err > stopThr: warnings.warn( "Stabilized Unbalanced Sinkhorn did not converge." + "Try a larger entropy `reg` or a lower mass `reg_m`." + "Or a larger absorption threshold `tau`." ) if log: log["niter"] = i log["logu"] = nx.log(u + 1e-300) log["logv"] = nx.log(v + 1e-300) return q, log else: return q def barycenter_unbalanced_sinkhorn( A, M, reg, reg_m, weights=None, numItermax=1000, stopThr=1e-6, verbose=False, log=False, ): r"""Compute the entropic unbalanced wasserstein barycenter of :math:`\mathbf{A}`. The function solves the following optimization problem with :math:`\mathbf{a}` .. math:: \mathbf{a} = \mathop{\arg \min}_\mathbf{a} \quad \sum_i W_{u_{reg}}(\mathbf{a},\mathbf{a}_i) where : - :math:`W_{u_{reg}}(\cdot,\cdot)` is the unbalanced entropic regularized Wasserstein distance (see :py:func:`ot.unbalanced.sinkhorn_unbalanced`) - :math:`\mathbf{a}_i` are training distributions in the columns of matrix :math:`\mathbf{A}` - reg and :math:`\mathbf{M}` are respectively the regularization term and the cost matrix for OT - reg_mis the marginal relaxation hyperparameter The algorithm used for solving the problem is the generalized Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[10] ` Parameters ---------- A : array-like, shape (dim, n_hists) `n_hists` training distributions :math:`\mathbf{a}_i` of dimension `dim` M : array-like, shape (dim, dim) ground metric matrix for OT. reg : float Entropy regularization term > 0 reg_m: float Marginal relaxation term > 0 weights : array-like, shape (n_hists,) optional Weight of each distribution (barycentric coordinates) If None, uniform weights are used. numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (> 0) verbose : bool, optional Print information along iterations log : bool, optional record `log` if `True` Returns ------- a : array-like, shape (dim,) Unbalanced Wasserstein barycenter log : dict log dictionary return only if :math:`log==True` in parameters .. _references-barycenter-unbalanced-sinkhorn: References ---------- .. [3] Benamou, J. D., Carlier, G., Cuturi, M., Nenna, L., & Peyré, G. (2015). Iterative Bregman projections for regularized transportation problems. SIAM Journal on Scientific Computing, 37(2), A1111-A1138. .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprin arXiv:1607.05816. """ A, M = list_to_array(A, M) nx = get_backend(A, M) dim, n_hists = A.shape if weights is None: weights = nx.ones(n_hists, type_as=A) / n_hists else: assert len(weights) == A.shape[1] if log: log = {"err": []} K = nx.exp(-M / reg) fi = reg_m / (reg_m + reg) v = nx.ones((dim, n_hists), type_as=A) u = nx.ones((dim, 1), type_as=A) q = nx.ones(dim, type_as=A) err = 1.0 for i in range(numItermax): uprev = nx.copy(u) vprev = nx.copy(v) qprev = nx.copy(q) Kv = nx.dot(K, v) u = (A / Kv) ** fi Ktu = nx.dot(K.T, u) q = nx.dot(Ktu ** (1 - fi), weights) q = q ** (1 / (1 - fi)) Q = q[:, None] v = (Q / Ktu) ** fi if ( nx.any(Ktu == 0.0) or nx.any(nx.isnan(u)) or nx.any(nx.isnan(v)) or nx.any(nx.isinf(u)) or nx.any(nx.isinf(v)) ): # we have reached the machine precision # come back to previous solution and quit loop warnings.warn("Numerical errors at iteration %s" % i) u = uprev v = vprev q = qprev break # compute change in barycenter err = nx.max(nx.abs(q - qprev)) / max( nx.max(nx.abs(q)), nx.max(nx.abs(qprev)), 1.0 ) if log: log["err"].append(err) # if barycenter did not change + at least 10 iterations - stop if err < stopThr and i > 10: break if verbose: if i % 10 == 0: print("{:5s}|{:12s}".format("It.", "Err") + "\n" + "-" * 19) print("{:5d}|{:8e}|".format(i, err)) if log: log["niter"] = i log["logu"] = nx.log(u + 1e-300) log["logv"] = nx.log(v + 1e-300) return q, log else: return q def barycenter_unbalanced( A, M, reg, reg_m, method="sinkhorn", weights=None, numItermax=1000, stopThr=1e-6, verbose=False, log=False, **kwargs, ): r"""Compute the entropic unbalanced wasserstein barycenter of :math:`\mathbf{A}`. The function solves the following optimization problem with :math:`\mathbf{a}` .. math:: \mathbf{a} = \mathop{\arg \min}_\mathbf{a} \quad \sum_i W_{u_{reg}}(\mathbf{a},\mathbf{a}_i) where : - :math:`W_{u_{reg}}(\cdot,\cdot)` is the unbalanced entropic regularized Wasserstein distance (see :py:func:`ot.unbalanced.sinkhorn_unbalanced`) - :math:`\mathbf{a}_i` are training distributions in the columns of matrix :math:`\mathbf{A}` - reg and :math:`\mathbf{M}` are respectively the regularization term and the cost matrix for OT - reg_mis the marginal relaxation hyperparameter The algorithm used for solving the problem is the generalized Sinkhorn-Knopp matrix scaling algorithm as proposed in :ref:`[10] ` Parameters ---------- A : array-like, shape (dim, n_hists) `n_hists` training distributions :math:`\mathbf{a}_i` of dimension `dim` M : array-like, shape (dim, dim) ground metric matrix for OT. reg : float Entropy regularization term > 0 reg_m: float Marginal relaxation term > 0 weights : array-like, shape (n_hists,) optional Weight of each distribution (barycentric coordinates) If None, uniform weights are used. numItermax : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (> 0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- a : array-like, shape (dim,) Unbalanced Wasserstein barycenter log : dict log dictionary return only if log==True in parameters .. _references-barycenter-unbalanced: References ---------- .. [3] Benamou, J. D., Carlier, G., Cuturi, M., Nenna, L., & Peyré, G. (2015). Iterative Bregman projections for regularized transportation problems. SIAM Journal on Scientific Computing, 37(2), A1111-A1138. .. [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). Scaling algorithms for unbalanced transport problems. arXiv preprin arXiv:1607.05816. """ if method.lower() == "sinkhorn": return barycenter_unbalanced_sinkhorn( A, M, reg, reg_m, weights=weights, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, **kwargs, ) elif method.lower() == "sinkhorn_stabilized": return barycenter_unbalanced_stabilized( A, M, reg, reg_m, weights=weights, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, **kwargs, ) elif method.lower() in ["sinkhorn_reg_scaling", "sinkhorn_translation_invariant"]: warnings.warn("Method not implemented yet. Using classic Sinkhorn Knopp") return barycenter_unbalanced( A, M, reg, reg_m, weights=weights, numItermax=numItermax, stopThr=stopThr, verbose=verbose, log=log, **kwargs, ) else: raise ValueError("Unknown method '%s'." % method) python-pot-0.9.7+dfsg/ot/unbalanced/_sliced.py000066400000000000000000000335021525521106200212630ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Sliced Unbalanced OT solvers """ # Author: Clément Bonet # # License: MIT License from ..backend import get_backend from ..utils import get_parameter_pair from ..sliced import get_random_projections from ._solver_1d import rescale_potentials, uot_1d from ..lp.solver_1d import emd_1d_dual_backprop, wasserstein_1d def sliced_unbalanced_ot( X_s, X_t, reg_m, a=None, b=None, n_projections=50, p=2, projections=None, seed=None, numItermax=10, log=False, ): r""" Compute the Sliced Unbalanced Optimal Transport (SUOT) between two empirical distributions. The 1D UOT problem is computed with KL regularization and solved with a Frank-Wolfe algorithm, see :ref:`[82] `. The Sliced Unbalanced Optimal Transport (SUOT) is defined as .. math:: \mathrm{SUOT}_p^p(\mu, \nu) = \int_{S^{d-1}} \mathrm{UOT}_p^p(P^\theta_\#\mu, P^\theta_\#\nu)\ \mathrm{d}\lambda(\theta) with :math:`P^\theta(x)=\langle x,\theta\rangle` and :math:`\lambda` the uniform distribution on the unit sphere. .. warning:: This function only works in pytorch or jax as it uses autodifferentiation to compute the 1D UOT problems. It is not maintained in jax. Parameters ---------- X_s : ndarray, shape (n_samples_a, dim) samples in the source domain X_t : ndarray, shape (n_samples_b, dim) samples in the target domain reg_m: float or indexable object of length 1 or 2 Marginal relaxation term. If `reg_m` is a scalar or an indexable object of length 1, then the same `reg_m` is applied to both marginal relaxations. The balanced OT can be recovered using `reg_m=float("inf")`. For semi-relaxed case, use either `reg_m=(float("inf"), scalar)` or `reg_m=(scalar, float("inf"))`. If `reg_m` is an array, it must have the same backend as input arrays `(X_s, X_t)`. a : ndarray, shape (n_samples_a,), optional samples weights in the source domain b : ndarray, shape (n_samples_b,), optional samples weights in the target domain n_projections : int, optional Number of projections used for the Monte-Carlo approximation p: float, optional, by default =2 Power p used for computing the sliced Wasserstein projections: shape (dim, n_projections), optional Projection matrix (n_projections and seed are not used in this case) seed: int or RandomState or None, optional Seed used for random number generator numItermax: int, optional log: bool, optional if True, returns the projections used and their associated UOTs and reweighted marginals. Returns ------- loss: float/array-like, shape (...) SUOT log: dict, optional If `log` is True, then returns a dictionary containing the projection directions used, the projected UOTs, and reweighted marginals on each slices. .. _references-suot: References ---------- .. [82] Bonet, C., Nadjahi, K., Séjourné, T., Fatras, K., & Courty, N. (2025). Slicing Unbalanced Optimal Transport. Transactions on Machine Learning Research. See Also -------- ot.unbalanced.uot_1d: 1D OT problem ot.unbalanced.unbalanced_sliced_ot: Unbalanced SOT problem """ nx = get_backend(X_s, X_t, a, b, projections) assert nx.__name__ in ["torch", "jax"], "Function only valid in torch and jax" n = X_s.shape[0] m = X_t.shape[0] if X_s.shape[1] != X_t.shape[1]: raise ValueError( "X_s and X_t must have the same number of dimensions {} and {} respectively given".format( X_s.shape[1], X_t.shape[1] ) ) if a is None: a = nx.full(n, 1 / n, type_as=X_s) if b is None: b = nx.full(m, 1 / m, type_as=X_s) d = X_s.shape[1] if projections is None: projections = get_random_projections( d, n_projections, seed, backend=nx, type_as=X_s ) else: n_projections = projections.shape[1] X_s_projections = nx.dot(X_s, projections) # shape (n, n_projs) X_t_projections = nx.dot(X_t, projections) # Compute UOT on each slice a_reweighted, b_reweighted, projected_uot = uot_1d( X_s_projections, X_t_projections, reg_m, a, b, p, require_sort=True, numItermax=numItermax, returnCost="total", ) res = nx.mean(projected_uot) if log: dico = { "projections": projections, "projected_uots": projected_uot, "a_reweighted": a_reweighted, "b_reweighted": b_reweighted, } return res, dico return res def get_reweighted_marginals_usot( f, g, a, b, reg_m1, reg_m2, X_s_sorter, X_t_sorter, nx ): r""" One step of the FW algorithm for the Unbalanced Sliced OT problem, see Algorithm 1 and 3 in :ref:`[82] `. This function computes the reweighted marginals given the current potentials and the translation term. It returns the current potentials, and the reweighted marginals (normalized by the mass, so that they sum to 1). Parameters ---------- f: array-like shape (n, ...) Current potential on the source samples g: array-like shape (m, ...) Current potential on the target samples a: array-like shape (n, ...) Current weights on the source samples b: array-like shape (m, ...) Current weights on the target samples reg_m1: float Marginal relaxation term for the source distribution reg_m2: float Marginal relaxation term for the target distribution X_s_sorter: array-like shape (n_projs, n) Sorter for the projected source samples X_t_sorter: array-like shape (n_projs, m) Sorter for the projected target samples nx: module backend module Returns ------- f: array-like shape (n, ...) Current potential on the source samples g: array-like shape (m, ...) Current potential on the target samples a_reweighted: array-like shape (n, ...) Reweighted weights on the source samples (normalized by the mass) b_reweighted: array-like shape (m, ...) Reweighted weights on the target samples (normalized by the mass) full_mass: array-like shape (...) Mass of the reweighted measures .. _references-uot: References ---------- [82] Bonet, C., Nadjahi, K., Séjourné, T., Fatras, K., & Courty, N. (2025). Slicing Unbalanced Optimal Transport. Transactions on Machine Learning Research. """ # translate potentials transl = rescale_potentials(f, g, a, b, reg_m1, reg_m2, nx) f = f + transl g = g - transl # update measures if reg_m1 != float("inf"): a_reweighted = (a * nx.exp(-f / reg_m1))[..., X_s_sorter] else: a_reweighted = a[..., X_s_sorter] if reg_m2 != float("inf"): b_reweighted = (b * nx.exp(-g / reg_m2))[..., X_t_sorter] else: b_reweighted = b[..., X_t_sorter] full_mass = nx.sum(a_reweighted, axis=1) # normalize the weights for compatibility with wasserstein_1d a_reweighted = a_reweighted / nx.sum(a_reweighted, axis=1, keepdims=True) b_reweighted = b_reweighted / nx.sum(b_reweighted, axis=1, keepdims=True) return f, g, a_reweighted, b_reweighted, full_mass def unbalanced_sliced_ot( X_s, X_t, reg_m, a=None, b=None, n_projections=50, p=2, projections=None, seed=None, numItermax=10, log=False, ): r""" Compute the Unbalanced Sliced Optimal Transpot (USOT) with KL regularization between two empirical distributions. The Unbalanced SOT problem reads as .. math:: \mathrm{USOT}_p^p(\mu, \nu) = \inf_{\pi_1,\pi_2} \mathrm{SW}_p^p(\pi_1, \pi_2) + \mathrm{reg_{m}}_1 \mathrm{KL}(\pi_1||\mu) + \mathrm{reg_{m}}_2 \mathrm{KL}(\pi_2||\nu). The USOT problem is solved with a Frank-Wolfe algorithm as proposed in :ref:`[82] `. .. warning:: This function only works in pytorch or jax as it uses autodifferentiation to compute the 1D potentials. It is not maintained in jax. Parameters ---------- X_s : ndarray, shape (n_samples_a, dim) samples in the source domain X_t : ndarray, shape (n_samples_b, dim) samples in the target domain reg_m: float or indexable object of length 1 or 2 Marginal relaxation term. If `reg_m` is a scalar or an indexable object of length 1, then the same `reg_m` is applied to both marginal relaxations. The balanced OT can be recovered using `reg_m=float("inf")`. For semi-relaxed case, use either `reg_m=(float("inf"), scalar)` or `reg_m=(scalar, float("inf"))`. If `reg_m` is an array, it must have the same backend as input arrays `(X_s, X_t)`. a : ndarray, shape (n_samples_a,), optional samples weights in the source domain b : ndarray, shape (n_samples_b,), optional samples weights in the target domain n_projections : int, optional Number of projections used for the Monte-Carlo approximation p: float, optional, by default =2 Power p used for computing the sliced Wasserstein projections: shape (dim, n_projections), optional Projection matrix (n_projections and seed are not used in this case) seed: int or RandomState or None, optional Seed used for random number generator numItermax: int, optional log: bool, optional if True, returns the sot loss, the projections used, their associated EMD and the full mass of the reweighted marginals. Returns ------- a_reweighted: array-like shape (n, ...) First marginal reweighted b_reweighted: array-like shape (m, ...) Second marginal reweighted loss: float/array-like, shape (...) USOT log: dict, optional If `log` is True, then returns a dictionary containing the projection directions used, the 1D OT losses, the SOT loss and the full mass of reweighted marginals. .. _references-usot: References ---------- .. [82] Bonet, C., Nadjahi, K., Séjourné, T., Fatras, K., & Courty, N. (2025). Slicing Unbalanced Optimal Transport. Transactions on Machine Learning Research. See Also -------- ot.unbalanced.uot_1d: 1D OT problem ot.unbalanced.sliced_unbalanced_ot: SUOT problem """ nx = get_backend(X_s, X_t, a, b, projections) assert nx.__name__ in ["torch", "jax"], "Function only valid in torch and jax" reg_m1, reg_m2 = get_parameter_pair(reg_m) n = X_s.shape[0] m = X_t.shape[0] if X_s.shape[1] != X_t.shape[1]: raise ValueError( "X_s and X_t must have the same number of dimensions {} and {} respectively given".format( X_s.shape[1], X_t.shape[1] ) ) if a is None: a = nx.full(n, 1 / n, type_as=X_s) if b is None: b = nx.full(m, 1 / m, type_as=X_s) d = X_s.shape[1] if projections is None: projections = get_random_projections( d, n_projections, seed, backend=nx, type_as=X_s ) else: n_projections = projections.shape[1] # Compute projections of the samples, and sort them for later use in the FW algorithm X_s_projections = nx.dot(X_s, projections).T # shape (n_projs, n) X_t_projections = nx.dot(X_t, projections).T X_s_sorter = nx.argsort(X_s_projections, -1) X_s_rev_sorter = nx.argsort(X_s_sorter, -1) X_s_sorted = nx.take_along_axis(X_s_projections, X_s_sorter, -1) X_t_sorter = nx.argsort(X_t_projections, -1) X_t_rev_sorter = nx.argsort(X_t_sorter, -1) X_t_sorted = nx.take_along_axis(X_t_projections, X_t_sorter, -1) # Initialize potentials - WARNING: They correspond to non-sorted samples f = nx.zeros(a.shape, type_as=a) g = nx.zeros(b.shape, type_as=b) for i in range(numItermax): f, g, a_reweighted, b_reweighted, _ = get_reweighted_marginals_usot( f, g, a, b, reg_m1, reg_m2, X_s_sorter, X_t_sorter, nx ) fd, gd, _ = emd_1d_dual_backprop( X_s_sorted.T, X_t_sorted.T, u_weights=a_reweighted.T, v_weights=b_reweighted.T, p=p, require_sort=False, ) fd, gd = fd.T, gd.T # default step for FW t = 2.0 / (2.0 + i) f = f + t * (nx.mean(nx.take_along_axis(fd, X_s_rev_sorter, 1), axis=0) - f) g = g + t * (nx.mean(nx.take_along_axis(gd, X_t_rev_sorter, 1), axis=0) - g) f, g, a_reweighted, b_reweighted, full_mass = get_reweighted_marginals_usot( f, g, a, b, reg_m1, reg_m2, X_s_sorter, X_t_sorter, nx ) ot_loss = wasserstein_1d( X_s_sorted.T, X_t_sorted.T, u_weights=a_reweighted.T, v_weights=b_reweighted.T, p=p, require_sort=False, ) sot_loss = nx.mean(ot_loss * full_mass) if reg_m1 != float("inf"): a_reweighted = a * nx.exp(-f / reg_m1) else: a_reweighted = a if reg_m2 != float("inf"): b_reweighted = b * nx.exp(-g / reg_m2) else: b_reweighted = b if reg_m1 == float("inf") and reg_m2 == float("inf"): uot_loss = sot_loss elif reg_m1 == float("inf"): uot_loss = sot_loss + reg_m2 * nx.kl_div(b_reweighted, b, mass=True) elif reg_m2 == float("inf"): uot_loss = sot_loss + reg_m1 * nx.kl_div(a_reweighted, a, mass=True) else: uot_loss = ( sot_loss + reg_m1 * nx.kl_div(a_reweighted, a, mass=True) + reg_m2 * nx.kl_div(b_reweighted, b, mass=True) ) if log: dico = { "projections": projections, "sot_loss": sot_loss, "1d_losses": ot_loss, "full_mass": full_mass, } return a_reweighted, b_reweighted, uot_loss, dico return a_reweighted, b_reweighted, uot_loss python-pot-0.9.7+dfsg/ot/unbalanced/_solver_1d.py000066400000000000000000000252451525521106200217230ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ 1D Unbalanced OT solvers """ # Author: Clément Bonet # # License: MIT License from ..backend import get_backend from ..utils import get_parameter_pair from ..lp.solver_1d import emd_1d_dual_backprop, wasserstein_1d def rescale_potentials(f, g, a, b, rho1, rho2, nx): r""" Find the optimal :math: `\lambda` in the translation invariant dual of UOT with KL regularization and returns it, see Proposition 2 in :ref:`[73] `. Parameters ---------- f: array-like, shape (n, ...) first dual potential g: array-like, shape (m, ...) second dual potential a: array-like, shape (n, ...) weights of the first empirical distribution b: array-like, shape (m, ...) weights of the second empirical distribution rho1: float Marginal relaxation term for the first marginal rho2: float Marginal relaxation term for the second marginal nx: module backend module Returns ------- transl: array-like, shape (...) optimal translation .. _references-uot: References ---------- .. [73] Séjourné, T., Vialard, F. X., & Peyré, G. (2022). Faster unbalanced optimal transport: Translation invariant sinkhorn and 1-d frank-wolfe. In International Conference on Artificial Intelligence and Statistics (pp. 4995-5021). PMLR. """ if rho1 == float("inf") and rho2 == float("inf"): return nx.zeros(shape=nx.sum(f, axis=0).shape, type_as=f) elif rho1 == float("inf"): tau = rho2 denom = nx.logsumexp(-g / rho2 + nx.log(b), axis=0) num = nx.log(nx.sum(a, axis=0)) elif rho2 == float("inf"): tau = rho1 num = nx.logsumexp(-f / rho1 + nx.log(a), axis=0) denom = nx.log(nx.sum(b, axis=0)) else: tau = (rho1 * rho2) / (rho1 + rho2) num = nx.logsumexp(-f / rho1 + nx.log(a), axis=0) denom = nx.logsumexp(-g / rho2 + nx.log(b), axis=0) transl = tau * (num - denom) return transl def get_reweighted_marginal_uot( f, g, u_weights_sorted, v_weights_sorted, reg_m1, reg_m2, nx ): r""" One step of the FW algorithm for the 1D UOT problem with KL regularization. This function computes the reweighted marginals given the current dual potentials. It returns the current potentials, and the reweighted marginals (normalized by the mass so that they sum to 1). Parameters ---------- f: array-like, shape (n, ...) first dual potential g: array-like, shape (m, ...) second dual potential u_weights_sorted: array-like, shape (n, ...) weights of the first empirical distribution, sorted w.r.t. the support v_weights_sorted: array-like, shape (m, ...) weights of the second empirical distribution, sorted w.r.t. the support reg_m1: float Marginal relaxation term for the first marginal reg_m2: float Marginal relaxation term for the second marginal nx: module backend module Returns ------- f: array-like, shape (n, ...) first dual potential g: array-like, shape (m, ...) second dual potential u_rescaled: array-like, shape (n, ...) reweighted first marginal, normalized by the mass v_rescaled: array-like, shape (m, ...) reweighted second marginal, normalized by the mass full_mass: array-like, shape (...) mass of the reweighted marginals """ transl = rescale_potentials( f, g, u_weights_sorted, v_weights_sorted, reg_m1, reg_m2, nx ) f = f + transl[None] g = g - transl[None] if reg_m1 != float("inf"): u_reweighted = u_weights_sorted * nx.exp(-f / reg_m1) else: u_reweighted = u_weights_sorted if reg_m2 != float("inf"): v_reweighted = v_weights_sorted * nx.exp(-g / reg_m2) else: v_reweighted = v_weights_sorted full_mass = nx.sum(u_reweighted, axis=0) # Normalize weights u_rescaled = u_reweighted / nx.sum(u_reweighted, axis=0, keepdims=True) v_rescaled = v_reweighted / nx.sum(v_reweighted, axis=0, keepdims=True) return f, g, u_rescaled, v_rescaled, full_mass def uot_1d( u_values, v_values, reg_m, u_weights=None, v_weights=None, p=2, require_sort=True, numItermax=10, returnCost="linear", log=False, ): r""" Solves the 1D unbalanced OT problem with KL regularization. The function implements the Frank-Wolfe algorithm to solve the dual problem, as proposed in :ref:`[73] `. The unbalanced OT problem reads .. math:: \mathrm{UOT}_p^p(\mu,\nu) = \min_{\gamma \in \mathcal{M}_{+}(\mathbb{R}\times\mathbb{R})} W_p^p(\pi^1_\#\gamma,\pi^2_\#\gamma) + \mathrm{reg_{m}}_1 \mathrm{KL}(\pi^1_\#\gamma|\mu) + \mathrm{reg_{m}}_2 \mathrm{KL}(\pi^2_\#\gamma|\nu). .. warning:: This function only works in pytorch or jax as it uses autodifferentiation to compute the potentials. It is not maintained in jax. Parameters ---------- u_values: array-like, shape (n, ...) locations of the first empirical distribution v_values: array-like, shape (m, ...) locations of the second empirical distribution reg_m: float or indexable object of length 1 or 2 Marginal relaxation term. If `reg_m` is a scalar or an indexable object of length 1, then the same `reg_m` is applied to both marginal relaxations. The balanced OT can be recovered using `reg_m=float("inf")`. For semi-relaxed case, use either `reg_m=(float("inf"), scalar)` or `reg_m=(scalar, float("inf"))`. If `reg_m` is an array, it must have the same backend as input arrays `(u_values, v_values)`. u_weights: array-like, shape (n, ...), optional weights of the first empirical distribution, if None then uniform weights are used v_weights: array-like, shape (m, ...), optional weights of the second empirical distribution, if None then uniform weights are used p: int, optional order of the ground metric used, should be at least 1, default is 2 require_sort: bool, optional sort the distributions atoms locations, if False we will consider they have been sorted prior to being passed to the function, default is True numItermax: int, optional returnCost: string, optional (default = "linear") If `returnCost` = "linear", then return the linear part of the unbalanced OT loss. If `returnCost` = "total", then return the total unbalanced OT loss. log: bool, optional Returns ------- u_reweighted: array-like shape (n, ...) First marginal reweighted v_reweighted: array-like shape (m, ...) Second marginal reweighted loss: float/array-like, shape (...) The batched 1D UOT log: dict, optional If `log` is True, then returns a dictionary containing the dual potentials, the total cost and the linear cost. .. _references-uot: References --------- .. [73] Séjourné, T., Vialard, F. X., & Peyré, G. (2022). Faster unbalanced optimal transport: Translation invariant sinkhorn and 1-d frank-wolfe. In International Conference on Artificial Intelligence and Statistics (pp. 4995-5021). PMLR. """ nx = get_backend(u_values, v_values, u_weights, v_weights) assert nx.__name__ in ["torch", "jax"], "Function only valid in torch and jax" reg_m1, reg_m2 = get_parameter_pair(reg_m) n = u_values.shape[0] m = v_values.shape[0] # Init weights or broadcast if necessary if u_weights is None: u_weights = nx.full(u_values.shape, 1.0 / n, type_as=u_values) elif u_weights.ndim != u_values.ndim: u_weights = nx.repeat(u_weights[..., None], u_values.shape[-1], -1) if v_weights is None: v_weights = nx.full(v_values.shape, 1.0 / m, type_as=v_values) elif v_weights.ndim != v_values.ndim: v_weights = nx.repeat(v_weights[..., None], v_values.shape[-1], -1) # Sort w.r.t. support if not already done if require_sort: u_sorter = nx.argsort(u_values, 0) u_rev_sorter = nx.argsort(u_sorter, 0) u_values_sorted = nx.take_along_axis(u_values, u_sorter, 0) v_sorter = nx.argsort(v_values, 0) v_rev_sorter = nx.argsort(v_sorter, 0) v_values_sorted = nx.take_along_axis(v_values, v_sorter, 0) u_weights_sorted = nx.take_along_axis(u_weights, u_sorter, 0) v_weights_sorted = nx.take_along_axis(v_weights, v_sorter, 0) f = nx.zeros(u_weights.shape, type_as=u_weights) fd = nx.zeros(u_weights.shape, type_as=u_weights) g = nx.zeros(v_weights.shape, type_as=v_weights) gd = nx.zeros(v_weights.shape, type_as=v_weights) for i in range(numItermax): # FW steps f, g, u_rescaled, v_rescaled, _ = get_reweighted_marginal_uot( f, g, u_weights_sorted, v_weights_sorted, reg_m1, reg_m2, nx ) fd, gd, loss = emd_1d_dual_backprop( u_values_sorted, v_values_sorted, u_weights=u_rescaled, v_weights=v_rescaled, p=p, require_sort=False, ) t = 2.0 / (2.0 + i) f = f + t * (fd - f) g = g + t * (gd - g) f, g, u_rescaled, v_rescaled, full_mass = get_reweighted_marginal_uot( f, g, u_weights_sorted, v_weights_sorted, reg_m1, reg_m2, nx ) loss = wasserstein_1d( u_values_sorted, v_values_sorted, u_rescaled, v_rescaled, p=p, require_sort=False, ) if require_sort: f = nx.take_along_axis(f, u_rev_sorter, 0) g = nx.take_along_axis(g, v_rev_sorter, 0) u_reweighted = nx.take_along_axis(u_rescaled, u_rev_sorter, 0) * full_mass v_reweighted = nx.take_along_axis(v_rescaled, v_rev_sorter, 0) * full_mass # rescale OT loss linear_loss = loss * full_mass if reg_m1 == float("inf") and reg_m2 == float("inf"): uot_loss = linear_loss elif reg_m1 == float("inf"): uot_loss = linear_loss + reg_m2 * nx.kl_div(v_reweighted, v_weights, mass=True) elif reg_m2 == float("inf"): uot_loss = linear_loss + reg_m1 * nx.kl_div(u_reweighted, u_weights, mass=True) else: uot_loss = ( linear_loss + reg_m1 * nx.kl_div(u_reweighted, u_weights, mass=True, axis=0) + reg_m2 * nx.kl_div(v_reweighted, v_weights, mass=True, axis=0) ) if returnCost == "linear": out_loss = linear_loss elif returnCost == "total": out_loss = uot_loss if log: dico = {"f": f, "g": g, "total_cost": uot_loss, "linear_cost": linear_loss} return u_reweighted, v_reweighted, out_loss, dico return u_reweighted, v_reweighted, out_loss python-pot-0.9.7+dfsg/ot/utils.py000066400000000000000000002117251525521106200167320ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Various useful functions """ # Author: Remi Flamary # # License: MIT License from functools import reduce import time import numpy as np from scipy.spatial.distance import cdist import sys import warnings from inspect import signature from .backend import get_backend, Backend, NumpyBackend, JaxBackend __time_tic_toc = time.perf_counter() def tic(): r"""Python implementation of Matlab tic() function""" global __time_tic_toc __time_tic_toc = time.perf_counter() def toc(message="Elapsed time : {} s"): r"""Python implementation of Matlab toc() function""" t = time.perf_counter() print(message.format(t - __time_tic_toc)) return t - __time_tic_toc def toq(): r"""Python implementation of Julia toc() function""" t = time.perf_counter() return t - __time_tic_toc def kernel(x1, x2, method="gaussian", sigma=1, **kwargs): r"""Compute kernel matrix""" nx = get_backend(x1, x2) if method.lower() in ["gaussian", "gauss", "rbf"]: K = nx.exp(-dist(x1, x2) / (2 * sigma**2)) return K def laplacian(x): r"""Compute Laplacian matrix""" nx = get_backend(x) L = nx.diag(nx.sum(x, axis=0)) - x return L def list_to_array(*lst, nx=None): r"""Convert a list if in numpy format""" # Filter to non-empty, non-list items (handle sparse matrices gracefully) lst_not_empty = [] for a in lst: if isinstance(a, list): continue try: if len(a) > 0: lst_not_empty.append(a) except TypeError: # Sparse matrices don't support len(), but they're not empty lst_not_empty.append(a) if nx is None: # find backend if len(lst_not_empty) == 0: type_as = np.zeros(0) nx = get_backend(type_as) else: nx = get_backend(*lst_not_empty) type_as = lst_not_empty[0] else: if len(lst_not_empty) == 0: type_as = None else: type_as = lst_not_empty[0] if len(lst) > 1: return [ nx.from_numpy(np.array(a), type_as=type_as) if isinstance(a, list) else a for a in lst ] else: if isinstance(lst[0], list): return nx.from_numpy(np.array(lst[0]), type_as=type_as) else: return lst[0] def proj_simplex(v, z=1): r"""Compute the closest point (orthogonal projection) on the generalized `(n-1)`-simplex of a vector :math:`\mathbf{v}` wrt. to the Euclidean distance, thus solving: .. math:: \mathcal{P}(w) \in \mathop{\arg \min}_\gamma \| \gamma - \mathbf{v} \|_2 s.t. \ \gamma^T \mathbf{1} = z \gamma \geq 0 If :math:`\mathbf{v}` is a 2d array, compute all the projections wrt. axis 0 .. note:: This function is backend-compatible and will work on arrays from all compatible backends. Parameters ---------- v : {array-like}, shape (n, d) z : int, optional 'size' of the simplex (each vectors sum to z, 1 by default) Returns ------- h : ndarray, shape (`n`, `d`) Array of projections on the simplex """ nx = get_backend(v) n = v.shape[0] if v.ndim == 1: d1 = 1 v = v[:, None] else: d1 = 0 d = v.shape[1] # sort u in ascending order u = nx.sort(v, axis=0) # take the descending order u = nx.flip(u, 0) cssv = nx.cumsum(u, axis=0) - z ind = nx.arange(n, type_as=v)[:, None] + 1 cond = u - cssv / ind > 0 rho = nx.sum(cond, 0) theta = cssv[rho - 1, nx.arange(d)] / rho w = nx.maximum(v - theta[None, :], nx.zeros(v.shape, type_as=v)) if d1: return w[:, 0] else: return w def projection_sparse_simplex(V, max_nz, z=1, axis=None, nx=None): r"""Projection of :math:`\mathbf{V}` onto the simplex with cardinality constraint (maximum number of non-zero elements) and then scaled by `z`. .. math:: P\left(\mathbf{V}, \text{max_nz}, z\right) = \mathop{\arg \min}_{\substack{\mathbf{y} >= 0 \\ \sum_i \mathbf{y}_i = z} \\ ||p||_0 \le \text{max_nz}} \quad \|\mathbf{y} - \mathbf{V}\|^2 Parameters ---------- V: 1-dim or 2-dim ndarray max_nz: int Maximum number of non-zero elements in the projection. If `max_nz` is larger than the number of elements in `V`, then the projection is equivalent to `proj_simplex(V, z)`. z: float or array If array, len(z) must be compatible with :math:`\mathbf{V}` axis: None or int - axis=None: project :math:`\mathbf{V}` by :math:`P(\mathbf{V}.\mathrm{ravel}(), \text{max_nz}, z)` - axis=1: project each :math:`\mathbf{V}_i` by :math:`P(\mathbf{V}_i, \text{max_nz}, z_i)` - axis=0: project each :math:`\mathbf{V}_{:, j}` by :math:`P(\mathbf{V}_{:, j}, \text{max_nz}, z_j)` Returns ------- projection: ndarray, shape :math:`\mathbf{V}`.shape References ---------- .. [1] Sparse projections onto the simplex Anastasios Kyrillidis, Stephen Becker, Volkan Cevher and, Christoph Koch ICML 2013 https://arxiv.org/abs/1206.1529 """ if nx is None: nx = get_backend(V) if V.ndim == 1: return projection_sparse_simplex( # V[nx.newaxis, :], max_nz, z, axis=1).ravel() V[None, :], max_nz, z, axis=1, ).ravel() if V.ndim > 2: raise ValueError("V.ndim must be <= 2") if axis == 1: # For each row of V, find top max_nz values; arrange the # corresponding column indices such that their values are # in a descending order. max_nz_indices = nx.argsort(V, axis=1)[:, -max_nz:] max_nz_indices = nx.flip(max_nz_indices, axis=1) row_indices = nx.arange(V.shape[0]) row_indices = row_indices.reshape(-1, 1) print(row_indices.shape) # Extract the top max_nz values for each row # and then project to simplex. U = V[row_indices, max_nz_indices] z = nx.ones(len(U)) * z cssv = nx.cumsum(U, axis=1) - z[:, None] ind = nx.arange(max_nz) + 1 cond = U - cssv / ind > 0 # rho = nx.count_nonzero(cond, axis=1) rho = nx.sum(cond, axis=1) theta = cssv[nx.arange(len(U)), rho - 1] / rho nz_projection = nx.maximum(U - theta[:, None], 0) # Put the projection of max_nz_values to their original column indices # while keeping other values zero. sparse_projection = nx.zeros(V.shape, type_as=nz_projection) if isinstance(nx, JaxBackend): # in Jax, we need to use the `at` property of `jax.numpy.ndarray` # to do in-place array modifications. sparse_projection = sparse_projection.at[row_indices, max_nz_indices].set( nz_projection ) else: sparse_projection[row_indices, max_nz_indices] = nz_projection return sparse_projection elif axis == 0: return projection_sparse_simplex(V.T, max_nz, z, axis=1).T else: V = V.ravel().reshape(1, -1) return projection_sparse_simplex(V, max_nz, z, axis=1).ravel() def unif(n, type_as=None): r""" Return a uniform histogram of length `n` (simplex). Parameters ---------- n : int number of bins in the histogram type_as : array-like array of the same type of the expected output (numpy/pytorch/jax) Returns ------- h : array-like, shape (n,) histogram of length `n` such that :math:`\forall i, \mathbf{h}_i = \frac{1}{n}` """ if type_as is None: return np.ones((n,)) / n else: nx = get_backend(type_as) return nx.ones((n,), type_as=type_as) / n def clean_zeros(a, b, M): r"""Remove all components with zeros weights in :math:`\mathbf{a}` and :math:`\mathbf{b}`""" M2 = M[a > 0, :][:, b > 0].copy() # copy force c style matrix (froemd) a2 = a[a > 0] b2 = b[b > 0] return a2, b2, M2 def euclidean_distances(X, Y, squared=False, nx=None): r""" Considering the rows of :math:`\mathbf{X}` (and :math:`\mathbf{Y} = \mathbf{X}`) as vectors, compute the distance matrix between each pair of vectors. .. note:: This function is backend-compatible and will work on arrays from all compatible backends. Parameters ---------- X : array-like, shape (n_samples_1, n_features) Y : array-like, shape (n_samples_2, n_features) squared : boolean, optional Return squared Euclidean distances. Returns ------- distances : array-like, shape (`n_samples_1`, `n_samples_2`) """ if nx is None: nx = get_backend(X, Y) a2 = nx.einsum("ij,ij->i", X, X) b2 = nx.einsum("ij,ij->i", Y, Y) c = -2 * nx.dot(X, nx.transpose(Y)) c += a2[:, None] c += b2[None, :] c = nx.maximum(c, 0) if not squared: c = nx.sqrt(c) if X is Y: c = c * (1 - nx.eye(X.shape[0], type_as=c)) return c def sparse_ot_dist( x1, x2, i, j, w=None, metric="sqeuclidean", p=2, batch_size=None, ): r"""Compute ot distance between samples in :math:`\mathbf{x_1}` and :math:`\mathbf{x_2}` with sparse weights given by `w` for the pairs of samples with indices `i` and `j`. .. note:: This function is backend-compatible and will work on arrays from all compatible backends for the following metrics: 'sqeuclidean', 'euclidean', 'cityblock', 'minkowski'. Parameters ---------- x1 : array-like, shape (n1,d) matrix with `n1` samples of size `d` x2 : array-like, shape (n2,d), optional matrix with `n2` samples of size `d` (if None then :math:`\mathbf{x_2} = \mathbf{x_1}`) i : array-like, shape (k,) indices of samples in `x1` to compute distance from j : array-like, shape (k,) indices of samples in `x2` to compute distance to w : array-like, shape (k,), optional weights for each pair of samples to compute distance between. If None, all pairs are weighted equally (=1/k). metric : str | callable, optional 'sqeuclidean', 'euclidean', 'cityblock' or 'minkowski'. p : float, optional p-norm for the Minkowski metric. Default value is 2. batch_size : int, optional If specified, compute the distance in batches of size `batch_size` to avoid memory issues for large datasets. Default is None (no batching). Returns ------- dist : float sum of the distance between :math:`\mathbf{x_1}_i` and :math:`\mathbf{x_2}_j` computed with given metric and weighted by `w` """ nx = get_backend(x1, x2) assert x1.ndim == 2, f"x1 must be a 2d array, got {x1.ndim}d array instead" assert x2.ndim == 2, f"x2 must be a 2d array, got {x2.ndim}d array instead" assert len(i) == len( j ), f"i and j must have the same length, got {len(i)} and {len(j)}" if w is not None: assert len(w) == len( i ), f"w must have the same length as i and j, got {len(w)} and {len(i)}" assert metric in ("minkowski", "euclidean", "cityblock", "sqeuclidean"), ( "sparse_dist work only with metrics from the following list: " + "`['sqeuclidean', 'minkowski', 'cityblock', 'euclidean']`" ) assert ( x1.shape[1] == x2.shape[1] ), f"x1 ({x1.shape}) and x2 ({x2.shape}) must have the same number of columns" if metric == "euclidean": p = 2 elif metric == "cityblock": p = 1 def dist_idxs(idx_x1, idx_x2): if metric == "sqeuclidean": return nx.sum((x1[idx_x1] - x2[idx_x2]) ** 2, axis=1) else: return nx.sum(nx.abs(x1[idx_x1] - x2[idx_x2]) ** p, axis=1) ** (1 / p) if w is None: w = nx.ones(len(i), type_as=x1) / len(i) d = 0 if batch_size is None: batch_size = len(i) for b in range(0, len(i), batch_size): d += nx.sum( dist_idxs(i[b : b + batch_size], j[b : b + batch_size]) * w[i[b : b + batch_size]] ) return d def dist( x1, x2=None, metric="sqeuclidean", p=2, w=None, backend="auto", nx=None, use_tensor=False, ): r"""Compute distance between samples in :math:`\mathbf{x_1}` and :math:`\mathbf{x_2}` .. note:: This function is backend-compatible and will work on arrays from all compatible backends for the following metrics: 'sqeuclidean', 'euclidean', 'cityblock', 'minkowski', 'cosine', 'correlation'. Parameters ---------- x1 : array-like, shape (n1,d) matrix with `n1` samples of size `d` x2 : array-like, shape (n2,d), optional matrix with `n2` samples of size `d` (if None then :math:`\mathbf{x_2} = \mathbf{x_1}`) metric : str | callable, optional 'sqeuclidean' or 'euclidean' on all backends. On numpy the function also accepts from the scipy.spatial.distance.cdist function : 'braycurtis', 'canberra', 'chebyshev', 'cityblock', 'correlation', 'cosine', 'dice', 'euclidean', 'hamming', 'jaccard', 'kulczynski1', 'mahalanobis', 'matching', 'minkowski', 'rogerstanimoto', 'russellrao', 'seuclidean', 'sokalmichener', 'sokalsneath', 'sqeuclidean', 'wminkowski', 'yule'. p : float, optional p-norm for the Minkowski and the Weighted Minkowski metrics. Default value is 2. w : array-like, rank 1 Weights for the weighted metrics. backend : str, optional Backend to use for the computation. If 'auto', the backend is automatically selected based on the input data. if 'scipy', the ``scipy.spatial.distance.cdist`` function is used (and gradients are detached). use_tensor : bool, optional If true use tensorized computation for the distance matrix which can cause memory issues for large datasets. Default is False and the parameter is used only for the 'cityblock' and 'minkowski' metrics. nx : Backend, optional Backend to perform computations on. If omitted, the backend defaults to that of `x1`. Returns ------- M : array-like, shape (`n1`, `n2`) distance matrix computed with given metric """ if nx is None: nx = get_backend(x1, x2) if x2 is None: x2 = x1 if backend == "scipy": # force scipy backend with cdist function x1 = nx.to_numpy(x1) x2 = nx.to_numpy(x2) if isinstance(metric, str) and metric.endswith("minkowski"): return nx.from_numpy(cdist(x1, x2, metric=metric, p=p, w=w)) if w is not None: return nx.from_numpy(cdist(x1, x2, metric=metric, w=w)) return nx.from_numpy(cdist(x1, x2, metric=metric)) elif metric == "sqeuclidean": return euclidean_distances(x1, x2, squared=True, nx=nx) elif metric == "euclidean": return euclidean_distances(x1, x2, squared=False, nx=nx) elif metric == "cityblock": if use_tensor: return nx.sum(nx.abs(x1[:, None, :] - x2[None, :, :]), axis=2) else: M = 0.0 for i in range(x1.shape[1]): M += nx.abs(x1[:, i][:, None] - x2[:, i][None, :]) return M elif metric == "minkowski": if w is None: if use_tensor: return nx.power( nx.sum( nx.power(nx.abs(x1[:, None, :] - x2[None, :, :]), p), axis=2 ), 1 / p, ) else: M = 0.0 for i in range(x1.shape[1]): M += nx.abs(x1[:, i][:, None] - x2[:, i][None, :]) ** p return M ** (1 / p) else: if use_tensor: return nx.power( nx.sum( w[None, None, :] * nx.power(nx.abs(x1[:, None, :] - x2[None, :, :]), p), axis=2, ), 1 / p, ) else: M = 0.0 for i in range(x1.shape[1]): M += w[i] * nx.abs(x1[:, i][:, None] - x2[:, i][None, :]) ** p return M ** (1 / p) elif metric == "cosine": nx1 = nx.sqrt(nx.einsum("ij,ij->i", x1, x1)) nx2 = nx.sqrt(nx.einsum("ij,ij->i", x2, x2)) return 1.0 - (nx.dot(x1, nx.transpose(x2)) / nx1[:, None] / nx2[None, :]) elif metric == "correlation": x1 = x1 - nx.mean(x1, axis=1)[:, None] x2 = x2 - nx.mean(x2, axis=1)[:, None] nx1 = nx.sqrt(nx.einsum("ij,ij->i", x1, x1)) nx2 = nx.sqrt(nx.einsum("ij,ij->i", x2, x2)) return 1.0 - (nx.dot(x1, nx.transpose(x2)) / nx1[:, None] / nx2[None, :]) else: if not get_backend(x1, x2).__name__ == "numpy": raise NotImplementedError() else: if isinstance(metric, str) and metric.endswith("minkowski"): return cdist(x1, x2, metric=metric, p=p, w=w) # Only pass w parameter for metrics that support it # According to SciPy docs, only 'minkowski' and 'wminkowski' support w if w is not None and metric in ["minkowski", "wminkowski"]: return cdist(x1, x2, metric=metric, w=w) return cdist(x1, x2, metric=metric) def dist0(n, method="lin_square"): r"""Compute standard cost matrices of size (`n`, `n`) for OT problems Parameters ---------- n : int Size of the cost matrix. method : str, optional Type of loss matrix chosen from: * 'lin_square' : linear sampling between 0 and `n-1`, quadratic loss Returns ------- M : ndarray, shape (`n1`, `n2`) Distance matrix computed with given metric. """ res = 0 if method == "lin_square": x = np.arange(n, dtype=np.float64).reshape((n, 1)) res = dist(x, x) return res def cost_normalization(C, norm=None, return_value=False, value=None): r"""Apply normalization to the loss matrix Parameters ---------- C : ndarray, shape (n1, n2) The cost matrix to normalize. norm : str Type of normalization from 'median', 'max', 'log', 'loglog'. Any other value do not normalize. Returns ------- C : ndarray, shape (`n1`, `n2`) The input cost matrix normalized according to given norm. """ nx = get_backend(C) if norm is None: pass elif norm == "median": if value is None: value = nx.median(C) C /= value elif norm == "max": if value is None: value = nx.max(C) C /= float(value) elif norm == "log": C = nx.log(1 + C) elif norm == "loglog": C = nx.log(1 + nx.log(1 + C)) else: raise ValueError( "Norm %s is not a valid option.\n" "Valid options are:\n" "median, max, log, loglog" % norm ) if return_value: return C, value else: return C def dots(*args): r"""dots function for multiple matrix multiply""" nx = get_backend(*args) return reduce(nx.dot, args) def is_all_finite(*args): r"""Tests element-wise for finiteness in all arguments.""" nx = get_backend(*args) return all(not nx.any(~nx.isfinite(arg)) for arg in args) def label_normalization(y, start=0, nx=None): r"""Transform labels to start at a given value Parameters ---------- y : array-like, shape (n, ) The vector of labels to be normalized. start : int Desired value for the smallest label in :math:`\mathbf{y}` (default=0) nx : Backend, optional Backend to perform computations on. If omitted, the backend defaults to that of `y`. Returns ------- y : array-like, shape (`n1`, ) The input vector of labels normalized according to given start value. """ if nx is None: nx = get_backend(y) diff = nx.min(y) - start return y if diff == 0 else (y - diff) def labels_to_masks(y, type_as=None, nx=None): r"""Transforms (n_samples,) vector of labels into a (n_samples, n_labels) matrix of masks. Parameters ---------- y : array-like, shape (n_samples, ) The vector of labels. type_as : array_like Array of the same type of the expected output. nx : Backend, optional Backend to perform computations on. If omitted, the backend defaults to that of `y`. Returns ------- masks : array-like, shape (n_samples, n_labels) The (n_samples, n_labels) matrix of label masks. """ if nx is None: nx = get_backend(y) if type_as is None: type_as = y labels_u, labels_idx = nx.unique(y, return_inverse=True) n_labels = labels_u.shape[0] masks = nx.eye(n_labels, type_as=type_as)[labels_idx] return masks def parmap(f, X, nprocs="default"): r"""parallel map for multiprocessing. The function has been deprecated and only performs a regular map. """ return list(map(f, X)) def check_params(**kwargs): r"""check_params: check whether some parameters are missing""" missing_params = [] check = True for param in kwargs: if kwargs[param] is None: missing_params.append(param) if len(missing_params) > 0: print("POT - Warning: following necessary parameters are missing") for p in missing_params: print("\n", p) check = False return check def check_random_state(seed): r"""Turn `seed` into a np.random.RandomState instance Parameters ---------- seed : None | int | instance of RandomState If `seed` is None, return the RandomState singleton used by np.random. If `seed` is an int, return a new RandomState instance seeded with `seed`. If `seed` is already a RandomState instance, return it. Otherwise raise ValueError. """ if seed is None or seed is np.random: return np.random.mtrand._rand if isinstance(seed, (int, np.integer)): return np.random.RandomState(seed) if isinstance(seed, np.random.RandomState): return seed raise ValueError( "{} cannot be used to seed a numpy.random.RandomState instance".format(seed) ) def get_coordinate_circle(x): r"""For :math:`x\in S^1 \subset \mathbb{R}^2`, returns the coordinates in turn (in [0,1[). .. math:: u = \frac{\pi + \mathrm{atan2}(-x_2,-x_1)}{2\pi} Parameters ---------- x: ndarray, shape (n, 2) Samples on the circle with ambient coordinates Returns ------- x_t: ndarray, shape (n,) Coordinates on [0,1[ Examples -------- >>> u = np.array([[0.2,0.5,0.8]]) * (2 * np.pi) >>> x1, y1 = np.cos(u), np.sin(u) >>> x = np.concatenate([x1, y1]).T >>> get_coordinate_circle(x) array([0.2, 0.5, 0.8]) """ nx = get_backend(x) x_t = (nx.atan2(-x[:, 1], -x[:, 0]) + np.pi) / (2 * np.pi) return x_t def reduce_lazytensor(a, func, axis=None, nx=None, batch_size=100): """Reduce a LazyTensor along an axis with function fun using batches. When axis=None, reduce the LazyTensor to a scalar as a sum of fun over batches taken along dim. .. warning:: This function works for tensor of any order but the reduction can be done only along the first two axis (or global). Also, in order to work, it requires that the slice of size `batch_size` along the axis to reduce (or axis 0 if `axis=None`) is can be computed and fits in memory. Parameters ---------- a : LazyTensor LazyTensor to reduce func : callable Function to apply to the LazyTensor axis : int, optional Axis along which to reduce the LazyTensor. If None, reduce the LazyTensor to a scalar as a sum of fun over batches taken along axis 0. If 0 or 1 reduce the LazyTensor to a vector/matrix as a sum of fun over batches taken along axis. nx : Backend, optional Backend to use for the reduction batch_size : int, optional Size of the batches to use for the reduction (default=100) Returns ------- res : array-like Result of the reduction """ if nx is None: nx = get_backend(a[0:1]) if axis is None: res = 0.0 for i in range(0, a.shape[0], batch_size): res += func(a[i : i + batch_size]) return res elif axis == 0: res = nx.zeros(a.shape[1:], type_as=a[0]) if nx.__name__ in ["jax", "tf"]: lst = [] for j in range(0, a.shape[1], batch_size): lst.append(func(a[:, j : j + batch_size], 0)) return nx.concatenate(lst, axis=0) else: for j in range(0, a.shape[1], batch_size): res[j : j + batch_size] = func(a[:, j : j + batch_size], axis=0) return res elif axis == 1: if len(a.shape) == 2: shape = a.shape[0] else: shape = (a.shape[0], *a.shape[2:]) res = nx.zeros(shape, type_as=a[0]) if nx.__name__ in ["jax", "tf"]: lst = [] for i in range(0, a.shape[0], batch_size): lst.append(func(a[i : i + batch_size], 1)) return nx.concatenate(lst, axis=0) else: for i in range(0, a.shape[0], batch_size): res[i : i + batch_size] = func(a[i : i + batch_size], axis=1) return res else: raise (NotImplementedError("Only axis=None, 0 or 1 is implemented for now.")) def get_lowrank_lazytensor(Q, R, d=None, nx=None): """Get a low rank LazyTensor T=Q@R^T or T=Q@diag(d)@R^T Parameters ---------- Q : ndarray, shape (n, r) First factor of the lowrank tensor R : ndarray, shape (m, r) Second factor of the lowrank tensor d : ndarray, shape (r,), optional Diagonal of the lowrank tensor nx : Backend, optional Backend to use for the reduction Returns ------- T : LazyTensor Lowrank tensor T=Q@R^T or T=Q@diag(d)@R^T """ if nx is None: nx = get_backend(Q, R, d) shape = (Q.shape[0], R.shape[0]) if d is None: def func(i, j, Q, R): return nx.dot(Q[i], R[j].T) T = LazyTensor(shape, func, Q=Q, R=R) else: def func(i, j, Q, R, d): return nx.dot(Q[i] * d[None, :], R[j].T) T = LazyTensor(shape, func, Q=Q, R=R, d=d) return T def get_parameter_pair(parameter): r"""Extract a pair of parameters from a given parameter Used in unbalanced OT and COOT solvers to handle marginal regularization and entropic regularization. Parameters ---------- parameter : float or indexable object nx : backend object Returns ------- param_1 : float param_2 : float """ if isinstance(parameter, float) or isinstance(parameter, int): param_1, param_2 = parameter, parameter elif len(parameter) == 1: param_1, param_2 = parameter[0], parameter[0] else: if len(parameter) > 2: raise ValueError( "Parameter must be either a scalar, \ or an indexable object of length 1 or 2." ) else: param_1, param_2 = parameter[0], parameter[1] return param_1, param_2 class deprecated(object): r"""Decorator to mark a function or class as deprecated. deprecated class from scikit-learn package https://github.com/scikit-learn/scikit-learn/blob/master/sklearn/utils/deprecation.py Issue a warning when the function is called/the class is instantiated and adds a warning to the docstring. The optional extra argument will be appended to the deprecation message and the docstring. .. note:: To use this with the default value for extra, use empty parentheses: >>> from ot.deprecation import deprecated # doctest: +SKIP >>> @deprecated() # doctest: +SKIP ... def some_function(): pass # doctest: +SKIP Parameters ---------- extra : str To be added to the deprecation messages. """ # Adapted from http://wiki.python.org/moin/PythonDecoratorLibrary, # but with many changes. def __init__(self, extra=""): self.extra = extra def __call__(self, obj): r"""Call method Parameters ---------- obj : object """ if isinstance(obj, type): return self._decorate_class(obj) else: return self._decorate_fun(obj) def _decorate_class(self, cls): msg = "Class %s is deprecated" % cls.__name__ if self.extra: msg += "; %s" % self.extra # FIXME: we should probably reset __new__ for full generality init = cls.__init__ def wrapped(*args, **kwargs): warnings.warn(msg, category=DeprecationWarning) return init(*args, **kwargs) cls.__init__ = wrapped wrapped.__name__ = "__init__" wrapped.__doc__ = self._update_doc(init.__doc__) wrapped.deprecated_original = init return cls def _decorate_fun(self, fun): r"""Decorate function fun""" msg = "Function %s is deprecated" % fun.__name__ if self.extra: msg += "; %s" % self.extra def wrapped(*args, **kwargs): warnings.warn(msg, category=DeprecationWarning) return fun(*args, **kwargs) wrapped.__name__ = fun.__name__ wrapped.__dict__ = fun.__dict__ wrapped.__doc__ = self._update_doc(fun.__doc__) return wrapped def _update_doc(self, olddoc): newdoc = "DEPRECATED" if self.extra: newdoc = "%s: %s" % (newdoc, self.extra) if olddoc: newdoc = "%s\n\n%s" % (newdoc, olddoc) return newdoc def _is_deprecated(func): r"""Helper to check if func is wrapped by our deprecated decorator""" if sys.version_info < (3, 5): raise NotImplementedError("This is only available for python3.5 or above") closures = getattr(func, "__closure__", []) if closures is None: closures = [] is_deprecated = "deprecated" in "".join( [c.cell_contents for c in closures if isinstance(c.cell_contents, str)] ) return is_deprecated class BaseEstimator(object): r"""Base class for most objects in POT Code adapted from sklearn BaseEstimator class Notes ----- All estimators should specify all the parameters that can be set at the class level in their ``__init__`` as explicit keyword arguments (no ``*args`` or ``**kwargs``). """ nx: Backend = None def _get_backend(self, *arrays): nx = get_backend(*[input_ for input_ in arrays if input_ is not None]) if nx.__name__ in ("tf",): raise TypeError("Domain adaptation does not support TF backend.") self.nx = nx return nx @classmethod def _get_param_names(cls): r"""Get parameter names for the estimator""" # fetch the constructor or the original constructor before # deprecation wrapping if any init = getattr(cls.__init__, "deprecated_original", cls.__init__) if init is object.__init__: # No explicit constructor to introspect return [] # introspect the constructor arguments to find the model parameters # to represent init_signature = signature(init) # Consider the constructor parameters excluding 'self' parameters = [ p for p in init_signature.parameters.values() if p.name != "self" and p.kind != p.VAR_KEYWORD ] for p in parameters: if p.kind == p.VAR_POSITIONAL: raise RuntimeError( "POT estimators should always " "specify their parameters in the signature" " of their __init__ (no varargs)." " %s with constructor %s doesn't " " follow this convention." % (cls, init_signature) ) # Extract and sort argument names excluding 'self' return sorted([p.name for p in parameters]) def get_params(self, deep=True): r"""Get parameters for this estimator. Parameters ---------- deep : bool, optional If True, will return the parameters for this estimator and contained subobjects that are estimators. Returns ------- params : mapping of string to any Parameter names mapped to their values. """ out = dict() for key in self._get_param_names(): # We need deprecation warnings to always be on in order to # catch deprecated param values. # This is set in utils/__init__.py but it gets overwritten # when running under python3 somehow. warnings.simplefilter("always", DeprecationWarning) try: with warnings.catch_warnings(record=True) as w: value = getattr(self, key, None) if len(w) and isinstance(w[0].category, DeprecationWarning): # if the parameter is deprecated, don't show it continue finally: warnings.filters.pop(0) # XXX: should we rather test if instance of estimator? if deep and hasattr(value, "get_params"): deep_items = value.get_params().items() out.update((key + "__" + k, val) for k, val in deep_items) out[key] = value return out def set_params(self, **params): r"""Set the parameters of this estimator. The method works on simple estimators as well as on nested objects (such as pipelines). The latter have parameters of the form ``__`` so that it's possible to update each component of a nested object. Returns ------- self """ if not params: # Simple optimisation to gain speed (inspect is slow) return self valid_params = self.get_params(deep=True) # for key, value in iteritems(params): for key, value in params.items(): split = key.split("__", 1) if len(split) > 1: # nested objects case name, sub_name = split if name not in valid_params: raise ValueError( "Invalid parameter %s for estimator %s. " "Check the list of available parameters " "with `estimator.get_params().keys()`." % (name, self) ) sub_object = valid_params[name] sub_object.set_params(**{sub_name: value}) else: # simple objects case if key not in valid_params: raise ValueError( "Invalid parameter %s for estimator %s. " "Check the list of available parameters " "with `estimator.get_params().keys()`." % (key, self.__class__.__name__) ) setattr(self, key, value) return self class UndefinedParameter(Exception): r""" Aim at raising an Exception when a undefined parameter is called """ pass class OTResult: """Base class for OT results. Parameters ---------- potentials : tuple of array-like, shape (`n1`, `n2`) Dual potentials, i.e. Lagrange multipliers for the marginal constraints. This pair of arrays has the same shape, numerical type and properties as the input weights "a" and "b". value : float, array-like Full transport cost, including possible regularization terms and quadratic term for Gromov Wasserstein solutions. value_linear : float, array-like The linear part of the transport cost, i.e. the product between the transport plan and the cost. value_quad : float, array-like The quadratic part of the transport cost for Gromov-Wasserstein solutions. plan : array-like, shape (`n1`, `n2`) Transport plan, encoded as a dense array. log : dict Dictionary containing potential information about the solver. backend : Backend Backend used to compute the results. sparse_plan : array-like, shape (`n1`, `n2`) Transport plan, encoded as a sparse array. lazy_plan : LazyTensor Transport plan, encoded as a symbolic POT or KeOps LazyTensor. status : int or str Status of the solver. batch_size : int Batch size used to compute the results/marginals for LazyTensor. Attributes ---------- potentials : tuple of array-like, shape (`n1`, `n2`) Dual potentials, i.e. Lagrange multipliers for the marginal constraints. This pair of arrays has the same shape, numerical type and properties as the input weights "a" and "b". potential_a : array-like, shape (`n1`,) First dual potential, associated to the "source" measure "a". potential_b : array-like, shape (`n2`,) Second dual potential, associated to the "target" measure "b". value : float, array-like Full transport cost, including possible regularization terms and quadratic term for Gromov Wasserstein solutions. value_linear : float, array-like The linear part of the transport cost, i.e. the product between the transport plan and the cost. value_quad : float, array-like The quadratic part of the transport cost for Gromov-Wasserstein solutions. plan : array-like, shape (`n1`, `n2`) Transport plan, encoded as a dense array. sparse_plan : array-like, shape (`n1`, `n2`) Transport plan, encoded as a sparse array. lazy_plan : LazyTensor Transport plan, encoded as a symbolic POT or KeOps LazyTensor. marginals : tuple of array-like, shape (`n1`,), (`n2`,) Marginals of the transport plan: should be very close to "a" and "b" for balanced OT. marginal_a : array-like, shape (`n1`,) Marginal of the transport plan for the "source" measure "a". marginal_b : array-like, shape (`n2`,) Marginal of the transport plan for the "target" measure "b". """ def __init__( self, potentials=None, value=None, value_linear=None, value_quad=None, plan=None, log=None, backend=None, sparse_plan=None, lazy_plan=None, status=None, batch_size=100, ): self._potentials = potentials self._value = value self._value_linear = value_linear self._value_quad = value_quad self._plan = plan self._log = log self._sparse_plan = sparse_plan self._lazy_plan = lazy_plan self._backend = backend if backend is not None else NumpyBackend() self._status = status self._batch_size = batch_size # I assume that other solvers may return directly # some primal objects? # In the code below, let's define the main quantities # that may be of interest to users. # An OT solver returns an object that inherits from OTResult # (e.g. SinkhornOTResult) and implements the relevant # methods (e.g. "plan" and "lazy_plan" but not "sparse_plan", etc.). # log is a dictionary containing potential information about the solver # Dual potentials -------------------------------------------- def __repr__(self): s = "OTResult(" if self._value is not None: s += "value={},".format(self._value) if self._value_linear is not None: s += "value_linear={},".format(self._value_linear) if self._plan is not None: s += "plan={}(shape={}),".format( self._plan.__class__.__name__, self._plan.shape ) if self._lazy_plan is not None: s += "lazy_plan={}(shape={}),".format( self._lazy_plan.__class__.__name__, self._lazy_plan.shape ) if s[-1] != "(": s = s[:-1] + ")" else: s = s + ")" return s @property def potentials(self): """Dual potentials, i.e. Lagrange multipliers for the marginal constraints. This pair of arrays has the same shape, numerical type and properties as the input weights "a" and "b". """ return self._potentials @property def potential_a(self): """First dual potential, associated to the "source" measure "a".""" if self._potentials is not None: return self._potentials[0] else: return None @property def potential_b(self): """Second dual potential, associated to the "target" measure "b".""" if self._potentials is not None: return self._potentials[1] else: return None # Transport plan ------------------------------------------- @property def plan(self): """Transport plan, encoded as a dense array.""" # N.B.: We may catch out-of-memory errors and suggest # the use of lazy_plan or sparse_plan when appropriate. if self._plan is not None: return self._plan elif self._sparse_plan is not None: return self._backend.todense(self._sparse_plan) else: return None @property def sparse_plan(self): """Transport plan, encoded as a sparse array.""" if self._sparse_plan is not None: return self._sparse_plan elif self._plan is not None: return self._backend.tocsr(self._plan) else: return None @property def lazy_plan(self): """Transport plan, encoded as a symbolic KeOps LazyTensor.""" return self._lazy_plan # Loss values -------------------------------- @property def value(self): """Full transport cost, including possible regularization terms and quadratic term for Gromov Wasserstein solutions.""" return self._value @property def value_linear(self): """The "minimal" transport cost, i.e. the product between the transport plan and the cost.""" return self._value_linear @property def value_quad(self): """The quadratic part of the transport cost for Gromov-Wasserstein solutions.""" return self._value_quad # Marginal constraints ------------------------- @property def marginals(self): """Marginals of the transport plan: should be very close to "a" and "b" for balanced OT.""" if self._plan is not None: return self.marginal_a, self.marginal_b else: return None @property def marginal_a(self): """First marginal of the transport plan, with the same shape as "a".""" if self._plan is not None: return self._backend.sum(self._plan, 1) elif self._lazy_plan is not None: lp = self._lazy_plan bs = self._batch_size nx = self._backend return reduce_lazytensor(lp, nx.sum, axis=1, nx=nx, batch_size=bs) else: return None @property def marginal_b(self): """Second marginal of the transport plan, with the same shape as "b".""" if self._plan is not None: return self._backend.sum(self._plan, 0) elif self._lazy_plan is not None: lp = self._lazy_plan bs = self._batch_size nx = self._backend return reduce_lazytensor(lp, nx.sum, axis=0, nx=nx, batch_size=bs) else: return None @property def status(self): """Optimization status of the solver.""" return self._status @property def log(self): """Dictionary containing potential information about the solver.""" return self._log # Barycentric mappings ------------------------- # Return the displacement vectors as an array # that has the same shape as "xa"/"xb" (for samples) # or "a"/"b" * D (for images)? @property def a_to_b(self): """Displacement vectors from the first to the second measure.""" raise NotImplementedError() @property def b_to_a(self): """Displacement vectors from the second to the first measure.""" raise NotImplementedError() # # Wasserstein barycenters ---------------------- # @property # def masses(self): # """Masses for the Wasserstein barycenter.""" # raise NotImplementedError() # @property # def samples(self): # """Sample locations for the Wasserstein barycenter.""" # raise NotImplementedError() # Miscellaneous -------------------------------- @property def citation(self): """Appropriate citation(s) for this result, in plain text and BibTex formats.""" # The string below refers to the POT library: # successor methods may concatenate the relevant references # to the original definitions, solvers and underlying numerical backends. return """POT library: POT Python Optimal Transport library, Journal of Machine Learning Research, 22(78):1−8, 2021. Website: https://pythonot.github.io/ Rémi Flamary, Nicolas Courty, Alexandre Gramfort, Mokhtar Z. Alaya, Aurélie Boisbunon, Stanislas Chambon, Laetitia Chapel, Adrien Corenflos, Kilian Fatras, Nemo Fournier, Léo Gautheron, Nathalie T.H. Gayraud, Hicham Janati, Alain Rakotomamonjy, Ievgen Redko, Antoine Rolet, Antony Schutz, Vivien Seguy, Danica J. Sutherland, Romain Tavenard, Alexander Tong, Titouan Vayer; @article{flamary2021pot, author = {R{\'e}mi Flamary and Nicolas Courty and Alexandre Gramfort and Mokhtar Z. Alaya and Aur{\'e}lie Boisbunon and Stanislas Chambon and Laetitia Chapel and Adrien Corenflos and Kilian Fatras and Nemo Fournier and L{\'e}o Gautheron and Nathalie T.H. Gayraud and Hicham Janati and Alain Rakotomamonjy and Ievgen Redko and Antoine Rolet and Antony Schutz and Vivien Seguy and Danica J. Sutherland and Romain Tavenard and Alexander Tong and Titouan Vayer}, title = {{POT}: {Python} {Optimal} {Transport}}, journal = {Journal of Machine Learning Research}, year = {2021}, volume = {22}, number = {78}, pages = {1-8}, url = {http://jmlr.org/papers/v22/20-451.html} } """ class BaryResult: """Base class for OT barycenter results. Parameters ---------- X : array-like, shape (`n`, `d`) Barycenter features. C: array-like, shape (`n`, `n`) Barycenter structure for Gromov Wasserstein solutions. b : array-like, shape (`n`,) Barycenter weights. value : float, array-like Full transport cost, including possible regularization terms and quadratic term for Gromov Wasserstein solutions. value_linear : float, array-like The linear part of the transport cost, i.e. the product between the transport plan and the cost. value_quad : float, array-like The quadratic part of the transport cost for Gromov-Wasserstein solutions. log : dict Dictionary containing potential information about the solver. list_res: list of OTResult List of results for the individual OT matching with input distributions considered as sources and the learned barycenter distribution as target. status : int or str Status of the solver. Attributes ---------- X : array-like, shape (`n`, `d`) Barycenter features. C: array-like, shape (`n`, `n`) Barycenter structure for Gromov Wasserstein solutions. b : array-like, shape (`n`,) Barycenter weights. value : float, array-like Full transport cost, including possible regularization terms and quadratic term for Gromov Wasserstein solutions. value_linear : float, array-like The linear part of the transport cost, i.e. the product between the transport plan and the cost. value_quad : float, array-like The quadratic part of the transport cost for Gromov-Wasserstein solutions. log : dict Dictionary containing potential information about the solver. list_res: list of OTResult List of results for the individual OT matching. status : int or str Status of the solver. backend : Backend Backend used to compute the results. """ def __init__( self, X=None, C=None, b=None, value=None, value_linear=None, value_quad=None, log=None, list_res=None, status=None, backend=None, ): self._X = X self._C = C self._b = b self._value = value self._value_linear = value_linear self._value_quad = value_quad self._log = log self._list_res = list_res self._status = status self._backend = backend if backend is not None else NumpyBackend() def __repr__(self): s = "BaryResult(" if self._value is not None: s += "value={},".format(self._value) if self._value_linear is not None: s += "value_linear={},".format(self._value_linear) if self._X is not None: s += "X={}(shape={}),".format(self._X.__class__.__name__, self._X.shape) if self._C is not None: s += "C={}(shape={}),".format(self._C.__class__.__name__, self._C.shape) if self._b is not None: s += "b={}(shape={}),".format(self._b.__class__.__name__, self._b.shape) if s[-1] != "(": s = s[:-1] + ")" else: s = s + ")" return s # Barycerters -------------------------------- @property def X(self): """Barycenter features.""" return self._X @property def C(self): """Barycenter structure for Gromov Wasserstein solutions.""" return self._C @property def b(self): """Barycenter weights.""" return self._b # Loss values -------------------------------- @property def value(self): """Full transport cost, including possible regularization terms and quadratic term for Gromov Wasserstein solutions.""" return self._value @property def value_linear(self): """The "minimal" transport cost, i.e. the product between the transport plan and the cost.""" return self._value_linear @property def value_quad(self): """The quadratic part of the transport cost for Gromov-Wasserstein solutions.""" return self._value_quad # List of OTResult objects ------------------------- @property def list_res(self): """List of results for the individual OT matching.""" return self._list_res @property def status(self): """Optimization status of the solver.""" return self._status @property def log(self): """Dictionary containing potential information about the solver.""" return self._log # Miscellaneous -------------------------------- @property def citation(self): """Appropriate citation(s) for this result, in plain text and BibTex formats.""" # The string below refers to the POT library: # successor methods may concatenate the relevant references # to the original definitions, solvers and underlying numerical backends. return """POT library: POT Python Optimal Transport library, Journal of Machine Learning Research, 22(78):1−8, 2021. Website: https://pythonot.github.io/ Rémi Flamary, Nicolas Courty, Alexandre Gramfort, Mokhtar Z. Alaya, Aurélie Boisbunon, Stanislas Chambon, Laetitia Chapel, Adrien Corenflos, Kilian Fatras, Nemo Fournier, Léo Gautheron, Nathalie T.H. Gayraud, Hicham Janati, Alain Rakotomamonjy, Ievgen Redko, Antoine Rolet, Antony Schutz, Vivien Seguy, Danica J. Sutherland, Romain Tavenard, Alexander Tong, Titouan Vayer; @article{flamary2021pot, author = {R{\'e}mi Flamary and Nicolas Courty and Alexandre Gramfort and Mokhtar Z. Alaya and Aur{\'e}lie Boisbunon and Stanislas Chambon and Laetitia Chapel and Adrien Corenflos and Kilian Fatras and Nemo Fournier and L{\'e}o Gautheron and Nathalie T.H. Gayraud and Hicham Janati and Alain Rakotomamonjy and Ievgen Redko and Antoine Rolet and Antony Schutz and Vivien Seguy and Danica J. Sutherland and Romain Tavenard and Alexander Tong and Titouan Vayer}, title = {{POT}: {Python} {Optimal} {Transport}}, journal = {Journal of Machine Learning Research}, year = {2021}, volume = {22}, number = {78}, pages = {1-8}, url = {http://jmlr.org/papers/v22/20-451.html} } """ class LazyTensor(object): """A lazy tensor is a tensor that is not stored in memory. Instead, it is defined by a function that computes its values on the fly from slices. Parameters ---------- shape : tuple shape of the tensor getitem : callable function that computes the values of the indices/slices and tensors as arguments kwargs : dict named arguments for the function, those names will be used as attributed of the LazyTensor object Examples -------- >>> import numpy as np >>> v = np.arange(5) >>> def getitem(i,j, v): ... return v[i,None]+v[None,j] >>> T = LazyTensor((5,5),getitem, v=v) >>> T[1,2] array([3]) >>> T[1,:] array([[1, 2, 3, 4, 5]]) >>> T[:] array([[0, 1, 2, 3, 4], [1, 2, 3, 4, 5], [2, 3, 4, 5, 6], [3, 4, 5, 6, 7], [4, 5, 6, 7, 8]]) """ def __init__(self, shape, getitem, **kwargs): self._getitem = getitem self.shape = shape self.ndim = len(shape) self.kwargs = kwargs # set attributes for named arguments/arrays for key, value in kwargs.items(): setattr(self, key, value) def __getitem__(self, key): k = [] if isinstance(key, int) or isinstance(key, slice): k.append(key) for i in range(self.ndim - 1): k.append(slice(None)) elif isinstance(key, tuple): k = list(key) for i in range(self.ndim - len(key)): k.append(slice(None)) else: raise NotImplementedError( "Only integer, slice, and tuple indexing is supported" ) return self._getitem(*k, **self.kwargs) def __repr__(self): return "LazyTensor(shape={},attributes=({}))".format( self.shape, ",".join(self.kwargs.keys()) ) def proj_SDP(S, nx=None, vmin=0.0): """ Project a symmetric matrix onto the space of symmetric matrices with eigenvalues larger or equal to `vmin`. Parameters ---------- S : array_like (n, d, d) or (d, d) The input symmetric matrix or matrices. nx : module, optional The numerical backend module to use. If not provided, the backend will be fetched from the input matrix `S`. vmin : float, optional The minimum value for the eigenvalues. Eigenvalues below this value will be clipped to vmin. .. note:: This function is backend-compatible and will work on arrays from all compatible backends. Returns ------- P : ndarray (n, d, d) or (d, d) The projected symmetric positive definite matrix. """ if nx is None: nx = get_backend(S) w, P = nx.eigh(S) w = nx.clip(w, vmin, None) if len(S.shape) == 2: # input was (d, d) return P @ nx.diag(w) @ P.T else: # input was (n, d, d): broadcasting Q = nx.einsum("ijk,ik->ijk", P, w) # Q[i] = P[i] @ diag(w[i]) # R[i] = Q[i] @ P[i].T return nx.einsum("ijk,ikl->ijl", Q, nx.transpose(P, (0, 2, 1))) def exp_bures(Sigma, S, nx=None): r""" Exponential map in Bures-Wasserstein space at Sigma: .. math:: \exp_\Sigma(S) = (I_d+S)\Sigma(I_d+S). Parameters ---------- Sigma : array-like (d,d) SPD matrix S : array-like (d,d) Symmetric matrix nx : module, optional The numerical backend module to use. If not provided, the backend will be fetched from the input matrices `Sigma, S`. Returns ------- P : array-like (d,d) SPD matrix obtained as the exponential map of S at Sigma """ if nx is None: nx = get_backend(Sigma, S) d = S.shape[-1] Id = nx.eye(d, type_as=S) C = Id + S return nx.einsum("ij,jk,kl -> il", C, Sigma, C) def check_number_threads(numThreads): """Checks whether or not the requested number of threads has a valid value. Parameters ---------- numThreads : int or str The requested number of threads, should either be a strictly positive integer or "max" or None Returns ------- numThreads : int Corrected number of threads """ if (numThreads is None) or ( isinstance(numThreads, str) and numThreads.lower() == "max" ): return -1 if (not isinstance(numThreads, int)) or numThreads < 1: raise ValueError( 'numThreads should either be "max" or a strictly positive integer' ) return numThreads class DataScaler: r"""Backend-aware data scaler with sklearn-compatible API. Fit normalization statistics on a single array or on the concatenation of multiple arrays (joint fitting), then apply the same fixed transform to any array. Supports NumPy, PyTorch, JAX, and TensorFlow backends via POT's backend abstraction. Parameters ---------- norm : str, optional Normalization method. One of: - ``'standard'`` (default) : zero mean, unit variance per feature - ``'minmax'`` : scale each feature to [0, 1] - ``'l2'`` : unit L2-norm per sample (row-wise, stateless) Attributes ---------- norm : str The normalization method. mean_ : array-like Per-feature means (only for ``norm='standard'``). std_ : array-like Per-feature standard deviations (only for ``norm='standard'``). min_ : array-like Per-feature minimums (only for ``norm='minmax'``). max_ : array-like Per-feature maximums (only for ``norm='minmax'``). Examples -------- >>> import numpy as np >>> from ot.utils import DataScaler >>> X_s = np.array([[1.0, 100.0], [2.0, 200.0]]) >>> X_t = np.array([[3.0, 300.0], [4.0, 400.0]]) >>> scaler = DataScaler(norm='standard').fit([X_s, X_t]) >>> X_s_scaled = scaler.transform(X_s) """ _VALID_NORMS = ("standard", "minmax", "l2") def __init__(self, norm="standard"): if norm not in self._VALID_NORMS: raise ValueError( "Invalid norm '{}'. Expected one of: {}".format(norm, self._VALID_NORMS) ) self.norm = norm self.mean_ = None self.std_ = None self.min_ = None self.max_ = None self._nx = None self._fitted = False def fit(self, X): r"""Compute normalization statistics from one array or a list of arrays. When given a list, arrays are concatenated along axis 0 before computing statistics (joint fitting). Parameters ---------- X : array-like or list of array-like Data to fit on. If a list, arrays must have the same number of features (columns). Returns ------- self : DataScaler """ if isinstance(X, (list, tuple)): if len(X) == 0: raise ValueError("Cannot fit on empty list.") nx = get_backend(*X) X_concat = nx.concatenate(list(X), axis=0) else: nx = get_backend(X) X_concat = X self._nx = nx if self.norm == "l2": self._fitted = True return self if self.norm == "standard": self.mean_ = nx.mean(X_concat, axis=0) self.std_ = nx.std(X_concat, axis=0) zero_var = self.std_ == 0 if nx.any(zero_var): warnings.warn( "Zero variance detected in one or more feature(s). " "Those columns will not be scaled.", RuntimeWarning, ) self.std_ = nx.where( zero_var, nx.ones(self.std_.shape, type_as=self.std_), self.std_, ) elif self.norm == "minmax": self.min_ = nx.min(X_concat, axis=0) self.max_ = nx.max(X_concat, axis=0) zero_range = self.max_ == self.min_ if nx.any(zero_range): warnings.warn( "Zero range detected in one or more feature(s). " "Those columns will not be scaled.", RuntimeWarning, ) self.max_ = nx.where( zero_range, self.min_ + 1.0, self.max_, ) self._fitted = True return self def _apply_transform(self, X): r"""Apply the fitted transform to one array or a list of arrays. Internal helper shared by :meth:`transform` and :meth:`fit_transform`. Validates that the input's backend matches the one used at fit time. Parameters ---------- X : array-like or list of array-like Returns ------- array-like or list of array-like Transformed data; a list is returned when ``X`` is a list. """ if isinstance(X, (list, tuple)): return [self._apply_transform(x) for x in X] nx = get_backend(X) if self._nx is not None and nx is not self._nx: raise ValueError( "Backend mismatch: DataScaler was fitted with backend '{}' " "but received input with backend '{}'.".format( type(self._nx).__name__, type(nx).__name__ ) ) if self.norm == "standard": return (X - self.mean_) / self.std_ elif self.norm == "minmax": return (X - self.min_) / (self.max_ - self.min_) elif self.norm == "l2": norms = nx.sqrt(nx.sum(X**2, axis=1, keepdims=True)) zero_norm = norms == 0 if nx.any(zero_norm): warnings.warn( "Zero-norm row(s) detected. These will be left unchanged.", RuntimeWarning, ) norms = nx.where( zero_norm, nx.ones(norms.shape, type_as=norms), norms, ) return X / norms def transform(self, X): r"""Apply the fitted transformation to X. Parameters ---------- X : array-like or list of array-like Data to transform. If a list, each element is transformed and returned as a list. Returns ------- X_scaled : array-like or list of array-like Transformed data, same shape and backend as X. If X was a list, returns a list of transformed arrays. """ if self.norm != "l2" and not self._fitted: raise RuntimeError( "DataScaler must be fitted before calling transform() " "for norm='{}'.".format(self.norm) ) return self._apply_transform(X) def fit_transform(self, X): r"""Fit then transform. Parameters ---------- X : array-like or list of array-like Returns ------- X_scaled : array-like or list of array-like If X was a list, returns a list of transformed arrays. """ self.fit(X) return self._apply_transform(X) def apply_scaler(X_s, X_t, scaler=None): r"""Apply a scaler to two arrays. Dispatches based on the type of ``scaler``: - ``None`` : returns inputs unchanged. - Object with a ``.transform()`` method : calls ``scaler.transform()`` on each. - Callable : calls ``scaler()`` on each (covers functions, lambdas, PyTorch transforms, neural network encoders, etc.). Parameters ---------- X_s : array-like Source samples. X_t : array-like Target samples. scaler : None, object with .transform(), or callable, optional Preprocessing to apply. Returns ------- X_s_out : array-like Possibly transformed source samples. X_t_out : array-like Possibly transformed target samples. """ if scaler is None: return X_s, X_t if hasattr(scaler, "transform") and callable(scaler.transform): return scaler.transform(X_s), scaler.transform(X_t) if callable(scaler): return scaler(X_s), scaler(X_t) raise ValueError( "scaler must be None, an object with a .transform() method, " "or a callable. Got type: {}".format(type(scaler).__name__) ) def fun_to_numpy(fun, arr, nx, warn=True): """Convert a function to a numpy function. Parameters ---------- fun : callable The function to convert. arr : array-like The input to test the function. Can be from any backend. nx : Backend The backend to use for the conversion. warn : bool, optional Whether to raise a warning if the function is not compatible with numpy. Default is True. Returns ------- fun_numpy : callable The converted function. """ if arr is None: raise ValueError("arr should not be None to test fun") nx_arr = get_backend(arr) if nx_arr.__name__ != "numpy": arr = nx.to_numpy(arr) try: fun(arr) return fun except BaseException: if warn: warnings.warn( "The callable function should be able to handle numpy arrays, a compatible function is created and comes with overhead" ) def fun_numpy(x): return nx.to_numpy(fun(nx.from_numpy(x))) return fun_numpy def split_sample_ratio( X_a, a=None, ratio=0.5, random_split=False, random_state=None, nx=None ): """Split distribution according to a ratio of weights (using point ordering). Parameters ---------- X_a : array-like, shape (n_samples_a, dim) samples in the source domain a : array-like, shape (dim_a,), optional Samples weights in the source domain (default is uniform) nx : backend, optional Backend for array operations, by default None (auto-detect) ratio : float, optional Ratio of the split, by default 0.5 random_split : bool, optional Whether to split randomly, by default False random_state : int, optional Random state for reproducibility, by default None Returns ------- X_a1 : array-like, shape (n_samples_a1, dim) First half of the samples in the source domain X_a2 : array-like, shape (n_samples_a2, dim) Second half of the samples in the source domain a1 : array-like, shape (dim_a1,) First half of the weights in the source domain a2 : array-like, shape (dim_a2,) Second half of the weights in the source domain sel_a1 : slice, ndarray-like Slice or indexes for the first half of the samples in the source domain sel_a2 : slice, ndarray-like Slice or indexes for the second half of the samples in the source domain """ if ratio < 0 or ratio > 1: raise ValueError("ratio should be in [0, 1]") if nx is None: nx = get_backend(X_a, a) n_a = X_a.shape[0] if a is None: a = nx.ones(n_a, type_as=X_a) / n_a if random_split: if random_state is not None: nx.seed(random_state) perm = nx.randperm(n_a, type_as=X_a) X_a = X_a[perm] a = a[perm] # find the split indices acs = nx.cumsum(a) thr_a = ratio * nx.sum(a) idx_a = nx.searchsorted(acs, thr_a, side="right") # split the samples and weights X_a1, X_a2 = X_a[: idx_a + 1], X_a[idx_a:] # compute weights for each half and adjust the last/first weight to sum to 0.5 a01, a02 = a[:idx_a], a[idx_a + 1 :] v_idx = a[idx_a] a1_1 = nx.maximum(v_idx * nx.detach((thr_a - nx.sum(a01)) / v_idx), 0) a2_0 = nx.maximum(v_idx * nx.detach((nx.sum(a) - thr_a - nx.sum(a02)) / v_idx), 0) # concat mass or remove samples if mass is 0 if a1_1 > 0: a1 = nx.concatenate([a01, nx.reshape(a1_1, (1,))]) if random_split: sel_a1 = perm[: idx_a + 1] else: sel_a1 = slice(0, idx_a + 1) else: X_a1 = X_a[:idx_a] if random_split: sel_a1 = perm[:idx_a] else: sel_a1 = slice(0, idx_a) a1 = a01 if a2_0 > 0: a2 = nx.concatenate([nx.reshape(a2_0, (1,)), a02]) if random_split: sel_a2 = perm[idx_a:] else: sel_a2 = slice(idx_a, n_a) else: X_a2 = X_a[idx_a + 1 :] if random_split: sel_a2 = perm[idx_a + 1 :] else: sel_a2 = slice(idx_a + 1, n_a) a2 = a02 return X_a1, X_a2, a1, a2, sel_a1, sel_a2 python-pot-0.9.7+dfsg/ot/weak.py000066400000000000000000000073031525521106200165140ustar00rootroot00000000000000""" Weak optimal ransport solvers """ # Author: Remi Flamary # # License: MIT License from .backend import get_backend from .optim import cg import numpy as np __all__ = ["weak_optimal_transport"] def weak_optimal_transport( Xa, Xb, a=None, b=None, verbose=False, log=False, G0=None, **kwargs ): r"""Solves the weak optimal transport problem between two empirical distributions .. math:: \gamma = \mathop{\arg \min}_\gamma \quad \sum_i \mathbf{a}_i \left(\mathbf{X^a}_i - \frac{1}{\mathbf{a}_i} \sum_j \gamma_{ij} \mathbf{X^b}_j \right)^2 s.t. \ \gamma \mathbf{1} = \mathbf{a} \gamma^T \mathbf{1} = \mathbf{b} \gamma \geq 0 where : - :math:`X^a` and :math:`X^b` are the sample matrices. - :math:`\mathbf{a}` and :math:`\mathbf{b}` are the sample weights .. note:: This function is backend-compatible and will work on arrays from all compatible backends. But the algorithm uses the C++ CPU backend which can lead to copy overhead on GPU arrays. Uses the conditional gradient algorithm to solve the problem proposed in :ref:`[39] `. Parameters ---------- Xa : (ns,d) array-like, float Source samples Xb : (nt,d) array-like, float Target samples a : (ns,) array-like, float Source histogram (uniform weight if empty list) b : (nt,) array-like, float Target histogram (uniform weight if empty list)) G0 : (ns,nt) array-like, float initial guess (default is indep joint density) numItermax : int, optional Max number of iterations numItermaxEmd : int, optional Max number of iterations for emd stopThr : float, optional Stop threshold on the relative variation (>0) stopThr2 : float, optional Stop threshold on the absolute variation (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True Returns ------- gamma: array-like, shape (ns, nt) Optimal transportation matrix for the given parameters log: dict, optional If input log is true, a dictionary containing the cost and dual variables and exit status .. _references-weak: References ---------- .. [39] Gozlan, N., Roberto, C., Samson, P. M., & Tetali, P. (2017). Kantorovich duality for general transport costs and applications. Journal of Functional Analysis, 273(11), 3327-3405. See Also -------- ot.bregman.sinkhorn : Entropic regularized OT ot.optim.cg : General regularized OT """ nx = get_backend(Xa, Xb) Xa2 = nx.to_numpy(Xa) Xb2 = nx.to_numpy(Xb) if a is None: a2 = np.ones((Xa.shape[0])) / Xa.shape[0] else: a2 = nx.to_numpy(a) if b is None: b2 = np.ones((Xb.shape[0])) / Xb.shape[0] else: b2 = nx.to_numpy(b) # init uniform if G0 is None: T0 = a2[:, None] * b2[None, :] else: T0 = nx.to_numpy(G0) # weak OT loss def f(T): return np.dot(a2, np.sum((Xa2 - np.dot(T, Xb2) / a2[:, None]) ** 2, 1)) # weak OT gradient def df(T): return -2 * np.dot(Xa2 - np.dot(T, Xb2) / a2[:, None], Xb2.T) # solve with conditional gradient and return solution if log: res, log = cg(a2, b2, 0, 1, f, df, T0, log=log, verbose=verbose, **kwargs) log["u"] = nx.from_numpy(log["u"], type_as=Xa) log["v"] = nx.from_numpy(log["v"], type_as=Xb) return nx.from_numpy(res, type_as=Xa), log else: return nx.from_numpy( cg(a2, b2, 0, 1, f, df, T0, log=log, verbose=verbose, **kwargs), type_as=Xa ) python-pot-0.9.7+dfsg/pyproject.toml000066400000000000000000000003221525521106200174770ustar00rootroot00000000000000[build-system] requires = [ "setuptools>=42", "oldest-supported-numpy; python_version < '3.9'", "numpy>=2.0.0; python_version >= '3.9'", "cython>=0.23" ] build-backend = "setuptools.build_meta" python-pot-0.9.7+dfsg/pytest.ini000066400000000000000000000000001525521106200166050ustar00rootroot00000000000000python-pot-0.9.7+dfsg/setup.cfg000066400000000000000000000013231525521106200164060ustar00rootroot00000000000000[metadata] description_file = README.md [flake8] exclude = __init__.py ignore = E265,E501,W605,W503,W504 [tool:pytest] addopts = --showlocals --durations=20 --doctest-modules -ra --cov-report= --cov=ot --doctest-ignore-import-errors --junit-xml=junit-results.xml --ignore=docs --ignore=examples --ignore=notebooks [pycodestyle] exclude = __init__.py,constants.py,fixes.py ignore = E241,E305,W504 [pydocstyle] convention = pep257 match_dir = ^(?!\.|docs|examples).*$ match = (?!tests/__init__\.py|fixes).*\.py add-ignore = D100,D104,D107,D413 add-select = D214,D215,D404,D405,D406,D407,D408,D409,D410,D411 ignore-decorators = ^(copy_.*_doc_to_|on_trait_change|cached_property|deprecated|property|.*setter).* python-pot-0.9.7+dfsg/setup.py000066400000000000000000000145631525521106200163110ustar00rootroot00000000000000#!/usr/bin/env python # Author: Remi Flamary # # License: MIT License import os import re import subprocess import sys from setuptools import find_packages, setup from setuptools.extension import Extension import numpy from Cython.Build import cythonize ROOT = os.path.abspath(os.path.dirname(__file__)) sys.path.append(os.path.join(ROOT, "ot", "helpers")) from openmp_helpers import check_openmp_support # dirty but working __version__ = re.search( r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]', # It excludes inline comment too open(os.path.join(ROOT, "ot/__init__.py")).read(), ).group(1) # The beautiful part is, I don't even need to check exceptions here. # If something messes up, let the build process fail noisy, BEFORE my release! # thanks PyPI for handling markdown now with open(os.path.join(ROOT, "README.md"), encoding="utf-8") as f: README = f.read() # clean cython output is clean is called if "clean" in sys.argv[1:]: for cpp_file in [ "ot/lp/emd_wrap.cpp", "ot/partial/partial_cython.cpp", ]: if os.path.isfile(cpp_file): os.remove(cpp_file) # add platform dependant optional compilation argument openmp_supported, flags = check_openmp_support() compile_args = ["/O2" if sys.platform == "win32" else "-O3"] link_args = [] if openmp_supported: compile_args += flags link_args += flags if sys.platform == "win32": compile_args.append("/std:c++17") # Needed for ot/bsp if sys.platform.startswith("darwin"): compile_args.append("-std=c++17") # Needed for ot/bsp sdk_path = subprocess.check_output(["xcrun", "--show-sdk-path"]) os.environ["CFLAGS"] = '-isysroot "{}"'.format(sdk_path.rstrip().decode("utf-8")) # Add libomp linker flags on macOS if os.path.isfile("/opt/homebrew/opt/libomp/lib/libomp.dylib"): link_args += ["-L/opt/homebrew/opt/libomp/lib", "-lomp"] elif os.path.isfile("/usr/local/opt/libomp/lib/libomp.dylib"): link_args += ["-L/usr/local/opt/libomp/lib", "-lomp"] setup( name="POT", version=__version__, description="Python Optimal Transport Library", long_description=README, long_description_content_type="text/markdown", author="Remi Flamary, Nicolas Courty, Cédric Vincent-Cuaz, POT Contributors", author_email="remi.flamary@gmail.com, ncourty@gmail.com, cedvincentcuaz@gmail.com", url="https://github.com/PythonOT/POT", packages=find_packages(exclude=["benchmarks"]), ext_modules=cythonize( [ Extension( name="ot.lp.emd_wrap", sources=[ "ot/lp/emd_wrap.pyx", "ot/lp/EMD_wrapper.cpp", ], # cython/c++ src files language="c++", include_dirs=[numpy.get_include(), os.path.join(ROOT, "ot/lp")], extra_compile_args=compile_args, extra_link_args=link_args, ), Extension( name="ot.partial.partial_cython", sources=["ot/partial/partial_cython.pyx"], include_dirs=[numpy.get_include(), os.path.join(ROOT, "ot/partial")], extra_compile_args=compile_args, language="c++", ), Extension( name="ot.bsp.bsp_wrap", sources=[ "ot/bsp/bsp_wrap.pyx", "ot/bsp/bsp_wrapper.cpp", ], # cython/c++ src files language="c++", include_dirs=[ numpy.get_include(), os.path.join(ROOT, "deps/eigen"), os.path.join(ROOT, "ot/bsp"), ], extra_compile_args=compile_args, extra_link_args=link_args, ), ] ), platforms=["linux", "macosx", "windows"], download_url="https://github.com/PythonOT/POT/archive/{}.tar.gz".format( __version__ ), license="MIT", scripts=[], data_files=[], install_requires=["numpy>=1.16", "scipy>=1.6"], extras_require={ "backend-numpy": [], # in requirements. "backend-jax": ["jax", "jaxlib"], "backend-cupy": [], # should be installed with conda, not pip "backend-tf": ["tensorflow"], "backend-torch": ["torch"], "cvxopt": ["cvxopt"], # on it's own to prevent accidental GPL violations "dr": ["scikit-learn", "pymanopt", "autograd"], "gnn": ["torch", "torch_geometric"], "plot": ["matplotlib"], "geomloss": ["geomloss"], "doc": [ "sphinx", "sphinx-rtd-theme", "sphinx-gallery", "sphinxcontrib-jquery", "numpydoc", "myst-parser", "pillow", "networkx", "memory_profiler", ], "tests": ["pytest", "pytest-cov"], "all": [ "jax", "jaxlib", "tensorflow", "torch", "cvxopt", "scikit-learn", "pymanopt", "autograd", "torch_geometric", "matplotlib", "geomloss", ], }, python_requires=">=3.7", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Environment :: Console", "Operating System :: OS Independent", "Operating System :: POSIX :: Linux", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Programming Language :: Python", "Programming Language :: C++", "Programming Language :: C", "Programming Language :: Cython", "Topic :: Utilities", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Scientific/Engineering :: Mathematics", "Topic :: Scientific/Engineering :: Information Analysis", "Programming Language :: Python :: 3", "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", ], ) python-pot-0.9.7+dfsg/test/000077500000000000000000000000001525521106200155455ustar00rootroot00000000000000python-pot-0.9.7+dfsg/test/batch/000077500000000000000000000000001525521106200166265ustar00rootroot00000000000000python-pot-0.9.7+dfsg/test/batch/test_solve_batch.py000066400000000000000000000130441525521106200225320ustar00rootroot00000000000000"""Tests for module batch""" # Author: Remi Flamary # Paul Krzakala # Sonia Mazelet # Thibaut Germain # # License: MIT License import numpy as np from ot.batch import ( solve_batch, solve_sample_batch, dist_batch, loss_linear_samples_batch, loss_linear_batch, bregman_projection_batch, bregman_log_projection_batch, proximal_bregman_log_plan_batch, ) from ot import solve from contextlib import nullcontext import pytest from ot.backend import torch @pytest.mark.parametrize("reg", [None, 0, 1e-0]) @pytest.mark.parametrize("method", ["auto", "proximal", "sinkhorn", "log_sinkhorn"]) @pytest.mark.parametrize("reg_type", ["kl", "entropy"]) def test_solve_batch_vs_solve(reg, method, reg_type): """Check that solve_batch gives the same results as solve for each instance in the batch.""" should_fail = method in ["sinkhorn", "log_sinkhorn"] and (reg is None or reg <= 0) ctx = pytest.raises(Exception) if should_fail else nullcontext() with ctx: tol = 1e-4 batchsize = 3 n = 5 d = 7 rng = np.random.RandomState(0) C = rng.rand(batchsize, n, d) base_plan = np.zeros((batchsize, n, d)) base_value = np.zeros(batchsize) for i in range(batchsize): C_i = C[i] res_i = solve(C_i, reg=reg, tol=tol, reg_type=reg_type) base_plan[i] = res_i.plan base_value[i] = res_i.value_linear res = solve_batch( C, max_iter=10000, tol=tol, grad="detach", reg=reg, method=method, reg_type=reg_type, inner_reg=1e-3, ) plan = res.plan value = res.value_linear np.testing.assert_allclose(plan, base_plan, atol=tol * 10) np.testing.assert_allclose(value, base_value, atol=tol * 10) @pytest.mark.parametrize("reg", [None, 0, 1e-0]) @pytest.mark.parametrize("inner_iter", [1, 5, 10]) def test_backend_proximal_bregman_log_plan_batch(nx, reg, inner_iter): tol = 1e-4 batchsize = 3 n = 5 d = 7 rng = np.random.RandomState(0) C = rng.rand(batchsize, n, d) res = proximal_bregman_log_plan_batch( nx.from_numpy(C), reg=reg, inner_reg=1e-3, max_iter=10000, tol=tol, inner_iter=inner_iter, grad="detach", ) plan = nx.to_numpy(res["T"]) for i in range(batchsize): C_i = C[i] res_i = solve( C_i, reg=reg, tol=tol, ) plan_i = res_i.plan np.testing.assert_allclose(plan_i, plan[i], atol=tol * 10) def test_bregman_batch(): batchsize = 4 d = 2 n = 4 rng = np.random.RandomState(0) X = rng.rand(batchsize, n, d) M = dist_batch(X, X) K = np.exp(-M / 0.01) log_K = -M / 0.01 res = bregman_projection_batch(K, max_iter=50, tol=1e-10) plan = res["T"] res_log = bregman_log_projection_batch(log_K, max_iter=50, tol=1e-10) plan_log = res_log["T"] np.testing.assert_allclose(plan, plan_log, atol=1e-3) @pytest.mark.parametrize("metric", ["sqeuclidean", "euclidean", "minkowski", "kl"]) @pytest.mark.parametrize("method", ["proximal", "sinkhorn", "log_sinkhorn"]) def test_sample_solve_batch_vs_solve_batch(metric, method): """Check that all functions run without error.""" tol = 1e-5 batchsize = 2 n = 4 d = 2 rng = np.random.RandomState(0) X = rng.rand(batchsize, n, d) if metric == "kl": X = np.abs(X) + 1e-6 X = X / np.sum(X, axis=-1, keepdims=True) M = dist_batch(X, X, metric=metric) is_positive = M >= 0 np.testing.assert_equal(is_positive.all(), True) # Solve sample batch res = solve_sample_batch( X, X, reg=0.1, max_iter=10, tol=tol, metric=metric, method=method ) # Compute loss loss = res.value_linear # loss given by solver loss2 = loss_linear_batch(M, res.plan) # recompute loss from plan loss3 = loss_linear_samples_batch( X, X, res.plan, metric=metric ) # recompute loss from plan and samples np.testing.assert_allclose(loss, loss2, atol=tol * 10) np.testing.assert_allclose(loss, loss3, atol=tol * 10) @pytest.mark.skipif(not torch, reason="torch not installed") @pytest.mark.parametrize("grad", ["detach", "envelope", "autodiff", "last_step"]) @pytest.mark.parametrize("method", ["proximal", "sinkhorn", "log_sinkhorn"]) def test_gradients_torch(grad, method): """Check that all gradient methods run without error.""" batchsize = 2 n = 4 d = 2 X = torch.randn((batchsize, n, d), requires_grad=True) M = dist_batch(X, X) res = solve_batch(M, reg=0.1, max_iter=10, tol=1e-5, grad=grad, method=method) loss = res.value_linear.sum() loss_plan = res.plan.sum() if grad == "detach": assert loss.grad == None elif grad == "envelope": loss.backward() assert X.grad is not None elif grad in ["autodiff", "last_step"]: loss_plan.backward() assert X.grad is not None @pytest.mark.parametrize("method", ["proximal", "sinkhorn", "log_sinkhorn"]) def test_backend(nx, method): """Check that all gradient methods run without error.""" batchsize = 2 n = 4 d = 2 X = np.random.randn(batchsize, n, d) X = nx.from_numpy(X) M = dist_batch(X, X) solve_batch(M, reg=0.1, max_iter=10, tol=1e-5, method=method) solve_sample_batch(X, X, reg=0.1, max_iter=10, tol=1e-5, method=method) python-pot-0.9.7+dfsg/test/batch/test_solve_gromov_batch.py000066400000000000000000000341471525521106200241320ustar00rootroot00000000000000"""Tests for module batch""" # Author: Remi Flamary # Paul Krzakala # Sonia Mazelet # # License: MIT License import numpy as np from ot.batch import ( solve_gromov_batch, loss_quadratic_batch, loss_linear_batch, ) from ot import solve_gromov from ot.batch._linear import dist_batch import pytest from itertools import product from ot.backend import torch from ot.batch._quadratic import ( tensor_batch, div_between_product_batch, loss_quadratic_samples_batch, ) from ot.gromov._utils import div_between_product def test_solve_gromov_batch(): """Check that solve_gromov_batch gives the same results as solve for each instance in the batch.""" b = 2 n = 8 d = 2 reg = 0.01 max_iter = 1000 max_iter_inner = 10000 tol = 1e-5 tol_inner = 1e-5 alpha = 0.5 rng = np.random.RandomState(0) X1 = rng.randn(b, n, d).astype("float32") C1 = rng.randn(b, n, n).astype("float32") permutation = np.random.permutation(n) X2 = X1[:, permutation, :] + 0.01 * rng.randn(b, n, d).astype("float32") C2 = C1[:, permutation, :][:, :, permutation] + 0.01 * rng.randn(b, n, n).astype( "float32" ) M = dist_batch(X1, X2) res = solve_gromov_batch( alpha=alpha, reg=reg, M=M, Ca=C1, Cb=C2, max_iter=max_iter, tol=tol, max_iter_inner=max_iter_inner, tol_inner=tol_inner, symmetric=False, ) plan_batch = res.plan values_quadratic_batch = res.value_quad values_linear_batch = res.value_linear for i in range(b): M_i = M[i] C1_i = C1[i] C2_i = C2[i] res_i = solve_gromov(C1_i, C2_i, M=M_i, alpha=alpha, symmetric=False) plan_i = res_i.plan values_quadratic_i = res_i.value_quad values_linear_i = res_i.value_linear np.testing.assert_allclose(values_linear_i, values_linear_batch[i], atol=1e-4) np.testing.assert_allclose( values_quadratic_i, values_quadratic_batch[i], atol=1e-4 ) np.testing.assert_allclose(plan_i, plan_batch[i], atol=1e-05) @pytest.mark.parametrize( "loss, logits", product( ["sqeuclidean", "kl"], [True, False], ), ) def test_all(loss, logits): """Check that all functions run without error.""" batchsize = 2 n = 4 d = 2 rng = np.random.RandomState(0) C = rng.rand(batchsize, n, n, d) a = np.ones((batchsize, n)) if loss == "kl": C = np.abs(C) + 1e-6 C = C / np.sum(C, axis=-1, keepdims=True) res = solve_gromov_batch(Ca=C, Cb=C, a=a, b=a, loss=loss, logits=logits) loss1 = res.value_quad loss2 = loss_quadratic_batch( a=a, b=a, Ca=C, Cb=C, T=res.plan, loss=loss, logits=logits ) np.testing.assert_allclose(loss1, loss2, atol=1e-5) @pytest.mark.skipif(not torch, reason="torch not installed") @pytest.mark.parametrize("grad", ["detach", "envelope", "autodiff"]) def test_gradients_torch(grad): """Check that all gradient methods run without error.""" batchsize = 2 n = 4 d = 2 C = torch.randn((batchsize, n, n, d), requires_grad=True) res = solve_gromov_batch( Ca=C, Cb=C, a=None, b=None, loss="sqeuclidean", logits=False, grad=grad ) loss = res.value.sum() loss_plan = res.plan.sum() if grad == "detach": assert loss.grad == None elif grad == "envelope": loss.backward() assert C.grad is not None elif grad == "autodiff": loss_plan.backward() assert C.grad is not None def test_backend(nx): """Check that all gradient methods run without error.""" batchsize = 2 n = 4 d = 2 C = np.random.randn(batchsize, n, n, d) C = nx.from_numpy(C) solve_gromov_batch(Ca=C, Cb=C, a=None, b=None, loss="sqeuclidean", logits=False) @pytest.mark.parametrize( "loss, logits, unbalanced_type", product(["sqeuclidean", "kl"], [True, False], ["kl", "l2"]), ) def test_fugw_loss(unbalanced_type, loss, logits): """Check that loss_quadratic_batch runs without error.""" batchsize = 2 n = 4 d = 2 rng = np.random.RandomState(0) C1 = rng.rand(batchsize, n, n, d) C2 = rng.rand(batchsize, n, n, d) M = rng.rand(batchsize, n, n) a = np.ones((batchsize, n)) T = rng.rand(batchsize, n, n) alpha = rng.rand() reg_marginals = rng.rand() loss_fugw = loss_quadratic_batch( a, a, C1, C2, T, M, alpha=alpha, unbalanced=reg_marginals, unbalanced_type=unbalanced_type, loss=loss, logits=logits, ) # unbalanced quadratic loss_fugw_unbalanced_only = loss_quadratic_batch( a, a, C1, C2, T, M=None, alpha=alpha, unbalanced=reg_marginals, unbalanced_type=unbalanced_type, loss=loss, logits=logits, ) # alpha is None loss_fugw_no_alpha = loss_quadratic_batch( a, a, C1, C2, T, M=None, alpha=None, unbalanced=reg_marginals, unbalanced_type=unbalanced_type, loss=loss, logits=logits, ) # balanced loss_fugw_no_unbalanced = loss_quadratic_batch( a, a, C1, C2, T, M=M, alpha=alpha, loss=loss, logits=logits, ) assert np.isfinite(loss_fugw).all() assert np.isfinite(loss_fugw_unbalanced_only).all() assert np.isfinite(loss_fugw_no_alpha).all() assert np.isfinite(loss_fugw_no_unbalanced).all() def test_fugw_backend(nx): """Check that loss_quadratic_batch runs without error for all backends.""" batchsize = 2 n = 4 d = 2 rng = np.random.RandomState(0) C1_np = rng.rand(batchsize, n, n, d) C1 = nx.from_numpy(C1_np) C2_np = rng.rand(batchsize, n, n, d) C2 = nx.from_numpy(C2_np) M_np = rng.rand(batchsize, n, n) M = nx.from_numpy(M_np) a_np = np.ones((batchsize, n)) a = nx.from_numpy(a_np) T_np = rng.rand(batchsize, n, n) T = nx.from_numpy(T_np) alpha_np = rng.rand() alpha = nx.from_numpy(np.array(alpha_np)) unbalanced_np = rng.rand() unbalanced = nx.from_numpy(np.array(unbalanced_np)) loss_fugw_np = loss_quadratic_batch( a_np, a_np, C1_np, C2_np, T_np, M_np, alpha=alpha_np, unbalanced=unbalanced_np, unbalanced_type="kl", loss="sqeuclidean", logits=False, ) loss_fugw = loss_quadratic_batch( a, a, C1, C2, T, M, alpha=alpha, unbalanced=unbalanced, unbalanced_type="kl", loss="sqeuclidean", logits=False, ) assert np.allclose(loss_fugw_np, loss_fugw, atol=1e-5) def test_fugw_paramters_arrays(): batchsize = 2 n = 4 d = 2 rng = np.random.RandomState(0) C1 = rng.rand(batchsize, n, n, d) C2 = rng.rand(batchsize, n, n, d) M = rng.rand(batchsize, n, n) a = np.ones((batchsize, n)) T = rng.rand(batchsize, n, n) alpha = rng.rand() # check that alpha and reg_marginals can be passed as lists or arrays of shape (batchsize,) alpha = rng.rand(batchsize) unbalanced = rng.rand(batchsize) alpha_list = alpha.tolist() unbalanced_list = unbalanced.tolist() loss_fugw = loss_quadratic_batch( a, a, C1, C2, T, M, alpha=alpha, unbalanced=unbalanced, unbalanced_type="kl", loss="l2", logits=False, ) loss_fugw_list = loss_quadratic_batch( a, a, C1, C2, T, M, alpha=alpha_list, unbalanced=unbalanced_list, unbalanced_type="kl", loss="l2", logits=False, ) assert np.isfinite(loss_fugw).all() assert np.isfinite(loss_fugw_list).all() np.testing.assert_allclose(loss_fugw, loss_fugw_list) def test_fugw_invalid_loss_values(): batchsize = 2 n = 4 d = 2 rng = np.random.RandomState(0) C1 = rng.rand(batchsize, n, n, d) C2 = rng.rand(batchsize, n, n, d) M = rng.rand(batchsize, n, n) a = np.ones((batchsize, n)) T = rng.rand(batchsize, n, n) alpha = rng.rand() unbalanced = rng.rand() # check that invalid loss raise an error with pytest.raises(ValueError): loss_quadratic_batch( a, a, C1, C2, T, M, alpha=alpha, unbalanced=unbalanced, unbalanced_type="kl", loss="test", logits=False, ) # check that invalid unbalanced_type raise an error with pytest.raises(ValueError): loss_quadratic_batch( a, a, C1, C2, T, M, alpha=alpha, unbalanced=unbalanced, unbalanced_type="test", loss="l2", logits=False, ) def test_fugw_invalid_shapes(): batchsize = 2 n = 4 d = 2 rng = np.random.RandomState(0) C1 = rng.rand(batchsize, n, n, d) C2 = rng.rand(batchsize, n, n, d) M = rng.rand(batchsize, n, n) a = np.ones((batchsize, n)) T = rng.rand(batchsize, n, n) alpha = rng.rand() unbalanced = rng.rand() # check that invalid alpha shape raise an error alpha = rng.rand(batchsize + 1) with pytest.raises(ValueError): loss_quadratic_batch( a, a, C1, C2, T, M, alpha=alpha, unbalanced=unbalanced, unbalanced_type="kl", loss="l2", logits=False, ) # check that invalid rho shape raise an error alpha = rng.rand(batchsize) unbalanced = rng.rand(batchsize + 1) with pytest.raises(ValueError): loss_quadratic_batch( a, a, C1, C2, T, M, alpha=alpha, unbalanced=unbalanced, unbalanced_type="kl", loss="l2", logits=False, ) @pytest.mark.parametrize("unbalanced_type", ["kl", "l2"]) @pytest.mark.parametrize("loss", ["sqeuclidean", "kl"]) def test_valid_fugw_loss_endpoints(unbalanced_type, loss): """Check that loss_fugw_batch gives the same results as solve_gromov_batch and solve_linear_batch for alpha=0 and alpha=1.""" batchsize = 2 n = 4 d = 2 rng = np.random.RandomState(0) C1 = rng.rand(batchsize, n, n, d) C2 = rng.rand(batchsize, n, n, d) M = rng.rand(batchsize, n, n) a = np.ones((batchsize, n)) reg_marginals = 0 T = rng.rand(batchsize, n, n) loss_fugw = loss_quadratic_batch( a, a, C1, C2, T, M, alpha=0.0, unbalanced=reg_marginals, unbalanced_type=unbalanced_type, loss=loss, logits=False, ) loss_linear = loss_linear_batch(M, T) np.testing.assert_allclose(loss_fugw, loss_linear, atol=1e-5) loss_fugw = loss_quadratic_batch( a, a, C1, C2, T, M, alpha=1.0, unbalanced=reg_marginals, unbalanced_type=unbalanced_type, loss=loss, logits=False, ) loss_gromov = loss_quadratic_batch( a, a, C1, C2, T, unbalanced=reg_marginals, unbalanced_type=unbalanced_type, loss=loss, logits=False, ) np.testing.assert_allclose(loss_fugw, loss_gromov, atol=1e-5) @pytest.mark.parametrize("divergence", ["kl", "l2"]) def test_div_between_product(divergence): batchsize = 2 n = 4 m = 3 rng = np.random.RandomState(0) mu = rng.rand(batchsize, n) nu = rng.rand(batchsize, m) alpha = rng.rand(batchsize, n) beta = rng.rand(batchsize, m) res_batch = div_between_product_batch( mu, nu, alpha, beta, divergence=divergence, nx=None ) res = np.array( [ div_between_product(mu[i], nu[i], alpha[i], beta[i], divergence) for i in range(batchsize) ] ) np.testing.assert_allclose(res_batch, res, atol=1e-5) def test_loss_quadratic_samples_batch_deprecated(): rng = np.random.RandomState(0) batchsize = 2 n = 4 d = 2 C1 = rng.rand(batchsize, n, n, d) C2 = rng.rand(batchsize, n, n, d) a = np.ones((batchsize, n)) T = rng.rand(batchsize, n, n) with pytest.warns(DeprecationWarning, match="loss_quadratic_batch"): loss_quadratic_samples_batch(a, a, C1, C2, T, loss="sqeuclidean") def test_loss_quadratic_batch_log_balanced(): batchsize = 2 n = 4 d = 2 rng = np.random.RandomState(0) C1 = rng.rand(batchsize, n, n, d) C2 = rng.rand(batchsize, n, n, d) a = np.ones((batchsize, n)) T = rng.rand(batchsize, n, n) value, log = loss_quadratic_batch(a, a, C1, C2, T, loss="sqeuclidean", log=True) expected = loss_quadratic_batch(a, a, C1, C2, T, loss="sqeuclidean") np.testing.assert_allclose(value, expected) np.testing.assert_allclose(log["value"], expected) np.testing.assert_allclose(log["value_quadratic"], expected) assert log["value_linear"] is None assert log["value_unbalanced"] is None def test_loss_quadratic_batch_log_fugw(): batchsize = 2 n = 4 d = 2 rng = np.random.RandomState(0) C1 = rng.rand(batchsize, n, n, d) C2 = rng.rand(batchsize, n, n, d) M = rng.rand(batchsize, n, n) a = np.ones((batchsize, n)) T = rng.rand(batchsize, n, n) alpha = rng.rand() unbalanced = rng.rand() value, log = loss_quadratic_batch( a, a, C1, C2, T, M=M, alpha=alpha, unbalanced=unbalanced, unbalanced_type="kl", loss="sqeuclidean", log=True, ) expected = (1 - alpha) * log["value_linear"] + alpha * log["value_quadratic"] expected = expected + unbalanced * log["value_unbalanced"] np.testing.assert_allclose(value, expected) np.testing.assert_allclose(log["value"], expected) python-pot-0.9.7+dfsg/test/conftest.py000066400000000000000000000040071525521106200177450ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Configuration file for pytest # License: MIT License import functools import os import pytest import numpy as np from sys import platform # set numpy print options : TODO update tests when all release use modern numpy if platform == "linux": np.set_printoptions(legacy="1.25") from ot.backend import get_backend_list, jax, tf if jax: os.environ["XLA_PYTHON_CLIENT_PREALLOCATE"] = "false" from jax import config config.update("jax_enable_x64", True) if tf: # make sure TF doesn't allocate entire GPU import tensorflow as tf physical_devices = tf.config.list_physical_devices("GPU") for device in physical_devices: try: tf.config.experimental.set_memory_growth(device, True) except Exception: pass # allow numpy API for TF from tensorflow.python.ops.numpy_ops import np_config np_config.enable_numpy_behavior() backend_list = get_backend_list() print(backend_list) @pytest.fixture(params=backend_list) def nx(request): backend = request.param yield backend def skip_arg(arg, value, reason=None, getter=lambda x: x): if isinstance(arg, (tuple, list)): n = len(arg) else: arg = (arg,) n = 1 if n != 1 and isinstance(value, (tuple, list)): pass else: value = (value,) if isinstance(getter, (tuple, list)): pass else: getter = [getter] * n if reason is None: reason = f"Param {arg} should be skipped for value {value}" def wrapper(function): @functools.wraps(function) def wrapped(*args, **kwargs): if all( arg[i] in kwargs.keys() and getter[i](kwargs[arg[i]]) == value[i] for i in range(n) ): pytest.skip(reason) return function(*args, **kwargs) return wrapped return wrapper def pytest_configure(config): pytest.skip_arg = skip_arg pytest.skip_backend = functools.partial(skip_arg, "nx", getter=str) python-pot-0.9.7+dfsg/test/gromov/000077500000000000000000000000001525521106200170565ustar00rootroot00000000000000python-pot-0.9.7+dfsg/test/gromov/__init__.py000066400000000000000000000000001525521106200211550ustar00rootroot00000000000000python-pot-0.9.7+dfsg/test/gromov/test_bregman.py000066400000000000000000001261311525521106200221060ustar00rootroot00000000000000"""Tests for gromov._bregman.py""" # Author: Rémi Flamary # Titouan Vayer # Cédric Vincent-Cuaz # # License: MIT License import numpy as np import pytest import ot @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tf backend") @pytest.mark.parametrize( "loss_fun", [ "square_loss", "kl_loss", pytest.param("unknown_loss", marks=pytest.mark.xfail(raises=ValueError)), ], ) def test_entropic_gromov(nx, loss_fun): n_samples = 10 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=42) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() C1b, C2b, pb, qb, G0b = nx.from_numpy(C1, C2, p, q, G0) G, log = ot.gromov.entropic_gromov_wasserstein( C1, C2, None, q, loss_fun, symmetric=None, G0=G0, epsilon=1e-2, max_iter=10, verbose=True, log=True, ) Gb = nx.to_numpy( ot.gromov.entropic_gromov_wasserstein( C1b, C2b, pb, None, loss_fun, symmetric=True, G0=None, epsilon=1e-2, max_iter=10, verbose=True, log=False, ) ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tf backend") @pytest.mark.parametrize( "loss_fun", [ "square_loss", "kl_loss", pytest.param("unknown_loss", marks=pytest.mark.xfail(raises=ValueError)), ], ) def test_entropic_gromov2(nx, loss_fun): n_samples = 10 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=42) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() C1b, C2b, pb, qb, G0b = nx.from_numpy(C1, C2, p, q, G0) gw, log = ot.gromov.entropic_gromov_wasserstein2( C1, C2, p, None, loss_fun, symmetric=True, G0=None, max_iter=10, epsilon=1e-2, log=True, ) gwb, logb = ot.gromov.entropic_gromov_wasserstein2( C1b, C2b, None, qb, loss_fun, symmetric=None, G0=G0b, max_iter=10, epsilon=1e-2, log=True, ) gwb = nx.to_numpy(gwb) G = log["T"] Gb = nx.to_numpy(logb["T"]) np.testing.assert_allclose(gw, gwb, atol=1e-06) np.testing.assert_allclose(gw, 0, atol=1e-1, rtol=1e-1) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov @pytest.skip_backend("tf", reason="test very slow with tf backend") def test_entropic_proximal_gromov(nx): n_samples = 10 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=42) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() C1b, C2b, pb, qb, G0b = nx.from_numpy(C1, C2, p, q, G0) with pytest.raises(ValueError): loss_fun = "weird_loss_fun" G, log = ot.gromov.entropic_gromov_wasserstein( C1, C2, None, q, loss_fun, symmetric=None, G0=G0, epsilon=1e-1, max_iter=10, solver="PPA", verbose=True, log=True, numItermax=1, ) G, log = ot.gromov.entropic_gromov_wasserstein( C1, C2, None, q, "square_loss", symmetric=None, G0=G0, epsilon=1e-1, max_iter=10, solver="PPA", verbose=True, log=True, numItermax=1, ) Gb = nx.to_numpy( ot.gromov.entropic_gromov_wasserstein( C1b, C2b, pb, None, "square_loss", symmetric=True, G0=None, epsilon=1e-1, max_iter=10, solver="PPA", verbose=True, log=False, numItermax=1, ) ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-02) # cf convergence gromov gw, log = ot.gromov.entropic_gromov_wasserstein2( C1, C2, p, q, "kl_loss", symmetric=True, G0=None, max_iter=10, epsilon=1e-1, solver="PPA", warmstart=True, log=True, ) gwb, logb = ot.gromov.entropic_gromov_wasserstein2( C1b, C2b, pb, qb, "kl_loss", symmetric=None, G0=G0b, max_iter=10, epsilon=1e-1, solver="PPA", warmstart=True, log=True, ) gwb = nx.to_numpy(gwb) G = log["T"] Gb = nx.to_numpy(logb["T"]) np.testing.assert_allclose(gw, gwb, atol=1e-06) np.testing.assert_allclose(gw, 0, atol=1e-1, rtol=1e-1) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-02) # cf convergence gromov @pytest.skip_backend("tf", reason="test very slow with tf backend") def test_asymmetric_entropic_gromov(nx): n_samples = 10 # nb samples rng = np.random.RandomState(0) C1 = rng.uniform(low=0.0, high=10, size=(n_samples, n_samples)) idx = np.arange(n_samples) rng.shuffle(idx) C2 = C1[idx, :][:, idx] p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1b, C2b, pb, qb, G0b = nx.from_numpy(C1, C2, p, q, G0) G = ot.gromov.entropic_gromov_wasserstein( C1, C2, p, q, "square_loss", symmetric=None, G0=G0, epsilon=1e-1, max_iter=5, verbose=True, log=False, ) Gb = nx.to_numpy( ot.gromov.entropic_gromov_wasserstein( C1b, C2b, pb, qb, "square_loss", symmetric=False, G0=None, epsilon=1e-1, max_iter=5, verbose=True, log=False, ) ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov gw = ot.gromov.entropic_gromov_wasserstein2( C1, C2, None, None, "kl_loss", symmetric=False, G0=None, max_iter=5, epsilon=1e-1, log=False, ) gwb = ot.gromov.entropic_gromov_wasserstein2( C1b, C2b, pb, qb, "kl_loss", symmetric=None, G0=G0b, max_iter=5, epsilon=1e-1, log=False, ) gwb = nx.to_numpy(gwb) np.testing.assert_allclose(gw, gwb, atol=1e-06) np.testing.assert_allclose(gw, 0, atol=1e-1, rtol=1e-1) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tf backend") def test_entropic_gromov_dtype_device(nx): # setup n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=42) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() for tp in nx.__type_list__: print(nx.dtype_device(tp)) C1b, C2b, pb, qb = nx.from_numpy(C1, C2, p, q, type_as=tp) for solver in ["PGD", "PPA", "BAPG"]: if solver == "BAPG": Gb = ot.gromov.BAPG_gromov_wasserstein( C1b, C2b, pb, qb, max_iter=2, verbose=True ) gw_valb = ot.gromov.BAPG_gromov_wasserstein2( C1b, C2b, pb, qb, max_iter=2, verbose=True ) else: Gb = ot.gromov.entropic_gromov_wasserstein( C1b, C2b, pb, qb, max_iter=2, solver=solver, verbose=True ) gw_valb = ot.gromov.entropic_gromov_wasserstein2( C1b, C2b, pb, qb, max_iter=2, solver=solver, verbose=True ) nx.assert_same_dtype_device(C1b, Gb) nx.assert_same_dtype_device(C1b, gw_valb) def test_BAPG_gromov(nx): n_samples = 10 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=42) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() C1b, C2b, pb, qb, G0b = nx.from_numpy(C1, C2, p, q, G0) # complete test with marginal loss = True marginal_loss = True with pytest.raises(ValueError): loss_fun = "weird_loss_fun" G, log = ot.gromov.BAPG_gromov_wasserstein( C1, C2, None, q, loss_fun, symmetric=None, G0=G0, epsilon=1e-1, max_iter=10, marginal_loss=marginal_loss, verbose=True, log=True, ) G, log = ot.gromov.BAPG_gromov_wasserstein( C1, C2, None, q, "square_loss", symmetric=None, G0=G0, epsilon=1e-1, max_iter=10, marginal_loss=marginal_loss, verbose=True, log=True, ) Gb = nx.to_numpy( ot.gromov.BAPG_gromov_wasserstein( C1b, C2b, pb, None, "square_loss", symmetric=True, G0=None, epsilon=1e-1, max_iter=10, marginal_loss=marginal_loss, verbose=True, log=False, ) ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-02) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-02) # cf convergence gromov with pytest.warns(UserWarning): gw = ot.gromov.BAPG_gromov_wasserstein2( C1, C2, p, q, "kl_loss", symmetric=False, G0=None, max_iter=10, epsilon=1e-2, marginal_loss=marginal_loss, log=False, ) gw, log = ot.gromov.BAPG_gromov_wasserstein2( C1, C2, p, q, "kl_loss", symmetric=False, G0=None, max_iter=10, epsilon=1.0, marginal_loss=marginal_loss, log=True, ) gwb, logb = ot.gromov.BAPG_gromov_wasserstein2( C1b, C2b, pb, qb, "kl_loss", symmetric=None, G0=G0b, max_iter=10, epsilon=1.0, marginal_loss=marginal_loss, log=True, ) gwb = nx.to_numpy(gwb) G = log["T"] Gb = nx.to_numpy(logb["T"]) np.testing.assert_allclose(gw, gwb, atol=1e-06) np.testing.assert_allclose(gw, 0, atol=1e-1, rtol=1e-1) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-02) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-02) # cf convergence gromov marginal_loss = False G, log = ot.gromov.BAPG_gromov_wasserstein( C1, C2, None, q, "square_loss", symmetric=None, G0=G0, epsilon=1e-1, max_iter=10, marginal_loss=marginal_loss, verbose=True, log=True, ) Gb = nx.to_numpy( ot.gromov.BAPG_gromov_wasserstein( C1b, C2b, pb, None, "square_loss", symmetric=False, G0=None, epsilon=1e-1, max_iter=10, marginal_loss=marginal_loss, verbose=True, log=False, ) ) @pytest.skip_backend("tf", reason="test very slow with tf backend") def test_entropic_fgw(nx): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=42) xt = xs[::-1].copy() rng = np.random.RandomState(42) ys = rng.randn(xs.shape[0], 2) yt = ys[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() M = ot.dist(ys, yt) Mb, C1b, C2b, pb, qb, G0b = nx.from_numpy(M, C1, C2, p, q, G0) with pytest.raises(ValueError): loss_fun = "weird_loss_fun" G, log = ot.gromov.entropic_fused_gromov_wasserstein( M, C1, C2, None, None, loss_fun, symmetric=None, G0=G0, epsilon=1e-1, max_iter=10, verbose=True, log=True, ) G, log = ot.gromov.entropic_fused_gromov_wasserstein( M, C1, C2, None, None, "square_loss", symmetric=None, G0=G0, epsilon=1e-1, max_iter=10, verbose=True, log=True, ) Gb = nx.to_numpy( ot.gromov.entropic_fused_gromov_wasserstein( Mb, C1b, C2b, pb, qb, "square_loss", symmetric=True, G0=None, epsilon=1e-1, max_iter=10, verbose=True, log=False, ) ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov fgw, log = ot.gromov.entropic_fused_gromov_wasserstein2( M, C1, C2, p, q, "kl_loss", symmetric=True, G0=None, max_iter=10, epsilon=1e-1, log=True, ) fgwb, logb = ot.gromov.entropic_fused_gromov_wasserstein2( Mb, C1b, C2b, pb, qb, "kl_loss", symmetric=None, G0=G0b, max_iter=10, epsilon=1e-1, log=True, ) fgwb = nx.to_numpy(fgwb) G = log["T"] Gb = nx.to_numpy(logb["T"]) np.testing.assert_allclose(fgw, fgwb, atol=1e-06) np.testing.assert_allclose(fgw, 0, atol=1e-1, rtol=1e-1) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov @pytest.skip_backend("tf", reason="test very slow with tf backend") def test_entropic_proximal_fgw(nx): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=42) xt = xs[::-1].copy() rng = np.random.RandomState(42) ys = rng.randn(xs.shape[0], 2) yt = ys[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() M = ot.dist(ys, yt) Mb, C1b, C2b, pb, qb, G0b = nx.from_numpy(M, C1, C2, p, q, G0) G, log = ot.gromov.entropic_fused_gromov_wasserstein( M, C1, C2, p, q, "square_loss", symmetric=None, G0=G0, epsilon=1e-1, max_iter=10, solver="PPA", verbose=True, log=True, numItermax=1, ) Gb = nx.to_numpy( ot.gromov.entropic_fused_gromov_wasserstein( Mb, C1b, C2b, pb, qb, "square_loss", symmetric=True, G0=None, epsilon=1e-1, max_iter=10, solver="PPA", verbose=True, log=False, numItermax=1, ) ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov fgw, log = ot.gromov.entropic_fused_gromov_wasserstein2( M, C1, C2, p, None, "kl_loss", symmetric=True, G0=None, max_iter=5, epsilon=1e-1, solver="PPA", warmstart=True, log=True, ) fgwb, logb = ot.gromov.entropic_fused_gromov_wasserstein2( Mb, C1b, C2b, None, qb, "kl_loss", symmetric=None, G0=G0b, max_iter=5, epsilon=1e-1, solver="PPA", warmstart=True, log=True, ) fgwb = nx.to_numpy(fgwb) G = log["T"] Gb = nx.to_numpy(logb["T"]) np.testing.assert_allclose(fgw, fgwb, atol=1e-06) np.testing.assert_allclose(fgw, 0, atol=1e-1, rtol=1e-1) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov def test_BAPG_fgw(nx): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=42) xt = xs[::-1].copy() rng = np.random.RandomState(42) ys = rng.randn(xs.shape[0], 2) yt = ys[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() M = ot.dist(ys, yt) Mb, C1b, C2b, pb, qb, G0b = nx.from_numpy(M, C1, C2, p, q, G0) with pytest.raises(ValueError): loss_fun = "weird_loss_fun" G, log = ot.gromov.BAPG_fused_gromov_wasserstein( M, C1, C2, p, q, loss_fun=loss_fun, max_iter=1, log=True ) # complete test with marginal loss = True marginal_loss = True G, log = ot.gromov.BAPG_fused_gromov_wasserstein( M, C1, C2, p, q, "square_loss", symmetric=None, G0=G0, epsilon=1e-1, max_iter=10, marginal_loss=marginal_loss, log=True, ) Gb = nx.to_numpy( ot.gromov.BAPG_fused_gromov_wasserstein( Mb, C1b, C2b, pb, qb, "square_loss", symmetric=True, G0=None, epsilon=1e-1, max_iter=10, marginal_loss=marginal_loss, verbose=True, ) ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-02) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-02) # cf convergence gromov with pytest.warns(UserWarning): fgw = ot.gromov.BAPG_fused_gromov_wasserstein2( M, C1, C2, p, q, "kl_loss", symmetric=False, G0=None, max_iter=10, epsilon=1e-3, marginal_loss=marginal_loss, log=False, ) fgw, log = ot.gromov.BAPG_fused_gromov_wasserstein2( M, C1, C2, p, None, "kl_loss", symmetric=True, G0=None, max_iter=5, epsilon=1, marginal_loss=marginal_loss, log=True, ) fgwb, logb = ot.gromov.BAPG_fused_gromov_wasserstein2( Mb, C1b, C2b, None, qb, "kl_loss", symmetric=None, G0=G0b, max_iter=5, epsilon=1, marginal_loss=marginal_loss, log=True, ) fgwb = nx.to_numpy(fgwb) G = log["T"] Gb = nx.to_numpy(logb["T"]) np.testing.assert_allclose(fgw, fgwb, atol=1e-06) np.testing.assert_allclose(fgw, 0, atol=1e-1, rtol=1e-1) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-02) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-02) # cf convergence gromov # Tests with marginal_loss = False marginal_loss = False G, log = ot.gromov.BAPG_fused_gromov_wasserstein( M, C1, C2, p, q, "square_loss", symmetric=False, G0=G0, epsilon=1e-1, max_iter=10, marginal_loss=marginal_loss, log=True, ) Gb = nx.to_numpy( ot.gromov.BAPG_fused_gromov_wasserstein( Mb, C1b, C2b, pb, qb, "square_loss", symmetric=None, G0=None, epsilon=1e-1, max_iter=10, marginal_loss=marginal_loss, verbose=True, ) ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-02) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-02) # cf convergence gromov def test_asymmetric_entropic_fgw(nx): n_samples = 5 # nb samples rng = np.random.RandomState(0) C1 = rng.uniform(low=0.0, high=10, size=(n_samples, n_samples)) idx = np.arange(n_samples) rng.shuffle(idx) C2 = C1[idx, :][:, idx] ys = rng.randn(n_samples, 2) yt = ys[idx, :] M = ot.dist(ys, yt) p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] Mb, C1b, C2b, pb, qb, G0b = nx.from_numpy(M, C1, C2, p, q, G0) G = ot.gromov.entropic_fused_gromov_wasserstein( M, C1, C2, p, q, "square_loss", symmetric=None, G0=G0, max_iter=5, epsilon=1e-1, verbose=True, log=False, ) Gb = nx.to_numpy( ot.gromov.entropic_fused_gromov_wasserstein( Mb, C1b, C2b, pb, qb, "square_loss", symmetric=False, G0=None, max_iter=5, epsilon=1e-1, verbose=True, log=False, ) ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov fgw = ot.gromov.entropic_fused_gromov_wasserstein2( M, C1, C2, p, q, "kl_loss", symmetric=False, G0=None, max_iter=5, epsilon=1e-1, log=False, ) fgwb = ot.gromov.entropic_fused_gromov_wasserstein2( Mb, C1b, C2b, pb, qb, "kl_loss", symmetric=None, G0=G0b, max_iter=5, epsilon=1e-1, log=False, ) fgwb = nx.to_numpy(fgwb) np.testing.assert_allclose(fgw, fgwb, atol=1e-06) np.testing.assert_allclose(fgw, 0, atol=1e-1, rtol=1e-1) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tf backend") def test_entropic_fgw_dtype_device(nx): # setup n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) rng = np.random.RandomState(42) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=rng) xt = xs[::-1].copy() ys = rng.randn(xs.shape[0], 2) yt = ys[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() M = ot.dist(ys, yt) for tp in nx.__type_list__: print(nx.dtype_device(tp)) Mb, C1b, C2b, pb, qb = nx.from_numpy(M, C1, C2, p, q, type_as=tp) for solver in ["PGD", "PPA", "BAPG"]: if solver == "BAPG": Gb = ot.gromov.BAPG_fused_gromov_wasserstein( Mb, C1b, C2b, pb, qb, max_iter=2 ) fgw_valb = ot.gromov.BAPG_fused_gromov_wasserstein2( Mb, C1b, C2b, pb, qb, max_iter=2 ) else: Gb = ot.gromov.entropic_fused_gromov_wasserstein( Mb, C1b, C2b, pb, qb, max_iter=2, solver=solver ) fgw_valb = ot.gromov.entropic_fused_gromov_wasserstein2( Mb, C1b, C2b, pb, qb, max_iter=2, solver=solver ) nx.assert_same_dtype_device(C1b, Gb) nx.assert_same_dtype_device(C1b, fgw_valb) def test_entropic_fgw_barycenter(nx): ns = 5 nt = 10 rng = np.random.RandomState(42) Xs, ys = ot.datasets.make_data_classif("3gauss", ns, random_state=42) Xt, yt = ot.datasets.make_data_classif("3gauss2", nt, random_state=42) ys = rng.randn(Xs.shape[0], 2) yt = rng.randn(Xt.shape[0], 2) C1 = ot.dist(Xs) C2 = ot.dist(Xt) p1 = ot.unif(ns) p2 = ot.unif(nt) n_samples = 3 p = ot.unif(n_samples) ysb, ytb, C1b, C2b, p1b, p2b, pb = nx.from_numpy(ys, yt, C1, C2, p1, p2, p) with pytest.raises(ValueError): loss_fun = "weird_loss_fun" X, C, log = ot.gromov.entropic_fused_gromov_barycenters( n_samples, [ys, yt], [C1, C2], None, p, [0.5, 0.5], loss_fun, 0.1, max_iter=10, tol=1e-3, verbose=True, warmstartT=True, random_state=42, solver="PPA", numItermax=10, log=True, symmetric=True, ) with pytest.raises(ValueError): stop_criterion = "unknown stop criterion" X, C, log = ot.gromov.entropic_fused_gromov_barycenters( n_samples, [ys, yt], [C1, C2], None, p, [0.5, 0.5], "square_loss", 0.1, max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=True, warmstartT=True, random_state=42, solver="PPA", numItermax=10, log=True, symmetric=True, ) for stop_criterion in ["barycenter", "loss"]: X, C, log = ot.gromov.entropic_fused_gromov_barycenters( n_samples, [ys, yt], [C1, C2], None, p, [0.5, 0.5], "square_loss", epsilon=0.1, max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=True, warmstartT=True, random_state=42, solver="PPA", numItermax=10, log=True, symmetric=True, ) Xb, Cb = ot.gromov.entropic_fused_gromov_barycenters( n_samples, [ysb, ytb], [C1b, C2b], [p1b, p2b], None, [0.5, 0.5], "square_loss", epsilon=0.1, max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=False, warmstartT=True, random_state=42, solver="PPA", numItermax=10, log=False, symmetric=True, ) Xb, Cb = nx.to_numpy(Xb, Cb) np.testing.assert_allclose(C, Cb, atol=1e-06) np.testing.assert_allclose(Cb.shape, (n_samples, n_samples)) np.testing.assert_allclose(X, Xb, atol=1e-06) np.testing.assert_allclose(Xb.shape, (n_samples, ys.shape[1])) # test with 'kl_loss' and log=True # providing init_C, init_Y generator = ot.utils.check_random_state(42) xalea = generator.randn(n_samples, 2) init_C = ot.utils.dist(xalea, xalea) init_C /= init_C.max() init_Cb = nx.from_numpy(init_C) init_Y = np.zeros((n_samples, ys.shape[1]), dtype=ys.dtype) init_Yb = nx.from_numpy(init_Y) X, C, log = ot.gromov.entropic_fused_gromov_barycenters( n_samples, [ys, yt], [C1, C2], [p1, p2], p, None, "kl_loss", 0.1, True, max_iter=10, tol=1e-3, verbose=False, warmstartT=False, random_state=42, solver="PPA", numItermax=1, init_C=init_C, init_Y=init_Y, log=True, ) Xb, Cb, logb = ot.gromov.entropic_fused_gromov_barycenters( n_samples, [ysb, ytb], [C1b, C2b], [p1b, p2b], pb, [0.5, 0.5], "kl_loss", 0.1, True, max_iter=10, tol=1e-3, verbose=False, warmstartT=False, random_state=42, solver="PPA", numItermax=1, init_C=init_Cb, init_Y=init_Yb, log=True, ) Xb, Cb = nx.to_numpy(Xb, Cb) np.testing.assert_allclose(C, Cb, atol=1e-06) np.testing.assert_allclose(Cb.shape, (n_samples, n_samples)) np.testing.assert_allclose(X, Xb, atol=1e-06) np.testing.assert_allclose(Xb.shape, (n_samples, ys.shape[1])) np.testing.assert_array_almost_equal( log["err_feature"], nx.to_numpy(*logb["err_feature"]) ) np.testing.assert_array_almost_equal( log["err_structure"], nx.to_numpy(*logb["err_structure"]) ) # add tests with fixed_structures or fixed_features init_C = ot.utils.dist(xalea, xalea) init_C /= init_C.max() init_Cb = nx.from_numpy(init_C) init_Y = np.zeros((n_samples, ys.shape[1]), dtype=ys.dtype) init_Yb = nx.from_numpy(init_Y) fixed_structure, fixed_features = True, False with pytest.raises( ot.utils.UndefinedParameter ): # to raise an error when `fixed_structure=True`and `init_C=None` Xb, Cb = ot.gromov.entropic_fused_gromov_barycenters( n_samples, [ysb, ytb], [C1b, C2b], ps=[p1b, p2b], lambdas=None, fixed_structure=fixed_structure, init_C=None, fixed_features=fixed_features, p=None, max_iter=10, tol=1e-3, ) Xb, Cb = ot.gromov.entropic_fused_gromov_barycenters( n_samples, [ysb, ytb], [C1b, C2b], ps=[p1b, p2b], lambdas=None, fixed_structure=fixed_structure, init_C=init_Cb, fixed_features=fixed_features, max_iter=10, tol=1e-3, ) Xb, Cb = nx.to_numpy(Xb), nx.to_numpy(Cb) np.testing.assert_allclose(Cb, init_Cb) np.testing.assert_allclose(Xb.shape, (n_samples, ys.shape[1])) fixed_structure, fixed_features = False, True with pytest.raises( ot.utils.UndefinedParameter ): # to raise an error when `fixed_features=True`and `init_X=None` Xb, Cb, logb = ot.gromov.entropic_fused_gromov_barycenters( n_samples, [ysb, ytb], [C1b, C2b], [p1b, p2b], lambdas=[0.5, 0.5], fixed_structure=fixed_structure, fixed_features=fixed_features, init_Y=None, p=pb, max_iter=10, tol=1e-3, warmstartT=True, log=True, random_state=98765, verbose=True, ) Xb, Cb, logb = ot.gromov.entropic_fused_gromov_barycenters( n_samples, [ysb, ytb], [C1b, C2b], [p1b, p2b], lambdas=[0.5, 0.5], fixed_structure=fixed_structure, fixed_features=fixed_features, init_Y=init_Yb, p=pb, max_iter=10, tol=1e-3, warmstartT=True, log=True, random_state=98765, verbose=True, ) X, C = nx.to_numpy(Xb), nx.to_numpy(Cb) np.testing.assert_allclose(C.shape, (n_samples, n_samples)) np.testing.assert_allclose(Xb, init_Yb) # test edge cases for fgw barycenters: # C1 as list with pytest.raises(ValueError): C1_list = [list(c) for c in C1b] _, _, _ = ot.gromov.entropic_fused_gromov_barycenters( n_samples, [ysb], [C1_list], [p1b], lambdas=None, fixed_structure=False, fixed_features=False, init_Y=None, p=pb, max_iter=10, tol=1e-3, warmstartT=True, log=True, random_state=98765, verbose=True, ) # p1, p2 as lists with pytest.raises(ValueError): p1_list = list(p1b) p2_list = list(p2b) _, _, _ = ot.gromov.entropic_fused_gromov_barycenters( n_samples, [ysb, ytb], [C1b, C2b], [p1_list, p2_list], lambdas=[0.5, 0.5], fixed_structure=False, fixed_features=False, init_Y=None, p=pb, max_iter=10, tol=1e-3, warmstartT=True, log=True, random_state=98765, verbose=True, ) # unique input structure X, C = ot.gromov.entropic_fused_gromov_barycenters( n_samples, [ys], [C1], [p1], lambdas=None, fixed_structure=False, fixed_features=False, init_Y=init_Y, p=p, max_iter=10, tol=1e-3, warmstartT=True, log=False, random_state=98765, verbose=True, ) Xb, Cb = ot.gromov.entropic_fused_gromov_barycenters( n_samples, [ysb], [C1b], [p1b], lambdas=None, fixed_structure=False, fixed_features=False, init_Y=init_Yb, p=pb, max_iter=10, tol=1e-3, warmstartT=True, log=False, random_state=98765, verbose=True, ) np.testing.assert_allclose(C, Cb, atol=1e-06) np.testing.assert_allclose(X, Xb, atol=1e-06) @pytest.mark.filterwarnings("ignore:divide") def test_gromov_entropic_barycenter(nx): ns = 5 nt = 10 Xs, ys = ot.datasets.make_data_classif("3gauss", ns, random_state=42) Xt, yt = ot.datasets.make_data_classif("3gauss2", nt, random_state=42) C1 = ot.dist(Xs) C2 = ot.dist(Xt) p1 = ot.unif(ns) p2 = ot.unif(nt) n_samples = 2 p = ot.unif(n_samples) C1b, C2b, p1b, p2b, pb = nx.from_numpy(C1, C2, p1, p2, p) with pytest.raises(ValueError): loss_fun = "weird_loss_fun" Cb = ot.gromov.entropic_gromov_barycenters( n_samples, [C1, C2], None, p, [0.5, 0.5], loss_fun, 1e-3, max_iter=10, tol=1e-3, verbose=True, warmstartT=True, random_state=42, ) with pytest.raises(ValueError): stop_criterion = "unknown stop criterion" Cb = ot.gromov.entropic_gromov_barycenters( n_samples, [C1, C2], None, p, [0.5, 0.5], "square_loss", 1e-3, max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=True, warmstartT=True, random_state=42, ) Cb = ot.gromov.entropic_gromov_barycenters( n_samples, [C1, C2], None, p, [0.5, 0.5], "square_loss", 1e-3, max_iter=10, tol=1e-3, verbose=True, warmstartT=True, random_state=42, ) Cbb = nx.to_numpy( ot.gromov.entropic_gromov_barycenters( n_samples, [C1b, C2b], [p1b, p2b], None, [0.5, 0.5], "square_loss", 1e-3, max_iter=10, tol=1e-3, verbose=True, warmstartT=True, random_state=42, ) ) np.testing.assert_allclose(Cb, Cbb, atol=1e-06) np.testing.assert_allclose(Cbb.shape, (n_samples, n_samples)) # test of entropic_gromov_barycenters with `log` on for stop_criterion in ["barycenter", "loss"]: Cb_, err_ = ot.gromov.entropic_gromov_barycenters( n_samples, [C1, C2], [p1, p2], p, None, "square_loss", 1e-3, max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=True, random_state=42, log=True, ) Cbb_, errb_ = ot.gromov.entropic_gromov_barycenters( n_samples, [C1b, C2b], [p1b, p2b], pb, [0.5, 0.5], "square_loss", 1e-3, max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=True, random_state=42, log=True, ) Cbb_ = nx.to_numpy(Cbb_) np.testing.assert_allclose(Cb_, Cbb_, atol=1e-06) np.testing.assert_array_almost_equal(err_["err"], nx.to_numpy(*errb_["err"])) np.testing.assert_allclose(Cbb_.shape, (n_samples, n_samples)) Cb2 = ot.gromov.entropic_gromov_barycenters( n_samples, [C1, C2], [p1, p2], p, [0.5, 0.5], "kl_loss", 1e-3, max_iter=10, tol=1e-3, random_state=42, ) Cb2b = nx.to_numpy( ot.gromov.entropic_gromov_barycenters( n_samples, [C1b, C2b], [p1b, p2b], pb, [0.5, 0.5], "kl_loss", 1e-3, max_iter=10, tol=1e-3, random_state=42, ) ) np.testing.assert_allclose(Cb2, Cb2b, atol=1e-06) np.testing.assert_allclose(Cb2b.shape, (n_samples, n_samples)) # test of entropic_gromov_barycenters with `log` on # providing init_C generator = ot.utils.check_random_state(42) xalea = generator.randn(n_samples, 2) init_C = ot.utils.dist(xalea, xalea) init_C /= init_C.max() init_Cb = nx.from_numpy(init_C) Cb2_, err2_ = ot.gromov.entropic_gromov_barycenters( n_samples, [C1, C2], [p1, p2], p, [0.5, 0.5], "kl_loss", 1e-3, max_iter=10, tol=1e-3, warmstartT=True, verbose=True, random_state=42, init_C=init_C, log=True, ) Cb2b_, err2b_ = ot.gromov.entropic_gromov_barycenters( n_samples, [C1b, C2b], [p1b, p2b], pb, [0.5, 0.5], "kl_loss", 1e-3, max_iter=10, tol=1e-3, warmstartT=True, verbose=True, random_state=42, init_Cb=init_Cb, log=True, ) Cb2b_ = nx.to_numpy(Cb2b_) np.testing.assert_allclose(Cb2_, Cb2b_, atol=1e-06) np.testing.assert_array_almost_equal(err2_["err"], nx.to_numpy(*err2b_["err"])) np.testing.assert_allclose(Cb2b_.shape, (n_samples, n_samples)) # test edge cases for gw barycenters: # C1 as list with pytest.raises(ValueError): C1_list = [list(c) for c in C1b] _, _ = ot.gromov.entropic_gromov_barycenters( n_samples, [C1_list], [p1b], pb, None, "square_loss", 1e-3, max_iter=10, tol=1e-3, warmstartT=True, verbose=True, random_state=42, init_C=None, log=True, ) # p1, p2 as lists with pytest.raises(ValueError): p1_list = list(p1b) p2_list = list(p2b) _, _ = ot.gromov.entropic_gromov_barycenters( n_samples, [C1b, C2b], [p1_list, p2_list], pb, None, "kl_loss", 1e-3, max_iter=10, tol=1e-3, warmstartT=True, verbose=True, random_state=42, init_Cb=None, log=True, ) # unique input structure Cb = ot.gromov.entropic_gromov_barycenters( n_samples, [C1], [p1], p, None, "square_loss", 1e-3, max_iter=10, tol=1e-3, warmstartT=True, verbose=True, random_state=42, init_C=None, log=False, ) Cbb = ot.gromov.entropic_gromov_barycenters( n_samples, [C1b], [p1b], pb, [1.0], "square_loss", 1e-3, max_iter=10, tol=1e-3, warmstartT=True, verbose=True, random_state=42, init_Cb=None, log=False, ) np.testing.assert_allclose(Cb, Cbb, atol=1e-06) np.testing.assert_allclose(Cbb.shape, (n_samples, n_samples)) def test_not_implemented_solver(): # test sinkhorn n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) rng = np.random.RandomState(42) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=rng) xt = xs[::-1].copy() ys = rng.randn(xs.shape[0], 2) yt = ys[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() M = ot.dist(ys, yt) solver = "not_implemented" # entropic gw and fgw with pytest.raises(ValueError): ot.gromov.entropic_gromov_wasserstein( C1, C2, p, q, "square_loss", epsilon=1e-1, solver=solver ) with pytest.raises(ValueError): ot.gromov.entropic_fused_gromov_wasserstein( M, C1, C2, p, q, "square_loss", epsilon=1e-1, solver=solver ) python-pot-0.9.7+dfsg/test/gromov/test_dictionary.py000066400000000000000000000770011525521106200226410ustar00rootroot00000000000000"""Tests for gromov._dictionary.py""" # Author: Cédric Vincent-Cuaz # # License: MIT License import numpy as np import ot def test_gromov_wasserstein_linear_unmixing(nx): n = 4 X1, y1 = ot.datasets.make_data_classif("3gauss", n, random_state=42) X2, y2 = ot.datasets.make_data_classif("3gauss2", n, random_state=42) C1 = ot.dist(X1) C2 = ot.dist(X2) Cdict = np.stack([C1, C2]) p = ot.unif(n) C1b, C2b, Cdictb, pb = nx.from_numpy(C1, C2, Cdict, p) tol = 10 ** (-5) # Tests without regularization reg = 0.0 unmixing1, C1_emb, OT, reconstruction1 = ( ot.gromov.gromov_wasserstein_linear_unmixing( C1, Cdict, reg=reg, p=p, q=p, tol_outer=tol, tol_inner=tol, max_iter_outer=20, max_iter_inner=200, ) ) unmixing1b, C1b_emb, OTb, reconstruction1b = ( ot.gromov.gromov_wasserstein_linear_unmixing( C1b, Cdictb, reg=reg, p=None, q=None, tol_outer=tol, tol_inner=tol, max_iter_outer=20, max_iter_inner=200, ) ) unmixing2, C2_emb, OT, reconstruction2 = ( ot.gromov.gromov_wasserstein_linear_unmixing( C2, Cdict, reg=reg, p=None, q=None, tol_outer=tol, tol_inner=tol, max_iter_outer=20, max_iter_inner=200, ) ) unmixing2b, C2b_emb, OTb, reconstruction2b = ( ot.gromov.gromov_wasserstein_linear_unmixing( C2b, Cdictb, reg=reg, p=pb, q=pb, tol_outer=tol, tol_inner=tol, max_iter_outer=20, max_iter_inner=200, ) ) np.testing.assert_allclose(unmixing1, nx.to_numpy(unmixing1b), atol=5e-06) np.testing.assert_allclose(unmixing1, [1.0, 0.0], atol=5e-01) np.testing.assert_allclose(unmixing2, nx.to_numpy(unmixing2b), atol=5e-06) np.testing.assert_allclose(unmixing2, [0.0, 1.0], atol=5e-01) np.testing.assert_allclose(C1_emb, nx.to_numpy(C1b_emb), atol=1e-06) np.testing.assert_allclose(C2_emb, nx.to_numpy(C2b_emb), atol=1e-06) np.testing.assert_allclose( reconstruction1, nx.to_numpy(reconstruction1b), atol=1e-06 ) np.testing.assert_allclose( reconstruction2, nx.to_numpy(reconstruction2b), atol=1e-06 ) np.testing.assert_allclose(C1b_emb.shape, (n, n)) np.testing.assert_allclose(C2b_emb.shape, (n, n)) # Tests with regularization reg = 0.001 unmixing1, C1_emb, OT, reconstruction1 = ( ot.gromov.gromov_wasserstein_linear_unmixing( C1, Cdict, reg=reg, p=p, q=p, tol_outer=tol, tol_inner=tol, max_iter_outer=20, max_iter_inner=200, ) ) unmixing1b, C1b_emb, OTb, reconstruction1b = ( ot.gromov.gromov_wasserstein_linear_unmixing( C1b, Cdictb, reg=reg, p=None, q=None, tol_outer=tol, tol_inner=tol, max_iter_outer=20, max_iter_inner=200, ) ) unmixing2, C2_emb, OT, reconstruction2 = ( ot.gromov.gromov_wasserstein_linear_unmixing( C2, Cdict, reg=reg, p=None, q=None, tol_outer=tol, tol_inner=tol, max_iter_outer=20, max_iter_inner=200, ) ) unmixing2b, C2b_emb, OTb, reconstruction2b = ( ot.gromov.gromov_wasserstein_linear_unmixing( C2b, Cdictb, reg=reg, p=pb, q=pb, tol_outer=tol, tol_inner=tol, max_iter_outer=20, max_iter_inner=200, ) ) np.testing.assert_allclose(unmixing1, nx.to_numpy(unmixing1b), atol=1e-06) np.testing.assert_allclose(unmixing1, [1.0, 0.0], atol=1e-01) np.testing.assert_allclose(unmixing2, nx.to_numpy(unmixing2b), atol=1e-06) np.testing.assert_allclose(unmixing2, [0.0, 1.0], atol=1e-01) np.testing.assert_allclose(C1_emb, nx.to_numpy(C1b_emb), atol=1e-06) np.testing.assert_allclose(C2_emb, nx.to_numpy(C2b_emb), atol=1e-06) np.testing.assert_allclose( reconstruction1, nx.to_numpy(reconstruction1b), atol=1e-06 ) np.testing.assert_allclose( reconstruction2, nx.to_numpy(reconstruction2b), atol=1e-06 ) np.testing.assert_allclose(C1b_emb.shape, (n, n)) np.testing.assert_allclose(C2b_emb.shape, (n, n)) def test_gromov_wasserstein_dictionary_learning(nx): # create dataset composed from 2 structures which are repeated 5 times shape = 4 n_samples = 2 n_atoms = 2 projection = "nonnegative_symmetric" X1, y1 = ot.datasets.make_data_classif("3gauss", shape, random_state=42) X2, y2 = ot.datasets.make_data_classif("3gauss2", shape, random_state=42) C1 = ot.dist(X1) C2 = ot.dist(X2) Cs = [C1.copy() for _ in range(n_samples // 2)] + [ C2.copy() for _ in range(n_samples // 2) ] ps = [ot.unif(shape) for _ in range(n_samples)] q = ot.unif(shape) # Provide initialization for the graph dictionary of shape (n_atoms, shape, shape) # following the same procedure than implemented in gromov_wasserstein_dictionary_learning. dataset_means = [C.mean() for C in Cs] rng = np.random.RandomState(0) Cdict_init = rng.normal( loc=np.mean(dataset_means), scale=np.std(dataset_means), size=(n_atoms, shape, shape), ) if projection == "nonnegative_symmetric": Cdict_init = 0.5 * (Cdict_init + Cdict_init.transpose((0, 2, 1))) Cdict_init[Cdict_init < 0.0] = 0.0 Csb = nx.from_numpy(*Cs) psb = nx.from_numpy(*ps) qb, Cdict_initb = nx.from_numpy(q, Cdict_init) # Test: compare reconstruction error using initial dictionary and dictionary learned using this initialization # > Compute initial reconstruction of samples on this random dictionary without backend use_adam_optimizer = True verbose = False tol = 10 ** (-5) epochs = 1 initial_total_reconstruction = 0 for i in range(n_samples): _, _, _, reconstruction = ot.gromov.gromov_wasserstein_linear_unmixing( Cs[i], Cdict_init, p=ps[i], q=q, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, ) initial_total_reconstruction += reconstruction # > Learn the dictionary using this init Cdict, log = ot.gromov.gromov_wasserstein_dictionary_learning( Cs, D=n_atoms, nt=shape, ps=ps, q=q, Cdict_init=Cdict_init, epochs=epochs, batch_size=2 * n_samples, learning_rate=1.0, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, projection=projection, use_log=False, use_adam_optimizer=use_adam_optimizer, verbose=verbose, ) # > Compute reconstruction of samples on learned dictionary without backend total_reconstruction = 0 for i in range(n_samples): _, _, _, reconstruction = ot.gromov.gromov_wasserstein_linear_unmixing( Cs[i], Cdict, p=None, q=None, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, ) total_reconstruction += reconstruction np.testing.assert_array_less(total_reconstruction, initial_total_reconstruction) # Test: Perform same experiments after going through backend Cdictb, log = ot.gromov.gromov_wasserstein_dictionary_learning( Csb, D=n_atoms, nt=shape, ps=None, q=None, Cdict_init=Cdict_initb, epochs=epochs, batch_size=n_samples, learning_rate=1.0, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, projection=projection, use_log=False, use_adam_optimizer=use_adam_optimizer, verbose=verbose, ) # Compute reconstruction of samples on learned dictionary total_reconstruction_b = 0 for i in range(n_samples): _, _, _, reconstruction = ot.gromov.gromov_wasserstein_linear_unmixing( Csb[i], Cdictb, p=psb[i], q=qb, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, ) total_reconstruction_b += reconstruction total_reconstruction_b = nx.to_numpy(total_reconstruction_b) np.testing.assert_array_less(total_reconstruction_b, initial_total_reconstruction) np.testing.assert_allclose(total_reconstruction_b, total_reconstruction, atol=1e-05) np.testing.assert_allclose(total_reconstruction_b, total_reconstruction, atol=1e-05) np.testing.assert_allclose(Cdict, nx.to_numpy(Cdictb), atol=1e-03) # Test: Perform same comparison without providing the initial dictionary being an optional input # knowing than the initialization scheme is the same than implemented to set the benchmarked initialization. Cdict_bis, log = ot.gromov.gromov_wasserstein_dictionary_learning( Cs, D=n_atoms, nt=shape, ps=None, q=None, Cdict_init=None, epochs=epochs, batch_size=n_samples, learning_rate=1.0, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, projection=projection, use_log=False, use_adam_optimizer=use_adam_optimizer, verbose=verbose, random_state=0, ) # > Compute reconstruction of samples on learned dictionary total_reconstruction_bis = 0 for i in range(n_samples): _, _, _, reconstruction = ot.gromov.gromov_wasserstein_linear_unmixing( Cs[i], Cdict_bis, p=ps[i], q=q, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, ) total_reconstruction_bis += reconstruction np.testing.assert_allclose( total_reconstruction_bis, total_reconstruction, atol=1e-05 ) # Test: Same after going through backend Cdictb_bis, log = ot.gromov.gromov_wasserstein_dictionary_learning( Csb, D=n_atoms, nt=shape, ps=psb, q=qb, Cdict_init=None, epochs=epochs, batch_size=n_samples, learning_rate=1.0, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, projection=projection, use_log=False, use_adam_optimizer=use_adam_optimizer, verbose=verbose, random_state=0, ) # > Compute reconstruction of samples on learned dictionary total_reconstruction_b_bis = 0 for i in range(n_samples): _, _, _, reconstruction = ot.gromov.gromov_wasserstein_linear_unmixing( Csb[i], Cdictb_bis, p=None, q=None, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, ) total_reconstruction_b_bis += reconstruction total_reconstruction_b_bis = nx.to_numpy(total_reconstruction_b_bis) np.testing.assert_allclose( total_reconstruction_b_bis, total_reconstruction_b, atol=1e-05 ) np.testing.assert_allclose(Cdict_bis, nx.to_numpy(Cdictb_bis), atol=1e-03) # Test: Perform same comparison without providing the initial dictionary being an optional input # and testing other optimization settings untested until now. # We pass previously estimated dictionaries to speed up the process. use_adam_optimizer = False verbose = True use_log = True Cdict_bis2, log = ot.gromov.gromov_wasserstein_dictionary_learning( Cs, D=n_atoms, nt=shape, ps=ps, q=q, Cdict_init=Cdict, epochs=epochs, batch_size=n_samples, learning_rate=10.0, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, projection=projection, use_log=use_log, use_adam_optimizer=use_adam_optimizer, verbose=verbose, random_state=0, ) # > Compute reconstruction of samples on learned dictionary total_reconstruction_bis2 = 0 for i in range(n_samples): _, _, _, reconstruction = ot.gromov.gromov_wasserstein_linear_unmixing( Cs[i], Cdict_bis2, p=ps[i], q=q, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, ) total_reconstruction_bis2 += reconstruction np.testing.assert_array_less(total_reconstruction_bis2, total_reconstruction) # Test: Same after going through backend Cdictb_bis2, log = ot.gromov.gromov_wasserstein_dictionary_learning( Csb, D=n_atoms, nt=shape, ps=psb, q=qb, Cdict_init=Cdictb, epochs=epochs, batch_size=n_samples, learning_rate=10.0, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, projection=projection, use_log=use_log, use_adam_optimizer=use_adam_optimizer, verbose=verbose, random_state=0, ) # > Compute reconstruction of samples on learned dictionary total_reconstruction_b_bis2 = 0 for i in range(n_samples): _, _, _, reconstruction = ot.gromov.gromov_wasserstein_linear_unmixing( Csb[i], Cdictb_bis2, p=psb[i], q=qb, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, ) total_reconstruction_b_bis2 += reconstruction total_reconstruction_b_bis2 = nx.to_numpy(total_reconstruction_b_bis2) np.testing.assert_allclose( total_reconstruction_b_bis2, total_reconstruction_bis2, atol=1e-05 ) def test_fused_gromov_wasserstein_linear_unmixing(nx): n = 4 X1, y1 = ot.datasets.make_data_classif("3gauss", n, random_state=42) X2, y2 = ot.datasets.make_data_classif("3gauss2", n, random_state=42) F, y = ot.datasets.make_data_classif("3gauss", n, random_state=42) C1 = ot.dist(X1) C2 = ot.dist(X2) Cdict = np.stack([C1, C2]) Ydict = np.stack([F, F]) p = ot.unif(n) C1b, C2b, Fb, Cdictb, Ydictb, pb = nx.from_numpy(C1, C2, F, Cdict, Ydict, p) # Tests without regularization reg = 0.0 unmixing1, C1_emb, Y1_emb, OT, reconstruction1 = ( ot.gromov.fused_gromov_wasserstein_linear_unmixing( C1, F, Cdict, Ydict, p=p, q=p, alpha=0.5, reg=reg, tol_outer=10 ** (-6), tol_inner=10 ** (-6), max_iter_outer=10, max_iter_inner=50, ) ) unmixing1b, C1b_emb, Y1b_emb, OTb, reconstruction1b = ( ot.gromov.fused_gromov_wasserstein_linear_unmixing( C1b, Fb, Cdictb, Ydictb, p=None, q=None, alpha=0.5, reg=reg, tol_outer=10 ** (-6), tol_inner=10 ** (-6), max_iter_outer=10, max_iter_inner=50, ) ) unmixing2, C2_emb, Y2_emb, OT, reconstruction2 = ( ot.gromov.fused_gromov_wasserstein_linear_unmixing( C2, F, Cdict, Ydict, p=None, q=None, alpha=0.5, reg=reg, tol_outer=10 ** (-6), tol_inner=10 ** (-6), max_iter_outer=10, max_iter_inner=50, ) ) unmixing2b, C2b_emb, Y2b_emb, OTb, reconstruction2b = ( ot.gromov.fused_gromov_wasserstein_linear_unmixing( C2b, Fb, Cdictb, Ydictb, p=pb, q=pb, alpha=0.5, reg=reg, tol_outer=10 ** (-6), tol_inner=10 ** (-6), max_iter_outer=10, max_iter_inner=50, ) ) np.testing.assert_allclose(unmixing1, nx.to_numpy(unmixing1b), atol=4e-06) np.testing.assert_allclose(unmixing1, [1.0, 0.0], atol=4e-01) np.testing.assert_allclose(unmixing2, nx.to_numpy(unmixing2b), atol=4e-06) np.testing.assert_allclose(unmixing2, [0.0, 1.0], atol=4e-01) np.testing.assert_allclose(C1_emb, nx.to_numpy(C1b_emb), atol=1e-03) np.testing.assert_allclose(C2_emb, nx.to_numpy(C2b_emb), atol=1e-03) np.testing.assert_allclose(Y1_emb, nx.to_numpy(Y1b_emb), atol=1e-03) np.testing.assert_allclose(Y2_emb, nx.to_numpy(Y2b_emb), atol=1e-03) np.testing.assert_allclose( reconstruction1, nx.to_numpy(reconstruction1b), atol=1e-06 ) np.testing.assert_allclose( reconstruction2, nx.to_numpy(reconstruction2b), atol=1e-06 ) np.testing.assert_allclose(C1b_emb.shape, (n, n)) np.testing.assert_allclose(C2b_emb.shape, (n, n)) # Tests with regularization reg = 0.001 unmixing1, C1_emb, Y1_emb, OT, reconstruction1 = ( ot.gromov.fused_gromov_wasserstein_linear_unmixing( C1, F, Cdict, Ydict, p=p, q=p, alpha=0.5, reg=reg, tol_outer=10 ** (-6), tol_inner=10 ** (-6), max_iter_outer=10, max_iter_inner=50, ) ) unmixing1b, C1b_emb, Y1b_emb, OTb, reconstruction1b = ( ot.gromov.fused_gromov_wasserstein_linear_unmixing( C1b, Fb, Cdictb, Ydictb, p=None, q=None, alpha=0.5, reg=reg, tol_outer=10 ** (-6), tol_inner=10 ** (-6), max_iter_outer=10, max_iter_inner=50, ) ) unmixing2, C2_emb, Y2_emb, OT, reconstruction2 = ( ot.gromov.fused_gromov_wasserstein_linear_unmixing( C2, F, Cdict, Ydict, p=None, q=None, alpha=0.5, reg=reg, tol_outer=10 ** (-6), tol_inner=10 ** (-6), max_iter_outer=10, max_iter_inner=50, ) ) unmixing2b, C2b_emb, Y2b_emb, OTb, reconstruction2b = ( ot.gromov.fused_gromov_wasserstein_linear_unmixing( C2b, Fb, Cdictb, Ydictb, p=pb, q=pb, alpha=0.5, reg=reg, tol_outer=10 ** (-6), tol_inner=10 ** (-6), max_iter_outer=10, max_iter_inner=50, ) ) np.testing.assert_allclose(unmixing1, nx.to_numpy(unmixing1b), atol=1e-06) np.testing.assert_allclose(unmixing1, [1.0, 0.0], atol=1e-01) np.testing.assert_allclose(unmixing2, nx.to_numpy(unmixing2b), atol=1e-06) np.testing.assert_allclose(unmixing2, [0.0, 1.0], atol=1e-01) np.testing.assert_allclose(C1_emb, nx.to_numpy(C1b_emb), atol=1e-03) np.testing.assert_allclose(C2_emb, nx.to_numpy(C2b_emb), atol=1e-03) np.testing.assert_allclose(Y1_emb, nx.to_numpy(Y1b_emb), atol=1e-03) np.testing.assert_allclose(Y2_emb, nx.to_numpy(Y2b_emb), atol=1e-03) np.testing.assert_allclose( reconstruction1, nx.to_numpy(reconstruction1b), atol=1e-06 ) np.testing.assert_allclose( reconstruction2, nx.to_numpy(reconstruction2b), atol=1e-06 ) np.testing.assert_allclose(C1b_emb.shape, (n, n)) np.testing.assert_allclose(C2b_emb.shape, (n, n)) def test_fused_gromov_wasserstein_dictionary_learning(nx): # create dataset composed from 2 structures which are repeated 5 times shape = 4 n_samples = 2 n_atoms = 2 projection = "nonnegative_symmetric" X1, y1 = ot.datasets.make_data_classif("3gauss", shape, random_state=42) X2, y2 = ot.datasets.make_data_classif("3gauss2", shape, random_state=42) F, y = ot.datasets.make_data_classif("3gauss", shape, random_state=42) C1 = ot.dist(X1) C2 = ot.dist(X2) Cs = [C1.copy() for _ in range(n_samples // 2)] + [ C2.copy() for _ in range(n_samples // 2) ] Ys = [F.copy() for _ in range(n_samples)] ps = [ot.unif(shape) for _ in range(n_samples)] q = ot.unif(shape) # Provide initialization for the graph dictionary of shape (n_atoms, shape, shape) # following the same procedure than implemented in gromov_wasserstein_dictionary_learning. dataset_structure_means = [C.mean() for C in Cs] rng = np.random.RandomState(0) Cdict_init = rng.normal( loc=np.mean(dataset_structure_means), scale=np.std(dataset_structure_means), size=(n_atoms, shape, shape), ) if projection == "nonnegative_symmetric": Cdict_init = 0.5 * (Cdict_init + Cdict_init.transpose((0, 2, 1))) Cdict_init[Cdict_init < 0.0] = 0.0 dataset_feature_means = np.stack([Y.mean(axis=0) for Y in Ys]) Ydict_init = rng.normal( loc=dataset_feature_means.mean(axis=0), scale=dataset_feature_means.std(axis=0), size=(n_atoms, shape, 2), ) Csb = nx.from_numpy(*Cs) Ysb = nx.from_numpy(*Ys) psb = nx.from_numpy(*ps) qb, Cdict_initb, Ydict_initb = nx.from_numpy(q, Cdict_init, Ydict_init) # Test: Compute initial reconstruction of samples on this random dictionary alpha = 0.5 use_adam_optimizer = True verbose = False tol = 1e-05 epochs = 1 initial_total_reconstruction = 0 for i in range(n_samples): _, _, _, _, reconstruction = ot.gromov.fused_gromov_wasserstein_linear_unmixing( Cs[i], Ys[i], Cdict_init, Ydict_init, p=ps[i], q=q, alpha=alpha, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, ) initial_total_reconstruction += reconstruction # > Learn a dictionary using this given initialization and check that the reconstruction loss # on the learned dictionary is lower than the one using its initialization. Cdict, Ydict, log = ot.gromov.fused_gromov_wasserstein_dictionary_learning( Cs, Ys, D=n_atoms, nt=shape, ps=ps, q=q, Cdict_init=Cdict_init, Ydict_init=Ydict_init, epochs=epochs, batch_size=n_samples, learning_rate_C=1.0, learning_rate_Y=1.0, alpha=alpha, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, projection=projection, use_log=False, use_adam_optimizer=use_adam_optimizer, verbose=verbose, ) # > Compute reconstruction of samples on learned dictionary total_reconstruction = 0 for i in range(n_samples): _, _, _, _, reconstruction = ot.gromov.fused_gromov_wasserstein_linear_unmixing( Cs[i], Ys[i], Cdict, Ydict, p=None, q=None, alpha=alpha, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, ) total_reconstruction += reconstruction # Compare both np.testing.assert_array_less(total_reconstruction, initial_total_reconstruction) # Test: Perform same experiments after going through backend Cdictb, Ydictb, log = ot.gromov.fused_gromov_wasserstein_dictionary_learning( Csb, Ysb, D=n_atoms, nt=shape, ps=None, q=None, Cdict_init=Cdict_initb, Ydict_init=Ydict_initb, epochs=epochs, batch_size=2 * n_samples, learning_rate_C=1.0, learning_rate_Y=1.0, alpha=alpha, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, projection=projection, use_log=False, use_adam_optimizer=use_adam_optimizer, verbose=verbose, random_state=0, ) # > Compute reconstruction of samples on learned dictionary total_reconstruction_b = 0 for i in range(n_samples): _, _, _, _, reconstruction = ot.gromov.fused_gromov_wasserstein_linear_unmixing( Csb[i], Ysb[i], Cdictb, Ydictb, p=psb[i], q=qb, alpha=alpha, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, ) total_reconstruction_b += reconstruction total_reconstruction_b = nx.to_numpy(total_reconstruction_b) np.testing.assert_array_less(total_reconstruction_b, initial_total_reconstruction) np.testing.assert_allclose(total_reconstruction_b, total_reconstruction, atol=1e-05) np.testing.assert_allclose(Cdict, nx.to_numpy(Cdictb), atol=1e-03) np.testing.assert_allclose(Ydict, nx.to_numpy(Ydictb), atol=1e-03) # Test: Perform similar experiment without providing the initial dictionary being an optional input Cdict_bis, Ydict_bis, log = ot.gromov.fused_gromov_wasserstein_dictionary_learning( Cs, Ys, D=n_atoms, nt=shape, ps=None, q=None, Cdict_init=None, Ydict_init=None, epochs=epochs, batch_size=n_samples, learning_rate_C=1.0, learning_rate_Y=1.0, alpha=alpha, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, projection=projection, use_log=False, use_adam_optimizer=use_adam_optimizer, verbose=verbose, random_state=0, ) # > Compute reconstruction of samples on learned dictionary total_reconstruction_bis = 0 for i in range(n_samples): _, _, _, _, reconstruction = ot.gromov.fused_gromov_wasserstein_linear_unmixing( Cs[i], Ys[i], Cdict_bis, Ydict_bis, p=ps[i], q=q, alpha=alpha, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, ) total_reconstruction_bis += reconstruction np.testing.assert_allclose( total_reconstruction_bis, total_reconstruction, atol=1e-05 ) # > Same after going through backend Cdictb_bis, Ydictb_bis, log = ( ot.gromov.fused_gromov_wasserstein_dictionary_learning( Csb, Ysb, D=n_atoms, nt=shape, ps=None, q=None, Cdict_init=None, Ydict_init=None, epochs=epochs, batch_size=n_samples, learning_rate_C=1.0, learning_rate_Y=1.0, alpha=alpha, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, projection=projection, use_log=False, use_adam_optimizer=use_adam_optimizer, verbose=verbose, random_state=0, ) ) # > Compute reconstruction of samples on learned dictionary total_reconstruction_b_bis = 0 for i in range(n_samples): _, _, _, _, reconstruction = ot.gromov.fused_gromov_wasserstein_linear_unmixing( Csb[i], Ysb[i], Cdictb_bis, Ydictb_bis, p=psb[i], q=qb, alpha=alpha, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, ) total_reconstruction_b_bis += reconstruction total_reconstruction_b_bis = nx.to_numpy(total_reconstruction_b_bis) np.testing.assert_allclose( total_reconstruction_b_bis, total_reconstruction_b, atol=1e-05 ) # Test: without using adam optimizer, with log and verbose set to True use_adam_optimizer = False verbose = True use_log = True # > Experiment providing previously estimated dictionary to speed up the test compared to providing initial random init. Cdict_bis2, Ydict_bis2, log = ( ot.gromov.fused_gromov_wasserstein_dictionary_learning( Cs, Ys, D=n_atoms, nt=shape, ps=ps, q=q, Cdict_init=Cdict, Ydict_init=Ydict, epochs=epochs, batch_size=n_samples, learning_rate_C=10.0, learning_rate_Y=10.0, alpha=alpha, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, projection=projection, use_log=use_log, use_adam_optimizer=use_adam_optimizer, verbose=verbose, random_state=0, ) ) # > Compute reconstruction of samples on learned dictionary total_reconstruction_bis2 = 0 for i in range(n_samples): _, _, _, _, reconstruction = ot.gromov.fused_gromov_wasserstein_linear_unmixing( Cs[i], Ys[i], Cdict_bis2, Ydict_bis2, p=ps[i], q=q, alpha=alpha, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, ) total_reconstruction_bis2 += reconstruction np.testing.assert_array_less(total_reconstruction_bis2, total_reconstruction) # > Same after going through backend Cdictb_bis2, Ydictb_bis2, log = ( ot.gromov.fused_gromov_wasserstein_dictionary_learning( Csb, Ysb, D=n_atoms, nt=shape, ps=None, q=None, Cdict_init=Cdictb, Ydict_init=Ydictb, epochs=epochs, batch_size=n_samples, learning_rate_C=10.0, learning_rate_Y=10.0, alpha=alpha, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, projection=projection, use_log=use_log, use_adam_optimizer=use_adam_optimizer, verbose=verbose, random_state=0, ) ) # > Compute reconstruction of samples on learned dictionary total_reconstruction_b_bis2 = 0 for i in range(n_samples): _, _, _, _, reconstruction = ot.gromov.fused_gromov_wasserstein_linear_unmixing( Csb[i], Ysb[i], Cdictb_bis2, Ydictb_bis2, p=None, q=None, alpha=alpha, reg=0.0, tol_outer=tol, tol_inner=tol, max_iter_outer=10, max_iter_inner=50, ) total_reconstruction_b_bis2 += reconstruction # > Compare results with/without backend total_reconstruction_b_bis2 = nx.to_numpy(total_reconstruction_b_bis2) np.testing.assert_allclose( total_reconstruction_bis2, total_reconstruction_b_bis2, atol=1e-05 ) python-pot-0.9.7+dfsg/test/gromov/test_estimators.py000066400000000000000000000071341525521106200226660ustar00rootroot00000000000000"""Tests for gromov._estimators.py""" # Author: Rémi Flamary # Tanguy Kerdoncuff # Cédric Vincent-Cuaz # # License: MIT License import numpy as np import pytest import ot from ot.backend import NumpyBackend def test_pointwise_gromov(nx): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=42) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() C1b, C2b, pb, qb = nx.from_numpy(C1, C2, p, q) def loss(x, y): return np.abs(x - y) def lossb(x, y): return nx.abs(x - y) G, log = ot.gromov.pointwise_gromov_wasserstein( C1, C2, p, q, loss, max_iter=100, log=True, verbose=True, random_state=42 ) G = NumpyBackend().todense(G) Gb, logb = ot.gromov.pointwise_gromov_wasserstein( C1b, C2b, pb, qb, lossb, max_iter=100, log=True, verbose=True, random_state=42 ) Gb = nx.to_numpy(nx.todense(Gb)) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(float(logb["gw_dist_estimated"]), 0.0, atol=1e-08) np.testing.assert_allclose(float(logb["gw_dist_std"]), 0.0, atol=1e-08) G, log = ot.gromov.pointwise_gromov_wasserstein( C1, C2, p, q, loss, max_iter=100, alpha=0.1, log=True, verbose=True, random_state=42, ) G = NumpyBackend().todense(G) Gb, logb = ot.gromov.pointwise_gromov_wasserstein( C1b, C2b, pb, qb, lossb, max_iter=100, alpha=0.1, log=True, verbose=True, random_state=42, ) Gb = nx.to_numpy(nx.todense(Gb)) np.testing.assert_allclose(G, Gb, atol=1e-06) @pytest.skip_backend("tf", reason="test very slow with tf backend") @pytest.skip_backend("jax", reason="test very slow with jax backend") def test_sampled_gromov(nx): n_samples = 5 # nb samples mu_s = np.array([0, 0], dtype=np.float64) cov_s = np.array([[1, 0], [0, 1]], dtype=np.float64) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=42) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() C1b, C2b, pb, qb = nx.from_numpy(C1, C2, p, q) def loss(x, y): return np.abs(x - y) def lossb(x, y): return nx.abs(x - y) G, log = ot.gromov.sampled_gromov_wasserstein( C1, C2, p, q, loss, max_iter=20, nb_samples_grad=2, epsilon=1, log=True, verbose=True, random_state=42, ) Gb, logb = ot.gromov.sampled_gromov_wasserstein( C1b, C2b, pb, qb, lossb, max_iter=20, nb_samples_grad=2, epsilon=1, log=True, verbose=True, random_state=42, ) Gb = nx.to_numpy(Gb) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov python-pot-0.9.7+dfsg/test/gromov/test_fugw.py000066400000000000000000000712361525521106200214500ustar00rootroot00000000000000"""Tests for module Fused Unbalanced Gromov-Wasserstein""" # Author: Quang Huy Tran # # License: MIT License import itertools import numpy as np import ot import pytest from ot.gromov._unbalanced import ( fused_unbalanced_gromov_wasserstein, fused_unbalanced_gromov_wasserstein2, fused_unbalanced_across_spaces_divergence, ) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence", itertools.product(["mm", "lbfgsb"], ["kl", "l2"]) ) def test_sanity(nx, unbalanced_solver, divergence): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] # linear part M_samp = np.ones((n_samples, n_samples)) np.fill_diagonal(np.fliplr(M_samp), 0) M_samp_nx = nx.from_numpy(M_samp) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() C1b, C2b, pb, qb, G0b = nx.from_numpy(C1, C2, p, q, G0) reg_m = (100, 50) eps = 0 alpha = 0.5 max_iter_ot = 100 max_iter = 100 tol = 1e-7 tol_ot = 1e-7 # test couplings anti_id_sample = np.flipud(np.eye(n_samples, n_samples)) / n_samples pi_sample, pi_feature = fused_unbalanced_gromov_wasserstein( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=G0, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx, pi_feature_nx = fused_unbalanced_gromov_wasserstein( C1b, C2b, wx=pb, wy=qb, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp_nx, init_duals=None, init_pi=G0b, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample, anti_id_sample, atol=1e-03) np.testing.assert_allclose(pi_sample_nx, pi_sample, atol=1e-06) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-06) # test divergence fugw = fused_unbalanced_gromov_wasserstein2( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=G0, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) fugw_nx = fused_unbalanced_gromov_wasserstein2( C1b, C2b, wx=pb, wy=qb, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp_nx, init_duals=None, init_pi=G0b, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) fugw_nx = nx.to_numpy(fugw_nx) np.testing.assert_allclose(fugw, fugw_nx, atol=1e-08) np.testing.assert_allclose(fugw, 0, atol=1e-02) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence, eps", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1e-2] ), ) def test_init_plans(nx, unbalanced_solver, divergence, eps): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] # linear part M_samp = np.ones((n_samples, n_samples)) np.fill_diagonal(np.fliplr(M_samp), 0) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() reg_m = (100, 50) alpha = 0.5 max_iter_ot = 5 max_iter = 5 tol = 1e-5 tol_ot = 1e-5 pi_sample, pi_feature = fused_unbalanced_gromov_wasserstein( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=G0, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx, pi_feature_nx = fused_unbalanced_gromov_wasserstein( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample, pi_sample_nx, atol=1e-06) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-06) # test divergence fugw = fused_unbalanced_gromov_wasserstein2( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=G0, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) fugw_nx = fused_unbalanced_gromov_wasserstein2( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) fugw_nx = nx.to_numpy(fugw_nx) np.testing.assert_allclose(fugw, fugw_nx, atol=1e-08) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence, eps", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1e-2] ), ) def test_init_duals(nx, unbalanced_solver, divergence, eps): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) dual1, dual2 = nx.from_numpy(np.zeros_like(p), np.zeros_like(q)) init_duals = (dual1, dual2) # linear part M_samp = np.ones((n_samples, n_samples)) np.fill_diagonal(np.fliplr(M_samp), 0) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() C1, C2, p, q, M_samp = nx.from_numpy(C1, C2, p, q, M_samp) reg_m = (100, 50) alpha = 0.5 max_iter_ot = 5 max_iter = 5 tol = 1e-5 tol_ot = 1e-5 pi_sample, pi_feature = fused_unbalanced_gromov_wasserstein( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx, pi_feature_nx = fused_unbalanced_gromov_wasserstein( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=init_duals, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample, pi_sample_nx, atol=1e-06) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-06) # test divergence fugw = fused_unbalanced_gromov_wasserstein2( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) fugw_nx = fused_unbalanced_gromov_wasserstein2( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=init_duals, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) fugw_nx = nx.to_numpy(fugw_nx) np.testing.assert_allclose(fugw, fugw_nx, atol=1e-08) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence, eps", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1e-2] ), ) def test_reg_marginals(nx, unbalanced_solver, divergence, eps): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] # linear part M_samp = np.ones((n_samples, n_samples)) np.fill_diagonal(np.fliplr(M_samp), 0) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() alpha = 0.5 max_iter_ot = 5 max_iter = 5 tol = 1e-5 tol_ot = 1e-5 reg_m = 100 full_list_reg_m = [reg_m, reg_m] full_tuple_reg_m = (reg_m, reg_m) tuple_reg_m, list_reg_m = (reg_m), [reg_m] list_options = [full_tuple_reg_m, tuple_reg_m, full_list_reg_m, list_reg_m] pi_sample, pi_feature = fused_unbalanced_gromov_wasserstein( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=G0, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) fugw = fused_unbalanced_gromov_wasserstein2( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=G0, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) for opt in list_options: pi_sample_nx, pi_feature_nx = fused_unbalanced_gromov_wasserstein( C1, C2, wx=p, wy=q, reg_marginals=opt, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample, pi_sample_nx, atol=1e-06) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-06) # test divergence fugw_nx = fused_unbalanced_gromov_wasserstein2( C1, C2, wx=p, wy=q, reg_marginals=opt, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) fugw_nx = nx.to_numpy(fugw_nx) np.testing.assert_allclose(fugw, fugw_nx, atol=1e-08) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence, eps", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1e-2] ), ) def test_log(nx, unbalanced_solver, divergence, eps): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) # linear part M_samp = np.ones((n_samples, n_samples)) np.fill_diagonal(np.fliplr(M_samp), 0) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() reg_m = (100, 50) alpha = 0.5 max_iter_ot = 5 max_iter = 5 tol = 1e-5 tol_ot = 1e-5 pi_sample, pi_feature = fused_unbalanced_gromov_wasserstein( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx, pi_feature_nx, log = fused_unbalanced_gromov_wasserstein( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=True, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample, pi_sample_nx, atol=1e-06) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-06) # test divergence fugw = fused_unbalanced_gromov_wasserstein2( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) fugw_nx, log = fused_unbalanced_gromov_wasserstein2( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=True, verbose=False, ) fugw_nx = nx.to_numpy(fugw_nx) np.testing.assert_allclose(fugw, fugw_nx, atol=1e-08) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence, eps", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1e-2] ), ) def test_marginals(nx, unbalanced_solver, divergence, eps): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) # linear part M_samp = np.ones((n_samples, n_samples)) np.fill_diagonal(np.fliplr(M_samp), 0) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() reg_m = (100, 50) alpha = 0.5 max_iter_ot = 5 max_iter = 5 tol = 1e-5 tol_ot = 1e-5 pi_sample, pi_feature = fused_unbalanced_gromov_wasserstein( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx, pi_feature_nx = fused_unbalanced_gromov_wasserstein( C1, C2, wx=None, wy=None, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample, pi_sample_nx, atol=1e-06) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-06) # test divergence fugw = fused_unbalanced_gromov_wasserstein2( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) fugw_nx = fused_unbalanced_gromov_wasserstein2( C1, C2, wx=None, wy=None, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M=M_samp, init_duals=None, init_pi=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) fugw_nx = nx.to_numpy(fugw_nx) np.testing.assert_allclose(fugw, fugw_nx, atol=1e-08) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") def test_raise_value_error(nx): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() C1b, C2b, pb, qb, G0b = nx.from_numpy(C1, C2, p, q, G0) eps = 1e-2 reg_m = (10, 100) max_iter_ot = 5 max_iter = 5 tol = 1e-6 tol_ot = 1e-6 # raise error of divergence def fugw_div(divergence): return fused_unbalanced_gromov_wasserstein( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver="mm", alpha=0, M=None, init_duals=None, init_pi=G0, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) def fugw_div_nx(divergence): return fused_unbalanced_gromov_wasserstein( C1b, C2b, wx=pb, wy=qb, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver="mm", alpha=0, M=None, init_duals=None, init_pi=G0b, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) np.testing.assert_raises(NotImplementedError, fugw_div, "div_not_existed") np.testing.assert_raises(NotImplementedError, fugw_div_nx, "div_not_existed") # raise error of solver def fugw_solver(unbalanced_solver): return fused_unbalanced_gromov_wasserstein( C1, C2, wx=p, wy=q, reg_marginals=reg_m, epsilon=eps, divergence="kl", unbalanced_solver=unbalanced_solver, alpha=0, M=None, init_duals=None, init_pi=G0, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) def fugw_solver_nx(unbalanced_solver): return fused_unbalanced_gromov_wasserstein( C1b, C2b, wx=pb, wy=qb, reg_marginals=reg_m, epsilon=eps, divergence="kl", unbalanced_solver=unbalanced_solver, alpha=0, M=None, init_duals=None, init_pi=G0b, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) np.testing.assert_raises(NotImplementedError, fugw_solver, "solver_not_existed") np.testing.assert_raises(NotImplementedError, fugw_solver_nx, "solver_not_existed") @pytest.mark.parametrize( "unbalanced_solver, divergence, eps", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1e-2] ), ) def test_fused_unbalanced_across_spaces_divergence_wrong_reg_type( nx, unbalanced_solver, divergence, eps ): n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) rng = np.random.RandomState(75) y = rng.randn(n, 2) x, y = nx.from_numpy(x), nx.from_numpy(y) reg_m = 100 def reg_type(reg_type): return fused_unbalanced_across_spaces_divergence( X=x, Y=y, reg_marginals=reg_m, epsilon=eps, reg_type=reg_type, divergence=divergence, unbalanced_solver=unbalanced_solver, ) np.testing.assert_raises(NotImplementedError, reg_type, "reg_type_not_existed") @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence, eps, reg_type", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1e-2], ["independent", "joint"], ), ) def test_fused_unbalanced_across_spaces_divergence_log( nx, unbalanced_solver, divergence, eps, reg_type ): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) xs_nx, xt_nx = nx.from_numpy(xs, xt) px_s_nx, px_f_nx, py_s_nx, py_f_nx = nx.from_numpy(px_s, px_f, py_s, py_f) # linear part M_samp = np.ones((n_samples, n_samples)) np.fill_diagonal(np.fliplr(M_samp), 0) M_feat = np.ones((2, 2)) np.fill_diagonal(M_feat, 0) M_samp_nx, M_feat_nx = nx.from_numpy(M_samp, M_feat) reg_m = (10, 5) alpha = (0.1, 0.2) max_iter_ot = 5 max_iter = 5 tol = 1e-7 tol_ot = 1e-7 # test couplings pi_sample, pi_feature = fused_unbalanced_across_spaces_divergence( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, reg_type=reg_type, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=M_samp_nx, M_feat=M_feat_nx, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx, pi_feature_nx, log = fused_unbalanced_across_spaces_divergence( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, reg_type=reg_type, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=M_samp_nx, M_feat=M_feat_nx, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=True, verbose=False, ) np.testing.assert_allclose(pi_sample_nx, pi_sample, atol=1e-06) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-06) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize("reg_type", ["independent", "joint"]) def test_fused_unbalanced_across_spaces_divergence_warning(nx, reg_type): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) xs_nx, xt_nx = nx.from_numpy(xs, xt) px_s_nx, px_f_nx, py_s_nx, py_f_nx = nx.from_numpy(px_s, px_f, py_s, py_f) unbalanced_solver = "mm" divergence = "kl" # linear part M_samp = np.ones((n_samples, n_samples)) np.fill_diagonal(np.fliplr(M_samp), 0) M_feat = np.ones((2, 2)) np.fill_diagonal(M_feat, 0) M_samp_nx, M_feat_nx = nx.from_numpy(M_samp, M_feat) reg_m = (1e6, 1e6) eps = 1e-2 alpha = (0.1, 0.2) max_iter_ot = 5 max_iter = 5 tol = 1e-7 tol_ot = 1e-7 def raise_warning(): return fused_unbalanced_across_spaces_divergence( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, reg_type=reg_type, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=M_samp_nx, M_feat=M_feat_nx, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) np.testing.assert_raises(ValueError, raise_warning) python-pot-0.9.7+dfsg/test/gromov/test_gw.py000066400000000000000000001140461525521106200211120ustar00rootroot00000000000000"""Tests for gromov._gw.py""" # Author: Erwan Vautier # Nicolas Courty # Titouan Vayer # Cédric Vincent-Cuaz # # License: MIT License import numpy as np import pytest import warnings import ot from ot.backend import torch, tf def test_gromov(nx): n_samples = 20 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=1) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() C1b, C2b, pb, qb, G0b = nx.from_numpy(C1, C2, p, q, G0) G = ot.gromov.gromov_wasserstein( C1, C2, None, q, "square_loss", G0=G0, verbose=True, alpha_min=0.0, alpha_max=1.0, ) Gb = nx.to_numpy( ot.gromov.gromov_wasserstein( C1b, C2b, pb, None, "square_loss", symmetric=True, G0=G0b, verbose=True ) ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov Id = (1 / (1.0 * n_samples)) * np.eye(n_samples, n_samples) np.testing.assert_allclose(Gb, np.flipud(Id), atol=1e-04) for armijo in [False, True]: gw, log = ot.gromov.gromov_wasserstein2( C1, C2, None, q, "kl_loss", armijo=armijo, log=True ) gwb, logb = ot.gromov.gromov_wasserstein2( C1b, C2b, pb, None, "kl_loss", armijo=armijo, log=True ) gwb = nx.to_numpy(gwb) gw_val = ot.gromov.gromov_wasserstein2( C1, C2, p, q, "kl_loss", armijo=armijo, G0=G0, log=False ) gw_valb = nx.to_numpy( ot.gromov.gromov_wasserstein2( C1b, C2b, pb, qb, "kl_loss", armijo=armijo, G0=G0b, log=False ) ) G = log["T"] Gb = nx.to_numpy(logb["T"]) np.testing.assert_allclose(gw, gwb, atol=1e-06) np.testing.assert_allclose(gwb, 0, atol=1e-1, rtol=1e-1) np.testing.assert_allclose(gw_val, gw_valb, atol=1e-06) np.testing.assert_allclose(gwb, gw_valb, atol=1e-1, rtol=1e-1) # cf log=False # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov def test_asymmetric_gromov(nx): n_samples = 20 # nb samples rng = np.random.RandomState(0) C1 = rng.uniform(low=0.0, high=10, size=(n_samples, n_samples)) idx = np.arange(n_samples) rng.shuffle(idx) C2 = C1[idx, :][:, idx] p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1b, C2b, pb, qb, G0b = nx.from_numpy(C1, C2, p, q, G0) G, log = ot.gromov.gromov_wasserstein( C1, C2, p, q, "square_loss", G0=G0, log=True, symmetric=False, verbose=True ) Gb, logb = ot.gromov.gromov_wasserstein( C1b, C2b, pb, qb, "square_loss", log=True, symmetric=False, G0=G0b, verbose=True ) Gb = nx.to_numpy(Gb) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(log["gw_dist"], 0.0, atol=1e-04) np.testing.assert_allclose(logb["gw_dist"], 0.0, atol=1e-04) gw, log = ot.gromov.gromov_wasserstein2( C1, C2, p, q, "square_loss", G0=G0, log=True, symmetric=False, verbose=True ) gwb, logb = ot.gromov.gromov_wasserstein2( C1b, C2b, pb, qb, "square_loss", log=True, symmetric=False, G0=G0b, verbose=True ) G = log["T"] Gb = nx.to_numpy(logb["T"]) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(log["gw_dist"], 0.0, atol=1e-04) np.testing.assert_allclose(logb["gw_dist"], 0.0, atol=1e-04) def test_gromov_integer_warnings(nx): n_samples = 10 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=1) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() C1 = C1.astype(np.int32) C1b, C2b, pb, qb, G0b = nx.from_numpy(C1, C2, p, q, G0) G = ot.gromov.gromov_wasserstein( C1, C2, None, q, "square_loss", G0=G0, verbose=True, alpha_min=0.0, alpha_max=1.0, ) Gb = nx.to_numpy( ot.gromov.gromov_wasserstein( C1b, C2b, pb, None, "square_loss", symmetric=True, G0=G0b, verbose=True ) ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(G, 0.0, atol=1e-09) def test_gromov_dtype_device(nx): # setup n_samples = 20 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() for tp in nx.__type_list__: print(nx.dtype_device(tp)) C1b, C2b, pb, qb, G0b = nx.from_numpy(C1, C2, p, q, G0, type_as=tp) with warnings.catch_warnings(): warnings.filterwarnings("error") Gb = ot.gromov.gromov_wasserstein( C1b, C2b, pb, qb, "square_loss", G0=G0b, verbose=True ) gw_valb = ot.gromov.gromov_wasserstein2( C1b, C2b, pb, qb, "kl_loss", armijo=True, G0=G0b, log=False ) nx.assert_same_dtype_device(C1b, Gb) nx.assert_same_dtype_device(C1b, gw_valb) @pytest.mark.skipif(not tf, reason="tf not installed") def test_gromov_device_tf(): nx = ot.backend.TensorflowBackend() n_samples = 20 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() # Check that everything stays on the CPU with tf.device("/CPU:0"): C1b, C2b, pb, qb, G0b = nx.from_numpy(C1, C2, p, q, G0) Gb = ot.gromov.gromov_wasserstein( C1b, C2b, pb, qb, "square_loss", G0=G0b, verbose=True ) gw_valb = ot.gromov.gromov_wasserstein2( C1b, C2b, pb, qb, "kl_loss", armijo=True, G0=G0b, log=False ) nx.assert_same_dtype_device(C1b, Gb) nx.assert_same_dtype_device(C1b, gw_valb) if len(tf.config.list_physical_devices("GPU")) > 0: # Check that everything happens on the GPU C1b, C2b, pb, qb, G0b = nx.from_numpy(C1, C2, p, q, G0) Gb = ot.gromov.gromov_wasserstein(C1b, C2b, pb, qb, "square_loss", verbose=True) gw_valb = ot.gromov.gromov_wasserstein2( C1b, C2b, pb, qb, "kl_loss", armijo=True, log=False ) nx.assert_same_dtype_device(C1b, Gb) nx.assert_same_dtype_device(C1b, gw_valb) assert nx.dtype_device(Gb)[1].startswith("GPU") def test_gromov2_gradients(): n_samples = 20 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=5) p = ot.unif(n_samples) q = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() if torch: devices = [torch.device("cpu")] if torch.cuda.is_available(): devices.append(torch.device("cuda")) for device in devices: # classical gradients p1 = torch.tensor(p, requires_grad=True, device=device) q1 = torch.tensor(q, requires_grad=True, device=device) C11 = torch.tensor(C1, requires_grad=True, device=device) C12 = torch.tensor(C2, requires_grad=True, device=device) # Test with exact line-search val = ot.gromov_wasserstein2(C11, C12, p1, q1) val.backward() assert val.device == p1.device assert q1.shape == q1.grad.shape assert p1.shape == p1.grad.shape assert C11.shape == C11.grad.shape assert C12.shape == C12.grad.shape # Test with armijo line-search # classical gradients p1 = torch.tensor(p, requires_grad=True, device=device) q1 = torch.tensor(q, requires_grad=True, device=device) C11 = torch.tensor(C1, requires_grad=True, device=device) C12 = torch.tensor(C2, requires_grad=True, device=device) q1.grad = None p1.grad = None C11.grad = None C12.grad = None val = ot.gromov_wasserstein2(C11, C12, p1, q1, armijo=True) val.backward() assert val.device == p1.device assert q1.shape == q1.grad.shape assert p1.shape == p1.grad.shape assert C11.shape == C11.grad.shape assert C12.shape == C12.grad.shape def test_gw_helper_backend(nx): n_samples = 10 # nb samples mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=0) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=1) p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() C1b, C2b, pb, qb, G0b = nx.from_numpy(C1, C2, p, q, G0) Gb, logb = ot.gromov.gromov_wasserstein( C1b, C2b, pb, qb, "square_loss", armijo=False, symmetric=True, G0=G0b, log=True ) # calls with nx=None constCb, hC1b, hC2b = ot.gromov.init_matrix( C1b, C2b, pb, qb, loss_fun="square_loss" ) def f(G): return ot.gromov.gwloss(constCb, hC1b, hC2b, G, None) def df(G): return ot.gromov.gwggrad(constCb, hC1b, hC2b, G, None) def line_search(cost, G, deltaG, Mi, cost_G, df_G): return ot.gromov.solve_gromov_linesearch( G, deltaG, cost_G, C1b, C2b, M=0.0, reg=1.0, nx=None ) # feed the precomputed local optimum Gb to cg res, log = ot.optim.cg( pb, qb, 0.0, 1.0, f, df, Gb, line_search, log=True, numItermax=1e4, stopThr=1e-9, stopThr2=1e-9, ) # check constraints np.testing.assert_allclose(res, Gb, atol=1e-06) @pytest.mark.parametrize( "loss_fun", [ "square_loss", "kl_loss", pytest.param("unknown_loss", marks=pytest.mark.xfail(raises=ValueError)), ], ) def test_gw_helper_validation(loss_fun): n_samples = 10 # nb samples mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=0) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=1) p = ot.unif(n_samples) q = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) ot.gromov.init_matrix(C1, C2, p, q, loss_fun=loss_fun) def test_gromov_barycenter(nx): ns = 5 nt = 8 Xs, ys = ot.datasets.make_data_classif("3gauss", ns, random_state=42) Xt, yt = ot.datasets.make_data_classif("3gauss2", nt, random_state=42) C1 = ot.dist(Xs) C2 = ot.dist(Xt) p1 = ot.unif(ns) p2 = ot.unif(nt) n_samples = 3 p = ot.unif(n_samples) C1b, C2b, p1b, p2b, pb = nx.from_numpy(C1, C2, p1, p2, p) with pytest.raises(ValueError): stop_criterion = "unknown stop criterion" Cb = ot.gromov.gromov_barycenters( n_samples, [C1, C2], None, p, [0.5, 0.5], "square_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, ) for stop_criterion in ["barycenter", "loss"]: Cb = ot.gromov.gromov_barycenters( n_samples, [C1, C2], None, p, [0.5, 0.5], "square_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, ) Cbb = nx.to_numpy( ot.gromov.gromov_barycenters( n_samples, [C1b, C2b], [p1b, p2b], None, [0.5, 0.5], "square_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, ) ) np.testing.assert_allclose(Cb, Cbb, atol=1e-06) np.testing.assert_allclose(Cbb.shape, (n_samples, n_samples)) # test of gromov_barycenters with `log` on Cb_, err_ = ot.gromov.gromov_barycenters( n_samples, [C1, C2], [p1, p2], p, None, "square_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=False, warmstartT=True, random_state=42, log=True, ) Cbb_, errb_ = ot.gromov.gromov_barycenters( n_samples, [C1b, C2b], [p1b, p2b], pb, [0.5, 0.5], "square_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=False, warmstartT=True, random_state=42, log=True, ) Cbb_ = nx.to_numpy(Cbb_) np.testing.assert_allclose(Cb_, Cbb_, atol=1e-06) np.testing.assert_array_almost_equal(err_["err"], nx.to_numpy(*errb_["err"])) np.testing.assert_allclose(Cbb_.shape, (n_samples, n_samples)) Cb2 = ot.gromov.gromov_barycenters( n_samples, [C1, C2], [p1, p2], p, [0.5, 0.5], "kl_loss", max_iter=10, tol=1e-3, warmstartT=True, random_state=42, ) Cb2b = nx.to_numpy( ot.gromov.gromov_barycenters( n_samples, [C1b, C2b], [p1b, p2b], pb, [0.5, 0.5], "kl_loss", max_iter=10, tol=1e-3, warmstartT=True, random_state=42, ) ) np.testing.assert_allclose(Cb2, Cb2b, atol=1e-06) np.testing.assert_allclose(Cb2b.shape, (n_samples, n_samples)) # test of gromov_barycenters with `log` on # providing init_C generator = ot.utils.check_random_state(42) xalea = generator.randn(n_samples, 2) init_C = ot.utils.dist(xalea, xalea) init_C /= init_C.max() init_Cb = nx.from_numpy(init_C) Cb2_, err2_ = ot.gromov.gromov_barycenters( n_samples, [C1, C2], [p1, p2], p, [0.5, 0.5], "kl_loss", max_iter=10, tol=1e-3, verbose=False, random_state=42, log=True, init_C=init_C, ) Cb2b_, err2b_ = ot.gromov.gromov_barycenters( n_samples, [C1b, C2b], [p1b, p2b], pb, [0.5, 0.5], "kl_loss", max_iter=10, tol=1e-3, verbose=True, random_state=42, init_C=init_Cb, log=True, ) Cb2b_ = nx.to_numpy(Cb2b_) np.testing.assert_allclose(Cb2_, Cb2b_, atol=1e-06) np.testing.assert_array_almost_equal(err2_["err"], nx.to_numpy(*err2b_["err"])) np.testing.assert_allclose(Cb2b_.shape, (n_samples, n_samples)) # test edge cases for gw barycenters: # C1 as list with pytest.raises(ValueError): C1_list = [list(c) for c in C1] _ = ot.gromov.gromov_barycenters( n_samples, [C1_list], None, p, None, "square_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, ) # p1, p2 as lists with pytest.raises(ValueError): p1_list = list(p1) p2_list = list(p2) _ = ot.gromov.gromov_barycenters( n_samples, [C1, C2], [p1_list, p2_list], p, None, "square_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, ) # unique input structure Cb = ot.gromov.gromov_barycenters( n_samples, [C1], None, p, None, "square_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, ) Cbb = nx.to_numpy( ot.gromov.gromov_barycenters( n_samples, [C1b], None, None, [1.0], "square_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, ) ) np.testing.assert_allclose(Cb, Cbb, atol=1e-06) np.testing.assert_allclose(Cbb.shape, (n_samples, n_samples)) def test_fgw(nx): n_samples = 20 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=42) xt = xs[::-1].copy() rng = np.random.RandomState(42) ys = rng.randn(xs.shape[0], 2) yt = ys[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() M = ot.dist(ys, yt) M /= M.max() Mb, C1b, C2b, pb, qb, G0b = nx.from_numpy(M, C1, C2, p, q, G0) G, log = ot.gromov.fused_gromov_wasserstein( M, C1, C2, None, q, "square_loss", alpha=0.5, armijo=True, symmetric=None, G0=G0, log=True, ) Gb, logb = ot.gromov.fused_gromov_wasserstein( Mb, C1b, C2b, pb, None, "square_loss", alpha=0.5, armijo=True, symmetric=True, G0=G0b, log=True, ) Gb = nx.to_numpy(Gb) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence fgw np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence fgw Id = (1 / (1.0 * n_samples)) * np.eye(n_samples, n_samples) np.testing.assert_allclose(Gb, np.flipud(Id), atol=1e-04) # cf convergence gromov fgw, log = ot.gromov.fused_gromov_wasserstein2( M, C1, C2, p, None, "square_loss", armijo=True, symmetric=True, G0=None, alpha=0.5, log=True, ) fgwb, logb = ot.gromov.fused_gromov_wasserstein2( Mb, C1b, C2b, None, qb, "square_loss", armijo=True, symmetric=None, G0=G0b, alpha=0.5, log=True, ) fgwb = nx.to_numpy(fgwb) G = log["T"] Gb = nx.to_numpy(logb["T"]) np.testing.assert_allclose(fgw, fgwb, atol=1e-08) np.testing.assert_allclose(fgwb, 0, atol=1e-1, rtol=1e-1) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov def test_asymmetric_fgw(nx): n_samples = 20 # nb samples rng = np.random.RandomState(0) C1 = rng.uniform(low=0.0, high=10, size=(n_samples, n_samples)) idx = np.arange(n_samples) rng.shuffle(idx) C2 = C1[idx, :][:, idx] # add features F1 = rng.uniform(low=0.0, high=10, size=(n_samples, 1)) F2 = F1[idx, :] p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] M = ot.dist(F1, F2) Mb, C1b, C2b, pb, qb, G0b = nx.from_numpy(M, C1, C2, p, q, G0) G, log = ot.gromov.fused_gromov_wasserstein( M, C1, C2, p, q, "square_loss", alpha=0.5, G0=G0, log=True, symmetric=False, verbose=True, ) Gb, logb = ot.gromov.fused_gromov_wasserstein( Mb, C1b, C2b, pb, qb, "square_loss", alpha=0.5, log=True, symmetric=None, G0=G0b, verbose=True, ) Gb = nx.to_numpy(Gb) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(log["fgw_dist"], 0.0, atol=1e-04) np.testing.assert_allclose(logb["fgw_dist"], 0.0, atol=1e-04) fgw, log = ot.gromov.fused_gromov_wasserstein2( M, C1, C2, p, q, "square_loss", alpha=0.5, G0=G0, log=True, symmetric=None, verbose=True, ) fgwb, logb = ot.gromov.fused_gromov_wasserstein2( Mb, C1b, C2b, pb, qb, "square_loss", alpha=0.5, log=True, symmetric=False, G0=G0b, verbose=True, ) G = log["T"] Gb = nx.to_numpy(logb["T"]) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(log["fgw_dist"], 0.0, atol=1e-04) np.testing.assert_allclose(logb["fgw_dist"], 0.0, atol=1e-04) # Tests with kl-loss: for armijo in [False, True]: G, log = ot.gromov.fused_gromov_wasserstein( M, C1, C2, p, q, "kl_loss", alpha=0.5, armijo=armijo, G0=G0, log=True, symmetric=False, verbose=True, ) Gb, logb = ot.gromov.fused_gromov_wasserstein( Mb, C1b, C2b, pb, qb, "kl_loss", alpha=0.5, armijo=armijo, log=True, symmetric=None, G0=G0b, verbose=True, ) Gb = nx.to_numpy(Gb) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(log["fgw_dist"], 0.0, atol=1e-04) np.testing.assert_allclose(logb["fgw_dist"], 0.0, atol=1e-04) fgw, log = ot.gromov.fused_gromov_wasserstein2( M, C1, C2, p, q, "kl_loss", alpha=0.5, G0=G0, log=True, symmetric=None, verbose=True, ) fgwb, logb = ot.gromov.fused_gromov_wasserstein2( Mb, C1b, C2b, pb, qb, "kl_loss", alpha=0.5, log=True, symmetric=False, G0=G0b, verbose=True, ) G = log["T"] Gb = nx.to_numpy(logb["T"]) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(q, Gb.sum(0), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(log["fgw_dist"], 0.0, atol=1e-04) np.testing.assert_allclose(logb["fgw_dist"], 0.0, atol=1e-04) def test_fgw_integer_warnings(nx): n_samples = 20 # nb samples rng = np.random.RandomState(0) C1 = rng.uniform(low=0.0, high=10, size=(n_samples, n_samples)) idx = np.arange(n_samples) rng.shuffle(idx) C2 = C1[idx, :][:, idx] # add features F1 = rng.uniform(low=0.0, high=10, size=(n_samples, 1)) F2 = F1[idx, :] p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] M = ot.dist(F1, F2).astype(np.int32) Mb, C1b, C2b, pb, qb, G0b = nx.from_numpy(M, C1, C2, p, q, G0) G, log = ot.gromov.fused_gromov_wasserstein( M, C1, C2, p, q, "square_loss", alpha=0.5, G0=G0, log=True, symmetric=False, verbose=True, ) Gb, logb = ot.gromov.fused_gromov_wasserstein( Mb, C1b, C2b, pb, qb, "square_loss", alpha=0.5, log=True, symmetric=None, G0=G0b, verbose=True, ) Gb = nx.to_numpy(Gb) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(G, 0.0, atol=1e-06) def test_fgw2_gradients(): n_samples = 20 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=5) p = ot.unif(n_samples) q = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) M = ot.dist(xs, xt) C1 /= C1.max() C2 /= C2.max() if torch: devices = [torch.device("cpu")] if torch.cuda.is_available(): devices.append(torch.device("cuda")) for device in devices: p1 = torch.tensor(p, requires_grad=True, device=device) q1 = torch.tensor(q, requires_grad=True, device=device) C11 = torch.tensor(C1, requires_grad=True, device=device) C12 = torch.tensor(C2, requires_grad=True, device=device) M1 = torch.tensor(M, requires_grad=True, device=device) val = ot.fused_gromov_wasserstein2(M1, C11, C12, p1, q1) val.backward() assert val.device == p1.device assert q1.shape == q1.grad.shape assert p1.shape == p1.grad.shape assert C11.shape == C11.grad.shape assert C12.shape == C12.grad.shape assert M1.shape == M1.grad.shape # full gradients with alpha p1 = torch.tensor(p, requires_grad=True, device=device) q1 = torch.tensor(q, requires_grad=True, device=device) C11 = torch.tensor(C1, requires_grad=True, device=device) C12 = torch.tensor(C2, requires_grad=True, device=device) M1 = torch.tensor(M, requires_grad=True, device=device) alpha = torch.tensor(0.5, requires_grad=True, device=device) val = ot.fused_gromov_wasserstein2(M1, C11, C12, p1, q1, alpha=alpha) val.backward() assert val.device == p1.device assert q1.shape == q1.grad.shape assert p1.shape == p1.grad.shape assert C11.shape == C11.grad.shape assert C12.shape == C12.grad.shape assert alpha.shape == alpha.grad.shape def test_fgw_helper_backend(nx): n_samples = 20 # nb samples mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 1]]) rng = np.random.RandomState(42) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=0) ys = rng.randn(xs.shape[0], 2) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=1) yt = rng.randn(xt.shape[0], 2) p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() M = ot.dist(ys, yt) M /= M.max() Mb, C1b, C2b, pb, qb, G0b = nx.from_numpy(M, C1, C2, p, q, G0) alpha = 0.5 Gb, logb = ot.gromov.fused_gromov_wasserstein( Mb, C1b, C2b, pb, qb, "square_loss", alpha=0.5, armijo=False, symmetric=True, G0=G0b, log=True, ) # calls with nx=None constCb, hC1b, hC2b = ot.gromov.init_matrix( C1b, C2b, pb, qb, loss_fun="square_loss" ) def f(G): return ot.gromov.gwloss(constCb, hC1b, hC2b, G, None) def df(G): return ot.gromov.gwggrad(constCb, hC1b, hC2b, G, None) def line_search(cost, G, deltaG, Mi, cost_G, df_G): return ot.gromov.solve_gromov_linesearch( G, deltaG, cost_G, C1b, C2b, M=(1 - alpha) * Mb, reg=alpha, nx=None ) # feed the precomputed local optimum Gb to cg res, log = ot.optim.cg( pb, qb, (1 - alpha) * Mb, alpha, f, df, Gb, line_search, log=True, numItermax=1e4, stopThr=1e-9, stopThr2=1e-9, ) def line_search(cost, G, deltaG, Mi, cost_G, df_G): return ot.optim.line_search_armijo(cost, G, deltaG, Mi, cost_G, nx=None) # feed the precomputed local optimum Gb to cg res_armijo, log_armijo = ot.optim.cg( pb, qb, (1 - alpha) * Mb, alpha, f, df, Gb, line_search, log=True, numItermax=1e4, stopThr=1e-9, stopThr2=1e-9, ) # check constraints np.testing.assert_allclose(res, Gb, atol=1e-06) np.testing.assert_allclose(res_armijo, Gb, atol=1e-06) def test_fgw_barycenter(nx): ns = 10 nt = 20 Xs, ys = ot.datasets.make_data_classif("3gauss", ns, random_state=42) Xt, yt = ot.datasets.make_data_classif("3gauss2", nt, random_state=42) rng = np.random.RandomState(42) ys = rng.randn(Xs.shape[0], 2) yt = rng.randn(Xt.shape[0], 2) C1 = ot.dist(Xs) C2 = ot.dist(Xt) C1 /= C1.max() C2 /= C2.max() p1, p2 = ot.unif(ns), ot.unif(nt) n_samples = 3 p = ot.unif(n_samples) ysb, ytb, C1b, C2b, p1b, p2b, pb = nx.from_numpy(ys, yt, C1, C2, p1, p2, p) lambdas = [0.5, 0.5] Csb = [C1b, C2b] Ysb = [ysb, ytb] Xb, Cb, logb = ot.gromov.fgw_barycenters( n_samples, Ysb, Csb, None, lambdas, 0.5, fixed_structure=False, fixed_features=False, p=pb, loss_fun="square_loss", max_iter=10, tol=1e-3, random_state=12345, log=True, ) # test correspondance with utils function recovered_Cb = ot.gromov.update_barycenter_structure( logb["Ts_iter"][-1], Csb, lambdas, pb, target=False, check_zeros=True ) recovered_Xb = ot.gromov.update_barycenter_feature( logb["Ts_iter"][-1], Ysb, lambdas, pb, target=False, check_zeros=True ) np.testing.assert_allclose(Cb, recovered_Cb) np.testing.assert_allclose(Xb, recovered_Xb) xalea = rng.randn(n_samples, 2) init_C = ot.dist(xalea, xalea) init_C /= init_C.max() init_Cb = nx.from_numpy(init_C) with pytest.raises( ot.utils.UndefinedParameter ): # to raise an error when `fixed_structure=True`and `init_C=None` Xb, Cb = ot.gromov.fgw_barycenters( n_samples, Ysb, Csb, ps=[p1b, p2b], lambdas=None, alpha=0.5, fixed_structure=True, init_C=None, fixed_features=False, p=None, loss_fun="square_loss", max_iter=10, tol=1e-3, ) Xb, Cb = ot.gromov.fgw_barycenters( n_samples, [ysb, ytb], [C1b, C2b], ps=[p1b, p2b], lambdas=None, alpha=0.5, fixed_structure=True, init_C=init_Cb, fixed_features=False, p=None, loss_fun="square_loss", max_iter=10, tol=1e-3, ) Xb, Cb = nx.to_numpy(Xb), nx.to_numpy(Cb) np.testing.assert_allclose(Cb.shape, (n_samples, n_samples)) np.testing.assert_allclose(Xb.shape, (n_samples, ys.shape[1])) init_X = rng.randn(n_samples, ys.shape[1]) init_Xb = nx.from_numpy(init_X) with pytest.raises( ot.utils.UndefinedParameter ): # to raise an error when `fixed_features=True`and `init_X=None` Xb, Cb, logb = ot.gromov.fgw_barycenters( n_samples, [ysb, ytb], [C1b, C2b], [p1b, p2b], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=True, init_X=None, p=pb, loss_fun="square_loss", max_iter=10, tol=1e-3, warmstartT=True, log=True, random_state=98765, verbose=True, ) Xb, Cb, logb = ot.gromov.fgw_barycenters( n_samples, [ysb, ytb], [C1b, C2b], [p1b, p2b], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=True, init_X=init_Xb, p=pb, loss_fun="square_loss", max_iter=10, tol=1e-3, warmstartT=True, log=True, random_state=98765, verbose=True, ) X, C = nx.to_numpy(Xb), nx.to_numpy(Cb) np.testing.assert_allclose(C.shape, (n_samples, n_samples)) np.testing.assert_allclose(X.shape, (n_samples, ys.shape[1])) # add test with 'kl_loss' with pytest.raises(ValueError): stop_criterion = "unknown stop criterion" X, C, log = ot.gromov.fgw_barycenters( n_samples, [ys, yt], [C1, C2], [p1, p2], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=False, p=p, loss_fun="kl_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, init_C=C, init_X=X, warmstartT=True, random_state=12345, log=True, ) for stop_criterion in ["barycenter", "loss"]: X, C, log = ot.gromov.fgw_barycenters( n_samples, [ys, yt], [C1, C2], [p1, p2], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=False, p=p, loss_fun="kl_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, init_C=C, init_X=X, warmstartT=True, random_state=12345, log=True, verbose=True, ) np.testing.assert_allclose(C.shape, (n_samples, n_samples)) np.testing.assert_allclose(X.shape, (n_samples, ys.shape[1])) # test correspondance with utils function recovered_C = ot.gromov.update_barycenter_structure( log["T"], [C1, C2], lambdas, p, loss_fun="kl_loss", target=False, check_zeros=False, ) np.testing.assert_allclose(C, recovered_C) # test edge cases for fgw barycenters: # C1 as list with pytest.raises(ValueError): C1b_list = [list(c) for c in C1b] _, _, _ = ot.gromov.fgw_barycenters( n_samples, [ysb], [C1b_list], [p1b], None, 0.5, fixed_structure=False, fixed_features=False, p=pb, loss_fun="square_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, init_C=Cb, init_X=Xb, warmstartT=True, random_state=12345, log=True, verbose=True, ) # p1, p2 as lists with pytest.raises(ValueError): p1_list = list(p1) p2_list = list(p2) _, _, _ = ot.gromov.fgw_barycenters( n_samples, [ysb, ytb], [C1b, C2b], [p1_list, p2_list], None, 0.5, fixed_structure=False, fixed_features=False, p=p, loss_fun="kl_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, init_C=Cb, init_X=Xb, warmstartT=True, random_state=12345, log=True, verbose=True, ) # unique input structure X, C = ot.gromov.fgw_barycenters( n_samples, [ys], [C1], [p1], None, 0.5, fixed_structure=False, fixed_features=False, p=p, loss_fun="square_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, warmstartT=True, random_state=12345, log=False, verbose=False, ) Xb, Cb = ot.gromov.fgw_barycenters( n_samples, [ysb], [C1b], [p1b], [1.0], 0.5, fixed_structure=False, fixed_features=False, p=pb, loss_fun="square_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, warmstartT=True, random_state=12345, log=False, verbose=False, ) np.testing.assert_allclose(C, Cb, atol=1e-06) np.testing.assert_allclose(X, Xb, atol=1e-06) python-pot-0.9.7+dfsg/test/gromov/test_lowrank.py000066400000000000000000000107761525521106200221570ustar00rootroot00000000000000"""Tests for gromov._lowrank.py""" # Author: Laurène DAVID # # License: MIT License import ot import numpy as np import pytest def test__flat_product_operator(): # test flat product operator n, d = 100, 2 X = np.reshape(1.0 * np.arange(2 * n), (n, d)) A1, A2 = ot.lowrank.compute_lr_sqeuclidean_matrix(X, X, rescale_cost=False) A1_ = ot.gromov._lowrank._flat_product_operator(A1) A2_ = ot.gromov._lowrank._flat_product_operator(A2) cost = ot.dist(X, X) # test value np.testing.assert_allclose(cost**2, np.dot(A1_, A2_.T), atol=1e-05) def test_lowrank_gromov_wasserstein_samples(): # test low rank gromov wasserstein n_samples = 20 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) X_s = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=1) X_t = X_s[::-1].copy() a = ot.unif(n_samples) b = ot.unif(n_samples) Q, R, g, log = ot.gromov.lowrank_gromov_wasserstein_samples( X_s, X_t, a, b, reg=0.1, log=True, rescale_cost=False ) P = log["lazy_plan"][:] # check constraints for P np.testing.assert_allclose(a, P.sum(1), atol=1e-04) np.testing.assert_allclose(b, P.sum(0), atol=1e-04) # check if lazy_plan is equal to the fully computed plan P_true = np.dot(Q, np.dot(np.diag(1 / g), R.T)) np.testing.assert_allclose(P, P_true, atol=1e-05) # check warn parameter when low rank GW algorithm doesn't converge with pytest.warns(UserWarning): ot.gromov.lowrank_gromov_wasserstein_samples( X_s, X_t, a, b, reg=0.1, stopThr=0, numItermax=1, warn=True, warn_dykstra=False, ) # check warn parameter when Dykstra algorithm doesn't converge with pytest.warns(UserWarning): ot.gromov.lowrank_gromov_wasserstein_samples( X_s, X_t, a, b, reg=0.1, stopThr_dykstra=0, numItermax_dykstra=1, warn=False, warn_dykstra=True, ) @pytest.mark.parametrize(("alpha, rank"), ((0.8, 2), (0.5, 3), (0.2, 6), (0.1, -1))) def test_lowrank_gromov_wasserstein_samples_alpha_error(alpha, rank): # Test warning for value of alpha and rank n_samples = 20 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) X_s = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=1) X_t = X_s[::-1].copy() a = ot.unif(n_samples) b = ot.unif(n_samples) with pytest.raises(ValueError): ot.gromov.lowrank_gromov_wasserstein_samples( X_s, X_t, a, b, reg=0.1, rank=rank, alpha=alpha, warn=False ) @pytest.mark.parametrize(("gamma_init"), ("rescale", "theory", "other")) def test_lowrank_wasserstein_samples_gamma_init(gamma_init): # Test lr sinkhorn with different init strategies n_samples = 20 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) X_s = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=1) X_t = X_s[::-1].copy() a = ot.unif(n_samples) b = ot.unif(n_samples) if gamma_init not in ["rescale", "theory"]: with pytest.raises(NotImplementedError): ot.gromov.lowrank_gromov_wasserstein_samples( X_s, X_t, a, b, reg=0.1, gamma_init=gamma_init, log=True ) else: Q, R, g, log = ot.gromov.lowrank_gromov_wasserstein_samples( X_s, X_t, a, b, reg=0.1, gamma_init=gamma_init, log=True ) P = log["lazy_plan"][:] # check constraints for P np.testing.assert_allclose(a, P.sum(1), atol=1e-04) np.testing.assert_allclose(b, P.sum(0), atol=1e-04) @pytest.skip_backend("tf") def test_lowrank_gromov_wasserstein_samples_backends(nx): # Test low rank sinkhorn for different backends n_samples = 20 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) X_s = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=1) X_t = X_s[::-1].copy() a = ot.unif(n_samples) b = ot.unif(n_samples) ab, bb, X_sb, X_tb = nx.from_numpy(a, b, X_s, X_t) Q, R, g, log = ot.gromov.lowrank_gromov_wasserstein_samples( X_sb, X_tb, ab, bb, reg=0.1, log=True ) lazy_plan = log["lazy_plan"] P = lazy_plan[:] np.testing.assert_allclose(ab, P.sum(1), atol=1e-04) np.testing.assert_allclose(bb, P.sum(0), atol=1e-04) python-pot-0.9.7+dfsg/test/gromov/test_partial.py000066400000000000000000000575711525521106200221420ustar00rootroot00000000000000"""Tests for gromov._partial.py""" # Author: # Laetitia Chapel # Cédric Vincent-Cuat # # License: MIT License import numpy as np import scipy as sp import ot from ot.backend import torch import pytest def test_raise_errors(): n_samples = 20 # nb samples (gaussian) n_noise = 20 # nb of samples (noise) mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 2]]) rng = np.random.RandomState(42) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=rng) xs = np.append(xs, (rng.rand(n_noise, 2) + 1) * 4).reshape((-1, 2)) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=rng) xt = np.append(xt, (rng.rand(n_noise, 2) + 1) * -3).reshape((-1, 2)) M = ot.dist(xs, xt) p = ot.unif(n_samples + n_noise) q = ot.unif(n_samples + n_noise) with pytest.raises(ValueError): ot.gromov.partial_gromov_wasserstein(M, M, p, q, m=2, log=True) with pytest.raises(ValueError): ot.gromov.partial_gromov_wasserstein(M, M, p, q, m=-1, log=True) with pytest.raises(ValueError): ot.gromov.entropic_partial_gromov_wasserstein(M, M, p, q, reg=1, m=2, log=True) with pytest.raises(ValueError): ot.gromov.entropic_partial_gromov_wasserstein(M, M, p, q, reg=1, m=-1, log=True) with pytest.raises(ValueError): ot.gromov.partial_fused_gromov_wasserstein(M, M, M, p, q, m=2, log=True) with pytest.raises(ValueError): ot.gromov.partial_fused_gromov_wasserstein(M, M, M, p, q, m=-1, log=True) with pytest.raises(ValueError): ot.gromov.entropic_partial_fused_gromov_wasserstein( M, M, M, p, q, m=2, log=True ) with pytest.raises(ValueError): ot.gromov.entropic_partial_fused_gromov_wasserstein( M, M, M, p, q, m=-1, log=True ) def test_partial_gromov_wasserstein(nx): rng = np.random.RandomState(42) n_samples = 20 # nb samples n_noise = 10 # nb of samples (noise) p = ot.unif(n_samples + n_noise) psub = ot.unif(n_samples - 5 + n_noise) q = ot.unif(n_samples + n_noise) mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([0, 0, 0]) cov_t = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) # clean samples xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=rng) P = sp.linalg.sqrtm(cov_t) xt = rng.randn(n_samples, 3).dot(P) + mu_t # add noise xs = np.concatenate((xs, ((rng.rand(n_noise, 2) + 1) * 4)), axis=0) xt = np.concatenate((xt, ((rng.rand(n_noise, 3) + 1) * 10)), axis=0) xt2 = xs[::-1].copy() C1 = ot.dist(xs, xs) C1sub = ot.dist(xs[5:], xs[5:]) C2 = ot.dist(xt, xt) C3 = ot.dist(xt2, xt2) m = 2.0 / 3.0 C1b, C1subb, C2b, C3b, pb, psubb, qb = nx.from_numpy(C1, C1sub, C2, C3, p, psub, q) G0 = ( np.outer(p, q) * m / (np.sum(p) * np.sum(q)) ) # make sure |G0|=m, G01_m\leq p, G0.T1_n\leq q. G0b = nx.from_numpy(G0) # check consistency across backends and stability w.r.t loss/marginals/sym list_sym = [True, None] for i, loss_fun in enumerate(["square_loss", "kl_loss"]): res, log = ot.gromov.partial_gromov_wasserstein( C1, C3, p=p, q=None, m=m, loss_fun=loss_fun, n_dummies=1, G0=G0, log=True, symmetric=list_sym[i], warn=True, verbose=True, ) resb, logb = ot.gromov.partial_gromov_wasserstein( C1b, C3b, p=None, q=qb, m=m, loss_fun=loss_fun, n_dummies=1, G0=G0b, log=True, symmetric=False, warn=True, verbose=True, ) resb_ = nx.to_numpy(resb) assert np.all(res.sum(1) <= p) # cf convergence wasserstein assert np.all(res.sum(0) <= q) # cf convergence wasserstein try: # precision error while doubling numbers of computations with symmetric=False # some instability can occur with kl. to investigate further. # changing log offset in _transform_matrix was a way to solve it # but it also negatively affects some other solvers in the API np.testing.assert_allclose(res, resb_, rtol=1e-4) except AssertionError: pass # tests with different number of samples across spaces m = 2.0 / 3.0 res, log = ot.gromov.partial_gromov_wasserstein( C1, C1sub, p=p, q=psub, m=m, log=True ) resb, logb = ot.gromov.partial_gromov_wasserstein( C1b, C1subb, p=pb, q=psubb, m=m, log=True ) resb_ = nx.to_numpy(resb) np.testing.assert_allclose(res, resb_, rtol=1e-4) assert np.all(res.sum(1) <= p) # cf convergence wasserstein assert np.all(res.sum(0) <= psub) # cf convergence wasserstein np.testing.assert_allclose(np.sum(res), m, atol=1e-15) # Edge cases - tests with m=1 set by default (coincide with gw) m = 1 res0 = ot.gromov.partial_gromov_wasserstein(C1, C2, p, q, m=m, log=False) res0b, log0b = ot.gromov.partial_gromov_wasserstein( C1b, C2b, pb, qb, m=None, log=True ) G = ot.gromov.gromov_wasserstein(C1, C2, p, q, "square_loss") np.testing.assert_allclose(G, res0, rtol=1e-4) np.testing.assert_allclose(res0b, res0, rtol=1e-4) # tests for pGW2 for loss_fun in ["square_loss", "kl_loss"]: w0, log0 = ot.gromov.partial_gromov_wasserstein2( C1, C2, p=None, q=q, m=m, loss_fun=loss_fun, log=True ) w0_val = ot.gromov.partial_gromov_wasserstein2( C1b, C2b, p=pb, q=None, m=m, loss_fun=loss_fun, log=False ) np.testing.assert_allclose(w0, w0_val, rtol=1e-4) # tests integers C1_int = C1.astype(int) C1b_int = nx.from_numpy(C1_int) C2_int = C2.astype(int) C2b_int = nx.from_numpy(C2_int) res0b, log0b = ot.gromov.partial_gromov_wasserstein( C1b_int, C2b_int, pb, qb, m=m, log=True ) assert nx.to_numpy(res0b).dtype == C1_int.dtype def test_partial_partial_gromov_linesearch(nx): rng = np.random.RandomState(42) n_samples = 20 # nb samples n_noise = 10 # nb of samples (noise) p = ot.unif(n_samples + n_noise) q = ot.unif(n_samples + n_noise) mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([0, 0, 0]) cov_t = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=rng) xs = np.concatenate((xs, ((rng.rand(n_noise, 2) + 1) * 4)), axis=0) P = sp.linalg.sqrtm(cov_t) xt = rng.randn(n_samples, 3).dot(P) + mu_t xt = np.concatenate((xt, ((rng.rand(n_noise, 3) + 1) * 10)), axis=0) xt2 = xs[::-1].copy() C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C3 = ot.dist(xt2, xt2) m = 2.0 / 3.0 C1b, C2b, C3b, pb, qb = nx.from_numpy(C1, C2, C3, p, q) G0 = ( np.outer(p, q) * m / (np.sum(p) * np.sum(q)) ) # make sure |G0|=m, G01_m\leq p, G0.T1_n\leq q. G0b = nx.from_numpy(G0) # computing necessary inputs to the line-search Gb, _ = ot.gromov.partial_gromov_wasserstein(C1b, C2b, pb, qb, m=m, log=True) deltaGb = Gb - G0b fC1, fC2, hC1, hC2 = ot.gromov._utils._transform_matrix(C1b, C2b, "square_loss") fC2t = fC2.T ones_p = nx.ones(p.shape[0], type_as=pb) ones_q = nx.ones(p.shape[0], type_as=pb) constC1 = nx.outer(nx.dot(fC1, pb), ones_q) constC2 = nx.outer(ones_p, nx.dot(qb, fC2t)) cost_G0b = ot.gromov.gwloss(constC1 + constC2, hC1, hC2, G0b) df_G0b = ot.gromov.gwggrad(constC1 + constC2, hC1, hC2, G0b) df_Gb = ot.gromov.gwggrad(constC1 + constC2, hC1, hC2, Gb) # perform line-search alpha, _, cost_Gb, _ = ot.gromov.solve_partial_gromov_linesearch( G0b, deltaGb, cost_G0b, df_G0b, df_Gb, 0.0, 1.0, alpha_min=0.0, alpha_max=1.0 ) np.testing.assert_allclose(alpha, 1.0, rtol=1e-4) def test_partial_fused_gromov_wasserstein(nx): rng = np.random.RandomState(42) n_samples = 20 # nb samples n_noise = 10 # nb of samples (noise) p = ot.unif(n_samples + n_noise) psub = ot.unif(n_samples - 5 + n_noise) q = ot.unif(n_samples + n_noise) mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([0, 0, 0]) cov_t = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) # clean samples xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=rng) P = sp.linalg.sqrtm(cov_t) xt = rng.randn(n_samples, 3).dot(P) + mu_t # add noise xs = np.concatenate((xs, ((rng.rand(n_noise, 2) + 1) * 4)), axis=0) xt = np.concatenate((xt, ((rng.rand(n_noise, 3) + 1) * 10)), axis=0) xt2 = xs[::-1].copy() C1 = ot.dist(xs, xs) F1 = xs C1sub = ot.dist(xs[5:], xs[5:]) F1sub = xs[5:] C2 = ot.dist(xt, xt) F2 = xs C3 = ot.dist(xt2, xt2) F3 = xt2 M11sub = ot.dist(F1, F1sub) M12 = ot.dist(F1, F2) M13 = ot.dist(F1, F3) m = 2.0 / 3.0 M11subb, M12b, M13b, C1b, C1subb, C2b, C3b, pb, psubb, qb = nx.from_numpy( M11sub, M12, M13, C1, C1sub, C2, C3, p, psub, q ) G0 = ( np.outer(p, q) * m / (np.sum(p) * np.sum(q)) ) # make sure |G0|=m, G01_m\leq p, G0.T1_n\leq q. G0b = nx.from_numpy(G0) # check consistency across backends and stability w.r.t loss/marginals/sym list_sym = [True, None] for i, loss_fun in enumerate(["square_loss", "kl_loss"]): res, log = ot.gromov.partial_fused_gromov_wasserstein( M13, C1, C3, p=p, q=None, m=m, loss_fun=loss_fun, alpha=0.3, n_dummies=1, G0=G0, log=True, symmetric=list_sym[i], warn=True, verbose=True, ) resb, logb = ot.gromov.partial_fused_gromov_wasserstein( M13b, C1b, C3b, p=None, q=qb, m=m, loss_fun=loss_fun, alpha=0.3, n_dummies=1, G0=G0b, log=True, symmetric=False, warn=True, verbose=True, ) resb_ = nx.to_numpy(resb) assert np.all(res.sum(1) <= p) # cf convergence wasserstein assert np.all(res.sum(0) <= q) # cf convergence wasserstein try: # precision error while doubling numbers of computations with symmetric=False # some instability can occur with kl. to investigate further. # changing log offset in _transform_matrix was a way to solve it # but it also negatively affects some other solvers in the API np.testing.assert_allclose(res, resb_, rtol=1e-4) except AssertionError: pass # tests with different number of samples across spaces m = 2.0 / 3.0 res, log = ot.gromov.partial_fused_gromov_wasserstein( M11sub, C1, C1sub, p=p, q=psub, m=m, log=True ) resb, logb = ot.gromov.partial_fused_gromov_wasserstein( M11subb, C1b, C1subb, p=pb, q=psubb, m=m, log=True ) resb_ = nx.to_numpy(resb) np.testing.assert_allclose(res, resb_, rtol=1e-4) assert np.all(res.sum(1) <= p) # cf convergence wasserstein assert np.all(res.sum(0) <= psub) # cf convergence wasserstein np.testing.assert_allclose(np.sum(res), m, atol=1e-15) # Edge cases - tests with m=1 set by default (coincide with gw) m = 1 res0 = ot.gromov.partial_fused_gromov_wasserstein(M12, C1, C2, p, q, m=m, log=False) res0b, log0b = ot.gromov.partial_fused_gromov_wasserstein( M12b, C1b, C2b, pb, qb, m=None, log=True ) G = ot.gromov.fused_gromov_wasserstein(M12, C1, C2, p, q, "square_loss") np.testing.assert_allclose(G, res0, rtol=1e-4) np.testing.assert_allclose(res0b, res0, rtol=1e-4) # tests for pGW2 for loss_fun in ["square_loss", "kl_loss"]: w0, log0 = ot.gromov.partial_fused_gromov_wasserstein2( M12, C1, C2, p=None, q=q, m=m, loss_fun=loss_fun, log=True ) w0_val = ot.gromov.partial_fused_gromov_wasserstein2( M12b, C1b, C2b, p=pb, q=None, m=m, loss_fun=loss_fun, log=False ) np.testing.assert_allclose(w0, w0_val, rtol=1e-4) # tests integers C1_int = C1.astype(int) C1b_int = nx.from_numpy(C1_int) C2_int = C2.astype(int) C2b_int = nx.from_numpy(C2_int) res0b, log0b = ot.gromov.partial_fused_gromov_wasserstein( M12b, C1b_int, C2b_int, pb, qb, m=m, log=True ) assert nx.to_numpy(res0b).dtype == C1_int.dtype def test_partial_fgw2_gradients(): n_samples = 20 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=5) p = ot.unif(n_samples) q = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) M = ot.dist(xs, xt) C1 /= C1.max() C2 /= C2.max() if torch: devices = [torch.device("cpu")] if torch.cuda.is_available(): devices.append(torch.device("cuda")) for device in devices: p1 = torch.tensor(p, requires_grad=False, device=device) q1 = torch.tensor(q, requires_grad=False, device=device) C11 = torch.tensor(C1, requires_grad=True, device=device) C12 = torch.tensor(C2, requires_grad=True, device=device) M1 = torch.tensor(M, requires_grad=True, device=device) val = ot.gromov.partial_fused_gromov_wasserstein2(M1, C11, C12, p1, q1) val.backward() assert val.device == p1.device assert C11.shape == C11.grad.shape assert C12.shape == C12.grad.shape assert M1.shape == M1.grad.shape # full gradients with alpha C11 = torch.tensor(C1, requires_grad=True, device=device) C12 = torch.tensor(C2, requires_grad=True, device=device) M1 = torch.tensor(M, requires_grad=True, device=device) alpha = torch.tensor(0.5, requires_grad=True, device=device) val = ot.gromov.partial_fused_gromov_wasserstein2( M1, C11, C12, p1, q1, alpha=alpha ) val.backward() assert val.device == p1.device assert C11.shape == C11.grad.shape assert C12.shape == C12.grad.shape assert alpha.shape == alpha.grad.shape @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tf backend") def test_entropic_partial_gromov_wasserstein(nx): rng = np.random.RandomState(42) n_samples = 10 # nb samples n_noise = 5 # nb of samples (noise) p = ot.unif(n_samples + n_noise) psub = ot.unif(n_samples - 5 + n_noise) q = ot.unif(n_samples + n_noise) mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([0, 0, 0]) cov_t = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) # clean samples xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=rng) P = sp.linalg.sqrtm(cov_t) xt = rng.randn(n_samples, 3).dot(P) + mu_t # add noise xs = np.concatenate((xs, ((rng.rand(n_noise, 2) + 1) * 4)), axis=0) xt = np.concatenate((xt, ((rng.rand(n_noise, 3) + 1) * 10)), axis=0) xt2 = xs[::-1].copy() C1 = ot.dist(xs, xs) C1sub = ot.dist(xs[5:], xs[5:]) C2 = ot.dist(xt, xt) C3 = ot.dist(xt2, xt2) m = 2.0 / 3.0 C1b, C1subb, C2b, C3b, pb, psubb, qb = nx.from_numpy(C1, C1sub, C2, C3, p, psub, q) G0 = ( np.outer(p, q) * m / (np.sum(p) * np.sum(q)) ) # make sure |G0|=m, G01_m\leq p, G0.T1_n\leq q. G0b = nx.from_numpy(G0) # check consistency across backends and stability w.r.t loss/marginals/sym list_sym = [True, None] for i, loss_fun in enumerate(["square_loss", "kl_loss"]): res, log = ot.gromov.entropic_partial_gromov_wasserstein( C1, C3, p=p, q=None, reg=1e4, m=m, loss_fun=loss_fun, G0=None, log=True, symmetric=list_sym[i], verbose=True, numItermax=10, ) resb, logb = ot.gromov.entropic_partial_gromov_wasserstein( C1b, C3b, p=None, q=qb, reg=1e4, m=m, loss_fun=loss_fun, G0=G0b, log=True, symmetric=False, verbose=True, numItermax=10, ) resb_ = nx.to_numpy(resb) try: # some instability can occur with kl. to investigate further. np.testing.assert_allclose(res, resb_, rtol=1e-4) except AssertionError: pass assert np.all(res.sum(1) <= p) # cf convergence wasserstein assert np.all(res.sum(0) <= q) # cf convergence wasserstein # tests with m is None res = ot.gromov.entropic_partial_gromov_wasserstein( C1, C3, p=p, q=None, reg=1e4, G0=None, log=False, symmetric=list_sym[i], verbose=True, numItermax=10, ) resb = ot.gromov.entropic_partial_gromov_wasserstein( C1b, C3b, p=None, q=qb, reg=1e4, G0=None, log=False, symmetric=False, verbose=True, numItermax=10, ) resb_ = nx.to_numpy(resb) np.testing.assert_allclose(res, resb_, rtol=1e-4) np.testing.assert_allclose(np.sum(res), 1.0, rtol=1e-4) # tests with different number of samples across spaces m = 0.5 res, log = ot.gromov.entropic_partial_gromov_wasserstein( C1, C1sub, p=p, q=psub, reg=1e4, m=m, log=True, numItermax=10, ) resb, logb = ot.gromov.entropic_partial_gromov_wasserstein( C1b, C1subb, p=pb, q=psubb, reg=1e4, m=m, log=True, numItermax=10, ) resb_ = nx.to_numpy(resb) np.testing.assert_allclose(res, resb_, rtol=1e-4) assert np.all(res.sum(1) <= p) # cf convergence wasserstein assert np.all(res.sum(0) <= psub) # cf convergence wasserstein np.testing.assert_allclose(np.sum(res), m, rtol=1e-4) # tests for pGW2 for loss_fun in ["square_loss", "kl_loss"]: w0, log0 = ot.gromov.entropic_partial_gromov_wasserstein2( C1, C2, p=None, q=q, reg=1e4, m=m, loss_fun=loss_fun, log=True, numItermax=10, ) w0_val = ot.gromov.entropic_partial_gromov_wasserstein2( C1b, C2b, p=pb, q=None, reg=1e4, m=m, loss_fun=loss_fun, log=False, numItermax=10, ) np.testing.assert_allclose(w0, w0_val, rtol=1e-8) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tf backend") def test_entropic_partial_fused_gromov_wasserstein(nx): rng = np.random.RandomState(42) n_samples = 20 # nb samples n_noise = 10 # nb of samples (noise) p = ot.unif(n_samples + n_noise) psub = ot.unif(n_samples - 5 + n_noise) q = ot.unif(n_samples + n_noise) mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([0, 0, 0]) cov_t = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) # clean samples xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=rng) P = sp.linalg.sqrtm(cov_t) xt = rng.randn(n_samples, 3).dot(P) + mu_t # add noise xs = np.concatenate((xs, ((rng.rand(n_noise, 2) + 1) * 4)), axis=0) xt = np.concatenate((xt, ((rng.rand(n_noise, 3) + 1) * 10)), axis=0) xt2 = xs[::-1].copy() C1 = ot.dist(xs, xs) F1 = xs C1sub = ot.dist(xs[5:], xs[5:]) F1sub = xs[5:] C2 = ot.dist(xt, xt) F2 = xs C3 = ot.dist(xt2, xt2) F3 = xt2 M11sub = ot.dist(F1, F1sub) M12 = ot.dist(F1, F2) M13 = ot.dist(F1, F3) m = 2.0 / 3.0 M11subb, M12b, M13b, C1b, C1subb, C2b, C3b, pb, psubb, qb = nx.from_numpy( M11sub, M12, M13, C1, C1sub, C2, C3, p, psub, q ) G0 = ( np.outer(p, q) * m / (np.sum(p) * np.sum(q)) ) # make sure |G0|=m, G01_m\leq p, G0.T1_n\leq q. G0b = nx.from_numpy(G0) # check consistency across backends and stability w.r.t loss/marginals/sym list_sym = [True, None] for i, loss_fun in enumerate(["square_loss", "kl_loss"]): res, log = ot.gromov.entropic_partial_fused_gromov_wasserstein( M13, C1, C3, p=p, q=None, reg=1e4, m=m, loss_fun=loss_fun, G0=None, log=True, symmetric=list_sym[i], verbose=True, numItermax=10, ) resb, logb = ot.gromov.entropic_partial_fused_gromov_wasserstein( M13b, C1b, C3b, p=None, q=qb, reg=1e4, m=m, loss_fun=loss_fun, G0=G0b, log=True, symmetric=False, verbose=True, numItermax=10, ) resb_ = nx.to_numpy(resb) try: # some instability can occur with kl. to investigate further. np.testing.assert_allclose(res, resb_, rtol=1e-4) except AssertionError: pass assert np.all(res.sum(1) <= p) # cf convergence wasserstein assert np.all(res.sum(0) <= q) # cf convergence wasserstein # tests with m is None res = ot.gromov.entropic_partial_fused_gromov_wasserstein( M13, C1, C3, p=p, q=None, reg=1e4, G0=None, log=False, symmetric=list_sym[i], verbose=True, numItermax=10, ) resb = ot.gromov.entropic_partial_fused_gromov_wasserstein( M13b, C1b, C3b, p=None, q=qb, reg=1e4, G0=None, log=False, symmetric=False, verbose=True, numItermax=10, ) resb_ = nx.to_numpy(resb) np.testing.assert_allclose(res, resb_, rtol=1e-4) np.testing.assert_allclose(np.sum(res), 1.0, rtol=1e-4) # tests with different number of samples across spaces m = 0.5 res, log = ot.gromov.entropic_partial_fused_gromov_wasserstein( M11sub, C1, C1sub, p=p, q=psub, reg=1e4, m=m, log=True, numItermax=10, ) resb, logb = ot.gromov.entropic_partial_fused_gromov_wasserstein( M11subb, C1b, C1subb, p=pb, q=psubb, reg=1e4, m=m, log=True, numItermax=10, ) resb_ = nx.to_numpy(resb) np.testing.assert_allclose(res, resb_, rtol=1e-4) assert np.all(res.sum(1) <= p) # cf convergence wasserstein assert np.all(res.sum(0) <= psub) # cf convergence wasserstein np.testing.assert_allclose(np.sum(res), m, rtol=1e-4) # tests for pGW2 for loss_fun in ["square_loss", "kl_loss"]: w0, log0 = ot.gromov.entropic_partial_fused_gromov_wasserstein2( M12, C1, C2, p=None, q=q, reg=1e4, m=m, loss_fun=loss_fun, log=True, numItermax=10, ) w0_val = ot.gromov.entropic_partial_fused_gromov_wasserstein2( M12b, C1b, C2b, p=pb, q=None, reg=1e4, m=m, loss_fun=loss_fun, log=False, numItermax=10, ) np.testing.assert_allclose(w0, w0_val, rtol=1e-8) python-pot-0.9.7+dfsg/test/gromov/test_quantized.py000066400000000000000000000344071525521106200225030ustar00rootroot00000000000000"""Tests for gromov._quantized.py""" # Author: Cédric Vincent-Cuaz # # License: MIT License import numpy as np import pytest import ot from ot.gromov._quantized import networkx_import, sklearn_import def test_quantized_gw(nx): n_samples = 30 # nb samples rng = np.random.RandomState(0) C1 = rng.uniform(low=0.0, high=10, size=(n_samples, n_samples)) C1 = (C1 + C1.T) / 2.0 C2 = rng.uniform(low=10.0, high=20.0, size=(n_samples, n_samples)) C2 = (C2 + C2.T) / 2.0 p = ot.unif(n_samples) q = ot.unif(n_samples) npart2 = 3 C1b, C2b, pb, qb = nx.from_numpy(C1, C2, p, q) for npart1 in [1, n_samples + 1, 2]: log_tests = [True, False, False, True, True, False] pairs_part_rep = [("random", "random")] if networkx_import: pairs_part_rep += [("louvain", "random"), ("fluid", "pagerank")] if sklearn_import: pairs_part_rep += [("spectral", "random")] count_mode = 0 for part_method, rep_method in pairs_part_rep: log_ = log_tests[count_mode] count_mode += 1 res = ot.gromov.quantized_fused_gromov_wasserstein( C1, C2, npart1, npart2, p, None, C1, None, part_method=part_method, rep_method=rep_method, log=log_, ) resb = ot.gromov.quantized_fused_gromov_wasserstein( C1b, C2b, npart1, npart2, None, qb, None, C2b, part_method=part_method, rep_method=rep_method, log=log_, ) if log_: T_global, Ts_local, T, log = res T_globalb, Ts_localb, Tb, logb = resb else: T_global, Ts_local, T = res T_globalb, Ts_localb, Tb = resb Tb = nx.to_numpy(Tb) # check constraints np.testing.assert_allclose(T, Tb, atol=1e-06) np.testing.assert_allclose( p, Tb.sum(1), atol=1e-06 ) # cf convergence gromov np.testing.assert_allclose( q, Tb.sum(0), atol=1e-06 ) # cf convergence gromov if log_: for key in log.keys(): # The inner test T_global[i, j] != 0. can lead to different # computation of 1D OT computations between partition depending # on the different float errors across backend if key in logb.keys(): np.testing.assert_allclose(log[key], logb[key], atol=1e-06) def test_quantized_fgw(nx): n_samples = 30 # nb samples rng = np.random.RandomState(0) C1 = rng.uniform(low=0.0, high=10, size=(n_samples, n_samples)) C1 = (C1 + C1.T) / 2.0 F1 = rng.uniform(low=0.0, high=10, size=(n_samples, 1)) C2 = rng.uniform(low=10.0, high=20.0, size=(n_samples, n_samples)) C2 = (C2 + C2.T) / 2.0 F2 = rng.uniform(low=0.0, high=10, size=(n_samples, 1)) p = ot.unif(n_samples) q = ot.unif(n_samples) npart1 = 2 npart2 = 3 C1b, C2b, F1b, F2b, pb, qb = nx.from_numpy(C1, C2, F1, F2, p, q) log_tests = [True, False, False, True, True, False] pairs_part_rep = [] if networkx_import: pairs_part_rep += [ ("louvain_fused", "pagerank"), ("louvain", "pagerank_fused"), ("fluid_fused", "pagerank_fused"), ] if sklearn_import: pairs_part_rep += [("spectral_fused", "random")] pairs_part_rep += [("random", "random")] count_mode = 0 alpha = 0.5 for part_method, rep_method in pairs_part_rep: log_ = log_tests[count_mode] count_mode += 1 res = ot.gromov.quantized_fused_gromov_wasserstein( C1, C2, npart1, npart2, p, None, C1, None, F1, F2, alpha, part_method, rep_method, log_, ) resb = ot.gromov.quantized_fused_gromov_wasserstein( C1b, C2b, npart1, npart2, None, qb, None, C2b, F1b, F2b, alpha, part_method, rep_method, log_, ) if log_: T_global, Ts_local, T, log = res T_globalb, Ts_localb, Tb, logb = resb else: T_global, Ts_local, T = res T_globalb, Ts_localb, Tb = resb Tb = nx.to_numpy(Tb) # check constraints np.testing.assert_allclose(T, Tb, atol=1e-06) np.testing.assert_allclose(p, Tb.sum(1), atol=1e-06) # cf convergence gromov np.testing.assert_allclose(q, Tb.sum(0), atol=1e-06) # cf convergence gromov if log_: for key in log.keys(): # The inner test T_global[i, j] != 0. can lead to different # computation of 1D OT computations between partition depending # on the different float errors across backend if key in logb.keys(): np.testing.assert_allclose(log[key], logb[key], atol=1e-06) # complementary tests for utils functions DF1b = ot.dist(F1b, F1b) DF2b = ot.dist(F2b, F2b) C1b_new = alpha * C1b + (1 - alpha) * DF1b C2b_new = alpha * C2b + (1 - alpha) * DF2b part1b = ot.gromov.get_graph_partition( C1b_new, npart1, part_method=pairs_part_rep[-1][0], random_state=0 ) part2b = ot.gromov._quantized.get_graph_partition( C2b_new, npart2, part_method=pairs_part_rep[-1][0], random_state=0 ) rep_indices1b = ot.gromov.get_graph_representants( C1b, part1b, rep_method=pairs_part_rep[-1][1], random_state=0 ) rep_indices2b = ot.gromov.get_graph_representants( C2b, part2b, rep_method=pairs_part_rep[-1][1], random_state=0 ) CR1b, list_R1b, list_p1b, FR1b = ot.gromov.format_partitioned_graph( C1b, pb, part1b, rep_indices1b, F1b, DF1b, alpha ) CR2b, list_R2b, list_p2b, FR2b = ot.gromov.format_partitioned_graph( C2b, qb, part2b, rep_indices2b, F2b, DF2b, alpha ) MRb = ot.dist(FR1b, FR2b) T_globalb, Ts_localb, _ = ot.gromov.quantized_fused_gromov_wasserstein_partitioned( CR1b, CR2b, list_R1b, list_R2b, list_p1b, list_p2b, MRb, alpha, build_OT=False ) T_globalb = nx.to_numpy(T_globalb) np.testing.assert_allclose(T_global, T_globalb, atol=1e-06) for key in Ts_localb.keys(): T_localb = nx.to_numpy(Ts_localb[key]) np.testing.assert_allclose(Ts_local[key], T_localb, atol=1e-06) # tests for edge cases of the graph partitioning for method in ["unknown_method", "GW", "FGW"]: with pytest.raises(ValueError): ot.gromov.get_graph_partition( C1b, npart1, part_method=method, random_state=0 ) with pytest.raises(ValueError): ot.gromov.get_graph_partition( C1b, npart1, part_method=method, alpha=0.5, F=None, random_state=0 ) # tests for edge cases of the representant selection with pytest.raises(ValueError): ot.gromov.get_graph_representants( C1b, part1b, rep_method="unknown_method", random_state=0 ) # tests for edge cases of the format_partitioned_graph function with pytest.raises(ValueError): CR1b, list_R1b, list_p1b, FR1b = ot.gromov.format_partitioned_graph( C1b, pb, part1b, rep_indices1b, F1b, None, alpha ) # Tests in qFGW solvers # for non admissible values of alpha with pytest.raises(ValueError): ot.gromov.quantized_fused_gromov_wasserstein_partitioned( CR1b, CR2b, list_R1b, list_R2b, list_p1b, list_p2b, MRb, 0, build_OT=False ) # for non-consistent feature information provided with pytest.raises(ValueError): ot.gromov.quantized_fused_gromov_wasserstein( C1, C2, npart1, npart2, p, q, None, None, F1, None, 0.5, "spectral_fused", "random", log_, ) @pytest.skip_backend("jax", reason="test very slow with jax backend") def test_quantized_gw_samples(nx): n_samples_1 = 15 # nb samples n_samples_2 = 20 # nb samples rng = np.random.RandomState(0) X1 = rng.uniform(low=0.0, high=10, size=(n_samples_1, 2)) X2 = rng.uniform(low=0.0, high=10, size=(n_samples_2, 4)) p = ot.unif(n_samples_1) q = ot.unif(n_samples_2) npart1 = 2 npart2 = 3 X1b, X2b, pb, qb = nx.from_numpy(X1, X2, p, q) log_tests = [True, False, True] methods = ["random"] if sklearn_import: methods += ["kmeans"] count_mode = 0 alpha = 1.0 for method in methods: log_ = log_tests[count_mode] count_mode += 1 res = ot.gromov.quantized_fused_gromov_wasserstein_samples( X1, X2, npart1, npart2, p, None, None, None, alpha, method, log_ ) resb = ot.gromov.quantized_fused_gromov_wasserstein_samples( X1b, X2b, npart1, npart2, None, qb, None, None, alpha, method, log_ ) if log_: T_global, Ts_local, T, log = res T_globalb, Ts_localb, Tb, logb = resb else: T_global, Ts_local, T = res T_globalb, Ts_localb, Tb = resb Tb = nx.to_numpy(Tb) # check constraints np.testing.assert_allclose(T, Tb, atol=1e-06) np.testing.assert_allclose(p, Tb.sum(1), atol=1e-06) # cf convergence gromov np.testing.assert_allclose(q, Tb.sum(0), atol=1e-06) # cf convergence gromov if log_: for key in log.keys(): # The inner test T_global[i, j] != 0. can lead to different # computation of 1D OT computations between partition depending # on the different float errors across backend if key in logb.keys(): np.testing.assert_allclose(log[key], logb[key], atol=1e-06) # tests for edge cases of the representant selection with pytest.raises(ValueError): ot.gromov.get_partition_and_representants_samples( X1, npart1, method="unknown_method", random_state=0 ) @pytest.skip_backend("jax", reason="test very slow with jax backend") def test_quantized_fgw_samples(nx): n_samples_1 = 20 # nb samples n_samples_2 = 30 # nb samples rng = np.random.RandomState(0) X1 = rng.uniform(low=0.0, high=10, size=(n_samples_1, 2)) X2 = rng.uniform(low=0.0, high=10, size=(n_samples_2, 4)) F1 = rng.uniform(low=0.0, high=10, size=(n_samples_1, 3)) F2 = rng.uniform(low=0.0, high=10, size=(n_samples_2, 3)) p = ot.unif(n_samples_1) q = ot.unif(n_samples_2) npart1 = 2 npart2 = 3 X1b, X2b, F1b, F2b, pb, qb = nx.from_numpy(X1, X2, F1, F2, p, q) methods = [] if sklearn_import: methods += ["kmeans", "kmeans_fused"] methods += ["random"] alpha = 0.5 for npart1 in [1, n_samples_1 + 1, 2]: log_tests = [True, False, True] count_mode = 0 for method in methods: log_ = log_tests[count_mode] count_mode += 1 res = ot.gromov.quantized_fused_gromov_wasserstein_samples( X1, X2, npart1, npart2, p, None, F1, F2, alpha, method, log_ ) resb = ot.gromov.quantized_fused_gromov_wasserstein_samples( X1b, X2b, npart1, npart2, None, qb, F1b, F2b, alpha, method, log_ ) if log_: T_global, Ts_local, T, log = res T_globalb, Ts_localb, Tb, logb = resb else: T_global, Ts_local, T = res T_globalb, Ts_localb, Tb = resb Tb = nx.to_numpy(Tb) # check constraints np.testing.assert_allclose(T, Tb, atol=1e-06) np.testing.assert_allclose( p, Tb.sum(1), atol=1e-06 ) # cf convergence gromov np.testing.assert_allclose( q, Tb.sum(0), atol=1e-06 ) # cf convergence gromov if log_: for key in log.keys(): # The inner test T_global[i, j] != 0. can lead to different # computation of 1D OT computations between partition depending # on the different float errors across backend if key in logb.keys(): np.testing.assert_allclose(log[key], logb[key], atol=1e-06) # complementary tests for utils functions part1b, rep_indices1 = ot.gromov.get_partition_and_representants_samples( X1b, npart1, method=method, random_state=0 ) part2b, rep_indices2 = ot.gromov.get_partition_and_representants_samples( X2b, npart2, method=method, random_state=0 ) CR1b, list_R1b, list_p1b, FR1b = ot.gromov.format_partitioned_samples( X1b, pb, part1b, rep_indices1, F1b, alpha ) CR2b, list_R2b, list_p2b, FR2b = ot.gromov.format_partitioned_samples( X2b, qb, part2b, rep_indices2, F2b, alpha ) MRb = ot.dist(FR1b, FR2b) T_globalb, Ts_localb, _ = ot.gromov.quantized_fused_gromov_wasserstein_partitioned( CR1b, CR2b, list_R1b, list_R2b, list_p1b, list_p2b, MRb, alpha, build_OT=False ) T_globalb = nx.to_numpy(T_globalb) np.testing.assert_allclose(T_global, T_globalb, atol=1e-06) for key in Ts_localb.keys(): T_localb = nx.to_numpy(Ts_localb[key]) np.testing.assert_allclose(Ts_local[key], T_localb, atol=1e-06) # tests for edge cases of the format_partitioned_graph function with pytest.raises(ValueError): CR1b, list_R1b, list_p1b, FR1b = ot.gromov.format_partitioned_samples( X1b, pb, part1b, rep_indices1, None, alpha ) # for non-consistent feature information provided with pytest.raises(ValueError): ot.gromov.quantized_fused_gromov_wasserstein_samples( X1, X2, npart1, npart2, p, None, None, F2, alpha, "fused_spectral", log_ ) python-pot-0.9.7+dfsg/test/gromov/test_semirelaxed.py000066400000000000000000001516201525521106200227760ustar00rootroot00000000000000"""Tests for gromov._semirelaxed.py""" # Author: Cédric Vincent-Cuaz # # License: MIT License import numpy as np import pytest import ot from ot.backend import torch from ot.gromov._utils import networkx_import, sklearn_import def test_semirelaxed_gromov(nx): rng = np.random.RandomState(0) # unbalanced proportions list_n = [30, 15] nt = 2 ns = np.sum(list_n) # create directed sbm with C2 as connectivity matrix C1 = np.zeros((ns, ns), dtype=np.float64) C2 = np.array([[0.8, 0.1], [0.1, 1.0]], dtype=np.float64) pos = [0, 30, 45] for i in range(nt): for j in range(nt): ni, nj = list_n[i], list_n[j] xij = rng.binomial(size=(ni, nj), n=1, p=C2[i, j]) pos_i_min, pos_i_max = pos[i], pos[i + 1] pos_j_min, pos_j_max = pos[j], pos[j + 1] C1[pos_i_min:pos_i_max, pos_j_min:pos_j_max] = xij p = ot.unif(ns, type_as=C1) q0 = ot.unif(C2.shape[0], type_as=C1) G0 = p[:, None] * q0[None, :] # asymmetric C1b, C2b, pb, q0b, G0b = nx.from_numpy(C1, C2, p, q0, G0) for loss_fun in ["square_loss", "kl_loss"]: G, log = ot.gromov.semirelaxed_gromov_wasserstein( C1, C2, p, loss_fun="square_loss", symmetric=None, log=True, G0=G0 ) Gb, logb = ot.gromov.semirelaxed_gromov_wasserstein( C1b, C2b, None, loss_fun="square_loss", symmetric=False, log=True, G0=None, alpha_min=0.0, alpha_max=1.0, ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, nx.sum(Gb, axis=1), atol=1e-04) np.testing.assert_allclose(list_n / ns, np.sum(G, axis=0), atol=1e-01) np.testing.assert_allclose(list_n / ns, nx.sum(Gb, axis=0), atol=1e-01) srgw, log2 = ot.gromov.semirelaxed_gromov_wasserstein2( C1, C2, None, loss_fun="square_loss", symmetric=False, log=True, G0=G0 ) srgwb, logb2 = ot.gromov.semirelaxed_gromov_wasserstein2( C1b, C2b, pb, loss_fun="square_loss", symmetric=None, log=True, G0=None ) G = log2["T"] Gb = nx.to_numpy(logb2["T"]) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose( list_n / ns, Gb.sum(0), atol=1e-04 ) # cf convergence gromov np.testing.assert_allclose(log2["srgw_dist"], logb["srgw_dist"], atol=1e-07) np.testing.assert_allclose(logb2["srgw_dist"], log["srgw_dist"], atol=1e-07) # symmetric - testing various initialization of the OT plan. C1 = 0.5 * (C1 + C1.T) C1b, C2b, pb, q0b, G0b = nx.from_numpy(C1, C2, p, q0, G0) init_plan_list = [ (None, G0b), ("product", None), ("random_product", "random_product"), ] if networkx_import: init_plan_list += [("fluid", "fluid"), ("fluid_soft", "fluid_soft")] if sklearn_import: init_plan_list += [ ("spectral", "spectral"), ("spectral_soft", "spectral_soft"), ("kmeans", "kmeans"), ("kmeans_soft", "kmeans_soft"), ] for init, init_b in init_plan_list: G, log = ot.gromov.semirelaxed_gromov_wasserstein( C1, C2, p, loss_fun="square_loss", symmetric=None, log=True, G0=init ) Gb = ot.gromov.semirelaxed_gromov_wasserstein( C1b, C2b, pb, loss_fun="square_loss", symmetric=True, log=False, G0=init_b ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose( p, nx.sum(Gb, axis=1), atol=1e-04 ) # cf convergence gromov if not isinstance(init, str): np.testing.assert_allclose( list_n / ns, nx.sum(Gb, axis=0), atol=1e-02 ) # cf convergence gromov else: if ( "spectral" not in init ): # issues with spectral clustering related to label switching np.testing.assert_allclose(list_n / ns, nx.sum(Gb, axis=0), atol=1e-02) srgw, log2 = ot.gromov.semirelaxed_gromov_wasserstein2( C1, C2, p, loss_fun="square_loss", symmetric=True, log=True, G0=G0 ) srgwb, logb2 = ot.gromov.semirelaxed_gromov_wasserstein2( C1b, C2b, pb, loss_fun="square_loss", symmetric=None, log=True, G0=None ) srgw_ = ot.gromov.semirelaxed_gromov_wasserstein2( C1, C2, p, loss_fun="square_loss", symmetric=True, log=False, G0=G0 ) G = log2["T"] # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, nx.sum(Gb, 1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose(list_n / ns, np.sum(G, axis=0), atol=1e-01) np.testing.assert_allclose(list_n / ns, nx.sum(Gb, axis=0), atol=1e-01) np.testing.assert_allclose(log2["srgw_dist"], log["srgw_dist"], atol=1e-07) np.testing.assert_allclose(logb2["srgw_dist"], log["srgw_dist"], atol=1e-07) np.testing.assert_allclose(srgw, srgw_, atol=1e-07) def test_semirelaxed_gromov2_gradients(): n_samples = 50 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=5) p = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() if torch: devices = [torch.device("cpu")] if torch.cuda.is_available(): devices.append(torch.device("cuda")) for device in devices: for loss_fun in ["square_loss", "kl_loss"]: # semirelaxed solvers do not support gradients over masses yet. p1 = torch.tensor(p, requires_grad=False, device=device) C11 = torch.tensor(C1, requires_grad=True, device=device) C12 = torch.tensor(C2, requires_grad=True, device=device) val = ot.gromov.semirelaxed_gromov_wasserstein2( C11, C12, p1, loss_fun=loss_fun ) val.backward() assert val.device == p1.device assert p1.grad is None assert C11.shape == C11.grad.shape assert C12.shape == C12.grad.shape def test_srgw_helper_backend(nx): n_samples = 20 # nb samples mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=0) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=1) p = ot.unif(n_samples) q = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() for loss_fun in ["square_loss", "kl_loss"]: C1b, C2b, pb, qb = nx.from_numpy(C1, C2, p, q) Gb, logb = ot.gromov.semirelaxed_gromov_wasserstein( C1b, C2b, pb, loss_fun, armijo=False, symmetric=True, G0=None, log=True ) # calls with nx=None constCb, hC1b, hC2b, fC2tb = ot.gromov.init_matrix_semirelaxed( C1b, C2b, pb, loss_fun ) ones_pb = nx.ones(pb.shape[0], type_as=pb) def f(G): qG = nx.sum(G, 0) marginal_product = nx.outer(ones_pb, nx.dot(qG, fC2tb)) return ot.gromov.gwloss(constCb + marginal_product, hC1b, hC2b, G, nx=None) def df(G): qG = nx.sum(G, 0) marginal_product = nx.outer(ones_pb, nx.dot(qG, fC2tb)) return ot.gromov.gwggrad(constCb + marginal_product, hC1b, hC2b, G, nx=None) def line_search(cost, G, deltaG, Mi, cost_G, df_G): return ot.gromov.solve_semirelaxed_gromov_linesearch( G, deltaG, cost_G, hC1b, hC2b, ones_pb, 0.0, 1.0, fC2t=fC2tb, nx=None ) # feed the precomputed local optimum Gb to semirelaxed_cg res, log = ot.optim.semirelaxed_cg( pb, qb, 0.0, 1.0, f, df, Gb, line_search, log=True, numItermax=1e4, stopThr=1e-9, stopThr2=1e-9, ) # check constraints np.testing.assert_allclose(res, Gb, atol=1e-06) @pytest.mark.parametrize( "loss_fun", [ "square_loss", "kl_loss", pytest.param("unknown_loss", marks=pytest.mark.xfail(raises=ValueError)), ], ) def test_gw_semirelaxed_helper_validation(loss_fun): n_samples = 20 # nb samples mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=0) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=1) p = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) ot.gromov.init_matrix_semirelaxed(C1, C2, p, loss_fun=loss_fun) def test_semirelaxed_fgw(nx): rng = np.random.RandomState(0) list_n = [16, 8] nt = 2 ns = 24 # create directed sbm with C2 as connectivity matrix C1 = np.zeros((ns, ns)) C2 = np.array([[0.7, 0.05], [0.05, 0.9]]) pos = [0, 16, 24] for i in range(nt): for j in range(nt): ni, nj = list_n[i], list_n[j] xij = rng.binomial(size=(ni, nj), n=1, p=C2[i, j]) pos_i_min, pos_i_max = pos[i], pos[i + 1] pos_j_min, pos_j_max = pos[j], pos[j + 1] C1[pos_i_min:pos_i_max, pos_j_min:pos_j_max] = xij F1 = np.zeros((ns, 1)) F1[:16] = rng.normal(loc=0.0, scale=0.01, size=(16, 1)) F1[16:] = rng.normal(loc=1.0, scale=0.01, size=(8, 1)) F2 = np.zeros((2, 1)) F2[1, :] = 1.0 M = ( (F1**2).dot(np.ones((1, nt))) + np.ones((ns, 1)).dot((F2**2).T) - 2 * F1.dot(F2.T) ) p = ot.unif(ns) q0 = ot.unif(C2.shape[0]) G0 = p[:, None] * q0[None, :] # asymmetric structure - checking constraints and values Mb, C1b, C2b, pb, q0b, G0b = nx.from_numpy(M, C1, C2, p, q0, G0) G, log = ot.gromov.semirelaxed_fused_gromov_wasserstein( M, C1, C2, None, loss_fun="square_loss", alpha=0.5, symmetric=None, log=True, G0=None, ) Gb, logb = ot.gromov.semirelaxed_fused_gromov_wasserstein( Mb, C1b, C2b, pb, loss_fun="square_loss", alpha=0.5, symmetric=False, log=True, G0=G0b, ) np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose( p, nx.sum(Gb, axis=1), atol=1e-04 ) # cf convergence gromov np.testing.assert_allclose( [2 / 3, 1 / 3], nx.sum(Gb, axis=0), atol=1e-02 ) # cf convergence gromov # asymmetric - check consistency between srFGW and srFGW2 srgw, log2 = ot.gromov.semirelaxed_fused_gromov_wasserstein2( M, C1, C2, p, loss_fun="square_loss", alpha=0.5, symmetric=False, log=True, G0=G0, ) srgwb, logb2 = ot.gromov.semirelaxed_fused_gromov_wasserstein2( Mb, C1b, C2b, None, loss_fun="square_loss", alpha=0.5, symmetric=None, log=True, G0=None, ) G = log2["T"] Gb = nx.to_numpy(logb2["T"]) np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose( [2 / 3, 1 / 3], G.sum(0), atol=1e-04 ) # cf convergence gromov np.testing.assert_allclose(log2["srfgw_dist"], logb["srfgw_dist"], atol=1e-07) np.testing.assert_allclose(logb2["srfgw_dist"], log["srfgw_dist"], atol=1e-07) # symmetric structures + checking losses + inits C1 = 0.5 * (C1 + C1.T) Mb, C1b, C2b, pb, q0b, G0b = nx.from_numpy(M, C1, C2, p, q0, G0) init_plan_list = [ (None, G0b), ("product", None), ("random_product", "random_product"), ] if networkx_import: init_plan_list += [("fluid", "fluid")] if sklearn_import: init_plan_list += [("kmeans", "kmeans")] for loss_fun in ["square_loss", "kl_loss"]: for init, init_b in init_plan_list: G, log = ot.gromov.semirelaxed_fused_gromov_wasserstein( M, C1, C2, p, loss_fun=loss_fun, alpha=0.5, symmetric=None, log=True, G0=init, ) Gb = ot.gromov.semirelaxed_fused_gromov_wasserstein( Mb, C1b, C2b, pb, loss_fun=loss_fun, alpha=0.5, symmetric=True, log=False, G0=init_b, ) np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose( p, nx.sum(Gb, axis=1), atol=1e-04 ) # cf convergence gromov np.testing.assert_allclose( [2 / 3, 1 / 3], nx.sum(Gb, axis=0), atol=1e-02 ) # cf convergence gromov # checking consistency with srFGW and srFGW2 solvers srgw, log2 = ot.gromov.semirelaxed_fused_gromov_wasserstein2( M, C1, C2, p, loss_fun=loss_fun, alpha=0.5, symmetric=True, log=True, G0=G0 ) srgwb, logb2 = ot.gromov.semirelaxed_fused_gromov_wasserstein2( Mb, C1b, C2b, pb, loss_fun=loss_fun, alpha=0.5, symmetric=None, log=True, G0=None, ) G2 = log2["T"] Gb2 = nx.to_numpy(logb2["T"]) # check constraints np.testing.assert_allclose(G2, Gb2, atol=1e-06) np.testing.assert_allclose(G2, G, atol=1e-06) np.testing.assert_allclose(log2["srfgw_dist"], log["srfgw_dist"], atol=1e-07) np.testing.assert_allclose(logb2["srfgw_dist"], log["srfgw_dist"], atol=1e-07) np.testing.assert_allclose(srgw, srgwb, atol=1e-07) def test_semirelaxed_fgw2_gradients(): n_samples = 20 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=5) p = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) M = ot.dist(xs, xt) C1 /= C1.max() C2 /= C2.max() if torch: devices = [torch.device("cpu")] if torch.cuda.is_available(): devices.append(torch.device("cuda")) for device in devices: # semirelaxed solvers do not support gradients over masses yet. for loss_fun in ["square_loss", "kl_loss"]: p1 = torch.tensor(p, requires_grad=False, device=device) C11 = torch.tensor(C1, requires_grad=True, device=device) C12 = torch.tensor(C2, requires_grad=True, device=device) M1 = torch.tensor(M, requires_grad=True, device=device) val = ot.gromov.semirelaxed_fused_gromov_wasserstein2( M1, C11, C12, p1, loss_fun=loss_fun ) val.backward() assert val.device == p1.device assert p1.grad is None assert C11.shape == C11.grad.shape assert C12.shape == C12.grad.shape assert M1.shape == M1.grad.shape # full gradients with alpha p1 = torch.tensor(p, requires_grad=False, device=device) C11 = torch.tensor(C1, requires_grad=True, device=device) C12 = torch.tensor(C2, requires_grad=True, device=device) M1 = torch.tensor(M, requires_grad=True, device=device) alpha = torch.tensor(0.5, requires_grad=True, device=device) val = ot.gromov.semirelaxed_fused_gromov_wasserstein2( M1, C11, C12, p1, loss_fun=loss_fun, alpha=alpha ) val.backward() assert val.device == p1.device assert p1.grad is None assert C11.shape == C11.grad.shape assert C12.shape == C12.grad.shape assert alpha.shape == alpha.grad.shape def test_srfgw_helper_backend(nx): n_samples = 20 # nb samples mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 1]]) rng = np.random.RandomState(42) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=0) ys = rng.randn(xs.shape[0], 2) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=1) yt = rng.randn(xt.shape[0], 2) p = ot.unif(n_samples) q = ot.unif(n_samples) G0 = p[:, None] * q[None, :] C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() M = ot.dist(ys, yt) M /= M.max() Mb, C1b, C2b, pb, qb, G0b = nx.from_numpy(M, C1, C2, p, q, G0) alpha = 0.5 Gb, logb = ot.gromov.semirelaxed_fused_gromov_wasserstein( Mb, C1b, C2b, pb, "square_loss", alpha=0.5, armijo=False, symmetric=True, G0=G0b, log=True, ) # calls with nx=None constCb, hC1b, hC2b, fC2tb = ot.gromov.init_matrix_semirelaxed( C1b, C2b, pb, loss_fun="square_loss" ) ones_pb = nx.ones(pb.shape[0], type_as=pb) def f(G): qG = nx.sum(G, 0) marginal_product = nx.outer(ones_pb, nx.dot(qG, fC2tb)) return ot.gromov.gwloss(constCb + marginal_product, hC1b, hC2b, G, nx=None) def df(G): qG = nx.sum(G, 0) marginal_product = nx.outer(ones_pb, nx.dot(qG, fC2tb)) return ot.gromov.gwggrad(constCb + marginal_product, hC1b, hC2b, G, nx=None) def line_search(cost, G, deltaG, Mi, cost_G, df_G): return ot.gromov.solve_semirelaxed_gromov_linesearch( G, deltaG, cost_G, C1b, C2b, ones_pb, M=(1 - alpha) * Mb, reg=alpha, nx=None ) # feed the precomputed local optimum Gb to semirelaxed_cg res, log = ot.optim.semirelaxed_cg( pb, qb, (1 - alpha) * Mb, alpha, f, df, Gb, line_search, log=True, numItermax=1e4, stopThr=1e-9, stopThr2=1e-9, ) # check constraints np.testing.assert_allclose(res, Gb, atol=1e-06) def test_entropic_semirelaxed_gromov(nx): # unbalanced proportions list_n = [30, 15] nt = 2 ns = np.sum(list_n) # create directed sbm with C2 as connectivity matrix C1 = np.zeros((ns, ns), dtype=np.float64) C2 = np.array([[0.8, 0.1], [0.1, 0.9]], dtype=np.float64) rng = np.random.RandomState(0) pos = [0, 30, 45] for i in range(nt): for j in range(nt): ni, nj = list_n[i], list_n[j] xij = rng.binomial(size=(ni, nj), n=1, p=C2[i, j]) pos_i_min, pos_i_max = pos[i], pos[i + 1] pos_j_min, pos_j_max = pos[j], pos[j + 1] C1[pos_i_min:pos_i_max, pos_j_min:pos_j_max] = xij p = ot.unif(ns, type_as=C1) q0 = ot.unif(C2.shape[0], type_as=C1) G0 = p[:, None] * q0[None, :] # asymmetric C1b, C2b, pb, q0b, G0b = nx.from_numpy(C1, C2, p, q0, G0) epsilon = 0.1 for loss_fun in ["square_loss", "kl_loss"]: G, log = ot.gromov.entropic_semirelaxed_gromov_wasserstein( C1, C2, p, loss_fun=loss_fun, epsilon=epsilon, symmetric=None, log=True, G0=G0, ) Gb, logb = ot.gromov.entropic_semirelaxed_gromov_wasserstein( C1b, C2b, None, loss_fun=loss_fun, epsilon=epsilon, symmetric=False, log=True, G0=None, ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, nx.sum(Gb, axis=1), atol=1e-04) np.testing.assert_allclose(list_n / ns, np.sum(G, axis=0), atol=1e-01) np.testing.assert_allclose(list_n / ns, nx.sum(Gb, axis=0), atol=1e-01) srgw, log2 = ot.gromov.entropic_semirelaxed_gromov_wasserstein2( C1, C2, None, loss_fun=loss_fun, epsilon=epsilon, symmetric=False, log=True, G0=G0, ) srgwb, logb2 = ot.gromov.entropic_semirelaxed_gromov_wasserstein2( C1b, C2b, pb, loss_fun=loss_fun, epsilon=epsilon, symmetric=None, log=True, G0=None, ) G = log2["T"] Gb = nx.to_numpy(logb2["T"]) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose( list_n / ns, Gb.sum(0), atol=1e-04 ) # cf convergence gromov np.testing.assert_allclose(log2["srgw_dist"], logb["srgw_dist"], atol=1e-07) np.testing.assert_allclose(logb2["srgw_dist"], log["srgw_dist"], atol=1e-07) # symmetric - testing various initialization of the OT plan. C1 = 0.5 * (C1 + C1.T) C1b, C2b, pb, q0b, G0b = nx.from_numpy(C1, C2, p, q0, G0) init_plan_list = [] # tests longer than with CG so we do not test all inits. if networkx_import: init_plan_list += [("fluid", "fluid")] if sklearn_import: init_plan_list += [("kmeans", "kmeans")] init_plan_list += [("product", None), (None, G0b)] for init, init_b in init_plan_list: print(f"---- init : {init} / init_b : {init_b}") G, log = ot.gromov.entropic_semirelaxed_gromov_wasserstein( C1, C2, p, loss_fun="square_loss", epsilon=epsilon, symmetric=None, log=True, G0=init, ) Gb, logb = ot.gromov.entropic_semirelaxed_gromov_wasserstein( C1b, C2b, pb, loss_fun="square_loss", epsilon=epsilon, symmetric=True, log=True, G0=init_b, ) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose( p, nx.sum(Gb, axis=1), atol=1e-04 ) # cf convergence gromov if not isinstance(init, str): np.testing.assert_allclose( list_n / ns, nx.sum(Gb, axis=0), atol=1e-02 ) # cf convergence gromov else: np.testing.assert_allclose(list_n / ns, nx.sum(Gb, axis=0), atol=1e-02) # comparison between srGW and srGW2 solvers srgw, log2 = ot.gromov.entropic_semirelaxed_gromov_wasserstein2( C1, C2, p, loss_fun="square_loss", epsilon=epsilon, symmetric=True, log=True, G0=init, ) srgwb, logb2 = ot.gromov.entropic_semirelaxed_gromov_wasserstein2( C1b, C2b, pb, loss_fun="square_loss", epsilon=epsilon, symmetric=None, log=True, G0=init_b, ) srgw_ = ot.gromov.entropic_semirelaxed_gromov_wasserstein2( C1, C2, p, loss_fun="square_loss", epsilon=epsilon, symmetric=True, log=False, G0=G0, ) G2 = log2["T"] G2b = logb2["T"] # check constraints np.testing.assert_allclose(G2, G2b, atol=1e-06) np.testing.assert_allclose(G2, G, atol=1e-06) np.testing.assert_allclose(log2["srgw_dist"], log["srgw_dist"], atol=1e-07) np.testing.assert_allclose(logb2["srgw_dist"], log["srgw_dist"], atol=1e-07) np.testing.assert_allclose(srgw, srgw_, atol=1e-07) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tf backend") def test_entropic_semirelaxed_gromov_dtype_device(nx): # setup n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=42) xt = xs[::-1].copy() p = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() for tp in nx.__type_list__: print(nx.dtype_device(tp)) for loss_fun in ["square_loss", "kl_loss"]: C1b, C2b, pb = nx.from_numpy(C1, C2, p, type_as=tp) Gb = ot.gromov.entropic_semirelaxed_gromov_wasserstein( C1b, C2b, pb, loss_fun, epsilon=0.1, verbose=True ) gw_valb = ot.gromov.entropic_semirelaxed_gromov_wasserstein2( C1b, C2b, pb, loss_fun, epsilon=0.1, verbose=True ) nx.assert_same_dtype_device(C1b, Gb) nx.assert_same_dtype_device(C1b, gw_valb) def test_entropic_semirelaxed_fgw(nx): rng = np.random.RandomState(0) list_n = [16, 8] nt = 2 ns = 24 # create directed sbm with C2 as connectivity matrix C1 = np.zeros((ns, ns)) C2 = np.array([[0.7, 0.05], [0.05, 0.9]]) pos = [0, 16, 24] for i in range(nt): for j in range(nt): ni, nj = list_n[i], list_n[j] xij = rng.binomial(size=(ni, nj), n=1, p=C2[i, j]) pos_i_min, pos_i_max = pos[i], pos[i + 1] pos_j_min, pos_j_max = pos[j], pos[j + 1] C1[pos_i_min:pos_i_max, pos_j_min:pos_j_max] = xij F1 = np.zeros((ns, 1)) F1[:16] = rng.normal(loc=0.0, scale=0.01, size=(16, 1)) F1[16:] = rng.normal(loc=1.0, scale=0.01, size=(8, 1)) F2 = np.zeros((2, 1)) F2[1, :] = 1.0 M = ( (F1**2).dot(np.ones((1, nt))) + np.ones((ns, 1)).dot((F2**2).T) - 2 * F1.dot(F2.T) ) p = ot.unif(ns) q0 = ot.unif(C2.shape[0]) G0 = p[:, None] * q0[None, :] # asymmetric structure - checking constraints and values Mb, C1b, C2b, pb, q0b, G0b = nx.from_numpy(M, C1, C2, p, q0, G0) G, log = ot.gromov.entropic_semirelaxed_fused_gromov_wasserstein( M, C1, C2, None, loss_fun="square_loss", epsilon=0.1, alpha=0.5, symmetric=None, log=True, G0=None, ) Gb, logb = ot.gromov.entropic_semirelaxed_fused_gromov_wasserstein( Mb, C1b, C2b, pb, loss_fun="square_loss", epsilon=0.1, alpha=0.5, symmetric=False, log=True, G0=G0b, ) np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose( p, nx.sum(Gb, axis=1), atol=1e-04 ) # cf convergence gromov np.testing.assert_allclose( [2 / 3, 1 / 3], nx.sum(Gb, axis=0), atol=1e-02 ) # cf convergence gromov srgw, log2 = ot.gromov.entropic_semirelaxed_fused_gromov_wasserstein2( M, C1, C2, p, loss_fun="square_loss", epsilon=0.1, alpha=0.5, symmetric=False, log=True, G0=G0, ) srgwb, logb2 = ot.gromov.entropic_semirelaxed_fused_gromov_wasserstein2( Mb, C1b, C2b, None, loss_fun="square_loss", epsilon=0.1, alpha=0.5, symmetric=None, log=True, G0=None, ) G = log2["T"] Gb = nx.to_numpy(logb2["T"]) # check constraints np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose(p, Gb.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose( [2 / 3, 1 / 3], Gb.sum(0), atol=1e-04 ) # cf convergence gromov np.testing.assert_allclose(log2["srfgw_dist"], logb["srfgw_dist"], atol=1e-07) np.testing.assert_allclose(logb2["srfgw_dist"], log["srfgw_dist"], atol=1e-07) # symmetric structures + checking losses + inits C1 = 0.5 * (C1 + C1.T) Mb, C1b, C2b, pb, q0b, G0b = nx.from_numpy(M, C1, C2, p, q0, G0) init_plan_list = [ (None, G0b), ("product", None), ("random_product", "random_product"), ] if networkx_import: init_plan_list += [("fluid", "fluid")] if sklearn_import: init_plan_list += [("kmeans", "kmeans")] for loss_fun in ["square_loss", "kl_loss"]: for init, init_b in init_plan_list: G, log = ot.gromov.entropic_semirelaxed_fused_gromov_wasserstein( M, C1, C2, p, loss_fun=loss_fun, epsilon=0.1, alpha=0.5, symmetric=None, log=True, G0=init, ) Gb = ot.gromov.entropic_semirelaxed_fused_gromov_wasserstein( Mb, C1b, C2b, pb, loss_fun=loss_fun, epsilon=0.1, alpha=0.5, symmetric=True, log=False, G0=init_b, ) np.testing.assert_allclose(G, Gb, atol=1e-06) np.testing.assert_allclose( p, nx.sum(Gb, axis=1), atol=1e-04 ) # cf convergence gromov np.testing.assert_allclose( [2 / 3, 1 / 3], nx.sum(Gb, axis=0), atol=1e-02 ) # cf convergence gromov # checking consistency with srFGW and srFGW2 solvers srgw, log2 = ot.gromov.entropic_semirelaxed_fused_gromov_wasserstein2( M, C1, C2, p, loss_fun=loss_fun, epsilon=0.1, alpha=0.5, symmetric=True, log=True, G0=init, ) srgwb, logb2 = ot.gromov.entropic_semirelaxed_fused_gromov_wasserstein2( Mb, C1b, C2b, pb, loss_fun=loss_fun, epsilon=0.1, alpha=0.5, symmetric=None, log=True, G0=init_b, ) G2 = log2["T"] Gb2 = nx.to_numpy(logb2["T"]) np.testing.assert_allclose(G2, Gb2, atol=1e-06) np.testing.assert_allclose(G2, G, atol=1e-06) np.testing.assert_allclose(p, Gb2.sum(1), atol=1e-04) # cf convergence gromov np.testing.assert_allclose( [2 / 3, 1 / 3], Gb2.sum(0), atol=1e-04 ) # cf convergence gromov np.testing.assert_allclose(log2["srfgw_dist"], log["srfgw_dist"], atol=1e-07) np.testing.assert_allclose(logb2["srfgw_dist"], log["srfgw_dist"], atol=1e-07) np.testing.assert_allclose(srgw, srgwb, atol=1e-07) @pytest.skip_backend("tf", reason="test very slow with tf backend") def test_entropic_semirelaxed_fgw_dtype_device(nx): # setup n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=42) xt = xs[::-1].copy() rng = np.random.RandomState(42) ys = rng.randn(xs.shape[0], 2) yt = ys[::-1].copy() p = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C1 /= C1.max() C2 /= C2.max() M = ot.dist(ys, yt) for tp in nx.__type_list__: print(nx.dtype_device(tp)) Mb, C1b, C2b, pb = nx.from_numpy(M, C1, C2, p, type_as=tp) for loss_fun in ["square_loss", "kl_loss"]: Gb = ot.gromov.entropic_semirelaxed_fused_gromov_wasserstein( Mb, C1b, C2b, pb, loss_fun, epsilon=0.1, verbose=True ) fgw_valb = ot.gromov.entropic_semirelaxed_fused_gromov_wasserstein2( Mb, C1b, C2b, pb, loss_fun, epsilon=0.1, verbose=True ) nx.assert_same_dtype_device(C1b, Gb) nx.assert_same_dtype_device(C1b, fgw_valb) @pytest.skip_backend("tf", reason="test very slow with tf backend") @pytest.skip_backend("jax", reason="test very slow with tf backend") def test_semirelaxed_gromov_barycenter(nx): ns = 5 nt = 8 Xs, ys = ot.datasets.make_data_classif("3gauss", ns, random_state=42) Xt, yt = ot.datasets.make_data_classif("3gauss2", nt, random_state=42) C1 = ot.dist(Xs) C2 = ot.dist(Xt) p1 = ot.unif(ns) p2 = ot.unif(nt) n_samples = 3 C1b, C2b, p1b, p2b = nx.from_numpy(C1, C2, p1, p2) # test on admissible stopping criterion with pytest.raises(ValueError): stop_criterion = "unknown stop criterion" _ = ot.gromov.semirelaxed_gromov_barycenters( n_samples, [C1, C2], None, [0.5, 0.5], "square_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, ) # test consistency of outputs across backends with 'square_loss' # using different losses # + tests on different inits init_plan_list = [("fluid", "fluid"), ("kmeans", "kmeans"), ("random", "random")] for init, init_b in init_plan_list: for stop_criterion in ["barycenter", "loss"]: print("--- stop_criterion:", stop_criterion) if (init == "fluid") and (not networkx_import): with pytest.raises(ValueError): Cb = ot.gromov.semirelaxed_gromov_barycenters( n_samples, [C1, C2], None, [0.5, 0.5], "square_loss", max_iter=5, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, G0=init, ) elif (init == "kmeans") and (not sklearn_import): with pytest.raises(ValueError): Cb = ot.gromov.semirelaxed_gromov_barycenters( n_samples, [C1, C2], None, [0.5, 0.5], "square_loss", max_iter=5, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, G0=init, ) else: Cb = ot.gromov.semirelaxed_gromov_barycenters( n_samples, [C1, C2], None, [0.5, 0.5], "square_loss", max_iter=5, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, G0=init, ) Cbb = nx.to_numpy( ot.gromov.semirelaxed_gromov_barycenters( n_samples, [C1b, C2b], [p1b, p2b], [0.5, 0.5], "square_loss", max_iter=5, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, G0=init_b, ) ) np.testing.assert_allclose(Cb, Cbb, atol=1e-06) np.testing.assert_allclose(Cbb.shape, (n_samples, n_samples)) # test of gromov_barycenters with `log` on Cb_, err_ = ot.gromov.semirelaxed_gromov_barycenters( n_samples, [C1, C2], [p1, p2], None, "square_loss", max_iter=5, tol=1e-3, stop_criterion=stop_criterion, verbose=False, warmstartT=True, random_state=42, log=True, G0=init, ) Cbb_, errb_ = ot.gromov.semirelaxed_gromov_barycenters( n_samples, [C1b, C2b], [p1b, p2b], [0.5, 0.5], "square_loss", max_iter=5, tol=1e-3, stop_criterion=stop_criterion, verbose=False, warmstartT=True, random_state=42, log=True, G0=init_b, ) Cbb_ = nx.to_numpy(Cbb_) np.testing.assert_allclose(Cb_, Cbb_, atol=1e-06) np.testing.assert_array_almost_equal( err_["err"], nx.to_numpy(*errb_["err"]) ) np.testing.assert_allclose(Cbb_.shape, (n_samples, n_samples)) # test consistency across backends with larger barycenter than inputs if sklearn_import: C = ot.gromov.semirelaxed_gromov_barycenters( ns, [C1, C2], None, [0.5, 0.5], "square_loss", max_iter=5, tol=1e-3, stop_criterion="loss", verbose=False, random_state=42, G0="kmeans", ) Cb = ot.gromov.semirelaxed_gromov_barycenters( ns, [C1b, C2b], [p1b, p2b], [0.5, 0.5], "square_loss", max_iter=5, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, G0="kmeans", ) np.testing.assert_allclose(C, nx.to_numpy(Cb), atol=1e-06) # test providing init_C C_ = ot.gromov.semirelaxed_gromov_barycenters( ns, [C1, C2], None, [0.5, 0.5], "square_loss", max_iter=5, tol=1e-3, stop_criterion="loss", verbose=False, random_state=42, G0=init, init_C=C1, ) Cb_ = ot.gromov.semirelaxed_gromov_barycenters( ns, [C1b, C2b], [p1b, p2b], [0.5, 0.5], "square_loss", max_iter=5, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, G0=init_b, init_C=C1b, ) np.testing.assert_allclose(C_, Cb_, atol=1e-06) # test consistency across backends with 'kl_loss' Cb2, err = ot.gromov.semirelaxed_gromov_barycenters( n_samples, [C1, C2], [p1, p2], [0.5, 0.5], "kl_loss", max_iter=5, tol=1e-3, warmstartT=False, stop_criterion="loss", log=True, G0=init_b, random_state=42, ) Cb2b, errb = ot.gromov.semirelaxed_gromov_barycenters( n_samples, [C1b, C2b], [p1b, p2b], [0.5, 0.5], "kl_loss", max_iter=5, tol=1e-3, warmstartT=False, stop_criterion="loss", log=True, G0=init_b, random_state=42, ) Cb2b = nx.to_numpy(Cb2b) try: np.testing.assert_allclose(Cb2, Cb2b, atol=1e-06) # may differ from permutation except AssertionError: np.testing.assert_allclose(err["loss"][-1], errb["loss"][-1], atol=1e-06) np.testing.assert_allclose(Cb2b.shape, (n_samples, n_samples)) # test of gromov_barycenters with `log` on # providing init_C similarly than in the function. rng = ot.utils.check_random_state(42) xalea = rng.randn(n_samples, 2) init_C = ot.utils.dist(xalea, xalea) init_C /= init_C.max() init_Cb = nx.from_numpy(init_C) Cb2_, err2_ = ot.gromov.semirelaxed_gromov_barycenters( n_samples, [C1, C2], [p1, p2], [0.5, 0.5], "square_loss", max_iter=10, tol=1e-3, verbose=False, random_state=42, log=True, init_C=init_C, ) Cb2b_, err2b_ = ot.gromov.semirelaxed_gromov_barycenters( n_samples, [C1b, C2b], [p1b, p2b], [0.5, 0.5], "square_loss", max_iter=10, tol=1e-3, verbose=True, random_state=42, init_C=init_Cb, log=True, ) Cb2b_ = nx.to_numpy(Cb2b_) np.testing.assert_allclose(Cb2_, Cb2b_, atol=1e-06) np.testing.assert_array_almost_equal(err2_["err"], nx.to_numpy(*err2b_["err"])) np.testing.assert_allclose(Cb2b_.shape, (n_samples, n_samples)) # test edge cases for gw barycenters: # unique input structure Cb = ot.gromov.semirelaxed_gromov_barycenters( n_samples, [C1], None, None, "square_loss", max_iter=1, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, ) Cbb = nx.to_numpy( ot.gromov.semirelaxed_gromov_barycenters( n_samples, [C1b], None, [1.0], "square_loss", max_iter=1, tol=1e-3, stop_criterion=stop_criterion, verbose=False, random_state=42, ) ) np.testing.assert_allclose(Cb, Cbb, atol=1e-06) np.testing.assert_allclose(Cbb.shape, (n_samples, n_samples)) def test_semirelaxed_fgw_barycenter(nx): ns = 10 nt = 20 Xs, ys = ot.datasets.make_data_classif("3gauss", ns, random_state=42) Xt, yt = ot.datasets.make_data_classif("3gauss2", nt, random_state=42) rng = np.random.RandomState(42) ys = rng.randn(Xs.shape[0], 2) yt = rng.randn(Xt.shape[0], 2) C1 = ot.dist(Xs) C2 = ot.dist(Xt) C1 /= C1.max() C2 /= C2.max() p1, p2 = ot.unif(ns), ot.unif(nt) n_samples = 3 ysb, ytb, C1b, C2b, p1b, p2b = nx.from_numpy(ys, yt, C1, C2, p1, p2) lambdas = [0.5, 0.5] Csb = [C1b, C2b] Ysb = [ysb, ytb] Xb, Cb, logb = ot.gromov.semirelaxed_fgw_barycenters( n_samples, Ysb, Csb, None, lambdas, 0.5, fixed_structure=False, fixed_features=False, loss_fun="square_loss", max_iter=10, tol=1e-3, random_state=12345, log=True, ) # test correspondance with utils function recovered_Cb = ot.gromov.update_barycenter_structure(logb["T"], Csb, lambdas) recovered_Xb = ot.gromov.update_barycenter_feature(logb["T"], Ysb, lambdas) np.testing.assert_allclose(Cb, recovered_Cb) np.testing.assert_allclose(Xb, recovered_Xb) xalea = rng.randn(n_samples, 2) init_C = ot.dist(xalea, xalea) init_C /= init_C.max() init_Cb = nx.from_numpy(init_C) with pytest.raises( ot.utils.UndefinedParameter ): # to raise an error when `fixed_structure=True`and `init_C=None` Xb, Cb, logb = ot.gromov.semirelaxed_fgw_barycenters( n_samples, Ysb, Csb, ps=[p1b, p2b], lambdas=None, alpha=0.5, fixed_structure=True, init_C=None, fixed_features=False, loss_fun="square_loss", max_iter=10, tol=1e-3, ) Xb, Cb = ot.gromov.semirelaxed_fgw_barycenters( n_samples, [ysb, ytb], [C1b, C2b], ps=[p1b, p2b], lambdas=None, alpha=0.5, fixed_structure=True, init_C=init_Cb, fixed_features=False, loss_fun="square_loss", max_iter=10, tol=1e-3, ) Xb, Cb = nx.to_numpy(Xb), nx.to_numpy(Cb) np.testing.assert_allclose(Cb.shape, (n_samples, n_samples)) np.testing.assert_allclose(Xb.shape, (n_samples, ys.shape[1])) init_X = rng.randn(n_samples, ys.shape[1]) init_Xb = nx.from_numpy(init_X) # Tests with `fixed_structure=False` and `fixed_features=True` with pytest.raises( ot.utils.UndefinedParameter ): # to raise an error when `fixed_features=True`and `init_X=None` Xb, Cb, logb = ot.gromov.semirelaxed_fgw_barycenters( n_samples, [ysb, ytb], [C1b, C2b], [p1b, p2b], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=True, init_X=None, loss_fun="square_loss", max_iter=10, tol=1e-3, warmstartT=True, log=True, random_state=98765, verbose=True, ) Xb, Cb, logb = ot.gromov.semirelaxed_fgw_barycenters( n_samples, [ysb, ytb], [C1b, C2b], [p1b, p2b], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=True, init_X=init_Xb, loss_fun="square_loss", max_iter=10, tol=1e-3, warmstartT=True, log=True, random_state=98765, verbose=True, ) X, C = nx.to_numpy(Xb), nx.to_numpy(Cb) np.testing.assert_allclose(C.shape, (n_samples, n_samples)) np.testing.assert_allclose(X.shape, (n_samples, ys.shape[1])) # add test with 'kl_loss' with pytest.raises(ValueError): stop_criterion = "unknown stop criterion" X, C, log = ot.gromov.semirelaxed_fgw_barycenters( n_samples, [ys, yt], [C1, C2], [p1, p2], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=False, loss_fun="kl_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, init_C=C, init_X=X, warmstartT=True, random_state=12345, log=True, ) for stop_criterion in ["barycenter", "loss"]: X, C, log = ot.gromov.semirelaxed_fgw_barycenters( n_samples, [ys, yt], [C1, C2], [p1, p2], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=False, loss_fun="kl_loss", max_iter=10, tol=1e-3, stop_criterion=stop_criterion, init_C=C, init_X=X, warmstartT=True, random_state=12345, log=True, verbose=True, ) np.testing.assert_allclose(C.shape, (n_samples, n_samples)) np.testing.assert_allclose(X.shape, (n_samples, ys.shape[1])) # test correspondance with utils function recovered_C = ot.gromov.update_barycenter_structure( log["T"], [C1, C2], lambdas, None, "kl_loss", True ) np.testing.assert_allclose(C, recovered_C) # test consistency of outputs across backends with 'square_loss' # with various initialization of G0 init_plan_list = [ ("fluid", "fluid"), ("kmeans", "kmeans"), ("product", "product"), ("random", "random"), ] for init, init_b in init_plan_list: print(f"---- init : {init} / init_b : {init_b}") if (init == "fluid") and (not networkx_import): with pytest.raises(ValueError): X, C, log = ot.gromov.semirelaxed_fgw_barycenters( n_samples, [ys, yt], [C1, C2], [p1, p2], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=False, loss_fun="square_loss", max_iter=10, tol=1e-3, stop_criterion="loss", G0=init, warmstartT=True, random_state=12345, log=True, verbose=True, ) elif (init == "kmeans") and (not sklearn_import): with pytest.raises(ValueError): X, C, log = ot.gromov.semirelaxed_fgw_barycenters( n_samples, [ys, yt], [C1, C2], [p1, p2], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=False, loss_fun="square_loss", max_iter=10, tol=1e-3, stop_criterion="loss", G0=init, warmstartT=True, random_state=12345, log=True, verbose=True, ) else: X, C, log = ot.gromov.semirelaxed_fgw_barycenters( n_samples, [ys, yt], [C1, C2], [p1, p2], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=False, loss_fun="square_loss", max_iter=10, tol=1e-3, stop_criterion="loss", G0=init, warmstartT=True, random_state=12345, log=True, verbose=True, ) Xb, Cb, logb = ot.gromov.semirelaxed_fgw_barycenters( n_samples, [ysb, ytb], [C1b, C2b], [p1b, p2b], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=False, loss_fun="square_loss", max_iter=10, tol=1e-3, stop_criterion="loss", G0=init_b, warmstartT=True, random_state=12345, log=True, verbose=True, ) np.testing.assert_allclose(X, nx.to_numpy(Xb)) np.testing.assert_allclose(C, nx.to_numpy(Cb)) # test while providing advanced T inits and init_X != None, and init_C !=None Xb_, Cb_, logb_ = ot.gromov.semirelaxed_fgw_barycenters( n_samples, [ysb, ytb], [C1b, C2b], [p1b, p2b], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=False, loss_fun="square_loss", max_iter=10, tol=1e-3, stop_criterion="loss", G0="random", warmstartT=True, random_state=12345, log=True, verbose=True, init_C=Cb, init_X=Xb, ) np.testing.assert_allclose(Xb, Xb_) np.testing.assert_allclose(Cb, Cb_) # test consistency of backends while barycenter size not strictly inferior to sizes if sklearn_import: Xb_, Cb_, logb_ = ot.gromov.semirelaxed_fgw_barycenters( n_samples, [ysb, ytb], [C1b, C2b], [p1b, p2b], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=False, loss_fun="square_loss", max_iter=10, tol=1e-3, stop_criterion="loss", G0="kmeans", warmstartT=True, random_state=12345, log=True, verbose=True, init_C=Cb, init_X=Xb, ) X, C, log = ot.gromov.semirelaxed_fgw_barycenters( ns, [ys, yt], [C1, C2], [p1, p2], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=False, loss_fun="square_loss", max_iter=10, tol=1e-3, stop_criterion="loss", G0="kmeans", warmstartT=True, random_state=12345, log=True, verbose=True, ) Xb, Cb, logb = ot.gromov.semirelaxed_fgw_barycenters( ns, [ysb, ytb], [C1b, C2b], [p1b, p2b], [0.5, 0.5], 0.5, fixed_structure=False, fixed_features=False, loss_fun="square_loss", max_iter=10, tol=1e-3, stop_criterion="loss", G0="kmeans", warmstartT=True, random_state=12345, log=True, verbose=True, ) np.testing.assert_allclose(X, nx.to_numpy(Xb)) np.testing.assert_allclose(C, nx.to_numpy(Cb)) # test edge cases for semirelaxed fgw barycenters: # unique input structure X, C = ot.gromov.semirelaxed_fgw_barycenters( n_samples, [ys], [C1], [p1], None, 0.5, fixed_structure=False, fixed_features=False, loss_fun="square_loss", max_iter=2, tol=1e-3, stop_criterion=stop_criterion, warmstartT=True, random_state=12345, log=False, verbose=False, ) Xb, Cb = ot.gromov.semirelaxed_fgw_barycenters( n_samples, [ysb], [C1b], [p1b], [1.0], 0.5, fixed_structure=False, fixed_features=False, loss_fun="square_loss", max_iter=2, tol=1e-3, stop_criterion=stop_criterion, warmstartT=True, random_state=12345, log=False, verbose=False, ) np.testing.assert_allclose(C, Cb, atol=1e-06) np.testing.assert_allclose(X, Xb, atol=1e-06) python-pot-0.9.7+dfsg/test/gromov/test_utils.py000066400000000000000000000121461525521106200216330ustar00rootroot00000000000000"""Tests for gromov._utils.py""" # Author: Cédric Vincent-Cuaz # # License: MIT License import itertools import numpy as np import pytest import ot from ot.gromov._utils import networkx_import, sklearn_import def test_update_barycenter(nx): ns = 5 nt = 10 Xs, ys = ot.datasets.make_data_classif("3gauss", ns, random_state=42) Xt, yt = ot.datasets.make_data_classif("3gauss2", nt, random_state=42) rng = np.random.RandomState(42) ys = rng.randn(Xs.shape[0], 2) yt = rng.randn(Xt.shape[0], 2) C1 = ot.dist(Xs) C2 = ot.dist(Xt) C1 /= C1.max() C2 /= C2.max() p1, p2 = ot.unif(ns), ot.unif(nt) n_samples = 3 ysb, ytb, C1b, C2b, p1b, p2b = nx.from_numpy(ys, yt, C1, C2, p1, p2) lambdas = [0.5, 0.5] Csb = [C1b, C2b] Ysb = [ysb, ytb] Tb = [nx.ones((m, n_samples), type_as=C1b) / (m * n_samples) for m in [ns, nt]] pb = nx.concatenate([nx.sum(elem, 0)[None, :] for elem in Tb], axis=0) # test edge cases for the update of the barycenter with `p != None` # and `target=False` Cb = ot.gromov.update_barycenter_structure( [elem.T for elem in Tb], Csb, lambdas, pb, target=False ) Xb = ot.gromov.update_barycenter_feature( [elem.T for elem in Tb], Ysb, lambdas, pb, target=False ) Cbt = ot.gromov.update_barycenter_structure( Tb, Csb, lambdas, None, target=True, check_zeros=False ) Xbt = ot.gromov.update_barycenter_feature( Tb, Ysb, lambdas, None, target=True, check_zeros=False ) np.testing.assert_allclose(Cb, Cbt) np.testing.assert_allclose(Xb, Xbt) # test not supported metrics with pytest.raises(ValueError): Cbt = ot.gromov.update_barycenter_structure( Tb, Csb, lambdas, None, loss_fun="unknown", target=True ) with pytest.raises(ValueError): Xbt = ot.gromov.update_barycenter_feature( Tb, Ysb, lambdas, None, loss_fun="unknown", target=True ) def test_semirelaxed_init_plan(nx): ns = 5 nt = 10 Xs, ys = ot.datasets.make_data_classif("3gauss", ns, random_state=42) Xt, yt = ot.datasets.make_data_classif("3gauss2", nt, random_state=42) rng = np.random.RandomState(42) ys = rng.randn(Xs.shape[0], 2) yt = rng.randn(Xt.shape[0], 2) C1 = ot.dist(Xs) C2 = ot.dist(Xt) C1 /= C1.max() C2 /= C2.max() p1, p2 = ot.unif(ns), ot.unif(nt) ysb, ytb, C1b, C2b, p1b, p2b = nx.from_numpy(ys, yt, C1, C2, p1, p2) # test not supported method with pytest.raises(ValueError): _ = ot.gromov.semirelaxed_init_plan(C1b, C2b, p1b, method="unknown") if sklearn_import: # tests consistency across backends with m > n for method in ["kmeans", "spectral"]: T = ot.gromov.semirelaxed_init_plan(C1b, C2b, p1b, method=method) Tb = ot.gromov.semirelaxed_init_plan(C1b, C2b, p1b, method=method) np.testing.assert_allclose(T, Tb) # tests consistency across backends with m = n T = ot.gromov.semirelaxed_init_plan(C1b, C1b, p1b, method=method) Tb = ot.gromov.semirelaxed_init_plan(C1b, C1b, p1b, method=method) np.testing.assert_allclose(T, Tb) if networkx_import: # tests consistency across backends with m > n T = ot.gromov.semirelaxed_init_plan(C1b, C2b, p1b, method="fluid") Tb = ot.gromov.semirelaxed_init_plan(C1b, C2b, p1b, method="fluid") np.testing.assert_allclose(T, Tb) # tests consistency across backends with m = n T = ot.gromov.semirelaxed_init_plan(C1b, C1b, p1b, method="fluid") Tb = ot.gromov.semirelaxed_init_plan(C1b, C1b, p1b, method="fluid") np.testing.assert_allclose(T, Tb) @pytest.mark.parametrize("divergence", ["kl", "l2"]) def test_div_between_product(nx, divergence): ns = 5 nt = 10 ps, pt = ot.unif(ns), ot.unif(nt) ps, pt = nx.from_numpy(ps), nx.from_numpy(pt) ps1, pt1 = 2 * ps, 2 * pt res_nx = ot.gromov.div_between_product(ps, pt, ps1, pt1, divergence, nx=nx) res = ot.gromov.div_between_product(ps, pt, ps1, pt1, divergence, nx=None) np.testing.assert_allclose(res_nx, res, atol=1e-06) @pytest.mark.parametrize( "divergence, mass", itertools.product(["kl", "l2"], [True, False]) ) def test_div_to_product(nx, divergence, mass): ns = 5 nt = 10 a, b = ot.unif(ns), ot.unif(nt) a, b = nx.from_numpy(a), nx.from_numpy(b) pi = 2 * a[:, None] * b[None, :] pi1, pi2 = nx.sum(pi, 1), nx.sum(pi, 0) res = ot.gromov.div_to_product( pi, a, b, pi1=None, pi2=None, divergence=divergence, mass=mass, nx=None ) res1 = ot.gromov.div_to_product( pi, a, b, pi1=None, pi2=None, divergence=divergence, mass=mass, nx=nx ) res2 = ot.gromov.div_to_product( pi, a, b, pi1=pi1, pi2=pi2, divergence=divergence, mass=mass, nx=None ) res3 = ot.gromov.div_to_product( pi, a, b, pi1=pi1, pi2=pi2, divergence=divergence, mass=mass, nx=nx ) np.testing.assert_allclose(res1, res, atol=1e-06) np.testing.assert_allclose(res2, res, atol=1e-06) np.testing.assert_allclose(res3, res, atol=1e-06) python-pot-0.9.7+dfsg/test/sliced/000077500000000000000000000000001525521106200170105ustar00rootroot00000000000000python-pot-0.9.7+dfsg/test/sliced/__init__.py000066400000000000000000000000001525521106200211070ustar00rootroot00000000000000python-pot-0.9.7+dfsg/test/sliced/test_sliced_distances.py000066400000000000000000000271251525521106200237300ustar00rootroot00000000000000"""Tests for module sliced_distances""" # Author: Adrien Corenflos # Nicolas Courty # Eloi Tanguy # Laetitia Chapel # # License: MIT License import numpy as np import pytest import ot from ot.sliced import get_random_projections from ot.backend import tf, torch def test_env(): import sys print(sys.executable) def test_get_random_projections(): rng = np.random.RandomState(0) projections = get_random_projections(1000, 50, rng) np.testing.assert_almost_equal(np.sum(projections**2, 0), 1.0) def test_sliced_same_dist(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) res = ot.sliced_wasserstein_distance(x, x, u, u, 10, seed=rng) np.testing.assert_almost_equal(res, 0.0) def test_sliced_bad_shapes(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(n, 4) u = ot.utils.unif(n) with pytest.raises(ValueError): _ = ot.sliced_wasserstein_distance(x, y, u, u, 10, seed=rng) def test_sliced_log(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 4) y = rng.randn(n, 4) u = ot.utils.unif(n) _, log = ot.sliced_wasserstein_distance(x, y, u, u, 10, p=1, seed=rng, log=True) assert len(log) == 2 projections = log["projections"] projected_emds = log["projected_emds"] assert projections.shape[1] == len(projected_emds) == 10 for emd in projected_emds: assert emd > 0 def test_sliced_different_dists(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) y = rng.randn(n, 2) res = ot.sliced_wasserstein_distance(x, y, u, u, 10, seed=rng) assert res > 0.0 def test_1d_sliced_equals_emd(): n = 100 m = 120 rng = np.random.RandomState(0) x = rng.randn(n, 1) a = rng.uniform(0, 1, n) a /= a.sum() y = rng.randn(m, 1) u = ot.utils.unif(m) res = ot.sliced_wasserstein_distance(x, y, a, u, 10, seed=42) expected = ot.emd2_1d(x.squeeze(), y.squeeze(), a, u) np.testing.assert_almost_equal(res**2, expected) def test_max_sliced_same_dist(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) res = ot.max_sliced_wasserstein_distance(x, x, u, u, 10, seed=rng) np.testing.assert_almost_equal(res, 0.0) def test_max_sliced_different_dists(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) y = rng.randn(n, 2) res, _ = ot.max_sliced_wasserstein_distance(x, y, u, u, 10, seed=rng, log=True) assert res > 0.0 def test_max_sliced_dim_check(): n = 3 x = np.zeros((n, 2)) y = np.zeros((n + 1, 3)) with pytest.raises(ValueError): _ = ot.max_sliced_wasserstein_distance(x, y, n_projections=10) def test_sliced_same_proj(): n_projections = 10 seed = 12 rng = np.random.RandomState(0) X = rng.randn(8, 2) Y = rng.randn(8, 2) cost1, log1 = ot.sliced_wasserstein_distance( X, Y, seed=seed, n_projections=n_projections, log=True ) P = get_random_projections(X.shape[1], n_projections=10, seed=seed) cost2, log2 = ot.sliced_wasserstein_distance(X, Y, projections=P, log=True) assert np.allclose(log1["projections"], log2["projections"]) assert np.isclose(cost1, cost2) def test_sliced_backend(nx): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(2 * n, 2) P = rng.randn(2, 20) P = P / np.sqrt((P**2).sum(0, keepdims=True)) n_projections = 20 xb, yb, Pb = nx.from_numpy(x, y, P) val0 = ot.sliced_wasserstein_distance(x, y, projections=P) val = ot.sliced_wasserstein_distance(xb, yb, n_projections=n_projections, seed=0) val2 = ot.sliced_wasserstein_distance(xb, yb, n_projections=n_projections, seed=0) assert val > 0 assert val == val2 valb = nx.to_numpy(ot.sliced_wasserstein_distance(xb, yb, projections=Pb)) assert np.allclose(val0, valb) a = rng.uniform(0, 1, n) a /= a.sum() b = rng.uniform(0, 1, 2 * n) b /= b.sum() a_b = nx.from_numpy(a) b_b = nx.from_numpy(b) val = ot.sliced_wasserstein_distance(x, y, a=a, b=b, projections=P) val_b = ot.sliced_wasserstein_distance(xb, yb, a=a_b, b=b_b, projections=Pb) np.testing.assert_almost_equal(val, nx.to_numpy(val_b)) def test_sliced_backend_type_devices(nx): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(2 * n, 2) P = rng.randn(2, 20) P = P / np.sqrt((P**2).sum(0, keepdims=True)) for tp in nx.__type_list__: print(nx.dtype_device(tp)) xb, yb, Pb = nx.from_numpy(x, y, P, type_as=tp) valb = ot.sliced_wasserstein_distance(xb, yb, projections=Pb) nx.assert_same_dtype_device(xb, valb) @pytest.mark.skipif(not tf, reason="tf not installed") def test_sliced_backend_device_tf(): nx = ot.backend.TensorflowBackend() n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(2 * n, 2) P = rng.randn(2, 20) P = P / np.sqrt((P**2).sum(0, keepdims=True)) # Check that everything stays on the CPU with tf.device("/CPU:0"): xb, yb, Pb = nx.from_numpy(x, y, P) valb = ot.sliced_wasserstein_distance(xb, yb, projections=Pb) nx.assert_same_dtype_device(xb, valb) if len(tf.config.list_physical_devices("GPU")) > 0: # Check that everything happens on the GPU xb, yb, Pb = nx.from_numpy(x, y, P) valb = ot.sliced_wasserstein_distance(xb, yb, projections=Pb) nx.assert_same_dtype_device(xb, valb) assert nx.dtype_device(valb)[1].startswith("GPU") def test_max_sliced_backend(nx): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(2 * n, 2) P = rng.randn(2, 20) P = P / np.sqrt((P**2).sum(0, keepdims=True)) n_projections = 20 xb, yb, Pb = nx.from_numpy(x, y, P) val0 = ot.max_sliced_wasserstein_distance(x, y, projections=P) val = ot.max_sliced_wasserstein_distance( xb, yb, n_projections=n_projections, seed=0 ) val2 = ot.max_sliced_wasserstein_distance( xb, yb, n_projections=n_projections, seed=0 ) assert val > 0 assert val == val2 valb = nx.to_numpy(ot.max_sliced_wasserstein_distance(xb, yb, projections=Pb)) assert np.allclose(val0, valb) a = rng.uniform(0, 1, n) a /= a.sum() b = rng.uniform(0, 1, 2 * n) b /= b.sum() a_b = nx.from_numpy(a) b_b = nx.from_numpy(b) val = ot.max_sliced_wasserstein_distance(x, y, a=a, b=b, projections=P) val_b = ot.max_sliced_wasserstein_distance(xb, yb, a=a_b, b=b_b, projections=Pb) np.testing.assert_almost_equal(val, nx.to_numpy(val_b)) def test_max_sliced_backend_type_devices(nx): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(2 * n, 2) P = rng.randn(2, 20) P = P / np.sqrt((P**2).sum(0, keepdims=True)) for tp in nx.__type_list__: print(nx.dtype_device(tp)) xb, yb, Pb = nx.from_numpy(x, y, P, type_as=tp) valb = ot.max_sliced_wasserstein_distance(xb, yb, projections=Pb) nx.assert_same_dtype_device(xb, valb) @pytest.mark.skipif(not tf, reason="tf not installed") def test_max_sliced_backend_device_tf(): nx = ot.backend.TensorflowBackend() n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(2 * n, 2) P = rng.randn(2, 20) P = P / np.sqrt((P**2).sum(0, keepdims=True)) # Check that everything stays on the CPU with tf.device("/CPU:0"): xb, yb, Pb = nx.from_numpy(x, y, P) valb = ot.max_sliced_wasserstein_distance(xb, yb, projections=Pb) nx.assert_same_dtype_device(xb, valb) if len(tf.config.list_physical_devices("GPU")) > 0: # Check that everything happens on the GPU xb, yb, Pb = nx.from_numpy(x, y, P) valb = ot.max_sliced_wasserstein_distance(xb, yb, projections=Pb) nx.assert_same_dtype_device(xb, valb) assert nx.dtype_device(valb)[1].startswith("GPU") class TestSlicedWassersteinScaler: """Integration tests for the scaler parameter in sliced_wasserstein_distance.""" def test_scaler_none_matches_no_scaler(self): rng = np.random.RandomState(0) X_s = rng.normal(0, 1, (50, 3)) X_t = rng.normal(1, 1, (50, 3)) result_default = ot.sliced_wasserstein_distance(X_s, X_t, seed=0) result_none = ot.sliced_wasserstein_distance(X_s, X_t, seed=0, scaler=None) np.testing.assert_allclose(result_default, result_none) def test_scaler_with_datascaler_runs(self): rng = np.random.RandomState(0) X_s = rng.normal(0, 1, (50, 3)) X_t = rng.normal(1, 1, (50, 3)) scaler = ot.utils.DataScaler(norm="standard").fit([X_s, X_t]) result = ot.sliced_wasserstein_distance(X_s, X_t, seed=0, scaler=scaler) assert np.isfinite(result) assert result >= 0 def test_scaler_surfaces_small_scale_signal(self): """Scaled SWD detects a shift in a small-magnitude feature that unscaled SWD misses.""" rng = np.random.RandomState(0) n = 500 X_s = np.column_stack( [ rng.normal(1000, 100, n), rng.normal(0, 1, n), ] ) X_t = np.column_stack( [ rng.normal(1000, 100, n), rng.normal(5, 1, n), ] ) scaler = ot.utils.DataScaler(norm="standard").fit([X_s, X_t]) swd_scaled = ot.sliced_wasserstein_distance( X_s, X_t, seed=0, n_projections=200, scaler=scaler ) assert swd_scaled > 1.0 def test_scaler_with_lambda(self): X_s = np.array([[1.0, 2.0], [3.0, 4.0]]) X_t = np.array([[5.0, 6.0], [7.0, 8.0]]) result = ot.sliced_wasserstein_distance( X_s, X_t, seed=0, scaler=lambda x: x / 10 ) assert np.isfinite(result) def test_invalid_scaler_raises(self): X_s = np.array([[1.0, 2.0]]) X_t = np.array([[2.0, 3.0]]) with pytest.raises(ValueError, match="scaler must be"): ot.sliced_wasserstein_distance(X_s, X_t, scaler=42) def test_max_sliced_scaler_integration(self): rng = np.random.RandomState(0) X_s = rng.normal(0, 1, (50, 3)) X_t = rng.normal(1, 1, (50, 3)) scaler = ot.utils.DataScaler(norm="standard").fit([X_s, X_t]) result = ot.max_sliced_wasserstein_distance(X_s, X_t, seed=0, scaler=scaler) assert np.isfinite(result) def test_sliced_wasserstein_scaler_backend(nx): rng = np.random.RandomState(0) X_s_np = rng.normal(0, 1, (60, 3)) X_t_np = rng.normal(2, 1, (60, 3)) n_projections = 50 P = get_random_projections(X_s_np.shape[1], n_projections, seed=0) X_s_b, X_t_b, Pb = nx.from_numpy(X_s_np, X_t_np, P) scaler_np = ot.utils.DataScaler(norm="standard").fit([X_s_np, X_t_np]) scaler_b = ot.utils.DataScaler(norm="standard").fit([X_s_b, X_t_b]) val_np = ot.sliced_wasserstein_distance( X_s_np, X_t_np, projections=P, scaler=scaler_np ) val_b = ot.sliced_wasserstein_distance( X_s_b, X_t_b, projections=Pb, scaler=scaler_b ) np.testing.assert_allclose(nx.to_numpy(val_b), val_np, atol=1e-5) val_np_max = ot.max_sliced_wasserstein_distance( X_s_np, X_t_np, projections=P, scaler=scaler_np ) val_b_max = ot.max_sliced_wasserstein_distance( X_s_b, X_t_b, projections=Pb, scaler=scaler_b ) np.testing.assert_allclose(nx.to_numpy(val_b_max), val_np_max, atol=1e-5) python-pot-0.9.7+dfsg/test/sliced/test_sliced_plans.py000066400000000000000000000177001525521106200230660ustar00rootroot00000000000000"""Tests for module sliced_plans""" # Author: Adrien Corenflos # Nicolas Courty # Eloi Tanguy # Laetitia Chapel # # License: MIT License import numpy as np import pytest import ot from ot.sliced import get_random_projections from ot.backend import tf, torch def test_sliced_permutations(): n = 4 n_projections = 10 d = 2 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(n, 2) projections = ot.sliced.get_random_projections(d, n_projections, seed=0) # test without provided projections _, _ = ot.sliced.sliced_plans(x, y, n_projections=n_projections) # test with invalid shapes with pytest.raises(AssertionError): ot.sliced.sliced_plans(x[:, 1:], y, projections=projections) def test_sliced_plans(): n = 4 m = 5 n_projections = 10 d = 2 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(m, 2) a = rng.uniform(0, 1, n) a /= a.sum() b = rng.uniform(0, 1, m) b /= b.sum() projections = ot.sliced.get_random_projections(d, n_projections, seed=0) # test with a and b not uniform ot.sliced.sliced_plans(x, y, a, b, projections=projections) # test with the minkowski metric ot.sliced.sliced_plans(x, y, projections=projections, metric="minkowski") # test with an unsupported metric with pytest.raises(AssertionError): ot.sliced.sliced_plans(x, y, projections=projections, metric="mahalanobis") # test with a batch size ot.sliced.sliced_plans(x, y, a, b, projections=projections, batch_size=2) # test permutations n = 5 m = 5 n_projections = 10 d = 2 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(m, 2) a = rng.uniform(0, 1, n) a /= a.sum() b = rng.uniform(0, 1, m) b /= b.sum() # test with the minkowski metric ot.sliced.sliced_plans(x, y, n_projections=10, metric="minkowski") def test_min_pivot_sliced(): n = 10 m = 4 n_projections = 10 d = 2 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(m, 2) a = rng.uniform(0, 1, n) a /= a.sum() b = rng.uniform(0, 1, m) b /= b.sum() projections = ot.sliced.get_random_projections(d, n_projections, seed=0) # identity of the indiscernibles _, min_cost = ot.sliced.min_sliced_transport_plan(x, x, a, a, n_projections=10) np.testing.assert_almost_equal(min_cost, 0.0) _, min_cost = ot.sliced.min_sliced_transport_plan( x, y, a, b, projections=projections, dense=True ) # result should be an upper-bound of W2 and relatively close w2 = ot.emd2(a, b, ot.dist(x, y)) assert min_cost >= w2 assert min_cost <= 1.5 * w2 # test without provided projections ot.sliced.min_sliced_transport_plan( x, y, a, b, n_projections=n_projections, log=True ) # test with invalid shapes with pytest.raises(AssertionError): ot.sliced.min_sliced_transport_plan(x[:, 1:], y, projections=projections) # test the logs _, min_cost, log = ot.sliced.min_sliced_transport_plan( x, y, a, b, projections=projections, dense=False, log=True ) assert len(log) == 5 costs = log["costs"] assert len(costs) == projections.shape[1] assert len(log["min_projection"]) == d assert (log["projections"] == projections).all() for c in costs: assert c > 0 # test with different metrics ot.sliced.min_sliced_transport_plan( x, y, projections=projections, metric="minkowski" ) ot.sliced.min_sliced_transport_plan( x, y, projections=projections, metric="euclidean" ) ot.sliced.min_sliced_transport_plan( x, y, projections=projections, metric="cityblock" ) # test with an unsupported metric with pytest.raises(AssertionError): ot.sliced.min_sliced_transport_plan( x, y, projections=projections, metric="mahalanobis" ) # test with a batch size ot.sliced.min_sliced_transport_plan( x, y, a, b, projections=projections, batch_size=2 ) def test_expected_sliced(): n = 10 m = 24 n_projections = 10 d = 2 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(m, 2) a = rng.uniform(0, 1, n) a /= a.sum() b = rng.uniform(0, 1, m) b /= b.sum() projections = ot.sliced.get_random_projections(d, n_projections, seed=0) _, expected_cost = ot.sliced.expected_sliced_plan( x, y, a, b, dense=True, projections=projections ) # result should be a coarse upper-bound of W2 w2 = ot.emd2(a, b, ot.dist(x, y)) assert expected_cost >= w2 assert expected_cost <= 3 * w2 # test without provided projections ot.sliced.expected_sliced_plan(x, y, n_projections=n_projections, log=True) ot.sliced.expected_sliced_plan(x, y, a, b, n_projections=n_projections, log=True) # test with invalid shapes with pytest.raises(AssertionError): ot.sliced.min_sliced_transport_plan(x[:, 1:], y, projections=projections) # with a small temperature (i.e. large beta), the cost should be close # to min_sliced_transport cost _, expected_cost = ot.sliced.expected_sliced_plan( x, y, a, b, projections=projections, dense=True, beta=1000.0 ) _, min_cost = ot.sliced.min_sliced_transport_plan( x, y, a, b, projections=projections, dense=True ) np.testing.assert_almost_equal(expected_cost, min_cost, decimal=3) # test the logs _, min_cost, log = ot.sliced.expected_sliced_plan( x, y, a, b, projections=projections, dense=False, log=True ) assert len(log) == 4 costs = log["costs"] assert len(costs) == projections.shape[1] assert len(log["weights"]) == projections.shape[1] assert (log["projections"] == projections).all() for c in costs: assert c > 0 # test with the minkowski metric ot.sliced.expected_sliced_plan(x, y, projections=projections, metric="minkowski") # test with an unsupported metric with pytest.raises(AssertionError): ot.sliced.expected_sliced_plan( x, y, projections=projections, metric="mahalanobis" ) # test with a batch size ot.sliced.expected_sliced_plan(x, y, a, b, projections=projections, batch_size=2) def test_sliced_plans_backends(nx): n = 10 m = 24 n_projections = 10 d = 2 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(m, 2) a = rng.uniform(0, 1, n) a /= a.sum() b = rng.uniform(0, 1, m) b /= b.sum() x_b, y_b, a_b, b_b = nx.from_numpy(x, y, a, b) projections_b = ot.sliced.get_random_projections( d, n_projections, seed=0, backend=nx, type_as=x_b ) projections = nx.to_numpy(projections_b) _, expected_cost_b = ot.sliced.expected_sliced_plan( x_b, y_b, a_b, b_b, dense=True, projections=projections_b ) # result should be the same than numpy version _, expected_cost = ot.sliced.expected_sliced_plan( x, y, a, b, dense=True, projections=projections ) np.testing.assert_almost_equal(expected_cost_b, expected_cost) # for min_sliced_transport_plan _, min_cost_b = ot.sliced.min_sliced_transport_plan( x_b, y_b, a_b, b_b, dense=True, projections=projections_b ) # result should be the same than numpy version _, min_cost = ot.sliced.min_sliced_transport_plan( x, y, a, b, dense=True, projections=projections ) np.testing.assert_almost_equal(min_cost_b, min_cost) # for projections projections_b = ot.sliced.get_random_projections( d, n_projections, seed=0, backend=nx, type_as=x_b ) # test with the minkowski metric ot.sliced.min_sliced_transport_plan( x_b, y_b, projections=projections_b, metric="minkowski" ) # test with a batch size ot.sliced.min_sliced_transport_plan( x_b, y_b, a_b, b_b, projections=projections_b, batch_size=2 ) python-pot-0.9.7+dfsg/test/sliced/test_spherical_sliced.py000066400000000000000000000267671525521106200237400ustar00rootroot00000000000000"""Tests for module spherical_sliced""" # Author: Clément Bonet # Nicolas Courty # # License: MIT License import numpy as np import pytest import ot from ot.sliced import get_random_projections from ot.backend import tf, torch def test_projections_stiefel(): rng = np.random.RandomState(0) n_projs = 500 x = rng.randn(100, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) ssw, log = ot.sliced_wasserstein_sphere( x, x, n_projections=n_projs, seed=rng, log=True ) P = log["projections"] P_T = np.transpose(P, [0, 2, 1]) np.testing.assert_almost_equal( np.matmul(P_T, P), np.array([np.eye(2) for k in range(n_projs)]) ) rng = np.random.RandomState(0) projections = ot.sliced.get_projections_sphere(3, n_projs, seed=rng) projections_T = np.transpose(projections, [0, 2, 1]) np.testing.assert_almost_equal( np.matmul(projections_T, projections), np.array([np.eye(2) for k in range(n_projs)]), ) # np.testing.assert_almost_equal(projections, P) def test_projections_sphere_to_circle(): rng = np.random.RandomState(0) n_projs = 500 x = rng.randn(100, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) x_projs, _ = ot.sliced.projection_sphere_to_circle(x, n_projs) assert x_projs.shape == (n_projs, 100) assert np.all(x_projs >= 0) and np.all(x_projs < 1) def test_sliced_sphere_same_dist(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) u = ot.utils.unif(n) res = ot.sliced_wasserstein_sphere(x, x, u, u, 10, seed=rng) np.testing.assert_almost_equal(res, 0.0) def test_sliced_sphere_same_proj(): n_projections = 10 n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) y = rng.randn(n, 3) y = y / np.sqrt(np.sum(y**2, -1, keepdims=True)) seed = 42 cost1, log1 = ot.sliced_wasserstein_sphere( x, y, seed=seed, n_projections=n_projections, log=True ) cost2, log2 = ot.sliced_wasserstein_sphere( x, y, seed=seed, n_projections=n_projections, log=True ) assert np.allclose(log1["projections"], log2["projections"]) assert np.isclose(cost1, cost2) def test_sliced_sphere_bad_shapes(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) y = rng.randn(n, 4) y = y / np.sqrt(np.sum(x**2, -1, keepdims=True)) u = ot.utils.unif(n) with pytest.raises(ValueError): _ = ot.sliced_wasserstein_sphere(x, y, u, u, 10, seed=rng) def test_sliced_sphere_values_on_the_sphere(): n = 100 rng = np.random.RandomState(0) u = ot.utils.unif(n) x = rng.randn(n, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) # dimension problem y = rng.randn(n, 4) with pytest.raises(ValueError): _ = ot.sliced_wasserstein_sphere(x, y, u, u, 10, seed=rng) # not on the sphere y = rng.randn(n, 3) with pytest.raises(ValueError): _ = ot.sliced_wasserstein_sphere(x, y, u, u, 10, seed=rng) with pytest.raises(ValueError): _ = ot.sliced_wasserstein_sphere(y, x, u, u, 10, seed=rng) def test_sliced_sphere_log(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 4) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) y = rng.randn(n, 4) y = y / np.sqrt(np.sum(y**2, -1, keepdims=True)) u = ot.utils.unif(n) res, log = ot.sliced_wasserstein_sphere(x, y, u, u, 10, p=1, seed=rng, log=True) assert len(log) == 2 projections = log["projections"] projected_emds = log["projected_emds"] assert projections.shape[0] == len(projected_emds) == 10 for emd in projected_emds: assert emd > 0 def test_sliced_sphere_different_dists(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) u = ot.utils.unif(n) y = rng.randn(n, 3) y = y / np.sqrt(np.sum(y**2, -1, keepdims=True)) res = ot.sliced_wasserstein_sphere(x, y, u, u, 10, seed=rng) assert res > 0.0 def test_1d_sliced_sphere_equals_emd(): n = 100 m = 120 rng = np.random.RandomState(0) x = rng.randn(n, 2) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) x_coords = (np.arctan2(-x[:, 1], -x[:, 0]) + np.pi) / (2 * np.pi) a = rng.uniform(0, 1, n) a /= a.sum() y = rng.randn(m, 2) y = y / np.sqrt(np.sum(y**2, -1, keepdims=True)) y_coords = (np.arctan2(-y[:, 1], -y[:, 0]) + np.pi) / (2 * np.pi) u = ot.utils.unif(m) res = ot.sliced_wasserstein_sphere(x, y, a, u, 10, seed=42, p=2) expected = ot.binary_search_circle(x_coords.T, y_coords.T, a, u, p=2) res1 = ot.sliced_wasserstein_sphere(x, y, a, u, 10, seed=42, p=1) expected1 = ot.binary_search_circle(x_coords.T, y_coords.T, a, u, p=1) np.testing.assert_almost_equal(res**2, expected) np.testing.assert_almost_equal(res1, expected1, decimal=3) @pytest.skip_backend("tf") def test_sliced_sphere_backend_type_devices(nx): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) y = rng.randn(2 * n, 3) y = y / np.sqrt(np.sum(y**2, -1, keepdims=True)) sw_np, log = ot.sliced_wasserstein_sphere(x, y, log=True) P = log["projections"] for tp in nx.__type_list__: print(nx.dtype_device(tp)) xb, yb = nx.from_numpy(x, y, type_as=tp) valb = ot.sliced_wasserstein_sphere( xb, yb, projections=nx.from_numpy(P, type_as=tp) ) nx.assert_same_dtype_device(xb, valb) np.testing.assert_almost_equal(sw_np, nx.to_numpy(valb)) def test_sliced_sphere_gradient(): if torch: import torch.nn.functional as F X0 = torch.randn((20, 3)) X0 = F.normalize(X0, p=2, dim=-1) X0.requires_grad_(True) X1 = torch.randn((20, 3)) X1 = F.normalize(X1, p=2, dim=-1) sw = ot.sliced_wasserstein_sphere(X1, X0, n_projections=100, p=2) grad_x0 = torch.autograd.grad(sw, X0)[0] assert not torch.any(torch.isnan(grad_x0)) def test_sliced_sphere_unif_values_on_the_sphere(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 3) u = ot.utils.unif(n) with pytest.raises(ValueError): _ = ot.sliced_wasserstein_sphere_unif(x, u, 10, seed=rng) def test_sliced_sphere_unif_log(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 4) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) u = ot.utils.unif(n) res, log = ot.sliced_wasserstein_sphere_unif(x, u, 10, seed=rng, log=True) assert len(log) == 2 projections = log["projections"] projected_emds = log["projected_emds"] assert projections.shape[0] == len(projected_emds) == 10 for emd in projected_emds: assert emd > 0 def test_sliced_sphere_unif_backend_type_devices(nx): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) for tp in nx.__type_list__: print(nx.dtype_device(tp)) xb = nx.from_numpy(x, type_as=tp) valb = ot.sliced_wasserstein_sphere_unif(xb) nx.assert_same_dtype_device(xb, valb) def test_linear_sliced_sphere_same_dist(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) u = ot.utils.unif(n) res = ot.linear_sliced_wasserstein_sphere(x, x, u, u, 10, seed=rng) np.testing.assert_almost_equal(res, 0.0) def test_linear_sliced_sphere_same_proj(): n_projections = 10 n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) y = rng.randn(n, 3) y = y / np.sqrt(np.sum(y**2, -1, keepdims=True)) seed = 42 cost1, log1 = ot.linear_sliced_wasserstein_sphere( x, y, seed=seed, n_projections=n_projections, log=True ) cost2, log2 = ot.linear_sliced_wasserstein_sphere( x, y, seed=seed, n_projections=n_projections, log=True ) assert np.allclose(log1["projections"], log2["projections"]) assert np.isclose(cost1, cost2) def test_linear_sliced_sphere_bad_shapes(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) y = rng.randn(n, 4) y = y / np.sqrt(np.sum(x**2, -1, keepdims=True)) u = ot.utils.unif(n) with pytest.raises(ValueError): _ = ot.linear_sliced_wasserstein_sphere(x, y, u, u, 10, seed=rng) def test_linear_sliced_sphere_values_on_the_sphere(): n = 100 rng = np.random.RandomState(0) u = ot.utils.unif(n) x = rng.randn(n, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) # shape problem y = rng.randn(n, 4) with pytest.raises(ValueError): _ = ot.linear_sliced_wasserstein_sphere(x, y, u, u, 10, seed=rng) # not on sphere y = rng.randn(n, 3) with pytest.raises(ValueError): _ = ot.linear_sliced_wasserstein_sphere(x, y, u, u, 10, seed=rng) with pytest.raises(ValueError): _ = ot.linear_sliced_wasserstein_sphere(y, x, u, u, 10, seed=rng) def test_linear_sliced_sphere_log(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 4) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) y = rng.randn(n, 4) y = y / np.sqrt(np.sum(y**2, -1, keepdims=True)) u = ot.utils.unif(n) res, log = ot.linear_sliced_wasserstein_sphere(x, y, u, u, 10, seed=rng, log=True) assert len(log) == 2 projections = log["projections"] projected_emds = log["projected_emds"] assert projections.shape[0] == len(projected_emds) == 10 for emd in projected_emds: assert emd > 0 def test_linear_sliced_sphere_different_dists(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) u = ot.utils.unif(n) y = rng.randn(n, 3) y = y / np.sqrt(np.sum(y**2, -1, keepdims=True)) res = ot.linear_sliced_wasserstein_sphere(x, y, u, u, 10, seed=rng) assert res > 0.0 def test_1d_linear_sliced_sphere_equals_emd(): n = 100 m = 120 rng = np.random.RandomState(0) x = rng.randn(n, 2) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) x_coords = (np.arctan2(-x[:, 1], -x[:, 0]) + np.pi) / (2 * np.pi) a = rng.uniform(0, 1, n) a /= a.sum() y = rng.randn(m, 2) y = y / np.sqrt(np.sum(y**2, -1, keepdims=True)) y_coords = (np.arctan2(-y[:, 1], -y[:, 0]) + np.pi) / (2 * np.pi) u = ot.utils.unif(m) res = ot.linear_sliced_wasserstein_sphere(x, y, a, u, 100, seed=42) expected = ot.linear_circular_ot(x_coords.T, y_coords.T, a, u) np.testing.assert_almost_equal(res**2, expected, decimal=5) @pytest.skip_backend("tf") def test_linear_sliced_sphere_backend_type_devices(nx): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 3) x = x / np.sqrt(np.sum(x**2, -1, keepdims=True)) y = rng.randn(2 * n, 3) y = y / np.sqrt(np.sum(y**2, -1, keepdims=True)) sw_np, log = ot.linear_sliced_wasserstein_sphere(x, y, log=True) P = log["projections"] for tp in nx.__type_list__: xb, yb = nx.from_numpy(x, y, type_as=tp) valb = ot.linear_sliced_wasserstein_sphere( xb, yb, projections=nx.from_numpy(P, type_as=tp) ) nx.assert_same_dtype_device(xb, valb) np.testing.assert_almost_equal(sw_np, nx.to_numpy(valb)) python-pot-0.9.7+dfsg/test/test_1d_solver.py000066400000000000000000000245371525521106200210670ustar00rootroot00000000000000"""Tests for module 1d Wasserstein solver""" # Author: Adrien Corenflos # Nicolas Courty # Clément Bonet # Laetitia Chapel # # License: MIT License import numpy as np import pytest import ot from ot.backend import tf from ot.lp import wasserstein_1d from scipy.stats import wasserstein_distance from scipy.sparse import coo_array def test_emd_1d_emd2_1d_with_weights(): # test emd1d gives similar results as emd n = 20 m = 30 rng = np.random.RandomState(0) u = rng.randn(n, 1) v = rng.randn(m, 1) w_u = rng.uniform(0.0, 1.0, n) w_u = w_u / w_u.sum() w_v = rng.uniform(0.0, 1.0, m) w_v = w_v / w_v.sum() M = ot.dist(u, v, metric="sqeuclidean") G, log = ot.emd(w_u, w_v, M, log=True) wass = log["cost"] G_1d, log = ot.emd_1d(u, v, w_u, w_v, metric="sqeuclidean", log=True) wass1d = log["cost"] wass1d_emd2 = ot.emd2_1d(u, v, w_u, w_v, metric="sqeuclidean", log=False) wass1d_euc = ot.emd2_1d(u, v, w_u, w_v, metric="euclidean", log=False) # check loss is similar np.testing.assert_allclose(wass, wass1d) np.testing.assert_allclose(wass, wass1d_emd2) # check loss is similar to scipy's implementation for Euclidean metric wass_sp = wasserstein_distance(u.reshape((-1,)), v.reshape((-1,)), w_u, w_v) np.testing.assert_allclose(wass_sp, wass1d_euc) # check constraints np.testing.assert_allclose(w_u, G.sum(1)) np.testing.assert_allclose(w_v, G.sum(0)) # check that an error is raised if the metric is not a Minkowski one np.testing.assert_raises(ValueError, ot.emd_1d, u, v, w_u, w_v, metric="cosine") np.testing.assert_raises(ValueError, ot.emd2_1d, u, v, w_u, w_v, metric="cosine") def test_wasserstein_1d(nx): rng = np.random.RandomState(0) n = 100 x = np.linspace(0, 5, n) rho_u = np.abs(rng.randn(n)) rho_u /= rho_u.sum() rho_v = np.abs(rng.randn(n)) rho_v /= rho_v.sum() xb, rho_ub, rho_vb = nx.from_numpy(x, rho_u, rho_v) # test 1 : wasserstein_1d should be close to scipy W_1 implementation np.testing.assert_almost_equal( wasserstein_1d(xb, xb, rho_ub, rho_vb, p=1), wasserstein_distance(x, x, rho_u, rho_v), ) # test 2 : wasserstein_1d should be close to one when only translating the support np.testing.assert_almost_equal(wasserstein_1d(xb, xb + 1, p=2), 1.0) # test 3 : arrays test X = np.stack((np.linspace(0, 5, n), np.linspace(0, 5, n) * 10), -1) Xb = nx.from_numpy(X) res = wasserstein_1d(Xb, Xb, rho_ub, rho_vb, p=2) np.testing.assert_almost_equal(100 * res[0], res[1], decimal=4) def test_wasserstein_1d_type_devices(nx): rng = np.random.RandomState(0) n = 10 x = np.linspace(0, 5, n) rho_u = np.abs(rng.randn(n)) rho_u /= rho_u.sum() rho_v = np.abs(rng.randn(n)) rho_v /= rho_v.sum() for tp in nx.__type_list__: # print(nx.dtype_device(tp)) xb, rho_ub, rho_vb = nx.from_numpy(x, rho_u, rho_v, type_as=tp) res = wasserstein_1d(xb, xb, rho_ub, rho_vb, p=1) nx.assert_same_dtype_device(xb, res) @pytest.mark.skipif(not tf, reason="tf not installed") def test_wasserstein_1d_device_tf(): nx = ot.backend.TensorflowBackend() rng = np.random.RandomState(0) n = 10 x = np.linspace(0, 5, n) rho_u = np.abs(rng.randn(n)) rho_u /= rho_u.sum() rho_v = np.abs(rng.randn(n)) rho_v /= rho_v.sum() # Check that everything stays on the CPU with tf.device("/CPU:0"): xb, rho_ub, rho_vb = nx.from_numpy(x, rho_u, rho_v) res = wasserstein_1d(xb, xb, rho_ub, rho_vb, p=1) nx.assert_same_dtype_device(xb, res) if len(tf.config.list_physical_devices("GPU")) > 0: # Check that everything happens on the GPU xb, rho_ub, rho_vb = nx.from_numpy(x, rho_u, rho_v) res = wasserstein_1d(xb, xb, rho_ub, rho_vb, p=1) nx.assert_same_dtype_device(xb, res) assert nx.dtype_device(res)[1].startswith("GPU") def test_emd_1d_emd2_1d(): # test emd1d gives similar results as emd n = 20 m = 30 rng = np.random.RandomState(0) u = rng.randn(n, 1) v = rng.randn(m, 1) M = ot.dist(u, v, metric="sqeuclidean") G, log = ot.emd([], [], M, log=True) wass = log["cost"] G_1d, log = ot.emd_1d(u, v, [], [], metric="sqeuclidean", log=True) wass1d = log["cost"] wass1d_emd2 = ot.emd2_1d(u, v, [], [], metric="sqeuclidean", log=False) wass1d_euc = ot.emd2_1d(u, v, [], [], metric="euclidean", log=False) # check loss is similar np.testing.assert_allclose(wass, wass1d) np.testing.assert_allclose(wass, wass1d_emd2) # check loss is similar to scipy's implementation for Euclidean metric wass_sp = wasserstein_distance(u.reshape((-1,)), v.reshape((-1,))) np.testing.assert_allclose(wass_sp, wass1d_euc) # check constraints np.testing.assert_allclose(np.ones((n,)) / n, G.sum(1)) np.testing.assert_allclose(np.ones((m,)) / m, G.sum(0)) # check G is similar np.testing.assert_allclose(G, G_1d, atol=1e-15) # check AssertionError is raised if called on non 1d arrays u = rng.randn(n, 2) v = rng.randn(m, 2) with pytest.raises(AssertionError): ot.emd_1d(u, v, [], []) def test_emd1d_type_devices(nx): rng = np.random.RandomState(0) n = 10 x = np.linspace(0, 5, n) rho_u = np.abs(rng.randn(n)) rho_u /= rho_u.sum() rho_v = np.abs(rng.randn(n)) rho_v /= rho_v.sum() for tp in nx.__type_list__: xb, rho_ub, rho_vb = nx.from_numpy(x, rho_u, rho_v, type_as=tp) emd = ot.emd_1d(xb, xb, rho_ub, rho_vb) emd2 = ot.emd2_1d(xb, xb, rho_ub, rho_vb) nx.assert_same_dtype_device(xb, emd) nx.assert_same_dtype_device(xb, emd2) @pytest.mark.skipif(not tf, reason="tf not installed") def test_emd1d_device_tf(): nx = ot.backend.TensorflowBackend() rng = np.random.RandomState(0) n = 10 x = np.linspace(0, 5, n) rho_u = np.abs(rng.randn(n)) rho_u /= rho_u.sum() rho_v = np.abs(rng.randn(n)) rho_v /= rho_v.sum() # Check that everything stays on the CPU with tf.device("/CPU:0"): xb, rho_ub, rho_vb = nx.from_numpy(x, rho_u, rho_v) emd = ot.emd_1d(xb, xb, rho_ub, rho_vb) emd2 = ot.emd2_1d(xb, xb, rho_ub, rho_vb) nx.assert_same_dtype_device(xb, emd) nx.assert_same_dtype_device(xb, emd2) if len(tf.config.list_physical_devices("GPU")) > 0: # Check that everything happens on the GPU xb, rho_ub, rho_vb = nx.from_numpy(x, rho_u, rho_v) emd = ot.emd_1d(xb, xb, rho_ub, rho_vb) emd2 = ot.emd2_1d(xb, xb, rho_ub, rho_vb) nx.assert_same_dtype_device(xb, emd) nx.assert_same_dtype_device(xb, emd2) assert nx.dtype_device(emd)[1].startswith("GPU") @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") def test_emd1d_dual_with_weights(nx): # test emd1d_dual gives similar results as emd n = 20 m = 30 rng = np.random.RandomState(0) u = rng.randn(n, 1) v = rng.randn(m, 1) w_u = rng.uniform(0.0, 1.0, n) w_u = w_u / w_u.sum() w_v = rng.uniform(0.0, 1.0, m) w_v = w_v / w_v.sum() u, v, w_u, w_v = nx.from_numpy(u, v, w_u, w_v) M = ot.dist(u, v, metric="sqeuclidean") G, log = ot.emd(w_u, w_v, M, log=True) wass = log["cost"] f, g, wass1d = ot.emd_1d_dual_backprop(u, v, w_u, w_v, p=2) # check loss is similar np.testing.assert_allclose(wass, wass1d) np.testing.assert_allclose(wass, nx.sum(f[:, 0] * w_u) + nx.sum(g[:, 0] * w_v)) @pytest.skip_backend("jax") # problem with jax on macOS def test_emd1d_dual_backprop_batch(nx): rng = np.random.RandomState(0) n = 20 rho_u = np.abs(rng.randn(n)) rho_u /= rho_u.sum() rho_v = np.abs(rng.randn(n)) rho_v /= rho_v.sum() rho_ub, rho_vb = nx.from_numpy(rho_u, rho_v) X = np.stack((np.linspace(0, 5, n), np.linspace(0, 5, n) * 10), -1) Xb = nx.from_numpy(X) if nx.__name__ in ["torch", "jax"]: f, g, res = ot.emd_1d_dual_backprop(Xb, Xb, rho_ub, rho_vb, p=2) cost_dual = nx.sum(f * rho_ub[:, None], axis=0) + nx.sum( g * rho_vb[:, None], axis=0 ) np.testing.assert_almost_equal(100 * res[0], res[1], decimal=4) np.testing.assert_allclose(cost_dual, res) else: np.testing.assert_raises( AssertionError, ot.emd_1d_dual_backprop, Xb, Xb, rho_ub, rho_vb, p=2 ) def test_emd1d_dual_type_devices(nx): rng = np.random.RandomState(0) n = 10 x = np.linspace(0, 5, n) rho_u = np.abs(rng.randn(n)) rho_u /= rho_u.sum() rho_v = np.abs(rng.randn(n)) rho_v /= rho_v.sum() for tp in nx.__type_list__: xb, rho_ub, rho_vb = nx.from_numpy(x, rho_u, rho_v, type_as=tp) if nx.__name__ == "torch" or nx.__name__ == "jax": f, g, res = ot.emd_1d_dual_backprop(xb, xb, rho_ub, rho_vb, p=1) nx.assert_same_dtype_device(xb, res) nx.assert_same_dtype_device(xb, f) nx.assert_same_dtype_device(xb, g) def test_index_quantile_function(nx): n = 10 rng = np.random.RandomState(0) u = rng.randn( n, ) a = ot.unif(n) u, a = nx.from_numpy(u, a) u = nx.sort(u) quantiles = nx.cumsum(a) q = ot.lp.solver_1d.quantile_function(quantiles, quantiles, u) q2, idx = ot.lp.solver_1d.quantile_function( quantiles, quantiles, u, return_index=True ) np.testing.assert_allclose(q, q2) np.testing.assert_allclose(nx.to_numpy(idx), np.arange(n)) def test_wasserstein_1d_plan(): rng = np.random.RandomState(0) n = 10 m = 4 d = 1 rng = np.random.RandomState(0) x = rng.randn(n, d) y = rng.randn(m, d) a = rng.uniform(0, 1, n) a /= a.sum() b = rng.uniform(0, 1, m) b /= b.sum() _, plan_1d = wasserstein_1d(x, y, a, b, p=2, return_plans=True) plan_1d = plan_1d[0].toarray() plan_emd = ot.emd(a, b, ot.dist(x, y)) np.testing.assert_allclose(plan_1d, plan_emd, atol=1e-05) _, plan_1d_tuple = wasserstein_1d(x, y, a, b, p=2, return_plans="coo_tuple") plan_1d_tuple = coo_array( (plan_1d_tuple[0].data, (plan_1d_tuple[0].rows, plan_1d_tuple[0].cols)), shape=(x.shape[0], y.shape[0]), ).toarray() np.testing.assert_allclose(plan_1d_tuple, plan_emd, atol=1e-05) python-pot-0.9.7+dfsg/test/test_backend.py000066400000000000000000000643141525521106200205550ustar00rootroot00000000000000"""Tests for backend module""" # Author: Remi Flamary # Nicolas Courty # # # License: MIT License import numpy as np import pytest from numpy.testing import assert_array_almost_equal_nulp import ot import ot.backend from ot.backend import get_backend, get_backend_list, jax, tf, to_numpy, torch def test_get_backend_list(): lst = get_backend_list() assert len(lst) > 0 assert isinstance(lst[0], ot.backend.NumpyBackend) def test_to_numpy(nx): v = nx.zeros(10) M = nx.ones((10, 10)) v2 = to_numpy(v) assert isinstance(v2, np.ndarray) v2, M2 = to_numpy(v, M) assert isinstance(M2, np.ndarray) def test_get_backend_invalid(): # error if no parameters with pytest.raises(ValueError): get_backend() # error if unknown types with pytest.raises(ValueError): get_backend(1, 2.0) def test_get_backend(nx): A = np.zeros((3, 2)) B = np.zeros((3, 1)) nx_np = get_backend(A) assert nx_np.__name__ == "numpy" A2, B2 = nx.from_numpy(A, B) effective_nx = get_backend(A2) assert effective_nx.__name__ == nx.__name__ effective_nx = get_backend(A2, B2) assert effective_nx.__name__ == nx.__name__ if nx.__name__ != "numpy": # test that types mathcing different backends in input raise an error with pytest.raises(ValueError): get_backend(A, B2) else: # Check that subclassing a numpy array does not break get_backend # note: This is only tested for numpy as this is hard to be consistent # with other backends class nx_subclass(nx.__type__): pass A3 = nx_subclass(0) effective_nx = get_backend(A3, B2) assert effective_nx.__name__ == nx.__name__ def test_get_backend_sparse_matrix(): """Test that get_backend correctly handles sparse matrices and rejects mixed backends.""" from scipy.sparse import coo_matrix a_np = np.array([0.5, 0.5]) b_np = np.array([0.5, 0.5]) M_scipy = coo_matrix(([1.0, 2.0], ([0, 1], [0, 1])), shape=(2, 2)) nx = get_backend(a_np, b_np, M_scipy) assert nx.__name__ == "numpy", "NumPy backend should accept scipy.sparse matrices" nx = get_backend(M_scipy) assert nx.__name__ == "numpy", "scipy.sparse should use NumPy backend" if torch: a_torch = torch.tensor([0.5, 0.5]) b_torch = torch.tensor([0.5, 0.5]) M_torch_sparse = torch.sparse_coo_tensor( torch.tensor([[0, 1], [0, 1]]), torch.tensor([1.0, 2.0]), (2, 2) ) nx = get_backend(a_torch, b_torch, M_torch_sparse) assert ( nx.__name__ == "torch" ), "PyTorch backend should accept torch.sparse tensors" nx = get_backend(M_torch_sparse) assert nx.__name__ == "torch", "torch.sparse should use PyTorch backend" # Case 1: PyTorch dense + scipy.sparse (incompatible) with pytest.raises(ValueError): get_backend(a_torch, b_torch, M_scipy) # Case 2: NumPy dense + torch.sparse (incompatible) with pytest.raises(ValueError): get_backend(a_np, b_np, M_torch_sparse) # Case 3: scipy.sparse + torch.sparse (incompatible) with pytest.raises(ValueError): get_backend(M_scipy, M_torch_sparse) def test_convert_between_backends(nx): A = np.zeros((3, 2)) B = np.zeros((3, 1)) A2 = nx.from_numpy(A) B2 = nx.from_numpy(B) assert isinstance(A2, nx.__type__) assert isinstance(B2, nx.__type__) nx2 = get_backend(A2, B2) assert nx2.__name__ == nx.__name__ assert_array_almost_equal_nulp(nx.to_numpy(A2), A) assert_array_almost_equal_nulp(nx.to_numpy(B2), B) def test_empty_backend(): rnd = np.random.RandomState(0) M = rnd.randn(10, 3) v = rnd.randn(3) inds = rnd.randint(10) nx = ot.backend.Backend() with pytest.raises(NotImplementedError): nx.from_numpy(M) with pytest.raises(NotImplementedError): nx.to_numpy(M) with pytest.raises(NotImplementedError): nx.set_gradients(0, 0, 0) with pytest.raises(NotImplementedError): nx.zeros((10, 3)) with pytest.raises(NotImplementedError): nx.ones((10, 3)) with pytest.raises(NotImplementedError): nx.arange(10, 1, 2) with pytest.raises(NotImplementedError): nx.full((10, 3), 3.14) with pytest.raises(NotImplementedError): nx.eye((10, 3)) with pytest.raises(NotImplementedError): nx.sum(M) with pytest.raises(NotImplementedError): nx.cumsum(M) with pytest.raises(NotImplementedError): nx.max(M) with pytest.raises(NotImplementedError): nx.min(M) with pytest.raises(NotImplementedError): nx.maximum(v, v) with pytest.raises(NotImplementedError): nx.minimum(v, v) with pytest.raises(NotImplementedError): nx.abs(M) with pytest.raises(NotImplementedError): nx.log(M) with pytest.raises(NotImplementedError): nx.exp(M) with pytest.raises(NotImplementedError): nx.sqrt(M) with pytest.raises(NotImplementedError): nx.power(v, 2) with pytest.raises(NotImplementedError): nx.dot(v, v) with pytest.raises(NotImplementedError): nx.norm(M) with pytest.raises(NotImplementedError): nx.exp(M) with pytest.raises(NotImplementedError): nx.any(M) with pytest.raises(NotImplementedError): nx.isnan(M) with pytest.raises(NotImplementedError): nx.isinf(M) with pytest.raises(NotImplementedError): nx.einsum("ij->i", M) with pytest.raises(NotImplementedError): nx.sort(M) with pytest.raises(NotImplementedError): nx.argsort(M) with pytest.raises(NotImplementedError): nx.searchsorted(v, v) with pytest.raises(NotImplementedError): nx.flip(M) with pytest.raises(NotImplementedError): nx.outer(v, v) with pytest.raises(NotImplementedError): nx.clip(M, -1, 1) with pytest.raises(NotImplementedError): nx.repeat(M, 0, 1) with pytest.raises(NotImplementedError): nx.take_along_axis(M, v, 0) with pytest.raises(NotImplementedError): nx.concatenate([v, v]) with pytest.raises(NotImplementedError): nx.zero_pad(M, v) with pytest.raises(NotImplementedError): nx.argmax(M) with pytest.raises(NotImplementedError): nx.argmin(M) with pytest.raises(NotImplementedError): nx.mean(M) with pytest.raises(NotImplementedError): nx.median(M) with pytest.raises(NotImplementedError): nx.std(M) with pytest.raises(NotImplementedError): nx.linspace(0, 1, 50) with pytest.raises(NotImplementedError): nx.meshgrid(v, v) with pytest.raises(NotImplementedError): nx.diag(M) with pytest.raises(NotImplementedError): nx.unique([M, M]) with pytest.raises(NotImplementedError): nx.logsumexp(M) with pytest.raises(NotImplementedError): nx.stack([M, M]) with pytest.raises(NotImplementedError): nx.reshape(M, (5, 3, 2)) with pytest.raises(NotImplementedError): nx.seed(42) with pytest.raises(NotImplementedError): nx.rand() with pytest.raises(NotImplementedError): nx.randperm(12) with pytest.raises(NotImplementedError): nx.randn() nx.coo_matrix(M, M, M) with pytest.raises(NotImplementedError): nx.issparse(M) with pytest.raises(NotImplementedError): nx.tocsr(M) with pytest.raises(NotImplementedError): nx.eliminate_zeros(M) with pytest.raises(NotImplementedError): nx.todense(M) with pytest.raises(NotImplementedError): nx.where(M, M, M) with pytest.raises(NotImplementedError): nx.copy(M) with pytest.raises(NotImplementedError): nx.pinv(M) with pytest.raises(NotImplementedError): nx.allclose(M, M) with pytest.raises(NotImplementedError): nx.squeeze(M) with pytest.raises(NotImplementedError): nx.bitsize(M) with pytest.raises(NotImplementedError): nx.device_type(M) with pytest.raises(NotImplementedError): nx._bench(lambda x: x, M, n_runs=1) with pytest.raises(NotImplementedError): nx.solve(M, v) with pytest.raises(NotImplementedError): nx.trace(M) with pytest.raises(NotImplementedError): nx.inv(M) with pytest.raises(NotImplementedError): nx.sqrtm(M) with pytest.raises(NotImplementedError): nx.kl_div(M, M) with pytest.raises(NotImplementedError): nx.isfinite(M) with pytest.raises(NotImplementedError): nx.array_equal(M, M) with pytest.raises(NotImplementedError): nx.is_floating_point(M) with pytest.raises(NotImplementedError): nx.tile(M, (10, 1)) with pytest.raises(NotImplementedError): nx.floor(M) with pytest.raises(NotImplementedError): nx.prod(M) with pytest.raises(NotImplementedError): nx.sort2(M) with pytest.raises(NotImplementedError): nx.qr(M) with pytest.raises(NotImplementedError): nx.atan2(v, v) with pytest.raises(NotImplementedError): nx.transpose(M) with pytest.raises(NotImplementedError): nx.detach(M) with pytest.raises(NotImplementedError): nx.matmul(M, M.T) with pytest.raises(NotImplementedError): nx.nan_to_num(M) with pytest.raises(NotImplementedError): nx.sign(M) with pytest.raises(NotImplementedError): nx.dtype_device(M) with pytest.raises(NotImplementedError): nx.assert_same_dtype_device(M, M) with pytest.raises(NotImplementedError): nx.eigh(M) with pytest.raises(NotImplementedError): nx.det(M) with pytest.raises(NotImplementedError): nx.slogdet(M) with pytest.raises(NotImplementedError): nx.unsqueeze(M, 0) def test_func_backends(nx): rnd = np.random.RandomState(0) M = rnd.randn(10, 3) SquareM = rnd.randn(10, 10) v = rnd.randn(3) val = np.array([1.0]) M1 = rnd.randn(1, 2, 10, 10) M2 = rnd.randn(3, 1, 10, 10) # Sparse tensors test sp_row = np.array([0, 3, 1, 0, 3]) sp_col = np.array([0, 3, 1, 2, 2]) sp_data = np.array([4, 5, 7, 9, 0], dtype=np.float64) M_complex = M + 1j * rnd.randn(10, 3) v_acos = np.clip(v, -0.99, 0.99) lst_tot = [] for nx in [ot.backend.NumpyBackend(), nx]: print("Backend: ", nx.__name__) lst_b = [] lst_name = [] Mb = nx.from_numpy(M) SquareMb = nx.from_numpy(SquareM) vb = nx.from_numpy(v) M1b = nx.from_numpy(M1) M2b = nx.from_numpy(M2) val = nx.from_numpy(val) sp_rowb = nx.from_numpy(sp_row) sp_colb = nx.from_numpy(sp_col) sp_datab = nx.from_numpy(sp_data) A = nx.set_gradients(val, v, v) lst_b.append(nx.to_numpy(A)) lst_name.append("set_gradients") A = nx.detach(Mb) A, B = nx.detach(Mb, Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("detach") A = nx.zeros((10, 3)) A = nx.zeros((10, 3), type_as=Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("zeros") A = nx.ones((10, 3)) A = nx.ones((10, 3), type_as=Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("ones") A = nx.arange(10, 1, 2) lst_b.append(nx.to_numpy(A)) lst_name.append("arange") A = nx.full((10, 3), 3.14) A = nx.full((10, 3), 3.14, type_as=Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("full") A = nx.eye(10, 3) A = nx.eye(10, 3, type_as=Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("eye") A = nx.sum(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("sum") A = nx.sum(Mb, axis=1, keepdims=True) lst_b.append(nx.to_numpy(A)) lst_name.append("sum(axis)") A = nx.cumsum(Mb, 0) lst_b.append(nx.to_numpy(A)) lst_name.append("cumsum(axis)") A = nx.max(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("max") A = nx.max(Mb, axis=1, keepdims=True) lst_b.append(nx.to_numpy(A)) lst_name.append("max(axis)") A = nx.min(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("min") A = nx.min(Mb, axis=1, keepdims=True) lst_b.append(nx.to_numpy(A)) lst_name.append("min(axis)") A = nx.maximum(vb, 0) lst_b.append(nx.to_numpy(A)) lst_name.append("maximum") A = nx.minimum(vb, 0) lst_b.append(nx.to_numpy(A)) lst_name.append("minimum") A = nx.sign(vb) lst_b.append(nx.to_numpy(A)) lst_name.append("sign") A = nx.abs(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("abs") A = nx.log(A) lst_b.append(nx.to_numpy(A)) lst_name.append("log") A = nx.exp(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("exp") A = nx.sqrt(nx.abs(Mb)) lst_b.append(nx.to_numpy(A)) lst_name.append("sqrt") A = nx.power(Mb, 2) lst_b.append(nx.to_numpy(A)) lst_name.append("power") A = nx.dot(vb, vb) lst_b.append(nx.to_numpy(A)) lst_name.append("dot(v,v)") A = nx.dot(Mb, vb) lst_b.append(nx.to_numpy(A)) lst_name.append("dot(M,v)") A = nx.dot(Mb, Mb.T) lst_b.append(nx.to_numpy(A)) lst_name.append("dot(M,M)") A = nx.norm(vb) lst_b.append(nx.to_numpy(A)) lst_name.append("norm") A = nx.norm(Mb, axis=1) lst_b.append(nx.to_numpy(A)) lst_name.append("norm(M,axis=1)") A = nx.norm(Mb, axis=1, keepdims=True) lst_b.append(nx.to_numpy(A)) lst_name.append("norm(M,axis=1,keepdims=True)") A = nx.any(vb > 0) lst_b.append(nx.to_numpy(A)) lst_name.append("any") A = nx.isnan(vb) lst_b.append(nx.to_numpy(A)) lst_name.append("isnan") A = nx.isinf(vb) lst_b.append(nx.to_numpy(A)) lst_name.append("isinf") A = nx.einsum("ij->i", Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("einsum(ij->i)") A = nx.einsum("ij,j->i", Mb, vb) lst_b.append(nx.to_numpy(A)) lst_name.append("nx.einsum(ij,j->i)") A = nx.einsum("ij->i", Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("nx.einsum(ij->i)") A = nx.sort(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("sort") A = nx.argsort(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("argsort") tmp = nx.sort(Mb) A = nx.searchsorted(tmp, tmp, "right") lst_b.append(nx.to_numpy(A)) lst_name.append("searchsorted") A = nx.flip(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("flip") A = nx.outer(vb, vb) lst_b.append(nx.to_numpy(A)) lst_name.append("outer") A = nx.clip(vb, 0, 1) lst_b.append(nx.to_numpy(A)) lst_name.append("clip") A = nx.repeat(Mb, 0) A = nx.repeat(Mb, 2, -1) lst_b.append(nx.to_numpy(A)) lst_name.append("repeat") A = nx.take_along_axis(vb, nx.arange(3), -1) lst_b.append(nx.to_numpy(A)) lst_name.append("take_along_axis") A = nx.concatenate((Mb, Mb), -1) lst_b.append(nx.to_numpy(A)) lst_name.append("concatenate") A = nx.zero_pad(Mb, len(Mb.shape) * [(3, 3)]) lst_b.append(nx.to_numpy(A)) lst_name.append("zero_pad") A = nx.argmax(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("argmax") A = nx.argmin(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("argmin") A = nx.mean(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("mean") A = nx.median(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("median") A = nx.std(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("std") A = nx.linspace(0, 1, 50) A = nx.linspace(0, 1, 50, type_as=Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("linspace") X, Y = nx.meshgrid(vb, vb) lst_b.append(np.stack([nx.to_numpy(X), nx.to_numpy(Y)])) lst_name.append("meshgrid") A = nx.diag(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("diag2D") A = nx.diag(vb, 1) lst_b.append(nx.to_numpy(A)) lst_name.append("diag1D") A = nx.unique(nx.from_numpy(np.stack([M, M]))) lst_b.append(nx.to_numpy(A)) lst_name.append("unique") A, A2 = nx.unique( nx.from_numpy(np.stack([M, M]).reshape(-1)), return_inverse=True ) lst_b.append(nx.to_numpy(A)) lst_name.append("unique(M,return_inverse=True)[0]") lst_b.append(nx.to_numpy(A2)) lst_name.append("unique(M,return_inverse=True)[1]") A = nx.logsumexp(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("logsumexp") A = nx.stack([Mb, Mb]) lst_b.append(nx.to_numpy(A)) lst_name.append("stack") A = nx.reshape(Mb, (5, 3, 2)) lst_b.append(nx.to_numpy(A)) lst_name.append("reshape") sp_Mb = nx.coo_matrix(sp_datab, sp_rowb, sp_colb, shape=(4, 4)) nx.todense(Mb) lst_b.append(nx.to_numpy(nx.todense(sp_Mb))) lst_name.append("coo_matrix") assert not nx.issparse(Mb), "Assert fail on: issparse (expected False)" assert nx.issparse(sp_Mb) or nx.__name__ in ( "jax", "tf", ), "Assert fail on: issparse (expected True)" A = nx.tocsr(sp_Mb) lst_b.append(nx.to_numpy(nx.todense(A))) lst_name.append("tocsr") A = nx.eliminate_zeros(nx.copy(sp_datab), threshold=5.0) lst_b.append(nx.to_numpy(A)) lst_name.append("eliminate_zeros (dense)") A = nx.eliminate_zeros(sp_Mb) lst_b.append(nx.to_numpy(nx.todense(A))) lst_name.append("eliminate_zeros (sparse)") A = nx.where(Mb >= nx.stack([nx.linspace(0, 1, 10)] * 3, axis=1), Mb, 0.0) lst_b.append(nx.to_numpy(A)) lst_name.append("where (cond, x, y)") A = nx.where(nx.from_numpy(np.array([True, False]))) lst_b.append(nx.to_numpy(nx.stack(A))) lst_name.append("where (cond)") A = nx.copy(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("copy") assert nx.allclose(Mb, Mb), "Assert fail on: allclose (expected True)" assert not nx.allclose(2 * Mb, Mb), "Assert fail on: allclose (expected False)" A = nx.squeeze(nx.zeros((3, 1, 4, 1))) assert tuple(A.shape) == (3, 4), "Assert fail on: squeeze" A = nx.unsqueeze(nx.zeros((3, 1, 4)), -1) assert tuple(A.shape) == (3, 1, 4, 1), "Assert fail on: unsqueeze" A = nx.bitsize(Mb) lst_b.append(float(A)) lst_name.append("bitsize") A = nx.device_type(Mb) assert A in ("CPU", "GPU") nx._bench(lambda x: x, M, n_runs=1) A = nx.solve(SquareMb, Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("solve") A = nx.trace(SquareMb) lst_b.append(nx.to_numpy(A)) lst_name.append("trace") A = nx.trace(nx.stack([SquareMb, SquareMb], axis=0)) lst_b.append(nx.to_numpy(A)) lst_name.append("broadcast trace") A = nx.inv(SquareMb) lst_b.append(nx.to_numpy(A)) lst_name.append("matrix inverse") A = nx.sqrtm(SquareMb.T @ SquareMb) lst_b.append(nx.to_numpy(A)) lst_name.append("matrix square root") A = nx.sqrtm(nx.stack([SquareMb.T @ SquareMb] * 2, axis=0))[None, :] lst_b.append(nx.to_numpy(A)) lst_name.append("broadcast matrix square root") D, U = nx.eigh(SquareMb.T @ SquareMb) lst_b.append(nx.to_numpy(nx.dot(U, nx.dot(nx.diag(D), U.T)))) lst_name.append("eigh ") A = nx.kl_div(nx.abs(Mb), nx.abs(Mb) + 1) lst_b.append(nx.to_numpy(A)) lst_name.append("Kullback-Leibler divergence") A = nx.concatenate([vb, nx.from_numpy(np.array([np.inf, np.nan]))], axis=0) A = nx.isfinite(A) lst_b.append(nx.to_numpy(A)) lst_name.append("isfinite") A = nx.tile(vb, (10, 1)) lst_b.append(nx.to_numpy(A)) lst_name.append("tile") A = nx.floor(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("floor") A = nx.prod(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("prod") A, B = nx.sort2(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("sort2 sort") lst_b.append(nx.to_numpy(B)) lst_name.append("sort2 argsort") A, B = nx.qr(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("QR Q") lst_b.append(nx.to_numpy(B)) lst_name.append("QR R") A = nx.atan2(vb, vb) lst_b.append(nx.to_numpy(A)) lst_name.append("atan2") M_complex_b = nx.from_numpy(M_complex) A = nx.real(M_complex_b) lst_b.append(nx.to_numpy(A)) lst_name.append("real") A = nx.imag(M_complex_b) lst_b.append(nx.to_numpy(A)) lst_name.append("imag") A = nx.conj(M_complex_b) lst_b.append(nx.to_numpy(A)) lst_name.append("conj") v_acos_b = nx.from_numpy(v_acos) A = nx.arccos(v_acos_b) lst_b.append(nx.to_numpy(A)) lst_name.append("arccos") A = nx.transpose(Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("transpose") A, B = nx.detach(Mb, Mb) lst_b.append(nx.to_numpy(A)) lst_name.append("detach A") lst_b.append(nx.to_numpy(B)) lst_name.append("detach B") A = nx.matmul(Mb, Mb.T) lst_b.append(nx.to_numpy(A)) lst_name.append("matmul") A = nx.matmul(M1b, M2b) lst_b.append(nx.to_numpy(A)) lst_name.append("matmul broadcast") vec = nx.from_numpy(np.array([1, np.nan, -1])) vec = nx.nan_to_num(vec, nan=0) lst_b.append(nx.to_numpy(vec)) lst_name.append("nan_to_num") d = nx.det(M1b) lst_b.append(nx.to_numpy(d)) lst_name.append("det") s, logabsd = nx.slogdet(M1b) s, logabsd = nx.to_numpy(s), nx.to_numpy(logabsd) lst_b.append(np.array([s, logabsd])) lst_name.append("slogdet") assert not nx.array_equal(Mb, vb), "array_equal (shape)" assert nx.array_equal(Mb, Mb), "array_equal (elements) - expected true" assert not nx.array_equal( Mb, Mb + nx.eye(*list(Mb.shape)) ), "array_equal (elements) - expected false" assert nx.is_floating_point(Mb), "is_floating_point - expected true" assert not nx.is_floating_point( nx.from_numpy(np.array([0, 1, 2], dtype=int)) ), "is_floating_point - expected false" lst_tot.append(lst_b) lst_np = lst_tot[0] lst_b = lst_tot[1] for a1, a2, name in zip(lst_np, lst_b, lst_name): np.testing.assert_allclose( a2, a1, atol=1e-7, err_msg=f"ASSERT FAILED ON: {name}" ) def test_random_backends(nx): tmp_u = nx.rand() assert tmp_u < 1 tmp_n = nx.randn() nx.seed(0) M1 = nx.to_numpy(nx.rand(5, 2)) nx.seed(0) M2 = nx.to_numpy(nx.rand(5, 2, type_as=tmp_n)) assert np.all(M1 >= 0) assert np.all(M1 < 1) assert M1.shape == (5, 2) assert np.allclose(M1, M2) nx.seed(0) M1 = nx.to_numpy(nx.randn(5, 2)) nx.seed(0) M2 = nx.to_numpy(nx.randn(5, 2, type_as=tmp_u)) nx.seed(42) v1 = nx.randn() v2 = nx.randn() assert v1 != v2 nx.seed(0) M1 = nx.to_numpy(nx.randperm(5)) nx.seed(0) M2 = nx.to_numpy(nx.randperm(5, type_as=tmp_u)) M3 = nx.arange(5) M4 = nx.sort(nx.randperm(5)) assert np.allclose(M3, M4) with pytest.raises(ValueError, match="size must be"): res = nx.randperm(size=[5, 12]) def test_gradients_backends(): rnd = np.random.RandomState(0) v = rnd.randn(10) c = rnd.randn() e = rnd.randn() if torch: nx = ot.backend.TorchBackend() v2 = torch.tensor(v, requires_grad=True) c2 = torch.tensor(c, requires_grad=True) val = c2 * torch.sum(v2 * v2) val2 = nx.set_gradients(val, (v2, c2), (v2, c2)) val2.backward() assert torch.equal(v2.grad, v2) assert torch.equal(c2.grad, c2) if jax: nx = ot.backend.JaxBackend() with jax.checking_leaks(): def fun(a, b, d): val = b * nx.sum(a**4) + d return nx.set_gradients(val, (a, b, d), (a, b, 2 * d)) grad_val = jax.grad(fun, argnums=(0, 1, 2))(v, c, e) np.testing.assert_almost_equal(fun(v, c, e), c * np.sum(v**4) + e, decimal=4) np.testing.assert_allclose(grad_val[0], v, atol=1e-4) np.testing.assert_allclose(grad_val[2], 2 * e, atol=1e-4) with jax.checking_leaks(): def f(x): return nx.sum(nx.abs(x)) grad_val = jax.grad(f)(nx.zeros((3,))) np.testing.assert_allclose(grad_val, nx.zeros((3,))) if tf: nx = ot.backend.TensorflowBackend() w = tf.Variable(tf.random.normal((3, 2)), name="w") b = tf.Variable(tf.random.normal((2,), dtype=tf.float32), name="b") x = tf.random.normal((1, 3), dtype=tf.float32) with tf.GradientTape() as tape: y = x @ w + b loss = tf.reduce_mean(y**2) manipulated_loss = nx.set_gradients(loss, (w, b), (w, b)) [dl_dw, dl_db] = tape.gradient(manipulated_loss, [w, b]) assert nx.allclose(dl_dw, w) assert nx.allclose(dl_db, b) def test_sqrtm_backward_torch(): if not torch: pytest.skip("Torch not available") nx = ot.backend.TorchBackend() torch.manual_seed(42) d = 5 A = torch.randn(d, d, dtype=torch.float64, device="cpu") A = A @ A.T A.requires_grad_(True) func = lambda x: nx.sqrtm(x).sum() assert torch.autograd.gradcheck(func, (A,), atol=1e-4, rtol=1e-4) def test_get_backend_none(): a, b = np.zeros((2, 3)), None nx = get_backend(a, b) assert str(nx) == "numpy" with pytest.raises(ValueError): get_backend(None, None) python-pot-0.9.7+dfsg/test/test_bregman.py000066400000000000000000001404331525521106200205760ustar00rootroot00000000000000"""Tests for module bregman on OT with bregman projections""" # Author: Remi Flamary # Kilian Fatras # Quang Huy Tran # Eduardo Fernandes Montesuma # # License: MIT License import warnings from itertools import product import numpy as np import pytest import ot from ot.backend import tf, torch from ot.bregman import geomloss @pytest.mark.parametrize("verbose, warn", product([True, False], [True, False])) def test_sinkhorn(verbose, warn): # test sinkhorn n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) G = ot.sinkhorn(u, u, M, 1, stopThr=1e-10, verbose=verbose, warn=warn) # check constraints np.testing.assert_allclose(u, G.sum(1), atol=1e-05) # cf convergence sinkhorn np.testing.assert_allclose(u, G.sum(0), atol=1e-05) # cf convergence sinkhorn with pytest.warns(UserWarning): ot.sinkhorn(u, u, M, 1, stopThr=0, numItermax=1) @pytest.mark.parametrize( "method", [ "sinkhorn", "sinkhorn_stabilized", "sinkhorn_epsilon_scaling", "greenkhorn", "sinkhorn_log", ], ) def test_convergence_warning(method): # test sinkhorn n = 100 a1 = ot.datasets.make_1D_gauss(n, m=30, s=10) a2 = ot.datasets.make_1D_gauss(n, m=40, s=10) A = np.asarray([a1, a2]).T M = ot.utils.dist0(n) with pytest.warns(UserWarning): ot.sinkhorn(a1, a2, M, 1.0, method=method, stopThr=0, numItermax=1) if method in ["sinkhorn", "sinkhorn_stabilized", "sinkhorn_log"]: with pytest.warns(UserWarning): ot.barycenter(A, M, 1, method=method, stopThr=0, numItermax=1) with pytest.warns(UserWarning): ot.sinkhorn2( a1, a2, M, 1, method=method, stopThr=0, numItermax=1, warn=True ) with warnings.catch_warnings(): warnings.simplefilter("error") ot.sinkhorn2( a1, a2, M, 1, method=method, stopThr=0, numItermax=1, warn=False ) def test_not_implemented_method(): # test sinkhorn w = 10 n = w**2 rng = np.random.RandomState(42) A_img = rng.rand(2, w, w) A_flat = A_img.reshape(n, 2) a1, a2 = A_flat.T M_flat = ot.utils.dist0(n) not_implemented = "new_method" reg = 0.01 with pytest.raises(ValueError): ot.sinkhorn(a1, a2, M_flat, reg, method=not_implemented) with pytest.raises(ValueError): ot.sinkhorn2(a1, a2, M_flat, reg, method=not_implemented) with pytest.raises(ValueError): ot.barycenter(A_flat, M_flat, reg, method=not_implemented) with pytest.raises(ValueError): ot.bregman.barycenter_debiased(A_flat, M_flat, reg, method=not_implemented) with pytest.raises(ValueError): ot.bregman.convolutional_barycenter2d(A_img, reg, method=not_implemented) with pytest.raises(ValueError): ot.bregman.convolutional_barycenter2d_debiased( A_img, reg, method=not_implemented ) @pytest.mark.parametrize("method", ["sinkhorn", "sinkhorn_stabilized"]) def test_nan_warning(method): # test sinkhorn n = 100 a1 = ot.datasets.make_1D_gauss(n, m=30, s=10) a2 = ot.datasets.make_1D_gauss(n, m=40, s=10) M = ot.utils.dist0(n) reg = 0 with pytest.warns(UserWarning): # warn set to False to avoid catching a convergence warning instead ot.sinkhorn(a1, a2, M, reg, method=method, warn=False) def test_sinkhorn_stabilization(): # test sinkhorn n = 100 a1 = ot.datasets.make_1D_gauss(n, m=30, s=10) a2 = ot.datasets.make_1D_gauss(n, m=40, s=10) M = ot.utils.dist0(n) reg = 1e-5 loss1 = ot.sinkhorn2(a1, a2, M, reg, method="sinkhorn_log") loss2 = ot.sinkhorn2(a1, a2, M, reg, tau=1, method="sinkhorn_stabilized") np.testing.assert_allclose(loss1, loss2, atol=1e-06) # cf convergence sinkhorn @pytest.mark.parametrize( "method, verbose, warn", product( ["sinkhorn", "sinkhorn_stabilized", "sinkhorn_log"], [True, False], [True, False], ), ) def test_sinkhorn_multi_b(method, verbose, warn): # test sinkhorn n = 10 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) b = rng.rand(n, 3) b = b / np.sum(b, 0, keepdims=True) M = ot.dist(x, x) loss0, log = ot.sinkhorn(u, b, M, 0.1, method=method, stopThr=1e-10, log=True) loss = [ ot.sinkhorn2( u, b[:, k], M, 0.1, method=method, stopThr=1e-10, verbose=verbose, warn=warn ) for k in range(3) ] # check constraints np.testing.assert_allclose(loss0, loss, atol=1e-4) # cf convergence sinkhorn def test_sinkhorn_backends(nx): n_samples = 100 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples, n_features) y = rng.randn(n_samples, n_features) a = ot.utils.unif(n_samples) M = ot.dist(x, y) G = ot.sinkhorn(a, a, M, 1) ab, M_nx = nx.from_numpy(a, M) Gb = ot.sinkhorn(ab, ab, M_nx, 1) np.allclose(G, nx.to_numpy(Gb)) def test_sinkhorn2_backends(nx): n_samples = 100 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples, n_features) y = rng.randn(n_samples, n_features) a = ot.utils.unif(n_samples) M = ot.dist(x, y) G = ot.sinkhorn(a, a, M, 1) ab, M_nx = nx.from_numpy(a, M) Gb = ot.sinkhorn2(ab, ab, M_nx, 1) np.allclose(G, nx.to_numpy(Gb)) def test_sinkhorn2_gradients(): n_samples = 100 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples, n_features) y = rng.randn(n_samples, n_features) a = ot.utils.unif(n_samples) M = ot.dist(x, y) if torch: a1 = torch.tensor(a, requires_grad=True) b1 = torch.tensor(a, requires_grad=True) M1 = torch.tensor(M, requires_grad=True) val = ot.sinkhorn2(a1, b1, M1, 1) val.backward() assert a1.shape == a1.grad.shape assert b1.shape == b1.grad.shape assert M1.shape == M1.grad.shape def test_sinkhorn_empty(): # test sinkhorn n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) G, log = ot.sinkhorn( [], [], M, 1, stopThr=1e-10, method="sinkhorn_log", verbose=True, log=True ) # check constraints np.testing.assert_allclose(u, G.sum(1), atol=1e-05) np.testing.assert_allclose(u, G.sum(0), atol=1e-05) G, log = ot.sinkhorn([], [], M, 1, stopThr=1e-10, verbose=True, log=True) # check constraints np.testing.assert_allclose(u, G.sum(1), atol=1e-05) np.testing.assert_allclose(u, G.sum(0), atol=1e-05) G, log = ot.sinkhorn( [], [], M, 1, stopThr=1e-10, method="sinkhorn_stabilized", verbose=True, log=True, ) # check constraints np.testing.assert_allclose(u, G.sum(1), atol=1e-05) np.testing.assert_allclose(u, G.sum(0), atol=1e-05) G, log = ot.sinkhorn( [], [], M, 1, stopThr=1e-10, method="sinkhorn_epsilon_scaling", verbose=True, log=True, ) # check constraints np.testing.assert_allclose(u, G.sum(1), atol=1e-05) np.testing.assert_allclose(u, G.sum(0), atol=1e-05) # test empty weights greenkhorn ot.sinkhorn([], [], M, 1, method="greenkhorn", stopThr=1e-10, log=True) @pytest.skip_backend("tf") @pytest.skip_backend("jax") def test_sinkhorn_variants(nx): # test sinkhorn n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) ub, M_nx = nx.from_numpy(u, M) G = ot.sinkhorn(u, u, M, 1, method="sinkhorn", stopThr=1e-10) Gl = nx.to_numpy(ot.sinkhorn(ub, ub, M_nx, 1, method="sinkhorn_log", stopThr=1e-10)) G0 = nx.to_numpy(ot.sinkhorn(ub, ub, M_nx, 1, method="sinkhorn", stopThr=1e-10)) Gs = nx.to_numpy( ot.sinkhorn(ub, ub, M_nx, 1, method="sinkhorn_stabilized", stopThr=1e-10) ) Ges = nx.to_numpy( ot.sinkhorn(ub, ub, M_nx, 1, method="sinkhorn_epsilon_scaling", stopThr=1e-10) ) G_green = nx.to_numpy( ot.sinkhorn(ub, ub, M_nx, 1, method="greenkhorn", stopThr=1e-10) ) # check values np.testing.assert_allclose(G, G0, atol=1e-05) np.testing.assert_allclose(G, Gl, atol=1e-05) np.testing.assert_allclose(G0, Gs, atol=1e-05) np.testing.assert_allclose(G0, Ges, atol=1e-05) np.testing.assert_allclose(G0, G_green, atol=1e-5) @pytest.mark.parametrize( "method", [ "sinkhorn", "sinkhorn_stabilized", "sinkhorn_epsilon_scaling", "greenkhorn", "sinkhorn_log", ], ) @pytest.skip_arg( ("nx", "method"), ("tf", "sinkhorn_epsilon_scaling"), reason="tf does not support sinkhorn_epsilon_scaling", getter=str, ) @pytest.skip_arg( ("nx", "method"), ("tf", "greenkhorn"), reason="tf does not support greenkhorn", getter=str, ) @pytest.skip_arg( ("nx", "method"), ("jax", "sinkhorn_epsilon_scaling"), reason="jax does not support sinkhorn_epsilon_scaling", getter=str, ) @pytest.skip_arg( ("nx", "method"), ("jax", "greenkhorn"), reason="jax does not support greenkhorn", getter=str, ) def test_sinkhorn_variants_dtype_device(nx, method): n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) for tp in nx.__type_list__: print(nx.dtype_device(tp)) ub, Mb = nx.from_numpy(u, M, type_as=tp) Gb = ot.sinkhorn(ub, ub, Mb, 1, method=method, stopThr=1e-10) nx.assert_same_dtype_device(Mb, Gb) @pytest.mark.parametrize("method", ["sinkhorn", "sinkhorn_stabilized", "sinkhorn_log"]) def test_sinkhorn2_variants_dtype_device(nx, method): n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) for tp in nx.__type_list__: print(nx.dtype_device(tp)) ub, Mb = nx.from_numpy(u, M, type_as=tp) lossb = ot.sinkhorn2(ub, ub, Mb, 1, method=method, stopThr=1e-10) nx.assert_same_dtype_device(Mb, lossb) @pytest.mark.skipif(not tf, reason="tf not installed") @pytest.mark.parametrize("method", ["sinkhorn", "sinkhorn_stabilized", "sinkhorn_log"]) def test_sinkhorn2_variants_device_tf(method): nx = ot.backend.TensorflowBackend() n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) # Check that everything stays on the CPU with tf.device("/CPU:0"): ub, Mb = nx.from_numpy(u, M) Gb = ot.sinkhorn(ub, ub, Mb, 1, method=method, stopThr=1e-10) lossb = ot.sinkhorn2(ub, ub, Mb, 1, method=method, stopThr=1e-10) nx.assert_same_dtype_device(Mb, Gb) nx.assert_same_dtype_device(Mb, lossb) # Check that everything happens on the GPU ub, Mb = nx.from_numpy(u, M) Gb = ot.sinkhorn(ub, ub, Mb, 1, method=method, stopThr=1e-10) lossb = ot.sinkhorn2(ub, ub, Mb, 1, method=method, stopThr=1e-10) nx.assert_same_dtype_device(Mb, Gb) nx.assert_same_dtype_device(Mb, lossb) # Check this only if GPU is available if len(tf.config.list_physical_devices("GPU")) > 0: assert nx.dtype_device(Gb)[1].startswith("GPU") @pytest.skip_backend("tf") @pytest.skip_backend("jax") def test_sinkhorn_variants_multi_b(nx): # test sinkhorn n = 50 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) b = rng.rand(n, 3) b = b / np.sum(b, 0, keepdims=True) M = ot.dist(x, x) ub, bb, M_nx = nx.from_numpy(u, b, M) G = ot.sinkhorn(u, b, M, 1, method="sinkhorn", stopThr=1e-10) Gl = nx.to_numpy(ot.sinkhorn(ub, bb, M_nx, 1, method="sinkhorn_log", stopThr=1e-10)) G0 = nx.to_numpy(ot.sinkhorn(ub, bb, M_nx, 1, method="sinkhorn", stopThr=1e-10)) Gs = nx.to_numpy( ot.sinkhorn(ub, bb, M_nx, 1, method="sinkhorn_stabilized", stopThr=1e-10) ) # check values np.testing.assert_allclose(G, G0, atol=1e-05) np.testing.assert_allclose(G, Gl, atol=1e-05) np.testing.assert_allclose(G0, Gs, atol=1e-05) @pytest.skip_backend("tf") @pytest.skip_backend("jax") def test_sinkhorn2_variants_multi_b(nx): # test sinkhorn n = 50 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) b = rng.rand(n, 3) b = b / np.sum(b, 0, keepdims=True) M = ot.dist(x, x) ub, bb, M_nx = nx.from_numpy(u, b, M) G = ot.sinkhorn2(u, b, M, 1, method="sinkhorn", stopThr=1e-10) Gl = nx.to_numpy( ot.sinkhorn2(ub, bb, M_nx, 1, method="sinkhorn_log", stopThr=1e-10) ) G0 = nx.to_numpy(ot.sinkhorn2(ub, bb, M_nx, 1, method="sinkhorn", stopThr=1e-10)) Gs = nx.to_numpy( ot.sinkhorn2(ub, bb, M_nx, 1, method="sinkhorn_stabilized", stopThr=1e-10) ) # check values np.testing.assert_allclose(G, G0, atol=1e-05) np.testing.assert_allclose(G, Gl, atol=1e-05) np.testing.assert_allclose(G0, Gs, atol=1e-05) def test_sinkhorn_variants_log(): # test sinkhorn n = 50 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) G0, log0 = ot.sinkhorn(u, u, M, 1, method="sinkhorn", stopThr=1e-10, log=True) Gl, logl = ot.sinkhorn(u, u, M, 1, method="sinkhorn_log", stopThr=1e-10, log=True) Gs, logs = ot.sinkhorn( u, u, M, 1, method="sinkhorn_stabilized", stopThr=1e-10, log=True ) Ges, loges = ot.sinkhorn( u, u, M, 1, method="sinkhorn_epsilon_scaling", stopThr=1e-10, log=True, ) G_green, loggreen = ot.sinkhorn( u, u, M, 1, method="greenkhorn", stopThr=1e-10, log=True ) # check values np.testing.assert_allclose(G0, Gs, atol=1e-05) np.testing.assert_allclose(G0, Gl, atol=1e-05) np.testing.assert_allclose(G0, Ges, atol=1e-05) np.testing.assert_allclose(G0, G_green, atol=1e-5) @pytest.mark.parametrize("verbose, warn", product([True, False], [True, False])) def test_sinkhorn_variants_log_multib(verbose, warn): # test sinkhorn n = 50 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) b = rng.rand(n, 3) b = b / np.sum(b, 0, keepdims=True) M = ot.dist(x, x) G0, log0 = ot.sinkhorn(u, b, M, 1, method="sinkhorn", stopThr=1e-10, log=True) Gl, logl = ot.sinkhorn( u, b, M, 1, method="sinkhorn_log", stopThr=1e-10, log=True, verbose=verbose, warn=warn, ) Gs, logs = ot.sinkhorn( u, b, M, 1, method="sinkhorn_stabilized", stopThr=1e-10, log=True, verbose=verbose, warn=warn, ) # check values np.testing.assert_allclose(G0, Gs, atol=1e-05) np.testing.assert_allclose(G0, Gl, atol=1e-05) @pytest.mark.parametrize( "method, verbose, warn", product( ["sinkhorn", "sinkhorn_stabilized", "sinkhorn_log"], [True, False], [True, False], ), ) def test_barycenter(nx, method, verbose, warn): n_bins = 100 # nb bins # Gaussian distributions a1 = ot.datasets.make_1D_gauss(n_bins, m=30, s=10) # m= mean, s= std a2 = ot.datasets.make_1D_gauss(n_bins, m=40, s=10) # creating matrix A containing all distributions A = np.vstack((a1, a2)).T # loss matrix + normalization M = ot.utils.dist0(n_bins) M /= M.max() alpha = 0.5 # 0<=alpha<=1 weights = np.array([1 - alpha, alpha]) A_nx, M_nx, weights_nx = nx.from_numpy(A, M, weights) reg = 1e-2 if nx.__name__ in ("jax", "tf") and method == "sinkhorn_log": with pytest.raises(NotImplementedError): ot.bregman.barycenter(A_nx, M_nx, reg, weights, method=method) else: # wasserstein bary_wass_np = ot.bregman.barycenter( A, M, reg, weights, method=method, verbose=verbose, warn=warn ) bary_wass, _ = ot.bregman.barycenter( A_nx, M_nx, reg, weights_nx, method=method, log=True ) bary_wass = nx.to_numpy(bary_wass) np.testing.assert_allclose(1, np.sum(bary_wass)) np.testing.assert_allclose(bary_wass, bary_wass_np) ot.bregman.barycenter(A_nx, M_nx, reg, log=True) def test_free_support_sinkhorn_barycenter(): measures_locations = [ np.array([-1.0]).reshape((1, 1)), # First dirac support np.array([1.0]).reshape((1, 1)), # Second dirac support ] measures_weights = [ np.array([1.0]), # First dirac sample weights np.array([1.0]), # Second dirac sample weights ] # Barycenter initialization X_init = np.array([-12.0]).reshape((1, 1)) # Obvious barycenter locations. Take a look on test_ot.py, test_free_support_barycenter bar_locations = np.array([0.0]).reshape((1, 1)) # Calculate free support barycenter w/ Sinkhorn algorithm. We set the entropic regularization # term to 1, but this should be, in general, fine-tuned to the problem. X = ot.bregman.free_support_sinkhorn_barycenter( measures_locations, measures_weights, X_init, reg=1 ) # Verifies if calculated barycenter matches ground-truth np.testing.assert_allclose(X, bar_locations, rtol=1e-5, atol=1e-7) @pytest.mark.parametrize( "method, verbose, warn", product( ["sinkhorn", "sinkhorn_stabilized", "sinkhorn_log"], [True, False], [True, False], ), ) def test_barycenter_assymetric_cost(nx, method, verbose, warn): n_bins = 20 # nb bins # Gaussian distributions A = ot.datasets.make_1D_gauss(n_bins, m=30, s=10) # m= mean, s= std # creating matrix A containing all distributions A = A[:, None] # assymetric loss matrix + normalization rng = np.random.RandomState(42) M = rng.randn(n_bins, n_bins) ** 2 M /= M.max() A_nx, M_nx = nx.from_numpy(A, M) reg = 1e-2 if nx.__name__ in ("jax", "tf") and method == "sinkhorn_log": with pytest.raises(NotImplementedError): ot.bregman.barycenter(A_nx, M_nx, reg, method=method) else: # wasserstein bary_wass_np = ot.bregman.barycenter( A, M, reg, method=method, verbose=verbose, warn=warn ) bary_wass, _ = ot.bregman.barycenter(A_nx, M_nx, reg, method=method, log=True) bary_wass = nx.to_numpy(bary_wass) np.testing.assert_allclose(1, np.sum(bary_wass)) np.testing.assert_allclose(bary_wass, bary_wass_np) ot.bregman.barycenter(A_nx, M_nx, reg, log=True) @pytest.mark.parametrize( "method, verbose, warn", product(["sinkhorn", "sinkhorn_log"], [True, False], [True, False]), ) def test_barycenter_debiased(nx, method, verbose, warn): n_bins = 100 # nb bins # Gaussian distributions a1 = ot.datasets.make_1D_gauss(n_bins, m=30, s=10) # m= mean, s= std a2 = ot.datasets.make_1D_gauss(n_bins, m=40, s=10) # creating matrix A containing all distributions A = np.vstack((a1, a2)).T # loss matrix + normalization M = ot.utils.dist0(n_bins) M /= M.max() alpha = 0.5 # 0<=alpha<=1 weights = np.array([1 - alpha, alpha]) A_nx, M_nx, weights_nx = nx.from_numpy(A, M, weights) # wasserstein reg = 1e-2 if nx.__name__ in ("jax", "tf") and method == "sinkhorn_log": with pytest.raises(NotImplementedError): ot.bregman.barycenter_debiased(A_nx, M_nx, reg, weights, method=method) else: bary_wass_np = ot.bregman.barycenter_debiased( A, M, reg, weights, method=method, verbose=verbose, warn=warn ) bary_wass, _ = ot.bregman.barycenter_debiased( A_nx, M_nx, reg, weights_nx, method=method, log=True ) bary_wass = nx.to_numpy(bary_wass) np.testing.assert_allclose(1, np.sum(bary_wass), atol=1e-3) np.testing.assert_allclose(bary_wass, bary_wass_np, atol=1e-5) ot.bregman.barycenter_debiased(A_nx, M_nx, reg, log=True, verbose=False) @pytest.mark.parametrize("method", ["sinkhorn", "sinkhorn_log"]) def test_convergence_warning_barycenters(method): w = 10 n_bins = w**2 # nb bins # Gaussian distributions a1 = ot.datasets.make_1D_gauss(n_bins, m=30, s=10) # m= mean, s= std a2 = ot.datasets.make_1D_gauss(n_bins, m=40, s=10) # creating matrix A containing all distributions A = np.vstack((a1, a2)).T A_img = A.reshape(2, w, w) A_img /= A_img.sum((1, 2))[:, None, None] # loss matrix + normalization M = ot.utils.dist0(n_bins) M /= M.max() alpha = 0.5 # 0<=alpha<=1 weights = np.array([1 - alpha, alpha]) reg = 0.1 with pytest.warns(UserWarning): ot.bregman.barycenter_debiased(A, M, reg, weights, method=method, numItermax=1) with pytest.warns(UserWarning): ot.bregman.barycenter(A, M, reg, weights, method=method, numItermax=1) with pytest.warns(UserWarning): ot.bregman.convolutional_barycenter2d( A_img, reg, weights, method=method, numItermax=1 ) with pytest.warns(UserWarning): ot.bregman.convolutional_barycenter2d_debiased( A_img, reg, weights, method=method, numItermax=1 ) def test_barycenter_stabilization(nx): n_bins = 100 # nb bins # Gaussian distributions a1 = ot.datasets.make_1D_gauss(n_bins, m=30, s=10) # m= mean, s= std a2 = ot.datasets.make_1D_gauss(n_bins, m=40, s=10) # creating matrix A containing all distributions A = np.vstack((a1, a2)).T # loss matrix + normalization M = ot.utils.dist0(n_bins) M /= M.max() alpha = 0.5 # 0<=alpha<=1 weights = np.array([1 - alpha, alpha]) A_nx, M_nx, weights_b = nx.from_numpy(A, M, weights) # wasserstein reg = 1e-2 bar_np = ot.bregman.barycenter( A, M, reg, weights, method="sinkhorn", stopThr=1e-8, verbose=True ) bar_stable = nx.to_numpy( ot.bregman.barycenter( A_nx, M_nx, reg, weights_b, method="sinkhorn_stabilized", stopThr=1e-8, verbose=True, ) ) bar = nx.to_numpy( ot.bregman.barycenter( A_nx, M_nx, reg, weights_b, method="sinkhorn", stopThr=1e-8, verbose=True ) ) np.testing.assert_allclose(bar, bar_stable) np.testing.assert_allclose(bar, bar_np) def create_random_images_dist(seed, size=20): """Creates an array of two random images of size (size, size). Returns an array of shape (2, size, size).""" rng = np.random.RandomState(seed) # First image a1 = rng.rand(size, size) a1 += a1.min() a1 = a1 / np.sum(a1) # Ensure that it is a probability distribution # Second image a2 = rng.rand(size, size) a2 += a2.min() a2 = a2 / np.sum(a2) # Creating matrix A containing all distributions A = np.zeros((2, size, size)) A[0, :, :] = a1 A[1, :, :] = a2 return A @pytest.mark.parametrize("method", ["sinkhorn", "sinkhorn_log"]) def test_wasserstein_bary_2d(nx, method): # Create the array of images to test A = create_random_images_dist(42, size=20) A_nx = nx.from_numpy(A) # wasserstein reg = 1e-2 if nx.__name__ in ("jax", "tf") and method == "sinkhorn_log": with pytest.raises(NotImplementedError): ot.bregman.convolutional_barycenter2d(A_nx, reg, method=method) else: bary_wass_np, log_np = ot.bregman.convolutional_barycenter2d( A, reg, method=method, verbose=True, log=True ) bary_wass = nx.to_numpy( ot.bregman.convolutional_barycenter2d(A_nx, reg, method=method) ) np.testing.assert_allclose(1, np.sum(bary_wass), rtol=1e-3) np.testing.assert_allclose(bary_wass, bary_wass_np, atol=1e-3) # help in checking if log and verbose do not bug the function ot.bregman.convolutional_barycenter2d(A, reg, log=True, verbose=True) @pytest.skip_backend("tf") @pytest.mark.parametrize("method", ["sinkhorn", "sinkhorn_log"]) def test_wasserstein_bary_2d_dtype_device(nx, method): # Create the array of images to test A = create_random_images_dist(42, size=20) for tp in nx.__type_list__: print(nx.dtype_device(tp)) Ab = nx.from_numpy(A, type_as=tp) # wasserstein reg = 1e-2 if nx.__name__ in ("jax", "tf") and method == "sinkhorn_log": with pytest.raises(NotImplementedError): ot.bregman.convolutional_barycenter2d(Ab, reg, method=method) else: # Compute the barycenter with numpy bary_wass_np, log_np = ot.bregman.convolutional_barycenter2d( A, reg, method=method, verbose=True, log=True ) # Compute the barycenter with the backend bary_wass_b = ot.bregman.convolutional_barycenter2d(Ab, reg, method=method) # Convert the backend result to numpy, to compare with the numpy result bary_wass = nx.to_numpy(bary_wass_b) np.testing.assert_allclose(1, np.sum(bary_wass), rtol=1e-3) np.testing.assert_allclose(bary_wass, bary_wass_np, atol=1e-3) # help in checking if log and verbose do not bug the function ot.bregman.convolutional_barycenter2d(A, reg, log=True, verbose=True) # Test that the dtype and device are the same after the computation nx.assert_same_dtype_device(Ab, bary_wass_b) @pytest.mark.skipif(not tf, reason="tf not installed") @pytest.mark.parametrize("method", ["sinkhorn", "sinkhorn_log"]) def test_wasserstein_bary_2d_device_tf(method): # Using the Tensorflow backend nx = ot.backend.TensorflowBackend() # Create the array of images to test A = create_random_images_dist(42, size=20) # Check that everything stays on the CPU with tf.device("/CPU:0"): Ab = nx.from_numpy(A) # wasserstein reg = 1e-2 if method == "sinkhorn_log": with pytest.raises(NotImplementedError): ot.bregman.convolutional_barycenter2d(Ab, reg, method=method) else: # Compute the barycenter with numpy bary_wass_np, log_np = ot.bregman.convolutional_barycenter2d( A, reg, method=method, verbose=True, log=True ) # Compute the barycenter with the backend bary_wass_b = ot.bregman.convolutional_barycenter2d(Ab, reg, method=method) # Convert the backend result to numpy, to compare with the numpy result bary_wass = nx.to_numpy(bary_wass_b) np.testing.assert_allclose(1, np.sum(bary_wass), rtol=1e-3) np.testing.assert_allclose(bary_wass, bary_wass_np, atol=1e-3) # help in checking if log and verbose do not bug the function ot.bregman.convolutional_barycenter2d(A, reg, log=True, verbose=True) # Test that the dtype and device are the same after the computation nx.assert_same_dtype_device(Ab, bary_wass_b) # Check that everything happens on the GPU Ab = nx.from_numpy(A) # wasserstein reg = 1e-2 if method == "sinkhorn_log": with pytest.raises(NotImplementedError): ot.bregman.convolutional_barycenter2d(Ab, reg, method=method) else: # Compute the barycenter with numpy bary_wass_np, log_np = ot.bregman.convolutional_barycenter2d( A, reg, method=method, verbose=True, log=True ) # Compute the barycenter with the backend bary_wass_b = ot.bregman.convolutional_barycenter2d(Ab, reg, method=method) # Convert the backend result to numpy, to compare with the numpy result bary_wass = nx.to_numpy(bary_wass_b) np.testing.assert_allclose(1, np.sum(bary_wass), rtol=1e-3) np.testing.assert_allclose(bary_wass, bary_wass_np, atol=1e-3) # help in checking if log and verbose do not bug the function ot.bregman.convolutional_barycenter2d(A, reg, log=True, verbose=True) # Test that the dtype and device are the same after the computation nx.assert_same_dtype_device(Ab, bary_wass_b) # Check this only if GPU is available if len(tf.config.list_physical_devices("GPU")) > 0: assert nx.dtype_device(bary_wass_b)[1].startswith("GPU") @pytest.mark.parametrize("method", ["sinkhorn", "sinkhorn_log"]) def test_wasserstein_bary_2d_debiased(nx, method): # Create the array of images to test A = create_random_images_dist(42, size=20) A_nx = nx.from_numpy(A) # wasserstein reg = 1e-2 if nx.__name__ in ("jax", "tf") and method == "sinkhorn_log": with pytest.raises(NotImplementedError): ot.bregman.convolutional_barycenter2d_debiased(A_nx, reg, method=method) else: bary_wass_np, log_np = ot.bregman.convolutional_barycenter2d_debiased( A, reg, method=method, verbose=True, log=True ) bary_wass = nx.to_numpy( ot.bregman.convolutional_barycenter2d_debiased(A_nx, reg, method=method) ) np.testing.assert_allclose(1, np.sum(bary_wass), rtol=1e-3) np.testing.assert_allclose(bary_wass, bary_wass_np, atol=1e-3) # help in checking if log and verbose do not bug the function ot.bregman.convolutional_barycenter2d_debiased(A, reg, log=True, verbose=True) @pytest.skip_backend("tf") @pytest.mark.parametrize("method", ["sinkhorn", "sinkhorn_log"]) def test_wasserstein_bary_2d_debiased_dtype_device(nx, method): # Create the array of images to test A = create_random_images_dist(42, size=20) for tp in nx.__type_list__: print(nx.dtype_device(tp)) Ab = nx.from_numpy(A, type_as=tp) # wasserstein reg = 1e-2 if nx.__name__ in ("jax", "tf") and method == "sinkhorn_log": with pytest.raises(NotImplementedError): ot.bregman.convolutional_barycenter2d_debiased(Ab, reg, method=method) else: # Compute the barycenter with numpy bary_wass_np, log_np = ot.bregman.convolutional_barycenter2d_debiased( A, reg, method=method, verbose=True, log=True ) # Compute the barycenter with the backend bary_wass_b = ot.bregman.convolutional_barycenter2d_debiased( Ab, reg, method=method ) # Convert the backend result to numpy, to compare with the numpy result bary_wass = nx.to_numpy(bary_wass_b) np.testing.assert_allclose(1, np.sum(bary_wass), rtol=1e-3) np.testing.assert_allclose(bary_wass, bary_wass_np, atol=1e-3) # help in checking if log and verbose do not bug the function ot.bregman.convolutional_barycenter2d_debiased( A, reg, log=True, verbose=True ) # Test that the dtype and device are the same after the computation nx.assert_same_dtype_device(Ab, bary_wass_b) @pytest.mark.skipif(not tf, reason="tf not installed") @pytest.mark.parametrize("method", ["sinkhorn", "sinkhorn_log"]) def test_wasserstein_bary_2d_debiased_device_tf(method): # Using the Tensorflow backend nx = ot.backend.TensorflowBackend() # Create the array of images to test A = create_random_images_dist(42, size=20) # Check that everything stays on the CPU with tf.device("/CPU:0"): Ab = nx.from_numpy(A) # wasserstein reg = 1e-2 if method == "sinkhorn_log": with pytest.raises(NotImplementedError): ot.bregman.convolutional_barycenter2d_debiased(Ab, reg, method=method) else: # Compute the barycenter with numpy bary_wass_np, log_np = ot.bregman.convolutional_barycenter2d_debiased( A, reg, method=method, verbose=True, log=True ) # Compute the barycenter with the backend bary_wass_b = ot.bregman.convolutional_barycenter2d_debiased( Ab, reg, method=method ) # Convert the backend result to numpy, to compare with the numpy result bary_wass = nx.to_numpy(bary_wass_b) np.testing.assert_allclose(1, np.sum(bary_wass), rtol=1e-3) np.testing.assert_allclose(bary_wass, bary_wass_np, atol=1e-3) # help in checking if log and verbose do not bug the function ot.bregman.convolutional_barycenter2d_debiased( A, reg, log=True, verbose=True ) # Test that the dtype and device are the same after the computation nx.assert_same_dtype_device(Ab, bary_wass_b) # Check that everything happens on the GPU Ab = nx.from_numpy(A) # wasserstein reg = 1e-2 if method == "sinkhorn_log": with pytest.raises(NotImplementedError): ot.bregman.convolutional_barycenter2d_debiased(Ab, reg, method=method) else: # Compute the barycenter with numpy bary_wass_np, log_np = ot.bregman.convolutional_barycenter2d_debiased( A, reg, method=method, verbose=True, log=True ) # Compute the barycenter with the backend bary_wass_b = ot.bregman.convolutional_barycenter2d_debiased( Ab, reg, method=method ) # Convert the backend result to numpy, to compare with the numpy result bary_wass = nx.to_numpy(bary_wass_b) np.testing.assert_allclose(1, np.sum(bary_wass), rtol=1e-3) def test_unmix(nx): n_bins = 50 # nb bins # Gaussian distributions a1 = ot.datasets.make_1D_gauss(n_bins, m=20, s=10) # m= mean, s= std a2 = ot.datasets.make_1D_gauss(n_bins, m=40, s=10) a = ot.datasets.make_1D_gauss(n_bins, m=30, s=10) # creating matrix A containing all distributions D = np.vstack((a1, a2)).T # loss matrix + normalization M = ot.utils.dist0(n_bins) M /= M.max() M0 = ot.utils.dist0(2) M0 /= M0.max() h0 = ot.unif(2) ab, Db, M_nx, M0b, h0b = nx.from_numpy(a, D, M, M0, h0) # wasserstein reg = 1e-3 um_np = ot.bregman.unmix(a, D, M, M0, h0, reg, 1, alpha=0.01) um = nx.to_numpy(ot.bregman.unmix(ab, Db, M_nx, M0b, h0b, reg, 1, alpha=0.01)) np.testing.assert_allclose(1, np.sum(um), rtol=1e-03, atol=1e-03) np.testing.assert_allclose([0.5, 0.5], um, rtol=1e-03, atol=1e-03) np.testing.assert_allclose(um, um_np) ot.bregman.unmix(ab, Db, M_nx, M0b, h0b, reg, 1, alpha=0.01, log=True, verbose=True) def test_empirical_sinkhorn(nx): # test sinkhorn n = 10 a = ot.unif(n) b = ot.unif(n) X_s = np.reshape(1.0 * np.arange(n), (n, 1)) X_t = np.reshape(1.0 * np.arange(0, n), (n, 1)) M = ot.dist(X_s, X_t) M_m = ot.dist(X_s, X_t, metric="euclidean") ab, bb, X_sb, X_tb, M_nx, M_mb = nx.from_numpy(a, b, X_s, X_t, M, M_m) G_sqe = nx.to_numpy(ot.bregman.empirical_sinkhorn(X_sb, X_tb, 1)) sinkhorn_sqe = nx.to_numpy(ot.sinkhorn(ab, bb, M_nx, 1)) G_log, log_es = ot.bregman.empirical_sinkhorn(X_sb, X_tb, 0.1, log=True) G_log = nx.to_numpy(G_log) sinkhorn_log, log_s = ot.sinkhorn(ab, bb, M_nx, 0.1, log=True) sinkhorn_log = nx.to_numpy(sinkhorn_log) G_m = nx.to_numpy(ot.bregman.empirical_sinkhorn(X_sb, X_tb, 1, metric="euclidean")) sinkhorn_m = nx.to_numpy(ot.sinkhorn(ab, bb, M_mb, 1)) loss_emp_sinkhorn = nx.to_numpy(ot.bregman.empirical_sinkhorn2(X_sb, X_tb, 1)) loss_sinkhorn = nx.to_numpy(ot.sinkhorn2(ab, bb, M_nx, 1)) # check constraints np.testing.assert_allclose( sinkhorn_sqe.sum(1), G_sqe.sum(1), atol=1e-05 ) # metric sqeuclidian np.testing.assert_allclose( sinkhorn_sqe.sum(0), G_sqe.sum(0), atol=1e-05 ) # metric sqeuclidian np.testing.assert_allclose(sinkhorn_log.sum(1), G_log.sum(1), atol=1e-05) # log np.testing.assert_allclose(sinkhorn_log.sum(0), G_log.sum(0), atol=1e-05) # log np.testing.assert_allclose( sinkhorn_m.sum(1), G_m.sum(1), atol=1e-05 ) # metric euclidian np.testing.assert_allclose( sinkhorn_m.sum(0), G_m.sum(0), atol=1e-05 ) # metric euclidian np.testing.assert_allclose(loss_emp_sinkhorn, loss_sinkhorn, atol=1e-05) @pytest.mark.skipif(not geomloss, reason="pytorch not installed") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") @pytest.skip_backend("jax") @pytest.mark.parametrize("metric", ["sqeuclidean", "euclidean"]) def test_geomloss_solver(nx, metric): # test sinkhorn n = 10 a = ot.unif(n) b = ot.unif(n) X_s = np.reshape(1.0 * np.arange(n), (n, 1)) X_t = np.reshape(1.0 * np.arange(0, n), (n, 1)) ab, bb, X_sb, X_tb = nx.from_numpy(a, b, X_s, X_t) G_sqe = nx.to_numpy(ot.bregman.empirical_sinkhorn(X_sb, X_tb, 1, metric=metric)) value, log = ot.bregman.empirical_sinkhorn2_geomloss( X_sb, X_tb, 1, metric=metric, log=True ) G_geomloss = nx.to_numpy(log["lazy_plan"][:]) print(value) # call with log = False ot.bregman.empirical_sinkhorn2_geomloss(X_sb, X_tb, 1, metric=metric) # check equality of plans np.testing.assert_allclose(G_sqe, G_geomloss, atol=1e-03) # metric sqeuclidian # check error on wrong metric with pytest.raises(ValueError): ot.bregman.empirical_sinkhorn2_geomloss(X_sb, X_tb, 1, metric="wrong_metric") def test_lazy_empirical_sinkhorn(nx): # test sinkhorn n = 10 a = ot.unif(n) b = ot.unif(n) numIterMax = 1000 X_s = np.reshape(np.arange(n, dtype=np.float64), (n, 1)) X_t = np.reshape(np.arange(0, n, dtype=np.float64), (n, 1)) M = ot.dist(X_s, X_t) M_m = ot.dist(X_s, X_t, metric="euclidean") ab, bb, X_sb, X_tb, M_nx, M_mb = nx.from_numpy(a, b, X_s, X_t, M, M_m) f, g = ot.bregman.empirical_sinkhorn( X_sb, X_tb, 1, numIterMax=numIterMax, isLazy=True, batchSize=(1, 3), verbose=True, ) f, g = nx.to_numpy(f), nx.to_numpy(g) G_sqe = np.exp(f[:, None] + g[None, :] - M / 1) sinkhorn_sqe = nx.to_numpy(ot.sinkhorn(ab, bb, M_nx, 1)) f, g, log_es = ot.bregman.empirical_sinkhorn( X_sb, X_tb, 1, numIterMax=numIterMax, isLazy=True, batchSize=5, log=True ) f, g = nx.to_numpy(f), nx.to_numpy(g) G_log = np.exp(f[:, None] + g[None, :] - M / 1) sinkhorn_log, log_s = ot.sinkhorn(ab, bb, M_nx, 1, log=True) sinkhorn_log = nx.to_numpy(sinkhorn_log) f, g = ot.bregman.empirical_sinkhorn( X_sb, X_tb, 1, metric="euclidean", numIterMax=numIterMax, isLazy=True, batchSize=1, ) f, g = nx.to_numpy(f), nx.to_numpy(g) G_m = np.exp(f[:, None] + g[None, :] - M_m / 1) sinkhorn_m = nx.to_numpy(ot.sinkhorn(ab, bb, M_mb, 1)) loss_emp_sinkhorn, log = ot.bregman.empirical_sinkhorn2( X_sb, X_tb, 1, numIterMax=numIterMax, isLazy=True, batchSize=5, log=True ) G_lazy = nx.to_numpy(log["lazy_plan"][:]) loss_emp_sinkhorn = nx.to_numpy(loss_emp_sinkhorn) loss_sinkhorn = nx.to_numpy(ot.sinkhorn2(ab, bb, M_nx, 1)) loss_emp_sinkhorn = ot.bregman.empirical_sinkhorn2( X_sb, X_tb, 1, numIterMax=numIterMax, isLazy=True, batchSize=1, log=False ) # test lazy plan np.testing.assert_allclose( G_sqe[1, 1], G_lazy[1, 1], atol=1e-03 ) # metric sqeuclidian # check constraints np.testing.assert_allclose( sinkhorn_sqe.sum(1), G_sqe.sum(1), atol=1e-05 ) # metric sqeuclidian np.testing.assert_allclose( sinkhorn_sqe.sum(0), G_sqe.sum(0), atol=1e-05 ) # metric sqeuclidian np.testing.assert_allclose(sinkhorn_log.sum(1), G_log.sum(1), atol=1e-05) # log np.testing.assert_allclose(sinkhorn_log.sum(0), G_log.sum(0), atol=1e-05) # log np.testing.assert_allclose( sinkhorn_m.sum(1), G_m.sum(1), atol=1e-05 ) # metric euclidian np.testing.assert_allclose( sinkhorn_m.sum(0), G_m.sum(0), atol=1e-05 ) # metric euclidian np.testing.assert_allclose(loss_emp_sinkhorn, loss_sinkhorn, atol=1e-05) np.testing.assert_allclose(G_log, G_lazy, atol=1e-05) def test_empirical_sinkhorn_divergence(nx): # Test sinkhorn divergence n = 10 a = np.linspace(1, n, n) a /= a.sum() b = ot.unif(n) X_s = np.reshape(np.arange(n, dtype=np.float64), (n, 1)) X_t = np.reshape(np.arange(0, n * 2, 2, dtype=np.float64), (n, 1)) M = ot.dist(X_s, X_t) M_s = ot.dist(X_s, X_s) M_t = ot.dist(X_t, X_t) ab, bb, X_sb, X_tb, M_nx, M_sb, M_tb = nx.from_numpy(a, b, X_s, X_t, M, M_s, M_t) emp_sinkhorn_div = nx.to_numpy( ot.bregman.empirical_sinkhorn_divergence(X_sb, X_tb, 1, a=ab, b=bb) ) sinkhorn_div = nx.to_numpy( ot.sinkhorn2(ab, bb, M_nx, 1) - 1 / 2 * ot.sinkhorn2(ab, ab, M_sb, 1) - 1 / 2 * ot.sinkhorn2(bb, bb, M_tb, 1) ) emp_sinkhorn_div_np = ot.bregman.empirical_sinkhorn_divergence( X_s, X_t, 1, a=a, b=b ) # check constraints np.testing.assert_allclose(emp_sinkhorn_div, emp_sinkhorn_div_np, atol=1e-05) np.testing.assert_allclose( emp_sinkhorn_div, sinkhorn_div, atol=1e-05 ) # cf conv emp sinkhorn ot.bregman.empirical_sinkhorn_divergence(X_sb, X_tb, 1, a=ab, b=bb, log=True) @pytest.mark.skipif(not torch, reason="No torch available") def test_empirical_sinkhorn_divergence_gradient(): # Test sinkhorn divergence n = 10 a = np.linspace(1, n, n) a /= a.sum() b = ot.unif(n) X_s = np.reshape(np.arange(n, dtype=np.float64), (n, 1)) X_t = np.reshape(np.arange(0, n * 2, 2, dtype=np.float64), (n, 1)) nx = ot.backend.TorchBackend() ab, bb, X_sb, X_tb = nx.from_numpy(a, b, X_s, X_t) ab.requires_grad = True bb.requires_grad = True X_sb.requires_grad = True X_tb.requires_grad = True emp_sinkhorn_div = ot.bregman.empirical_sinkhorn_divergence( X_sb, X_tb, 1, a=ab, b=bb ) emp_sinkhorn_div.backward() assert ab.grad is not None assert bb.grad is not None assert X_sb.grad is not None assert X_tb.grad is not None def test_stabilized_vs_sinkhorn_multidim(nx): # test if stable version matches sinkhorn # for multidimensional inputs n = 100 # Gaussian distributions a = ot.datasets.make_1D_gauss(n, m=20, s=5) # m= mean, s= std b1 = ot.datasets.make_1D_gauss(n, m=60, s=8) b2 = ot.datasets.make_1D_gauss(n, m=30, s=4) # creating matrix A containing all distributions b = np.vstack((b1, b2)).T M = ot.utils.dist0(n) M /= np.median(M) epsilon = 0.1 ab, bb, M_nx = nx.from_numpy(a, b, M) G_np, _ = ot.bregman.sinkhorn(a, b, M, reg=epsilon, method="sinkhorn", log=True) G, log = ot.bregman.sinkhorn( ab, bb, M_nx, reg=epsilon, method="sinkhorn_stabilized", log=True ) G = nx.to_numpy(G) G2, log2 = ot.bregman.sinkhorn(ab, bb, M_nx, epsilon, method="sinkhorn", log=True) G2 = nx.to_numpy(G2) np.testing.assert_allclose(G_np, G2) np.testing.assert_allclose(G, G2) def test_implemented_methods(): IMPLEMENTED_METHODS = ["sinkhorn", "sinkhorn_stabilized"] ONLY_1D_methods = ["greenkhorn", "sinkhorn_epsilon_scaling"] NOT_VALID_TOKENS = ["foo"] # test generalized sinkhorn for unbalanced OT barycenter n = 3 rng = np.random.RandomState(42) x = rng.randn(n, 2) a = ot.utils.unif(n) # make dists unbalanced b = ot.utils.unif(n) A = rng.rand(n, 2) A /= A.sum(0, keepdims=True) M = ot.dist(x, x) epsilon = 1.0 for method in IMPLEMENTED_METHODS: ot.bregman.sinkhorn(a, b, M, epsilon, method=method) ot.bregman.sinkhorn2(a, b, M, epsilon, method=method) ot.bregman.barycenter(A, M, reg=epsilon, method=method) with pytest.raises(ValueError): for method in set(NOT_VALID_TOKENS): ot.bregman.sinkhorn(a, b, M, epsilon, method=method) ot.bregman.sinkhorn2(a, b, M, epsilon, method=method) ot.bregman.barycenter(A, M, reg=epsilon, method=method) for method in ONLY_1D_methods: ot.bregman.sinkhorn(a, b, M, epsilon, method=method) with pytest.raises(ValueError): ot.bregman.sinkhorn2(a, b, M, epsilon, method=method) @pytest.skip_backend("tf") @pytest.skip_backend("cupy") @pytest.skip_backend("jax") @pytest.mark.filterwarnings("ignore:Bottleneck") def test_screenkhorn(nx): # test screenkhorn rng = np.random.RandomState(0) n = 100 a = ot.unif(n) b = ot.unif(n) x = rng.randn(n, 2) M = ot.dist(x, x) ab, bb, M_nx = nx.from_numpy(a, b, M) # sinkhorn G_sink = nx.to_numpy(ot.sinkhorn(ab, bb, M_nx, 1e-1)) # screenkhorn G_screen = nx.to_numpy( ot.bregman.screenkhorn(ab, bb, M_nx, 1e-1, uniform=True, verbose=True) ) # check marginals np.testing.assert_allclose(G_sink.sum(0), G_screen.sum(0), atol=1e-02) np.testing.assert_allclose(G_sink.sum(1), G_screen.sum(1), atol=1e-02) def test_convolutional_barycenter_non_square(nx): # test for image with height not equal width A = np.ones((2, 2, 3)) / (2 * 3) A_nx = nx.from_numpy(A) b_np = ot.bregman.convolutional_barycenter2d(A, 1e-03) b = nx.to_numpy(ot.bregman.convolutional_barycenter2d(A_nx, 1e-03)) np.testing.assert_allclose(np.ones((2, 3)) / (2 * 3), b, atol=1e-02) np.testing.assert_allclose(np.ones((2, 3)) / (2 * 3), b, atol=1e-02) np.testing.assert_allclose(b, b_np) def test_sinkhorn_warmstart(): m, n = 10, 20 a = ot.unif(m) b = ot.unif(n) Xs = np.arange(m) * 1.0 Xt = np.arange(n) * 1.0 M = ot.dist(Xs.reshape(-1, 1), Xt.reshape(-1, 1)) # Generate warmstart from dual vectors of unregularized OT _, log = ot.lp.emd(a, b, M, log=True) warmstart = (log["u"], log["v"]) reg = 1 # Optimal plan with uniform warmstart pi_unif, _ = ot.bregman.sinkhorn( a, b, M, reg, method="sinkhorn", log=True, warmstart=None ) # Optimal plan with warmstart generated from unregularized OT pi_sh, _ = ot.bregman.sinkhorn( a, b, M, reg, method="sinkhorn", log=True, warmstart=warmstart ) pi_sh_log, _ = ot.bregman.sinkhorn( a, b, M, reg, method="sinkhorn_log", log=True, warmstart=warmstart ) pi_sh_stab, _ = ot.bregman.sinkhorn( a, b, M, reg, method="sinkhorn_stabilized", log=True, warmstart=warmstart ) pi_sh_sc, _ = ot.bregman.sinkhorn( a, b, M, reg, method="sinkhorn_epsilon_scaling", log=True, warmstart=warmstart ) np.testing.assert_allclose(pi_unif, pi_sh, atol=1e-05) np.testing.assert_allclose(pi_unif, pi_sh_log, atol=1e-05) np.testing.assert_allclose(pi_unif, pi_sh_stab, atol=1e-05) np.testing.assert_allclose(pi_unif, pi_sh_sc, atol=1e-05) def test_empirical_sinkhorn_warmstart(): m, n = 10, 20 Xs = np.arange(m).reshape(-1, 1) * 1.0 Xt = np.arange(n).reshape(-1, 1) * 1.0 M = ot.dist(Xs, Xt) # Generate warmstart from dual vectors of unregularized OT a = ot.unif(m) b = ot.unif(n) _, log = ot.lp.emd(a, b, M, log=True) warmstart = (log["u"], log["v"]) reg = 1 # Optimal plan with uniform warmstart f, g, _ = ot.bregman.empirical_sinkhorn( X_s=Xs, X_t=Xt, reg=reg, isLazy=True, log=True, warmstart=None ) pi_unif = np.exp(f[:, None] + g[None, :] - M / reg) # Optimal plan with warmstart generated from unregularized OT f, g, _ = ot.bregman.empirical_sinkhorn( X_s=Xs, X_t=Xt, reg=reg, isLazy=True, log=True, warmstart=warmstart ) pi_ws_lazy = np.exp(f[:, None] + g[None, :] - M / reg) pi_ws_not_lazy, _ = ot.bregman.empirical_sinkhorn( X_s=Xs, X_t=Xt, reg=reg, isLazy=False, log=True, warmstart=warmstart ) np.testing.assert_allclose(pi_unif, pi_ws_lazy, atol=1e-05) np.testing.assert_allclose(pi_unif, pi_ws_not_lazy, atol=1e-05) def test_empirical_sinkhorn_divergence_warmstart(): m, n = 10, 20 Xs = np.arange(m).reshape(-1, 1) * 1.0 Xt = np.arange(n).reshape(-1, 1) * 1.0 M = ot.dist(Xs, Xt) # Generate warmstart from dual vectors of unregularized OT a = ot.unif(m) b = ot.unif(n) _, log = ot.lp.emd(a, b, M, log=True) warmstart = (log["u"], log["v"]) reg = 1 # Optimal plan with uniform warmstart sd_unif, _ = ot.bregman.empirical_sinkhorn_divergence( X_s=Xs, X_t=Xt, reg=reg, isLazy=True, log=True, warmstart=None ) # Optimal plan with warmstart generated from unregularized OT sd_ws_lazy, _ = ot.bregman.empirical_sinkhorn_divergence( X_s=Xs, X_t=Xt, reg=reg, isLazy=True, log=True, warmstart=warmstart ) sd_ws_not_lazy, _ = ot.bregman.empirical_sinkhorn_divergence( X_s=Xs, X_t=Xt, reg=reg, isLazy=False, log=True, warmstart=warmstart ) np.testing.assert_allclose(sd_unif, sd_ws_lazy, atol=1e-05) np.testing.assert_allclose(sd_unif, sd_ws_not_lazy, atol=1e-05) python-pot-0.9.7+dfsg/test/test_bspot.py000066400000000000000000000073351525521106200203150ustar00rootroot00000000000000"""Tests for module ot.bsp""" # Author: Baptiste Genest # # License: MIT License import ot import ot.bsp import numpy as np import pytest def test_bsp_ot_exact_identity(): # test that bsp-ot is exact for similarity transform n = 50 rng = np.random.RandomState(0) A = rng.randn(n, 2) # create B as a similarity transform of A, scale by 2 and translate x coord by 1 B = 2 * A + np.array([1, 0]) _, perm, _ = ot.bsp.compute_bspot_bijection(A, B, 1) # check that the permutation is the identity np.testing.assert_allclose(perm, np.arange(n)) def test_bsp_ot_identity_null_cost(): # test that the cost returned by bsp-ot is zero for the identity transform n = 50 rng = np.random.RandomState(0) A = rng.randn(n, 2) cost, _, _ = ot.bsp.compute_bspot_bijection(A, A, 1) # check that the cost is zero np.testing.assert_allclose(cost, 0) def test_bsp_ot_bijective(): # test that the output of bsp-ot is indeed a bijection n = 50 rng = np.random.RandomState(0) A = rng.randn(n, 2) # create B as a similarity transform of A, scale by 2 and translate x coord by 1 B = 2 * A + np.array([1, 0]) _, perm, _ = ot.bsp.compute_bspot_bijection(A, B, 1) # check that the permutation is a bijection assert len(set(perm)) == n, "Permutation is not a bijection" # check that the permutation is a valid permutation assert set(perm) == set(range(n)), "Permutation is not valid" def test_bsp_ot_plan_merge_decrease(): # test that merging two plans gives a smaller cost than the plans separately n = 50 rng = np.random.RandomState(0) A = rng.randn(n, 2) B = rng.randn(n, 2) cost, plan, plans = ot.bsp.compute_bspot_bijection(A, B, 2) # evaluate mean squared cost lambda def cost_lambda(A, B, T): return np.mean(np.sum((A - B[T]) ** 2, axis=1)) cost_plan_1 = cost_lambda(A, B, plans[0]) cost_plan_2 = cost_lambda(A, B, plans[1]) # check that the cost of the merged plan is smaller than the cost of the separate plans assert ( cost <= cost_plan_1 ), "Cost of merged plan is not smaller than one of the input plans" assert ( cost <= cost_plan_2 ), "Cost of merged plan is not smaller than one of the input plans" def test_bsp_ot_relative_error(): # test that the cost returned by bsp-ot is close to the cost of the optimal transport plan n = 100 rng = np.random.RandomState(0) A = rng.randn(n, 2) B = rng.randn(n, 2) cost, perm, _ = ot.bsp.compute_bspot_bijection(A, B, 1000) w = ot.utils.unif(n) # solve exact with ot solver M = ot.dist(A, B, metric="sqeuclidean") G = ot.emd(w, w, M) cost_ot = np.sum(G * M) # check that the relative error is small relative_error = np.abs(cost - cost_ot) / (cost_ot + 1e-8) assert relative_error < 0.1, "Relative error is too large: {}".format( relative_error ) from ot.backend import torch @pytest.mark.skipif(not torch, reason="PyTorch not installed") def test_bsp_ot_torch_backend(): # test that bsp-ot works with torch tensors n = 50 rng = np.random.RandomState(0) A = rng.randn(n, 2) B = rng.randn(n, 2) A_torch = torch.from_numpy(A) B_torch = torch.from_numpy(B) A_torch.requires_grad_() B_torch.requires_grad_() cost, perm, _ = ot.bsp.compute_bspot_bijection(A_torch, B_torch, 1) # compute gradients w.r.t. A and B cost.backward() # advected points (should match B) A_new = A - A_torch.grad.detach().numpy() * n / 2 # compute new cost with advected points cost_new, _, _ = ot.bsp.compute_bspot_bijection(A_new, B, 1) # cost should be zero np.testing.assert_allclose(cost_new, 0, atol=1e-5) python-pot-0.9.7+dfsg/test/test_circle_solver.py000066400000000000000000000126241525521106200220160ustar00rootroot00000000000000"""Tests for module Circle Wasserstein solver""" # Author: Clément Bonet # # License: MIT License import numpy as np import pytest import ot def test_wasserstein_1d_circle(): # test binary_search_circle and wasserstein_circle give similar results as emd n = 20 m = 30 rng = np.random.RandomState(0) u = rng.rand( n, ) v = rng.rand( m, ) w_u = rng.uniform(0.0, 1.0, n) w_u = w_u / w_u.sum() w_v = rng.uniform(0.0, 1.0, m) w_v = w_v / w_v.sum() M1 = np.minimum(np.abs(u[:, None] - v[None]), 1 - np.abs(u[:, None] - v[None])) wass1 = ot.emd2(w_u, w_v, M1) wass1_bsc = ot.binary_search_circle(u, v, w_u, w_v, p=1) w1_circle = ot.wasserstein_circle(u, v, w_u, w_v, p=1) M2 = M1**2 wass2 = ot.emd2(w_u, w_v, M2) wass2_bsc = ot.binary_search_circle(u, v, w_u, w_v, p=2) w2_circle = ot.wasserstein_circle(u, v, w_u, w_v, p=2) # check loss is similar np.testing.assert_allclose(wass1, wass1_bsc) np.testing.assert_allclose(wass1, w1_circle, rtol=1e-2) np.testing.assert_allclose(wass2, wass2_bsc) np.testing.assert_allclose(wass2, w2_circle) @pytest.skip_backend("tf") def test_wasserstein1d_circle_devices(nx): rng = np.random.RandomState(0) n = 10 x = np.linspace(0, 1, n) rho_u = np.abs(rng.randn(n)) rho_u /= rho_u.sum() rho_v = np.abs(rng.randn(n)) rho_v /= rho_v.sum() for tp in nx.__type_list__: # print(nx.dtype_device(tp)) xb, rho_ub, rho_vb = nx.from_numpy(x, rho_u, rho_v, type_as=tp) w1 = ot.wasserstein_circle(xb, xb, rho_ub, rho_vb, p=1) w2_bsc = ot.wasserstein_circle(xb, xb, rho_ub, rho_vb, p=2) nx.assert_same_dtype_device(xb, w1) nx.assert_same_dtype_device(xb, w2_bsc) def test_wasserstein_1d_unif_circle(): # test semidiscrete_wasserstein2_unif_circle versus wasserstein_circle n = 20 m = 1000 rng = np.random.RandomState(0) u = rng.rand( n, ) v = rng.rand( m, ) # w_u = rng.uniform(0., 1., n) # w_u = w_u / w_u.sum() w_u = ot.utils.unif(n) w_v = ot.utils.unif(m) M1 = np.minimum(np.abs(u[:, None] - v[None]), 1 - np.abs(u[:, None] - v[None])) wass2 = ot.emd2(w_u, w_v, M1**2) wass2_circle = ot.wasserstein_circle(u, v, w_u, w_v, p=2, eps=1e-15) wass2_unif_circle = ot.semidiscrete_wasserstein2_unif_circle(u, w_u) # check loss is similar np.testing.assert_allclose(wass2, wass2_unif_circle, atol=1e-2) np.testing.assert_allclose(wass2_circle, wass2_unif_circle, atol=1e-2) def test_wasserstein1d_unif_circle_devices(nx): rng = np.random.RandomState(0) n = 10 x = np.linspace(0, 1, n) rho_u = np.abs(rng.randn(n)) rho_u /= rho_u.sum() for tp in nx.__type_list__: # print(nx.dtype_device(tp)) xb, rho_ub = nx.from_numpy(x, rho_u, type_as=tp) w2 = ot.semidiscrete_wasserstein2_unif_circle(xb, rho_ub) nx.assert_same_dtype_device(xb, w2) def test_binary_search_circle_log(): n = 20 m = 30 rng = np.random.RandomState(0) u = rng.rand( n, ) v = rng.rand( m, ) wass2_bsc, log = ot.binary_search_circle(u, v, p=2, log=True) optimal_thetas = log["optimal_theta"] assert optimal_thetas.shape[0] == 1 def test_wasserstein_circle_bad_shape(): n = 20 m = 30 rng = np.random.RandomState(0) u = rng.rand(n, 2) v = rng.rand(m, 1) with pytest.raises(ValueError): _ = ot.wasserstein_circle(u, v, p=2) with pytest.raises(ValueError): _ = ot.wasserstein_circle(u, v, p=1) @pytest.skip_backend("tf") def test_linear_circular_ot_devices(nx): rng = np.random.RandomState(0) n = 10 x = np.linspace(0, 1, n) rho_u = np.abs(rng.randn(n)) rho_u /= rho_u.sum() rho_v = np.abs(rng.randn(n)) rho_v /= rho_v.sum() for tp in nx.__type_list__: print(nx.dtype_device(tp)) xb, rho_ub, rho_vb = nx.from_numpy(x, rho_u, rho_v, type_as=tp) lcot = ot.linear_circular_ot(xb, xb, rho_ub, rho_vb) nx.assert_same_dtype_device(xb, lcot) def test_linear_circular_ot_bad_shape(): n = 20 m = 30 rng = np.random.RandomState(0) u = rng.rand(n, 2) v = rng.rand(m, 1) with pytest.raises(ValueError): _ = ot.linear_circular_ot(u, v) def test_linear_circular_ot_same_dist(): n = 20 rng = np.random.RandomState(0) u = rng.rand(n) lcot = ot.linear_circular_ot(u, u) np.testing.assert_almost_equal(lcot, 0.0) def test_linear_circular_ot_different_dist(): n = 20 m = 30 rng = np.random.RandomState(0) u = rng.rand(n) v = rng.rand(m) lcot = ot.linear_circular_ot(u, v) assert lcot > 0.0 def test_linear_circular_embedding_shape(): n = 20 rng = np.random.RandomState(0) u = rng.rand(n, 2) ts = np.linspace(0, 1, 101)[:-1] emb = ot.lp.solver_circle.linear_circular_embedding(ts, u) assert emb.shape == (100, 2) emb = ot.lp.solver_circle.linear_circular_embedding(ts, u[:, 0]) assert emb.shape == (100, 1) def test_linear_circular_ot_unif_circle(): n = 20 m = 1000 rng = np.random.RandomState(0) u = rng.rand( n, ) v = rng.rand( m, ) lcot = ot.linear_circular_ot(u, v) lcot_unif = ot.linear_circular_ot(u) # check loss is similar np.testing.assert_allclose(lcot, lcot_unif, atol=1e-2) python-pot-0.9.7+dfsg/test/test_coot.py000066400000000000000000000307651525521106200201350ustar00rootroot00000000000000"""Tests for module COOT on OT""" # Author: Quang Huy Tran # # License: MIT License import numpy as np import ot from ot.coot import co_optimal_transport as coot from ot.coot import co_optimal_transport2 as coot2 import pytest @pytest.mark.parametrize("verbose", [False, True, 1, 0]) def test_coot(nx, verbose): n_samples = 60 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() xs_nx = nx.from_numpy(xs) xt_nx = nx.from_numpy(xt) # test couplings pi_sample, pi_feature = coot(X=xs, Y=xt, verbose=verbose) pi_sample_nx, pi_feature_nx = coot(X=xs_nx, Y=xt_nx, verbose=verbose) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) anti_id_sample = np.flipud(np.eye(n_samples, n_samples)) / n_samples id_feature = np.eye(2, 2) / 2 np.testing.assert_allclose(pi_sample, anti_id_sample, atol=1e-04) np.testing.assert_allclose(pi_sample_nx, anti_id_sample, atol=1e-04) np.testing.assert_allclose(pi_feature, id_feature, atol=1e-04) np.testing.assert_allclose(pi_feature_nx, id_feature, atol=1e-04) # test marginal distributions px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) np.testing.assert_allclose(px_s, pi_sample_nx.sum(0), atol=1e-04) np.testing.assert_allclose(py_s, pi_sample_nx.sum(1), atol=1e-04) np.testing.assert_allclose(px_f, pi_feature_nx.sum(0), atol=1e-04) np.testing.assert_allclose(py_f, pi_feature_nx.sum(1), atol=1e-04) np.testing.assert_allclose(px_s, pi_sample.sum(0), atol=1e-04) np.testing.assert_allclose(py_s, pi_sample.sum(1), atol=1e-04) np.testing.assert_allclose(px_f, pi_feature.sum(0), atol=1e-04) np.testing.assert_allclose(py_f, pi_feature.sum(1), atol=1e-04) # test COOT distance coot_np = coot2(X=xs, Y=xt, verbose=verbose) coot_nx = nx.to_numpy(coot2(X=xs_nx, Y=xt_nx, verbose=verbose)) np.testing.assert_allclose(coot_np, 0, atol=1e-08) np.testing.assert_allclose(coot_nx, 0, atol=1e-08) def test_entropic_coot(nx): n_samples = 60 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() xs_nx = nx.from_numpy(xs) xt_nx = nx.from_numpy(xt) epsilon = (1, 1e-1) nits_ot = 2000 # test couplings pi_sample, pi_feature = coot(X=xs, Y=xt, epsilon=epsilon, nits_ot=nits_ot) pi_sample_nx, pi_feature_nx = coot( X=xs_nx, Y=xt_nx, epsilon=epsilon, nits_ot=nits_ot ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample, pi_sample_nx, atol=1e-04) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-04) # test marginal distributions px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) np.testing.assert_allclose(px_s, pi_sample_nx.sum(0), atol=1e-04) np.testing.assert_allclose(py_s, pi_sample_nx.sum(1), atol=1e-04) np.testing.assert_allclose(px_f, pi_feature_nx.sum(0), atol=1e-04) np.testing.assert_allclose(py_f, pi_feature_nx.sum(1), atol=1e-04) np.testing.assert_allclose(px_s, pi_sample.sum(0), atol=1e-04) np.testing.assert_allclose(py_s, pi_sample.sum(1), atol=1e-04) np.testing.assert_allclose(px_f, pi_feature.sum(0), atol=1e-04) np.testing.assert_allclose(py_f, pi_feature.sum(1), atol=1e-04) # test entropic COOT distance coot_np = coot2(X=xs, Y=xt, epsilon=epsilon, nits_ot=nits_ot) coot_nx = nx.to_numpy(coot2(X=xs_nx, Y=xt_nx, epsilon=epsilon, nits_ot=nits_ot)) np.testing.assert_allclose(coot_np, coot_nx, atol=1e-08) def test_coot_with_linear_terms(nx): n_samples = 60 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() xs_nx = nx.from_numpy(xs) xt_nx = nx.from_numpy(xt) M_samp = np.ones((n_samples, n_samples)) np.fill_diagonal(np.fliplr(M_samp), 0) M_feat = np.ones((2, 2)) np.fill_diagonal(M_feat, 0) M_samp_nx, M_feat_nx = nx.from_numpy(M_samp), nx.from_numpy(M_feat) alpha = (1, 2) # test couplings anti_id_sample = np.flipud(np.eye(n_samples, n_samples)) / n_samples id_feature = np.eye(2, 2) / 2 pi_sample, pi_feature = coot(X=xs, Y=xt, alpha=alpha, M_samp=M_samp, M_feat=M_feat) pi_sample_nx, pi_feature_nx = coot( X=xs_nx, Y=xt_nx, alpha=alpha, M_samp=M_samp_nx, M_feat=M_feat_nx ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample, anti_id_sample, atol=1e-04) np.testing.assert_allclose(pi_sample_nx, anti_id_sample, atol=1e-04) np.testing.assert_allclose(pi_feature, id_feature, atol=1e-04) np.testing.assert_allclose(pi_feature_nx, id_feature, atol=1e-04) # test marginal distributions px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) np.testing.assert_allclose(px_s, pi_sample_nx.sum(0), atol=1e-04) np.testing.assert_allclose(py_s, pi_sample_nx.sum(1), atol=1e-04) np.testing.assert_allclose(px_f, pi_feature_nx.sum(0), atol=1e-04) np.testing.assert_allclose(py_f, pi_feature_nx.sum(1), atol=1e-04) np.testing.assert_allclose(px_s, pi_sample.sum(0), atol=1e-04) np.testing.assert_allclose(py_s, pi_sample.sum(1), atol=1e-04) np.testing.assert_allclose(px_f, pi_feature.sum(0), atol=1e-04) np.testing.assert_allclose(py_f, pi_feature.sum(1), atol=1e-04) # test COOT distance coot_np = coot2(X=xs, Y=xt, alpha=alpha, M_samp=M_samp, M_feat=M_feat) coot_nx = nx.to_numpy( coot2(X=xs_nx, Y=xt_nx, alpha=alpha, M_samp=M_samp_nx, M_feat=M_feat_nx) ) np.testing.assert_allclose(coot_np, 0, atol=1e-08) np.testing.assert_allclose(coot_nx, 0, atol=1e-08) def test_coot_raise_value_error(nx): n_samples = 80 # nb samples mu_s = np.array([2, 4]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=43) xt = xs[::-1].copy() xs_nx = nx.from_numpy(xs) xt_nx = nx.from_numpy(xt) # raise value error of method sinkhorn def coot_sh(method_sinkhorn): return coot(X=xs, Y=xt, method_sinkhorn=method_sinkhorn) def coot_sh_nx(method_sinkhorn): return coot(X=xs_nx, Y=xt_nx, method_sinkhorn=method_sinkhorn) np.testing.assert_raises(ValueError, coot_sh, "not_sinkhorn") np.testing.assert_raises(ValueError, coot_sh_nx, "not_sinkhorn") # raise value error for epsilon def coot_eps(epsilon): return coot(X=xs, Y=xt, epsilon=epsilon) def coot_eps_nx(epsilon): return coot(X=xs_nx, Y=xt_nx, epsilon=epsilon) np.testing.assert_raises(ValueError, coot_eps, (1, 2, 3)) np.testing.assert_raises(ValueError, coot_eps_nx, [1, 2, 3, 4]) # raise value error for alpha def coot_alpha(alpha): return coot(X=xs, Y=xt, alpha=alpha) def coot_alpha_nx(alpha): return coot(X=xs_nx, Y=xt_nx, alpha=alpha) np.testing.assert_raises(ValueError, coot_alpha, [1]) np.testing.assert_raises(ValueError, coot_alpha_nx, np.arange(4)) def test_coot_warmstart(nx): n_samples = 80 # nb samples mu_s = np.array([2, 3]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=125) xt = xs[::-1].copy() xs_nx = nx.from_numpy(xs) xt_nx = nx.from_numpy(xt) # initialize warmstart rng = np.random.RandomState(42) init_pi_sample = rng.rand(n_samples, n_samples) init_pi_sample = init_pi_sample / np.sum(init_pi_sample) init_pi_sample_nx = nx.from_numpy(init_pi_sample) init_pi_feature = rng.rand(2, 2) init_pi_feature /= init_pi_feature / np.sum(init_pi_feature) init_pi_feature_nx = nx.from_numpy(init_pi_feature) init_duals_sample = (rng.random(n_samples) * 2 - 1, rng.random(n_samples) * 2 - 1) init_duals_sample_nx = ( nx.from_numpy(init_duals_sample[0]), nx.from_numpy(init_duals_sample[1]), ) init_duals_feature = (rng.random(2) * 2 - 1, rng.random(2) * 2 - 1) init_duals_feature_nx = ( nx.from_numpy(init_duals_feature[0]), nx.from_numpy(init_duals_feature[1]), ) warmstart = { "pi_sample": init_pi_sample, "pi_feature": init_pi_feature, "duals_sample": init_duals_sample, "duals_feature": init_duals_feature, } warmstart_nx = { "pi_sample": init_pi_sample_nx, "pi_feature": init_pi_feature_nx, "duals_sample": init_duals_sample_nx, "duals_feature": init_duals_feature_nx, } # test couplings pi_sample, pi_feature = coot(X=xs, Y=xt, warmstart=warmstart) pi_sample_nx, pi_feature_nx = coot(X=xs_nx, Y=xt_nx, warmstart=warmstart_nx) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) anti_id_sample = np.flipud(np.eye(n_samples, n_samples)) / n_samples id_feature = np.eye(2, 2) / 2 np.testing.assert_allclose(pi_sample, anti_id_sample, atol=1e-04) np.testing.assert_allclose(pi_sample_nx, anti_id_sample, atol=1e-04) np.testing.assert_allclose(pi_feature, id_feature, atol=1e-04) np.testing.assert_allclose(pi_feature_nx, id_feature, atol=1e-04) # test marginal distributions px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) np.testing.assert_allclose(px_s, pi_sample_nx.sum(0), atol=1e-04) np.testing.assert_allclose(py_s, pi_sample_nx.sum(1), atol=1e-04) np.testing.assert_allclose(px_f, pi_feature_nx.sum(0), atol=1e-04) np.testing.assert_allclose(py_f, pi_feature_nx.sum(1), atol=1e-04) np.testing.assert_allclose(px_s, pi_sample.sum(0), atol=1e-04) np.testing.assert_allclose(py_s, pi_sample.sum(1), atol=1e-04) np.testing.assert_allclose(px_f, pi_feature.sum(0), atol=1e-04) np.testing.assert_allclose(py_f, pi_feature.sum(1), atol=1e-04) # test COOT distance coot_np = coot2(X=xs, Y=xt, warmstart=warmstart) coot_nx = nx.to_numpy(coot2(X=xs_nx, Y=xt_nx, warmstart=warmstart_nx)) np.testing.assert_allclose(coot_np, 0, atol=1e-08) np.testing.assert_allclose(coot_nx, 0, atol=1e-08) def test_coot_log(nx): n_samples = 90 # nb samples mu_s = np.array([-2, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=43) xt = xs[::-1].copy() xs_nx = nx.from_numpy(xs) xt_nx = nx.from_numpy(xt) pi_sample, pi_feature, log = coot(X=xs, Y=xt, log=True) pi_sample_nx, pi_feature_nx, log_nx = coot(X=xs_nx, Y=xt_nx, log=True) duals_sample, duals_feature = log["duals_sample"], log["duals_feature"] assert len(duals_sample) == 2 assert len(duals_feature) == 2 assert len(duals_sample[0]) == n_samples assert len(duals_sample[1]) == n_samples assert len(duals_feature[0]) == 2 assert len(duals_feature[1]) == 2 duals_sample_nx = log_nx["duals_sample"] assert len(duals_sample_nx) == 2 assert len(duals_sample_nx[0]) == n_samples assert len(duals_sample_nx[1]) == n_samples duals_feature_nx = log_nx["duals_feature"] assert len(duals_feature_nx) == 2 assert len(duals_feature_nx[0]) == 2 assert len(duals_feature_nx[1]) == 2 list_coot = log["distances"] assert len(list_coot) >= 1 list_coot_nx = log_nx["distances"] assert len(list_coot_nx) >= 1 # test with coot distance coot_np, log = coot2(X=xs, Y=xt, log=True) coot_nx, log_nx = coot2(X=xs_nx, Y=xt_nx, log=True) duals_sample, duals_feature = log["duals_sample"], log["duals_feature"] assert len(duals_sample) == 2 assert len(duals_feature) == 2 assert len(duals_sample[0]) == n_samples assert len(duals_sample[1]) == n_samples assert len(duals_feature[0]) == 2 assert len(duals_feature[1]) == 2 duals_sample_nx = log_nx["duals_sample"] assert len(duals_sample_nx) == 2 assert len(duals_sample_nx[0]) == n_samples assert len(duals_sample_nx[1]) == n_samples duals_feature_nx = log_nx["duals_feature"] assert len(duals_feature_nx) == 2 assert len(duals_feature_nx[0]) == 2 assert len(duals_feature_nx[1]) == 2 list_coot = log["distances"] assert len(list_coot) >= 1 list_coot_nx = log_nx["distances"] assert len(list_coot_nx) >= 1 python-pot-0.9.7+dfsg/test/test_da.py000066400000000000000000001044701525521106200175500ustar00rootroot00000000000000"""Tests for module da on Domain Adaptation""" # Author: Remi Flamary # # License: MIT License import numpy as np from numpy.testing import assert_allclose, assert_equal import pytest import ot from ot.datasets import make_data_classif from ot.utils import unif try: # test if cudamat installed import sklearn # noqa: F401 nosklearn = False except ImportError: nosklearn = True try: # test if cvxpy is installed import cvxpy # noqa: F401 nocvxpy = False except ImportError: nocvxpy = True def test_class_jax_tf(): from ot.backend import tf backends = [] if tf: backends.append(ot.backend.TensorflowBackend()) for nx in backends: ns = 150 nt = 200 Xs, ys = make_data_classif("3gauss", ns) Xt, yt = make_data_classif("3gauss2", nt) Xs, ys, Xt, yt = nx.from_numpy(Xs, ys, Xt, yt) otda = ot.da.SinkhornLpl1Transport() with pytest.raises(TypeError): otda.fit(Xs=Xs, ys=ys, Xt=Xt) @pytest.skip_backend("jax") @pytest.skip_backend("tf") @pytest.mark.parametrize( "class_to_test", [ ot.da.EMDTransport, ot.da.SinkhornTransport, ot.da.SinkhornLpl1Transport, ot.da.SinkhornL1l2Transport, ot.da.SinkhornL1l2Transport, ], ) def test_log_da(nx, class_to_test): ns = 50 nt = 50 Xs, ys = make_data_classif("3gauss", ns) Xt, yt = make_data_classif("3gauss2", nt) Xs, ys, Xt, yt = nx.from_numpy(Xs, ys, Xt, yt) otda = class_to_test(log=True) # test its computed otda.fit(Xs=Xs, ys=ys, Xt=Xt) assert hasattr(otda, "log_") @pytest.skip_backend("tf") def test_sinkhorn_lpl1_transport_class(nx): """test_sinkhorn_transport""" ns = 50 nt = 50 Xs, ys = make_data_classif("3gauss", ns, random_state=42) Xt, yt = make_data_classif("3gauss2", nt, random_state=43) # prepare semi-supervised labels yt_semi = np.copy(yt) yt_semi[np.arange(0, nt, 2)] = -1 Xs, ys, Xt, yt, yt_semi = nx.from_numpy(Xs, ys, Xt, yt, yt_semi) otda = ot.da.SinkhornLpl1Transport() # test its computed otda.fit(Xs=Xs, ys=ys, Xt=Xt) assert hasattr(otda, "cost_") assert not np.any(np.isnan(nx.to_numpy(otda.cost_))), "cost is finite" assert hasattr(otda, "coupling_") assert np.all(np.isfinite(nx.to_numpy(otda.coupling_))), "coupling is finite" # test dimensions of coupling assert_equal(otda.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) assert_allclose( nx.to_numpy(nx.sum(otda.coupling_, axis=0)), mu_t, rtol=1e-3, atol=1e-3 ) assert_allclose( nx.to_numpy(nx.sum(otda.coupling_, axis=1)), mu_s, rtol=1e-3, atol=1e-3 ) # test transform transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) Xs_new = nx.from_numpy(make_data_classif("3gauss", ns + 1, random_state=44)[0]) transp_Xs_new = otda.transform(Xs_new) # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) # test inverse transform transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) Xt_new = nx.from_numpy(make_data_classif("3gauss2", nt + 1, random_state=45)[0]) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) # check that the oos method is working assert_equal(transp_Xt_new.shape, Xt_new.shape) # test fit_transform transp_Xs = otda.fit_transform(Xs=Xs, ys=ys, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) # check label propagation transp_yt = otda.transform_labels(ys) assert_equal(transp_yt.shape[0], yt.shape[0]) assert_equal(transp_yt.shape[1], len(np.unique(ys))) # check inverse label propagation transp_ys = otda.inverse_transform_labels(yt) assert_equal(transp_ys.shape[0], ys.shape[0]) assert_equal(transp_ys.shape[1], len(np.unique(yt))) # test unsupervised vs semi-supervised mode otda_unsup = ot.da.SinkhornLpl1Transport() otda_unsup.fit(Xs=Xs, ys=ys, Xt=Xt) assert np.all( np.isfinite(nx.to_numpy(otda_unsup.coupling_)) ), "unsup coupling is finite" n_unsup = nx.sum(otda_unsup.cost_) otda_semi = ot.da.SinkhornLpl1Transport() otda_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt_semi) assert np.all( np.isfinite(nx.to_numpy(otda_semi.coupling_)) ), "semi coupling is finite" assert_equal(otda_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) n_semisup = nx.sum(otda_semi.cost_) # check that the cost matrix norms are indeed different assert not np.allclose( n_unsup, n_semisup, atol=1e-7 ), "semisupervised mode is not working" # check that the coupling forbids mass transport between labeled source # and labeled target samples mass_semi = nx.sum(otda_semi.coupling_[otda_semi.cost_ == otda_semi.limit_max]) assert mass_semi == 0, "semisupervised mode not working" @pytest.skip_backend("tf") def test_sinkhorn_l1l2_transport_class(nx): """test_sinkhorn_transport""" ns = 50 nt = 50 Xs, ys = make_data_classif("3gauss", ns, random_state=42) Xt, yt = make_data_classif("3gauss2", nt, random_state=43) # prepare semi-supervised labels yt_semi = np.copy(yt) yt_semi[np.arange(0, nt, 2)] = -1 Xs, ys, Xt, yt, yt_semi = nx.from_numpy(Xs, ys, Xt, yt, yt_semi) otda = ot.da.SinkhornL1l2Transport(max_inner_iter=500) otda.fit(Xs=Xs, ys=ys, Xt=Xt) # test its computed assert hasattr(otda, "cost_") assert hasattr(otda, "coupling_") assert hasattr(otda, "log_") # test dimensions of coupling assert_equal(otda.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) assert_allclose( nx.to_numpy(nx.sum(otda.coupling_, axis=0)), mu_t, rtol=1e-3, atol=1e-3 ) assert_allclose( nx.to_numpy(nx.sum(otda.coupling_, axis=1)), mu_s, rtol=1e-3, atol=1e-3 ) # test transform transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) Xs_new = nx.from_numpy(make_data_classif("3gauss", ns + 1)[0]) transp_Xs_new = otda.transform(Xs_new) # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) # test inverse transform transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) # check label propagation transp_yt = otda.transform_labels(ys) assert_equal(transp_yt.shape[0], yt.shape[0]) assert_equal(transp_yt.shape[1], len(np.unique(ys))) # check inverse label propagation transp_ys = otda.inverse_transform_labels(yt) assert_equal(transp_ys.shape[0], ys.shape[0]) assert_equal(transp_ys.shape[1], len(np.unique(yt))) Xt_new = nx.from_numpy(make_data_classif("3gauss2", nt + 1)[0]) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) # check that the oos method is working assert_equal(transp_Xt_new.shape, Xt_new.shape) # test fit_transform transp_Xs = otda.fit_transform(Xs=Xs, ys=ys, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) # test unsupervised vs semi-supervised mode otda_unsup = ot.da.SinkhornL1l2Transport() otda_unsup.fit(Xs=Xs, ys=ys, Xt=Xt) n_unsup = nx.sum(otda_unsup.cost_) otda_semi = ot.da.SinkhornL1l2Transport() otda_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt_semi) assert_equal(otda_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) n_semisup = nx.sum(otda_semi.cost_) # check that the cost matrix norms are indeed different assert not np.allclose( n_unsup, n_semisup, atol=1e-7 ), "semisupervised mode is not working" # check that the coupling forbids mass transport between labeled source # and labeled target samples mass_semi = nx.sum(otda_semi.coupling_[otda_semi.cost_ == otda_semi.limit_max]) mass_semi = otda_semi.coupling_[otda_semi.cost_ == otda_semi.limit_max] assert_allclose( nx.to_numpy(mass_semi), np.zeros_like(mass_semi), rtol=1e-9, atol=1e-9 ) # check everything runs well with log=True otda = ot.da.SinkhornL1l2Transport(log=True) otda.fit(Xs=Xs, ys=ys, Xt=Xt) assert len(otda.log_.keys()) != 0 @pytest.skip_backend("jax") @pytest.skip_backend("tf") def test_sinkhorn_transport_class(nx): """test_sinkhorn_transport""" ns = 50 nt = 50 Xs, ys = make_data_classif("3gauss", ns) Xt, yt = make_data_classif("3gauss2", nt) Xs, ys, Xt, yt = nx.from_numpy(Xs, ys, Xt, yt) otda = ot.da.SinkhornTransport() # test its computed otda.fit(Xs=Xs, Xt=Xt) assert hasattr(otda, "cost_") assert hasattr(otda, "coupling_") assert hasattr(otda, "log_") # test dimensions of coupling assert_equal(otda.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) assert_allclose( nx.to_numpy(nx.sum(otda.coupling_, axis=0)), mu_t, rtol=1e-3, atol=1e-3 ) assert_allclose( nx.to_numpy(nx.sum(otda.coupling_, axis=1)), mu_s, rtol=1e-3, atol=1e-3 ) # test transform transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) Xs_new = nx.from_numpy(make_data_classif("3gauss", ns + 1)[0]) transp_Xs_new = otda.transform(Xs_new) # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) # test inverse transform transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) # check label propagation transp_yt = otda.transform_labels(ys) assert_equal(transp_yt.shape[0], yt.shape[0]) assert_equal(transp_yt.shape[1], len(np.unique(ys))) # check inverse label propagation transp_ys = otda.inverse_transform_labels(yt) assert_equal(transp_ys.shape[0], ys.shape[0]) assert_equal(transp_ys.shape[1], len(np.unique(yt))) Xt_new = nx.from_numpy(make_data_classif("3gauss2", nt + 1)[0]) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) # check that the oos method is working assert_equal(transp_Xt_new.shape, Xt_new.shape) # test fit_transform transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) # test unsupervised vs semi-supervised mode otda_unsup = ot.da.SinkhornTransport() otda_unsup.fit(Xs=Xs, Xt=Xt) n_unsup = nx.sum(otda_unsup.cost_) otda_semi = ot.da.SinkhornTransport() otda_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) assert_equal(otda_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) n_semisup = nx.sum(otda_semi.cost_) # check that the cost matrix norms are indeed different assert not np.allclose( n_unsup, n_semisup, atol=1e-7 ), "semisupervised mode is not working" # check that the coupling forbids mass transport between labeled source # and labeled target samples mass_semi = nx.sum(otda_semi.coupling_[otda_semi.cost_ == otda_semi.limit_max]) assert mass_semi == 0, "semisupervised mode not working" # check everything runs well with log=True otda = ot.da.SinkhornTransport(log=True) otda.fit(Xs=Xs, ys=ys, Xt=Xt) assert len(otda.log_.keys()) != 0 # test diffeernt transform and inverse transform otda = ot.da.SinkhornTransport(out_of_sample_map="ferradans") transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) # test diffeernt transform otda = ot.da.SinkhornTransport(out_of_sample_map="continuous", method="sinkhorn") transp_Xs2 = otda.fit_transform(Xs=Xs, Xt=Xt) assert_equal(transp_Xs2.shape, Xs.shape) transp_Xt2 = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt2.shape, Xt.shape) np.testing.assert_almost_equal( nx.to_numpy(transp_Xs), nx.to_numpy(transp_Xs2), decimal=5 ) np.testing.assert_almost_equal( nx.to_numpy(transp_Xt), nx.to_numpy(transp_Xt2), decimal=5 ) with pytest.raises(ValueError): otda = ot.da.SinkhornTransport(out_of_sample_map="unknown") @pytest.skip_backend("jax") @pytest.skip_backend("tf") def test_unbalanced_sinkhorn_transport_class(nx): """test_sinkhorn_transport""" ns = 50 nt = 50 Xs, ys = make_data_classif("3gauss", ns) Xt, yt = make_data_classif("3gauss2", nt) Xs, ys, Xt, yt = nx.from_numpy(Xs, ys, Xt, yt) for log in [True, False]: otda = ot.da.UnbalancedSinkhornTransport(log=log) # test its computed otda.fit(Xs=Xs, Xt=Xt) assert hasattr(otda, "cost_") assert hasattr(otda, "coupling_") assert hasattr(otda, "log_") # test dimensions of coupling assert_equal(otda.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) assert not np.any(np.isnan(nx.to_numpy(otda.cost_))), "cost is finite" # test coupling assert np.all(np.isfinite(nx.to_numpy(otda.coupling_))), "coupling is finite" # test transform transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) # check label propagation transp_yt = otda.transform_labels(ys) assert_equal(transp_yt.shape[0], yt.shape[0]) assert_equal(transp_yt.shape[1], len(np.unique(ys))) # check inverse label propagation transp_ys = otda.inverse_transform_labels(yt) assert_equal(transp_ys.shape[0], ys.shape[0]) assert_equal(transp_ys.shape[1], len(np.unique(yt))) Xs_new = nx.from_numpy(make_data_classif("3gauss", ns + 1)[0]) transp_Xs_new = otda.transform(Xs_new) # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) # test inverse transform transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) Xt_new = nx.from_numpy(make_data_classif("3gauss2", nt + 1)[0]) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) # check that the oos method is working assert_equal(transp_Xt_new.shape, Xt_new.shape) # test fit_transform transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) # test unsupervised vs semi-supervised mode otda_unsup = ot.da.SinkhornTransport() otda_unsup.fit(Xs=Xs, Xt=Xt) assert not np.any(np.isnan(nx.to_numpy(otda_unsup.cost_))), "cost is finite" n_unsup = nx.sum(otda_unsup.cost_) otda_semi = ot.da.SinkhornTransport() otda_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) assert not np.any(np.isnan(nx.to_numpy(otda_semi.cost_))), "cost is finite" assert_equal(otda_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) n_semisup = nx.sum(otda_semi.cost_) # check that the cost matrix norms are indeed different assert not np.allclose( n_unsup, n_semisup, atol=1e-7 ), "semisupervised mode is not working" # check everything runs well with log=True otda = ot.da.SinkhornTransport(log=True) otda.fit(Xs=Xs, ys=ys, Xt=Xt) assert not np.any(np.isnan(nx.to_numpy(otda.cost_))), "cost is finite" assert len(otda.log_.keys()) != 0 @pytest.skip_backend("jax") @pytest.skip_backend("tf") def test_emd_transport_class(nx): """test_sinkhorn_transport""" ns = 50 nt = 50 Xs, ys = make_data_classif("3gauss", ns) Xt, yt = make_data_classif("3gauss2", nt) Xs, ys, Xt, yt = nx.from_numpy(Xs, ys, Xt, yt) otda = ot.da.EMDTransport() # test its computed otda.fit(Xs=Xs, Xt=Xt) assert hasattr(otda, "cost_") assert hasattr(otda, "coupling_") # test dimensions of coupling assert_equal(otda.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) assert not np.any(np.isnan(nx.to_numpy(otda.cost_))), "cost is finite" assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) assert np.all(np.isfinite(nx.to_numpy(otda.coupling_))), "coupling is finite" # test margin constraints mu_s = unif(ns) mu_t = unif(nt) assert_allclose( nx.to_numpy(nx.sum(otda.coupling_, axis=0)), mu_t, rtol=1e-3, atol=1e-3 ) assert_allclose( nx.to_numpy(nx.sum(otda.coupling_, axis=1)), mu_s, rtol=1e-3, atol=1e-3 ) # test transform transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) Xs_new = nx.from_numpy(make_data_classif("3gauss", ns + 1)[0]) transp_Xs_new = otda.transform(Xs_new) # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) # test inverse transform transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) # check label propagation transp_yt = otda.transform_labels(ys) assert_equal(transp_yt.shape[0], yt.shape[0]) assert_equal(transp_yt.shape[1], len(np.unique(ys))) # check inverse label propagation transp_ys = otda.inverse_transform_labels(yt) assert_equal(transp_ys.shape[0], ys.shape[0]) assert_equal(transp_ys.shape[1], len(np.unique(yt))) Xt_new = nx.from_numpy(make_data_classif("3gauss2", nt + 1)[0]) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) # check that the oos method is working assert_equal(transp_Xt_new.shape, Xt_new.shape) # test fit_transform transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) # test unsupervised vs semi-supervised mode otda_unsup = ot.da.EMDTransport() otda_unsup.fit(Xs=Xs, ys=ys, Xt=Xt) assert_equal(otda_unsup.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) assert not np.any(np.isnan(nx.to_numpy(otda_unsup.cost_))), "cost is finite" assert_equal(otda_unsup.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) assert np.all(np.isfinite(nx.to_numpy(otda_unsup.coupling_))), "coupling is finite" n_unsup = nx.sum(otda_unsup.cost_) otda_semi = ot.da.EMDTransport() otda_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) assert_equal(otda_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) assert not np.any(np.isnan(nx.to_numpy(otda_semi.cost_))), "cost is finite" assert_equal(otda_semi.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) assert np.all(np.isfinite(nx.to_numpy(otda_semi.coupling_))), "coupling is finite" n_semisup = nx.sum(otda_semi.cost_) # check that the cost matrix norms are indeed different assert not np.allclose( n_unsup, n_semisup, atol=1e-7 ), "semisupervised mode is not working" # check that the coupling forbids mass transport between labeled source # and labeled target samples mass_semi = nx.sum(otda_semi.coupling_[otda_semi.cost_ == otda_semi.limit_max]) mass_semi = otda_semi.coupling_[otda_semi.cost_ == otda_semi.limit_max] # we need to use a small tolerance here, otherwise the test breaks assert_allclose( nx.to_numpy(mass_semi), np.zeros(list(mass_semi.shape)), rtol=1e-2, atol=1e-2 ) @pytest.skip_backend("tf") def test_semisupervised_cost_correction(nx): # gh-664: the label-aware cost correction must push apart labeled source # and target samples with *different* labels, and must leave pairs # involving an unlabeled (-1) sample untouched. rng = np.random.RandomState(0) Xs = rng.randn(6, 2) Xt = rng.randn(6, 2) ys = np.array([0, 0, 0, 1, 1, 1]) yt = np.array([0, 1, 0, 1, 0, 1]) Xs, ys, Xt, yt = nx.from_numpy(Xs, ys, Xt, yt) # all labels known: different-label pairs get the (scaled) limit_max, # same-label pairs keep their original, smaller cost otda = ot.da.EMDTransport(limit_max=10) otda.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) cost = nx.to_numpy(otda.cost_) limit = float(nx.to_numpy(otda.limit_max)) ys_np, yt_np = nx.to_numpy(ys), nx.to_numpy(yt) mismatch = ys_np[:, None] != yt_np[None, :] assert np.all(cost[mismatch] >= limit - 1e-9), "different labels not penalized" assert np.all(cost[~mismatch] < limit), "same labels wrongly penalized" # semi-supervised: unlabeled (-1) targets must never be forbidden yt_semi = nx.from_numpy(np.array([0, -1, 0, -1, 0, -1])) otda2 = ot.da.EMDTransport(limit_max=10) otda2.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt_semi) cost2 = nx.to_numpy(otda2.cost_) limit2 = float(nx.to_numpy(otda2.limit_max)) unlabeled = nx.to_numpy(yt_semi) == -1 assert np.all(cost2[:, unlabeled] < limit2), "unlabeled targets wrongly forbidden" @pytest.skip_backend("jax") @pytest.skip_backend("tf") @pytest.mark.parametrize("kernel", ["linear", "gaussian"]) @pytest.mark.parametrize("bias", ["unbiased", "biased"]) def test_mapping_transport_class(nx, kernel, bias): """test_mapping_transport""" ns = 20 nt = 30 Xs, ys = make_data_classif("3gauss", ns) Xt, yt = make_data_classif("3gauss2", nt) Xs_new, _ = make_data_classif("3gauss", ns + 1) Xs, Xt, Xs_new = nx.from_numpy(Xs, Xt, Xs_new) # Mapping tests bias = bias == "biased" otda = ot.da.MappingTransport(kernel=kernel, bias=bias) otda.fit(Xs=Xs, Xt=Xt) assert hasattr(otda, "coupling_") assert hasattr(otda, "mapping_") assert hasattr(otda, "log_") assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) S = Xs.shape[0] if kernel == "gaussian" else Xs.shape[1] # if linear if bias: S += 1 assert_equal(otda.mapping_.shape, ((S, Xt.shape[1]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) assert_allclose( nx.to_numpy(nx.sum(otda.coupling_, axis=0)), mu_t, rtol=1e-3, atol=1e-3 ) assert_allclose( nx.to_numpy(nx.sum(otda.coupling_, axis=1)), mu_s, rtol=1e-3, atol=1e-3 ) # test transform transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) transp_Xs_new = otda.transform(Xs_new) # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) # check everything runs well with log=True otda = ot.da.MappingTransport(kernel=kernel, bias=bias, log=True) otda.fit(Xs=Xs, Xt=Xt) assert len(otda.log_.keys()) != 0 @pytest.skip_backend("jax") @pytest.skip_backend("tf") def test_mapping_transport_class_specific_seed(nx): # check that it does not crash when derphi is very close to 0 ns = 20 nt = 30 rng = np.random.RandomState(39) Xs, ys = make_data_classif("3gauss", ns, random_state=rng) Xt, yt = make_data_classif("3gauss2", nt, random_state=rng) otda = ot.da.MappingTransport(kernel="gaussian", bias=False) otda.fit(Xs=nx.from_numpy(Xs), Xt=nx.from_numpy(Xt)) @pytest.skip_backend("jax") @pytest.skip_backend("tf") def test_linear_mapping_class(nx): ns = 50 nt = 50 Xs, ys = make_data_classif("3gauss", ns) Xt, yt = make_data_classif("3gauss2", nt) Xsb, Xtb = nx.from_numpy(Xs, Xt) for log in [True, False]: otmap = ot.da.LinearTransport(log=log) otmap.fit(Xs=Xsb, Xt=Xtb) assert hasattr(otmap, "A_") assert hasattr(otmap, "B_") assert hasattr(otmap, "A1_") assert hasattr(otmap, "B1_") Xst = nx.to_numpy(otmap.transform(Xs=Xsb)) Ct = np.cov(Xt.T) Cst = np.cov(Xst.T) np.testing.assert_allclose(Ct, Cst, rtol=1e-2, atol=1e-2) Xts = nx.to_numpy(otmap.inverse_transform(Xt=Xtb)) Cs = np.cov(Xs.T) Cts = np.cov(Xts.T) np.testing.assert_allclose(Cs, Cts, rtol=1e-2, atol=1e-2) @pytest.skip_backend("jax") @pytest.skip_backend("tf") def test_linear_gw_mapping_class(nx): ns = 50 nt = 50 Xs, ys = make_data_classif("3gauss", ns) Xt, yt = make_data_classif("3gauss2", nt) Xsb, Xtb = nx.from_numpy(Xs, Xt) for log in [True, False]: otmap = ot.da.LinearGWTransport(log=log) otmap.fit(Xs=Xsb, Xt=Xtb) assert hasattr(otmap, "A_") assert hasattr(otmap, "B_") assert hasattr(otmap, "A1_") assert hasattr(otmap, "B1_") Xst = nx.to_numpy(otmap.transform(Xs=Xsb)) Ct = np.cov(Xt.T) Cst = np.cov(Xst.T) np.testing.assert_allclose(Ct, Cst, rtol=1e-2, atol=1e-2) @pytest.skip_backend("jax") @pytest.skip_backend("tf") def test_jcpot_transport_class(nx): """test_jcpot_transport""" ns1 = 50 ns2 = 50 nt = 50 Xs1, ys1 = make_data_classif("3gauss", ns1) Xs2, ys2 = make_data_classif("3gauss", ns2) Xt, yt = make_data_classif("3gauss2", nt) Xs1, ys1, Xs2, ys2, Xt, yt = nx.from_numpy(Xs1, ys1, Xs2, ys2, Xt, yt) Xs = [Xs1, Xs2] ys = [ys1, ys2] for log in [True, False]: otda = ot.da.JCPOTTransport( reg_e=1, max_iter=10000, tol=1e-9, verbose=True, log=log ) # test its computed otda.fit(Xs=Xs, ys=ys, Xt=Xt) assert hasattr(otda, "coupling_") assert hasattr(otda, "proportions_") assert hasattr(otda, "log_") # test dimensions of coupling for i, xs in enumerate(Xs): assert_equal(otda.coupling_[i].shape, ((xs.shape[0], Xt.shape[0]))) # test all margin constraints mu_t = unif(nt) for i in range(len(Xs)): # test margin constraints w.r.t. uniform target weights for each coupling matrix assert_allclose( nx.to_numpy(nx.sum(otda.coupling_[i], axis=0)), mu_t, rtol=1e-3, atol=1e-3, ) if log: # test margin constraints w.r.t. modified source weights for each source domain assert_allclose( nx.to_numpy( nx.dot(otda.log_["D1"][i], nx.sum(otda.coupling_[i], axis=1)) ), nx.to_numpy(otda.proportions_), rtol=1e-3, atol=1e-3, ) # test transform transp_Xs = otda.transform(Xs=Xs) [assert_equal(x.shape, y.shape) for x, y in zip(transp_Xs, Xs)] Xs_new = nx.from_numpy(make_data_classif("3gauss", ns1 + 1)[0]) transp_Xs_new = otda.transform(Xs_new) # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) # check label propagation transp_yt = otda.transform_labels(ys) assert_equal(transp_yt.shape[0], yt.shape[0]) assert_equal(transp_yt.shape[1], len(np.unique(nx.to_numpy(*ys)))) # check inverse label propagation transp_ys = otda.inverse_transform_labels(yt) for x, y in zip(transp_ys, ys): assert_equal(x.shape[0], y.shape[0]) assert_equal(x.shape[1], len(np.unique(nx.to_numpy(y)))) def test_jcpot_barycenter(nx): """test_jcpot_barycenter""" ns1 = 50 ns2 = 50 nt = 50 sigma = 0.1 ps1 = 0.2 ps2 = 0.9 pt = 0.4 Xs1, ys1 = make_data_classif("2gauss_prop", ns1, nz=sigma, p=ps1) Xs2, ys2 = make_data_classif("2gauss_prop", ns2, nz=sigma, p=ps2) Xt, _ = make_data_classif("2gauss_prop", nt, nz=sigma, p=pt) Xs1b, ys1b, Xs2b, ys2b, Xtb = nx.from_numpy(Xs1, ys1, Xs2, ys2, Xt) Xsb = [Xs1b, Xs2b] ysb = [ys1b, ys2b] prop = ot.bregman.jcpot_barycenter( Xsb, ysb, Xtb, reg=0.5, metric="sqeuclidean", numItermax=10000, stopThr=1e-9, verbose=False, log=False, ) np.testing.assert_allclose(nx.to_numpy(prop), [1 - pt, pt], rtol=1e-3, atol=1e-3) @pytest.mark.skipif(nosklearn, reason="No sklearn available") @pytest.skip_backend("jax") @pytest.skip_backend("tf") def test_emd_laplace_class(nx): """test_emd_laplace_transport""" ns = 50 nt = 50 Xs, ys = make_data_classif("3gauss", ns) Xt, yt = make_data_classif("3gauss2", nt) Xs, ys, Xt, yt = nx.from_numpy(Xs, ys, Xt, yt) for log in [True, False]: otda = ot.da.EMDLaplaceTransport( reg_lap=0.01, max_iter=1000, tol=1e-9, verbose=False, log=log ) # test its computed otda.fit(Xs=Xs, ys=ys, Xt=Xt) assert hasattr(otda, "coupling_") assert hasattr(otda, "log_") # test dimensions of coupling assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) # test all margin constraints mu_s = unif(ns) mu_t = unif(nt) assert_allclose( nx.to_numpy(nx.sum(otda.coupling_, axis=0)), mu_t, rtol=1e-3, atol=1e-3 ) assert_allclose( nx.to_numpy(nx.sum(otda.coupling_, axis=1)), mu_s, rtol=1e-3, atol=1e-3 ) # test transform transp_Xs = otda.transform(Xs=Xs) [assert_equal(x.shape, y.shape) for x, y in zip(transp_Xs, Xs)] Xs_new = nx.from_numpy(make_data_classif("3gauss", ns + 1)[0]) transp_Xs_new = otda.transform(Xs_new) # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) # test inverse transform transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) Xt_new = nx.from_numpy(make_data_classif("3gauss2", nt + 1)[0]) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) # check that the oos method is working assert_equal(transp_Xt_new.shape, Xt_new.shape) # test fit_transform transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) # check label propagation transp_yt = otda.transform_labels(ys) assert_equal(transp_yt.shape[0], yt.shape[0]) assert_equal(transp_yt.shape[1], len(np.unique(nx.to_numpy(ys)))) # check inverse label propagation transp_ys = otda.inverse_transform_labels(yt) assert_equal(transp_ys.shape[0], ys.shape[0]) assert_equal(transp_ys.shape[1], len(np.unique(nx.to_numpy(yt)))) @pytest.mark.skipif(nocvxpy, reason="No CVXPY available") def test_nearest_brenier_potential(nx): X = nx.ones((2, 2)) for ssnb in [ ot.da.NearestBrenierPotential(log=True, its=5), ot.da.NearestBrenierPotential(log=False, its=5), ]: ssnb.fit(Xs=X, Xt=X) G_lu = ssnb.transform(Xs=X) # 'new' input isn't new, so should be equal to target np.testing.assert_almost_equal(nx.to_numpy(G_lu[0]), nx.to_numpy(X)) np.testing.assert_almost_equal(nx.to_numpy(G_lu[1]), nx.to_numpy(X)) @pytest.mark.skipif(nosklearn, reason="No sklearn available") @pytest.skip_backend("jax") @pytest.skip_backend("tf") def test_emd_laplace(nx): """Complements :code:`test_emd_laplace_class` for uncovered options in :code:`emd_laplace`""" ns = 50 nt = 50 Xs, ys = make_data_classif("3gauss", ns) Xt, yt = make_data_classif("3gauss2", nt) Xs, ys, Xt, yt = nx.from_numpy(Xs, ys, Xt, yt) M = ot.dist(Xs, Xt) with pytest.raises(ValueError): ot.da.emd_laplace( ot.unif(ns), ot.unif(nt), Xs, Xt, M, sim_param=["INVALID", "INPUT", 2] ) with pytest.raises(ValueError): ot.da.emd_laplace( ot.unif(ns), ot.unif(nt), Xs, Xt, M, sim=["INVALID", "INPUT", 2] ) # test all margin constraints with gaussian similarity and disp regularisation coupling = ot.da.emd_laplace( ot.unif(ns, type_as=Xs), ot.unif(nt, type_as=Xs), Xs, Xt, M, sim="gauss", reg="disp", ) assert_allclose( nx.to_numpy(nx.sum(coupling, axis=0)), unif(nt), rtol=1e-3, atol=1e-3 ) assert_allclose( nx.to_numpy(nx.sum(coupling, axis=1)), unif(ns), rtol=1e-3, atol=1e-3 ) @pytest.skip_backend("jax") @pytest.skip_backend("tf") def test_sinkhorn_l1l2_gl_cost_vectorized(nx): n_samples, n_labels = 150, 3 rng = np.random.RandomState(42) G = rng.rand(n_samples, n_samples) labels_a = rng.randint(n_labels, size=(n_samples,)) G, labels_a = nx.from_numpy(G), nx.from_numpy(labels_a) # previously used implementation for the cost estimator lstlab = nx.unique(labels_a) def f(G): res = 0 for i in range(G.shape[1]): for lab in lstlab: temp = G[labels_a == lab, i] res += nx.norm(temp) return res def df(G): W = nx.zeros(G.shape, type_as=G) for i in range(G.shape[1]): for lab in lstlab: temp = G[labels_a == lab, i] n = nx.norm(temp) if n: W[labels_a == lab, i] = temp / n return W # new vectorized implementation for the cost estimator labels_u, labels_idx = nx.unique(labels_a, return_inverse=True) n_labels = labels_u.shape[0] unroll_labels_idx = nx.eye(n_labels, type_as=labels_u)[None, labels_idx] def f2(G): G_split = nx.repeat(G.T[:, :, None], n_labels, axis=2) return nx.sum(nx.norm(G_split * unroll_labels_idx, axis=1)) def df2(G): G_split = nx.repeat(G.T[:, :, None], n_labels, axis=2) * unroll_labels_idx W = nx.norm(G_split * unroll_labels_idx, axis=1, keepdims=True) G_norm = G_split / nx.clip(W, 1e-12, None) return nx.sum(G_norm, axis=2).T assert np.allclose(f(G), f2(G)) assert np.allclose(df(G), df2(G)) @pytest.skip_backend("jax") @pytest.skip_backend("tf") def test_sinkhorn_lpl1_vectorization(nx): n_samples, n_labels = 150, 3 rng = np.random.RandomState(42) M = rng.rand(n_samples, n_samples) labels_a = rng.randint(n_labels, size=(n_samples,)) M, labels_a = nx.from_numpy(M), nx.from_numpy(labels_a) # hard-coded params from the original code p, epsilon = 0.5, 1e-3 T = nx.from_numpy(rng.rand(n_samples, n_samples)) def unvectorized(transp): indices_labels = [] classes = nx.unique(labels_a) for c in classes: (idxc,) = nx.where(labels_a == c) indices_labels.append(idxc) W = nx.ones(M.shape, type_as=M) for i, c in enumerate(classes): majs = nx.sum(transp[indices_labels[i]], axis=0) majs = p * ((majs + epsilon) ** (p - 1)) W[indices_labels[i]] = majs return W def vectorized(transp): labels_u, labels_idx = nx.unique(labels_a, return_inverse=True) n_labels = labels_u.shape[0] unroll_labels_idx = nx.eye(n_labels, type_as=transp)[labels_idx] W = ( nx.repeat(transp.T[:, :, None], n_labels, axis=2) * unroll_labels_idx[None, :, :] ) W = nx.sum(W, axis=1) W = p * ((W + epsilon) ** (p - 1)) W = nx.dot(W, unroll_labels_idx.T) return W.T assert np.allclose(unvectorized(T), vectorized(T)) python-pot-0.9.7+dfsg/test/test_dmmot.py000066400000000000000000000045371525521106200203070ustar00rootroot00000000000000"""Tests for ot.lp.dmmot module""" # Author: Ronak Mehta # Xizheng Yu # # License: MIT License import numpy as np import ot def create_test_data(nx): n = 4 a1 = ot.datasets.make_1D_gauss(n, m=20, s=5) a2 = ot.datasets.make_1D_gauss(n, m=60, s=8) A = np.vstack([a1, a2]).T x = np.arange(n, dtype=np.float64).reshape((n, 1)) A, x = nx.from_numpy(A, x) return A, x def test_dmmot_monge_1dgrid_loss(nx): A, x = create_test_data(nx) # Compute loss using dmmot_monge_1dgrid_loss primal_obj = ot.lp.dmmot_monge_1dgrid_loss(A) primal_obj = nx.to_numpy(primal_obj) expected_primal_obj = 0.13667759626298503 np.testing.assert_allclose( primal_obj, expected_primal_obj, rtol=1e-7, err_msg="Test failed: \ Expected different primal objective value", ) # Compute loss using exact OT solver with absolute ground metric A, x = nx.to_numpy(A, x) M = ot.utils.dist(x, metric="cityblock") # absolute ground metric bary, _ = ot.barycenter(A, M, 1e-2, weights=None, verbose=False, log=True) ot_obj = 0.0 for x in A.T: # deal with C-contiguous error from tensorflow backend (not sure why) x = np.ascontiguousarray(x) # compute loss _, log = ot.lp.emd(x, np.array(bary / np.sum(bary)), M, log=True) ot_obj += log["cost"] np.testing.assert_allclose( primal_obj, ot_obj, rtol=1e-7, err_msg="Test failed: \ Expected different primal objective value", ) def test_dmmot_monge_1dgrid_optimize(nx): # test discrete_mmot_converge result A, _ = create_test_data(nx) d = 2 niters = 10 result = ot.lp.dmmot_monge_1dgrid_optimize(A, niters, lr_init=1e-3, lr_decay=1) expected_obj = np.array( [ [0.05553516, 0.13082618, 0.27327479, 0.54036388], [0.04185365, 0.09570724, 0.24384705, 0.61859206], ] ) assert len(result) == d, "Test failed: Expected a list of length n" for i in range(d): np.testing.assert_allclose( result[i], expected_obj[i], atol=1e-7, rtol=1e-7, err_msg="Test failed: \ Expected vectors of all zeros", ) python-pot-0.9.7+dfsg/test/test_dr.py000066400000000000000000000136751525521106200175770ustar00rootroot00000000000000"""Tests for module dr on Dimensionality Reduction""" # Author: Remi Flamary # Minhui Huang # Antoine Collas # # License: MIT License import numpy as np import ot import pytest try: # test if autograd and pymanopt are installed import ot.dr nogo = False except ImportError: nogo = True @pytest.mark.skipif(nogo, reason="Missing modules (autograd or pymanopt)") def test_fda(): n_samples = 90 # nb samples in source and target datasets rng = np.random.RandomState(0) # generate gaussian dataset xs, ys = ot.datasets.make_data_classif("gaussrot", n_samples, random_state=rng) n_features_noise = 8 xs = np.hstack((xs, rng.randn(n_samples, n_features_noise))) p = 1 Pfda, projfda = ot.dr.fda(xs, ys, p) projfda(xs) np.testing.assert_allclose(np.sum(Pfda**2, 0), np.ones(p)) @pytest.mark.skipif(nogo, reason="Missing modules (autograd or pymanopt)") def test_wda(): n_samples = 100 # nb samples in source and target datasets rng = np.random.RandomState(0) # generate gaussian dataset xs, ys = ot.datasets.make_data_classif("gaussrot", n_samples, random_state=rng) n_features_noise = 8 xs = np.hstack((xs, rng.randn(n_samples, n_features_noise))) p = 2 Pwda, projwda = ot.dr.wda(xs, ys, p, maxiter=10) projwda(xs) np.testing.assert_allclose(np.sum(Pwda**2, 0), np.ones(p)) @pytest.mark.skipif(nogo, reason="Missing modules (autograd or pymanopt)") def test_wda_low_reg(): n_samples = 100 # nb samples in source and target datasets rng = np.random.RandomState(0) # generate gaussian dataset xs, ys = ot.datasets.make_data_classif("gaussrot", n_samples, random_state=rng) n_features_noise = 8 xs = np.hstack((xs, rng.randn(n_samples, n_features_noise))) p = 2 Pwda, projwda = ot.dr.wda( xs, ys, p, reg=0.01, maxiter=10, sinkhorn_method="sinkhorn_log" ) projwda(xs) np.testing.assert_allclose(np.sum(Pwda**2, 0), np.ones(p)) @pytest.mark.skipif(nogo, reason="Missing modules (autograd or pymanopt)") def test_wda_normalized(): n_samples = 100 # nb samples in source and target datasets rng = np.random.RandomState(0) # generate gaussian dataset xs, ys = ot.datasets.make_data_classif("gaussrot", n_samples, random_state=rng) n_features_noise = 8 xs = np.hstack((xs, rng.randn(n_samples, n_features_noise))) p = 2 P0 = rng.randn(10, p) P0 /= P0.sum(0, keepdims=True) Pwda, projwda = ot.dr.wda(xs, ys, p, maxiter=10, P0=P0, normalize=True) projwda(xs) np.testing.assert_allclose(np.sum(Pwda**2, 0), np.ones(p)) @pytest.mark.skipif(nogo, reason="Missing modules (autograd or pymanopt)") def test_prw(): d = 100 # Dimension n = 100 # Number samples k = 3 # Subspace dimension dim = 3 def fragmented_hypercube(n, d, dim, rng): assert dim <= d assert dim >= 1 assert dim == int(dim) a = (1.0 / n) * np.ones(n) b = (1.0 / n) * np.ones(n) # First measure : uniform on the hypercube X = rng.uniform(-1, 1, size=(n, d)) # Second measure : fragmentation tmp_y = rng.uniform(-1, 1, size=(n, d)) Y = tmp_y + 2 * np.sign(tmp_y) * np.array(dim * [1] + (d - dim) * [0]) return a, b, X, Y rng = np.random.RandomState(42) a, b, X, Y = fragmented_hypercube(n, d, dim, rng) tau = 0.002 reg = 0.2 pi, U = ot.dr.projection_robust_wasserstein( X, Y, a, b, tau, reg=reg, k=k, maxiter=1000, verbose=1 ) U0 = rng.randn(d, k) U0, _ = np.linalg.qr(U0) pi, U = ot.dr.projection_robust_wasserstein( X, Y, a, b, tau, U0=U0, reg=reg, k=k, maxiter=1000, verbose=1 ) @pytest.mark.skipif(nogo, reason="Missing modules (autograd or pymanopt)") def test_ewca(): d = 5 n_samples = 50 k = 3 rng = np.random.RandomState(0) # generate gaussian dataset A = rng.normal(size=(d, d)) Q, _ = np.linalg.qr(A) D = rng.normal(size=d) D = (D / np.linalg.norm(D)) ** 4 cov = Q @ np.diag(D) @ Q.T X = rng.multivariate_normal(np.zeros(d), cov, size=n_samples) X = X - X.mean(0, keepdims=True) assert X.shape == (n_samples, d) # compute first 3 components with BCD pi, U = ot.dr.ewca( X, reg=0.01, method="BCD", k=k, verbose=1, sinkhorn_method="sinkhorn_log" ) assert pi.shape == (n_samples, n_samples) assert (pi >= 0).all() assert np.allclose(pi.sum(0), 1 / n_samples, atol=1e-3) assert np.allclose(pi.sum(1), 1 / n_samples, atol=1e-3) assert U.shape == (d, k) assert np.allclose(U.T @ U, np.eye(k), atol=1e-3) # test that U contains the principal components U_first_eigvec = np.linalg.svd(X.T, full_matrices=False)[0][:, :k] _, cos, _ = np.linalg.svd(U.T @ U_first_eigvec, full_matrices=False) assert np.allclose(cos, np.ones(k), atol=1e-3) # compute first 3 components with MM pi, U = ot.dr.ewca( X, reg=0.01, method="MM", k=k, verbose=1, sinkhorn_method="sinkhorn_log" ) assert pi.shape == (n_samples, n_samples) assert (pi >= 0).all() assert np.allclose(pi.sum(0), 1 / n_samples, atol=1e-3) assert np.allclose(pi.sum(1), 1 / n_samples, atol=1e-3) assert U.shape == (d, k) assert np.allclose(U.T @ U, np.eye(k), atol=1e-3) # test that U contains the principal components U_first_eigvec = np.linalg.svd(X.T, full_matrices=False)[0][:, :k] _, cos, _ = np.linalg.svd(U.T @ U_first_eigvec, full_matrices=False) assert np.allclose(cos, np.ones(k), atol=1e-3) # compute last 3 components pi, U = ot.dr.ewca( X, reg=100000, method="MM", k=k, verbose=1, sinkhorn_method="sinkhorn_log" ) # test that U contains the last principal components U_last_eigvec = np.linalg.svd(X.T, full_matrices=False)[0][:, -k:] _, cos, _ = np.linalg.svd(U.T @ U_last_eigvec, full_matrices=False) assert np.allclose(cos, np.ones(k), atol=1e-3) python-pot-0.9.7+dfsg/test/test_factored.py000066400000000000000000000030001525521106200207360ustar00rootroot00000000000000"""Tests for main module ot.weak""" # Author: Remi Flamary # # License: MIT License import ot import numpy as np def test_factored_ot(): # test weak ot solver and identity stationary point n = 50 rng = np.random.RandomState(0) xs = rng.randn(n, 2) xt = rng.randn(n, 2) u = ot.utils.unif(n) Ga, Gb, X, log = ot.factored_optimal_transport(xs, xt, u, u, r=10, log=True) # check constraints np.testing.assert_allclose(u, Ga.sum(1)) np.testing.assert_allclose(u, Gb.sum(0)) Ga, Gb, X, log = ot.factored_optimal_transport(xs, xt, u, u, reg=1, r=10, log=True) # check constraints np.testing.assert_allclose(u, Ga.sum(1)) np.testing.assert_allclose(u, Gb.sum(0)) np.testing.assert_allclose(1, log["lazy_plan"][:].sum()) def test_factored_ot_backends(nx): # test weak ot solver for different backends n = 50 rng = np.random.RandomState(0) xs = rng.randn(n, 2) xt = rng.randn(n, 2) u = ot.utils.unif(n) xs2 = nx.from_numpy(xs) xt2 = nx.from_numpy(xt) u2 = nx.from_numpy(u) Ga2, Gb2, X2 = ot.factored_optimal_transport(xs2, xt2, u2, u2, r=10) # check constraints np.testing.assert_allclose(u, nx.to_numpy(Ga2).sum(1)) np.testing.assert_allclose(u, nx.to_numpy(Gb2).sum(0)) Ga2, Gb2, X2 = ot.factored_optimal_transport(xs2, xt2, reg=1, r=10, X0=X2) # check constraints np.testing.assert_allclose(u, nx.to_numpy(Ga2).sum(1)) np.testing.assert_allclose(u, nx.to_numpy(Gb2).sum(0)) python-pot-0.9.7+dfsg/test/test_gaussian.py000066400000000000000000000455671525521106200210110ustar00rootroot00000000000000"""Tests for module gaussian""" # Author: Theo Gnassounou # Remi Flamary # # License: MIT License import numpy as np import pytest import ot from ot.datasets import make_data_classif, make_gauss_hd from ot.utils import is_all_finite, dots def test_bures_wasserstein_mapping(nx): ns = 50 nt = 50 Xs, ys = make_data_classif("3gauss", ns) Xt, yt = make_data_classif("3gauss2", nt) ms = np.mean(Xs, axis=0)[None, :] mt = np.mean(Xt, axis=0)[None, :] Cs = np.cov(Xs.T) Ct = np.cov(Xt.T) Xsb, msb, mtb, Csb, Ctb = nx.from_numpy(Xs, ms, mt, Cs, Ct) A_log, b_log, log = ot.gaussian.bures_wasserstein_mapping( msb, mtb, Csb, Ctb, log=True ) A, b = ot.gaussian.bures_wasserstein_mapping(msb, mtb, Csb, Ctb, log=False) Xst = nx.to_numpy(nx.dot(Xsb, A) + b) Xst_log = nx.to_numpy(nx.dot(Xsb, A_log) + b_log) Cst = np.cov(Xst.T) Cst_log = np.cov(Xst_log.T) np.testing.assert_allclose(Cst_log, Cst, rtol=1e-2, atol=1e-2) np.testing.assert_allclose(Ct, Cst, rtol=1e-2, atol=1e-2) def test_bures_wasserstein_mapping_hd(nx): ns = 100 nt = 100 Xs, Xt, ll = make_gauss_hd( ns, nt, p=50, dim=10, m_diff=5.0, a=(7.0, 7.0), b=(1.0, 1.0) ) ms = ll["ms"] mt = ll["mt"] sigma2_s = ll["sigma2_s"] sigma2_t = ll["sigma2_t"] ls = ll["ls"] lt = ll["lt"] Us = ll["Us"] Ut = ll["Ut"] Cs = ll["Cs"] Ct = ll["Ct"] ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t, Cs, Ct = nx.from_numpy( ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t, Cs, Ct ) A_hd, b_hd = ot.gaussian.bures_wasserstein_mapping_hd( ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t, log=False ) A, b = ot.gaussian.bures_wasserstein_mapping(ms, mt, Cs, Ct, log=False) np.testing.assert_allclose(A_hd, A, rtol=1e-2, atol=1e-2) np.testing.assert_allclose(b_hd, b, rtol=1e-2, atol=1e-2) @pytest.mark.parametrize("bias", [True, False]) def test_empirical_bures_wasserstein_mapping(nx, bias): ns = 50 nt = 50 Xs, ys = make_data_classif("3gauss", ns) Xt, yt = make_data_classif("3gauss2", nt) if not bias: ms = np.mean(Xs, axis=0)[None, :] mt = np.mean(Xt, axis=0)[None, :] Xs = Xs - ms Xt = Xt - mt Xsb, Xtb = nx.from_numpy(Xs, Xt) A, b, log = ot.gaussian.empirical_bures_wasserstein_mapping( Xsb, Xtb, log=True, bias=bias ) A_log, b_log = ot.gaussian.empirical_bures_wasserstein_mapping( Xsb, Xtb, log=False, bias=bias ) Xst = nx.to_numpy(nx.dot(Xsb, A) + b) Xst_log = nx.to_numpy(nx.dot(Xsb, A_log) + b_log) Ct = np.cov(Xt.T) Cst = np.cov(Xst.T) Cst_log = np.cov(Xst_log.T) np.testing.assert_allclose(Cst_log, Cst, rtol=1e-2, atol=1e-2) np.testing.assert_allclose(Ct, Cst, rtol=1e-2, atol=1e-2) @pytest.mark.parametrize("bias", [True, False]) def test_empirical_bures_wasserstein_mapping_hd(nx, bias): ns = 100 nt = 100 id_ = 2 p_ = 30 Xs, Xt, ll = ot.datasets.make_gauss_hd( ns, nt, p=p_, dim=id_, m_diff=5.0, a=(6.0, 6.0), b=(1.0, 1.0) ) # covariance matrix of the target Ct = ll["Ct"] if not bias: ms = np.mean(Xs, axis=0)[None, :] mt = np.mean(Xt, axis=0)[None, :] Xs = Xs - ms Xt = Xt - mt # ds = dt = np.array([id_]) Xs, Xt = nx.from_numpy(Xs, Xt) A, b, log = ot.gaussian.empirical_bures_wasserstein_mapping_hd( Xs, Xt, id_, log=True, bias=bias ) Xst = nx.dot(Xs, A) + b mst = nx.mean(Xst, axis=0)[None, :] Xst = Xst - mst Cst = nx.dot(Xst.T, Xst) / ns eigtp = nx.eigh(Cst) a_tp = eigtp[0][-id_:] sgm2_tp = (nx.trace(Cst) - nx.sum(a_tp)) / (Cst.shape[0] - id_) Qtp = eigtp[1] Utp = Qtp[:, -id_:] ltp = a_tp - sgm2_tp Cst = nx.to_numpy(dots(Utp, nx.diag(ltp), Utp.T) + sgm2_tp * nx.eye(p_)) np.testing.assert_allclose(Ct, Cst, rtol=1.0, atol=1.0) def test_empirical_bures_wasserstein_mapping_numerical_error_warning(): rng = np.random.RandomState(42) Xs = rng.rand(766, 800) * 5 Xt = rng.rand(295, 800) * 2 with pytest.warns(): A, b = ot.gaussian.empirical_bures_wasserstein_mapping(Xs, Xt, reg=1e-8) assert not is_all_finite(A, b) def test_bures_wasserstein_distance(nx): ms, mt = np.array([0]).astype(np.float32), np.array([10]).astype(np.float32) Cs, Ct = np.array([[1]]).astype(np.float32), np.array([[1]]).astype(np.float32) msb, mtb, Csb, Ctb = nx.from_numpy(ms, mt, Cs, Ct) Wb_log, log = ot.gaussian.bures_wasserstein_distance(msb, mtb, Csb, Ctb, log=True) Wb = ot.gaussian.bures_wasserstein_distance(msb, mtb, Csb, Ctb, log=False) Wb2 = ot.gaussian.bures_distance(Csb, Ctb, log=False) np.testing.assert_allclose( nx.to_numpy(Wb_log), nx.to_numpy(Wb), rtol=1e-2, atol=1e-2 ) np.testing.assert_allclose(10, nx.to_numpy(Wb), rtol=1e-2, atol=1e-2) np.testing.assert_allclose(0, Wb2, rtol=1e-2, atol=1e-2) def test_bures_wasserstein_distance_batch(nx): n = 50 k = 2 X = [] y = [] m = [] C = [] for _ in range(k): X_, y_ = make_data_classif("3gauss", n) m_ = np.mean(X_, axis=0)[None, :] C_ = np.cov(X_.T) X.append(X_) y.append(y_) m.append(m_) C.append(C_) m = np.array(m) C = np.array(C) X = nx.from_numpy(*X) m = nx.from_numpy(m) C = nx.from_numpy(C) Wb = ot.gaussian.bures_wasserstein_distance(m[0, 0], m[1, 0], C[0], C[1], log=False) # Test cross vs 1 Wb2 = ot.gaussian.bures_wasserstein_distance( m[0, 0][None], m[1, 0][None], C[0][None], C[1][None] ) np.testing.assert_allclose(nx.to_numpy(Wb), nx.to_numpy(Wb2[0, 0]), atol=1e-5) np.testing.assert_equal(Wb2.shape, (1, 1)) Wb2 = ot.gaussian.bures_wasserstein_distance(m[:, 0], m[1, 0][None], C, C[1][None]) np.testing.assert_allclose(nx.to_numpy(Wb), nx.to_numpy(Wb2[0, 0]), atol=1e-5) np.testing.assert_allclose(0, nx.to_numpy(Wb2[1, 0]), atol=1e-5) np.testing.assert_equal(Wb2.shape, (2, 1)) Wb2 = ot.gaussian.bures_wasserstein_distance(m[:, 0], m[:, 0], C, C) np.testing.assert_allclose(nx.to_numpy(Wb), nx.to_numpy(Wb2[1, 0]), atol=1e-5) np.testing.assert_allclose(nx.to_numpy(Wb), nx.to_numpy(Wb2[0, 1]), atol=1e-5) np.testing.assert_allclose(0, nx.to_numpy(Wb2[0, 0]), atol=1e-5) np.testing.assert_allclose(0, nx.to_numpy(Wb2[1, 1]), atol=1e-5) np.testing.assert_equal(Wb2.shape, (2, 2)) # Test paired Wb3 = ot.gaussian.bures_wasserstein_distance(m[:, 0], m[:, 0], C, C, paired=True) np.testing.assert_allclose(0, nx.to_numpy(Wb3[0]), atol=1e-5) np.testing.assert_allclose(0, nx.to_numpy(Wb3[1]), atol=1e-5) m_rev = np.zeros((k, 2)) C_rev = np.zeros((k, 2, 2)) m_rev[0] = m[1, 0] m_rev[1] = m[0, 0] C_rev[0] = C[1] C_rev[1] = C[0] m_rev = nx.from_numpy(m_rev) C_rev = nx.from_numpy(C_rev) Wb3 = ot.gaussian.bures_wasserstein_distance(m_rev, m[:, 0], C_rev, C, paired=True) np.testing.assert_allclose(nx.to_numpy(Wb2)[0, 1], nx.to_numpy(Wb3)[0], atol=1e-5) np.testing.assert_allclose(nx.to_numpy(Wb2)[0, 1], nx.to_numpy(Wb3)[0], atol=1e-5) with pytest.raises(AssertionError): Wb3 = ot.gaussian.bures_wasserstein_distance(m[0, 0], m[:, 0], C[0], C) @pytest.mark.parametrize("sub_the_same", [True, False]) def test_bures_wasserstein_distance_hd(nx, sub_the_same): ns = 100 nt = 100 m_diff = 4.0 if sub_the_same: Xs, Xt, ll = make_gauss_hd( ns, nt, p=50, dim=10, m_diff=m_diff, a=(7.0, 7.0), b=(1.0, 1.0), sub_the_same=True, ) ms = ll["ms"] mt = ll["mt"] sigma2_s = ll["sigma2_s"] sigma2_t = ll["sigma2_t"] ls = ll["ls"] lt = ll["lt"] Us = ll["Us"] Ut = ll["Ut"] ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t = nx.from_numpy( ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t ) W = ot.gaussian.bures_wasserstein_distance_hd( ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t ) np.testing.assert_allclose(m_diff, W, rtol=1e-4, atol=1e-4) else: Xs, Xt, ll = make_gauss_hd( ns, nt, p=50, dim=10, m_diff=m_diff, a=(7.0, 7.0), b=(1.0, 1.0) ) ms = ll["ms"] mt = ll["mt"] sigma2_s = ll["sigma2_s"] sigma2_t = ll["sigma2_t"] ls = ll["ls"] lt = ll["lt"] Us = ll["Us"] Ut = ll["Ut"] Cs = ll["Cs"] Ct = ll["Ct"] ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t, Cs, Ct = nx.from_numpy( ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t, Cs, Ct ) W = ot.gaussian.bures_wasserstein_distance_hd( ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t ) W_ = ot.gaussian.bures_wasserstein_distance(ms, mt, Cs, Ct) np.testing.assert_allclose(W, W_, rtol=1e-1, atol=1e-1) @pytest.mark.parametrize("bias", [True, False]) def test_empirical_bures_wasserstein_distance(nx, bias): ns = 400 nt = 400 rng = np.random.RandomState(10) Xs = rng.normal(0, 1, ns)[:, np.newaxis] Xt = rng.normal(10 * bias, 1, nt)[:, np.newaxis] Xsb, Xtb = nx.from_numpy(Xs, Xt) Wb_log, log = ot.gaussian.empirical_bures_wasserstein_distance( Xsb, Xtb, log=True, bias=bias ) Wb = ot.gaussian.empirical_bures_wasserstein_distance( Xsb, Xtb, log=False, bias=bias ) np.testing.assert_allclose( nx.to_numpy(Wb_log), nx.to_numpy(Wb), rtol=1e-2, atol=1e-2 ) np.testing.assert_allclose(10 * bias, nx.to_numpy(Wb), rtol=1e-2, atol=1e-2) @pytest.mark.parametrize("bias", [True, False]) def test_empirical_bures_wasserstein_distance_hd(nx, bias): ns = 400 nt = 400 id_ = 2 p_ = 30 if bias: m_diff = 5.0 else: m_diff = 0.0 Xs, Xt, ll = ot.datasets.make_gauss_hd( ns, nt, p=p_, dim=id_, m_diff=m_diff, a=(6.0, 6.0), b=(1.0, 1.0) ) # The actual Wasserstein distance: ms = ll["ms"] mt = ll["mt"] sigma2_s = ll["sigma2_s"] sigma2_t = ll["sigma2_t"] ls = ll["ls"] lt = ll["lt"] Us = ll["Us"] Ut = ll["Ut"] ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t = nx.from_numpy( ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t ) W = ot.gaussian.bures_wasserstein_distance_hd( ms, mt, Us, Ut, ls, lt, sigma2_s, sigma2_t ) # The estimated Wasserstein distance if not bias: ms = np.mean(Xs, axis=0)[None, :] mt = np.mean(Xt, axis=0)[None, :] Xs = Xs - ms Xt = Xt - mt Xsb, Xtb = nx.from_numpy(Xs, Xt) W_, log = ot.gaussian.empirical_bures_wasserstein_distance_hd( Xsb, Xtb, id_, log=True, bias=bias ) np.testing.assert_allclose(W, W_, rtol=1e-1, atol=1e-1) @pytest.mark.parametrize( "method", [ "fixed_point", "gradient_descent", "stochastic_gradient_descent", "averaged_stochastic_gradient_descent", ], ) def test_bures_wasserstein_barycenter(nx, method): n = 30 k = 3 X = [] y = [] m = [] C = [] for _ in range(k): X_, y_ = make_data_classif("3gauss", n, random_state=42 + k) m_ = np.mean(X_, axis=0)[None, :] C_ = np.cov(X_.T) X.append(X_) y.append(y_) m.append(m_) C.append(C_) m = np.array(m) C = np.array(C) X = nx.from_numpy(*X) m = nx.from_numpy(m)[:, 0] C = nx.from_numpy(C) mblog, Cblog, log = ot.gaussian.bures_wasserstein_barycenter( m, C, method=method, log=True, num_iter=10 ) mb, Cb = ot.gaussian.bures_wasserstein_barycenter( m, C, method=method, log=False, num_iter=10 ) np.testing.assert_allclose(Cb, Cblog, rtol=1e-1, atol=1e-1) np.testing.assert_allclose(mb, mblog, rtol=1e-2, atol=1e-2) # Test weights argument weights = nx.ones(k) / k mbw, Cbw = ot.gaussian.bures_wasserstein_barycenter( m, C, weights=weights, method=method, log=False ) np.testing.assert_allclose(Cbw, Cb, rtol=1e-1, atol=1e-1) # test with closed form for diagonal covariance matrices Cdiag = [nx.diag(nx.diag(C[i])) for i in range(k)] Cdiag = nx.stack(Cdiag, axis=0) mbdiag, Cbdiag = ot.gaussian.bures_wasserstein_barycenter( m, Cdiag, method=method, log=False ) Cdiag_sqrt = [nx.sqrtm(C) for C in Cdiag] Cdiag_sqrt = nx.stack(Cdiag_sqrt, axis=0) Cdiag_mean = nx.mean(Cdiag_sqrt, axis=0) Cdiag_cf = Cdiag_mean @ Cdiag_mean np.testing.assert_allclose(Cbdiag, Cdiag_cf, rtol=1e-2, atol=1e-2) def test_fixedpoint_vs_gradientdescent_bures_wasserstein_barycenter(nx): n = 30 k = 3 X = [] y = [] m = [] C = [] for _ in range(k): X_, y_ = make_data_classif("3gauss", n, random_state=42 + k) m_ = np.mean(X_, axis=0)[None, :] C_ = np.cov(X_.T) X.append(X_) y.append(y_) m.append(m_) C.append(C_) m = np.array(m) C = np.array(C) X = nx.from_numpy(*X) m = nx.from_numpy(m)[:, 0] C = nx.from_numpy(C) mb, Cb = ot.gaussian.bures_wasserstein_barycenter( m, C, method="fixed_point", log=False ) mb2, Cb2 = ot.gaussian.bures_wasserstein_barycenter( m, C, method="gradient_descent", log=False ) np.testing.assert_allclose(mb, mb2, atol=1e-5) np.testing.assert_allclose(Cb, Cb2, atol=1e-5) # Test weights argument Cbw = ot.gaussian.bures_barycenter_fixpoint(C, weights=None) Cbw2 = ot.gaussian.bures_barycenter_gradient_descent(C, weights=None) np.testing.assert_allclose(Cbw, Cb, rtol=1e-5, atol=1e-5) np.testing.assert_allclose(Cbw2, Cb2, rtol=1e-5, atol=1e-5) @pytest.mark.parametrize( "method", ["stochastic_gradient_descent", "averaged_stochastic_gradient_descent"] ) def test_stochastic_gd_bures_wasserstein_barycenter(nx, method): n = 30 k = 30 X = [] y = [] m = [] C = [] for _ in range(k): X_, y_ = make_data_classif("3gauss", n) m_ = np.mean(X_, axis=0)[None, :] C_ = np.cov(X_.T) X.append(X_) y.append(y_) m.append(m_) C.append(C_) m = np.array(m) C = np.array(C) X = nx.from_numpy(*X) m = nx.from_numpy(m)[:, 0] C = nx.from_numpy(C) mb, Cb = ot.gaussian.bures_wasserstein_barycenter( m, C, method="fixed_point", log=False, num_iter=100 ) loss = nx.mean(ot.gaussian.bures_wasserstein_distance(mb[None], m, Cb[None], C)) mb2, Cb2 = ot.gaussian.bures_wasserstein_barycenter( m, C, method=method, log=False, batch_size=n, num_iter=100 ) loss2 = nx.mean(ot.gaussian.bures_wasserstein_distance(mb2[None], m, Cb2[None], C)) np.testing.assert_allclose(mb, mb2, atol=1e-5) # atol big for now because too slow, need to see if # it can be improved... np.testing.assert_allclose(Cb, Cb2, atol=1e-0) np.testing.assert_allclose(loss, loss2, atol=1e-2) with pytest.raises(ValueError): mb2, Cb2 = ot.gaussian.bures_wasserstein_barycenter( m, C, method=method, log=False, batch_size=-5 ) def test_not_implemented_method(nx): n = 50 k = 10 X = [] y = [] m = [] C = [] for _ in range(k): X_, y_ = make_data_classif("3gauss", n) m_ = np.mean(X_, axis=0)[None, :] C_ = np.cov(X_.T) X.append(X_) y.append(y_) m.append(m_) C.append(C_) m = np.array(m) C = np.array(C) X = nx.from_numpy(*X) m = nx.from_numpy(m)[:, 0] C = nx.from_numpy(C) not_implemented = "new_method" with pytest.raises(ValueError): mb, Cb = ot.gaussian.bures_wasserstein_barycenter( m, C, method=not_implemented, log=False ) @pytest.mark.parametrize("bias", [True, False]) def test_empirical_bures_wasserstein_barycenter(nx, bias): n = 50 k = 10 X = [] y = [] for _ in range(k): X_, y_ = make_data_classif("3gauss", n) X.append(X_) y.append(y_) X = nx.from_numpy(*X) mblog, Cblog, log = ot.gaussian.empirical_bures_wasserstein_barycenter( X, log=True, bias=bias ) mb, Cb = ot.gaussian.empirical_bures_wasserstein_barycenter(X, log=False, bias=bias) np.testing.assert_allclose(Cb, Cblog, rtol=1e-2, atol=1e-2) np.testing.assert_allclose(mb, mblog, rtol=1e-2, atol=1e-2) @pytest.mark.parametrize("d_target", [1, 2, 3, 10]) def test_gaussian_gromov_wasserstein_distance(nx, d_target): ns = 400 nt = 400 rng = np.random.RandomState(10) Xs, ys = make_data_classif("3gauss", ns, random_state=rng) Xt, yt = make_data_classif("3gauss2", nt, random_state=rng) Xt = np.concatenate((Xt, rng.normal(0, 1, (nt, 8))), axis=1) Xt = Xt[:, 0:d_target].reshape((nt, d_target)) ms = np.mean(Xs, axis=0)[None, :] mt = np.mean(Xt, axis=0)[None, :] Cs = np.cov(Xs.T) Ct = np.cov(Xt.T).reshape((d_target, d_target)) Xsb, Xtb, msb, mtb, Csb, Ctb = nx.from_numpy(Xs, Xt, ms, mt, Cs, Ct) Gb, log = ot.gaussian.gaussian_gromov_wasserstein_distance(Csb, Ctb, log=True) Ge, log = ot.gaussian.empirical_gaussian_gromov_wasserstein_distance( Xsb, Xtb, log=True ) # no log Ge0 = ot.gaussian.empirical_gaussian_gromov_wasserstein_distance( Xsb, Xtb, log=False ) np.testing.assert_allclose(nx.to_numpy(Gb), nx.to_numpy(Ge), rtol=1e-2, atol=1e-2) np.testing.assert_allclose(nx.to_numpy(Ge), nx.to_numpy(Ge0), rtol=1e-2, atol=1e-2) @pytest.mark.parametrize("d_target", [1, 2, 3, 10]) def test_gaussian_gromov_wasserstein_mapping(nx, d_target): ns = 400 nt = 400 rng = np.random.RandomState(10) Xs, ys = make_data_classif("3gauss", ns, random_state=rng) Xt, yt = make_data_classif("3gauss2", nt, random_state=rng) Xt = np.concatenate((Xt, rng.normal(0, 1, (nt, 8))), axis=1) Xt = Xt[:, 0:d_target].reshape((nt, d_target)) ms = np.mean(Xs, axis=0)[None, :] mt = np.mean(Xt, axis=0)[None, :] Cs = np.cov(Xs.T) Ct = np.cov(Xt.T).reshape((d_target, d_target)) Xsb, Xtb, msb, mtb, Csb, Ctb = nx.from_numpy(Xs, Xt, ms, mt, Cs, Ct) A, b, log = ot.gaussian.gaussian_gromov_wasserstein_mapping( msb, mtb, Csb, Ctb, log=True ) Ae, be, loge = ot.gaussian.empirical_gaussian_gromov_wasserstein_mapping( Xsb, Xtb, log=True ) # no log + skewness Ae0, be0 = ot.gaussian.empirical_gaussian_gromov_wasserstein_mapping( Xsb, Xtb, log=False, sign_eigs="skewness" ) Xst = nx.to_numpy(nx.dot(Xsb, A) + b) Cst = np.cov(Xst.T) np.testing.assert_allclose(nx.to_numpy(A), nx.to_numpy(Ae)) if d_target <= 2: np.testing.assert_allclose(Ct, Cst) # test the other way around (target to source) Ai, bi, logi = ot.gaussian.gaussian_gromov_wasserstein_mapping( mtb, msb, Ctb, Csb, log=True ) Xtt = nx.to_numpy(nx.dot(Xtb, Ai) + bi) Ctt = np.cov(Xtt.T) if d_target >= 2: np.testing.assert_allclose(Cs, Ctt) python-pot-0.9.7+dfsg/test/test_gmm.py000066400000000000000000000154441525521106200177460ustar00rootroot00000000000000"""Tests for module gaussian""" # Author: Eloi Tanguy # Remi Flamary # Julie Delon # # License: MIT License import numpy as np import pytest from ot.utils import proj_simplex from ot.gmm import ( gaussian_pdf, gmm_pdf, dist_bures_squared, gmm_ot_loss, gmm_ot_plan, gmm_ot_apply_map, gmm_ot_plan_density, gmm_barycenter_fixed_point, ) try: import torch except ImportError: torch = False def get_gmms(nx=None): rng = np.random.RandomState(seed=42) ks = 3 kt = 5 d = 3 m_s = rng.randn(ks, d) m_t = rng.randn(kt, d) C_s = rng.randn(ks, d, d) C_s = np.matmul(C_s, np.transpose(C_s, (0, 2, 1))) C_t = rng.randn(kt, d, d) C_t = np.matmul(C_t, np.transpose(C_t, (0, 2, 1))) w_s = proj_simplex(rng.rand(ks)) w_t = proj_simplex(rng.rand(kt)) if nx is not None: m_s = nx.from_numpy(m_s) m_t = nx.from_numpy(m_t) C_s = nx.from_numpy(C_s) C_t = nx.from_numpy(C_t) w_s = nx.from_numpy(w_s) w_t = nx.from_numpy(w_t) return m_s, m_t, C_s, C_t, w_s, w_t def test_gaussian_pdf(nx): rng = np.random.RandomState(seed=42) n = 7 d = 3 x = nx.from_numpy(rng.randn(n, d)) m, _, C, _, _, _ = get_gmms(nx) pdf = gaussian_pdf(x, m[0], C[0]) assert pdf.shape == (n,) x = nx.from_numpy(rng.randn(n, n, d)) pdf = gaussian_pdf(x, m[0], C[0]) assert pdf.shape == ( n, n, ) with pytest.raises(AssertionError): gaussian_pdf(x, m[0, :-1], C[0]) def test_gmm_pdf(nx): rng = np.random.RandomState(seed=42) n = 7 d = 3 x = nx.from_numpy(rng.randn(n, d)) m, _, C, _, w, _ = get_gmms(nx) pdf = gmm_pdf(x, m, C, w) assert pdf.shape == (n,) x = nx.from_numpy(rng.randn(n, n, d)) pdf = gmm_pdf(x, m, C, w) assert pdf.shape == ( n, n, ) with pytest.raises(AssertionError): gmm_pdf(x, m[:-1], C, w) @pytest.skip_backend("tf") # skips because of array assignment @pytest.skip_backend("jax") def test_dist_bures_squared(nx): m_s, m_t, C_s, C_t, _, _ = get_gmms(nx) dist_bures_squared(m_s, m_t, C_s, C_t) D0 = dist_bures_squared(m_s, m_s, C_s, C_s) assert np.allclose(np.diag(D0), 0, atol=1e-6) with pytest.raises(AssertionError): dist_bures_squared(m_s[:, 1:], m_t, C_s, C_t) with pytest.raises(AssertionError): dist_bures_squared(m_s[1:], m_t, C_s, C_t) with pytest.raises(AssertionError): dist_bures_squared(m_s, m_t[1:], C_s, C_t) @pytest.skip_backend("tf") # skips because of array assignment @pytest.skip_backend("jax") def test_gmm_ot_loss(nx): m_s, m_t, C_s, C_t, w_s, w_t = get_gmms(nx) loss = gmm_ot_loss(m_s, m_t, C_s, C_t, w_s, w_t) assert loss > 0 loss = gmm_ot_loss(m_s, m_s, C_s, C_s, w_s, w_s) assert np.allclose(loss, 0, atol=1e-6) with pytest.raises(AssertionError): gmm_ot_loss(m_s, m_t, C_s, C_t, w_s[1:], w_t) with pytest.raises(AssertionError): gmm_ot_loss(m_s, m_t, C_s, C_t, w_s, w_t[1:]) @pytest.skip_backend("tf") # skips because of array assignment @pytest.skip_backend("jax") def test_gmm_ot_plan(nx): m_s, m_t, C_s, C_t, w_s, w_t = get_gmms(nx) plan = gmm_ot_plan(m_s, m_t, C_s, C_t, w_s, w_t) assert np.allclose(plan.sum(0), w_t, atol=1e-6) assert np.allclose(plan.sum(1), w_s, atol=1e-6) plan = gmm_ot_plan(m_s, m_s + 1, C_s, C_s, w_s, w_s) assert np.allclose(plan, np.diag(w_s), atol=1e-6) with pytest.raises(AssertionError): gmm_ot_loss(m_s, m_t, C_s, C_t, w_s[1:], w_t) with pytest.raises(AssertionError): gmm_ot_loss(m_s, m_t, C_s, C_t, w_s, w_t[1:]) def test_gmm_apply_map(): m_s, m_t, C_s, C_t, w_s, w_t = get_gmms() rng = np.random.RandomState(seed=42) x = rng.randn(7, 3) for method in ["bary", "rand"]: gmm_ot_apply_map(x, m_s, m_t, C_s, C_t, w_s, w_t, method=method) plan = gmm_ot_plan(m_s, m_t, C_s, C_t, w_s, w_t) gmm_ot_apply_map(x, m_s, m_t, C_s, C_t, w_s, w_t, plan=plan) @pytest.mark.skipif(not torch, reason="No torch available") def test_gradient_gmm_ot_loss_pytorch(): m_s, m_t, C_s, C_t, w_s, w_t = get_gmms() m_s = torch.tensor(m_s, requires_grad=True) m_t = torch.tensor(m_t, requires_grad=True) C_s = torch.tensor(C_s, requires_grad=True) C_t = torch.tensor(C_t, requires_grad=True) w_s = torch.tensor(w_s, requires_grad=True) w_t = torch.tensor(w_t, requires_grad=True) loss = gmm_ot_loss(m_s, m_t, C_s, C_t, w_s, w_t) loss.backward() grad_m_s = m_s.grad grad_C_s = C_s.grad grad_w_s = w_s.grad assert (grad_m_s**2).sum().item() > 0 assert (grad_C_s**2).sum().item() > 0 assert (grad_w_s**2).sum().item() > 0 def test_gmm_ot_plan_density(nx): m_s, m_t, C_s, C_t, w_s, w_t = get_gmms(nx) rng = np.random.RandomState(seed=42) n = 7 x = nx.from_numpy(rng.randn(n, 3)) y = nx.from_numpy(rng.randn(n + 1, 3)) density = gmm_ot_plan_density(x, y, m_s, m_t, C_s, C_t, w_s, w_t) assert density.shape == (n, n + 1) plan = gmm_ot_plan(m_s, m_t, C_s, C_t, w_s, w_t) gmm_ot_plan_density(x, x, m_s, m_t, C_s, C_t, w_s, w_t, plan=plan) with pytest.raises(AssertionError): gmm_ot_plan_density(x[:, 1:], y, m_s, m_t, C_s, C_t, w_s, w_t) @pytest.skip_backend("tf") # skips because of array assignment @pytest.skip_backend("jax") def test_gmm_barycenter_fixed_point(nx): m_s, m_t, C_s, C_t, w_s, w_t = get_gmms(nx) means_list = [m_s, m_t] covs_list = [C_s, C_t] w_list = [w_s, w_t] n_iter = 3 n = m_s.shape[0] # number of components of barycenter means_init = m_s covs_init = C_s weights = nx.ones(2, type_as=m_s) / 2 # barycenter coefficients # with euclidean barycentric projections means, covs = gmm_barycenter_fixed_point( means_list, covs_list, w_list, means_init, covs_init, weights, iterations=n_iter ) # with bures barycentric projections and assigned weights to uniform means_bures_proj, covs_bures_proj, log = gmm_barycenter_fixed_point( means_list, covs_list, w_list, means_init, covs_init, weights, iterations=n_iter, w_bar=nx.ones(n, type_as=m_s) / n, barycentric_proj_method="bures", log=True, ) assert "means_its" in log assert "covs_its" in log assert np.allclose(means, means_bures_proj, atol=1e-6) assert np.allclose(covs, covs_bures_proj, atol=1e-6) with pytest.raises(ValueError): gmm_barycenter_fixed_point( means_list, covs_list, w_list, means_init, covs_init, weights, iterations=n_iter, barycentric_proj_method="unknown", ) python-pot-0.9.7+dfsg/test/test_gnn.py000066400000000000000000000210751525521106200177450ustar00rootroot00000000000000"""Tests for module gnn""" # Author: Sonia Mazelet # Rémi Flamary # # License: MIT License import pytest try: # test if pytorch_geometric is installed import torch_geometric import torch from torch_geometric.nn import Linear from torch_geometric.data import Data as GraphData from torch_geometric.loader import DataLoader import torch.nn as nn from ot.gnn import TFGWPooling, TWPooling except ImportError: torch_geometric = False @pytest.mark.skipif(not torch_geometric, reason="pytorch_geometric not installed") def test_TFGW_optim(): # Test the TFGW layer by passing two graphs through the layer and doing backpropagation. class pooling_TFGW(nn.Module): """ Pooling architecture using the TFGW layer. """ def __init__(self, n_features, n_templates, n_template_nodes): """ Pooling architecture using the TFGW layer. """ super().__init__() self.n_features = n_features self.n_templates = n_templates self.n_template_nodes = n_template_nodes self.TFGW = TFGWPooling( self.n_templates, self.n_template_nodes, self.n_features ) self.linear = Linear(self.n_templates, 1) def forward(self, x, edge_index): x = self.TFGW(x, edge_index) x = self.linear(x) return x torch.manual_seed(0) n_templates = 3 n_template_nodes = 3 n_nodes = 10 n_features = 3 n_epochs = 3 C1 = torch.randint(0, 2, size=(n_nodes, n_nodes)) C2 = torch.randint(0, 2, size=(n_nodes, n_nodes)) edge_index1 = torch.stack(torch.where(C1 == 1)) edge_index2 = torch.stack(torch.where(C2 == 1)) x1 = torch.rand(n_nodes, n_features) x2 = torch.rand(n_nodes, n_features) graph1 = GraphData(x=x1, edge_index=edge_index1, y=torch.tensor([0.0])) graph2 = GraphData(x=x2, edge_index=edge_index2, y=torch.tensor([1.0])) dataset = DataLoader([graph1, graph2], batch_size=1) model_FGW = pooling_TFGW(n_features, n_templates, n_template_nodes) optimizer = torch.optim.Adam(model_FGW.parameters(), lr=0.01) criterion = torch.nn.CrossEntropyLoss() model_FGW.train() for i in range(n_epochs): for data in dataset: out = model_FGW(data.x, data.edge_index) loss = criterion(out, data.y) loss.backward() optimizer.step() optimizer.zero_grad() @pytest.mark.skipif(not torch_geometric, reason="pytorch_geometric not installed") def test_TFGW_variants(): # Test the TFGW layer by passing two graphs through the layer and doing backpropagation. class GNN_pooling(nn.Module): """ Pooling architecture using the TW layer. """ def __init__(self, n_features, n_templates, n_template_nodes, pooling_layer): """ Pooling architecture using the TW layer. """ super().__init__() self.n_features = n_features self.n_templates = n_templates self.n_template_nodes = n_template_nodes self.TFGW = pooling_layer self.linear = Linear(self.n_templates, 1) def forward(self, x, edge_index, batch=None): x = self.TFGW(x, edge_index, batch=batch) x = self.linear(x) return x n_templates = 3 n_template_nodes = 3 n_nodes = 10 n_features = 3 torch.manual_seed(0) C1 = torch.randint(0, 2, size=(n_nodes, n_nodes)) edge_index1 = torch.stack(torch.where(C1 == 1)) x1 = torch.rand(n_nodes, n_features) graph1 = GraphData(x=x1, edge_index=edge_index1, y=torch.tensor([0.0])) batch1 = torch.tensor([1] * n_nodes) batch1[: n_nodes // 2] = 0 criterion = torch.nn.CrossEntropyLoss() for train_node_weights in [True, False]: for alpha in [None, 0, 0.5]: for multi_alpha in [True, False]: model = GNN_pooling( n_features, n_templates, n_template_nodes, pooling_layer=TFGWPooling( n_templates, n_template_nodes, n_features, alpha=alpha, multi_alpha=multi_alpha, train_node_weights=train_node_weights, ), ) # predict out1 = model(graph1.x, graph1.edge_index) loss = criterion(out1, graph1.y) loss.backward() # predict on batch out1 = model(graph1.x, graph1.edge_index, batch1) @pytest.mark.skipif(not torch_geometric, reason="pytorch_geometric not installed") def test_TW_variants(): # Test the TFGW layer by passing two graphs through the layer and doing backpropagation. class GNN_pooling(nn.Module): """ Pooling architecture using the TW layer. """ def __init__(self, n_features, n_templates, n_template_nodes, pooling_layer): """ Pooling architecture using the TW layer. """ super().__init__() self.n_features = n_features self.n_templates = n_templates self.n_template_nodes = n_template_nodes self.TFGW = pooling_layer self.linear = Linear(self.n_templates, 1) def forward(self, x, edge_index, batch=None): x = self.TFGW(x, edge_index, batch=batch) x = self.linear(x) return x n_templates = 3 n_template_nodes = 3 n_nodes = 10 n_features = 3 torch.manual_seed(0) C1 = torch.randint(0, 2, size=(n_nodes, n_nodes)) edge_index1 = torch.stack(torch.where(C1 == 1)) x1 = torch.rand(n_nodes, n_features) graph1 = GraphData(x=x1, edge_index=edge_index1, y=torch.tensor([0.0])) batch1 = torch.tensor([1] * n_nodes) batch1[: n_nodes // 2] = 0 criterion = torch.nn.CrossEntropyLoss() for train_node_weights in [True, False]: model = GNN_pooling( n_features, n_templates, n_template_nodes, pooling_layer=TWPooling( n_templates, n_template_nodes, n_features, train_node_weights=train_node_weights, ), ) out1 = model(graph1.x, graph1.edge_index) loss = criterion(out1, graph1.y) loss.backward() # predict on batch out1 = model(graph1.x, graph1.edge_index, batch1) @pytest.mark.skipif(not torch_geometric, reason="pytorch_geometric not installed") def test_TW(): # Test the TW layer by passing two graphs through the layer and doing backpropagation. class pooling_TW(nn.Module): """ Pooling architecture using the TW layer. """ def __init__(self, n_features, n_templates, n_template_nodes): """ Pooling architecture using the TW layer. """ super().__init__() self.n_features = n_features self.n_templates = n_templates self.n_template_nodes = n_template_nodes self.TFGW = TWPooling( self.n_templates, self.n_template_nodes, self.n_features ) self.linear = Linear(self.n_templates, 1) def forward(self, x, edge_index): x = self.TFGW(x, edge_index) x = self.linear(x) return x torch.manual_seed(0) n_templates = 3 n_template_nodes = 3 n_nodes = 10 n_features = 3 n_epochs = 3 C1 = torch.randint(0, 2, size=(n_nodes, n_nodes)) C2 = torch.randint(0, 2, size=(n_nodes, n_nodes)) edge_index1 = torch.stack(torch.where(C1 == 1)) edge_index2 = torch.stack(torch.where(C2 == 1)) x1 = torch.rand(n_nodes, n_features) x2 = torch.rand(n_nodes, n_features) graph1 = GraphData(x=x1, edge_index=edge_index1, y=torch.tensor([0.0])) graph2 = GraphData(x=x2, edge_index=edge_index2, y=torch.tensor([1.0])) dataset = DataLoader([graph1, graph2], batch_size=1) model_W = pooling_TW(n_features, n_templates, n_template_nodes) optimizer = torch.optim.Adam(model_W.parameters(), lr=0.01) criterion = torch.nn.CrossEntropyLoss() model_W.train() for i in range(n_epochs): for data in dataset: out = model_W(data.x, data.edge_index) loss = criterion(out, data.y) loss.backward() optimizer.step() optimizer.zero_grad() python-pot-0.9.7+dfsg/test/test_helpers.py000066400000000000000000000012071525521106200206200ustar00rootroot00000000000000"""Tests for helpers functions""" # Author: Remi Flamary # # License: MIT License import os import sys sys.path.append(os.path.join("ot", "helpers")) from openmp_helpers import ( get_openmp_flag, check_openmp_support, _get_compiler, compile_test_program, ) # noqa def test_helpers(): compiler = _get_compiler() get_openmp_flag(compiler) s = '#include \n#include \n\nint main(void) {\n\tprintf("Hello world!\\n");\n\treturn 0;\n}' output, _ = compile_test_program(s) assert len(output) == 1 and output[0] == "Hello world!" check_openmp_support() python-pot-0.9.7+dfsg/test/test_lowrank.py000066400000000000000000000177021525521106200206420ustar00rootroot00000000000000"""Test for low rank sinkhorn solvers""" # Author: Laurène DAVID # Titouan Vayer # # License: MIT License import ot import numpy as np import pytest from ot.lowrank import sklearn_import # check sklearn installation def test_nystroem_kernel_approx(): # test nystroem kernel approx in easy regime (nb anchors = nb points) n = 30 d = 3 Xs = np.random.randn(n, d) Xt = np.random.randn(n, d) + 2 sigma = 2.0 K = np.exp(-ot.dist(Xs, Xt) / (2 * sigma**2)) U, V = ot.lowrank.kernel_nystroem(Xs, Xt, anchors=60, sigma=sigma, random_state=42) np.testing.assert_allclose(K, U @ V.T, atol=1e-7) @pytest.mark.parametrize("log, warn", [[False, False], [True, True]]) def test_nystroem_sinkhorn(log, warn): # test Nystrom approximation for Sinkhorn (ot plan) offset = 2 n_samples_per_blob = 50 random_state = 42 std = 0.1 np.random.seed(random_state) centers = np.array( [ [-offset, -offset], # Class 0 - blob 1 [-offset, offset], # Class 0 - blob 2 [offset, -offset], # Class 1 - blob 1 [offset, offset], # Class 1 - blob 2 ] ) X_list = [] y_list = [] for i, center in enumerate(centers): blob_points = np.random.randn(n_samples_per_blob, 2) * std + center label = 0 if i < 2 else 1 X_list.append(blob_points) y_list.append(np.full(n_samples_per_blob, label)) X = np.vstack(X_list) y = np.concatenate(y_list) Xs = X[y == 0] Xt = X[y == 1] reg = 5.0 anchors = 5 res = ot.bregman.empirical_sinkhorn_nystroem( Xs, Xt, anchors=anchors, reg=reg, numItermax=3000, verbose=True, random_state=random_state, log=log, warn=warn, ) if log: G_nys, log_ = res else: G_nys = res G_sinkh = ot.bregman.empirical_sinkhorn( Xs, Xt, reg=reg, numIterMax=3000, verbose=True ) a = ot.unif(Xs.shape[0]) b = ot.unif(Xt.shape[0]) np.testing.assert_allclose(G_sinkh, G_nys[:], atol=1e-04) np.testing.assert_allclose(a, G_nys[:].sum(1), atol=1e-05) np.testing.assert_allclose(b, G_nys[:].sum(0), atol=1e-05) @pytest.mark.parametrize("log", [False, True]) def test_nystroem_sinkhorn2(log): # test Nystrom approximation for Sinkhorn (loss) offset = 2 n_samples_per_blob = 50 random_state = 42 std = 0.1 np.random.seed(random_state) centers = np.array( [ [-offset, -offset], # Class 0 - blob 1 [-offset, offset], # Class 0 - blob 2 [offset, -offset], # Class 1 - blob 1 [offset, offset], # Class 1 - blob 2 ] ) X_list = [] y_list = [] for i, center in enumerate(centers): blob_points = np.random.randn(n_samples_per_blob, 2) * std + center label = 0 if i < 2 else 1 X_list.append(blob_points) y_list.append(np.full(n_samples_per_blob, label)) X = np.vstack(X_list) y = np.concatenate(y_list) Xs = X[y == 0] Xt = X[y == 1] reg = 5.0 anchors = 15 res = ot.bregman.empirical_sinkhorn_nystroem2( Xs, Xt, anchors=anchors, reg=reg, numItermax=3000, verbose=True, random_state=random_state, log=log, ) if log: loss1, log_ = res else: loss1 = res loss2 = ot.bregman.empirical_sinkhorn2( Xs, Xt, reg=reg, numIterMax=3000, verbose=True ) np.testing.assert_allclose(loss1, loss2, atol=1e-07, rtol=1e-3) with pytest.raises(ValueError, match="anchors must"): res = ot.bregman.empirical_sinkhorn_nystroem2( Xs, Xt, anchors=1, reg=reg, numItermax=3000, verbose=True, random_state=random_state, log=log, ) def test_compute_lr_sqeuclidean_matrix(): # test computation of low rank cost matrices M1 and M2 n = 100 X_s = np.reshape(1.0 * np.arange(2 * n), (n, 2)) X_t = np.reshape(1.0 * np.arange(2 * n), (n, 2)) M1, M2 = ot.lowrank.compute_lr_sqeuclidean_matrix(X_s, X_t, rescale_cost=False) M = ot.dist(X_s, X_t, metric="sqeuclidean") # original cost matrix np.testing.assert_allclose(np.dot(M1, M2.T), M, atol=1e-05) def test_lowrank_sinkhorn(): # test low rank sinkhorn n = 100 a = ot.unif(n) b = ot.unif(n) X_s = np.reshape(1.0 * np.arange(n), (n, 1)) X_t = np.reshape(1.0 * np.arange(n), (n, 1)) Q, R, g, log = ot.lowrank.lowrank_sinkhorn( X_s, X_t, a, b, reg=0.1, log=True, rescale_cost=False ) P = log["lazy_plan"][:] value_linear = log["value_linear"] # check constraints for P np.testing.assert_allclose(a, P.sum(1), atol=1e-05) np.testing.assert_allclose(b, P.sum(0), atol=1e-05) # check if lazy_plan is equal to the fully computed plan P_true = np.dot(Q, np.dot(np.diag(1 / g), R.T)) np.testing.assert_allclose(P, P_true, atol=1e-05) # check if value_linear is correct with its original formula M = ot.dist(X_s, X_t, metric="sqeuclidean") value_linear_true = np.sum(M * P_true) np.testing.assert_allclose(value_linear, value_linear_true, atol=1e-05) # check warn parameter when Dykstra algorithm doesn't converge with pytest.warns(UserWarning): ot.lowrank.lowrank_sinkhorn(X_s, X_t, a, b, reg=0.1, stopThr=0, numItermax=1) @pytest.mark.parametrize(("init"), ("random", "deterministic", "kmeans")) def test_lowrank_sinkhorn_init(init): # test lowrank inits n = 100 a = ot.unif(n) b = ot.unif(n) X_s = np.reshape(1.0 * np.arange(n), (n, 1)) X_t = np.reshape(1.0 * np.arange(n), (n, 1)) # test ImportError if init="kmeans" and sklearn not imported if init in ["random", "deterministic"] or ( (init == "kmeans") and (sklearn_import is True) ): Q, R, g, log = ot.lowrank.lowrank_sinkhorn( X_s, X_t, a, b, reg=0.1, init=init, log=True ) P = log["lazy_plan"][:] # check constraints for P np.testing.assert_allclose(a, P.sum(1), atol=1e-05) np.testing.assert_allclose(b, P.sum(0), atol=1e-05) else: with pytest.raises(ImportError): Q, R, g = ot.lowrank.lowrank_sinkhorn(X_s, X_t, a, b, reg=0.1, init=init) @pytest.mark.parametrize(("alpha, rank"), ((0.8, 2), (0.5, 3), (0.2, 6))) def test_lowrank_sinkhorn_alpha_error(alpha, rank): # Test warning for value of alpha n = 100 a = ot.unif(n) b = ot.unif(n) X_s = np.reshape(1.0 * np.arange(n), (n, 1)) X_t = np.reshape(1.0 * np.arange(0, n), (n, 1)) with pytest.raises(ValueError): ot.lowrank.lowrank_sinkhorn( X_s, X_t, a, b, reg=0.1, rank=rank, alpha=alpha, warn=False ) @pytest.mark.parametrize(("gamma_init"), ("rescale", "theory")) def test_lowrank_sinkhorn_gamma_init(gamma_init): # Test lr sinkhorn with different init strategies n = 100 a = ot.unif(n) b = ot.unif(n) X_s = np.reshape(1.0 * np.arange(n), (n, 1)) X_t = np.reshape(1.0 * np.arange(n), (n, 1)) Q, R, g, log = ot.lowrank.lowrank_sinkhorn( X_s, X_t, a, b, reg=0.1, gamma_init=gamma_init, log=True ) P = log["lazy_plan"][:] # check constraints for P np.testing.assert_allclose(a, P.sum(1), atol=1e-05) np.testing.assert_allclose(b, P.sum(0), atol=1e-05) @pytest.skip_backend("tf") def test_lowrank_sinkhorn_backends(nx): # Test low rank sinkhorn for different backends n = 100 a = ot.unif(n) b = ot.unif(n) X_s = np.reshape(1.0 * np.arange(n), (n, 1)) X_t = np.reshape(1.0 * np.arange(0, n), (n, 1)) ab, bb, X_sb, X_tb = nx.from_numpy(a, b, X_s, X_t) Q, R, g, log = ot.lowrank.lowrank_sinkhorn(X_sb, X_tb, ab, bb, reg=0.1, log=True) lazy_plan = log["lazy_plan"] P = lazy_plan[:] np.testing.assert_allclose(ab, P.sum(1), atol=1e-05) np.testing.assert_allclose(bb, P.sum(0), atol=1e-05) python-pot-0.9.7+dfsg/test/test_mapping.py000066400000000000000000000042601525521106200206130ustar00rootroot00000000000000"""Tests for module mapping""" # Author: Eloi Tanguy # # License: MIT License import numpy as np import ot import pytest from ot.backend import to_numpy try: # test if cvxpy is installed import cvxpy # noqa: F401 nocvxpy = False except ImportError: nocvxpy = True @pytest.mark.skipif(nocvxpy, reason="No CVXPY available") def test_ssnb_qcqp_constants(): c1, c2, c3 = ot.mapping._ssnb_qcqp_constants(0.5, 1) np.testing.assert_almost_equal(c1, 1) np.testing.assert_almost_equal(c2, 0.5) np.testing.assert_almost_equal(c3, 1) @pytest.mark.skipif(nocvxpy, reason="No CVXPY available") def test_nearest_brenier_potential_fit(nx): X = nx.ones((2, 2)) phi, G, log = ot.mapping.nearest_brenier_potential_fit(X, X, its=3, log=True) np.testing.assert_almost_equal( to_numpy(G), to_numpy(X) ) # image of source should be close to target # test without log but with X_classes, a, b and other init method a = nx.ones(2) / 2 ot.mapping.nearest_brenier_potential_fit( X, X, X_classes=nx.ones(2), a=a, b=a, its=1, init_method="target" ) @pytest.mark.skipif(nocvxpy, reason="No CVXPY available") def test_brenier_potential_predict_bounds(nx): X = nx.ones((2, 2)) phi, G = ot.mapping.nearest_brenier_potential_fit(X, X, its=3) phi_lu, G_lu, log = ot.mapping.nearest_brenier_potential_predict_bounds( X, phi, G, X, log=True ) # 'new' input isn't new, so should be equal to target np.testing.assert_almost_equal(to_numpy(G_lu[0]), to_numpy(X)) np.testing.assert_almost_equal(to_numpy(G_lu[1]), to_numpy(X)) # test with no log but classes ot.mapping.nearest_brenier_potential_predict_bounds( X, phi, G, X, X_classes=nx.ones(2), Y_classes=nx.ones(2) ) def test_joint_OT_mapping(): """ Complements the tests in test_da, for verbose, log and bias options """ xs = np.array([[0.1, 0.2], [-0.1, 0.3]]) ot.mapping.joint_OT_mapping_kernel(xs, xs, verbose=True) ot.mapping.joint_OT_mapping_linear(xs, xs, verbose=True) ot.mapping.joint_OT_mapping_kernel(xs, xs, log=True, bias=True) ot.mapping.joint_OT_mapping_linear(xs, xs, log=True, bias=True) python-pot-0.9.7+dfsg/test/test_optim.py000066400000000000000000000164351525521106200203170ustar00rootroot00000000000000"""Tests for module optim fro OT optimization""" # Author: Remi Flamary # # License: MIT License import numpy as np import ot def test_conditional_gradient(nx): n_bins = 100 # nb bins # bin positions x = np.arange(n_bins, dtype=np.float64) # Gaussian distributions a = ot.datasets.make_1D_gauss(n_bins, m=20, s=5) # m= mean, s= std b = ot.datasets.make_1D_gauss(n_bins, m=60, s=10) # loss matrix M = ot.dist(x.reshape((n_bins, 1)), x.reshape((n_bins, 1))) M /= M.max() def f(G): return 0.5 * np.sum(G**2) def df(G): return G def fb(G): return 0.5 * nx.sum(G**2) ab, bb, Mb = nx.from_numpy(a, b, M) reg = 1e-1 G, log = ot.optim.cg(a, b, M, reg, f, df, verbose=True, log=True) Gb, log = ot.optim.cg(ab, bb, Mb, reg, fb, df, verbose=True, log=True) Gb = nx.to_numpy(Gb) np.testing.assert_allclose(Gb, G) np.testing.assert_allclose(a, Gb.sum(1)) np.testing.assert_allclose(b, Gb.sum(0)) def test_conditional_gradient_itermax(nx): n = 100 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([4, 4]) cov_t = np.array([[1, -0.8], [-0.8, 1]]) xs = ot.datasets.make_2D_samples_gauss(n, mu_s, cov_s) xt = ot.datasets.make_2D_samples_gauss(n, mu_t, cov_t) a, b = np.ones((n,)) / n, np.ones((n,)) / n # loss matrix M = ot.dist(xs, xt) M /= M.max() def f(G): return 0.5 * np.sum(G**2) def df(G): return G def fb(G): return 0.5 * nx.sum(G**2) ab, bb, Mb = nx.from_numpy(a, b, M) reg = 1e-1 G, log = ot.optim.cg( a, b, M, reg, f, df, numItermaxEmd=10000, verbose=True, log=True ) Gb, log = ot.optim.cg( ab, bb, Mb, reg, fb, df, numItermaxEmd=10000, verbose=True, log=True ) Gb = nx.to_numpy(Gb) np.testing.assert_allclose(Gb, G) np.testing.assert_allclose(a, Gb.sum(1)) np.testing.assert_allclose(b, Gb.sum(0)) def test_generalized_conditional_gradient(nx): n_bins = 100 # nb bins # bin positions x = np.arange(n_bins, dtype=np.float64) # Gaussian distributions a = ot.datasets.make_1D_gauss(n_bins, m=20, s=5) # m= mean, s= std b = ot.datasets.make_1D_gauss(n_bins, m=60, s=10) # loss matrix M = ot.dist(x.reshape((n_bins, 1)), x.reshape((n_bins, 1))) M /= M.max() def f(G): return 0.5 * np.sum(G**2) def df(G): return G def fb(G): return 0.5 * nx.sum(G**2) reg1 = 1e-3 reg2 = 1e-1 ab, bb, Mb = nx.from_numpy(a, b, M) G, log = ot.optim.gcg(a, b, M, reg1, reg2, f, df, verbose=True, log=True) Gb, log = ot.optim.gcg(ab, bb, Mb, reg1, reg2, fb, df, verbose=True, log=True) Gb = nx.to_numpy(Gb) np.testing.assert_allclose(Gb, G, atol=1e-12) np.testing.assert_allclose(a, Gb.sum(1), atol=1e-05) np.testing.assert_allclose(b, Gb.sum(0), atol=1e-05) def test_gcg_no_reg(): n = 100 X = ot.datasets.make_2D_samples_gauss(n, m=[0, 0], sigma=np.eye(2)) Y = ot.datasets.make_2D_samples_gauss(n, m=[1, 1], sigma=np.eye(2)) a = ot.unif(n) b = ot.unif(n) M = ot.dist(X, Y) M /= M.max() eps = 1e-2 G_gcg = ot.optim.gcg( a, b, M, eps, 0.0, lambda x: 0.0, lambda x: np.zeros(x.shape), numInnerItermax=1000, numItermax=10, ) G_sinkhorn = ot.sinkhorn(a, b, M, eps, numItermax=1000) np.testing.assert_allclose(G_gcg, G_sinkhorn, atol=1e-12) np.testing.assert_allclose(a, G_gcg.sum(1), atol=1e-5) np.testing.assert_allclose(b, G_gcg.sum(0), atol=1e-5) def test_solve_1d_linesearch_quad_funct(): np.testing.assert_allclose(ot.optim.solve_1d_linesearch_quad(1, -1), 0.5) np.testing.assert_allclose(ot.optim.solve_1d_linesearch_quad(-1, 5), 0) np.testing.assert_allclose(ot.optim.solve_1d_linesearch_quad(-1, 0.5), 1) def test_line_search_armijo(nx): xk = np.array([[0.25, 0.25], [0.25, 0.25]]) pk = np.array([[-0.25, 0.25], [0.25, -0.25]]) gfk = np.array([[23.04273441, 23.0449082], [23.04273441, 23.0449082]]) old_fval = -123.0 xkb, pkb, gfkb = nx.from_numpy(xk, pk, gfk) def f(x): return 1.0 # Should not throw an exception and return 0. for alpha alpha, a, b = ot.optim.line_search_armijo(f, xkb, pkb, gfkb, old_fval) alpha_np, anp, bnp = ot.optim.line_search_armijo(f, xk, pk, gfk, old_fval) assert a == anp assert b == bnp assert alpha == 0.0 # check line search armijo def f(x): return nx.sum((x - 5.0) ** 2) def grad(x): return 2 * (x - 5.0) xk = nx.from_numpy(np.array([[[-5.0, -5.0]]])) pk = nx.from_numpy(np.array([[[100.0, 100.0]]])) gfk = grad(xk) old_fval = f(xk) # chech the case where the optimum is on the direction alpha, _, _ = ot.optim.line_search_armijo(f, xk, pk, gfk, old_fval) np.testing.assert_allclose(alpha, 0.1) # check the case where the direction is not far enough pk = nx.from_numpy(np.array([[[3.0, 3.0]]])) alpha, _, _ = ot.optim.line_search_armijo(f, xk, pk, gfk, old_fval, alpha0=1.0) np.testing.assert_allclose(alpha, 1.0) # check the case where checking the wrong direction alpha, _, _ = ot.optim.line_search_armijo(f, xk, -pk, gfk, old_fval) assert alpha <= 0 # check the case where the point is not a vector xk = nx.from_numpy(np.array(-5.0)) pk = nx.from_numpy(np.array(100.0)) gfk = grad(xk) old_fval = f(xk) alpha, _, _ = ot.optim.line_search_armijo(f, xk, pk, gfk, old_fval) np.testing.assert_allclose(alpha, 0.1) def test_line_search_armijo_dtype_device(nx): for tp in nx.__type_list__: def f(x): return nx.sum((x - 5.0) ** 2) def grad(x): return 2 * (x - 5.0) xk = np.array([[[-5.0, -5.0]]]) pk = np.array([[[100.0, 100.0]]]) xkb, pkb = nx.from_numpy(xk, pk, type_as=tp) gfkb = grad(xkb) old_fval = f(xkb) # chech the case where the optimum is on the direction alpha, _, fval = ot.optim.line_search_armijo(f, xkb, pkb, gfkb, old_fval) alpha = nx.to_numpy(alpha) np.testing.assert_allclose(alpha, 0.1) nx.assert_same_dtype_device(old_fval, fval) # check the case where the direction is not far enough pk = np.array([[[3.0, 3.0]]]) pkb = nx.from_numpy(pk, type_as=tp) alpha, _, fval = ot.optim.line_search_armijo( f, xkb, pkb, gfkb, old_fval, alpha0=1.0 ) alpha = nx.to_numpy(alpha) np.testing.assert_allclose(alpha, 1.0) nx.assert_same_dtype_device(old_fval, fval) # check the case where checking the wrong direction alpha, _, fval = ot.optim.line_search_armijo(f, xkb, -pkb, gfkb, old_fval) alpha = nx.to_numpy(alpha) assert alpha <= 0 nx.assert_same_dtype_device(old_fval, fval) # check the case where the point is not a vector xkb = nx.from_numpy(np.array(-5.0), type_as=tp) pkb = nx.from_numpy(np.array(100), type_as=tp) gfkb = grad(xkb) old_fval = f(xkb) alpha, _, fval = ot.optim.line_search_armijo(f, xkb, pkb, gfkb, old_fval) alpha = nx.to_numpy(alpha) np.testing.assert_allclose(alpha, 0.1) nx.assert_same_dtype_device(old_fval, fval) python-pot-0.9.7+dfsg/test/test_ot.py000066400000000000000000001176571525521106200176210ustar00rootroot00000000000000"""Tests for main module ot""" # Author: Remi Flamary # # License: MIT License import warnings import numpy as np import pytest import ot from ot.datasets import make_1D_gauss as gauss from ot.backend import torch, tf, get_backend from ot.lp._network_simplex import center_ot_dual def test_emd_dimension_and_mass_mismatch(): # test emd and emd2 for dimension mismatch n_samples = 100 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples, n_features) a = ot.utils.unif(n_samples + 1) M = ot.dist(x, x) np.testing.assert_raises(AssertionError, ot.emd, a, a, M) np.testing.assert_raises(AssertionError, ot.emd2, a, a, M) # test emd and emd2 for mass mismatch a = ot.utils.unif(n_samples) b = a.copy() a[0] = 100 np.testing.assert_raises(AssertionError, ot.emd, a, b, M) np.testing.assert_raises(AssertionError, ot.emd2, a, b, M) def test_emd_backends(nx): n_samples = 100 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples, n_features) y = rng.randn(n_samples, n_features) a = ot.utils.unif(n_samples) M = ot.dist(x, y) G = ot.emd(a, a, M) ab, Mb = nx.from_numpy(a, M) Gb = ot.emd(ab, ab, Mb) np.allclose(G, nx.to_numpy(Gb)) def test_emd2_backends(nx): n_samples = 100 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples, n_features) y = rng.randn(n_samples, n_features) a = ot.utils.unif(n_samples) M = ot.dist(x, y) val = ot.emd2(a, a, M) ab, Mb = nx.from_numpy(a, M) valb = ot.emd2(ab, ab, Mb) # check with empty inputs valb2 = ot.emd2([], [], Mb) np.allclose(val, nx.to_numpy(valb)) np.allclose(val, nx.to_numpy(valb2)) def test_emd_emd2_types_devices(nx): n_samples = 100 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples, n_features) y = rng.randn(n_samples, n_features) a = ot.utils.unif(n_samples) M = ot.dist(x, y) for tp in nx.__type_list__: print(nx.dtype_device(tp)) ab, Mb = nx.from_numpy(a, M, type_as=tp) Gb = ot.emd(ab, ab, Mb) w = ot.emd2(ab, ab, Mb) nx.assert_same_dtype_device(Mb, Gb) nx.assert_same_dtype_device(Mb, w) @pytest.mark.skipif(not tf, reason="tf not installed") def test_emd_emd2_devices_tf(): nx = ot.backend.TensorflowBackend() n_samples = 100 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples, n_features) y = rng.randn(n_samples, n_features) a = ot.utils.unif(n_samples) M = ot.dist(x, y) # Check that everything stays on the CPU with tf.device("/CPU:0"): ab, Mb = nx.from_numpy(a, M) Gb = ot.emd(ab, ab, Mb) w = ot.emd2(ab, ab, Mb) nx.assert_same_dtype_device(Mb, Gb) nx.assert_same_dtype_device(Mb, w) if len(tf.config.list_physical_devices("GPU")) > 0: # Check that everything happens on the GPU ab, Mb = nx.from_numpy(a, M) Gb = ot.emd(ab, ab, Mb) w = ot.emd2(ab, ab, Mb) nx.assert_same_dtype_device(Mb, Gb) nx.assert_same_dtype_device(Mb, w) assert nx.dtype_device(Gb)[1].startswith("GPU") def test_emd2_gradients(): n_samples = 100 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples, n_features) y = rng.randn(n_samples, n_features) a = ot.utils.unif(n_samples) M = ot.dist(x, y) if torch: a1 = torch.tensor(a, requires_grad=True) b1 = torch.tensor(a, requires_grad=True) M1 = torch.tensor(M, requires_grad=True) val, log = ot.emd2(a1, b1, M1, log=True) val.backward() assert a1.shape == a1.grad.shape assert b1.shape == b1.grad.shape assert M1.shape == M1.grad.shape assert np.allclose( a1.grad.cpu().detach().numpy(), log["u"].cpu().detach().numpy() - log["u"].cpu().detach().numpy().mean(), ) assert np.allclose( b1.grad.cpu().detach().numpy(), log["v"].cpu().detach().numpy() - log["v"].cpu().detach().numpy().mean(), ) # Testing for bug #309, checking for scaling of gradient a2 = torch.tensor(a, requires_grad=True) b2 = torch.tensor(a, requires_grad=True) M2 = torch.tensor(M, requires_grad=True) val = 10.0 * ot.emd2(a2, b2, M2) val.backward() assert np.allclose( 10.0 * a1.grad.cpu().detach().numpy(), a2.grad.cpu().detach().numpy() ) assert np.allclose( 10.0 * b1.grad.cpu().detach().numpy(), b2.grad.cpu().detach().numpy() ) assert np.allclose( 10.0 * M1.grad.cpu().detach().numpy(), M2.grad.cpu().detach().numpy() ) def test_emd_emd2(): # test emd and emd2 for simple identity n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) G = ot.emd(u, u, M) # check G is identity np.testing.assert_allclose(G, np.eye(n) / n) # check constraints np.testing.assert_allclose(u, G.sum(1)) # cf convergence sinkhorn np.testing.assert_allclose(u, G.sum(0)) # cf convergence sinkhorn w = ot.emd2(u, u, M) # check loss=0 np.testing.assert_allclose(w, 0) def test_emd2_num_threads_compatibility(): # numThreads is kept as a no-op compatibility parameter. n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) w = ot.emd2(u, u, M) with pytest.warns(DeprecationWarning, match="numThreads"): w2 = ot.emd2(u, u, M, numThreads=2) np.testing.assert_allclose(w, w2) def test_emd_num_threads_deprecation_warning(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) with pytest.warns(DeprecationWarning, match="numThreads"): ot.emd(u, u, M, numThreads=2) def test_emd_empty(): # test emd and emd2 for simple identity n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) G = ot.emd([], [], M) # check G is identity np.testing.assert_allclose(G, np.eye(n) / n) # check constraints np.testing.assert_allclose(u, G.sum(1)) # cf convergence sinkhorn np.testing.assert_allclose(u, G.sum(0)) # cf convergence sinkhorn w = ot.emd2([], [], M) # check loss=0 np.testing.assert_allclose(w, 0) def test_emd2_multi(): n = 500 # nb bins # bin positions x = np.arange(n, dtype=np.float64) # Gaussian distributions a = gauss(n, m=20, s=5) # m= mean, s= std ls = np.arange(20, 500, 100) nb = len(ls) b = np.zeros((n, nb)) for i in range(nb): b[:, i] = gauss(n, m=ls[i], s=10) # loss matrix M = ot.dist(x.reshape((n, 1)), x.reshape((n, 1))) # M/=M.max() print("Computing {} EMD ".format(nb)) # emd loss 1 proc ot.tic() emd1 = ot.emd2(a, b, M, 1) ot.toc("1 proc : {} s") # emd loss multipro proc ot.tic() emdn = ot.emd2(a, b, M) ot.toc("multi proc : {} s") np.testing.assert_allclose(emd1, emdn) # emd loss multipro proc with log ot.tic() emdn = ot.emd2(a, b, M, log=True, return_matrix=True) ot.toc("multi proc : {} s") for i in range(len(emdn)): emd = emdn[i] log = emd[1] cost = emd[0] check_duality_gap(a, b[:, i], M, log["G"], log["u"], log["v"], cost) emdn[i] = cost emdn = np.array(emdn) np.testing.assert_allclose(emd1, emdn) def test_lp_barycenter(): a1 = np.array([1.0, 0, 0])[:, None] a2 = np.array([0, 0, 1.0])[:, None] A = np.hstack((a1, a2)) M = np.array([[0, 1.0, 4.0], [1.0, 0, 1.0], [4.0, 1.0, 0]]) # obvious barycenter between two Diracs bary0 = np.array([0, 1.0, 0]) bary = ot.lp.barycenter(A, M, [0.5, 0.5]) np.testing.assert_allclose(bary, bary0, rtol=1e-5, atol=1e-7) np.testing.assert_allclose(bary.sum(), 1) def test_free_support_barycenter(): measures_locations = [ np.array([-1.0]).reshape((1, 1)), np.array([1.0]).reshape((1, 1)), ] measures_weights = [np.array([1.0]), np.array([1.0])] X_init = np.array([-12.0]).reshape((1, 1)) # obvious barycenter location between two Diracs bar_locations = np.array([0.0]).reshape((1, 1)) X = ot.lp.free_support_barycenter(measures_locations, measures_weights, X_init) np.testing.assert_allclose(X, bar_locations, rtol=1e-5, atol=1e-7) def test_free_support_barycenter_backends(nx): measures_locations = [ np.array([-1.0]).reshape((1, 1)), np.array([1.0]).reshape((1, 1)), ] measures_weights = [np.array([1.0]), np.array([1.0])] X_init = np.array([-12.0]).reshape((1, 1)) X = ot.lp.free_support_barycenter(measures_locations, measures_weights, X_init) measures_locations2 = nx.from_numpy(*measures_locations) measures_weights2 = nx.from_numpy(*measures_weights) X_init2 = nx.from_numpy(X_init) X2 = ot.lp.free_support_barycenter(measures_locations2, measures_weights2, X_init2) np.testing.assert_allclose(X, nx.to_numpy(X2)) def test_generalised_free_support_barycenter(): X = [ np.array([-1.0, -1.0]).reshape((1, 2)), np.array([1.0, 1.0]).reshape((1, 2)), ] # two 2D points bar is obviously 0 a = [np.array([1.0]), np.array([1.0])] P = [np.eye(2), np.eye(2)] Y_init = np.array([-12.0, 7.0]).reshape((1, 2)) # obvious barycenter location between two 2D Diracs Y_true = np.array([0.0, 0.0]).reshape((1, 2)) # test without log and no init Y = ot.lp.generalized_free_support_barycenter(X, a, P, 1) np.testing.assert_allclose(Y, Y_true, rtol=1e-5, atol=1e-7) # test with log and init Y, _ = ot.lp.generalized_free_support_barycenter( X, a, P, 1, Y_init=Y_init, b=np.array([1.0]), log=True ) np.testing.assert_allclose(Y, Y_true, rtol=1e-5, atol=1e-7) def test_generalised_free_support_barycenter_backends(nx): X = [np.array([-1.0]).reshape((1, 1)), np.array([1.0]).reshape((1, 1))] a = [np.array([1.0]), np.array([1.0])] P = [np.array([1.0]).reshape((1, 1)), np.array([1.0]).reshape((1, 1))] Y_init = np.array([-12.0]).reshape((1, 1)) Y = ot.lp.generalized_free_support_barycenter(X, a, P, 1, Y_init=Y_init) X2 = nx.from_numpy(*X) a2 = nx.from_numpy(*a) P2 = nx.from_numpy(*P) Y_init2 = nx.from_numpy(Y_init) Y2 = ot.lp.generalized_free_support_barycenter(X2, a2, P2, 1, Y_init=Y_init2) np.testing.assert_allclose(Y, nx.to_numpy(Y2)) def test_free_support_barycenter_generic_costs(): measures_locations = [ np.array([-1.0]).reshape((1, 1)), np.array([1.0]).reshape((1, 1)), ] measures_weights = [np.array([1.0]), np.array([1.0])] X_init = np.array([-12.0]).reshape((1, 1)) # obvious barycenter location between two Diracs bar_locations = np.array([0.0]).reshape((1, 1)) def cost(x, y): return ot.dist(x, y) cost_list = [cost, cost] def ground_bary(y): out = 0 for yk in y: out += yk / len(y) return out X = ot.lp.free_support_barycenter_generic_costs( measures_locations, measures_weights, X_init, cost_list, ground_bary ) np.testing.assert_allclose(X, bar_locations, rtol=1e-5, atol=1e-7) # test with log and specific weights X2, log = ot.lp.free_support_barycenter_generic_costs( measures_locations, measures_weights, X_init, cost_list, ground_bary, a=ot.unif(1), log=True, ) assert "X_list" in log assert "exit_status" in log assert "diff_list" in log np.testing.assert_allclose(X, X2, rtol=1e-5, atol=1e-7) # test with one iteration for Max Iterations Reached X3, log2 = ot.lp.free_support_barycenter_generic_costs( measures_locations, measures_weights, X_init, cost_list, ground_bary, numItermax=1, log=True, ) assert log2["exit_status"] == "Max iterations reached" # test with a single callable cost X3, log3 = ot.lp.free_support_barycenter_generic_costs( measures_locations, measures_weights, X_init, cost, ground_bary, numItermax=1, log=True, ) # test with no ground_bary but in numpy: requires pytorch backend with pytest.raises(AssertionError): ot.lp.free_support_barycenter_generic_costs( measures_locations, measures_weights, X_init, cost_list, ground_bary=None, numItermax=1, ) # test with unknown method with pytest.raises(AssertionError): ot.lp.free_support_barycenter_generic_costs( measures_locations, measures_weights, X_init, cost_list, ground_bary, numItermax=1, method="unknown_method", ) # test true fixed-point method X4, a4, log4 = ot.lp.free_support_barycenter_generic_costs( measures_locations, measures_weights, X_init, cost_list, ground_bary, numItermax=3, method="true_fixed_point", log=True, ) assert "a_list" in log4 assert X4.shape[0] == a4.shape[0] == 1 np.testing.assert_allclose(a4, ot.unif(1), rtol=1e-5, atol=1e-7) np.testing.assert_allclose(X, X4, rtol=1e-5, atol=1e-7) # test with measure cleaning and no log X5, a5 = ot.lp.free_support_barycenter_generic_costs( measures_locations, measures_weights, X_init, cost_list, ground_bary, numItermax=3, method="true_fixed_point", clean_measure=True, ) np.testing.assert_allclose(a5, ot.unif(1), rtol=1e-5, atol=1e-7) np.testing.assert_allclose(X, X5, rtol=1e-5, atol=1e-7) # test with (too) lax convergence criterion # for Stationary Point exit status X6, log6 = ot.lp.free_support_barycenter_generic_costs( [np.array([-1.0]).reshape((1, 1))], measures_weights, X_init, cost_list, ground_bary, numItermax=3, stopThr=1e20, log=True, ) assert log6["exit_status"] == "Stationary Point" @pytest.mark.skipif(not torch, reason="No torch available") def test_free_support_barycenter_generic_costs_auto_ground_bary(): measures_locations = [ torch.tensor([1.0]).reshape((1, 1)), torch.tensor([2.0]).reshape((1, 1)), ] measures_weights = [torch.tensor([1.0]), torch.tensor([1.0])] X_init = torch.tensor([1.2]).reshape((1, 1)) def cost(x, y): return ot.dist(x, y) cost_list = [cost, cost] def ground_bary(y): out = 0 for yk in y: out += yk / len(y) return out X = ot.lp.free_support_barycenter_generic_costs( measures_locations, measures_weights, X_init, cost_list, ground_bary, numItermax=1, stopThr=-1, ) X2, log2 = ot.lp.free_support_barycenter_generic_costs( measures_locations, measures_weights, X_init, cost_list, ground_bary=None, ground_bary_lr=2e-2, ground_bary_stopThr=1e-20, ground_bary_numItermax=100, numItermax=10, stopThr=-1, log=True, ) np.testing.assert_allclose(X2.numpy(), X.numpy(), rtol=1e-4, atol=1e-4) X3 = ot.lp.free_support_barycenter_generic_costs( measures_locations, measures_weights, X_init, cost_list, ground_bary=None, ground_bary_lr=1e-2, ground_bary_stopThr=1e-20, ground_bary_numItermax=100, numItermax=10, ground_bary_solver="Adam", stopThr=-1, ) np.testing.assert_allclose(X2.numpy(), X3.numpy(), rtol=1e-3, atol=1e-3) # test with (too) lax convergence criterion for ground barycenter ot.lp.free_support_barycenter_generic_costs( measures_locations, measures_weights, X_init, cost_list, ground_bary=None, numItermax=1, ground_bary_stopThr=100, ) @pytest.skip_backend("tf") # skips because of array assignment @pytest.skip_backend("jax") def test_free_support_barycenter_generic_costs_backends(nx): measures_locations = [ np.array([-1.0]).reshape((1, 1)), np.array([1.0]).reshape((1, 1)), ] measures_weights = [np.array([1.0]), np.array([1.0])] X_init = np.array([-12.0]).reshape((1, 1)) def cost(x, y): return ot.dist(x, y) cost_list = [cost, cost] def ground_bary(y): out = 0 for yk in y: out += yk / len(y) return out X = ot.lp.free_support_barycenter_generic_costs( measures_locations, measures_weights, X_init, cost_list, ground_bary, method="L2_barycentric_proj", ) measures_locations2 = nx.from_numpy(*measures_locations) measures_weights2 = nx.from_numpy(*measures_weights) X_init2 = nx.from_numpy(X_init) X2 = ot.lp.free_support_barycenter_generic_costs( measures_locations2, measures_weights2, X_init2, cost_list, ground_bary, method="L2_barycentric_proj", ) np.testing.assert_allclose(X, nx.to_numpy(X2)) X, a = ot.lp.free_support_barycenter_generic_costs( measures_locations, measures_weights, X_init, cost_list, ground_bary, method="true_fixed_point", ) measures_locations2 = nx.from_numpy(*measures_locations) measures_weights2 = nx.from_numpy(*measures_weights) X_init2 = nx.from_numpy(X_init) X2, a2 = ot.lp.free_support_barycenter_generic_costs( measures_locations2, measures_weights2, X_init2, cost_list, ground_bary, method="true_fixed_point", ) np.testing.assert_allclose(a, nx.to_numpy(a2)) np.testing.assert_allclose(X, nx.to_numpy(X2)) ot.lp.ot_barycenter_energy( # test without backend and callable cost measures_locations, measures_weights, X, a, cost, nx=None ) def verify_gluing_validity(gamma, J, w, pi_list): """ Test the validity of the North-West gluing. """ nx = get_backend(gamma) K = len(pi_list) n = pi_list[0].shape[0] nk_list = [pi.shape[1] for pi in pi_list] # Check first marginal a = nx.sum(gamma, axis=tuple(range(1, K + 1))) assert nx.allclose(a, nx.sum(pi_list[0], axis=1)) # Check other marginals for k in range(K): b_k = nx.sum(gamma, axis=tuple(i for i in range(K + 1) if i != k + 1)) assert nx.allclose(b_k, nx.sum(pi_list[k], axis=0)) # Check bi-marginals for k in range(K): gamma_0k = nx.sum(gamma, axis=tuple(i for i in range(1, K + 1) if i != k + 1)) assert nx.allclose(gamma_0k, pi_list[k]) # Check that N <= n + sum_k n_k - K N = J.shape[0] n_k_sum = sum(nk_list) assert N <= n + n_k_sum - K, f"N={N}, n={n}, sum(n_k)={n_k_sum}, K={K}" # Check that w is on the simplex w_sum = nx.sum(w) assert nx.allclose(w_sum, 1), f"Sum of weights w is not 1: {w_sum}" # Check that gamma_1...K and (J, w) are consistent rho = nx.zeros(nk_list, type_as=gamma) for i in range(N): jj = J[i] rho[tuple(jj)] += w[i] gamma_1toK = nx.sum(gamma, axis=0) assert nx.allclose(rho, gamma_1toK), "rho and gamma_1...K are not consistent" def test_north_west_mm_gluing(): rng = np.random.RandomState(0) n = 7 nk_list = [5, 6, 4] a = rng.rand(n) a = a / np.sum(a) b_list = [rng.rand(nk) for nk in nk_list] b_list = [b / np.sum(b) for b in b_list] M_list = [rng.rand(n, nk) for nk in nk_list] pi_list = [ot.emd(a, b, M) for b, M in zip(b_list, M_list)] J, w, log_dict = ot.lp.NorthWestMMGluing(pi_list, log=True) # Test the validity of the gluing gamma = log_dict["gamma"] verify_gluing_validity(gamma, J, w, pi_list) # test without log J2, w2 = ot.lp.NorthWestMMGluing(pi_list, log=False) np.testing.assert_allclose(J, J2) np.testing.assert_allclose(w, w2) # test setting with highly non-injective plans n = 6 a = ot.unif(n) b_list = [a] * 3 pi_list = [a[:, None] @ a[None, :]] * 3 J, w, log_dict = ot.lp.NorthWestMMGluing(pi_list, log=True) # Test the validity of the gluing gamma = log_dict["gamma"] verify_gluing_validity(gamma, J, w, pi_list) @pytest.skip_backend("tf") # skips because of array assignment @pytest.skip_backend("jax") def test_north_west_mm_gluing_backends(nx): rng = np.random.RandomState(0) n = 7 nk_list = [5, 6, 4] a = rng.rand(n) a = a / np.sum(a) b_list = [rng.rand(nk) for nk in nk_list] b_list = [b / np.sum(b) for b in b_list] M_list = [rng.rand(n, nk) for nk in nk_list] pi_list = [ot.emd(a, b, M) for b, M in zip(b_list, M_list)] pi_list2 = [nx.from_numpy(pi) for pi in pi_list] J, w, log_dict = ot.lp.NorthWestMMGluing(pi_list2, log=True, nx=nx) gamma = log_dict["gamma"] # Test equality with numpy solution J_np, w_np, log_dict_np = ot.lp.NorthWestMMGluing(pi_list, log=True) gamma_np = log_dict_np["gamma"] np.testing.assert_allclose(J, J_np) np.testing.assert_allclose(w, w_np) np.testing.assert_allclose(gamma, gamma_np) def test_clean_discrete_measure(nx): a = nx.ones(3) / 3.0 X = nx.from_numpy(np.array([[1.0, 1.0], [1.0, 1.0], [2.0, 2.0]])) X_clean, a_clean = ot.lp._barycenter_solvers._clean_discrete_measure(X, a) a_true = nx.from_numpy(np.array([2 / 3, 1 / 3])) X_true = nx.from_numpy(np.array([[1.0, 1.0], [2.0, 2.0]])) assert a_clean.shape == a_true.shape assert X_clean.shape == X_true.shape np.testing.assert_allclose(a_clean, a_true) np.testing.assert_allclose(X_clean, X_true) a = nx.ones(3) / 3.0 X = nx.from_numpy(np.array([[1.0, 1.0], [2.0, 2.0], [1.0, 1.0]])) X_clean, a_clean = ot.lp._barycenter_solvers._clean_discrete_measure(X, a, nx=nx) a_true = nx.from_numpy(np.array([2 / 3, 1 / 3])) X_true = nx.from_numpy(np.array([[1.0, 1.0], [2.0, 2.0]])) assert a_clean.shape == a_true.shape assert X_clean.shape == X_true.shape np.testing.assert_allclose(a_clean, a_true) np.testing.assert_allclose(X_clean, X_true) n = 5 a = nx.ones(n) / n v = nx.from_numpy(np.array([1.0, 2.0, 3.0])) X = nx.stack([v] * n, axis=0) X_clean, a_clean = ot.lp._barycenter_solvers._clean_discrete_measure(X, a) a_true = np.array([1.0]) X_true = np.array([1.0, 2.0, 3.0]).reshape(1, 3) assert a_clean.shape == a_true.shape assert X_clean.shape == X_true.shape np.testing.assert_allclose(a_clean, a_true) np.testing.assert_allclose(X_clean, X_true) def test_to_int_array(nx): a_np = np.array([1.0, 2.0, 3.0]) a = nx.from_numpy(a_np) a_int = ot.lp._barycenter_solvers._to_int_array(a) a_np_int = a_np.astype(int) np.testing.assert_allclose(nx.to_numpy(a_int), a_np_int) ot.lp._barycenter_solvers._to_int_array(a, nx=nx) @pytest.mark.skipif(not ot.lp._barycenter_solvers.cvxopt, reason="No cvxopt available") def test_lp_barycenter_cvxopt(): a1 = np.array([1.0, 0, 0])[:, None] a2 = np.array([0, 0, 1.0])[:, None] A = np.hstack((a1, a2)) M = np.array([[0, 1.0, 4.0], [1.0, 0, 1.0], [4.0, 1.0, 0]]) # obvious barycenter between two Diracs bary0 = np.array([0, 1.0, 0]) bary = ot.lp.barycenter(A, M, [0.5, 0.5], solver=None) np.testing.assert_allclose(bary, bary0, rtol=1e-5, atol=1e-7) np.testing.assert_allclose(bary.sum(), 1) def test_warnings(): n = 100 # nb bins m = 100 # nb bins mean1 = 30 mean2 = 50 # bin positions x = np.arange(n, dtype=np.float64) y = np.arange(m, dtype=np.float64) # Gaussian distributions a = gauss(n, m=mean1, s=5) # m= mean, s= std b = gauss(m, m=mean2, s=10) # loss matrix M = ot.dist(x.reshape((-1, 1)), y.reshape((-1, 1))) ** (1.0 / 2) print("Computing {} EMD ".format(1)) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") print("Computing {} EMD ".format(1)) ot.emd(a, b, M, numItermax=1) assert "numItermax" in str(w[-1].message) # assert len(w) == 1 def test_dual_variables(): n = 500 # nb bins m = 600 # nb bins mean1 = 300 mean2 = 400 # bin positions x = np.arange(n, dtype=np.float64) y = np.arange(m, dtype=np.float64) # Gaussian distributions a = gauss(n, m=mean1, s=5) # m= mean, s= std b = gauss(m, m=mean2, s=10) # loss matrix M = ot.dist(x.reshape((-1, 1)), y.reshape((-1, 1))) ** (1.0 / 2) print("Computing {} EMD ".format(1)) # emd loss 1 proc ot.tic() G, log = ot.emd(a, b, M, log=True) ot.toc("1 proc : {} s") ot.tic() G2 = ot.emd(b, a, np.ascontiguousarray(M.T)) ot.toc("1 proc : {} s") cost1 = (G * M).sum() # Check symmetry np.testing.assert_array_almost_equal(cost1, (M * G2.T).sum()) # Check with closed-form solution for gaussians np.testing.assert_almost_equal(cost1, np.abs(mean1 - mean2)) # Check that both cost computations are equivalent np.testing.assert_almost_equal(cost1, log["cost"]) check_duality_gap(a, b, M, G, log["u"], log["v"], log["cost"]) constraint_violation = log["u"][:, None] + log["v"][None, :] - M assert constraint_violation.max() < 1e-8 def _get_sparse_test_matrices(n1, n2, k=2, seed=42, nx=None): """Helper function to create sparse and dense test matrices.""" from scipy.sparse import coo_array from ot.backend import NumpyBackend if nx is None: nx = NumpyBackend() rng = np.random.RandomState(seed) M_orig = rng.rand(n1, n2) mask = np.zeros((n1, n2)) for i in range(n1): j_list = rng.choice(n2, min(k, n2), replace=False) for j in j_list: mask[i, j] = 1 for j in range(n2): i_list = rng.choice(n1, min(k, n1), replace=False) for i in i_list: mask[i, j] = 1 M_sparse_np = coo_array(M_orig * mask) rows, cols, data = M_sparse_np.row, M_sparse_np.col, M_sparse_np.data if nx.__name__ == "numpy": M_sparse = M_sparse_np else: rows_b = nx.from_numpy(rows.astype(np.int64)) cols_b = nx.from_numpy(cols.astype(np.int64)) data_b = nx.from_numpy(data) M_sparse = nx.coo_matrix(data_b, rows_b, cols_b, shape=(n1, n2)) M_dense = nx.from_numpy(M_orig + 1e8 * (1 - mask)) return M_sparse, M_dense def test_emd_sparse_vs_dense(nx): """Test that sparse and dense EMD solvers produce identical results. Uses random sparse graphs with k=2 edges per row/column, which guarantees feasibility with uniform marginals. """ # Skip for backends that don't support sparse matrices backend_name = nx.__class__.__name__.lower() if "jax" in backend_name or "tensorflow" in backend_name: pytest.skip("Backend does not support sparse matrices") n1 = 100 n2 = 100 k = 2 M_sparse, M_dense = _get_sparse_test_matrices(n1, n2, k=k, seed=42, nx=nx) a = ot.utils.unif(n1, type_as=M_dense) b = ot.utils.unif(n2, type_as=M_dense) # Solve with both dense and sparse solvers G_dense, log_dense = ot.emd(a, b, M_dense, log=True) G_sparse, log_sparse = ot.emd(a, b, M_sparse, log=True) cost_dense = log_dense["cost"] cost_sparse = log_sparse["cost"] np.testing.assert_allclose(cost_dense, cost_sparse, rtol=1e-5, atol=1e-7) np.testing.assert_allclose(a, G_dense.sum(1), rtol=1e-5, atol=1e-7) np.testing.assert_allclose(b, G_dense.sum(0), rtol=1e-5, atol=1e-7) assert nx.issparse(G_sparse), "Sparse solver should return a sparse matrix" G_sparse_dense = nx.todense(G_sparse) np.testing.assert_allclose( a, nx.to_numpy(nx.sum(G_sparse_dense, 1)), rtol=1e-5, atol=1e-7 ) np.testing.assert_allclose( b, nx.to_numpy(nx.sum(G_sparse_dense, 0)), rtol=1e-5, atol=1e-7 ) # Test coo_array element-wise multiplication (only works with coo_array, not coo_matrix) if nx.__name__ == "numpy": # This tests that we're using coo_array which supports element-wise operations M_sparse_np = M_sparse G_sparse_np = G_sparse loss_sparse = np.sum(G_sparse_np * M_sparse_np) # Verify the loss calculation is reasonable assert loss_sparse >= 0, "Sparse loss should be non-negative" def test_emd2_sparse_vs_dense(nx): """Test that sparse and dense emd2 solvers produce identical costs. Uses random sparse graphs with k=2 edges per row/column, which guarantees feasibility with uniform marginals. """ # Skip for backends that don't support sparse matrices backend_name = nx.__class__.__name__.lower() if "jax" in backend_name or "tensorflow" in backend_name: pytest.skip("Backend does not support sparse matrices") n1 = 100 n2 = 150 k = 2 M_sparse, M_dense = _get_sparse_test_matrices(n1, n2, k=k, seed=43, nx=nx) a = ot.utils.unif(n1, type_as=M_dense) b = ot.utils.unif(n2, type_as=M_dense) # Solve with both dense and sparse solvers cost_dense = ot.emd2(a, b, M_dense) cost_sparse = ot.emd2(a, b, M_sparse) # Check costs match np.testing.assert_allclose(cost_dense, cost_sparse, rtol=1e-5, atol=1e-7) def test_emd2_sparse_gradients(): """Test that PyTorch sparse tensors support gradient computation.""" if not torch: pytest.skip("PyTorch not available") n = 10 a = torch.tensor(ot.utils.unif(n), requires_grad=True, dtype=torch.float64) b = torch.tensor(ot.utils.unif(n), requires_grad=True, dtype=torch.float64) rows, cols, costs = [], [], [] for i in range(n): rows.append(i) cols.append(i) costs.append(0.1) for offset in [1, 2]: j = (i + offset) % n rows.append(i) cols.append(j) costs.append(float(offset)) indices = torch.tensor( np.vstack([np.array(rows), np.array(cols)]), dtype=torch.int64 ) values = torch.tensor(costs, dtype=torch.float64) M_sparse = torch.sparse_coo_tensor(indices, values, (n, n), dtype=torch.float64) cost = ot.emd2(a, b, M_sparse) cost.backward() assert a.grad is not None assert b.grad is not None np.testing.assert_allclose( a.grad.sum().item(), -b.grad.sum().item(), rtol=1e-5, atol=1e-7 ) def test_emd2_sparse_vs_dense_gradients(): """Verify gradient w.r.t. cost matrix M equals transport plan G.""" if not torch: pytest.skip("PyTorch not available") n = 4 a = torch.tensor([0.25, 0.25, 0.25, 0.25], requires_grad=True, dtype=torch.float64) b = torch.tensor([0.25, 0.25, 0.25, 0.25], requires_grad=True, dtype=torch.float64) M_full = torch.tensor( [ [0.1, 1.0, 2.0, 3.0], [1.0, 0.1, 1.0, 2.0], [2.0, 1.0, 0.1, 1.0], [3.0, 2.0, 1.0, 0.1], ], dtype=torch.float64, requires_grad=True, ) cost_dense = ot.emd2(a, b, M_full) cost_dense.backward() G_dense = ot.emd(a.detach(), b.detach(), M_full.detach()) np.testing.assert_allclose( M_full.grad.numpy(), G_dense.numpy(), rtol=1e-7, atol=1e-10 ) a.grad = None b.grad = None rows, cols, costs = [], [], [] for i in range(n): for j in range(max(0, i - 1), min(n, i + 2)): rows.append(i) cols.append(j) costs.append(M_full[i, j].item()) rows_t = torch.tensor(rows, dtype=torch.int64) cols_t = torch.tensor(cols, dtype=torch.int64) M_sparse = torch.sparse_coo_tensor( torch.stack([rows_t, cols_t]), torch.tensor(costs, dtype=torch.float64), (n, n), dtype=torch.float64, requires_grad=True, ) cost_sparse = ot.emd2(a, b, M_sparse) cost_sparse.backward() G_sparse = ot.emd(a.detach(), b.detach(), M_sparse.detach()).to_dense() grad_values = M_sparse.grad.coalesce().values().numpy() G_values = G_sparse[rows_t, cols_t].numpy() np.testing.assert_allclose(grad_values, G_values, rtol=1e-7, atol=1e-10) assert grad_values.sum() > 0 assert np.abs(grad_values.sum() - 1.0) < 1e-7 def test_emd__dual_warmstart(): # Test that warmstarting the network simplex from partial duals n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) y = rng.randn(n, 2) a = ot.utils.unif(n) b = ot.utils.unif(n) M = ot.dist(x, y) # run solver with limited iterations (stop early) G_partial, log_partial = ot.emd(a, b, M, numItermax=100, log=True) u_partial = log_partial["u"] v_partial = log_partial["v"] # resume from the partial duals with full iterations G_warm, log_warm = ot.emd(a, b, M, log=True, potentials_init=(u_partial, v_partial)) # cold-start reference G_cold, log_cold = ot.emd(a, b, M, log=True) # costs should match np.testing.assert_allclose(G_warm, G_cold, rtol=1e-7, atol=1e-10) np.testing.assert_allclose(log_warm["cost"], log_cold["cost"], rtol=1e-7) # Both should satisfy marginal constraints np.testing.assert_allclose(G_warm.sum(1), a, atol=1e-7) np.testing.assert_allclose(G_warm.sum(0), b, atol=1e-7) # Test emd2 with warmstart cost_warm_emd2, log_warm_emd2 = ot.emd2( a, b, M, log=True, potentials_init=(u_partial, v_partial) ) cost_cold_emd2, log_cold_emd2 = ot.emd2(a, b, M, log=True) # costs should match between warmstart and cold start np.testing.assert_allclose(cost_warm_emd2, cost_cold_emd2, rtol=1e-7) np.testing.assert_allclose(cost_warm_emd2, log_cold["cost"], rtol=1e-7) def test_emd_lazy_warmstart(): n = 100 rng = np.random.RandomState(42) X_s = rng.randn(n, 2) X_t = rng.randn(n, 2) a = ot.utils.unif(n) b = ot.utils.unif(n) M = ot.dist(X_s, X_t, metric="sqeuclidean") # run dense solver with limited iterations (stop early) G_partial, log_partial = ot.emd(a, b, M, numItermax=100, log=True) u_partial = log_partial["u"] v_partial = log_partial["v"] from ot.lp import emd2_lazy # resume from the partial duals with full iterations cost_warm, log_warm = emd2_lazy( X_s, X_t, a, b, metric="sqeuclidean", log=True, potentials_init=(u_partial, v_partial), ) # cold-start reference cost_cold, log_cold = emd2_lazy(X_s, X_t, a, b, metric="sqeuclidean", log=True) # costs should match np.testing.assert_allclose(cost_warm, cost_cold, rtol=1e-7) np.testing.assert_allclose(cost_warm, log_cold["cost"], rtol=1e-7) # Check that warmstart returns valid potentials assert "u" in log_warm and "v" in log_warm assert len(log_warm["u"]) == n and len(log_warm["v"]) == n # Test emd2_lazy with warmstart (same pattern as dense emd2 test) cost_warm_emd2, log_warm_emd2 = emd2_lazy( X_s, X_t, a, b, metric="sqeuclidean", log=True, potentials_init=(u_partial, v_partial), ) cost_cold_emd2, log_cold_emd2 = emd2_lazy( X_s, X_t, a, b, metric="sqeuclidean", log=True, ) # costs should match between warmstart and cold start np.testing.assert_allclose(cost_warm_emd2, cost_cold_emd2, rtol=1e-7) np.testing.assert_allclose(cost_warm_emd2, log_cold["cost"], rtol=1e-7) def test_emd2_lazy_returns_sparse_plan(): from scipy.sparse import issparse from ot.lp import emd2_lazy n_s = 12 n_t = 15 rng = np.random.RandomState(42) X_s = rng.randn(n_s, 2) X_t = rng.randn(n_t, 2) a = ot.utils.unif(n_s) b = ot.utils.unif(n_t) M = ot.dist(X_s, X_t, metric="sqeuclidean") cost_dense = ot.emd2(a, b, M) cost_lazy, log_lazy = emd2_lazy( X_s, X_t, a, b, metric="sqeuclidean", log=True, return_matrix=True, ) G_lazy = log_lazy["G"] assert issparse(G_lazy) assert G_lazy.shape == (n_s, n_t) assert G_lazy.nnz <= n_s + n_t np.testing.assert_allclose(cost_lazy, cost_dense, rtol=1e-10, atol=1e-10) G_lazy_dense = G_lazy.toarray() np.testing.assert_allclose(G_lazy_dense.sum(axis=1), a, rtol=1e-6, atol=1e-8) np.testing.assert_allclose(G_lazy_dense.sum(axis=0), b, rtol=1e-6, atol=1e-8) def test_emd_sparse_warmstart(): n = 100 rng = np.random.RandomState(42) a = ot.utils.unif(n) b = ot.utils.unif(n) # Create a cost matrix and solve with limited iterations (stop early) M = rng.rand(n, n) G_partial, log_partial = ot.emd(a, b, M, numItermax=100, log=True) u_partial = log_partial["u"] v_partial = log_partial["v"] # resume from the partial duals with full iterations G_warm, log_warm = ot.emd(a, b, M, log=True, potentials_init=(u_partial, v_partial)) # cold-start reference G_cold, log_cold = ot.emd(a, b, M, log=True) # costs should match np.testing.assert_allclose(G_warm, G_cold, rtol=1e-7, atol=1e-10) np.testing.assert_allclose(log_warm["cost"], log_cold["cost"], rtol=1e-7) # Both should satisfy marginal constraints np.testing.assert_allclose(G_warm.sum(1), a, atol=1e-7) np.testing.assert_allclose(G_warm.sum(0), b, atol=1e-7) # Test sparse solver directly with warmstart via COO format import scipy.sparse as sp from ot.lp.emd_wrap import emd_c_sparse row_indices = np.repeat(np.arange(n), n).astype(np.uint64) col_indices = np.tile(np.arange(n), n).astype(np.uint64) costs = M.ravel() ( flow_sources, flow_targets, flow_values, cost_warm_sparse, alpha_warm, beta_warm, result_code, ) = emd_c_sparse( a, b, row_indices, col_indices, costs, max_iter=100000, alpha_init=u_partial, beta_init=v_partial, ) ( flow_sources_cold, flow_targets_cold, flow_values_cold, cost_cold_sparse, alpha_cold, beta_cold, result_code_cold, ) = emd_c_sparse(a, b, row_indices, col_indices, costs, max_iter=100000) # Costs should match np.testing.assert_allclose(cost_warm_sparse, cost_cold_sparse, rtol=1e-7) np.testing.assert_allclose(cost_warm_sparse, log_cold["cost"], rtol=1e-7) # Check that we got valid flows assert len(flow_sources) > 0, "Should have non-zero flows" assert len(flow_sources) == len(flow_targets) == len(flow_values) # Test emd2 with warmstart for sparse (same pattern as dense emd2 test) cost_warm_emd2, log_warm_emd2 = ot.emd2( a, b, M, log=True, potentials_init=(u_partial, v_partial) ) cost_cold_emd2, log_cold_emd2 = ot.emd2(a, b, M, log=True) # costs should match between warmstart and cold start np.testing.assert_allclose(cost_warm_emd2, cost_cold_emd2, rtol=1e-7) np.testing.assert_allclose(cost_warm_emd2, log_cold["cost"], rtol=1e-7) def check_duality_gap(a, b, M, G, u, v, cost): cost_dual = np.vdot(a, u) + np.vdot(b, v) # Check that dual and primal cost are equal np.testing.assert_almost_equal(cost_dual, cost) [ind1, ind2] = np.nonzero(G) # Check that reduced cost is zero on transport arcs np.testing.assert_array_almost_equal( (M - u.reshape(-1, 1) - v.reshape(1, -1))[ind1, ind2], np.zeros(ind1.size) ) def test_batch_center_ot_dual(nx): n = 100 rng = np.random.RandomState(42) X_s = rng.randn(n, 2) X_t = rng.randn(n, 2) a = ot.utils.unif(n) b = ot.utils.unif(n) X_s, X_t, a, b = nx.from_numpy(X_s, X_t, a, b) M = ot.dist(X_s, X_t, metric="sqeuclidean") # run dense solver with limited iterations (stop early) G, log = ot.emd(a, b, M, log=True) u = log["u"] v = log["v"] alpha, beta = center_ot_dual(u, v) alpha2, beta2 = center_ot_dual(u, v, a, b) np.testing.assert_allclose(alpha, alpha2) np.testing.assert_allclose(beta, beta2) u_concat = nx.concatenate([u[:, None], u[:, None]], axis=1) v_concat = nx.concatenate([v[:, None], v[:, None]], axis=1) alpha_concat, beta_concat = center_ot_dual(u_concat, v_concat) np.testing.assert_allclose(alpha, alpha_concat[:, 0]) np.testing.assert_allclose(beta, beta_concat[:, 0]) python-pot-0.9.7+dfsg/test/test_partial.py000077500000000000000000000433611525521106200206240ustar00rootroot00000000000000"""Tests for module partial""" # Author: # Laetitia Chapel # # License: MIT License import numpy as np import scipy as sp import ot from ot.backend import to_numpy, torch import pytest def test_raise_errors(): n_samples = 20 # nb samples (gaussian) n_noise = 20 # nb of samples (noise) mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 2]]) rng = np.random.RandomState(42) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=rng) xs = np.append(xs, (rng.rand(n_noise, 2) + 1) * 4).reshape((-1, 2)) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=rng) xt = np.append(xt, (rng.rand(n_noise, 2) + 1) * -3).reshape((-1, 2)) M = ot.dist(xs, xt) p = ot.unif(n_samples + n_noise) q = ot.unif(n_samples + n_noise) with pytest.raises(ValueError): ot.partial.partial_wasserstein_lagrange(p + 1, q, M, 1, log=True) with pytest.raises(ValueError): ot.partial.partial_wasserstein(p, q, M, m=2, log=True) with pytest.raises(ValueError): ot.partial.partial_wasserstein(p, q, M, m=-1, log=True) with pytest.raises(ValueError): ot.partial.entropic_partial_wasserstein(p, q, M, reg=1, m=2, log=True) with pytest.raises(ValueError): ot.partial.entropic_partial_wasserstein(p, q, M, reg=1, m=-1, log=True) with pytest.raises(ValueError): ot.partial.partial_gromov_wasserstein(M, M, p, q, m=2, log=True) with pytest.raises(ValueError): ot.partial.partial_gromov_wasserstein(M, M, p, q, m=-1, log=True) with pytest.raises(ValueError): ot.partial.entropic_partial_gromov_wasserstein(M, M, p, q, reg=1, m=2, log=True) with pytest.raises(ValueError): ot.partial.entropic_partial_gromov_wasserstein( M, M, p, q, reg=1, m=-1, log=True ) with pytest.raises(AssertionError): xs_2d = rng.randn(n_samples, 2) xt_2d = rng.randn(n_samples, 2) ot.partial.partial_wasserstein_1d(xs_2d, xt_2d) def test_partial_wasserstein_lagrange(): n_samples = 20 # nb samples (gaussian) n_noise = 20 # nb of samples (noise) mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 2]]) rng = np.random.RandomState(42) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=rng) xs = np.append(xs, (rng.rand(n_noise, 2) + 1) * 4).reshape((-1, 2)) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=rng) xt = np.append(xt, (rng.rand(n_noise, 2) + 1) * -3).reshape((-1, 2)) M = ot.dist(xs, xt) p = ot.unif(n_samples + n_noise) q = ot.unif(n_samples + n_noise) w0, log0 = ot.partial.partial_wasserstein_lagrange(p, q, M, 1, log=True) w0, log0 = ot.partial.partial_wasserstein_lagrange(p, q, M, 100, log=True) def test_partial_wasserstein(nx): n_samples = 20 # nb samples (gaussian) n_noise = 20 # nb of samples (noise) mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 2]]) rng = np.random.RandomState(42) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=rng) xs = np.append(xs, (rng.rand(n_noise, 2) + 1) * 4).reshape((-1, 2)) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=rng) xt = np.append(xt, (rng.rand(n_noise, 2) + 1) * -3).reshape((-1, 2)) M = ot.dist(xs, xt) p = ot.unif(n_samples + n_noise) q = ot.unif(n_samples + n_noise) m = 0.5 p, q, M = nx.from_numpy(p, q, M) w0, log0 = ot.partial.partial_wasserstein(p, q, M, m=m, log=True) w, log = ot.partial.entropic_partial_wasserstein( p, q, M, reg=1, m=m, log=True, verbose=True ) # check constraints np.testing.assert_equal(to_numpy(nx.sum(w0, axis=1) - p) <= 1e-5, [True] * len(p)) np.testing.assert_equal(to_numpy(nx.sum(w0, axis=0) - q) <= 1e-5, [True] * len(q)) np.testing.assert_equal(to_numpy(nx.sum(w0, axis=1) - p) <= 1e-5, [True] * len(p)) np.testing.assert_equal(to_numpy(nx.sum(w0, axis=0) - q) <= 1e-5, [True] * len(q)) # check transported mass np.testing.assert_allclose(np.sum(to_numpy(w0)), m, atol=1e-04) np.testing.assert_allclose(np.sum(to_numpy(w)), m, atol=1e-04) w0, log0 = ot.partial.partial_wasserstein2(p, q, M, m=m, log=True) w0_val = ot.partial.partial_wasserstein2(p, q, M, m=m, log=False) G = log0["T"] np.testing.assert_allclose(w0, w0_val, atol=1e-1, rtol=1e-1) # check constraints np.testing.assert_equal(to_numpy(nx.sum(G, axis=1) - p) <= 1e-5, [True] * len(p)) np.testing.assert_equal(to_numpy(nx.sum(G, axis=0) - q) <= 1e-5, [True] * len(q)) np.testing.assert_allclose(np.sum(to_numpy(G)), m, atol=1e-04) empty_array = nx.zeros(0, type_as=M) w = ot.partial.partial_wasserstein(empty_array, empty_array, M=M, m=None) # check constraints np.testing.assert_equal(to_numpy(nx.sum(w, axis=1) - p) <= 1e-5, [True] * len(p)) np.testing.assert_equal(to_numpy(nx.sum(w, axis=0) - q) <= 1e-5, [True] * len(q)) np.testing.assert_equal(to_numpy(nx.sum(w, axis=1) - p) <= 1e-5, [True] * len(p)) np.testing.assert_equal(to_numpy(nx.sum(w, axis=0) - q) <= 1e-5, [True] * len(q)) # check transported mass np.testing.assert_allclose(np.sum(to_numpy(w)), 1, atol=1e-04) w0 = ot.partial.entropic_partial_wasserstein( empty_array, empty_array, M=M, reg=10, m=None ) # check constraints np.testing.assert_equal(to_numpy(nx.sum(w0, axis=1) - p) <= 1e-5, [True] * len(p)) np.testing.assert_equal(to_numpy(nx.sum(w0, axis=0) - q) <= 1e-5, [True] * len(q)) np.testing.assert_equal(to_numpy(nx.sum(w0, axis=1) - p) <= 1e-5, [True] * len(p)) np.testing.assert_equal(to_numpy(nx.sum(w0, axis=0) - q) <= 1e-5, [True] * len(q)) # check transported mass np.testing.assert_allclose(np.sum(to_numpy(w0)), 1, atol=1e-04) def test_partial_wasserstein2_gradient(): if torch: n_samples = 40 mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 2]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov) M = torch.tensor(ot.dist(xs, xt), requires_grad=True, dtype=torch.float64) p = torch.tensor(ot.unif(n_samples), dtype=torch.float64) q = torch.tensor(ot.unif(n_samples), dtype=torch.float64) m = 0.5 w, log = ot.partial.partial_wasserstein2(p, q, M, m=m, log=True) w.backward() assert M.grad is not None assert M.grad.shape == M.shape def test_entropic_partial_wasserstein_gradient(): if torch: n_samples = 40 mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 2]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov) xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov) M = torch.tensor(ot.dist(xs, xt), requires_grad=True, dtype=torch.float64) p = torch.tensor(ot.unif(n_samples), requires_grad=True, dtype=torch.float64) q = torch.tensor(ot.unif(n_samples), requires_grad=True, dtype=torch.float64) m = 0.5 reg = 1 _, log = ot.partial.entropic_partial_wasserstein( p, q, M, m=m, reg=reg, log=True ) log["partial_w_dist"].backward() assert M.grad is not None assert p.grad is not None assert q.grad is not None assert M.grad.shape == M.shape assert p.grad.shape == p.shape assert q.grad.shape == q.shape def test_partial_gromov_wasserstein(): rng = np.random.RandomState(42) n_samples = 20 # nb samples n_noise = 10 # nb of samples (noise) p = ot.unif(n_samples + n_noise) q = ot.unif(n_samples + n_noise) mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([0, 0, 0]) cov_t = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=rng) xs = np.concatenate((xs, ((rng.rand(n_noise, 2) + 1) * 4)), axis=0) P = sp.linalg.sqrtm(cov_t) xt = rng.randn(n_samples, 3).dot(P) + mu_t xt = np.concatenate((xt, ((rng.rand(n_noise, 3) + 1) * 10)), axis=0) xt2 = xs[::-1].copy() C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) C3 = ot.dist(xt2, xt2) m = 2 / 3 res0, log0 = ot.partial.partial_gromov_wasserstein( C1, C3, p, q, m=m, log=True, verbose=True ) np.testing.assert_allclose(res0, 0, atol=1e-1, rtol=1e-1) C1 = sp.spatial.distance.cdist(xs, xs) C2 = sp.spatial.distance.cdist(xt, xt) m = 1 res0, log0 = ot.partial.partial_gromov_wasserstein(C1, C2, p, q, m=m, log=True) G = ot.gromov.gromov_wasserstein(C1, C2, p, q, "square_loss") np.testing.assert_allclose(G, res0, atol=1e-04) res, log = ot.partial.entropic_partial_gromov_wasserstein( C1, C2, p, q, 10, m=m, log=True ) G = ot.gromov.entropic_gromov_wasserstein(C1, C2, p, q, "square_loss", epsilon=10) np.testing.assert_allclose(G, res, atol=1e-02) w0, log0 = ot.partial.partial_gromov_wasserstein2(C1, C2, p, q, m=m, log=True) w0_val = ot.partial.partial_gromov_wasserstein2(C1, C2, p, q, m=m, log=False) G = log0["T"] np.testing.assert_allclose(w0, w0_val, atol=1e-1, rtol=1e-1) m = 2 / 3 res0, log0 = ot.partial.partial_gromov_wasserstein(C1, C2, p, q, m=m, log=True) res, log = ot.partial.entropic_partial_gromov_wasserstein( C1, C2, p, q, 100, m=m, log=True ) # check constraints np.testing.assert_equal( res0.sum(1) <= p, [True] * len(p) ) # cf convergence wasserstein np.testing.assert_equal( res0.sum(0) <= q, [True] * len(q) ) # cf convergence wasserstein np.testing.assert_allclose(np.sum(res0), m, atol=1e-04) np.testing.assert_equal( res.sum(1) <= p, [True] * len(p) ) # cf convergence wasserstein np.testing.assert_equal( res.sum(0) <= q, [True] * len(q) ) # cf convergence wasserstein np.testing.assert_allclose(np.sum(res), m, atol=1e-04) def test_partial_wasserstein_1d(): n_samples = 20 # nb samples rng = np.random.RandomState(42) xs = rng.randn(n_samples, 1) xt = rng.randn(n_samples, 1) ind_xs_half, ind_xt_half, marginal_costs_half = ot.partial.partial_wasserstein_1d( xs, xt, n_transported_samples=n_samples // 2, p=1 ) ind_xs, ind_xt, marginal_costs = ot.partial.partial_wasserstein_1d(xs, xt, p=1) np.testing.assert_allclose( marginal_costs_half, marginal_costs[: n_samples // 2], atol=1e-04 ) np.testing.assert_allclose( np.sum(np.abs(np.sort(xs[ind_xs_half]) - np.sort(xt[ind_xt_half]))), np.sum(marginal_costs_half), atol=1e-04, ) n = 20 x = np.random.rand(n) y = np.random.rand(n) M = ot.dist(x[:, None], y[:, None], metric="minkowski", p=1) indices_x, indices_y, marginal_costs = ot.partial.partial_wasserstein_1d( x, y, n_transported_samples=n ) costs = np.cumsum(marginal_costs) for i in [1, 5, 10]: np.testing.assert_allclose( costs[i - 1] / n, ot.partial.partial_wasserstein2([], [], M, m=i / n), atol=1e-8, ) t = ot.partial.partial_wasserstein([], [], M, m=i / n) ind_x, ind_y = np.where(t > 1e-6) np.testing.assert_array_equal(np.sort(indices_x[:i]), np.sort(ind_x)) np.testing.assert_array_equal(np.sort(indices_y[:i]), np.sort(ind_y)) # --------------------------------------------------------------------------- # entropic_partial_wasserstein_logscale — new in this PR (rescue of #724) # --------------------------------------------------------------------------- @pytest.mark.parametrize("reg", [10.0, 1.0]) def test_entropic_partial_wasserstein_logscale_matches_old_at_large_reg(reg): """At large reg both solvers are stable; the plans must agree.""" rng = np.random.RandomState(0) n = 20 a = rng.rand(n) a /= a.sum() b = rng.rand(n) b /= b.sum() M = ot.dist(rng.rand(n, 2), rng.rand(n, 2)) m = 0.5 G_old = ot.partial.entropic_partial_wasserstein( a, b, M, reg=reg, m=m, numItermax=2000 ) G_log = ot.partial.entropic_partial_wasserstein_logscale( a, b, M, reg=reg, m=m, numItermax=2000 ) # At reg >= 1.0 the two solvers agree to machine precision; if this # tightens it would indicate the logscale path silently diverged. np.testing.assert_allclose(G_old, G_log, atol=1e-10, rtol=1e-10) np.testing.assert_allclose(G_log.sum(), m, atol=1e-10) @pytest.mark.parametrize("reg", [0.1, 0.05, 0.01, 5e-3, 1e-3, 5e-4]) def test_entropic_partial_wasserstein_logscale_no_nan_at_small_reg(reg): """Issue #723: entropic_partial_wasserstein returns NaN at small reg. The logscale variant introduced by this PR is the fix; check that it stays finite and conserves mass across the regime that breaks the original solver. """ rng = np.random.RandomState(1) n = 50 a = rng.rand(n) a /= a.sum() b = rng.rand(n) b /= b.sum() M = ot.dist(rng.rand(n, 2), rng.rand(n, 2)) * 50.0 # match issue cost scale m = 0.6 G = ot.partial.entropic_partial_wasserstein_logscale( a, b, M, reg=reg, m=m, numItermax=2000 ) assert np.isfinite(G).all(), f"non-finite plan at reg={reg}" np.testing.assert_allclose(G.sum(), m, atol=5e-3) def test_entropic_partial_wasserstein_logscale_approaches_exact_at_small_reg(): """At small `reg` the entropic plan should approach the exact partial OT plan (modulo discretisation). Verifies the fix is mathematically meaningful, not just NaN-free.""" rng = np.random.RandomState(3) n = 30 a = np.ones(n) / n b = np.ones(n) / n M = ot.dist(rng.rand(n, 2), rng.rand(n, 2)) m = 0.5 G_exact = ot.partial.partial_wasserstein(a, b, M, m=m) G_log = ot.partial.entropic_partial_wasserstein_logscale( a, b, M, reg=1e-3, m=m, numItermax=5000 ) cost_exact = float((G_exact * M).sum()) cost_log = float((G_log * M).sum()) # The entropic objective is a relaxation of the exact one, so the # plan-cost gap should be small but non-negative at reg → 0. assert cost_log >= cost_exact - 1e-6 assert ( cost_log - cost_exact < 0.01 ), f"logscale plan cost {cost_log:.4f} diverges from exact {cost_exact:.4f}" def test_entropic_partial_wasserstein_logscale_log_dict(): """`log=True` returns a dict with `err` and `partial_w_dist` keys.""" rng = np.random.RandomState(2) n = 10 a = rng.rand(n) a /= a.sum() b = rng.rand(n) b /= b.sum() M = ot.dist(rng.rand(n, 2), rng.rand(n, 2)) G, log = ot.partial.entropic_partial_wasserstein_logscale( a, b, M, reg=0.1, m=0.5, log=True ) assert "err" in log assert "partial_w_dist" in log assert np.isfinite(G).all() def test_entropic_partial_wasserstein_logscale_input_validation(): """Out-of-range `m` should raise ValueError, matching the unstable solver.""" n = 10 a = np.ones(n) / n b = np.ones(n) / n M = np.ones((n, n)) with pytest.raises(ValueError): ot.partial.entropic_partial_wasserstein_logscale(a, b, M, reg=0.1, m=-1.0) with pytest.raises(ValueError): ot.partial.entropic_partial_wasserstein_logscale(a, b, M, reg=0.1, m=2.0) # --------------------------------------------------------------------------- # entropic_partial_wasserstein method dispatch (sinkhorn / sinkhorn_log) # --------------------------------------------------------------------------- def _partial_problem(seed=7, n=20, m=0.5, scale=1.0): rng = np.random.RandomState(seed) a = rng.rand(n) a /= a.sum() b = rng.rand(n) b /= b.sum() M = ot.dist(rng.rand(n, 2), rng.rand(n, 2)) * scale return a, b, M, m def test_entropic_partial_wasserstein_method_default_is_sinkhorn(): """The default call and ``method='sinkhorn'`` must be identical.""" a, b, M, m = _partial_problem() G_default = ot.partial.entropic_partial_wasserstein(a, b, M, reg=1.0, m=m) G_sinkhorn = ot.partial.entropic_partial_wasserstein( a, b, M, reg=1.0, m=m, method="sinkhorn" ) np.testing.assert_array_equal(G_default, G_sinkhorn) @pytest.mark.parametrize("reg", [1.0, 0.05]) def test_entropic_partial_wasserstein_method_sinkhorn_log_matches_logscale(reg): """``method='sinkhorn_log'`` must dispatch to the standalone logscale solver.""" a, b, M, m = _partial_problem(scale=50.0) G_wrap = ot.partial.entropic_partial_wasserstein( a, b, M, reg=reg, m=m, method="sinkhorn_log", numItermax=2000 ) G_log = ot.partial.entropic_partial_wasserstein_logscale( a, b, M, reg=reg, m=m, numItermax=2000 ) np.testing.assert_array_equal(G_wrap, G_log) def test_entropic_partial_wasserstein_method_is_case_insensitive(): """Method matching follows ``ot.sinkhorn`` and is case-insensitive.""" a, b, M, m = _partial_problem() G_lower = ot.partial.entropic_partial_wasserstein( a, b, M, reg=1.0, m=m, method="sinkhorn_log" ) G_upper = ot.partial.entropic_partial_wasserstein( a, b, M, reg=1.0, m=m, method="Sinkhorn_Log" ) np.testing.assert_array_equal(G_lower, G_upper) def test_entropic_partial_wasserstein_method_log_dict(): """``log=True`` is forwarded to the selected solver for both methods.""" a, b, M, m = _partial_problem(n=10) for method in ("sinkhorn", "sinkhorn_log"): G, log = ot.partial.entropic_partial_wasserstein( a, b, M, reg=0.1, m=m, method=method, log=True ) assert "err" in log assert "partial_w_dist" in log assert np.isfinite(G).all() def test_entropic_partial_wasserstein_method_invalid(): """An unknown method raises ValueError, matching ``ot.sinkhorn``.""" a, b, M, m = _partial_problem(n=10) with pytest.raises(ValueError): ot.partial.entropic_partial_wasserstein( a, b, M, reg=0.1, m=m, method="not_a_solver" ) python-pot-0.9.7+dfsg/test/test_plot.py000066400000000000000000000043661525521106200201450ustar00rootroot00000000000000"""Tests for module plot for visualization""" # Author: Remi Flamary # # License: MIT License import numpy as np import pytest try: # test if matplotlib is installed import matplotlib matplotlib.use("Agg") nogo = False except ImportError: nogo = True @pytest.mark.skipif(nogo, reason="Matplotlib not installed") def test_plot1D_mat(): import ot import ot.plot n_bins = 100 # nb bins # bin positions x = np.arange(n_bins, dtype=np.float64) # Gaussian distributions a = ot.datasets.make_1D_gauss(n_bins, m=20, s=5) # m= mean, s= std b = ot.datasets.make_1D_gauss(n_bins, m=60, s=10) # loss matrix M = ot.dist(x.reshape((n_bins, 1)), x.reshape((n_bins, 1))) M /= M.max() ot.plot.plot1D_mat(a, b, M) ot.plot.plot1D_mat(a, b, M, plot_style="xy") with pytest.raises(AssertionError): ot.plot.plot1D_mat(a, b, M, plot_style="NotAValidStyle") @pytest.mark.skipif(nogo, reason="Matplotlib not installed") def test_rescale_for_imshow_plot(): import ot import ot.plot n = 7 a_x, b_x = -1, 3 x = np.linspace(a_x, b_x, n) a_y, b_y = 2, 6 y = np.linspace(a_y, b_y, n) x_rescaled, y_rescaled = ot.plot.rescale_for_imshow_plot(x, y, n) assert x_rescaled.shape == (n,) assert y_rescaled.shape == (n,) x_rescaled, y_rescaled = ot.plot.rescale_for_imshow_plot( x, y, n, m=n, a_y=a_y + 1, b_y=b_y - 1 ) assert x_rescaled.shape[0] <= n assert y_rescaled.shape[0] <= n with pytest.raises(AssertionError): ot.plot.rescale_for_imshow_plot(x[3:], y, n) @pytest.mark.skipif(nogo, reason="Matplotlib not installed") def test_plot2D_samples_mat(): import ot import ot.plot n_bins = 50 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) mu_t = np.array([4, 4]) cov_t = np.array([[1, -0.8], [-0.8, 1]]) rng = np.random.RandomState(42) xs = ot.datasets.make_2D_samples_gauss(n_bins, mu_s, cov_s, random_state=rng) xt = ot.datasets.make_2D_samples_gauss(n_bins, mu_t, cov_t, random_state=rng) G = 1.0 * (rng.rand(n_bins, n_bins) < 0.01) ot.plot.plot2D_samples_mat(xs, xt, G, thr=1e-5) ot.plot.plot2D_samples_mat(xs, xt, G, thr=1e-5, alpha=0.5) python-pot-0.9.7+dfsg/test/test_regpath.py000066400000000000000000000033011525521106200206050ustar00rootroot00000000000000"""Tests for module regularization path""" # Author: Haoran Wu # # License: MIT License import numpy as np import ot def test_fully_relaxed_path(): n_source = 50 # nb source samples (gaussian) n_target = 40 # nb target samples (gaussian) mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 2]]) rng = np.random.RandomState(0) xs = ot.datasets.make_2D_samples_gauss(n_source, mu, cov, random_state=rng) xt = ot.datasets.make_2D_samples_gauss(n_target, mu, cov, random_state=rng) # source and target distributions a = ot.utils.unif(n_source) b = ot.utils.unif(n_target) # loss matrix M = ot.dist(xs, xt) M /= M.max() t, _, _ = ot.regpath.regularization_path(a, b, M, reg=1e-8, semi_relaxed=False) G = t.reshape((n_source, n_target)) np.testing.assert_allclose(a, G.sum(1), atol=1e-05) np.testing.assert_allclose(b, G.sum(0), atol=1e-05) def test_semi_relaxed_path(): n_source = 50 # nb source samples (gaussian) n_target = 40 # nb target samples (gaussian) mu = np.array([0, 0]) cov = np.array([[1, 0], [0, 2]]) rng = np.random.RandomState(0) xs = ot.datasets.make_2D_samples_gauss(n_source, mu, cov, random_state=rng) xt = ot.datasets.make_2D_samples_gauss(n_target, mu, cov, random_state=rng) # source and target distributions a = ot.utils.unif(n_source) b = ot.utils.unif(n_target) # loss matrix M = ot.dist(xs, xt) M /= M.max() t, _, _ = ot.regpath.regularization_path(a, b, M, reg=1e-8, semi_relaxed=True) G = t.reshape((n_source, n_target)) np.testing.assert_allclose(a, G.sum(1), atol=1e-05) np.testing.assert_allclose(b, G.sum(0), atol=1e-10) python-pot-0.9.7+dfsg/test/test_semidiscrete.py000066400000000000000000000356071525521106200216510ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Tests for ``ot/semidiscrete.py``. We rely on three small toy problems whose optimal semi-dual potential is known in closed form, and check that the solver converges close to that optimum. All tests run across every POT backend (via the ``nx`` fixture). """ # License: MIT License import numpy as np import pytest from ot.backend import get_backend from ot.semidiscrete import ( semidiscrete_atom_weights, semidiscrete_c_transform, semidiscrete_ot_map, solve_semidiscrete, ) N_ITER = 2_000 BATCH_SIZE = 16 TOLERANCE = 0.05 # --------------------------------------------------------------------- # Three toy problems with known optimal potentials. # Each builder returns numpy arrays so we can move them onto any backend. # --------------------------------------------------------------------- def regular_grid_problem(): """Uniform source on ``[0, 1]^3``, 10 target atoms regularly placed on axis 0. By symmetry the optimal centred potential is zero on every atom. """ m, d = 10, 3 target = np.zeros((m, d)) target[:, 0] = (np.arange(m) + 0.5) / m target[:, 1:] = 0.5 weights = np.full(m, 1.0 / m) optimal = np.zeros(m) return target, weights, optimal, 1.0, d, "uniform_cube" def nonuniform_weights_problem(): """Uniform source on ``[0, 1]^3``, fixed support, nonuniform weights. The weights were computed by Monte Carlo so that the optimal centred potential is exactly ``optimal_potential`` (used as a regression target). """ d = 3 target = np.array( [ [0.54488318, 0.4236548, 0.64589411], [0.77815675, 0.87001215, 0.97861834], [0.11827443, 0.63992102, 0.14335329], [0.94466892, 0.52184832, 0.41466194], ] ) weights = np.array([0.3806643, 0.012264, 0.5503486, 0.0567231]) optimal_potential = np.array([0.77423369, 0.61209572, 0.94374808, 0.6818203]) return target, weights, optimal_potential, 1.0, d, "uniform_cube" def shifted_1d_problem(): r"""Uniform source on ``[delta, 1 + delta]``, atoms at ``k/m`` on the line. For this 1D problem with uniform target weights the optimal potential has the closed form .. math:: g^*_j = j \left( \frac{1}{2 m^2} - \frac{\delta}{m} \right), \qquad j = 0, \dots, m - 1. See Appendix E.1 in "Stochastic Optimization in Semi-Discrete Optimal Transport: Convergence Analysis and Minimax Rate", Genans et al. (NeurIPS 2025). """ m, delta = 10, 0.5 target = np.linspace(1 / m, 1.0, m).reshape(m, 1) weights = np.full(m, 1.0 / m) optimal = np.zeros(m) for j in range(1, m): optimal[j] = optimal[j - 1] + 1 / (2 * m * m) - delta / m return target, weights, optimal, 1.0 + delta, 1, ("shifted_1d", delta) ALL_PROBLEMS = [ pytest.param(regular_grid_problem, id="regular_grid"), pytest.param(nonuniform_weights_problem, id="nonuniform_weights"), pytest.param(shifted_1d_problem, id="shifted_1d"), ] # --------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------- def make_sampler(kind, d, nx, target): """Return a backend-aware sampler with a fixed numpy RNG inside.""" rng = np.random.default_rng(0) if kind == "uniform_cube": def sampler(b): return nx.from_numpy(rng.random((b, d)), type_as=target) else: # ("shifted_1d", delta) delta = kind[1] def sampler(b): return nx.from_numpy(delta + rng.random((b, d)), type_as=target) return sampler def centered_l2_error(estimated, reference): """L2 distance between the centred (mean-zero) potentials. The semi-dual is invariant to a global additive constant, so we factor that out. """ estimated = estimated - estimated.mean() reference = reference - reference.mean() return float(np.linalg.norm(estimated - reference)) def half_sqeuclidean(x, y): r"""Backend-aware ``0.5 * ||x - y||^2`` cost, used to pin closed forms. The closed-form optima below are derived for this cost; passing it explicitly keeps the tests valid independently of the default metric. """ nx = get_backend(x, y) diff = x[:, None, :] - y[None, :, :] return 0.5 * nx.sum(diff**2, axis=2) # --------------------------------------------------------------------- # Convergence on every backend # --------------------------------------------------------------------- @pytest.mark.parametrize("build_problem", ALL_PROBLEMS) def test_solve_converges(nx, build_problem): """Plain SGD (no decreasing reg) reaches the optimum on every toy problem.""" target_np, weights_np, optimal, max_cost, d, kind = build_problem() target, weights = nx.from_numpy(target_np, weights_np) sampler = make_sampler(kind, d, nx, target) g = solve_semidiscrete( target, sampler, a_target=weights, metric=half_sqeuclidean, max_iter=N_ITER, batch_size=BATCH_SIZE, decreasing_reg=False, max_cost=max_cost, ) err = centered_l2_error(nx.to_numpy(g), optimal) assert err < TOLERANCE, f"err={err:.4f}" @pytest.mark.parametrize("build_problem", ALL_PROBLEMS) def test_drag_converges(nx, build_problem): """DRAG (decreasing entropic reg) reaches the optimum on every toy problem.""" target_np, weights_np, optimal, max_cost, d, kind = build_problem() target, weights = nx.from_numpy(target_np, weights_np) sampler = make_sampler(kind, d, nx, target) g = solve_semidiscrete( target, sampler, a_target=weights, metric=half_sqeuclidean, max_iter=N_ITER, batch_size=BATCH_SIZE, decreasing_reg=True, max_cost=max_cost, ) err = centered_l2_error(nx.to_numpy(g), optimal) assert err < TOLERANCE, f"err={err:.4f}" # --------------------------------------------------------------------- # Entropic regime # --------------------------------------------------------------------- def test_entropic_solver_runs(nx): """In the entropic regime, the solver and ``semidiscrete_c_transform`` produce finite values.""" target_np, weights_np, _, max_cost, d, kind = regular_grid_problem() target, weights = nx.from_numpy(target_np, weights_np) sampler = make_sampler(kind, d, nx, target) g = solve_semidiscrete( target, sampler, a_target=weights, reg=0.05, max_iter=N_ITER, batch_size=BATCH_SIZE, max_cost=max_cost, ) samples = sampler(64) phi = semidiscrete_c_transform(target, samples, g, a_target=weights, reg=0.05) assert np.isfinite(nx.to_numpy(g)).all() assert np.isfinite(nx.to_numpy(phi)).all() # --------------------------------------------------------------------- # Custom cost # --------------------------------------------------------------------- def test_string_metric_runs(nx): """A string metric is routed through ``ot.dist`` and yields finite output.""" target_np, weights_np, _, max_cost, d, kind = regular_grid_problem() target, weights = nx.from_numpy(target_np, weights_np) sampler = make_sampler(kind, d, nx, target) g = solve_semidiscrete( target, sampler, a_target=weights, metric="euclidean", max_iter=N_ITER, batch_size=BATCH_SIZE, max_cost=max_cost, ) samples = sampler(32) phi = semidiscrete_c_transform( target, samples, g, a_target=weights, metric="euclidean" ) assert np.isfinite(nx.to_numpy(g)).all() assert np.isfinite(nx.to_numpy(phi)).all() # --------------------------------------------------------------------- # Built-in samplers # --------------------------------------------------------------------- @pytest.mark.parametrize("name", ["unif", "unif_cube", "ball", "unif_ball", "normal"]) def test_string_sampler_runs(nx, name): """Each built-in string sampler yields a finite potential of the right shape.""" target_np, weights_np, _, max_cost, d, kind = regular_grid_problem() target, weights = nx.from_numpy(target_np, weights_np) g = solve_semidiscrete( target, name, a_target=weights, max_iter=200, batch_size=BATCH_SIZE, max_cost=max_cost, ) assert nx.to_numpy(g).shape == (target_np.shape[0],) assert np.isfinite(nx.to_numpy(g)).all() def test_default_sampler_unif_converges(nx): """The default ``sampler_source='unif'`` matches the uniform-cube problem.""" target_np, weights_np, optimal, max_cost, d, kind = regular_grid_problem() target, weights = nx.from_numpy(target_np, weights_np) g = solve_semidiscrete( target, a_target=weights, metric=half_sqeuclidean, max_iter=N_ITER, batch_size=BATCH_SIZE, max_cost=max_cost, ) err = centered_l2_error(nx.to_numpy(g), optimal) assert err < TOLERANCE, f"err={err:.4f}" def test_unknown_sampler_raises(nx): """An unknown sampler string is rejected with ``ValueError``.""" target_np, weights_np, _, _, _, _ = regular_grid_problem() target, _ = nx.from_numpy(target_np, weights_np) with pytest.raises(ValueError): solve_semidiscrete(target, "nope", max_iter=10) # --------------------------------------------------------------------- # Helper functions # --------------------------------------------------------------------- @pytest.mark.parametrize("reg", [0.0, 0.1]) def test_atom_weights_are_row_stochastic(nx, reg): """``semidiscrete_atom_weights`` returns nonnegative weights that sum to 1 per row.""" target_np, weights_np, _, _, d, kind = nonuniform_weights_problem() target, weights = nx.from_numpy(target_np, weights_np) sampler = make_sampler(kind, d, nx, target) samples = sampler(32) g = nx.zeros((target_np.shape[0],), type_as=target) w = semidiscrete_atom_weights(target, samples, g, a_target=weights, reg=reg) w_np = nx.to_numpy(w) assert w_np.shape == (32, target_np.shape[0]) assert (w_np >= 0).all() np.testing.assert_allclose(w_np.sum(axis=1), 1.0, atol=1e-10) def test_ot_map_shape_and_finiteness(nx): """``semidiscrete_ot_map`` returns finite values with the source-sample shape.""" target_np, weights_np, _, _, d, kind = regular_grid_problem() target, weights = nx.from_numpy(target_np, weights_np) sampler = make_sampler(kind, d, nx, target) samples = sampler(16) g = nx.zeros((target_np.shape[0],), type_as=target) transported = semidiscrete_ot_map(target, samples, g, a_target=weights) transported_np = nx.to_numpy(transported) samples_np = nx.to_numpy(samples) assert transported_np.shape == samples_np.shape assert np.isfinite(transported_np).all() def test_c_transform_minimum_for_zero_potential(nx): """At ``g = 0``, ``phi_g(x) = -max_j(-c(x, y_j)) = min_j c(x, y_j)``.""" target_np, weights_np, _, _, d, kind = regular_grid_problem() target, weights = nx.from_numpy(target_np, weights_np) sampler = make_sampler(kind, d, nx, target) samples = sampler(8) g = nx.zeros((target_np.shape[0],), type_as=target) phi = semidiscrete_c_transform( target, samples, g, a_target=weights, metric=half_sqeuclidean ) samples_np = nx.to_numpy(samples) target_np = nx.to_numpy(target) diff = samples_np[:, None, :] - target_np[None, :, :] expected = 0.5 * (diff**2).sum(axis=2).min(axis=1) np.testing.assert_allclose(nx.to_numpy(phi), expected, atol=1e-10) # --------------------------------------------------------------------- # Solver options # --------------------------------------------------------------------- def test_warm_start_converges(nx): """Splitting one run into two warm-started halves still converges.""" target_np, weights_np, optimal, max_cost, d, kind = regular_grid_problem() target, weights = nx.from_numpy(target_np, weights_np) sampler = make_sampler(kind, d, nx, target) half = solve_semidiscrete( target, sampler, a_target=weights, metric=half_sqeuclidean, max_iter=N_ITER // 2, batch_size=BATCH_SIZE, max_cost=max_cost, ) g = solve_semidiscrete( target, sampler, a_target=weights, metric=half_sqeuclidean, max_iter=N_ITER // 2, batch_size=BATCH_SIZE, init_potential=half, max_cost=max_cost, ) err = centered_l2_error(nx.to_numpy(g), optimal) assert err < TOLERANCE, f"err={err:.4f}" def test_init_potential_is_not_mutated(nx): """The ``init_potential`` array passed by the caller is left intact.""" target_np, weights_np, _, max_cost, d, kind = regular_grid_problem() target, weights = nx.from_numpy(target_np, weights_np) sampler = make_sampler(kind, d, nx, target) init_np = np.full(target_np.shape[0], 0.5) init = nx.from_numpy(init_np, type_as=target) snapshot = nx.to_numpy(init).copy() solve_semidiscrete( target, sampler, a_target=weights, init_potential=init, max_iter=10, batch_size=4, max_cost=max_cost, ) np.testing.assert_array_equal(nx.to_numpy(init), snapshot) def test_projection_clamps_last_iterate(nx): """With ``max_cost=b``, every coordinate of the last iterate lies in ``[-b, b]``.""" target_np, weights_np, _, _, d, kind = regular_grid_problem() target, weights = nx.from_numpy(target_np, weights_np) sampler = make_sampler(kind, d, nx, target) bound = 0.05 _, info = solve_semidiscrete( target, sampler, a_target=weights, max_iter=300, batch_size=4, max_cost=bound, log=True, ) last = nx.to_numpy(info["last_potential"]) assert np.abs(last).max() <= bound + 1e-10 def test_polyak_average_off_returns_last_iterate(nx): """With ``polyak_average=False`` the returned potential equals the last iterate.""" target_np, weights_np, _, max_cost, d, kind = regular_grid_problem() target, weights = nx.from_numpy(target_np, weights_np) sampler = make_sampler(kind, d, nx, target) final, info = solve_semidiscrete( target, sampler, a_target=weights, max_iter=20, batch_size=4, polyak_average=False, max_cost=max_cost, log=True, ) np.testing.assert_array_equal( nx.to_numpy(final), nx.to_numpy(info["last_potential"]) ) def test_log_returns_metadata(nx): """``log=True`` returns an info dict with the expected fields.""" target_np, weights_np, _, max_cost, d, kind = regular_grid_problem() target, weights = nx.from_numpy(target_np, weights_np) sampler = make_sampler(kind, d, nx, target) g, info = solve_semidiscrete( target, sampler, a_target=weights, max_iter=50, batch_size=4, max_cost=max_cost, log=True, ) assert nx.to_numpy(g).shape == (target_np.shape[0],) assert info["max_iter"] == 50 assert info["batch_size"] == 4 assert info["max_cost"] == max_cost python-pot-0.9.7+dfsg/test/test_sgot.py000066400000000000000000000200541525521106200201330ustar00rootroot00000000000000"""Tests for ot.sgot module""" # Author: Sienna O'Shea # Thibaut Germain # License: MIT License import numpy as np import pytest from ot.sgot import ( eigenvalue_cost_matrix, _delta_matrix_1d, _grassmann_distance_squared, sgot_cost_matrix, sgot_metric, ) def random_atoms(d=8, r=4, seed=42): """Deterministic complex atoms for given d, r.""" def _rand_complex(shape, seed_): rng = np.random.RandomState(seed_) real = rng.randn(*shape) imag = rng.randn(*shape) return real + 1j * imag Ds = _rand_complex((r,), seed + 0) Rs = _rand_complex((d, r), seed + 1) Ls = _rand_complex((d, r), seed + 2) Dt = _rand_complex((r,), seed + 3) Rt = _rand_complex((d, r), seed + 4) Lt = _rand_complex((d, r), seed + 5) return Ds, Rs, Ls, Dt, Rt, Lt # --------------------------------------------------------------------- # DATA / SAMPLING TESTS # --------------------------------------------------------------------- def test_random_d_r(nx): """Sample d and r uniformly and run sgot_cost_matrix (and sgot_metric when available) with those shapes.""" rng = np.random.RandomState(0) d_min, d_max = 4, 12 r_min, r_max = 2, 6 for _ in range(5): d = int(rng.randint(d_min, d_max + 1)) r = int(rng.randint(r_min, r_max + 1)) Ds, Rs, Ls, Dt, Rt, Lt = random_atoms(d=d, r=r) Ds_b, Rs_b, Ls_b, Dt_b, Rt_b, Lt_b = nx.from_numpy(Ds, Rs, Ls, Dt, Rt, Lt) C = sgot_cost_matrix(Ds_b, Rs_b, Ls_b, Dt_b, Rt_b, Lt_b) C_np = nx.to_numpy(C) np.testing.assert_allclose(C_np.shape, (r, r)) assert np.all(np.isfinite(C_np)) and np.all(C_np >= 0) try: dist = sgot_metric(Ds_b, Rs_b, Ls_b, Dt_b, Rt_b, Lt_b) dist_np = nx.to_numpy(dist) assert np.isfinite(dist_np) and dist_np >= 0 except TypeError: pytest.skip("sgot_metric() unavailable (emd_c signature mismatch)") # --------------------------------------------------------------------- # DELTA MATRIX TESTS # --------------------------------------------------------------------- def test_eigenvalue_cost_matrix_simple(): Ds = np.array([0.0, 1.0]) Dt = np.array([0.0, 2.0]) C = eigenvalue_cost_matrix(Ds, Dt, q=2) expected = np.array([[0.0, 4.0], [1.0, 1.0]]) np.testing.assert_allclose(C, expected) def test_delta_matrix_1d_identity(): r = 4 I = np.eye(r, dtype=complex) delta = _delta_matrix_1d(I, I, I, I) np.testing.assert_allclose(delta, np.eye(r), atol=1e-6) def test_delta_matrix_1d_swap_invariance(): d, r = 6, 3 _, R, _, _, _, _ = random_atoms(d=d, r=r) L = R.copy() delta1 = _delta_matrix_1d(R, L, R, L) delta2 = _delta_matrix_1d(L, R, L, R) np.testing.assert_allclose(delta1, delta2, atol=1e-6) # --------------------------------------------------------------------- # GRASSMANN DISTANCE TESTS # --------------------------------------------------------------------- @pytest.mark.parametrize( "grassmann_metric", ["geodesic", "chordal", "procrustes", "martin"] ) def test_grassmann_zero_distance(grassmann_metric, nx): delta = nx.from_numpy(np.ones((3, 3))) dist2 = _grassmann_distance_squared(delta, grassmann_metric=grassmann_metric, nx=nx) dist2_np = nx.to_numpy(dist2) np.testing.assert_allclose(dist2_np, 0.0, atol=1e-6) def test_grassmann_distance_invalid_name(): delta = np.ones((2, 2)) with pytest.raises(ValueError): _grassmann_distance_squared(delta, grassmann_metric="cordal") # --------------------------------------------------------------------- # COST TESTS # --------------------------------------------------------------------- def test_cost_self_zero(nx): """(D_S R_S L_S D_S): diagonal of sgot_cost_matrix matrix (same atom to same atom) should be near zero.""" Ds, Rs, Ls, _, _, _ = random_atoms() Ds_b, Rs_b, Ls_b, Ds_b2, Rs_b2, Ls_b2 = nx.from_numpy(Ds, Rs, Ls, Ds, Rs, Ls) C = sgot_cost_matrix(Ds_b, Rs_b, Ls_b, Ds_b2, Rs_b2, Ls_b2) C_np = nx.to_numpy(C) np.testing.assert_allclose(np.diag(C_np), np.zeros(C_np.shape[0]), atol=1e-6) np.testing.assert_allclose(C_np, C_np.T, atol=1e-6) def test_grassmann_cost_reference(nx): """Cost with same inputs and HPs should be deterministic (np.testing.assert_allclose).""" Ds, Rs, Ls, Dt, Rt, Lt = random_atoms() Ds_b, Rs_b, Ls_b, Dt_b, Rt_b, Lt_b = nx.from_numpy(Ds, Rs, Ls, Dt, Rt, Lt) eta, p, q = 0.5, 2, 1 C1 = sgot_cost_matrix(Ds_b, Rs_b, Ls_b, Dt_b, Rt_b, Lt_b, eta=eta, p=p, q=q) C2 = sgot_cost_matrix(Ds_b, Rs_b, Ls_b, Dt_b, Rt_b, Lt_b, eta=eta, p=p, q=q) np.testing.assert_allclose(nx.to_numpy(C1), nx.to_numpy(C2), atol=1e-6) @pytest.mark.parametrize( "grassmann_metric", ["geodesic", "chordal", "procrustes", "martin"] ) def test_grassmann_cost_basic_properties(grassmann_metric, nx): Ds, Rs, Ls, Dt, Rt, Lt = random_atoms() Ds_b, Rs_b, Ls_b, Dt_b, Rt_b, Lt_b = nx.from_numpy(Ds, Rs, Ls, Dt, Rt, Lt) C = sgot_cost_matrix( Ds_b, Rs_b, Ls_b, Dt_b, Rt_b, Lt_b, grassmann_metric=grassmann_metric ) C_np = nx.to_numpy(C) assert C_np.shape == (Ds.shape[0], Dt.shape[0]) assert np.all(np.isfinite(C_np)) assert np.all(C_np >= 0) def test_sgot_cost_input_validation(): Ds, Rs, Ls, Dt, Rt, Lt = random_atoms() with pytest.raises(ValueError): sgot_cost_matrix(Ds.reshape(-1, 1), Rs, Ls, Dt, Rt, Lt) with pytest.raises(ValueError): sgot_cost_matrix(Ds, Rs[:, :-1], Ls, Dt, Rt, Lt) # --------------------------------------------------------------------- # METRIC TESTS # --------------------------------------------------------------------- def test_sgot_metric_self_zero(nx): Ds, Rs, Ls, _, _, _ = random_atoms() Ds_b, Rs_b, Ls_b, Ds_b2, Rs_b2, Ls_b2 = nx.from_numpy(Ds, Rs, Ls, Ds, Rs, Ls) dist = sgot_metric(Ds_b, Rs_b, Ls_b, Ds_b2, Rs_b2, Ls_b2, nx=nx) dist_np = nx.to_numpy(dist) assert np.isfinite(dist_np) assert abs(float(dist_np)) < 2e-2 def test_sgot_metric_symmetry(): Ds, Rs, Ls, Dt, Rt, Lt = random_atoms() d1 = sgot_metric(Ds, Rs, Ls, Dt, Rt, Lt) d2 = sgot_metric(Dt, Rt, Lt, Ds, Rs, Ls) np.testing.assert_allclose(d1, d2, atol=1e-6) def test_sgot_metric_with_weights(): Ds, Rs, Ls, Dt, Rt, Lt = random_atoms() r = Ds.shape[0] rng = np.random.RandomState(1) Ws = rng.rand(r) Ws = Ws / np.sum(Ws) Wt = rng.rand(r) Wt = Wt / np.sum(Wt) dist = sgot_metric(Ds, Rs, Ls, Dt, Rt, Lt, Ws=Ws, Wt=Wt) assert np.isfinite(dist) # --------------------------------------------------------------------- # HYPERPARAMETER SWEEP TEST # --------------------------------------------------------------------- @pytest.mark.parametrize( "eta, p, q, grassmann_metric", [ (0.5, 1, 1, "geodesic"), (0.5, 2, 1, "chordal"), (0.3, 2, 2, "procrustes"), (0.7, 1, 2, "martin"), ], ) def test_hyperparameter_sweep_cost(nx, eta, p, q, grassmann_metric): """Sweep over a set of fixed HPs and run cost().""" Ds, Rs, Ls, Dt, Rt, Lt = random_atoms() Ds_b, Rs_b, Ls_b, Dt_b, Rt_b, Lt_b = nx.from_numpy(Ds, Rs, Ls, Dt, Rt, Lt) C = sgot_cost_matrix( Ds_b, Rs_b, Ls_b, Dt_b, Rt_b, Lt_b, eta=eta, p=p, q=q, grassmann_metric=grassmann_metric, ) C_np = nx.to_numpy(C) assert C_np.shape == (Ds.shape[0], Dt.shape[0]) assert np.all(np.isfinite(C_np)) assert np.all(C_np >= 0) @pytest.mark.parametrize( "grassmann_metric", ["geodesic", "chordal", "procrustes", "martin"] ) def test_hyperparameter_sweep(grassmann_metric): Ds, Rs, Ls, Dt, Rt, Lt = random_atoms() rng = np.random.RandomState(3) eta = rng.uniform(0.0, 1.0) p = rng.choice([1, 2]) q = rng.choice([1, 2]) r = rng.choice([1, 2]) dist = sgot_metric( Ds, Rs, Ls, Dt, Rt, Lt, eta=eta, p=p, q=q, r=r, grassmann_metric=grassmann_metric, ) assert np.isfinite(dist) assert dist >= 0 python-pot-0.9.7+dfsg/test/test_smooth.py000066400000000000000000000076171525521106200205020ustar00rootroot00000000000000"""Tests for ot.smooth model""" # Author: Remi Flamary # # License: MIT License import numpy as np import ot import pytest from scipy.optimize import check_grad def test_smooth_ot_dual(): # get data n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) with pytest.raises(NotImplementedError): Gl2, log = ot.smooth.smooth_ot_dual(u, u, M, 1, reg_type="none") # squared l2 regularisation Gl2, log = ot.smooth.smooth_ot_dual( u, u, M, 1, reg_type="l2", log=True, stopThr=1e-10 ) # check constraints np.testing.assert_allclose(u, Gl2.sum(1), atol=1e-05) # cf convergence sinkhorn np.testing.assert_allclose(u, Gl2.sum(0), atol=1e-05) # cf convergence sinkhorn # kl regularisation G = ot.smooth.smooth_ot_dual(u, u, M, 1, reg_type="kl", stopThr=1e-10) # check constraints np.testing.assert_allclose(u, G.sum(1), atol=1e-05) # cf convergence sinkhorn np.testing.assert_allclose(u, G.sum(0), atol=1e-05) # cf convergence sinkhorn G2 = ot.sinkhorn(u, u, M, 1, stopThr=1e-10) np.testing.assert_allclose(G, G2, atol=1e-05) # sparsity-constrained regularisation max_nz = 2 Gsc, log = ot.smooth.smooth_ot_dual( u, u, M, 1, max_nz=max_nz, log=True, reg_type="sparsity_constrained", stopThr=1e-10, ) # check marginal constraints np.testing.assert_allclose(u, Gsc.sum(1), atol=1e-03) np.testing.assert_allclose(u, Gsc.sum(0), atol=1e-03) # check sparsity constraints np.testing.assert_array_less(np.sum(Gsc > 0, axis=0), np.ones(n) * max_nz + 1) def test_smooth_ot_semi_dual(): # get data n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) with pytest.raises(NotImplementedError): Gl2, log = ot.smooth.smooth_ot_semi_dual(u, u, M, 1, reg_type="none") # squared l2 regularisation Gl2, log = ot.smooth.smooth_ot_semi_dual( u, u, M, 1, reg_type="l2", log=True, stopThr=1e-10 ) # check constraints np.testing.assert_allclose(u, Gl2.sum(1), atol=1e-05) # cf convergence sinkhorn np.testing.assert_allclose(u, Gl2.sum(0), atol=1e-05) # cf convergence sinkhorn # kl regularisation G = ot.smooth.smooth_ot_semi_dual(u, u, M, 1, reg_type="kl", stopThr=1e-10) # check constraints np.testing.assert_allclose(u, G.sum(1), atol=1e-05) # cf convergence sinkhorn np.testing.assert_allclose(u, G.sum(0), atol=1e-05) # cf convergence sinkhorn G2 = ot.sinkhorn(u, u, M, 1, stopThr=1e-10) np.testing.assert_allclose(G, G2, atol=1e-05) # sparsity-constrained regularisation max_nz = 2 Gsc = ot.smooth.smooth_ot_semi_dual( u, u, M, 1, reg_type="sparsity_constrained", max_nz=max_nz, stopThr=1e-10 ) # check marginal constraints np.testing.assert_allclose(u, Gsc.sum(1), atol=1e-03) np.testing.assert_allclose(u, Gsc.sum(0), atol=1e-03) # check sparsity constraints np.testing.assert_array_less(np.sum(Gsc > 0, axis=0), np.ones(n) * max_nz + 1) def test_sparsity_constrained_gradient(): max_nz = 5 regularizer = ot.smooth.SparsityConstrained(max_nz=max_nz) rng = np.random.RandomState(0) X = rng.randn( 10, ) b = 0.5 def delta_omega_func(X): return regularizer.delta_Omega(X)[0] def delta_omega_grad(X): return regularizer.delta_Omega(X)[1] dual_grad_err = check_grad(delta_omega_func, delta_omega_grad, X) np.testing.assert_allclose(dual_grad_err, 0.0, atol=1e-07) def max_omega_func(X, b): return regularizer.max_Omega(X, b)[0] def max_omega_grad(X, b): return regularizer.max_Omega(X, b)[1] semi_dual_grad_err = check_grad(max_omega_func, max_omega_grad, X, b) np.testing.assert_allclose(semi_dual_grad_err, 0.0, atol=1e-07) python-pot-0.9.7+dfsg/test/test_solvers.py000066400000000000000000001052611525521106200206600ustar00rootroot00000000000000"""Tests for ot solvers""" # Author: Remi Flamary # Cédric Vincent-Cuaz # # License: MIT License import itertools import numpy as np import pytest import sys import ot from ot.bregman import geomloss from ot.backend import torch from ot.solvers._linear import lst_method_solve_sample from ot.utils import DataScaler lst_reg = [None, 0.1] lst_reg_type = ["KL", "entropy", "L2", "tuple"] lst_unbalanced = [None, 0.9] lst_unbalanced_type = ["KL", "L2", "TV"] lst_reg_type_gromov = ["entropy"] lst_gw_losses = ["L2", "KL"] lst_unbalanced_type_gromov = ["KL", "semirelaxed", "partial", "L2"] lst_unbalanced_gromov = [None, 1.0] lst_alpha = [0, 0.4, 0.9, 1] lst_method_params_solve_sample = [ {"method": "1d"}, {"method": "1d", "metric": "euclidean"}, {"method": "gaussian"}, {"method": "gaussian", "reg": 1}, {"method": "gaussian_hd", "rank": 1}, {"method": "gaussian_hd", "rank": 3}, {"method": "factored", "rank": 2}, {"method": "lowrank", "rank": 2, "max_iter": 5}, {"method": "nystroem", "rank": 2}, {"method": "sliced", "n_projections": 10}, {"method": "sliced", "n_projections": 10, "metric": "euclidean"}, {"method": "max_sliced", "n_projections": 10}, {"method": "max_sliced", "n_projections": 10, "metric": "euclidean"}, ] lst_parameters_solve_sample_NotImplemented = [ {"method": "1d", "metric": "any other one"}, # fail 1d on weird metrics { "method": "gaussian", "metric": "euclidean", }, # fail gaussian on metric not euclidean { "method": "gaussian_hd", "metric": "euclidean", }, # fail gaussian on metric not euclidean { "method": "factored", "metric": "euclidean", }, # fail factored on metric not euclidean { "method": "lowrank", "metric": "euclidean", }, # fail lowrank on metric not euclidean { "method": "nystroem", "metric": "euclidean", }, # fail nystroem on metric not euclidean {"lazy": True, "unbalanced": 1}, # fail lazy for unbalanced (not supported) { "lazy": True, "reg": 1, "unbalanced": 1, }, # fail lazy for unbalanced and regularized { "method": "sliced", "metric": "wrong_metric", }, # fail sliced on metric not euclidean { "method": "max_sliced", "metric": "wrong_metric", }, # fail sliced on metric not euclidean ] lst_parameters_solve_bary_sample_NotImplemented = [ {"method": method} for method in lst_method_solve_sample ] + [ {"lazy": True}, # fail lazy {"metric": "cosine"}, # fail on invalid metric ] # set readable ids for each param lst_method_params_solve_sample = [ pytest.param(param, id=str(param)) for param in lst_method_params_solve_sample ] lst_parameters_solve_sample_NotImplemented = [ pytest.param(param, id=str(param)) for param in lst_parameters_solve_sample_NotImplemented ] lst_parameters_solve_bary_sample_NotImplemented = [ pytest.param(param, id=str(param)) for param in lst_parameters_solve_bary_sample_NotImplemented ] def assert_allclose_sol(sol1, sol2): lst_attr = [ "value", "value_linear", "plan", "potential_a", "potential_b", "marginal_a", "marginal_b", ] nx1 = sol1._backend if sol1._backend is not None else ot.backend.NumpyBackend() nx2 = sol2._backend if sol2._backend is not None else ot.backend.NumpyBackend() for attr in lst_attr: if getattr(sol1, attr) is not None and getattr(sol2, attr) is not None: try: np.allclose( nx1.to_numpy(getattr(sol1, attr)), nx2.to_numpy(getattr(sol2, attr)), equal_nan=True, ) except NotImplementedError: pass elif getattr(sol1, attr) is None and getattr(sol2, attr) is None: return True else: return False def test_solve(nx): n_samples_s = 10 n_samples_t = 7 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples_s, n_features) y = rng.randn(n_samples_t, n_features) a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) M = ot.dist(x, y) reg = 1e-1 # solve unif weights sol0 = ot.solve(M, reg=reg) print(sol0) # solve signe weights sol = ot.solve(M, a, b, reg=reg) # check some attributes sol.potentials sol.sparse_plan sol.marginals sol.status # print("dual = {}".format(sol.potentials)) # assert_allclose_sol(sol0, sol) # solve in backend ab, bb, Mb = nx.from_numpy(a, b, M) solb = ot.solve(M, a, b) assert_allclose_sol(sol, solb) # test not implemented unbalanced and check raise with pytest.raises(NotImplementedError): sol0 = ot.solve(M, unbalanced=1, unbalanced_type="cryptic divergence") # test not implemented reg_type and check raise with pytest.raises(NotImplementedError): sol0 = ot.solve(M, reg=1, reg_type="cryptic divergence") @pytest.mark.skipif(not torch, reason="torch no installed") def test_solve_last_step(): n_samples_s = 10 n_samples_t = 7 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples_s, n_features) y = rng.randn(n_samples_t, n_features) a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) M = ot.dist(x, y) # Check that last_step and autodiff give the same result and similar gradients a = torch.tensor(a, requires_grad=True) b = torch.tensor(b, requires_grad=True) M = torch.tensor(M, requires_grad=True) sol0 = ot.solve(M, a, b, reg=10, grad="autodiff") sol0.value.backward() gM0 = M.grad.clone() ga0 = a.grad.clone() gb0 = b.grad.clone() a = torch.tensor(a, requires_grad=True) b = torch.tensor(b, requires_grad=True) M = torch.tensor(M, requires_grad=True) sol = ot.solve(M, a, b, reg=10, grad="last_step") sol.value.backward() gM = M.grad.clone() ga = a.grad.clone() gb = b.grad.clone() # Note, gradients are invariant to change in constant so we center them cos = torch.nn.CosineSimilarity(dim=0, eps=1e-6) tolerance = 0.96 assert cos(gM0.flatten(), gM.flatten()) > tolerance assert cos(ga0 - ga0.mean(), ga - ga.mean()) > tolerance assert cos(gb0 - gb0.mean(), gb - gb.mean()) > tolerance assert torch.allclose(sol0.plan, sol.plan) assert torch.allclose(sol0.value, sol.value) assert torch.allclose(sol0.value_linear, sol.value_linear) assert torch.allclose(sol0.potentials[0], sol.potentials[0]) assert torch.allclose(sol0.potentials[1], sol.potentials[1]) with pytest.raises(ValueError): ot.solve(M, a, b, grad="last_step", max_iter=0, reg=10) @pytest.mark.skipif(not torch, reason="torch no installed") def test_solve_detach(): n_samples_s = 10 n_samples_t = 7 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples_s, n_features) y = rng.randn(n_samples_t, n_features) a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) M = ot.dist(x, y) # Check that last_step and autodiff give the same result and similar gradients a = torch.tensor(a, requires_grad=True) b = torch.tensor(b, requires_grad=True) M = torch.tensor(M, requires_grad=True) sol0 = ot.solve(M, a, b, reg=10, grad="detach") with pytest.raises(RuntimeError): sol0.value.backward() sol = ot.solve(M, a, b, reg=10, grad="autodiff") assert torch.allclose(sol0.plan, sol.plan) assert torch.allclose(sol0.value, sol.value) assert torch.allclose(sol0.value_linear, sol.value_linear) assert torch.allclose(sol0.potentials[0], sol.potentials[0]) assert torch.allclose(sol0.potentials[1], sol.potentials[1]) @pytest.mark.skipif(not torch, reason="torch no installed") def test_solve_envelope(): n_samples_s = 10 n_samples_t = 7 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples_s, n_features) y = rng.randn(n_samples_t, n_features) a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) M = ot.dist(x, y) a = torch.tensor(a, requires_grad=True) b = torch.tensor(b, requires_grad=True) M = torch.tensor(M, requires_grad=True) sol0 = ot.solve(M, a, b, reg=10, grad="envelope") sol0.value.backward() gM0 = M.grad.clone() ga0 = a.grad.clone() gb0 = b.grad.clone() a = torch.tensor(a, requires_grad=True) b = torch.tensor(b, requires_grad=True) M = torch.tensor(M, requires_grad=True) sol = ot.solve(M, a, b, reg=10, grad="autodiff") sol.value.backward() gM = M.grad.clone() ga = a.grad.clone() gb = b.grad.clone() # Note, gradients are invariant to change in constant so we center them assert torch.allclose(gM0, gM) assert torch.allclose(ga0 - ga0.mean(), ga - ga.mean()) assert torch.allclose(gb0 - gb0.mean(), gb - gb.mean()) @pytest.mark.parametrize( "reg,reg_type,unbalanced,unbalanced_type", itertools.product(lst_reg, lst_reg_type, lst_unbalanced, lst_unbalanced_type), ) def test_solve_grid(nx, reg, reg_type, unbalanced, unbalanced_type): n_samples_s = 10 n_samples_t = 7 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples_s, n_features) y = rng.randn(n_samples_t, n_features) a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) M = ot.dist(x, y) try: if reg_type == "tuple": def f(G): return np.sum(G**2) def df(G): return 2 * G reg_type = (f, df) # solve unif weights sol0 = ot.solve( M, reg=reg, reg_type=reg_type, unbalanced=unbalanced, unbalanced_type=unbalanced_type, ) # solve signe weights sol = ot.solve( M, a, b, reg=reg, reg_type=reg_type, unbalanced=unbalanced, unbalanced_type=unbalanced_type, ) assert_allclose_sol(sol0, sol) # solve in backend ab, bb, Mb = nx.from_numpy(a, b, M) if isinstance(reg_type, tuple): def f(G): return nx.sum(G**2) def df(G): return 2 * G reg_type = (f, df) solb = ot.solve( Mb, ab, bb, reg=reg, reg_type=reg_type, unbalanced=unbalanced, unbalanced_type=unbalanced_type, ) assert_allclose_sol(sol, solb) except NotImplementedError: pytest.skip("Not implemented") def test_solve_not_implemented(nx): n_samples_s = 10 n_samples_t = 7 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples_s, n_features) y = rng.randn(n_samples_t, n_features) M = ot.dist(x, y) # test not implemented and check raise with pytest.raises(NotImplementedError): ot.solve(M, reg=1.0, reg_type="cryptic divergence") with pytest.raises(NotImplementedError): ot.solve(M, unbalanced=1.0, unbalanced_type="cryptic divergence") def test_solve_gromov(nx): np.random.seed(0) n_samples_s = 3 n_samples_t = 5 Ca = np.random.rand(n_samples_s, n_samples_s) Ca = (Ca + Ca.T) / 2 Cb = np.random.rand(n_samples_t, n_samples_t) Cb = (Cb + Cb.T) / 2 a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) M = np.random.rand(n_samples_s, n_samples_t) sol0 = ot.solve_gromov(Ca, Cb) # GW sol = ot.solve_gromov(Ca, Cb, a=a, b=b) # GW sol0_fgw = ot.solve_gromov(Ca, Cb, M) # FGW # check some attributes sol.potentials sol.marginals assert_allclose_sol(sol0, sol) # solve in backend ax, bx, Mx, Cax, Cbx = nx.from_numpy(a, b, M, Ca, Cb) solx = ot.solve_gromov(Cax, Cbx, a=ax, b=bx) # GW solx_fgw = ot.solve_gromov(Cax, Cbx, Mx) # FGW assert_allclose_sol(sol, solx) assert_allclose_sol(sol0_fgw, solx_fgw) @pytest.mark.parametrize( "reg,reg_type,unbalanced,unbalanced_type,alpha,loss", itertools.product( lst_reg, lst_reg_type_gromov, lst_unbalanced_gromov, lst_unbalanced_type_gromov, lst_alpha, lst_gw_losses, ), ) def test_solve_gromov_grid(nx, reg, reg_type, unbalanced, unbalanced_type, alpha, loss): np.random.seed(0) n_samples_s = 3 n_samples_t = 5 Ca = np.random.rand(n_samples_s, n_samples_s) Ca = (Ca + Ca.T) / 2 Cb = np.random.rand(n_samples_t, n_samples_t) Cb = (Cb + Cb.T) / 2 a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) M = np.random.rand(n_samples_s, n_samples_t) try: sol0 = ot.solve_gromov( Ca, Cb, reg=reg, reg_type=reg_type, unbalanced=unbalanced, unbalanced_type=unbalanced_type, loss=loss, ) # GW sol0_fgw = ot.solve_gromov( Ca, Cb, M, reg=reg, reg_type=reg_type, unbalanced=unbalanced, unbalanced_type=unbalanced_type, alpha=alpha, loss=loss, ) # FGW # solve in backend ax, bx, Mx, Cax, Cbx = nx.from_numpy(a, b, M, Ca, Cb) solx = ot.solve_gromov( Cax, Cbx, reg=reg, reg_type=reg_type, unbalanced=unbalanced, unbalanced_type=unbalanced_type, loss=loss, ) # GW solx_fgw = ot.solve_gromov( Cax, Cbx, Mx, reg=reg, reg_type=reg_type, unbalanced=unbalanced, unbalanced_type=unbalanced_type, alpha=alpha, loss=loss, ) # FGW solx.value_quad assert_allclose_sol(sol0, solx) assert_allclose_sol(sol0_fgw, solx_fgw) except NotImplementedError: pytest.skip("Not implemented") def test_solve_gromov_not_implemented(nx): np.random.seed(0) n_samples_s = 3 n_samples_t = 5 Ca = np.random.rand(n_samples_s, n_samples_s) Ca = (Ca + Ca.T) / 2 Cb = np.random.rand(n_samples_t, n_samples_t) Cb = (Cb + Cb.T) / 2 a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) M = np.random.rand(n_samples_s, n_samples_t) Ca, Cb, M, a, b = nx.from_numpy(Ca, Cb, M, a, b) # test not implemented and check raise with pytest.raises(NotImplementedError): ot.solve_gromov(Ca, Cb, loss="weird loss") with pytest.raises(NotImplementedError): ot.solve_gromov(Ca, Cb, unbalanced=1, unbalanced_type="cryptic divergence") with pytest.raises(NotImplementedError): ot.solve_gromov(Ca, Cb, reg=1, reg_type="cryptic divergence") # detect partial not implemented and error detect in value with pytest.raises(ValueError): ot.solve_gromov(Ca, Cb, unbalanced_type="partial", unbalanced=1.5) with pytest.raises(ValueError): ot.solve_gromov(Ca, Cb, reg=1, unbalanced_type="partial", unbalanced=1.5) with pytest.raises(ValueError): ot.solve_gromov(Ca, Cb, M, unbalanced_type="partial", unbalanced=1.5) with pytest.raises(ValueError): ot.solve_gromov(Ca, Cb, M, reg=1, unbalanced_type="partial", unbalanced=1.5) def test_solve_sample(nx): # test solve_sample when is_Lazy = False n = 20 X_s = np.reshape(1.0 * np.arange(n), (n, 1)) X_t = np.reshape(1.0 * np.arange(0, n), (n, 1)) a = ot.utils.unif(X_s.shape[0]) b = ot.utils.unif(X_t.shape[0]) M = ot.dist(X_s, X_t) # solve with ot.solve sol00 = ot.solve(M, a, b) # solve unif weights sol0 = ot.solve_sample(X_s, X_t) # solve signe weights sol = ot.solve_sample(X_s, X_t, a, b) # check some attributes sol.potentials sol.sparse_plan sol.marginals sol.status assert_allclose_sol(sol0, sol) assert_allclose_sol(sol0, sol00) # solve in backend X_sb, X_tb, ab, bb = nx.from_numpy(X_s, X_t, a, b) solb = ot.solve_sample(X_sb, X_tb, ab, bb) assert_allclose_sol(sol, solb) # test not implemented unbalanced and check raise with pytest.raises(NotImplementedError): sol0 = ot.solve_sample( X_s, X_t, unbalanced=1, unbalanced_type="cryptic divergence" ) # test not implemented reg_type and check raise with pytest.raises(NotImplementedError): sol0 = ot.solve_sample(X_s, X_t, reg=1, reg_type="cryptic divergence") def test_solve_sample_lazy(nx): # test solve_sample when is_Lazy = False n = 20 X_s = np.reshape(1.0 * np.arange(n), (n, 1)) X_t = np.reshape(1.0 * np.arange(0, n), (n, 1)) a = ot.utils.unif(X_s.shape[0]) b = ot.utils.unif(X_t.shape[0]) X_s, X_t, a, b = nx.from_numpy(X_s, X_t, a, b) M = ot.dist(X_s, X_t) # solve with ot.solve sol00 = ot.solve(M, a, b, reg=1) sol0 = ot.solve_sample(X_s, X_t, a, b, reg=1) # solve signe weights sol = ot.solve_sample(X_s, X_t, a, b, reg=1, lazy=True) assert_allclose_sol(sol0, sol00) np.testing.assert_allclose(sol0.plan, sol.lazy_plan[:], rtol=1e-5, atol=1e-5) @pytest.mark.parametrize("metric", ["sqeuclidean", "euclidean", "cityblock"]) def test_solve_sample_lazy_emd(nx, metric): # test lazy EMD solver (no regularization, computes distances on-the-fly) n_s = 20 n_t = 25 d = 2 rng = np.random.RandomState(42) X_s = rng.rand(n_s, d) X_t = rng.rand(n_t, d) a = ot.utils.unif(n_s) b = ot.utils.unif(n_t) X_sb, X_tb, ab, bb = nx.from_numpy(X_s, X_t, a, b) # Standard solver: pre-compute distance matrix M = ot.dist(X_sb, X_tb, metric=metric) sol_standard = ot.solve(M, ab, bb) # Lazy solver: compute distances on-the-fly sol_lazy = ot.solve_sample(X_sb, X_tb, ab, bb, lazy=True, metric=metric) # Check that optimal costs match np.testing.assert_allclose( nx.to_numpy(sol_standard.value), nx.to_numpy(sol_lazy.value), rtol=1e-10, atol=1e-10, err_msg=f"Lazy EMD cost mismatch for metric {metric}", ) # Check that the lazy plan has the same cost when evaluated against M # (OT can have multiple optimal plans with the same cost) cost_standard = nx.to_numpy(nx.sum(sol_standard.plan * M)) cost_lazy = nx.to_numpy(nx.sum(sol_lazy.plan * M)) np.testing.assert_allclose( cost_standard, cost_lazy, rtol=1e-10, atol=1e-10, err_msg=f"Lazy EMD plan cost mismatch for metric {metric}", ) # Check that the lazy plan satisfies marginal constraints np.testing.assert_allclose( nx.to_numpy(nx.sum(sol_lazy.plan, axis=1)), nx.to_numpy(ab), rtol=1e-6, atol=1e-8, err_msg=f"Lazy EMD row marginal mismatch for metric {metric}", ) np.testing.assert_allclose( nx.to_numpy(nx.sum(sol_lazy.plan, axis=0)), nx.to_numpy(bb), rtol=1e-6, atol=1e-8, err_msg=f"Lazy EMD column marginal mismatch for metric {metric}", ) def test_solve_sample_lazy_emd_large(nx): # Test larger problem to verify memory savings benefit n_large = 100 d = 2 rng = np.random.RandomState(42) X_s_large = rng.rand(n_large, d) X_t_large = rng.rand(n_large, d) a_large = ot.utils.unif(n_large) b_large = ot.utils.unif(n_large) X_sb_large, X_tb_large, ab_large, bb_large = nx.from_numpy( X_s_large, X_t_large, a_large, b_large ) # Standard solver M_large = ot.dist(X_sb_large, X_tb_large, metric="sqeuclidean") sol_standard_large = ot.solve(M_large, ab_large, bb_large) # Lazy solver (avoids storing 100x100 cost matrix) sol_lazy_large = ot.solve_sample( X_sb_large, X_tb_large, ab_large, bb_large, lazy=True, metric="sqeuclidean" ) np.testing.assert_allclose( nx.to_numpy(sol_standard_large.value), nx.to_numpy(sol_lazy_large.value), rtol=1e-9, atol=1e-9, err_msg="Lazy EMD cost mismatch for large problem", ) @pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher") @pytest.mark.skipif(not geomloss, reason="pytorch not installed") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") @pytest.skip_backend("jax") @pytest.mark.parametrize("metric", ["sqeuclidean", "euclidean"]) def test_solve_sample_geomloss(nx, metric): # test solve_sample when is_Lazy = False n_samples_s = 13 n_samples_t = 7 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples_s, n_features) y = rng.randn(n_samples_t, n_features) a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) xb, yb, ab, bb = nx.from_numpy(x, y, a, b) sol0 = ot.solve_sample(xb, yb, ab, bb, reg=1) # solve signe weights sol = ot.solve_sample(xb, yb, ab, bb, reg=1, method="geomloss") assert_allclose_sol(sol0, sol) sol1 = ot.solve_sample(xb, yb, ab, bb, reg=1, lazy=False, method="geomloss") assert_allclose_sol(sol0, sol) # commented because geomloss_tensorized and geomloss_online are not # implemented in geomloss 0.2.0 yet # sol1 = ot.solve_sample( # xb, yb, ab, bb, reg=1, lazy=True, method="geomloss_tensorized" # ) # np.testing.assert_allclose( # nx.to_numpy(sol1.plan), # nx.to_numpy(sol.plan), # rtol=1e-5, # atol=1e-5, # ) # sol1 = ot.solve_sample(xb, yb, ab, bb, reg=1, lazy=True, method="geomloss_online") # np.testing.assert_allclose( # nx.to_numpy(sol1.plan), # nx.to_numpy(sol.plan), # rtol=1e-5, # atol=1e-5, # ) # sol1 = ot.solve_sample( # xb, yb, ab, bb, reg=1, lazy=True, method="geomloss_multiscale" # ) # np.testing.assert_allclose( # nx.to_numpy(sol1.plan), # nx.to_numpy(sol.plan), # rtol=1e-5, # atol=1e-5, # ) sol1 = ot.solve_sample(xb, yb, ab, bb, reg=1, lazy=True, method="geomloss") np.testing.assert_allclose( nx.to_numpy(sol1.plan), nx.to_numpy(sol.plan), rtol=1e-5, atol=1e-5, ) @pytest.mark.parametrize("method_params", lst_method_params_solve_sample) def test_solve_sample_methods(nx, method_params): n_samples_s = 10 n_samples_t = 9 n_features = 2 rng = np.random.RandomState(42) x = rng.randn(n_samples_s, n_features) y = rng.randn(n_samples_t, n_features) a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) xb, yb, ab, bb = nx.from_numpy(x, y, a, b) sol = ot.solve_sample(x, y, **method_params) solb = ot.solve_sample(xb, yb, ab, bb, **method_params) # check some attributes (no need ) assert_allclose_sol(sol, solb) sol2 = ot.solve_sample(x, x, **method_params) if method_params["method"] not in [ "factored", "lowrank", "nystroem", "gaussian_hd", ]: np.testing.assert_allclose(sol2.value, 0, atol=1e-10) def test_zero_mass_solvers(): # test that solvers handle zero mass distributions correctly n_samples_s = 10 n_samples_t = 9 n_features = 2 rng = np.random.RandomState(42) x = rng.randn(n_samples_s, n_features) y = rng.randn(n_samples_t, n_features) a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) # Set one of the distributions to zero mass a[0] = 0.0 b[0] = 0.0 a /= a.sum() b /= b.sum() res = ot.solve_sample(x, y, a, b) res_reg = ot.solve_sample(x, y, a, b, reg=1.0) assert np.isnan(res.value) == False assert np.isnan(res_reg.value) == False @pytest.skip_backend("tf", reason="Not implemented for tf backend") @pytest.mark.parametrize("debias", [True, False, "split"]) @pytest.mark.parametrize("reg", [None, 10]) def test_solve_sample_debias(nx, debias, reg): n_samples_s = 10 n_samples_t = 9 n_features = 2 rng = np.random.RandomState(42) x = rng.randn(n_samples_s, n_features) y = rng.randn(n_samples_t, n_features) a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) xb, yb, ab, bb = nx.from_numpy(x, y, a, b) sol = ot.solve_sample(x, y, reg=reg, debias=debias) solb = ot.solve_sample(xb, yb, ab, bb, reg=reg, debias=debias) # check some attributes (no need ) assert_allclose_sol(sol, solb) def test_solve_sample_bsp(nx): n_samples_s = 10 n_samples_t = 10 # same number of samples for source and target n_features = 2 rng = np.random.RandomState(42) x = rng.randn(n_samples_s, n_features) y = rng.randn(n_samples_t, n_features) xb, yb = nx.from_numpy(x, y) sol = ot.solve_sample(x, y, method="bsp") solb = ot.solve_sample(xb, yb, method="bsp") # check some attributes (no need ) assert_allclose_sol(sol, solb) with pytest.raises(ValueError): # bsp method requires same number of samples for source and target ot.solve_sample(x, y[:5], method="bsp") with pytest.raises(NotImplementedError): # bsp method requires same number of samples for source and target ot.solve_sample(x, y, method="bsp", metric="wrong_metric") def test_solvers_bad_method(): n_samples_s = 20 n_samples_t = 7 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples_s, n_features) y = rng.randn(n_samples_t, n_features) C = ot.dist(x, y) with pytest.raises(ValueError): ot.solve_sample(x, y, method="invalid_method") with pytest.raises(ValueError): ot.solve(C, method="invalid_method") @pytest.mark.parametrize("norm", ["standard", "minmax", "l2"]) def test_solve_sample_scaler(nx, norm): n_samples_s = 20 n_samples_t = 7 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples_s, n_features) y = rng.randn(n_samples_t, n_features) a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) xb, yb, ab, bb = nx.from_numpy(x, y, a, b) scaler0 = DataScaler(norm=norm) scaler0.fit(x) scaler1 = DataScaler(norm=norm) scaler1.fit(xb) sol0 = ot.solve_sample(x, y, a, b, scaler=scaler0) sol1 = ot.solve_sample(xb, yb, ab, bb, scaler=scaler1) assert_allclose_sol(sol0, sol1) @pytest.mark.parametrize("method_params", lst_parameters_solve_sample_NotImplemented) def test_solve_sample_NotImplemented(nx, method_params): n_samples_s = 20 n_samples_t = 7 n_features = 2 rng = np.random.RandomState(0) x = rng.randn(n_samples_s, n_features) y = rng.randn(n_samples_t, n_features) a = ot.utils.unif(n_samples_s) b = ot.utils.unif(n_samples_t) xb, yb, ab, bb = nx.from_numpy(x, y, a, b) with pytest.raises(NotImplementedError): ot.solve_sample(xb, yb, ab, bb, **method_params) def assert_allclose_bary_sol(sol1, sol2): lst_attr = ["X", "b", "value", "value_linear", "log"] nx1 = sol1._backend if sol1._backend is not None else ot.backend.NumpyBackend() nx2 = sol2._backend if sol2._backend is not None else ot.backend.NumpyBackend() for attr in lst_attr: if getattr(sol1, attr) is not None and getattr(sol2, attr) is not None: try: var1 = getattr(sol1, attr) var2 = getattr(sol2, attr) if isinstance(var1, dict): # only contains lists for key in var1.keys(): np.allclose( np.array(var1[key]), np.array(var2[key]), equal_nan=True, ) else: np.allclose( nx1.to_numpy(getattr(sol1, attr)), nx2.to_numpy(getattr(sol2, attr)), equal_nan=True, ) except NotImplementedError: pass elif getattr(sol1, attr) is None and getattr(sol2, attr) is None: return True else: return False @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tf backend") @pytest.mark.parametrize( "reg,reg_type,unbalanced,unbalanced_type,warmstart", itertools.product( lst_reg, lst_reg_type, lst_unbalanced, lst_unbalanced_type, [True, False], # lst_reg, lst_reg_type, lst_unbalanced, lst_unbalanced_type, warmstart ), ) def test_solve_bary_sample(nx, reg, reg_type, unbalanced, unbalanced_type, warmstart): # test bary_sample when is_Lazy = False rng = np.random.RandomState() K = 2 # number of distributions ns = rng.randint(10, 20, K) # number of samples within each distribution n = 5 # number of samples in the barycenter X_list = [rng.randn(ns_i, 2) for ns_i in ns] # X_init = np.reshape(1.0 * np.randn(n, 2), (n, 1)) a_list = [ot.utils.unif(X.shape[0]) for X in X_list] b = ot.utils.unif(n) w = ot.utils.unif(K) stopping_criterion = "loss" if rng.choice([True, False]) else "bary" # try: if reg_type == "tuple": def f(G): return np.sum(G**2) def df(G): return 2 * G reg_type = (f, df) # print('test reg_type:', reg_type[0](None), reg_type[1](None)) # solve default None weights sol0 = ot.solve_bary_sample( X_list, n, w=None, metric="sqeuclidean", reg=reg, reg_type=reg_type, unbalanced=unbalanced, unbalanced_type=unbalanced_type, warmstart=warmstart, max_iter_bary=2, tol_bary=1e-3, stopping_criterion=stopping_criterion, verbose=True, ) print("------ [done] sol0 - no backend") # solve provided uniform weights sol = ot.solve_bary_sample( X_list, n, a_list=a_list, b=b, w=w, metric="sqeuclidean", reg=reg, reg_type=reg_type, unbalanced=unbalanced, unbalanced_type=unbalanced_type, warmstart=warmstart, max_iter_bary=2, tol_bary=1e-3, stopping_criterion=stopping_criterion, verbose=True, ) print("------ [done] sol - no backend") assert_allclose_bary_sol(sol0, sol) # solve in backend X_listb = nx.from_numpy(*X_list) a_listb = nx.from_numpy(*a_list) wb, bb = nx.from_numpy(w, b) if isinstance(reg_type, tuple): def fb(G): return nx.sum( G**2 ) # otherwise we keep previously defined (f, df) as required by inner solver reg_type = (fb, df) solb = ot.solve_bary_sample( X_listb, n, a_list=a_listb, b=bb, w=wb, metric="sqeuclidean", reg=reg, reg_type=reg_type, unbalanced=unbalanced, unbalanced_type=unbalanced_type, warmstart=warmstart, max_iter_bary=2, tol_bary=1e-3, stopping_criterion=stopping_criterion, verbose=True, ) print("------ [done] sol - with backend") assert_allclose_bary_sol(sol, solb) # except NotImplementedError: # pytest.skip("Not implemented") @pytest.mark.parametrize( "method_params", lst_parameters_solve_bary_sample_NotImplemented ) def test_solve_bary_sample_NotImplemented(nx, method_params): # test bary_sample when is_Lazy = False rng = np.random.RandomState() K = 2 # number of distributions ns = rng.randint(10, 20, K) # number of samples within each distribution n = 5 # number of samples in the barycenter X_list = [rng.randn(ns_i, 2) for ns_i in ns] # X_init = np.reshape(1.0 * np.randn(n, 2), (n, 1)) a_list = [ot.utils.unif(X.shape[0]) for X in X_list] b = ot.utils.unif(n) w = ot.utils.unif(K) # solve in backend X_listb = nx.from_numpy(*X_list) a_listb = nx.from_numpy(*a_list) wb, bb = nx.from_numpy(w, b) with pytest.raises(NotImplementedError): ot.solve_bary_sample(X_listb, n, a_list=a_listb, b=bb, w=wb, **method_params) def test_solve_bary_sample_ValueError(nx): # Test ValueError cases: stopping_criterion and X_b_init shape rng = np.random.RandomState(42) K = 2 ns = [10, 12] n = 5 X_list = [rng.randn(ns_i, 2) for ns_i in ns] a_list = [ot.utils.unif(ns_i) for ns_i in ns] w = ot.utils.unif(K) X_listb = nx.from_numpy(*X_list) a_listb = nx.from_numpy(*a_list) wb = nx.from_numpy(w) # Test Invalid stopping_criterion with pytest.raises(ValueError, match="stopping_criterion must be"): ot.solve_bary_sample( X_listb, n, a_list=a_listb, w=wb, stopping_criterion="invalid" ) # Test Invalid X_b_init shape bad_X_b_init = rng.randn(n + 1, 2) bad_X_b_init = nx.from_numpy(bad_X_b_init) with pytest.raises(ValueError, match="X_b_init must have shape"): ot.solve_bary_sample(X_listb, n, X_b_init=bad_X_b_init, a_list=a_listb, w=wb) def test_solve_bary_sample_callable_metric(nx): # Test callable metric paths rng = np.random.RandomState(42) K = 2 ns = [8, 9] n = 4 X_list = [rng.randn(ns_i, 2) for ns_i in ns] a_list = [ot.utils.unif(ns_i) for ns_i in ns] w = ot.utils.unif(K) X_listb = nx.from_numpy(*X_list) a_listb = nx.from_numpy(*a_list) wb = nx.from_numpy(w) # Define custom metric function def custom_metric(X_b, X_a): return nx.sqrt(nx.sum((X_b[:, None, :] - X_a[None, :, :]) ** 2, axis=2)) if str(nx) == "torch": # Test single callable metric sol_callable = ot.solve_bary_sample( X_listb, n, a_list=a_listb, w=wb, metric=custom_metric, max_iter_bary=1 ) assert sol_callable is not None assert sol_callable.X is not None # Test list of callable metrics metrics_list = [custom_metric, custom_metric] sol_list = ot.solve_bary_sample( X_listb, n, a_list=a_listb, w=wb, metric=metrics_list, max_iter_bary=1 ) assert sol_list is not None assert sol_list.X is not None # Test with auto_bary_method="true_fixed_point" with pytest.raises( ValueError, match="ground_bary must be provided in kwargs for true_fixed_point method with callable metrics", ): sol_fixed_point = ot.solve_bary_sample( X_listb, n, a_list=a_listb, w=wb, metric=custom_metric, auto_bary_method="true_fixed_point", max_iter_bary=1, ) else: with pytest.raises( AssertionError, match=f"Backend {str(nx)} is not compatible with ground_bary=None, it must be provided if not using PyTorch backend", ): sol_wrong_backend = ot.solve_bary_sample( X_listb, n, a_list=a_listb, w=wb, metric=custom_metric, max_iter_bary=1, ) python-pot-0.9.7+dfsg/test/test_stochastic.py000066400000000000000000000201661525521106200213270ustar00rootroot00000000000000""" ========================== Stochastic test ========================== This example is designed to test the stochatic optimization algorithms module for descrete and semicontinous measures from the POT library. """ # Authors: Kilian Fatras # Rémi Flamary # # License: MIT License import numpy as np import ot ############################################################################# # COMPUTE TEST FOR SEMI-DUAL PROBLEM ############################################################################# ############################################################################# # # TEST SAG algorithm # --------------------------------------------- # 2 identical discrete measures u defined on the same space with a # regularization term, a learning rate and a number of iteration def test_stochastic_sag(): # test sag n = 10 reg = 1 numItermax = 30000 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) G = ot.stochastic.solve_semi_dual_entropic( u, u, M, reg, "sag", numItermax=numItermax ) # check constraints np.testing.assert_allclose(u, G.sum(1), atol=1e-03) # cf convergence sag np.testing.assert_allclose(u, G.sum(0), atol=1e-03) # cf convergence sag ############################################################################# # # TEST ASGD algorithm # --------------------------------------------- # 2 identical discrete measures u defined on the same space with a # regularization term, a learning rate and a number of iteration def test_stochastic_asgd(): # test asgd n = 10 reg = 1 numItermax = 10000 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) G, log = ot.stochastic.solve_semi_dual_entropic( u, u, M, reg, "asgd", numItermax=numItermax, log=True ) # check constraints np.testing.assert_allclose(u, G.sum(1), atol=1e-02) # cf convergence asgd np.testing.assert_allclose(u, G.sum(0), atol=1e-02) # cf convergence asgd ############################################################################# # # TEST Convergence SAG and ASGD toward Sinkhorn's solution # -------------------------------------------------------- # 2 identical discrete measures u defined on the same space with a # regularization term, a learning rate and a number of iteration def test_sag_asgd_sinkhorn(): # test all algorithms n = 10 reg = 1 nb_iter = 10000 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) G_asgd = ot.stochastic.solve_semi_dual_entropic( u, u, M, reg, "asgd", numItermax=nb_iter ) G_sag = ot.stochastic.solve_semi_dual_entropic( u, u, M, reg, "sag", numItermax=nb_iter ) G_sinkhorn = ot.sinkhorn(u, u, M, reg) # check constraints np.testing.assert_allclose(G_sag.sum(1), G_sinkhorn.sum(1), atol=1e-02) np.testing.assert_allclose(G_sag.sum(0), G_sinkhorn.sum(0), atol=1e-02) np.testing.assert_allclose(G_asgd.sum(1), G_sinkhorn.sum(1), atol=1e-02) np.testing.assert_allclose(G_asgd.sum(0), G_sinkhorn.sum(0), atol=1e-02) np.testing.assert_allclose(G_sag, G_sinkhorn, atol=1e-02) # cf convergence sag np.testing.assert_allclose(G_asgd, G_sinkhorn, atol=1e-02) # cf convergence asgd ############################################################################# # COMPUTE TEST FOR DUAL PROBLEM ############################################################################# ############################################################################# # # TEST SGD algorithm # --------------------------------------------- # 2 identical discrete measures u defined on the same space with a # regularization term, a batch_size and a number of iteration def test_stochastic_dual_sgd(): # test sgd n = 10 reg = 1 numItermax = 5000 batch_size = 10 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) G, log = ot.stochastic.solve_dual_entropic( u, u, M, reg, batch_size, numItermax=numItermax, log=True ) # check constraints np.testing.assert_allclose(u, G.sum(1), atol=1e-03) # cf convergence sgd np.testing.assert_allclose(u, G.sum(0), atol=1e-03) # cf convergence sgd ############################################################################# # # TEST Convergence SGD toward Sinkhorn's solution # -------------------------------------------------------- # 2 identical discrete measures u defined on the same space with a # regularization term, a batch_size and a number of iteration def test_dual_sgd_sinkhorn(): # test all dual algorithms n = 10 reg = 1 nb_iter = 5000 batch_size = 10 rng = np.random.RandomState(0) # Test uniform x = rng.randn(n, 2) u = ot.utils.unif(n) M = ot.dist(x, x) G_sgd = ot.stochastic.solve_dual_entropic( u, u, M, reg, batch_size, numItermax=nb_iter ) G_sinkhorn = ot.sinkhorn(u, u, M, reg) # check constraints np.testing.assert_allclose(G_sgd.sum(1), G_sinkhorn.sum(1), atol=1e-02) np.testing.assert_allclose(G_sgd.sum(0), G_sinkhorn.sum(0), atol=1e-02) np.testing.assert_allclose(G_sgd, G_sinkhorn, atol=1e-02) # cf convergence sgd # Test gaussian n = 30 reg = 1 batch_size = 30 a = ot.datasets.make_1D_gauss(n, 15, 5) # m= mean, s= std b = ot.datasets.make_1D_gauss(n, 15, 5) X_source = np.arange(n, dtype=np.float64) Y_target = np.arange(n, dtype=np.float64) M = ot.dist(X_source.reshape((n, 1)), Y_target.reshape((n, 1))) M /= M.max() G_sgd = ot.stochastic.solve_dual_entropic( a, b, M, reg, batch_size, numItermax=nb_iter ) G_sinkhorn = ot.sinkhorn(a, b, M, reg) # check constraints np.testing.assert_allclose(G_sgd.sum(1), G_sinkhorn.sum(1), atol=1e-03) np.testing.assert_allclose(G_sgd.sum(0), G_sinkhorn.sum(0), atol=1e-03) np.testing.assert_allclose(G_sgd, G_sinkhorn, atol=1e-03) # cf convergence sgd def test_loss_dual_entropic(nx): nx.seed(0) xs = nx.randn(50, 2) xt = nx.randn(40, 2) + 2 ws = nx.rand(50) ws = ws / nx.sum(ws) wt = nx.rand(40) wt = wt / nx.sum(wt) u = nx.randn(50) v = nx.randn(40) def metric(x, y): return -nx.dot(x, y.T) ot.stochastic.loss_dual_entropic(u, v, xs, xt) ot.stochastic.loss_dual_entropic(u, v, xs, xt, ws=ws, wt=wt, metric=metric) def test_plan_dual_entropic(nx): nx.seed(0) xs = nx.randn(50, 2) xt = nx.randn(40, 2) + 2 ws = nx.rand(50) ws = ws / nx.sum(ws) wt = nx.rand(40) wt = wt / nx.sum(wt) u = nx.randn(50) v = nx.randn(40) def metric(x, y): return -nx.dot(x, y.T) G1 = ot.stochastic.plan_dual_entropic(u, v, xs, xt) assert np.all(nx.to_numpy(G1) >= 0) assert G1.shape[0] == 50 assert G1.shape[1] == 40 G2 = ot.stochastic.plan_dual_entropic(u, v, xs, xt, ws=ws, wt=wt, metric=metric) assert np.all(nx.to_numpy(G2) >= 0) assert G2.shape[0] == 50 assert G2.shape[1] == 40 def test_loss_dual_quadratic(nx): nx.seed(0) xs = nx.randn(50, 2) xt = nx.randn(40, 2) + 2 ws = nx.rand(50) ws = ws / nx.sum(ws) wt = nx.rand(40) wt = wt / nx.sum(wt) u = nx.randn(50) v = nx.randn(40) def metric(x, y): return -nx.dot(x, y.T) ot.stochastic.loss_dual_quadratic(u, v, xs, xt) ot.stochastic.loss_dual_quadratic(u, v, xs, xt, ws=ws, wt=wt, metric=metric) def test_plan_dual_quadratic(nx): nx.seed(0) xs = nx.randn(50, 2) xt = nx.randn(40, 2) + 2 ws = nx.rand(50) ws = ws / nx.sum(ws) wt = nx.rand(40) wt = wt / nx.sum(wt) u = nx.randn(50) v = nx.randn(40) def metric(x, y): return -nx.dot(x, y.T) G1 = ot.stochastic.plan_dual_quadratic(u, v, xs, xt) assert np.all(nx.to_numpy(G1) >= 0) assert G1.shape[0] == 50 assert G1.shape[1] == 40 G2 = ot.stochastic.plan_dual_quadratic(u, v, xs, xt, ws=ws, wt=wt, metric=metric) assert np.all(nx.to_numpy(G2) >= 0) assert G2.shape[0] == 50 assert G2.shape[1] == 40 python-pot-0.9.7+dfsg/test/test_ucoot.py000066400000000000000000001133111525521106200203070ustar00rootroot00000000000000"""Tests for module Unbalanced Co-Optimal Transport""" # Author: Quang Huy Tran # # License: MIT License import itertools import numpy as np import ot import pytest from ot.gromov._unbalanced import ( unbalanced_co_optimal_transport, unbalanced_co_optimal_transport2, ) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence", itertools.product(["mm", "lbfgsb"], ["kl", "l2"]) ) def test_sanity(nx, unbalanced_solver, divergence): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) xs_nx, xt_nx = nx.from_numpy(xs, xt) px_s_nx, px_f_nx, py_s_nx, py_f_nx = nx.from_numpy(px_s, px_f, py_s, py_f) reg_m = (10, 5) eps = 0 max_iter_ot = 200 max_iter = 200 tol = 1e-7 tol_ot = 1e-7 # test couplings anti_id_sample = np.flipud(np.eye(n_samples, n_samples)) / n_samples id_feature = np.eye(2, 2) / 2 pi_sample, pi_feature = unbalanced_co_optimal_transport( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=0, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx, pi_feature_nx = unbalanced_co_optimal_transport( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=0, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample, anti_id_sample, atol=1e-05) np.testing.assert_allclose(pi_sample_nx, pi_sample, atol=1e-06) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-06) np.testing.assert_allclose(pi_feature, id_feature, atol=1e-05) # test divergence ucoot = unbalanced_co_optimal_transport2( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=0, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) ucoot_nx = unbalanced_co_optimal_transport2( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=0, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) ucoot_nx = nx.to_numpy(ucoot_nx) np.testing.assert_allclose(ucoot, ucoot_nx, atol=1e-08) np.testing.assert_allclose(ucoot, 0, atol=1e-06) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence, eps", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1] ), ) def test_init_plans(nx, unbalanced_solver, divergence, eps): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) G0_samp = px_s[:, None] * py_s[None, :] G0_feat = px_f[:, None] * py_f[None, :] xs_nx, xt_nx, G0_samp_nx, G0_feat_nx = nx.from_numpy(xs, xt, G0_samp, G0_feat) px_s_nx, px_f_nx, py_s_nx, py_f_nx = nx.from_numpy(px_s, px_f, py_s, py_f) reg_m = (1, 5) alpha = (0.1, 0.2) max_iter_ot = 5 max_iter = 5 tol = 1e-7 tol_ot = 1e-7 # test couplings pi_sample, pi_feature = unbalanced_co_optimal_transport( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx, pi_feature_nx = unbalanced_co_optimal_transport( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=(G0_samp_nx, G0_feat_nx), init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample_nx, pi_sample, atol=1e-03) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-03) # test divergence ucoot = unbalanced_co_optimal_transport2( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) ucoot_nx = unbalanced_co_optimal_transport2( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=(G0_samp_nx, G0_feat_nx), init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) ucoot_nx = nx.to_numpy(ucoot_nx) np.testing.assert_allclose(ucoot, ucoot_nx, atol=1e-08) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence, eps", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1] ), ) def test_init_duals(nx, unbalanced_solver, divergence, eps): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) xs_nx, xt_nx = nx.from_numpy(xs, xt) px_s_nx, px_f_nx, py_s_nx, py_f_nx = nx.from_numpy(px_s, px_f, py_s, py_f) init_duals_samp = nx.from_numpy(np.zeros(n_samples), np.zeros(n_samples)) init_duals_feat = nx.from_numpy(np.zeros(2), np.zeros(2)) init_duals = (init_duals_samp, init_duals_feat) reg_m = (10, 5) alpha = (0.1, 0.2) max_iter_ot = 5 max_iter = 5 tol = 1e-7 tol_ot = 1e-7 # test couplings pi_sample, pi_feature = unbalanced_co_optimal_transport( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx, pi_feature_nx = unbalanced_co_optimal_transport( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=init_duals, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample_nx, pi_sample, atol=1e-03) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-03) # test divergence ucoot = unbalanced_co_optimal_transport2( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) ucoot_nx = unbalanced_co_optimal_transport2( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=init_duals, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) ucoot_nx = nx.to_numpy(ucoot_nx) np.testing.assert_allclose(ucoot, ucoot_nx, atol=1e-08) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence, eps", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1e-2] ), ) def test_linear_part(nx, unbalanced_solver, divergence, eps): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) xs_nx, xt_nx = nx.from_numpy(xs, xt) px_s_nx, px_f_nx, py_s_nx, py_f_nx = nx.from_numpy(px_s, px_f, py_s, py_f) # linear part M_samp = np.ones((n_samples, n_samples)) np.fill_diagonal(np.fliplr(M_samp), 0) M_feat = np.ones((2, 2)) np.fill_diagonal(M_feat, 0) M_samp_nx, M_feat_nx = nx.from_numpy(M_samp, M_feat) reg_m = (10, 5) alpha = (0.1, 0.2) max_iter_ot = 5 max_iter = 5 tol = 1e-7 tol_ot = 1e-7 # test couplings pi_sample, pi_feature = unbalanced_co_optimal_transport( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=M_samp, M_feat=M_feat, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx, pi_feature_nx = unbalanced_co_optimal_transport( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=M_samp_nx, M_feat=M_feat_nx, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample_nx, pi_sample, atol=1e-06) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-06) # test divergence ucoot = unbalanced_co_optimal_transport2( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=M_samp, M_feat=M_feat, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) ucoot_nx = unbalanced_co_optimal_transport2( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=M_samp_nx, M_feat=M_feat_nx, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) ucoot_nx = nx.to_numpy(ucoot_nx) np.testing.assert_allclose(ucoot, ucoot_nx, atol=1e-08) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence, eps", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1] ), ) def test_reg_marginals(nx, unbalanced_solver, divergence, eps): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) xs_nx, xt_nx = nx.from_numpy(xs, xt) px_s_nx, px_f_nx, py_s_nx, py_f_nx = nx.from_numpy(px_s, px_f, py_s, py_f) alpha = (0.1, 0.2) max_iter_ot = 5 max_iter = 5 tol = 1e-7 tol_ot = 1e-7 reg_m = 100 full_list_reg_m = [reg_m, reg_m] full_tuple_reg_m = (reg_m, reg_m) tuple_reg_m, list_reg_m = (reg_m), [reg_m] list_options = [full_tuple_reg_m, tuple_reg_m, full_list_reg_m, list_reg_m] # test couplings pi_sample, pi_feature = unbalanced_co_optimal_transport( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) # test divergence ucoot = unbalanced_co_optimal_transport2( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) for opt in list_options: pi_sample_nx, pi_feature_nx = unbalanced_co_optimal_transport( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=opt, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample_nx, pi_sample, atol=1e-06) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-06) # test divergence ucoot_nx = unbalanced_co_optimal_transport2( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=opt, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, method_sinkhorn="sinkhorn", log=False, verbose=False, ) ucoot_nx = nx.to_numpy(ucoot_nx) np.testing.assert_allclose(ucoot, ucoot_nx, atol=1e-08) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence, alpha", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1] ), ) def test_eps(nx, unbalanced_solver, divergence, alpha): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) xs_nx, xt_nx = nx.from_numpy(xs, xt) px_s_nx, px_f_nx, py_s_nx, py_f_nx = nx.from_numpy(px_s, px_f, py_s, py_f) reg_m = (10, 5) alpha = (0.1, 0.2) max_iter_ot = 5 max_iter = 5 tol = 1e-7 tol_ot = 1e-7 eps = 1 full_list_eps = [eps, eps] full_tuple_eps = (eps, eps) tuple_eps, list_eps = (eps), [eps] list_options = [full_list_eps, full_tuple_eps, tuple_eps, list_eps] # test couplings pi_sample, pi_feature = unbalanced_co_optimal_transport( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) # test divergence ucoot = unbalanced_co_optimal_transport2( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) for opt in list_options: pi_sample_nx, pi_feature_nx = unbalanced_co_optimal_transport( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=opt, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample_nx, pi_sample, atol=1e-06) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-06) # test divergence ucoot_nx = unbalanced_co_optimal_transport2( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=opt, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) ucoot_nx = nx.to_numpy(ucoot_nx) np.testing.assert_allclose(ucoot, ucoot_nx, atol=1e-08) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence, eps", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1e-2] ), ) def test_alpha(nx, unbalanced_solver, divergence, eps): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) xs_nx, xt_nx = nx.from_numpy(xs, xt) px_s_nx, px_f_nx, py_s_nx, py_f_nx = nx.from_numpy(px_s, px_f, py_s, py_f) # linear part M_samp = np.ones((n_samples, n_samples)) np.fill_diagonal(np.fliplr(M_samp), 0) M_feat = np.ones((2, 2)) np.fill_diagonal(M_feat, 0) M_samp_nx, M_feat_nx = nx.from_numpy(M_samp, M_feat) reg_m = (10, 5) max_iter_ot = 5 max_iter = 5 tol = 1e-7 tol_ot = 1e-7 alpha = 1 full_list_alpha = [alpha, alpha] full_tuple_alpha = (alpha, alpha) tuple_alpha, list_alpha = (alpha), [alpha] list_options = [full_list_alpha, full_tuple_alpha, tuple_alpha, list_alpha] # test couplings pi_sample, pi_feature = unbalanced_co_optimal_transport( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=M_samp, M_feat=M_feat, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) # test divergence ucoot = unbalanced_co_optimal_transport2( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=M_samp, M_feat=M_feat, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) for opt in list_options: pi_sample_nx, pi_feature_nx = unbalanced_co_optimal_transport( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=opt, M_samp=M_samp_nx, M_feat=M_feat_nx, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample_nx, pi_sample, atol=1e-06) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-06) ucoot_nx = unbalanced_co_optimal_transport2( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=opt, M_samp=M_samp_nx, M_feat=M_feat_nx, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) ucoot_nx = nx.to_numpy(ucoot_nx) np.testing.assert_allclose(ucoot, ucoot_nx, atol=1e-08) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence, eps", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1] ), ) def test_log(nx, unbalanced_solver, divergence, eps): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) xs_nx, xt_nx = nx.from_numpy(xs, xt) px_s_nx, px_f_nx, py_s_nx, py_f_nx = nx.from_numpy(px_s, px_f, py_s, py_f) reg_m = (10, 5) alpha = (0.1, 0.2) max_iter_ot = 5 max_iter = 5 tol = 1e-7 tol_ot = 1e-7 # test couplings pi_sample, pi_feature = unbalanced_co_optimal_transport( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx, pi_feature_nx, log = unbalanced_co_optimal_transport( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=True, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample_nx, pi_sample, atol=1e-06) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-06) # test divergence ucoot = unbalanced_co_optimal_transport2( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) ucoot_nx = unbalanced_co_optimal_transport2( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) ucoot_nx = nx.to_numpy(ucoot_nx) np.testing.assert_allclose(ucoot, ucoot_nx, atol=1e-08) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") @pytest.mark.parametrize( "unbalanced_solver, divergence, eps", itertools.product( ["sinkhorn", "sinkhorn_log", "mm", "lbfgsb"], ["kl", "l2"], [0, 1] ), ) def test_marginals(nx, unbalanced_solver, divergence, eps): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) xs_nx, xt_nx = nx.from_numpy(xs, xt) px_s_nx, px_f_nx, py_s_nx, py_f_nx = nx.from_numpy(px_s, px_f, py_s, py_f) reg_m = (10, 5) alpha = (0.1, 0.2) max_iter_ot = 5 max_iter = 5 tol = 1e-7 tol_ot = 1e-7 # test couplings pi_sample, pi_feature = unbalanced_co_optimal_transport( X=xs, Y=xt, wx_samp=None, wx_feat=None, wy_samp=None, wy_feat=None, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx, pi_feature_nx = unbalanced_co_optimal_transport( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) pi_sample_nx = nx.to_numpy(pi_sample_nx) pi_feature_nx = nx.to_numpy(pi_feature_nx) np.testing.assert_allclose(pi_sample_nx, pi_sample, atol=1e-06) np.testing.assert_allclose(pi_feature, pi_feature_nx, atol=1e-06) # test divergence ucoot = unbalanced_co_optimal_transport2( X=xs, Y=xt, wx_samp=None, wx_feat=None, wy_samp=None, wy_feat=None, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) ucoot_nx = unbalanced_co_optimal_transport2( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver=unbalanced_solver, alpha=alpha, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) ucoot_nx = nx.to_numpy(ucoot_nx) np.testing.assert_allclose(ucoot, ucoot_nx, atol=1e-08) @pytest.skip_backend("jax", reason="test very slow with jax backend") @pytest.skip_backend("tf", reason="test very slow with tensorflow backend") def test_raise_value_error(nx): n_samples = 5 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s, random_state=4) xt = xs[::-1].copy() px_s, px_f = ot.unif(n_samples), ot.unif(2) py_s, py_f = ot.unif(n_samples), ot.unif(2) xs_nx, xt_nx = nx.from_numpy(xs, xt) px_s_nx, px_f_nx, py_s_nx, py_f_nx = nx.from_numpy(px_s, px_f, py_s, py_f) reg_m = (10, 5) eps = 0 max_iter_ot = 5 max_iter = 5 tol = 1e-7 tol_ot = 1e-7 # raise error of divergence def ucoot_div(divergence): return unbalanced_co_optimal_transport( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver="mm", alpha=0, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) def ucoot_div_nx(divergence): return unbalanced_co_optimal_transport( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence=divergence, unbalanced_solver="mm", alpha=0, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) np.testing.assert_raises(NotImplementedError, ucoot_div, "div_not_existed") np.testing.assert_raises(NotImplementedError, ucoot_div_nx, "div_not_existed") # raise error of solver def ucoot_solver(unbalanced_solver): return unbalanced_co_optimal_transport( X=xs, Y=xt, wx_samp=px_s, wx_feat=px_f, wy_samp=py_s, wy_feat=py_f, reg_marginals=reg_m, epsilon=eps, divergence="kl", unbalanced_solver=unbalanced_solver, alpha=0, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) def ucoot_solver_nx(unbalanced_solver): return unbalanced_co_optimal_transport( X=xs_nx, Y=xt_nx, wx_samp=px_s_nx, wx_feat=px_f_nx, wy_samp=py_s_nx, wy_feat=py_f_nx, reg_marginals=reg_m, epsilon=eps, divergence="kl", unbalanced_solver=unbalanced_solver, alpha=0, M_samp=None, M_feat=None, init_pi=None, init_duals=None, max_iter=max_iter, tol=tol, max_iter_ot=max_iter_ot, tol_ot=tol_ot, log=False, verbose=False, ) np.testing.assert_raises(NotImplementedError, ucoot_solver, "solver_not_existed") np.testing.assert_raises(NotImplementedError, ucoot_solver_nx, "solver_not_existed") python-pot-0.9.7+dfsg/test/test_utils.py000066400000000000000000000650361525521106200203300ustar00rootroot00000000000000"""Tests for module utils for timing and parallel computation""" # Author: Remi Flamary # # License: MIT License import ot import numpy as np import sys import pytest import scipy lst_metrics = [ "euclidean", "sqeuclidean", "cityblock", "cosine", "minkowski", "correlation", ] lst_all_metrics_candidates = lst_metrics + [ "braycurtis", "canberra", "chebyshev", "dice", "hamming", "jaccard", "matching", "rogerstanimoto", "russellrao", "sokalmichener", "sokalsneath", "yule", ] # Filter to only include metrics available in current SciPy version # (some metrics like sokalmichener were removed in newer SciPy versions) lst_all_metrics = [] for metric in lst_all_metrics_candidates: try: scipy.spatial.distance.cdist( np.array([[0, 0]]), np.array([[1, 1]]), metric=metric ) lst_all_metrics.append(metric) except ValueError: pass def get_LazyTensor(nx): n1 = 100 n2 = 200 rng = np.random.RandomState(42) a = rng.rand(n1) a /= a.sum() b = rng.rand(n2) b /= b.sum() a, b = nx.from_numpy(a, b) def getitem(i, j, a, b): return a[i, None] * b[None, j] # create a lazy tensor T = ot.utils.LazyTensor((n1, n2), getitem, a=a, b=b) return T, a, b def test_proj_simplex(nx): n = 10 rng = np.random.RandomState(0) # test on matrix when projection is done on axis 0 x = rng.randn(n, 2) x1 = nx.from_numpy(x) # all projections should sum to 1 proj = ot.utils.proj_simplex(x1) l1 = np.sum(nx.to_numpy(proj), axis=0) l2 = np.ones(2) np.testing.assert_allclose(l1, l2, atol=1e-5) # all projections should sum to 3 proj = ot.utils.proj_simplex(x1, 3) l1 = np.sum(nx.to_numpy(proj), axis=0) l2 = 3 * np.ones(2) np.testing.assert_allclose(l1, l2, atol=1e-5) # tets on vector x = rng.randn(n) x1 = nx.from_numpy(x) # all projections should sum to 1 proj = ot.utils.proj_simplex(x1) l1 = np.sum(nx.to_numpy(proj), axis=0) l2 = np.ones(2) np.testing.assert_allclose(l1, l2, atol=1e-5) def test_projection_sparse_simplex(): def double_sort_projection_sparse_simplex(X, max_nz, z=1, axis=None): r"""This is an equivalent but less efficient version of ot.utils.projection_sparse_simplex, as it uses two sorts instead of one. """ if axis == 0: # For each column of X, find top max_nz values and # their corresponding indices. This incurs a sort. max_nz_indices = np.argpartition(X, kth=-max_nz, axis=0)[-max_nz:] max_nz_values = X[max_nz_indices, np.arange(X.shape[1])] # Project the top max_nz values onto the simplex. # This incurs a second sort. G_nz_values = ot.smooth.projection_simplex(max_nz_values, z=z, axis=0) # Put the projection of max_nz_values to their original indices # and set all other values zero. G = np.zeros_like(X) G[max_nz_indices, np.arange(X.shape[1])] = G_nz_values return G elif axis == 1: return double_sort_projection_sparse_simplex(X.T, max_nz, z, axis=0).T else: X = X.ravel().reshape(-1, 1) return double_sort_projection_sparse_simplex(X, max_nz, z, axis=0).ravel() m, n = 5, 10 rng = np.random.RandomState(0) X = rng.uniform(size=(m, n)) max_nz = 3 for axis in [0, 1, None]: slow_sparse_proj = double_sort_projection_sparse_simplex(X, max_nz, axis=axis) fast_sparse_proj = ot.utils.projection_sparse_simplex(X, max_nz, axis=axis) # check that two versions produce consistent results np.testing.assert_allclose(slow_sparse_proj, fast_sparse_proj) def test_parmap(): n = 10 def f(i): return 1.0 * i * i a = np.arange(n) l1 = list(map(f, a)) l2 = list(ot.utils.parmap(f, a)) np.testing.assert_allclose(l1, l2) def test_tic_toc(): import time ot.tic() time.sleep(0.1) t = ot.toc() t2 = ot.toq() # test timing # np.testing.assert_allclose(0.1, t, rtol=1e-1, atol=1e-1) # very slow macos github action equality not possible assert t > 0.09 # test toc vs toq np.testing.assert_allclose(t, t2, rtol=1e-1, atol=1e-1) def test_kernel(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) K = ot.utils.kernel(x, x) # gaussian kernel has ones on the diagonal np.testing.assert_allclose(np.diag(K), np.ones(n)) def test_unif(): n = 100 u = ot.unif(n) np.testing.assert_allclose(1, np.sum(u)) def test_unif_backend(nx): n = 100 for tp in nx.__type_list__: print(nx.dtype_device(tp)) u = ot.unif(n, type_as=tp) np.testing.assert_allclose(1, np.sum(nx.to_numpy(u)), atol=1e-6) def test_dist(): n = 10 rng = np.random.RandomState(0) x = rng.randn(n, 2) D = np.zeros((n, n)) for i in range(n): for j in range(n): D[i, j] = np.sum(np.square(x[i, :] - x[j, :])) D2 = ot.dist(x, x) D3 = ot.dist(x) D4 = ot.dist(x, x, metric="minkowski", p=2) assert D4[0, 1] == D4[1, 0] # dist should return squared euclidean np.testing.assert_allclose(D, D2, atol=1e-14) np.testing.assert_allclose(D, D3, atol=1e-14) # tests that every metric runs correctly metrics_w = [ "braycurtis", "canberra", "chebyshev", "cityblock", "correlation", "cosine", "dice", "euclidean", "hamming", "jaccard", "matching", "minkowski", "rogerstanimoto", "russellrao", "sokalmichener", "sokalsneath", "sqeuclidean", "yule", ] # those that support weights metrics = [ "mahalanobis", "seuclidean", ] # do not support weights depending on scipy's version # Filter out metrics not available in current scipy version from scipy.spatial.distance import cdist available_metrics_w = [] for metric in metrics_w: try: cdist(x[:2], x[:2], metric=metric) available_metrics_w.append(metric) except ValueError: pass for metric in available_metrics_w: print(metric) ot.dist(x, x, metric=metric, p=3, w=rng.random((2,))) ot.dist( x, x, metric=metric, p=3, w=None ) # check that not having any weight does not cause issues for metric in metrics: print(metric) ot.dist(x, x, metric=metric, p=3) # weighted minkowski but with no weights with pytest.raises(ValueError): ot.dist(x, x, metric="wminkowski") with pytest.raises(ValueError): ot.dist(x, x, metric="fakeone") @pytest.mark.parametrize("metric", lst_metrics) def test_dist_backends(nx, metric): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) x1 = nx.from_numpy(x) # force numpy backend D0 = ot.dist(x, x, metric=metric, backend="numpy") # default backend D = ot.dist(x, x, metric=metric) # force nx arrays D1 = ot.dist(x1, x1, metric=metric) # low atol because jax forces float32 np.testing.assert_allclose(D, nx.to_numpy(D1), atol=1e-5) np.testing.assert_allclose(D, D0, atol=1e-5) @pytest.mark.parametrize("metric", lst_all_metrics) def test_dist_vs_cdist(metric): n = 10 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(n + 1, 2) D = ot.dist(x, y, metric=metric) Dt = ot.dist(x, y, metric=metric, use_tensor=True) D2 = scipy.spatial.distance.cdist(x, y, metric=metric) np.testing.assert_allclose(D, D2, atol=1e-15) np.testing.assert_allclose(D, Dt, atol=1e-15) def test_dist0(): n = 100 M = ot.utils.dist0(n, method="lin_square") # dist0 default to linear sampling with quadratic loss np.testing.assert_allclose(M[0, -1], (n - 1) * (n - 1)) def test_dots(): n1, n2, n3, n4 = 100, 50, 200, 100 rng = np.random.RandomState(0) A = rng.randn(n1, n2) B = rng.randn(n2, n3) C = rng.randn(n3, n4) X1 = ot.utils.dots(A, B, C) X2 = A.dot(B.dot(C)) np.testing.assert_allclose(X1, X2) def test_clean_zeros(): n = 100 nz = 50 nz2 = 20 u1 = ot.unif(n) u1[:nz] = 0 u1 = u1 / u1.sum() u2 = ot.unif(n) u2[:nz2] = 0 u2 = u2 / u2.sum() M = ot.utils.dist0(n) a, b, M2 = ot.utils.clean_zeros(u1, u2, M) assert len(a) == n - nz assert len(b) == n - nz2 def test_cost_normalization(nx): rng = np.random.RandomState(0) C = rng.rand(10, 10) C1 = nx.from_numpy(C) # does nothing M0 = ot.utils.cost_normalization(C1) M1 = nx.to_numpy(M0) np.testing.assert_allclose(C, M1) M = ot.utils.cost_normalization(C1, "median") M1 = nx.to_numpy(M) np.testing.assert_allclose(np.median(M1), 1) M = ot.utils.cost_normalization(C1, "max") M1 = nx.to_numpy(M) np.testing.assert_allclose(M1.max(), 1) M = ot.utils.cost_normalization(C1, "log") M1 = nx.to_numpy(M) np.testing.assert_allclose(M1.max(), np.log(1 + C).max()) M = ot.utils.cost_normalization(C1, "loglog") M1 = nx.to_numpy(M) np.testing.assert_allclose(M1.max(), np.log(1 + np.log(1 + C)).max()) with pytest.raises(ValueError): ot.utils.cost_normalization(C1, "error") def test_list_to_array(nx): lst = [np.array([1, 2, 3]), np.array([4, 5, 6])] a1, a2 = ot.utils.list_to_array(*lst) assert a1.shape == (3,) assert a2.shape == (3,) a, b, M = ot.utils.list_to_array([], [], [[1.0, 2.0], [3.0, 4.0]]) def test_check_params(): res1 = ot.utils.check_params(first="OK", second=20) assert res1 is True res0 = ot.utils.check_params(first="OK", second=None) assert res0 is False def test_check_random_state_error(): with pytest.raises(ValueError): ot.utils.check_random_state("error") def test_get_parameter_pair_error(): with pytest.raises(ValueError): ot.utils.get_parameter_pair((1, 2, 3)) # not pair ;) def test_deprecated_func(): @ot.utils.deprecated("deprecated text for fun") def fun(): pass def fun2(): pass @ot.utils.deprecated("deprecated text for class") class Class: pass with pytest.warns(DeprecationWarning): fun() with pytest.warns(DeprecationWarning): cl = Class() print(cl) if sys.version_info < (3, 5): print("Not tested") else: assert ot.utils._is_deprecated(fun) is True assert ot.utils._is_deprecated(fun2) is False def test_BaseEstimator(): class Class(ot.utils.BaseEstimator): def __init__(self, first="spam", second="eggs"): self.first = first self.second = second cl = Class() names = cl._get_param_names() assert "first" in names assert "second" in names params = cl.get_params() assert "first" in params assert "second" in params params["first"] = "spam again" cl.set_params(**params) with pytest.raises(ValueError): cl.set_params(bibi=10) assert cl.first == "spam again" def test_OTResult(): res = ot.utils.OTResult() # test print print(res) # test get citation print(res.citation) lst_attributes = [ "lazy_plan", "marginal_a", "marginal_b", "marginals", "plan", "potential_a", "potential_b", "potentials", "sparse_plan", "status", "value", "value_linear", "value_quad", "log", ] for at in lst_attributes: print(at) assert getattr(res, at) is None list_not_implemented = ["a_to_b", "b_to_a"] for at in list_not_implemented: print(at) with pytest.raises(NotImplementedError): getattr(res, at) def test_BaryResult(): res = ot.utils.BaryResult() # test print print(res) # test get citation print(res.citation) lst_attributes = [ "X", "C", "b", "value", "value_linear", "value_quad", "list_res", "status", "log", ] for at in lst_attributes: print(at) assert getattr(res, at) is None def test_get_coordinate_circle(): rng = np.random.RandomState(42) u = rng.rand(1, 100) x1, y1 = np.cos(u * (2 * np.pi)), np.sin(u * (2 * np.pi)) x = np.concatenate([x1, y1]).T x_p = ot.utils.get_coordinate_circle(x) np.testing.assert_allclose(u[0], x_p) def test_LazyTensor(nx): n1 = 100 n2 = 200 shape = (n1, n2) rng = np.random.RandomState(42) x1 = rng.randn(n1, 2) x2 = rng.randn(n2, 2) x1, x2 = nx.from_numpy(x1, x2) # i,j can be integers or slices, x1,x2 have to be passed as keyword arguments def getitem(i, j, x1, x2): return nx.dot(x1[i], x2[j].T) # create a lazy tensor T = ot.utils.LazyTensor((n1, n2), getitem, x1=x1, x2=x2) assert T.shape == (n1, n2) assert str(T) == "LazyTensor(shape=(100, 200),attributes=(x1,x2))" assert T.x1 is x1 assert T.x2 is x2 # get the full tensor (not lazy) assert T[:].shape == shape # get one component assert T[1, 1] == nx.dot(x1[1], x2[1].T) # get one row assert T[1].shape == (n2,) # get one column with slices assert T[::10, 5].shape == (10,) with pytest.raises(NotImplementedError): T["error"] def test_OTResult_LazyTensor(nx): T, a, b = get_LazyTensor(nx) res = ot.utils.OTResult(lazy_plan=T, batch_size=9, backend=nx) np.testing.assert_allclose(nx.to_numpy(a), nx.to_numpy(res.marginal_a)) np.testing.assert_allclose(nx.to_numpy(b), nx.to_numpy(res.marginal_b)) def test_LazyTensor_reduce(nx): T, a, b = get_LazyTensor(nx) T0 = T[:] s0 = nx.sum(T0) # total sum s = ot.utils.reduce_lazytensor(T, nx.sum, nx=nx) np.testing.assert_allclose(nx.to_numpy(s), 1) np.testing.assert_allclose(nx.to_numpy(s), nx.to_numpy(s0)) s2 = ot.utils.reduce_lazytensor(T, nx.sum) np.testing.assert_allclose(nx.to_numpy(s), nx.to_numpy(s2)) s2 = ot.utils.reduce_lazytensor(T, nx.sum, batch_size=500) np.testing.assert_allclose(nx.to_numpy(s), nx.to_numpy(s2)) s2 = ot.utils.reduce_lazytensor(T, nx.sum, batch_size=11) np.testing.assert_allclose(nx.to_numpy(s), nx.to_numpy(s2)) # sum over axis 0 s = ot.utils.reduce_lazytensor(T, nx.sum, axis=0, nx=nx) np.testing.assert_allclose(nx.to_numpy(s), nx.to_numpy(b)) # sum over axis 1 s = ot.utils.reduce_lazytensor(T, nx.sum, axis=1, nx=nx) np.testing.assert_allclose(nx.to_numpy(s), nx.to_numpy(a)) # test otehr reduction function s = ot.utils.reduce_lazytensor(T, nx.logsumexp, axis=1, nx=nx) s2 = nx.logsumexp(T[:], axis=1) np.testing.assert_allclose(nx.to_numpy(s), nx.to_numpy(s2)) # test 3D tensors def getitem(i, j, k, a, b, c): return a[i, None, None] * b[None, j, None] * c[None, None, k] # create a lazy tensor n = a.shape[0] T = ot.utils.LazyTensor((n, n, n), getitem, a=a, b=a, c=a) # total sum s1 = ot.utils.reduce_lazytensor(T, nx.sum, axis=0, nx=nx) s2 = ot.utils.reduce_lazytensor(T, nx.sum, axis=1, nx=nx) np.testing.assert_allclose(nx.to_numpy(s1), nx.to_numpy(s2)) with pytest.raises(NotImplementedError): ot.utils.reduce_lazytensor(T, nx.sum, axis=2, nx=nx, batch_size=10) def test_lowrank_LazyTensor(nx): p = 5 n1 = 100 n2 = 200 shape = (n1, n2) rng = np.random.RandomState(42) X1 = rng.randn(n1, p) X2 = rng.randn(n2, p) diag_d = rng.rand(p) X1, X2, diag_d = nx.from_numpy(X1, X2, diag_d) T0 = nx.dot(X1, X2.T) T = ot.utils.get_lowrank_lazytensor(X1, X2) np.testing.assert_allclose(nx.to_numpy(T[:]), nx.to_numpy(T0)) assert T.Q is X1 assert T.R is X2 # get the full tensor (not lazy) assert T[:].shape == shape # get one component assert T[1, 1] == nx.dot(X1[1], X2[1].T) # get one row assert T[1].shape == (n2,) # get one column with slices assert T[::10, 5].shape == (10,) T0 = nx.dot(X1 * diag_d[None, :], X2.T) T = ot.utils.get_lowrank_lazytensor(X1, X2, diag_d, nx=nx) np.testing.assert_allclose(nx.to_numpy(T[:]), nx.to_numpy(T0)) def test_labels_to_mask_helper(nx): y = np.array([1, 0, 2, 2, 1]) out = np.array( [ [0, 1, 0], [1, 0, 0], [0, 0, 1], [0, 0, 1], [0, 1, 0], ] ) y = nx.from_numpy(y) masks = ot.utils.labels_to_masks(y) np.testing.assert_array_equal(out, masks) def test_label_normalization(nx): y = nx.from_numpy(np.arange(5) + 1) out = np.arange(5) # labels are shifted y_normalized = ot.utils.label_normalization(y) np.testing.assert_array_equal(out, y_normalized) # labels are shifted but the shift if expected y_normalized_start = ot.utils.label_normalization(y, start=1) np.testing.assert_array_equal(y, y_normalized_start) def test_proj_SDP(nx): t = np.pi / 8 U = np.array([[np.cos(t), -np.sin(t)], [np.sin(t), np.cos(t)]]) w = np.array([1.0, -1.0]) S = np.stack([U @ np.diag(w) @ U.T] * 2, axis=0) S_nx = nx.from_numpy(S) R = ot.utils.proj_SDP(S_nx) w_expected = np.array([1.0, 0.0]) S_expected = np.stack([U @ np.diag(w_expected) @ U.T] * 2, axis=0) assert np.allclose(nx.to_numpy(R), S_expected) R0 = ot.utils.proj_SDP(S_nx[0]) assert np.allclose(nx.to_numpy(R0), S_expected[0]) def test_laplacian(): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) M = ot.dist(x, x) L = ot.utils.laplacian(M) assert L.shape == (n, n) def test_kl_div(nx): n = 10 rng = np.random.RandomState(0) # test on non-negative tensors x = rng.randn(n) x = x - x.min() + 1e-5 y = rng.randn(n) y = y - y.min() + 1e-5 xb = nx.from_numpy(x) yb = nx.from_numpy(y) kl = nx.kl_div(xb, yb) kl_mass = nx.kl_div(xb, yb, True) recovered_kl = kl_mass - nx.sum(yb - xb) np.testing.assert_allclose(kl, recovered_kl) def test_exp_bures(nx): d = 2 rng = np.random.RandomState(42) X = rng.randn(d, d) z = rng.randn(d) X, z = nx.from_numpy(X, z) S = X + nx.transpose(X) Sigma = nx.eye(d, type_as=S) Lambda = ot.utils.exp_bures(Sigma, S) # asserst SPD np.testing.assert_array_less(np.zeros(1), nx.to_numpy(z.T @ Lambda @ z)) # OT map from Lambda to Sigma Lambda12 = nx.sqrtm(Lambda) Lambda12inv = nx.inv(Lambda12) M = nx.sqrtm(nx.einsum("ij, jk, kl -> il", Lambda12, Sigma, Lambda12)) T = nx.einsum("ij, jk, kl -> il", Lambda12inv, M, Lambda12inv) # exp_\Lambda(log_\Lambda(Sigma)) = Sigma Sigma_exp = ot.utils.exp_bures(Lambda, T - nx.eye(d, type_as=T)) np.testing.assert_allclose(nx.to_numpy(Sigma), nx.to_numpy(Sigma_exp), atol=1e-5) def test_fun_to_numpy(nx): arr = np.arange(5) arrb = nx.from_numpy(arr) def fun(x): # backend function return nx.sum(x) fun_numpy = ot.utils.fun_to_numpy(fun, arrb, nx, warn=True) res = nx.to_numpy(fun(arrb)) res_np = fun_numpy(arr) np.testing.assert_allclose(res, res_np) with pytest.raises(ValueError): ot.utils.fun_to_numpy(fun, None, nx, warn=True) class TestDataScaler: """Tests for the DataScaler class.""" def test_invalid_norm_raises(self): with pytest.raises(ValueError, match="Invalid norm"): ot.utils.DataScaler(norm="bogus") def test_standard_joint_fit_mean_std(self): X_s = np.array([[0.0, 0.0], [2.0, 200.0]]) X_t = np.array([[4.0, 400.0], [6.0, 600.0]]) scaler = ot.utils.DataScaler(norm="standard").fit([X_s, X_t]) expected_mean = np.array([3.0, 300.0]) np.testing.assert_allclose(scaler.mean_, expected_mean) def test_standard_transform_zero_mean_unit_var(self): rng = np.random.RandomState(0) X_s = rng.normal(5, 2, (100, 3)) X_t = rng.normal(10, 3, (100, 3)) scaler = ot.utils.DataScaler(norm="standard").fit([X_s, X_t]) X_s_n = scaler.transform(X_s) X_t_n = scaler.transform(X_t) joint = np.concatenate([X_s_n, X_t_n], axis=0) np.testing.assert_allclose(joint.mean(axis=0), 0, atol=1e-10) np.testing.assert_allclose(joint.std(axis=0), 1, atol=1e-10) def test_minmax_transform_in_unit_range(self): X_s = np.array([[0.0], [5.0]]) X_t = np.array([[10.0]]) scaler = ot.utils.DataScaler(norm="minmax").fit([X_s, X_t]) joint = np.concatenate([scaler.transform(X_s), scaler.transform(X_t)], axis=0) assert joint.min() == 0.0 assert joint.max() == 1.0 def test_l2_unit_norm_rows(self): X = np.array([[3.0, 4.0], [1.0, 0.0]]) scaler = ot.utils.DataScaler(norm="l2") X_n = scaler.fit_transform(X) norms = np.linalg.norm(X_n, axis=1) np.testing.assert_allclose(norms, 1.0) def test_l2_no_explicit_fit_via_fit_transform(self): scaler = ot.utils.DataScaler(norm="l2") X = np.array([[3.0, 4.0]]) result = scaler.fit_transform(X) np.testing.assert_allclose(np.linalg.norm(result, axis=1), 1.0) def test_l2_transform_without_fit(self): scaler = ot.utils.DataScaler(norm="l2") X = np.array([[3.0, 4.0]]) result = scaler.transform(X) np.testing.assert_allclose(np.linalg.norm(result, axis=1), 1.0) def test_zero_variance_column_warns(self): X = np.array([[1.0, 5.0], [2.0, 5.0], [3.0, 5.0]]) with pytest.warns(RuntimeWarning, match="Zero variance"): ot.utils.DataScaler(norm="standard").fit(X) def test_zero_range_column_warns(self): X = np.array([[1.0, 5.0], [2.0, 5.0], [3.0, 5.0]]) with pytest.warns(RuntimeWarning, match="Zero range"): ot.utils.DataScaler(norm="minmax").fit(X) def test_zero_norm_row_warns(self): X = np.array([[0.0, 0.0], [3.0, 4.0]]) scaler = ot.utils.DataScaler(norm="l2") with pytest.warns(RuntimeWarning, match="Zero-norm"): scaler.fit_transform(X) def test_transform_before_fit_raises(self): scaler = ot.utils.DataScaler(norm="standard") with pytest.raises(RuntimeError, match="must be fitted"): scaler.transform(np.array([[1.0, 2.0]])) def test_fit_empty_list_raises(self): with pytest.raises(ValueError, match="empty list"): ot.utils.DataScaler(norm="standard").fit([]) def test_fit_single_array(self): X = np.array([[1.0, 2.0], [3.0, 4.0]]) scaler = ot.utils.DataScaler(norm="standard").fit(X) assert scaler.mean_ is not None def test_fit_transform_list_returns_list(self): X_s = np.array([[1.0, 2.0], [3.0, 4.0]]) X_t = np.array([[5.0, 6.0], [7.0, 8.0]]) results = ot.utils.DataScaler(norm="standard").fit_transform([X_s, X_t]) assert isinstance(results, list) assert len(results) == 2 assert results[0].shape == X_s.shape assert results[1].shape == X_t.shape class TestApplyScaler: """Tests for the apply_scaler helper.""" def test_none_returns_inputs_unchanged(self): X_s = np.array([[1.0, 2.0]]) X_t = np.array([[3.0, 4.0]]) out_s, out_t = ot.utils.apply_scaler(X_s, X_t, None) np.testing.assert_array_equal(out_s, X_s) np.testing.assert_array_equal(out_t, X_t) def test_with_datascaler(self): rng = np.random.RandomState(0) X_s = rng.normal(0, 1, (50, 4)) X_t = rng.normal(5, 1, (50, 4)) scaler = ot.utils.DataScaler(norm="standard").fit([X_s, X_t]) out_s, out_t = ot.utils.apply_scaler(X_s, X_t, scaler) np.testing.assert_allclose(out_s, scaler.transform(X_s)) np.testing.assert_allclose(out_t, scaler.transform(X_t)) def test_with_lambda(self): X_s = np.array([[1.0]]) X_t = np.array([[2.0]]) out_s, out_t = ot.utils.apply_scaler(X_s, X_t, lambda x: x * 10) np.testing.assert_array_equal(out_s, X_s * 10) np.testing.assert_array_equal(out_t, X_t * 10) def test_invalid_scaler_raises(self): with pytest.raises(ValueError, match="scaler must be"): ot.utils.apply_scaler(np.zeros((2, 2)), np.zeros((2, 2)), scaler=42) @pytest.mark.parametrize("norm", ["standard", "minmax", "l2"]) def test_datascaler_backend(nx, norm): rng = np.random.RandomState(0) X_s_np = rng.normal(0, 1, (50, 3)) X_t_np = rng.normal(5, 2, (50, 3)) X_s_b, X_t_b = nx.from_numpy(X_s_np, X_t_np) ref = ot.utils.DataScaler(norm=norm).fit([X_s_np, X_t_np]) ref_s = ref.transform(X_s_np) scaler = ot.utils.DataScaler(norm=norm).fit([X_s_b, X_t_b]) out_s = scaler.transform(X_s_b) np.testing.assert_allclose(nx.to_numpy(out_s), ref_s, atol=1e-5) outs = scaler.transform([X_s_b, X_t_b]) assert isinstance(outs, list) and len(outs) == 2 np.testing.assert_allclose(nx.to_numpy(outs[0]), ref_s, atol=1e-5) ft_ref = ot.utils.DataScaler(norm=norm).fit_transform([X_s_np, X_t_np]) ft_b = ot.utils.DataScaler(norm=norm).fit_transform([X_s_b, X_t_b]) assert isinstance(ft_b, list) and len(ft_b) == 2 np.testing.assert_allclose(nx.to_numpy(ft_b[0]), ft_ref[0], atol=1e-5) single_out = ot.utils.DataScaler(norm=norm).fit(X_s_b).transform(X_s_b) assert nx.to_numpy(single_out).shape == X_s_np.shape def test_datascaler_backend_mismatch_raises(): X = np.array([[1.0, 2.0], [3.0, 4.0]]) scaler = ot.utils.DataScaler(norm="standard").fit(X) scaler._nx = object() with pytest.raises(ValueError, match="Backend mismatch"): scaler.transform(X) @pytest.skip_backend("tf") @pytest.mark.parametrize("ratio", [0.3, 0.5, 0.51, 0.7]) @pytest.mark.parametrize("n", [10, 50, 100, 101]) @pytest.mark.parametrize("random", [True, False]) def test_split_sample_ratio(nx, ratio, n, random): rng = np.random.RandomState(0) X = rng.normal(0, 1, (100, 3)) a = rng.uniform(size=(100,)) a = a / np.sum(a) X = nx.from_numpy(X) a = nx.from_numpy(a) seed = 42 # test uniform weights X1, X2, a1, a2, id1, id2 = ot.utils.split_sample_ratio( X, ratio=ratio, random_split=random, random_state=seed, nx=nx ) np.testing.assert_allclose(nx.to_numpy(a1).sum(), ratio, atol=1e-5) np.testing.assert_allclose(nx.to_numpy(a2).sum(), 1 - ratio, atol=1e-5) assert X1.shape[0] == a1.shape[0] assert X2.shape[0] == a2.shape[0] # test non uniform weights X1, X2, a1, a2, id1, id2 = ot.utils.split_sample_ratio( X, ratio=ratio, a=a, random_split=random, random_state=seed ) with pytest.raises(ValueError): ot.utils.split_sample_ratio(X, ratio=1.5, a=a, random_state=seed, nx=nx) # python-pot-0.9.7+dfsg/test/test_weak.py000066400000000000000000000022711525521106200201070ustar00rootroot00000000000000"""Tests for main module ot.weak""" # Author: Remi Flamary # # License: MIT License import ot import numpy as np def test_weak_ot(): # test weak ot solver and identity stationary point n = 50 rng = np.random.RandomState(0) xs = rng.randn(n, 2) xt = rng.randn(n, 2) u = ot.utils.unif(n) G, log = ot.weak_optimal_transport(xs, xt, u, u, log=True) # check constraints np.testing.assert_allclose(u, G.sum(1)) np.testing.assert_allclose(u, G.sum(0)) # chaeck that identity is recovered G = ot.weak_optimal_transport(xs, xs, G0=np.eye(n) / n) # check G is identity np.testing.assert_allclose(G, np.eye(n) / n) # check constraints np.testing.assert_allclose(u, G.sum(1)) np.testing.assert_allclose(u, G.sum(0)) def test_weak_ot_bakends(nx): # test weak ot solver for different backends n = 50 rng = np.random.RandomState(0) xs = rng.randn(n, 2) xt = rng.randn(n, 2) u = ot.utils.unif(n) G = ot.weak_optimal_transport(xs, xt, u, u) xs2, xt2, u2 = nx.from_numpy(xs, xt, u) G2 = ot.weak_optimal_transport(xs2, xt2, u2, u2) np.testing.assert_allclose(nx.to_numpy(G2), G) python-pot-0.9.7+dfsg/test/unbalanced/000077500000000000000000000000001525521106200176415ustar00rootroot00000000000000python-pot-0.9.7+dfsg/test/unbalanced/__init__.py000066400000000000000000000000001525521106200217400ustar00rootroot00000000000000python-pot-0.9.7+dfsg/test/unbalanced/test_1d_solver.py000066400000000000000000000164321525521106200231560ustar00rootroot00000000000000"""Tests for module 1D Unbalanced OT""" # Author: Clément Bonet # # License: MIT License import itertools import numpy as np import ot import pytest @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") def test_uot_1d(nx): n_samples = 20 # nb samples rng = np.random.RandomState(42) xs = rng.randn(n_samples, 1) xt = rng.randn(n_samples, 1) a_np = ot.utils.unif(n_samples) b_np = ot.utils.unif(n_samples) reg_m = 1.0 M = ot.dist(xs, xt) a, b, M = nx.from_numpy(a_np, b_np, M) xs, xt = nx.from_numpy(xs, xt) G, log = ot.unbalanced.mm_unbalanced(a, b, M, reg_m, div="kl", log=True) loss_mm = log["cost"] f, g, loss_1d = ot.unbalanced.uot_1d(xs, xt, reg_m, p=2) np.testing.assert_allclose(loss_1d, loss_mm, atol=1e-2) np.testing.assert_allclose(G.sum(0), g[:, 0], atol=1e-2) np.testing.assert_allclose(G.sum(1), f[:, 0], atol=1e-2) @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") def test_uot_1d_convergence(nx): n_samples = 20 # nb samples rng = np.random.RandomState(42) xs = rng.randn(n_samples, 1) xt = rng.randn(n_samples, 1) xs, xt = nx.from_numpy(xs, xt) reg_m = 1000 # wass1d = ot.wasserstein_1d(xs, xt, p=2) G_1d, log = ot.emd_1d(xs, xt, metric="sqeuclidean", log=True) wass1d = log["cost"] u_w1d, v_w1d = nx.sum(G_1d, 1), nx.sum(G_1d, 0) u, v, loss_1d = ot.unbalanced.uot_1d(xs, xt, reg_m, p=2) np.testing.assert_allclose(loss_1d, wass1d, atol=1e-2) np.testing.assert_allclose(v_w1d, v[:, 0], atol=1e-2) np.testing.assert_allclose(u_w1d, u[:, 0], atol=1e-2) @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") def test_uot_1d_batch(nx): n_samples = 20 # nb samples m_samples = 30 rng = np.random.RandomState(42) xs = rng.randn(n_samples, 1) xt = rng.randn(m_samples, 1) xs = np.concatenate([xs, xs], axis=1) xt = np.concatenate([xt, xt], axis=1) a_np = rng.uniform(0, 1, n_samples) # unbalanced b_np = ot.utils.unif(m_samples) xs, xt, a, b = nx.from_numpy(xs, xt, a_np, b_np) reg_m = 1 u1, v1, uot_1d = ot.unbalanced.uot_1d(xs[:, 0], xt[:, 0], reg_m, a, b, p=2) u, v, loss_1d = ot.unbalanced.uot_1d(xs, xt, reg_m, a, b, p=2) np.testing.assert_allclose(loss_1d[0], loss_1d[1], atol=1e-5) np.testing.assert_allclose(loss_1d[0], uot_1d, atol=1e-5) u1, v1, uot_1d = ot.unbalanced.uot_1d( xs[:, 0], xt[:, 0], reg_m, a, b, p=2, returnCost="total" ) u, v, loss_1d = ot.unbalanced.uot_1d(xs, xt, reg_m, a, b, p=2, returnCost="total") np.testing.assert_allclose(loss_1d[0], loss_1d[1], atol=1e-5) np.testing.assert_allclose(loss_1d[0], uot_1d, atol=1e-5) @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") def test_uot_1d_inf_reg_m_backprop(nx): n_samples = 20 # nb samples rng = np.random.RandomState(42) xs = rng.randn(n_samples, 1) xt = rng.randn(n_samples, 1) a_np = ot.utils.unif(n_samples) b_np = ot.utils.unif(n_samples) reg_m = float("inf") a, b = nx.from_numpy(a_np, b_np) xs, xt = nx.from_numpy(xs, xt) f_w1d, g_w1d, wass1d = ot.emd_1d_dual_backprop(xs, xt, a, b, p=2) u, v, loss_1d, log = ot.unbalanced.uot_1d(xs, xt, reg_m, a, b, p=2, log=True) # Check right loss np.testing.assert_allclose(loss_1d, wass1d) # Check right marginals np.testing.assert_allclose(a, u[:, 0]) np.testing.assert_allclose(b, v[:, 0]) # Check potentials np.testing.assert_allclose(f_w1d, log["f"]) np.testing.assert_allclose(g_w1d, log["g"]) @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") def test_semi_uot_1d_backprop(nx): n_samples = 20 # nb samples rng = np.random.RandomState(42) xs = rng.randn(n_samples, 1) xt = rng.randn(n_samples, 1) a_np = ot.utils.unif(n_samples) b_np = ot.utils.unif(n_samples) a, b = nx.from_numpy(a_np, b_np) xs, xt = nx.from_numpy(xs, xt) reg_m = (float("inf"), 1.0) u, v, loss_1d = ot.unbalanced.uot_1d(xs, xt, reg_m, p=2) # Check right marginals np.testing.assert_allclose(a, u[:, 0]) np.testing.assert_allclose(v[:, 0].sum(), 1) reg_m = (1.0, float("inf")) u, v, loss_1d = ot.unbalanced.uot_1d(xs, xt, reg_m, p=2) # Check right marginals np.testing.assert_allclose(b, v[:, 0]) np.testing.assert_allclose(u[:, 0].sum(), 1) @pytest.skip_backend("jax") # problem with jax on macOS @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") @pytest.mark.parametrize( "reg_m", itertools.product( [1, float("inf")], ), ) def test_unbalanced_relaxation_parameters_backprop(nx, reg_m): n = 100 rng = np.random.RandomState(50) x = rng.randn(n, 2) y = rng.randn(n, 2) a = ot.utils.unif(n) # make dists unbalanced b = rng.rand(n, 2) a, b, x, y = nx.from_numpy(a, b, x, y) reg_m = reg_m[0] # options for reg_m full_list_reg_m = [reg_m, reg_m] full_tuple_reg_m = (reg_m, reg_m) tuple_reg_m, list_reg_m = (reg_m), [reg_m] nx_reg_m = reg_m * nx.ones(1) list_options = [ nx_reg_m, full_tuple_reg_m, tuple_reg_m, full_list_reg_m, list_reg_m, ] u, v, loss = ot.unbalanced.uot_1d(x, y, reg_m, u_weights=a, v_weights=b, p=2) for opt in list_options: u, v, loss_opt = ot.unbalanced.uot_1d(x, y, opt, u_weights=a, v_weights=b, p=2) np.testing.assert_allclose(nx.to_numpy(loss), nx.to_numpy(loss_opt), atol=1e-05) @pytest.skip_backend("jax") # problem with jax on macOS @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") @pytest.mark.parametrize( "reg_m1, reg_m2", itertools.product( [1, float("inf")], [1, float("inf")], ), ) def test_unbalanced_relaxation_parameters_pair_backprop(nx, reg_m1, reg_m2): # test generalized sinkhorn for unbalanced OT n = 100 rng = np.random.RandomState(50) x = rng.randn(n, 2) y = rng.randn(n, 2) a = ot.utils.unif(n) b = ot.utils.unif(n) a, b, x, y = nx.from_numpy(a, b, x, y) # options for reg_m full_list_reg_m = [reg_m1, reg_m2] full_tuple_reg_m = (reg_m1, reg_m2) list_options = [full_tuple_reg_m, full_list_reg_m] _, _, loss = ot.unbalanced.uot_1d( x, y, (reg_m1, reg_m2), u_weights=a, v_weights=b, p=2 ) for opt in list_options: _, _, loss_opt = ot.unbalanced.uot_1d(x, y, opt, u_weights=a, v_weights=b, p=2) np.testing.assert_allclose(nx.to_numpy(loss), nx.to_numpy(loss_opt), atol=1e-05) def test_uot_1d_type_devices_backprop(nx): rng = np.random.RandomState(0) n = 10 x = np.linspace(0, 5, n) rho_u = np.abs(rng.randn(n)) rho_u /= rho_u.sum() rho_v = np.abs(rng.randn(n)) rho_v /= rho_v.sum() reg_m = 1.0 xb, rho_ub, rho_vb = nx.from_numpy(x, rho_u, rho_v) if nx.__name__ in ["torch", "jax"]: f, g, _ = ot.unbalanced.uot_1d(xb, xb, reg_m, rho_ub, rho_vb, p=2) nx.assert_same_dtype_device(xb, f) nx.assert_same_dtype_device(xb, g) else: np.testing.assert_raises( AssertionError, ot.unbalanced.uot_1d, xb, xb, reg_m, rho_ub, rho_vb, p=2 ) python-pot-0.9.7+dfsg/test/unbalanced/test_lbfgs.py000066400000000000000000000207401525521106200223520ustar00rootroot00000000000000"""Tests for module Unbalanced OT with entropy regularization""" # Author: Hicham Janati # Laetitia Chapel # Quang Huy Tran # # License: MIT License import itertools import numpy as np import ot import pytest @pytest.mark.parametrize( "reg_div,regm_div,returnCost", itertools.product(["kl", "l2", "entropy"], ["kl", "l2", "tv"], ["linear", "total"]), ) def test_lbfgsb_unbalanced(nx, reg_div, regm_div, returnCost): np.random.seed(42) xs = np.random.randn(5, 2) xt = np.random.randn(6, 2) M = ot.dist(xs, xt) a = ot.unif(5) b = ot.unif(6) G, log = ot.unbalanced.lbfgsb_unbalanced( a, b, M, 1, 10, reg_div=reg_div, regm_div=regm_div, log=True, verbose=False ) loss, _ = ot.unbalanced.lbfgsb_unbalanced2( a, b, M, 1, 10, reg_div=reg_div, regm_div=regm_div, returnCost=returnCost, log=True, verbose=False, ) ab, bb, Mb = nx.from_numpy(a, b, M) Gb, log = ot.unbalanced.lbfgsb_unbalanced( ab, bb, Mb, 1, 10, reg_div=reg_div, regm_div=regm_div, log=True, verbose=False ) loss0, log = ot.unbalanced.lbfgsb_unbalanced2( ab, bb, Mb, 1, 10, reg_div=reg_div, regm_div=regm_div, returnCost=returnCost, log=True, verbose=False, ) np.testing.assert_allclose(G, nx.to_numpy(Gb)) np.testing.assert_allclose(loss, nx.to_numpy(loss0), atol=1e-06) @pytest.mark.parametrize( "reg_div,regm_div,returnCost", itertools.product(["kl", "l2", "entropy"], ["kl", "l2", "tv"], ["linear", "total"]), ) def test_lbfgsb_unbalanced_relaxation_parameters(nx, reg_div, regm_div, returnCost): np.random.seed(42) xs = np.random.randn(5, 2) xt = np.random.randn(6, 2) M = ot.dist(xs, xt) a = ot.unif(5) b = ot.unif(6) a, b, M = nx.from_numpy(a, b, M) reg_m = 10 full_list_reg_m = [reg_m, reg_m] full_tuple_reg_m = (reg_m, reg_m) tuple_reg_m, list_reg_m = (reg_m), [reg_m] np1_reg_m = reg_m * np.ones(1) np2_reg_m = reg_m * np.ones(2) list_options = [ np1_reg_m, np2_reg_m, full_tuple_reg_m, tuple_reg_m, full_list_reg_m, list_reg_m, ] G = ot.unbalanced.lbfgsb_unbalanced( a, b, M, 1, reg_m=reg_m, reg_div=reg_div, regm_div=regm_div, log=False, verbose=False, ) loss = ot.unbalanced.lbfgsb_unbalanced2( a, b, M, 1, reg_m=reg_m, reg_div=reg_div, regm_div=regm_div, returnCost=returnCost, log=False, verbose=False, ) for opt in list_options: G0 = ot.unbalanced.lbfgsb_unbalanced( a, b, M, 1, reg_m=opt, reg_div=reg_div, regm_div=regm_div, log=False, verbose=False, ) loss0 = ot.unbalanced.lbfgsb_unbalanced2( a, b, M, 1, reg_m=opt, reg_div=reg_div, regm_div=regm_div, returnCost=returnCost, log=False, verbose=False, ) np.testing.assert_allclose(nx.to_numpy(G), nx.to_numpy(G0), atol=1e-06) np.testing.assert_allclose(nx.to_numpy(loss), nx.to_numpy(loss0), atol=1e-06) @pytest.mark.parametrize( "reg_div,regm_div,returnCost", itertools.product(["kl", "l2", "entropy"], ["kl", "l2", "tv"], ["linear", "total"]), ) def test_lbfgsb_reference_measure(nx, reg_div, regm_div, returnCost): np.random.seed(42) xs = np.random.randn(5, 2) xt = np.random.randn(6, 2) M = ot.dist(xs, xt) a = ot.unif(5) b = ot.unif(6) a, b, M = nx.from_numpy(a, b, M) c = a[:, None] * b[None, :] G, _ = ot.unbalanced.lbfgsb_unbalanced( a, b, M, reg=1, reg_m=10, c=None, reg_div=reg_div, regm_div=regm_div, log=True, verbose=False, ) loss, _ = ot.unbalanced.lbfgsb_unbalanced2( a, b, M, reg=1, reg_m=10, c=None, reg_div=reg_div, regm_div=regm_div, returnCost=returnCost, log=True, verbose=False, ) G0, _ = ot.unbalanced.lbfgsb_unbalanced( a, b, M, reg=1, reg_m=10, c=c, reg_div=reg_div, regm_div=regm_div, log=True, verbose=False, ) loss0, _ = ot.unbalanced.lbfgsb_unbalanced2( a, b, M, reg=1, reg_m=10, c=c, reg_div=reg_div, regm_div=regm_div, returnCost=returnCost, log=True, verbose=False, ) np.testing.assert_allclose(nx.to_numpy(G), nx.to_numpy(G0), atol=1e-06) np.testing.assert_allclose(nx.to_numpy(loss), nx.to_numpy(loss0), atol=1e-06) @pytest.mark.parametrize( "reg_div,regm_div,returnCost", itertools.product(["kl", "l2", "entropy"], ["kl", "l2", "tv"], ["linear", "total"]), ) def test_lbfgsb_marginals(nx, reg_div, regm_div, returnCost): np.random.seed(42) xs = np.random.randn(5, 2) xt = np.random.randn(6, 2) M = ot.dist(xs, xt) a = ot.unif(5) b = ot.unif(6) a, b, M = nx.from_numpy(a, b, M) G, _ = ot.unbalanced.lbfgsb_unbalanced( a, b, M, reg=1, reg_m=10, reg_div=reg_div, regm_div=regm_div, log=True, verbose=False, ) loss, _ = ot.unbalanced.lbfgsb_unbalanced2( a, b, M, reg=1, reg_m=10, reg_div=reg_div, regm_div=regm_div, returnCost=returnCost, log=True, verbose=False, ) a_empty, b_empty = np.array([]), np.array([]) a_empty, b_empty = nx.from_numpy(a_empty, b_empty) G0, _ = ot.unbalanced.lbfgsb_unbalanced( a_empty, b_empty, M, reg=1, reg_m=10, reg_div=reg_div, regm_div=regm_div, log=True, verbose=False, ) loss0, _ = ot.unbalanced.lbfgsb_unbalanced2( a_empty, b_empty, M, reg=1, reg_m=10, reg_div=reg_div, regm_div=regm_div, returnCost=returnCost, log=True, verbose=False, ) np.testing.assert_allclose(nx.to_numpy(G), nx.to_numpy(G0), atol=1e-06) np.testing.assert_allclose(nx.to_numpy(loss), nx.to_numpy(loss0), atol=1e-06) def test_lbfgsb_wrong_divergence(nx): n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) rng = np.random.RandomState(75) y = rng.randn(n, 2) a_np = ot.utils.unif(n) b_np = ot.utils.unif(n) M = ot.dist(x, y) M = M / M.max() a, b, M = nx.from_numpy(a_np, b_np, M) def lbfgsb_div(div): return ot.unbalanced.lbfgsb_unbalanced(a, b, M, reg=1, reg_m=10, reg_div=div) def lbfgsb2_div(div): return ot.unbalanced.lbfgsb_unbalanced2(a, b, M, reg=1, reg_m=10, reg_div=div) np.testing.assert_raises(ValueError, lbfgsb_div, "div_not_existed") np.testing.assert_raises(ValueError, lbfgsb2_div, "div_not_existed") def test_lbfgsb_wrong_marginal_divergence(nx): n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) rng = np.random.RandomState(75) y = rng.randn(n, 2) a_np = ot.utils.unif(n) b_np = ot.utils.unif(n) M = ot.dist(x, y) M = M / M.max() a, b, M = nx.from_numpy(a_np, b_np, M) def lbfgsb_div(div): return ot.unbalanced.lbfgsb_unbalanced(a, b, M, reg=1, reg_m=10, regm_div=div) def lbfgsb2_div(div): return ot.unbalanced.lbfgsb_unbalanced2(a, b, M, reg=1, reg_m=10, regm_div=div) np.testing.assert_raises(ValueError, lbfgsb_div, "div_not_existed") np.testing.assert_raises(ValueError, lbfgsb2_div, "div_not_existed") def test_lbfgsb_wrong_returnCost(nx): n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) rng = np.random.RandomState(75) y = rng.randn(n, 2) a_np = ot.utils.unif(n) b_np = ot.utils.unif(n) M = ot.dist(x, y) M = M / M.max() a, b, M = nx.from_numpy(a_np, b_np, M) def lbfgsb2(returnCost): return ot.unbalanced.lbfgsb_unbalanced2( a, b, M, reg=1, reg_m=10, returnCost=returnCost, verbose=True ) np.testing.assert_raises(ValueError, lbfgsb2, "invalid_returnCost") python-pot-0.9.7+dfsg/test/unbalanced/test_mm.py000066400000000000000000000154171525521106200216730ustar00rootroot00000000000000"""Tests for module Unbalanced OT with entropy regularization""" # Author: Hicham Janati # Laetitia Chapel # Quang Huy Tran # # License: MIT License import numpy as np import ot import pytest @pytest.mark.parametrize("div", ["kl", "l2"]) def test_mm_convergence(nx, div): n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) rng = np.random.RandomState(75) y = rng.randn(n, 2) a_np = ot.utils.unif(n) b_np = ot.utils.unif(n) M = ot.dist(x, y) M = M / M.max() reg_m = 100 a, b, M = nx.from_numpy(a_np, b_np, M) G, _ = ot.unbalanced.mm_unbalanced( a, b, M, reg_m=reg_m, div=div, verbose=False, log=True ) _, log = ot.unbalanced.mm_unbalanced2( a, b, M, reg_m, div=div, verbose=True, log=True ) linear_cost = nx.to_numpy(log["cost"]) # check if the marginals come close to the true ones when large reg np.testing.assert_allclose(np.sum(nx.to_numpy(G), 1), a_np, atol=1e-03) np.testing.assert_allclose(np.sum(nx.to_numpy(G), 0), b_np, atol=1e-03) # check if mm_unbalanced2 returns the correct loss np.testing.assert_allclose(nx.to_numpy(nx.sum(G * M)), linear_cost, atol=1e-5) # check in case no histogram is provided a_np, b_np = np.array([]), np.array([]) a, b = nx.from_numpy(a_np, b_np) G_null = ot.unbalanced.mm_unbalanced(a, b, M, reg_m=reg_m, div=div, verbose=False) np.testing.assert_allclose(nx.to_numpy(G_null), nx.to_numpy(G)) # test when G0 is given G0 = ot.emd(a, b, M) G0_np = nx.to_numpy(G0) reg_m = 10000 G = ot.unbalanced.mm_unbalanced(a, b, M, reg_m=reg_m, div=div, G0=G0, verbose=False) np.testing.assert_allclose(G0_np, nx.to_numpy(G), atol=1e-05) @pytest.mark.parametrize("div", ["kl", "l2"]) def test_mm_relaxation_parameters(nx, div): n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) rng = np.random.RandomState(75) y = rng.randn(n, 2) a_np = ot.utils.unif(n) b_np = ot.utils.unif(n) M = ot.dist(x, y) M = M / M.max() a, b, M = nx.from_numpy(a_np, b_np, M) reg = 1e-2 reg_m = 100 full_list_reg_m = [reg_m, reg_m] full_tuple_reg_m = (reg_m, reg_m) tuple_reg_m, list_reg_m = (reg_m), [reg_m] nx1_reg_m = reg_m * nx.ones(1) nx2_reg_m = reg_m * nx.ones(2) list_options = [ nx1_reg_m, nx2_reg_m, full_tuple_reg_m, tuple_reg_m, full_list_reg_m, list_reg_m, ] G0, _ = ot.unbalanced.mm_unbalanced( a, b, M, reg_m=reg_m, reg=reg, div=div, verbose=False, log=True ) loss_0 = nx.to_numpy( ot.unbalanced.mm_unbalanced2( a, b, M, reg_m=reg_m, reg=reg, div=div, verbose=True ) ) for opt in list_options: G1, _ = ot.unbalanced.mm_unbalanced( a, b, M, reg_m=opt, reg=reg, div=div, verbose=False, log=True ) loss_1 = nx.to_numpy( ot.unbalanced.mm_unbalanced2( a, b, M, reg_m=opt, reg=reg, div=div, verbose=True ) ) np.testing.assert_allclose(nx.to_numpy(G0), nx.to_numpy(G1), atol=1e-05) np.testing.assert_allclose(loss_0, loss_1, atol=1e-5) @pytest.mark.parametrize("div", ["kl", "l2"]) def test_mm_reference_measure(nx, div): n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) rng = np.random.RandomState(75) y = rng.randn(n, 2) a_np = ot.utils.unif(n) b_np = ot.utils.unif(n) M = ot.dist(x, y) M = M / M.max() a, b, M = nx.from_numpy(a_np, b_np, M) c = a[:, None] * b[None, :] reg = 1e-2 reg_m = 100 G0, _ = ot.unbalanced.mm_unbalanced( a, b, M, reg_m=reg_m, c=None, reg=reg, div=div, verbose=False, log=True ) loss_0 = ot.unbalanced.mm_unbalanced2( a, b, M, reg_m=reg_m, c=None, reg=reg, div=div, verbose=True ) loss_0 = nx.to_numpy(loss_0) G1, _ = ot.unbalanced.mm_unbalanced( a, b, M, reg_m=reg_m, c=c, reg=reg, div=div, verbose=False, log=True ) loss_1 = ot.unbalanced.mm_unbalanced2( a, b, M, reg_m=reg_m, c=c, reg=reg, div=div, verbose=True ) loss_1 = nx.to_numpy(loss_1) np.testing.assert_allclose(nx.to_numpy(G0), nx.to_numpy(G1), atol=1e-05) np.testing.assert_allclose(loss_0, loss_1, atol=1e-5) @pytest.mark.parametrize("div", ["kl", "l2"]) def test_mm_marginals(nx, div): n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) rng = np.random.RandomState(75) y = rng.randn(n, 2) a_np = ot.utils.unif(n) b_np = ot.utils.unif(n) M = ot.dist(x, y) M = M / M.max() a, b, M = nx.from_numpy(a_np, b_np, M) reg = 1e-2 reg_m = 100 G0, _ = ot.unbalanced.mm_unbalanced( a, b, M, reg_m=reg_m, c=None, reg=reg, div=div, verbose=False, log=True ) loss_0 = ot.unbalanced.mm_unbalanced2( a, b, M, reg_m=reg_m, c=None, reg=reg, div=div, verbose=True ) loss_0 = nx.to_numpy(loss_0) a_empty, b_empty = np.array([]), np.array([]) a_empty, b_empty = nx.from_numpy(a_empty, b_empty) G1, _ = ot.unbalanced.mm_unbalanced( a_empty, b_empty, M, reg_m=reg_m, reg=reg, div=div, verbose=False, log=True ) loss_1 = ot.unbalanced.mm_unbalanced2( a_empty, b_empty, M, reg_m=reg_m, reg=reg, div=div, verbose=True ) loss_1 = nx.to_numpy(loss_1) np.testing.assert_allclose(nx.to_numpy(G0), nx.to_numpy(G1), atol=1e-05) np.testing.assert_allclose(loss_0, loss_1, atol=1e-5) def test_mm_wrong_divergence(nx): n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) rng = np.random.RandomState(75) y = rng.randn(n, 2) a_np = ot.utils.unif(n) b_np = ot.utils.unif(n) M = ot.dist(x, y) M = M / M.max() a, b, M = nx.from_numpy(a_np, b_np, M) reg = 1e-2 reg_m = 100 def mm_div(div): return ot.unbalanced.mm_unbalanced( a, b, M, reg_m=reg_m, reg=reg, div=div, verbose=False, log=True ) def mm2_div(div): return ot.unbalanced.mm_unbalanced2( a, b, M, reg_m=reg_m, reg=reg, div=div, verbose=True ) np.testing.assert_raises(ValueError, mm_div, "div_not_existed") np.testing.assert_raises(ValueError, mm2_div, "div_not_existed") def test_mm_wrong_returnCost(nx): n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) rng = np.random.RandomState(75) y = rng.randn(n, 2) a_np = ot.utils.unif(n) b_np = ot.utils.unif(n) M = ot.dist(x, y) M = M / M.max() a, b, M = nx.from_numpy(a_np, b_np, M) def mm2(returnCost): return ot.unbalanced.mm_unbalanced2( a, b, M, reg_m=100, reg=1e-2, returnCost=returnCost, verbose=True ) np.testing.assert_raises(ValueError, mm2, "invalid_returnCost") python-pot-0.9.7+dfsg/test/unbalanced/test_sinkhorn.py000066400000000000000000000524261525521106200231160ustar00rootroot00000000000000"""Tests for module Unbalanced OT with entropy regularization""" # Author: Hicham Janati # Laetitia Chapel # Quang Huy Tran # # License: MIT License import itertools import numpy as np import ot import pytest from ot.unbalanced import barycenter_unbalanced @pytest.mark.parametrize( "method,reg_type", itertools.product( [ "sinkhorn", "sinkhorn_stabilized", "sinkhorn_reg_scaling", "sinkhorn_translation_invariant", ], ["kl", "entropy"], ), ) def test_unbalanced_convergence(nx, method, reg_type): # test generalized sinkhorn for unbalanced OT n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) a = ot.utils.unif(n) # make dists unbalanced b = ot.utils.unif(n) * 1.5 M = ot.dist(x, x) a, b, M = nx.from_numpy(a, b, M) epsilon = 1.0 reg_m = 1.0 G, log = ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, log=True, verbose=True, ) loss = nx.to_numpy( ot.unbalanced.sinkhorn_unbalanced2( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, verbose=True, ) ) # check fixed point equations # in log-domain fi = reg_m / (reg_m + epsilon) logb = nx.log(b + 1e-16) loga = nx.log(a + 1e-16) if reg_type == "entropy": logKtu = nx.logsumexp(log["logu"][None, :] - M.T / epsilon, axis=1) logKv = nx.logsumexp(log["logv"][None, :] - M / epsilon, axis=1) elif reg_type == "kl": log_ab = loga[:, None] + logb[None, :] logKtu = nx.logsumexp(log["logu"][None, :] - M.T / epsilon + log_ab.T, axis=1) logKv = nx.logsumexp(log["logv"][None, :] - M / epsilon + log_ab, axis=1) v_final = fi * (logb - logKtu) u_final = fi * (loga - logKv) np.testing.assert_allclose( nx.to_numpy(u_final), nx.to_numpy(log["logu"]), atol=1e-05 ) np.testing.assert_allclose( nx.to_numpy(v_final), nx.to_numpy(log["logv"]), atol=1e-05 ) # check if sinkhorn_unbalanced2 returns the correct loss np.testing.assert_allclose(nx.to_numpy(nx.sum(G * M)), loss, atol=1e-5) @pytest.mark.parametrize( "method,reg_type", itertools.product( [ "sinkhorn", "sinkhorn_stabilized", "sinkhorn_reg_scaling", "sinkhorn_translation_invariant", ], ["kl", "entropy"], ), ) def test_unbalanced_marginals(nx, method, reg_type): # test generalized sinkhorn for unbalanced OT n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) a = ot.utils.unif(n) b = ot.utils.unif(n) M = ot.dist(x, x) a, b, M = nx.from_numpy(a, b, M) epsilon = 1.0 reg_m = 1.0 G0, log0 = ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, log=True ) loss0 = ot.unbalanced.sinkhorn_unbalanced2( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, ) # check in case no histogram is provided or histogram is None a_empty, b_empty = np.array([]), np.array([]) a_empty, b_empty = nx.from_numpy(a_empty, b_empty) G_empty, log_empty = ot.unbalanced.sinkhorn_unbalanced( a_empty, b_empty, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, log=True, ) loss_empty = ot.unbalanced.sinkhorn_unbalanced2( a_empty, b_empty, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type ) np.testing.assert_allclose( nx.to_numpy(log_empty["logu"]), nx.to_numpy(log0["logu"]), atol=1e-05 ) np.testing.assert_allclose( nx.to_numpy(log_empty["logv"]), nx.to_numpy(log0["logv"]), atol=1e-05 ) np.testing.assert_allclose(nx.to_numpy(G_empty), nx.to_numpy(G0), atol=1e-05) np.testing.assert_allclose(nx.to_numpy(loss_empty), nx.to_numpy(loss0), atol=1e-5) @pytest.mark.parametrize( "method,reg_type", itertools.product( [ "sinkhorn", "sinkhorn_stabilized", "sinkhorn_reg_scaling", "sinkhorn_translation_invariant", ], ["kl", "entropy"], ), ) def test_unbalanced_warmstart(nx, method, reg_type): # test generalized sinkhorn for unbalanced OT n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) a = ot.utils.unif(n) b = ot.utils.unif(n) M = ot.dist(x, x) a, b, M = nx.from_numpy(a, b, M) epsilon = 1.0 reg_m = 1.0 G0, log0 = ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, warmstart=None, log=True, verbose=True, ) loss0 = ot.unbalanced.sinkhorn_unbalanced2( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, warmstart=None, verbose=True, ) dim_a, dim_b = M.shape warmstart = (nx.zeros(dim_a, type_as=M), nx.zeros(dim_b, type_as=M)) G, log = ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, warmstart=warmstart, log=True, verbose=True, ) loss = ot.unbalanced.sinkhorn_unbalanced2( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, warmstart=warmstart, verbose=True, ) _, log_emd = ot.lp.emd(a, b, M, log=True) warmstart1 = (log_emd["u"], log_emd["v"]) G1, log1 = ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, warmstart=warmstart1, log=True, verbose=True, ) loss1 = ot.unbalanced.sinkhorn_unbalanced2( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, warmstart=warmstart1, verbose=True, ) np.testing.assert_allclose( nx.to_numpy(log["logu"]), nx.to_numpy(log0["logu"]), atol=1e-05 ) np.testing.assert_allclose( nx.to_numpy(log["logv"]), nx.to_numpy(log0["logv"]), atol=1e-05 ) np.testing.assert_allclose( nx.to_numpy(log0["logu"]), nx.to_numpy(log1["logu"]), atol=1e-05 ) np.testing.assert_allclose( nx.to_numpy(log0["logv"]), nx.to_numpy(log1["logv"]), atol=1e-05 ) np.testing.assert_allclose(nx.to_numpy(G), nx.to_numpy(G0), atol=1e-05) np.testing.assert_allclose(nx.to_numpy(G0), nx.to_numpy(G1), atol=1e-05) np.testing.assert_allclose(nx.to_numpy(loss), nx.to_numpy(loss0), atol=1e-5) np.testing.assert_allclose(nx.to_numpy(loss0), nx.to_numpy(loss1), atol=1e-5) @pytest.mark.parametrize( "method,reg_type", itertools.product( [ "sinkhorn", "sinkhorn_stabilized", "sinkhorn_reg_scaling", "sinkhorn_translation_invariant", ], ["kl", "entropy"], ), ) def test_unbalanced_reference_measure(nx, method, reg_type): # test generalized sinkhorn for unbalanced OT n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) a = ot.utils.unif(n) b = ot.utils.unif(n) M = ot.dist(x, x) a, b, M = nx.from_numpy(a, b, M) epsilon = 1.0 reg_m = 1.0 G0, log0 = ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, c=None, log=True, ) loss0 = ot.unbalanced.sinkhorn_unbalanced2( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, c=None ) if reg_type == "kl": c = a[:, None] * b[None, :] elif reg_type == "entropy": c = nx.ones(M.shape, type_as=M) G, log = ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, c=c, log=True, ) loss = ot.unbalanced.sinkhorn_unbalanced2( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type=reg_type, c=c ) np.testing.assert_allclose( nx.to_numpy(log["logu"]), nx.to_numpy(log0["logu"]), atol=1e-05 ) np.testing.assert_allclose( nx.to_numpy(log["logv"]), nx.to_numpy(log0["logv"]), atol=1e-05 ) np.testing.assert_allclose(nx.to_numpy(G), nx.to_numpy(G0), atol=1e-05) np.testing.assert_allclose(nx.to_numpy(loss), nx.to_numpy(loss0), atol=1e-5) @pytest.mark.parametrize( "method, log", itertools.product( [ "sinkhorn", "sinkhorn_stabilized", "sinkhorn_reg_scaling", "sinkhorn_translation_invariant", ], [True, False], ), ) def test_sinkhorn_unbalanced2(nx, method, log): n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) a = ot.utils.unif(n) # make dists unbalanced b = ot.utils.unif(n) * 1.5 M = ot.dist(x, x) a, b, M = nx.from_numpy(a, b, M) epsilon = 1.0 reg_m = 1.0 loss = nx.to_numpy( ot.unbalanced.sinkhorn_unbalanced2( a, b, M, reg=epsilon, reg_m=reg_m, method=method, c=None, log=False, verbose=True, ) ) res = ot.unbalanced.sinkhorn_unbalanced2( a, b, M, reg=epsilon, reg_m=reg_m, method=method, c=None, log=log, verbose=True ) loss0 = res[0] if log else res np.testing.assert_allclose(nx.to_numpy(loss), nx.to_numpy(loss0), atol=1e-5) @pytest.mark.parametrize( "method,reg_m", itertools.product( [ "sinkhorn", "sinkhorn_stabilized", "sinkhorn_reg_scaling", "sinkhorn_translation_invariant", ], [1, float("inf")], ), ) def test_unbalanced_relaxation_parameters(nx, method, reg_m): # test generalized sinkhorn for unbalanced OT n = 100 rng = np.random.RandomState(50) x = rng.randn(n, 2) a = ot.utils.unif(n) # make dists unbalanced b = rng.rand(n, 2) M = ot.dist(x, x) epsilon = 1.0 a, b, M = nx.from_numpy(a, b, M) # options for reg_m full_list_reg_m = [reg_m, reg_m] full_tuple_reg_m = (reg_m, reg_m) tuple_reg_m, list_reg_m = (reg_m), [reg_m] nx_reg_m = reg_m * nx.ones(1) list_options = [ nx_reg_m, full_tuple_reg_m, tuple_reg_m, full_list_reg_m, list_reg_m, ] loss, log = ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg=epsilon, reg_m=reg_m, method=method, log=True, verbose=True ) for opt in list_options: loss_opt, log_opt = ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg=epsilon, reg_m=opt, method=method, log=True, verbose=True ) np.testing.assert_allclose( nx.to_numpy(log["logu"]), nx.to_numpy(log_opt["logu"]), atol=1e-05 ) np.testing.assert_allclose( nx.to_numpy(log["logv"]), nx.to_numpy(log_opt["logv"]), atol=1e-05 ) np.testing.assert_allclose(nx.to_numpy(loss), nx.to_numpy(loss_opt), atol=1e-05) @pytest.mark.parametrize( "method, reg_m1, reg_m2", itertools.product( [ "sinkhorn", "sinkhorn_stabilized", "sinkhorn_reg_scaling", "sinkhorn_translation_invariant", ], [1, float("inf")], [1, float("inf")], ), ) def test_unbalanced_relaxation_parameters_pair(nx, method, reg_m1, reg_m2): # test generalized sinkhorn for unbalanced OT n = 100 rng = np.random.RandomState(50) x = rng.randn(n, 2) a = ot.utils.unif(n) # make dists unbalanced b = rng.rand(n, 2) M = ot.dist(x, x) epsilon = 1.0 a, b, M = nx.from_numpy(a, b, M) # options for reg_m full_list_reg_m = [reg_m1, reg_m2] full_tuple_reg_m = (reg_m1, reg_m2) list_options = [full_tuple_reg_m, full_list_reg_m] loss, log = ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg=epsilon, reg_m=(reg_m1, reg_m2), method=method, log=True, verbose=True, ) for opt in list_options: loss_opt, log_opt = ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg=epsilon, reg_m=opt, method=method, log=True, verbose=True ) np.testing.assert_allclose( nx.to_numpy(log["logu"]), nx.to_numpy(log_opt["logu"]), atol=1e-05 ) np.testing.assert_allclose( nx.to_numpy(log["logv"]), nx.to_numpy(log_opt["logv"]), atol=1e-05 ) np.testing.assert_allclose(nx.to_numpy(loss), nx.to_numpy(loss_opt), atol=1e-05) @pytest.mark.parametrize( "method", [ "sinkhorn", "sinkhorn_stabilized", "sinkhorn_reg_scaling", "sinkhorn_translation_invariant", ], ) def test_unbalanced_multiple_inputs(nx, method): # test generalized sinkhorn for unbalanced OT n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) a = ot.utils.unif(n) # make dists unbalanced b = rng.rand(n, 2) M = ot.dist(x, x) epsilon = 1.0 reg_m = 1.0 a, b, M = nx.from_numpy(a, b, M) G, log = ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg=epsilon, reg_m=reg_m, method=method, log=True, verbose=True ) # check fixed point equations # in log-domain fi = reg_m / (reg_m + epsilon) logb = nx.log(b + 1e-16) loga = nx.log(a + 1e-16)[:, None] logKtu = nx.logsumexp(log["logu"][:, None, :] - M[:, :, None] / epsilon, axis=0) logKv = nx.logsumexp(log["logv"][None, :] - M[:, :, None] / epsilon, axis=1) v_final = fi * (logb - logKtu) u_final = fi * (loga - logKv) print("u_final shape = {}".format(u_final.shape)) print("v_final shape = {}".format(v_final.shape)) print("logu shape = {}".format(log["logu"].shape)) print("logv shape = {}".format(log["logv"].shape)) np.testing.assert_allclose( nx.to_numpy(u_final), nx.to_numpy(log["logu"]), atol=1e-05 ) np.testing.assert_allclose( nx.to_numpy(v_final), nx.to_numpy(log["logv"]), atol=1e-05 ) # reg_type="entropy" as multiple inputs does not work for KL yet losses = ot.unbalanced.sinkhorn_unbalanced2( a, b, M, reg=epsilon, reg_m=reg_m, method=method, reg_type="entropy" ) loss1 = ot.unbalanced.sinkhorn_unbalanced2( a, b[:, 0], M, reg=epsilon, reg_m=reg_m, method=method, reg_type="entropy" ) loss2 = ot.unbalanced.sinkhorn_unbalanced2( a, b[:, 1], M, reg=epsilon, reg_m=reg_m, method=method, reg_type="entropy" ) np.testing.assert_allclose(nx.to_numpy(losses), np.array([loss1, loss2]), atol=1e-4) def test_stabilized_vs_sinkhorn(nx): # test if stable version matches sinkhorn n = 100 # Gaussian distributions a = ot.datasets.make_1D_gauss(n, m=20, s=5) # m= mean, s= std b1 = ot.datasets.make_1D_gauss(n, m=60, s=8) b2 = ot.datasets.make_1D_gauss(n, m=30, s=4) # creating matrix A containing all distributions b = np.vstack((b1, b2)).T M = ot.utils.dist0(n) M /= np.median(M) epsilon = 1 reg_m = 1.0 stopThr = 1e-12 ab, bb, Mb = nx.from_numpy(a, b, M) G, _ = ot.unbalanced.sinkhorn_unbalanced2( ab, bb, Mb, epsilon, reg_m, method="sinkhorn_stabilized", log=True, stopThr=stopThr, ) G2, _ = ot.unbalanced.sinkhorn_unbalanced2( ab, bb, Mb, epsilon, reg_m, method="sinkhorn", log=True, stopThr=stopThr ) G2_np, _ = ot.unbalanced.sinkhorn_unbalanced2( a, b, M, epsilon, reg_m, method="sinkhorn", log=True, stopThr=stopThr ) G3, _ = ot.unbalanced.sinkhorn_unbalanced2( ab, bb, Mb, epsilon, reg_m, method="sinkhorn_translation_invariant", log=True, stopThr=stopThr, ) G = nx.to_numpy(G) G2 = nx.to_numpy(G2) G3 = nx.to_numpy(G3) np.testing.assert_allclose(G, G2, atol=1e-5) np.testing.assert_allclose(G2, G2_np, atol=1e-5) np.testing.assert_allclose(G3, G, atol=1e-5) def test_sinkhorn_wrong_returnCost(nx): n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) rng = np.random.RandomState(75) y = rng.randn(n, 2) a_np = ot.utils.unif(n) b_np = ot.utils.unif(n) M = ot.dist(x, y) M = M / M.max() a, b, M = nx.from_numpy(a_np, b_np, M) epsilon = 1 reg_m = 1.0 def sinkhorn2(returnCost): return ot.unbalanced.sinkhorn_unbalanced2( a, b, M, epsilon, reg_m, returnCost=returnCost ) np.testing.assert_raises(ValueError, sinkhorn2, "invalid_returnCost") @pytest.mark.parametrize( "method", ["sinkhorn", "sinkhorn_stabilized", "sinkhorn_reg_scaling"] ) def test_unbalanced_barycenter(nx, method): # test generalized sinkhorn for unbalanced OT barycenter n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) A = rng.rand(n, 2) # make dists unbalanced A = A * np.array([1, 2])[None, :] M = ot.dist(x, x) epsilon = 1.0 reg_m = 1.0 A, M = nx.from_numpy(A, M) q, log = barycenter_unbalanced( A, M, reg=epsilon, reg_m=reg_m, method=method, log=True, verbose=True ) # check fixed point equations fi = reg_m / (reg_m + epsilon) logA = nx.log(A + 1e-16) logq = nx.log(q + 1e-16)[:, None] logKtu = nx.logsumexp(log["logu"][:, None, :] - M[:, :, None] / epsilon, axis=0) logKv = nx.logsumexp(log["logv"][None, :] - M[:, :, None] / epsilon, axis=1) v_final = fi * (logq - logKtu) u_final = fi * (logA - logKv) np.testing.assert_allclose( nx.to_numpy(u_final), nx.to_numpy(log["logu"]), atol=1e-05 ) np.testing.assert_allclose( nx.to_numpy(v_final), nx.to_numpy(log["logv"]), atol=1e-05 ) def test_barycenter_stabilized_vs_sinkhorn(nx): # test generalized sinkhorn for unbalanced OT barycenter n = 100 rng = np.random.RandomState(42) x = rng.randn(n, 2) A = rng.rand(n, 2) # make dists unbalanced A = A * np.array([1, 4])[None, :] M = ot.dist(x, x) epsilon = 0.5 reg_m = 10 Ab, Mb = nx.from_numpy(A, M) qstable, _ = barycenter_unbalanced( Ab, Mb, reg=epsilon, reg_m=reg_m, log=True, tau=100, method="sinkhorn_stabilized", verbose=True, ) q, _ = barycenter_unbalanced( Ab, Mb, reg=epsilon, reg_m=reg_m, method="sinkhorn", log=True ) q_np, _ = barycenter_unbalanced( A, M, reg=epsilon, reg_m=reg_m, method="sinkhorn", log=True ) q, qstable = nx.to_numpy(q, qstable) np.testing.assert_allclose(q, qstable, atol=1e-05) np.testing.assert_allclose(q, q_np, atol=1e-05) def test_wrong_method(nx): n = 10 rng = np.random.RandomState(42) x = rng.randn(n, 2) a = ot.utils.unif(n) # make dists unbalanced b = ot.utils.unif(n) * 1.5 M = ot.dist(x, x) epsilon = 1.0 reg_m = 1.0 a, b, M = nx.from_numpy(a, b, M) with pytest.raises(ValueError): ot.unbalanced.sinkhorn_unbalanced( a, b, M, reg=epsilon, reg_m=reg_m, method="badmethod", log=True, verbose=True, ) with pytest.raises(ValueError): ot.unbalanced.sinkhorn_unbalanced2( a, b, M, epsilon, reg_m, method="badmethod", verbose=True ) def test_implemented_methods(nx): IMPLEMENTED_METHODS = ["sinkhorn", "sinkhorn_stabilized"] TO_BE_IMPLEMENTED_METHODS = [ "sinkhorn_reg_scaling", "sinkhorn_translation_invariant", ] NOT_VALID_TOKENS = ["foo"] # test generalized sinkhorn for unbalanced OT barycenter n = 3 rng = np.random.RandomState(42) x = rng.randn(n, 2) a = ot.utils.unif(n) # make dists unbalanced b = ot.utils.unif(n) * 1.5 A = rng.rand(n, 2) M = ot.dist(x, x) epsilon = 1.0 reg_m = 1.0 a, b, M, A = nx.from_numpy(a, b, M, A) for method in IMPLEMENTED_METHODS: ot.unbalanced.sinkhorn_unbalanced(a, b, M, epsilon, reg_m, method=method) ot.unbalanced.sinkhorn_unbalanced2(a, b, M, epsilon, reg_m, method=method) barycenter_unbalanced(A, M, reg=epsilon, reg_m=reg_m, method=method) with pytest.warns(UserWarning, match="not implemented"): for method in set(TO_BE_IMPLEMENTED_METHODS): ot.unbalanced.sinkhorn_unbalanced(a, b, M, epsilon, reg_m, method=method) ot.unbalanced.sinkhorn_unbalanced2(a, b, M, epsilon, reg_m, method=method) barycenter_unbalanced(A, M, reg=epsilon, reg_m=reg_m, method=method) with pytest.raises(ValueError): for method in set(NOT_VALID_TOKENS): ot.unbalanced.sinkhorn_unbalanced(a, b, M, epsilon, reg_m, method=method) ot.unbalanced.sinkhorn_unbalanced2(a, b, M, epsilon, reg_m, method=method) barycenter_unbalanced(A, M, reg=epsilon, reg_m=reg_m, method=method) python-pot-0.9.7+dfsg/test/unbalanced/test_sliced.py000066400000000000000000000240071525521106200225200ustar00rootroot00000000000000"""Tests for module sliced Unbalanced OT""" # Author: Clément Bonet # # License: MIT License import itertools import numpy as np import ot import pytest @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") def test_sliced_uot_same_dist(nx): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) u = ot.utils.unif(n) x, u = nx.from_numpy(x, u) res = ot.sliced_unbalanced_ot(x, x, 1, u, u, 10, seed=42) np.testing.assert_almost_equal(res, 0.0) _, _, res = ot.unbalanced_sliced_ot(x, x, 1, u, u, 10, seed=42) np.testing.assert_almost_equal(res, 0.0) @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") def test_sliced_uot_bad_shapes(nx): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 2) y = rng.randn(n, 4) u = ot.utils.unif(n) x, y, u = nx.from_numpy(x, y, u) with pytest.raises(ValueError): _ = ot.sliced_unbalanced_ot(x, y, 1, u, u, 10, seed=42) with pytest.raises(ValueError): _ = ot.unbalanced_sliced_ot(x, y, 1, u, u, 10, seed=42) @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") def test_sliced_uot_log(nx): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 4) y = rng.randn(n, 4) u = ot.utils.unif(n) x, y, u = nx.from_numpy(x, y, u) res, log = ot.sliced_unbalanced_ot(x, y, 1, u, u, 10, p=1, seed=42, log=True) assert len(log) == 4 projections = log["projections"] projected_uots = log["projected_uots"] a_reweighted = log["a_reweighted"] b_reweighted = log["b_reweighted"] assert projections.shape[1] == len(projected_uots) == 10 for emd in projected_uots: assert emd > 0 assert res > 0 assert a_reweighted.shape == b_reweighted.shape == (n, 10) @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") def test_usot_log(nx): n = 100 rng = np.random.RandomState(0) x = rng.randn(n, 4) y = rng.randn(n, 4) u = ot.utils.unif(n) x, y, u = nx.from_numpy(x, y, u) f, g, res, log = ot.unbalanced_sliced_ot(x, y, 1, u, u, 10, p=1, seed=42, log=True) assert len(log) == 4 projections = log["projections"] sot_loss = log["sot_loss"] ot_loss = log["1d_losses"] full_mass = log["full_mass"] assert projections.shape[1] == 10 assert res > 0 assert f.shape == g.shape == u.shape np.testing.assert_almost_equal(f.sum(), g.sum()) np.testing.assert_equal(sot_loss, nx.mean(ot_loss * full_mass)) @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") def test_1d_sliced_equals_uot(nx): n = 100 m = 120 rng = np.random.RandomState(42) x = rng.randn(n, 1) y = rng.randn(m, 1) a = rng.uniform(0, 1, n) / 10 # unbalanced u = ot.utils.unif(m) reg_m = 1 x, y, a, u = nx.from_numpy(x, y, a, u) res, log = ot.sliced_unbalanced_ot(x, y, reg_m, a, u, 10, seed=42, p=2, log=True) a_exp, u_exp, expected = ot.uot_1d( x.squeeze(), y.squeeze(), reg_m, a, u, returnCost="total", p=2 ) np.testing.assert_almost_equal(res, expected) np.testing.assert_allclose(log["a_reweighted"][:, 0], a_exp) np.testing.assert_allclose(log["b_reweighted"][:, 0], u_exp) f, g, res, log = ot.unbalanced_sliced_ot( x, y, reg_m, a, u, 10, seed=42, p=2, log=True ) np.testing.assert_almost_equal(res, expected) np.testing.assert_allclose(f, a_exp) np.testing.assert_allclose(g, u_exp) @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") def test_sliced_projections(nx): n = 100 m = 120 rng = np.random.RandomState(42) x = rng.randn(n, 4) y = rng.randn(m, 4) a = rng.uniform(0, 1, n) / 10 # unbalanced u = ot.utils.unif(m) reg_m = 1 x, y, a, u = nx.from_numpy(x, y, a, u) res, log = ot.sliced_unbalanced_ot(x, y, reg_m, a, u, 10, seed=42, p=2, log=True) projections = log["projections"] res2 = ot.sliced_unbalanced_ot(x, y, reg_m, a, u, 10, seed=42, p=2) np.testing.assert_almost_equal(res, res2) res3 = ot.sliced_unbalanced_ot(x, y, reg_m, a, u, 10, projections=projections, p=2) np.testing.assert_almost_equal(res, res3) _, _, res = ot.unbalanced_sliced_ot(x, y, reg_m, a, u, 10, seed=42, p=2) _, _, res2 = ot.unbalanced_sliced_ot( x, y, reg_m, a, u, 10, projections=projections, p=2 ) np.testing.assert_almost_equal(res, res2) @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") def test_sliced_inf_reg_m(nx): n_samples = 20 # nb samples rng = np.random.RandomState(42) xs = rng.randn(n_samples, 4) xt = rng.randn(n_samples, 4) a_np = ot.utils.unif(n_samples) b_np = ot.utils.unif(n_samples) reg_m = float("inf") a, b = nx.from_numpy(a_np, b_np) xs, xt = nx.from_numpy(xs, xt) suot = ot.sliced_unbalanced_ot(xs, xt, reg_m, a, b, 10, seed=42, p=2) a_reweighted, b_reweighted, usot = ot.unbalanced_sliced_ot( xs, xt, reg_m, a, b, 10, seed=42, p=2 ) sw = ot.sliced_wasserstein_distance(xs, xt, n_projections=10, seed=42, p=2) # Check right loss np.testing.assert_almost_equal(suot, sw**2) np.testing.assert_almost_equal(usot, sw**2) np.testing.assert_allclose(a_reweighted, a) np.testing.assert_allclose(b_reweighted, b) @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") def test_semi_usot_1d(nx): n_samples = 20 # nb samples rng = np.random.RandomState(42) xs = rng.randn(n_samples, 1) xt = rng.randn(n_samples, 1) a_np = ot.utils.unif(n_samples) b_np = ot.utils.unif(n_samples) a, b = nx.from_numpy(a_np, b_np) xs, xt = nx.from_numpy(xs, xt) reg_m = (float("inf"), 1.0) a_reweighted, b_reweighted, usot = ot.unbalanced_sliced_ot( xs, xt, reg_m, a, b, 10, seed=42, p=2 ) # Check right marginals np.testing.assert_allclose(a, a_reweighted) np.testing.assert_allclose(b_reweighted.sum(), 1) reg_m = (1.0, float("inf")) a_reweighted, b_reweighted, usot = ot.unbalanced_sliced_ot( xs, xt, reg_m, a, b, 10, seed=42, p=2 ) # Check right marginals np.testing.assert_allclose(b, b_reweighted) np.testing.assert_allclose(a_reweighted.sum(), 1) @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") @pytest.mark.parametrize( "reg_m", itertools.product( [1, float("inf")], ), ) def test_sliced_unbalanced_relaxation_parameters(nx, reg_m): n = 100 rng = np.random.RandomState(50) x = rng.randn(n, 2) a = ot.utils.unif(n) # make dists unbalanced b = rng.rand(n) a, b, x = nx.from_numpy(a, b, x) reg_m = reg_m[0] # options for reg_m full_list_reg_m = [reg_m, reg_m] full_tuple_reg_m = (reg_m, reg_m) tuple_reg_m, list_reg_m = (reg_m), [reg_m] nx_reg_m = reg_m * nx.ones(1) list_options = [ nx_reg_m, full_tuple_reg_m, tuple_reg_m, full_list_reg_m, list_reg_m, ] _, _, usot = ot.unbalanced_sliced_ot(x, x, reg_m, a, b, 10, seed=42, p=2) suot = ot.sliced_unbalanced_ot(x, x, reg_m, a, b, 10, seed=42, p=2) for opt in list_options: _, _, usot_opt = ot.unbalanced_sliced_ot(x, x, opt, a, b, 10, seed=42, p=2) np.testing.assert_allclose(nx.to_numpy(usot), nx.to_numpy(usot_opt), atol=1e-05) suot_opt = ot.sliced_unbalanced_ot(x, x, opt, a, b, 10, seed=42, p=2) np.testing.assert_allclose(nx.to_numpy(suot), nx.to_numpy(suot_opt), atol=1e-05) @pytest.skip_backend("numpy") @pytest.skip_backend("tf") @pytest.skip_backend("cupy") @pytest.mark.parametrize( "reg_m1, reg_m2", itertools.product( [1, float("inf")], [1, float("inf")], ), ) def test_sliced_unbalanced_relaxation_parameters_pair(nx, reg_m1, reg_m2): n = 100 rng = np.random.RandomState(50) x = rng.randn(n, 2) a = ot.utils.unif(n) # make dists unbalanced b = rng.rand(n) a, b, x = nx.from_numpy(a, b, x) # options for reg_m full_list_reg_m = [reg_m1, reg_m2] full_tuple_reg_m = (reg_m1, reg_m2) list_options = [full_tuple_reg_m, full_list_reg_m] _, _, usot = ot.unbalanced_sliced_ot(x, x, (reg_m1, reg_m2), a, b, 10, seed=42, p=2) suot = ot.sliced_unbalanced_ot(x, x, (reg_m1, reg_m2), a, b, 10, seed=42, p=2) for opt in list_options: _, _, usot_opt = ot.unbalanced_sliced_ot(x, x, opt, a, b, 10, seed=42, p=2) np.testing.assert_allclose(nx.to_numpy(usot), nx.to_numpy(usot_opt), atol=1e-05) suot_opt = ot.sliced_unbalanced_ot(x, x, opt, a, b, 10, seed=42, p=2) np.testing.assert_allclose(nx.to_numpy(suot), nx.to_numpy(suot_opt), atol=1e-05) def test_sliced_uot_type_devices(nx): rng = np.random.RandomState(0) n = 10 x = rng.randn(n, 2) rho_u = np.abs(rng.randn(n)) rho_u /= rho_u.sum() rho_v = np.abs(rng.randn(n)) rho_v /= rho_v.sum() reg_m = 1.0 xb, rho_ub, rho_vb = nx.from_numpy(x, rho_u, rho_v) if nx.__name__ in ["torch", "jax"]: f, g, usot = ot.unbalanced_sliced_ot( xb, xb, reg_m, rho_ub, rho_vb, 10, seed=42, p=2 ) nx.assert_same_dtype_device(xb, f) nx.assert_same_dtype_device(xb, g) nx.assert_same_dtype_device(xb, usot) else: np.testing.assert_raises( AssertionError, ot.unbalanced_sliced_ot, xb, xb, reg_m, rho_ub, rho_vb, 10, seed=42, p=2, ) if nx.__name__ in ["torch", "jax"]: suot = ot.sliced_unbalanced_ot(xb, xb, reg_m, rho_ub, rho_vb, 10, seed=42, p=2) nx.assert_same_dtype_device(xb, suot) else: np.testing.assert_raises( AssertionError, ot.sliced_unbalanced_ot, xb, xb, reg_m, rho_ub, rho_vb, 10, seed=42, p=2, )