diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 7e0fecc..0000000 --- a/.coveragerc +++ /dev/null @@ -1,18 +0,0 @@ -[report] -exclude_lines = - pragma: no cover - pragma: gt no cover - if __name__ == .__main__.: - def __repr__ - def __eq__ - def __lt__ - raise RuntimeWarning - raise NotImplementedError - except ImportError: - @abstractmethod - @abc.abstractmethod - -omit = - nestmodel/tests/* - -# pytest --cov nestmodel --cov-report html . \ No newline at end of file diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index b61d2a9..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.ipynb filter=jupyternotebook \ No newline at end of file diff --git a/.gitconfig b/.gitconfig deleted file mode 100644 index ca42857..0000000 --- a/.gitconfig +++ /dev/null @@ -1,3 +0,0 @@ -[filter "jupyternotebook"] - clean = python clean_errors.py %f - required diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..c26eeb8 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,136 @@ +# GitHub Actions configuration **EXAMPLE**, +# MODIFY IT ACCORDING TO YOUR NEEDS! +# Reference: https://docs.github.com/en/actions + +name: ci + +on: + release: + types: [published] + push: + # Avoid using all the resources/limits available by checking only + # relevant branches and tags. Other branches can be checked via PRs. + branches: [main] + tags: ['v?[0-9]+.[0-9]+.?[0-9]?*'] # Match tags that resemble a version + pull_request: # Run in every PR + workflow_dispatch: # Allow manually triggering the workflow + schedule: + # Run roughly every 15 days at 00:00 UTC + # (useful to check if updates on dependencies break the package) + - cron: '0 0 1,16 * *' + +permissions: + contents: read + +concurrency: + group: >- + ${{ github.workflow }}-${{ github.ref_type }}- + ${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + wheel-distribution: ${{ steps.wheel-distribution.outputs.path }} + steps: + - uses: actions/checkout@v4 + with: {fetch-depth: 0} # deep clone for setuptools-scm + - uses: actions/setup-python@v4 + id: setup-python + with: {python-version: "3.11"} + - name: Run static analysis and format checkers + run: pipx run pre-commit run --all-files --show-diff-on-failure + - name: Build package distribution files + run: >- + pipx run --python '${{ steps.setup-python.outputs.python-path }}' + tox -e clean,build + - name: Record the path of wheel distribution + id: wheel-distribution + run: echo "path=$(ls dist/*.whl)" >> $GITHUB_OUTPUT + - name: Store the distribution files for use in other stages + # `tests` and `publish` will use the same pre-built distributions, + # so we make sure to release the exact same package that was tested + uses: actions/upload-artifact@v4 + with: + name: python-distribution-files + path: dist/ + retention-days: 1 + + test: + needs: prepare + strategy: + matrix: + python: + - "3.9" # oldest Python supported by PSF + - "3.11" # newest Python that is stable + platform: + - ubuntu-latest +# - macos-latest + - windows-latest + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + id: setup-python + with: + python-version: ${{ matrix.python }} + - name: Retrieve pre-built distribution files + uses: actions/download-artifact@v4 + with: {name: python-distribution-files, path: dist/} + - name: Run tests + run: >- + pipx run --python '${{ steps.setup-python.outputs.python-path }}' + tox --installpkg '${{ needs.prepare.outputs.wheel-distribution }}' + -- -rFEx --durations 10 --color yes # pytest args + - name: Generate coverage report + run: pipx run coverage[toml] lcov -o coverage.lcov + - name: Upload partial coverage report + uses: coverallsapp/github-action@master + with: + path-to-lcov: coverage.lcov + github-token: ${{ secrets.github_token }} + flag-name: ${{ matrix.platform }} - py${{ matrix.python }} + parallel: true + + finalize: + needs: test + runs-on: ubuntu-latest + steps: + - name: Finalize coverage report + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true + + publish: + needs: finalize + if: ${{ (github.event_name == 'push' && contains(github.ref, 'refs/tags/')) || (github.event_name == 'release' && github.event.action == 'published') }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: {python-version: "3.11"} + - name: Retrieve pre-built distribution files + uses: actions/download-artifact@v4 + with: {name: python-distribution-files, path: dist/} + # - name: Publish Package (TEST) + # env: + # # TODO: Set your PYPI_TOKEN as a secret using GitHub UI + # # - https://pypi.org/help/#apitoken + # # - https://docs.github.com/en/actions/security-guides/encrypted-secrets + # TWINE_REPOSITORY: pypi + # TWINE_USERNAME: __token__ + # TWINE_PASSWORD: ${{ secrets.PYPI_TEST_TOKEN }} + # run: pipx run tox -e publish + - name: Publish Package + env: + # TODO: Set your PYPI_TOKEN as a secret using GitHub UI + # - https://pypi.org/help/#apitoken + # - https://docs.github.com/en/actions/security-guides/encrypted-secrets + TWINE_REPOSITORY: pypi + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: pipx run tox -e publish diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml deleted file mode 100644 index 45fbbec..0000000 --- a/.github/workflows/pythonapp.yml +++ /dev/null @@ -1,44 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: build - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - - - build: - - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.8", "3.9", "3.10"] - - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install . - - - name: Test with pytest - run: | - pip install pytest - pytest - -# - name: Lint with flake8 -# run: | -# pip install flake8 -# # stop the build if there are Python syntax errors or undefined names -# flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics -# # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide -# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics \ No newline at end of file diff --git a/.github/workflows/release_to_pypi.yml b/.github/workflows/release_to_pypi.yml deleted file mode 100644 index 6292ce4..0000000 --- a/.github/workflows/release_to_pypi.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Publish nestmodel to PyPI / GitHub - -on: - push: - tags: - - "v*" - -jobs: - publish: - - name: Publish to Pypi - runs-on: ubuntu-latest - - steps: - - name: Checkout source - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: "3.x" - - name: Install pypa/build - run: >- - python -m - pip install - build - --user - - name: Build a binary wheel and a source tarball - run: >- - python -m - build - --sdist - --wheel - --outdir dist/ - . - - - name: Publish a Python distribution to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9b355ab..7b30534 100644 --- a/.gitignore +++ b/.gitignore @@ -154,4 +154,7 @@ scripts/datasets_path.txt env nestmodel.egginfo Untitled.ipynb -Untitled1.ipynb +Untitled*.ipynb +csvs +demo/images +experiments diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..eca2e76 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,68 @@ +exclude: '^docs/conf.py' + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: check-added-large-files + - id: check-ast + - id: check-json + - id: check-merge-conflict + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: mixed-line-ending + args: ['--fix=auto'] # replace 'auto' with 'lf' to enforce Linux/Mac line endings or 'crlf' for Windows + +## If you want to automatically "modernize" your Python code: +# - repo: https://github.com/asottile/pyupgrade +# rev: v3.7.0 +# hooks: +# - id: pyupgrade +# args: ['--py37-plus'] + +## If you want to avoid flake8 errors due to unused vars or imports: +# - repo: https://github.com/PyCQA/autoflake +# rev: v2.1.1 +# hooks: +# - id: autoflake +# args: [ +# --in-place, +# --remove-all-unused-imports, +# --remove-unused-variables, +# ] + +# - repo: https://github.com/PyCQA/isort +# rev: 5.13.2 +# hooks: +# - id: isort + +- repo: https://github.com/psf/black + rev: 24.10.0 + hooks: + - id: black + language_version: python3 + +## If like to embrace black styles even in the docs: +# - repo: https://github.com/asottile/blacken-docs +# rev: v1.13.0 +# hooks: +# - id: blacken-docs +# additional_dependencies: [black] + +- repo: https://github.com/PyCQA/flake8 + rev: 7.1.1 + hooks: + - id: flake8 + additional_dependencies: [Flake8-pyproject] + ## You can add flake8 plugins via `additional_dependencies`: + # additional_dependencies: [flake8-bugbear] + +## Check for misspells in documentation files: +# - repo: https://github.com/codespell-project/codespell +# rev: v2.2.5 +# hooks: +# - id: codespell diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index ecdb121..0000000 --- a/.pylintrc +++ /dev/null @@ -1,8 +0,0 @@ -[MASTER] -disable= missing-module-docstring -[FORMAT] -max-line-length=200 - -# allow 1 or 2 length variable names -good-names-rgxs=^[_a-zGW][_a-z0-9L]?$ -good-names=WL_fast,WL_both, M, A, A0, A1, A_work, M_in, C1, C2, WL1, WL2, SAE, ERGM, ERGM_experiments, G_str \ No newline at end of file diff --git a/demo/Quotient graphs.ipynb b/demo/Quotient graphs.ipynb new file mode 100644 index 0000000..f1b4ef1 --- /dev/null +++ b/demo/Quotient graphs.ipynb @@ -0,0 +1,515 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 7, + "id": "d27b1bfa-2e9b-4288-9dc7-2cd51c2af03d", + "metadata": {}, + "outputs": [], + "source": [ + "import networkx as nx\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4966112-e269-485e-b6c0-a1c5c1df3772", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d76bc4e5-847e-403a-b7ce-d15c97844985", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9127061d-5e34-4e88-821c-729ac9488b9a", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "ce830f39-84bc-428d-865e-00a0ba7363bd", + "metadata": {}, + "outputs": [], + "source": [ + "G = nx.Graph()\n", + "G.add_nodes_from(range(7))\n", + "G.add_edges_from([(0,3), (0,4), (1,3), (1,6), (2,4), (2,5), (3,6), (4,5), (5,6)])" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "3d7e9037-5cb9-44e4-83cb-0eb547323a17", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0, 0, 0, 1, 1, 0, 0],\n", + " [0, 0, 0, 1, 0, 0, 1],\n", + " [0, 0, 0, 0, 1, 1, 0],\n", + " [1, 1, 0, 0, 0, 0, 1],\n", + " [1, 0, 1, 0, 0, 1, 0],\n", + " [0, 0, 1, 0, 1, 0, 1],\n", + " [0, 1, 0, 1, 0, 1, 0]], dtype=int32)" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A = nx.adjacency_matrix(G).todense()\n", + "A" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "eb0d8008-11e1-405b-b75c-2c8ad1cae7d7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[1, 0, 0, 0],\n", + " [0, 1, 0, 0],\n", + " [0, 1, 0, 0],\n", + " [0, 0, 1, 0],\n", + " [0, 0, 1, 0],\n", + " [0, 0, 0, 1],\n", + " [0, 0, 0, 1]])" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "H4 = np.zeros((7,4), dtype=int)\n", + "H4[0,0]=1\n", + "H4[1:3,1]=1\n", + "H4[3:5,2]=1\n", + "H4[5:7,3]=1\n", + "H4" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "6b2fa372-21aa-4f52-a6a2-15ceaeb5d896", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0, 0, 2, 0],\n", + " [0, 0, 1, 1],\n", + " [0, 0, 1, 1],\n", + " [1, 1, 0, 1],\n", + " [1, 1, 0, 1],\n", + " [0, 1, 1, 1],\n", + " [0, 1, 1, 1]])" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A@H4" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "b66727c5-ad93-4afe-8ac0-48d9de184720", + "metadata": {}, + "outputs": [], + "source": [ + "Api = np.array(\n", + " [[0,0,2,0],\n", + " [0,0,1,1],\n", + " [1,0,1,1],\n", + " [0,1,1,1]]\n", + " ,dtype=int)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "025b4aba-f278-4a58-93aa-031496c16ced", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0, 0, 2, 0],\n", + " [0, 0, 1, 1],\n", + " [0, 0, 1, 1],\n", + " [1, 0, 1, 1],\n", + " [1, 0, 1, 1],\n", + " [0, 1, 1, 1],\n", + " [0, 1, 1, 1]])" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "H4@Api" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "7a552d8c-f3e1-4c44-975f-c410ef46a047", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0, 0, 2, 0],\n", + " [0, 0, 2, 2],\n", + " [2, 2, 0, 2],\n", + " [0, 2, 2, 2]])" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "H4.T@A@H4" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "9f54341e-8372-4ec5-9e08-bfe3ba44b42f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0.66666667, 0.66666667, 0. , 0.66666667],\n", + " [0.33333333, 0.66666667, 0.33333333, 0.66666667],\n", + " [0.33333333, 0.66666667, 1.33333333, 0.66666667],\n", + " [0.33333333, 0.66666667, 0.83333333, 1.16666667]])" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Api@np.diag(1/np.sum(H4.T@A@H4,axis=1))@H4.T@A@H4" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "b0fa82af-fa36-43fa-8668-50595fac74fc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0. , 0. , 1. , 0. ],\n", + " [0. , 0. , 0.5 , 0.5 ],\n", + " [0.33333333, 0.33333333, 0. , 0.33333333],\n", + " [0. , 0.33333333, 0.33333333, 0.33333333]])" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "np.diag(1/np.sum(H4.T@A@H4,axis=1))@H4.T@A@H4" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "bd427e93-a7fa-42cd-9d2c-2fdc3e5e0926", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0.66666667, 0.66666667, 0. , 0.66666667],\n", + " [0.33333333, 0.66666667, 0.33333333, 0.66666667],\n", + " [0.33333333, 0.66666667, 1.33333333, 0.66666667],\n", + " [0.33333333, 0.66666667, 0.83333333, 1.16666667]])" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Api @ np.diag(1/np.sum(H4.T@A@H4,axis=1))@H4.T@A@H4" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "b10acd28-2b2e-40a7-b649-30bea37632ff", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([1., 1., 5., 3.])" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(Api.T @ np.diag(1/np.sum(H4.T@A@H4,axis=1))@H4.T@A@H4).sum(axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "60bea038-e237-4c14-ae93-60a4c1c318f1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([2., 2., 3., 3.])" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(Api @ np.diag(1/np.sum(H4.T@A@H4,axis=1))@H4.T@A@H4).sum(axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "5c7c34a8-e038-42e8-857c-33c4c50ad6b7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0.66666667, 0.66666667, 0. , 0.66666667],\n", + " [0.33333333, 0.66666667, 0.33333333, 0.66666667],\n", + " [0.33333333, 0.66666667, 1.33333333, 0.66666667],\n", + " [0.33333333, 0.66666667, 0.83333333, 1.16666667]])" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Api @ np.diag(1/np.sum(H4.T@A@H4,axis=1))@H4.T@A@H4" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "1d92fab1-885a-43d7-b416-5886bbe72656", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0. , 0. , 1. , 0. ],\n", + " [0. , 0. , 0.5 , 0.5 ],\n", + " [0.33333333, 0.33333333, 0. , 0.33333333],\n", + " [0. , 0.33333333, 0.33333333, 0.33333333]])" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "np.diag(1/np.sum(H4.T@A@H4,axis=1))@H4.T@A@H4" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "cef48364-85f0-40dc-af8d-20606de70555", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0.33333333, 0.33333333, 0. , 0.33333333],\n", + " [0. , 0.33333333, 0.33333333, 0.33333333],\n", + " [0.33333333, 0.66666667, 2.83333333, 1.16666667],\n", + " [0.33333333, 0.66666667, 0.83333333, 1.16666667]])" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Api.T@np.diag(1/np.sum(H4.T@A@H4,axis=1))@H4.T@A@H4\n" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "eed70d26-4c10-417f-9bd8-9c658095da43", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0.33333333, 0.33333333, 0.33333333, 0.33333333],\n", + " [0. , 0.33333333, 0.33333333, 0.33333333],\n", + " [0.33333333, 0.66666667, 2.66666667, 1.16666667],\n", + " [0.33333333, 0.66666667, 0.66666667, 1.16666667]])" + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Api.T@np.diag(1/np.sum(H4.T@A@H4,axis=1))@Api.T@H4.T@H4" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "00e2d34d-2924-4f0a-964e-39cb7060e566", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0.4 , 0. , 0.8 , 0.8 ],\n", + " [0.2 , 0.33333333, 0.73333333, 0.73333333],\n", + " [0.2 , 0.33333333, 1.73333333, 0.73333333],\n", + " [0.2 , 0.33333333, 1.23333333, 1.23333333]])" + ] + }, + "execution_count": 68, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Api@np.diag(1/np.sum(Api@H4.T@H4,axis=1))@Api@H4.T@H4" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "21a47f8c-526d-424c-95ad-3ad3adf1681d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0, 0, 2, 0],\n", + " [0, 0, 2, 2],\n", + " [2, 2, 0, 2],\n", + " [0, 2, 2, 2]])" + ] + }, + "execution_count": 69, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "H4.T@A@H4" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "d4f3af88-7e33-4a8c-9514-df416c61d3ef", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[1, 0, 0, 0],\n", + " [0, 2, 0, 0],\n", + " [0, 0, 2, 0],\n", + " [0, 0, 0, 2]])" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "H4.T@H4" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "81d06770-596b-476d-a9e4-15071784d4c4", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demo/Visualize Flips of the Markov chain.ipynb b/demo/Visualize Flips of the Markov chain.ipynb new file mode 100644 index 0000000..1420031 --- /dev/null +++ b/demo/Visualize Flips of the Markov chain.ipynb @@ -0,0 +1,354 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "81b9f64a-cef8-4d91-a65e-7886abc26983", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import matplotlib.pyplot as plt\n", + "import networkx as nx\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "b35318e2-474f-480a-bb16-6653b240a11f", + "metadata": {}, + "source": [ + "# Overview of the flips required\n", + "\n", + "Depending on the properties of the original graph that should be preserved in NeSt samples, there are different flip strategies necessary" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c7050060-a6e3-46dc-9caf-d221f92696c8", + "metadata": {}, + "outputs": [], + "source": [ + "def draw_and_save(G, p, name, node_colors, positions = None, xlim=None, ylim=None, arrowsize=45):\n", + " num_nodes = G.number_of_nodes()\n", + " fig = plt.figure(figsize=(3,3))\n", + " \n", + " p.mkdir(parents=True, exist_ok=True)\n", + " p_pdf = p/\"pdf\"\n", + " p_pdf.mkdir(parents=True, exist_ok=True)\n", + " dx = 0.7\n", + " dy=0.6\n", + " if positions is None:\n", + " positions = [(-dx,-dy), (-dx,dy), (dx,-dy), (dx,dy)]\n", + " nx.draw(G,\n", + " pos=positions, \n", + " width=edge_width,\n", + " node_size=1.5*node_size,\n", + " node_color = node_colors,\n", + " arrowsize=arrowsize,\n", + " )\n", + " a = 0.9\n", + " b = a\n", + " plt.axis(\"square\")\n", + " # fig.patch.set_facecolor('lightgray')\n", + " if xlim is None:\n", + " xlim =(-a,a)\n", + " plt.xlim(*xlim)\n", + " if ylim is None:\n", + " ylim=(-b,b)\n", + " plt.ylim(*ylim)\n", + " \n", + " plt.savefig(p/f\"{name}.png\", bbox_inches=\"tight\", transparent=True, dpi=600)\n", + " plt.savefig(p_pdf/f\"{name}.pdf\", bbox_inches=\"tight\", transparent=True, dpi=600)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "04df41a2-0b18-4915-b7b0-c14957017f88", + "metadata": {}, + "outputs": [], + "source": [ + "edge_width = 9\n", + "with_labels=True\n", + "node_size=850" + ] + }, + { + "cell_type": "markdown", + "id": "f745eed8-9d04-40ca-8b9f-94e77260b618", + "metadata": {}, + "source": [ + "## Preserving the in and out colors\n", + "\n", + "Also called in-out NeSt or both-NeSt\n", + "\n", + "You additionally also need the triangle flip approach" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "b77b2012-92b3-4162-b469-41d25ffb83e4", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "p = Path(f\"./images/rewire_demo\")\n", + "\n", + "G = nx.DiGraph()\n", + "G.add_nodes_from(range(4))\n", + "G.add_edges_from([(0,2),(1,3)])\n", + "\n", + "node_colors = ['paleturquoise',\n", + " 'paleturquoise',\n", + " 'peachpuff',\n", + " 'peachpuff',]\n", + "draw_and_save(G, p, \"basic_dir\", node_colors)\n", + "\n", + "G2 = nx.DiGraph()\n", + "G2.add_nodes_from(range(4))\n", + "G2.add_edges_from([(0,3),(1,2)])\n", + "draw_and_save(G2, p, \"basic_dir_flipped\", node_colors)" + ] + }, + { + "cell_type": "markdown", + "id": "e80f157d-696f-47d7-8d98-732df853d4f5", + "metadata": {}, + "source": [ + "## Preserving in colors plus out degree (Pagerank)\n", + "\n", + "You additionally also need the triangle flip approach" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "2a6e61e0-a8b5-4116-a449-bab41c443079", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "G = nx.DiGraph()\n", + "G.add_nodes_from(range(4))\n", + "G.add_edges_from([(0,2),(1,3)])\n", + "\n", + "node_colors = ['paleturquoise',\n", + " 'paleturquoise',\n", + " 'xkcd:pale violet',\n", + " 'peachpuff',]\n", + "draw_and_save(G, p, \"loose_dir\", node_colors)\n", + "\n", + "G2 = nx.DiGraph()\n", + "G2.add_nodes_from(range(4))\n", + "G2.add_edges_from([(0,3),(1,2)])\n", + "draw_and_save(G2, p, \"loose_dir_flipped\", node_colors)" + ] + }, + { + "cell_type": "markdown", + "id": "dfce06ea-55ae-451b-8617-eb2f3306db30", + "metadata": {}, + "source": [ + "## The triangle reverse\n", + "\n", + "For both of the approaches presented above, a directed triangle reverse move is necessary for mono colored subgrahs" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f3d7d971-992a-46c3-9e43-240704783f24", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "G = nx.DiGraph()\n", + "G.add_nodes_from(range(3))\n", + "G.add_edges_from([(0,1),(1,2), (2,0)])\n", + "\n", + "node_colors = ['paleturquoise',\n", + " 'paleturquoise',\n", + " 'paleturquoise',]\n", + "h = 0.7\n", + "a = np.sqrt(4/3)*h\n", + "positions = [(0,h), (a/2, 0), (-a/2, 0)]\n", + "draw_and_save(G, p, \"triangle_dir\", node_colors, positions=positions,ylim =[-0.1,0.8], xlim=[-0.55,0.55], arrowsize=35)\n", + "\n", + "G2 = nx.DiGraph()\n", + "G2.add_nodes_from(range(3))\n", + "G2.add_edges_from([(1,0),(2,1), (0,2)])\n", + "draw_and_save(G2, p, \"triangle_dir_flipped\", node_colors, positions=positions,ylim =[-0.1,0.8], xlim=[-0.55,0.55], arrowsize=35)" + ] + }, + { + "cell_type": "markdown", + "id": "dec2b673-379e-4293-b1ca-bce4eec6ff53", + "metadata": {}, + "source": [ + "## Preserving only the in-colors\n", + "\n", + "While it is possible todo MCMC sampling here, you can also use a direct sampling approach in this case." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "521d0891-8b4f-4c19-a829-3b8a4e802a3a", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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DqTL70tbNwr+IDdeqv/Q18YBLFklAZTGs8Hzm5u6OBwq5OpkkyW52s7pZLutY8z6gP3xXrpxSbr+mZQmUugF71xDmj8sLZ2obIGGygI4McxzXRoO9WmSqJ/n+/4VT973cBzreLutQ3FM+IAQvJcGV8a7xoP7WUtw+6+02Rnf1zt/OOjSVvsaUdUBnPDe60qa6vJopOtZprU0/cJZ450p2znglnNajGMtRJEemZtTl11doDijgbT+ubR1c3ztYDa85QWTv9Qdf6UuGl/O0j4gdZfGZ3enZHIyNQG8lfde/TzXWpYpyXNl299rOKdIUjeKtBBFanE5C1CddCglN8dh9Pn2cvtu/NzeaG68fH3zgakJ3q2mNoAAUDSmTwDMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMyKqx7AnXjv1c9zrWWZkjxXtv29hnOKJHWjSAtRpFaDhgPYn6kJoPdeV9JUn49Gup6mupFlynb8v9v52h3/bjmng3Gsg1GkR+bm1I2ikkYMoO6c997f/WXFGea5Lg2H+jRJ1M9zOX0xcHs1ed/Dcazj7bYOxbGcc3d7GwDDKgtgkuc6v7mpi8Oh8oDbnYSw7ZxOtts61moRQgC3VUkAV4ZDvTMYKPV+X7O9e7EYRVrudtVj1xjALUoNYJLnencw0OpoVNZHym1/Pdnp6DizQQA7lBbA62mqNzc2NCph1ncnS3Gsp3s9NYkgAJUUwCujkd7c2PjCWd2qHIginen1NMflM4B5hVfgaprqjSmJnyTdzDL95/ZMFIBthQbwRprqjfX1oGd575eXtJ5lenN9XRkRBEwrLICZ9/pDvz9V8Zvwkq5lmc5vblY9FAAVKiyA5zc3Ncjzyk547MXHSaJraVr1MABUpJAAXktTfZwkRWw6KCfpXL/PrjBgVPAAZt7rrX5fdbjQxEvqb9+RAsCe4AG8OBxO/a7vrT5JEm3l03i0EkCRggbQe69PtrZCbrIUXtKFGuyyAwgraACvpqk2ajqT+ixJlHMsEDAlaAA/S5JaHPu7ncR7fV7iPcoAqhcsgMM81+poVKtjfzs5jQMOwI5gAbyeZbWNn7R9cXSaquL1YQGUKFgA19K0tru/E5nGl8UAsIEZ4C3WsmlZtgFA0cIFcAZuKXOajZ8DwN4ECeDIeyUzcOzMS7rBDBAwI0gA0xmI38Qs/SwAdhckgLO0mMAs/SwAdse68ADMChLAaIYeMjRLPwuA3QUJYDxD0ZilnwXA7oIEsOmcWjMQDqfxU+MA2BDsGODBOA61qcp4zcbPAWBvwgUwimp/K5wkLTADBMwIFsCFOK79rXCRpC4PTAfMYAa4zUlajGO5GTiWCWBvggVwrtHQ4WazthH0ko61WlUPA0CJgu7vHWu1arsb3HJOX2k2qx4GgBIFDeBDcaxeTY+hHWu11GD3FzAlaK2cc3q83Q65yVI4SUfZ/QXMCT5de2xuTt1Go1bHAh9vtdSu6cwVwP4F/6uPnNNT3W4tjgU6jS97OdXpVD0UABUoZNqzGMc6UYNdSi/pqW6XBRAAowrb7zvV6Uz9rvCJVkuL3PoGmFVYACPn9O1udyoXHHSSFqOIXV/AuEL7dCCOdXp+fqoi6CTNR5G+Mz/Pri9gXOFteiiOdbrX07QsMXAgivTPvZ6axA8wz3lfzkMw1tJUb2xsaOR9ZWeIl+JYTxM/ANtKC6AkJXmu9wYDrYxGZX2k3PbXk52OjrdaLHYA4G9KDeDE6nCotwcDpSXMBhejSMvdrnqs8wfgFpUEUBrPBs9vburicKg84Hadxtf3tZ3TyXZbx5j1AbiDygI4McxzXRoO9WmSqJ/nfwvYvZq87+E41vF2W4dY2w/AXVQewAnvva6kqS6PRrqeZVpLU2U7/n9nynYOuOWcFuNYC1GkR+bm1GVXF8AeTU0Ab+W9Vz/PtZZlSvJc2fb3Gs6Nl66PIi1EkVosYgBgn6Y2gABQNKZPAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzCKAAMwigADMIoAAzPp/mtZ8YV58f9sAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "G = nx.DiGraph()\n", + "G.add_nodes_from([0,1,3])\n", + "G.add_edges_from([(1,3)])\n", + "\n", + "node_colors = ['paleturquoise',\n", + " 'paleturquoise',\n", + " 'peachpuff',]\n", + "draw_and_save(G, p, \"tilt_dir\", node_colors)\n", + "\n", + "G2 = nx.DiGraph()\n", + "G2.add_nodes_from([0,1,3])\n", + "G2.add_edges_from([(0,3)])\n", + "draw_and_save(G2, p, \"tilt_dir_flipped\", node_colors)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58e1d881-5cc0-493e-80c6-d85eaeec93e9", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55e8335c-81e9-41d1-81c6-7ffcb7815bac", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9353b631-dcb5-4b97-9f3b-f99124b47fc7", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1e38c35c-9b7c-4ef6-a4e2-55566842e491", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/nestmodel/fast_graph.py b/nestmodel/fast_graph.py deleted file mode 100644 index 5e1f8ab..0000000 --- a/nestmodel/fast_graph.py +++ /dev/null @@ -1,314 +0,0 @@ -from copy import copy -import numpy as np -import warnings -from nestmodel.utils import networkx_from_edges, graph_tool_from_edges, calc_color_histogram, switch_in_out, make_directed -from nestmodel.fast_wl import WL_fast, WL_both - -from nestmodel.fast_rewire import rewire_fast, dir_rewire_source_only_fast, sort_edges, get_block_indices - -def ensure_is_numpy_or_none(arr, dtype=np.int64): - """Validates that sth is numpy array of sth iterable""" - if arr is None: - return arr - if isinstance(arr, (tuple, list)): - return np.array(arr, dtype=dtype) - if isinstance(arr, np.ndarray): - return arr - else: - raise ValueError(f"Unexpected input type received {type(arr)}. {arr}") - - -class FastGraph: - """A custom class representing Graphs through edge lists that can be used to efficiently be rewired""" - def __init__(self, edges, is_directed, check_results=False, num_nodes=None): - assert edges.dtype==np.uint32 or edges.dtype==np.uint64 - assert isinstance(is_directed, bool), f"wrong type of is_directed: {type(is_directed)}" - self._edges = edges.copy() - self.edges_ordered = None - self.is_directed = is_directed - self.base_partitions = None - self.latest_iteration_rewiring = None - if num_nodes is None: - self.num_nodes = edges.ravel().max()+1 - else: - self.num_nodes = num_nodes - self.check_results = check_results - self.wl_iterations = None - - # these will be set in reset_edges_ordered - self.edges_classes = None - self.dead_arr = None - self.is_mono = None - self.block_indices = None - self.block_dead = None - - self.out_degree = np.array(np.bincount(edges[:,0].ravel(), minlength=self.num_nodes), dtype=np.uint32) - self.in_degree = np.array(np.bincount(edges[:,1].ravel(), minlength=self.num_nodes), dtype=np.uint32) - - if self.is_directed: - self.out_dead_ends = np.nonzero(self.out_degree==0)[0] - self.corr_out_degree=self.out_degree.copy() - self.corr_out_degree[self.out_dead_ends]+=1 - - self.in_dead_ends = np.nonzero(self.in_degree==0)[0] - self.corr_in_degree=self.in_degree.copy() - self.corr_in_degree[self.in_dead_ends]+=1 - - #print(len(self.out_dead_ends), len(self.in_dead_ends)) - else: - self.out_degree=self.out_degree+self.in_degree - self.in_degree=self.out_degree - self.out_dead_ends = np.nonzero(self.out_degree==0)[0] - self.in_dead_ends = self.out_dead_ends - self.sorting_strategy = None - - - - @property - def edges(self,): - """Return the current edges of the graph""" - if self.edges_ordered is None: - return self._edges - else: - return self.edges_ordered - - - - @staticmethod - def from_gt(G): # pragma: gt no cover - """Creates a FastGraph object from a graphtool graph""" - edges = np.array(G.get_edges(), dtype=np.uint32) - is_directed = G.is_directed() - return FastGraph(edges, is_directed) - - - - @staticmethod - def from_nx(G, allow_advanced_node_labels=False): - """Creates a FastGraph object from a networkx graph - - if the networkx graph has non integer node labes you need to set allow_advanced_node_labels=True - - """ - unmapping = None - if allow_advanced_node_labels: - mapping = {node: index for index, node in enumerate(G.nodes)} - unmapping = {value: key for key, value in mapping.items()} - edges_nx = [(mapping[u], mapping[v]) for u, v in G.edges] - else: - edges_nx = G.edges - edges = np.array(edges_nx, dtype=np.uint32) - is_directed = G.is_directed() - - if unmapping is None: - return FastGraph(edges, is_directed) - else: - return FastGraph(edges, is_directed), unmapping - def switch_directions(self): - """Creates a FastGraph object from a graphtool graph""" - edges = switch_in_out(self.edges) - is_directed = self.is_directed - return FastGraph(edges, is_directed, num_nodes=self.num_nodes) - - - def to_gt(self): - """Convert the graph to a graph-tool graph""" - edges = self.edges - return graph_tool_from_edges(edges, self.num_nodes, self.is_directed) - - - def to_nx(self, is_multi=False): - """Convert the graph to a networkx graph""" - edges = self.edges - return networkx_from_edges(edges, self.num_nodes, self.is_directed, is_multi=is_multi) - - - - def to_coo(self): - """Returns a sparse coo-matrix representation of the graph""" - from scipy.sparse import coo_matrix # pylint: disable=import-outside-toplevel - edges = self.edges - if not self.is_directed: - edges = make_directed(edges) - - return coo_matrix((np.ones(edges.shape[0]), (edges[:,0], edges[:,1])), shape = (self.num_nodes, self.num_nodes)) - - - def save_npz(self, outfile, include_wl=False): - """Save the FastGraph object as .npz""" - if not include_wl: - np.savez(outfile, edges=self.edges, is_directed=self.is_directed) - else: - if self.base_partitions is None or self.wl_iterations is None: - raise NotImplementedError("Saving without computing the information first makes no sense") - kwargs = { "edges":self.edges, - "is_directed":self.is_directed, - "base_partitions":self.base_partitions, - "wl_iterations":self.wl_iterations, - "edges_classes" : self.edges_classes, - "mono_len": len(self.is_mono), - "block_len" : len(self.block_indices)} - for i, x in enumerate(self.is_mono): - kwargs[f"mono{i}_keys"] = np.array(list(x.keys()), np.int64) - kwargs[f"mono{i}_values"] = np.array(list(x.values()), np.bool_) - for i, x in enumerate(self.block_indices): - kwargs[f"block_indices{i}"] = x - np.savez(outfile, **kwargs) - - @staticmethod - def load_npz(file): - """Load a FastGraph object from a npz file""" - npzfile = np.load(file) - if len(npzfile) == 2: - return FastGraph(npzfile["edges"], bool(npzfile["is_directed"])) - else: - G = FastGraph(npzfile["edges"], bool(npzfile["is_directed"])) - G.base_partitions = npzfile["base_partitions"] - G.wl_iterations = npzfile["wl_iterations"] - G.edges_classes = npzfile["edges_classes"] - - from nestmodel.fast_rewire import create_mono_from_arrs - G.is_mono = [] - for i in range(npzfile["mono_len"]): - G.is_mono.append(create_mono_from_arrs(npzfile[f'mono{i}_keys'], npzfile[f'mono{i}_values'])) - G.block_indices = [] - for i in range(npzfile["block_len"]): - G.block_indices.append(npzfile[f'block_indices{i}']) - return G - - - def calc_wl(self, initial_colors=None, max_depth=None): - """Compute the WL colors of this graph using the provided initial colors""" - return self._calc_wl(WL_fast, initial_colors, max_depth=max_depth) - - - def calc_wl_both(self, initial_colors=None, max_depth=None): - """Compute the WL partition over both the in and out neighborhood""" - return self._calc_wl(WL_both, initial_colors, max_depth=max_depth) - - - def _calc_wl(self, method, initial_colors=None, max_depth=None): - edges = self.edges - if not self.is_directed: - edges2 = np.vstack((edges[:,1], edges[:,0])).T - edges = np.vstack((edges, edges2)) - if type(initial_colors).__module__ == np.__name__: # is numpy array - return method(edges, self.num_nodes, labels = initial_colors, max_iter=max_depth) - elif isinstance(initial_colors, str): - assert initial_colors in ("out-degree", "out_degree") - return method(edges, self.num_nodes, labels = self.out_degree, max_iter=max_depth) - else: - return method(edges, self.num_nodes, max_iter=max_depth) - - def ensure_base_wl(self, initial_colors=None, both=False, max_depth=None): - """Compute the base WL partition if they have not yet been computed""" - if self.base_partitions is None: - self.calc_base_wl(initial_colors=initial_colors, both=both, max_depth=max_depth) - - - def calc_base_wl(self, initial_colors=None, both=False, max_depth=None): - """Compute and store the base WL partition""" - if not self.latest_iteration_rewiring is None: - raise ValueError("Seems some rewiring already employed, cannot calc base WL") - if both is False: - partitions = self.calc_wl(initial_colors=initial_colors, max_depth=max_depth) - else: - partitions = self.calc_wl_both(initial_colors=initial_colors, max_depth=max_depth) - - self.base_partitions = np.array(partitions, dtype=np.uint32) - self.wl_iterations = len(self.base_partitions) - - - def ensure_edges_prepared(self, initial_colors=None, both=False, max_depth=None, sorting_strategy=None): - """Prepare the edges by first ensuring the base WL and then sorting edges by base WL""" - initial_colors = ensure_is_numpy_or_none(initial_colors, dtype=np.uint32) - if self.base_partitions is None: - self.ensure_base_wl(initial_colors=initial_colors, both=both, max_depth=max_depth) - if self.edges_ordered is None: - self.reset_edges_ordered(sorting_strategy) - - - def reset_edges_ordered(self, sorting_strategy=None): - """Sort edges according to the partitions""" - self.edges_ordered, self.edges_classes, self.dead_arr, self.is_mono, self.sorting_strategy = sort_edges(self._edges, self.base_partitions, self.is_directed, sorting_strategy) - self.block_indices, self.block_dead = get_block_indices(self.edges_classes, self.dead_arr) - - - def copy(self): - """Returns a copy of this graph which has no data shared with the original graph""" - G = FastGraph(self._edges.copy(), self.is_directed) - for key, value in self.__dict__.items(): - setattr(G, key, copy(value)) - return G - - - def rewire(self, depth, method, **kwargs): - """Rewire the edges of the graph in place, thereby preserving the colors of depth d - Note you cannot call this function with increasing depth, but rather with decreasing depth only - """ - assert self.base_partitions is not None, "Base partitions are none. Call G.ensure_edges_prepared() first." - assert depth < len(self.base_partitions), f"{depth} {len(self.base_partitions)}" - assert self.latest_iteration_rewiring is None or depth <= self.latest_iteration_rewiring, f"{depth} {self.latest_iteration_rewiring}" - assert method in (1, 2) - if kwargs is not None: - for key in kwargs: - assert key in ("seed", "n_rewire", "r", "parallel", "source_only"), "Invalid keyword provided {key}" - self.latest_iteration_rewiring = depth - - self.ensure_edges_prepared() - if self.check_results: # pragma: no cover - if self.is_directed: - ins, outs = calc_color_histogram(self._edges, self.base_partitions[depth], self.is_directed) - else: - hist = calc_color_histogram(self._edges, self.base_partitions[depth], self.is_directed) - if method==1: - seed = kwargs.get("seed", None) - r = kwargs.get("r", 1) - parallel = kwargs.get("parallel", False) - source_only = kwargs.get("source_only", False) - if self.is_directed and source_only: - if self.sorting_strategy != "source": - warnings.warn(message=f"source only rewiring should be performed but sorting strategy is {self.sorting_strategy}!=source."+ - " Dubious behaviour expected. Use sorting_strategy='source' when calling .ensure_edges_prepared", category=RuntimeWarning) - dir_rewire_source_only_fast(self.edges_ordered, - self.base_partitions[depth], - self.block_indices[depth], - seed=seed, - num_flip_attempts_in=r, - parallel=parallel) - else: - rewire_fast(self.edges_ordered, - self.edges_classes[:,depth], - self.is_mono[depth], - self.block_indices[depth][np.logical_not(self.block_dead[depth]),:], - self.is_directed, - seed=seed, - num_flip_attempts_in=r, - parallel=parallel) - res = None - elif method == 2: - from nestmodel.fast_rewire2 import fg_rewire_nest # pylint: disable=import-outside-toplevel - res = fg_rewire_nest(self, depth, kwargs["n_rewire"], kwargs["seed"]) - - if self.check_results: # pragma: no cover - if self.is_directed: - from nestmodel.tests.testing import check_color_histograms_agree # pylint: disable=import-outside-toplevel - ins2, outs2 = calc_color_histogram(self.edges_ordered, self.base_partitions[depth], self.is_directed) - check_color_histograms_agree(ins, ins2) - check_color_histograms_agree(outs, outs2) - - assert np.all(self.in_degree == np.bincount(self.edges[:,1].ravel(), minlength=self.num_nodes)) - assert np.all(self.out_degree == np.bincount(self.edges[:,0].ravel(), minlength=self.num_nodes)) - - #check_colors_are_correct(self, depth) - - else: - #print("checking degree") - degree = self.in_degree - curr_degree1 = np.bincount(self.edges[:,0].ravel(), minlength=self.num_nodes) - curr_degree2 = np.bincount(self.edges[:,1].ravel(), minlength=self.num_nodes) - assert np.all(degree == (curr_degree1+curr_degree2)) - - hist2 = calc_color_histogram(self.edges, self.base_partitions[depth], self.is_directed) - check_color_histograms_agree(hist, hist2) - return res diff --git a/nestmodel/tests/test_fast_graph.py b/nestmodel/tests/test_fast_graph.py deleted file mode 100644 index 5043e3f..0000000 --- a/nestmodel/tests/test_fast_graph.py +++ /dev/null @@ -1,277 +0,0 @@ -# pylint: disable=missing-function-docstring, missing-class-docstring -import unittest -import networkx as nx -from nestmodel.fast_graph import FastGraph -from numpy.testing import assert_array_equal -import numpy as np -import os - - - - - - -class TestFastGraph(unittest.TestCase): - def test_edges(self): - edges = np.array([[0,1]], dtype=np.uint32) - G = FastGraph(edges.copy(), is_directed=True) - assert_array_equal(edges, G.edges) - - def test_save_npz(self): - edges = np.array([[0,1]], dtype=np.uint32) - G = FastGraph(edges.copy(), is_directed=True) - G.save_npz("./out.npz") - - G2 = FastGraph.load_npz("./out.npz") - assert_array_equal(edges, G2.edges) - - os.remove("./out.npz") - - - def test_save_npz_wl(self): - edges = np.array([[0,1], [1,2]], dtype=np.uint32) - G = FastGraph(edges.copy(), is_directed=False) - G.ensure_edges_prepared() - G.save_npz("./out.npz", include_wl=True) - - G2 = FastGraph.load_npz("./out.npz") - preserved_attrs = ["edges", "base_partitions", - "wl_iterations", - "edges_classes", - "is_mono", - "block_indices",] - for attr in preserved_attrs: - assert_array_equal(getattr(G, attr), getattr(G2, attr)) - - os.remove("./out.npz") - - - def test_copy(self): - edges = np.array([[0,1]], dtype=np.uint32) - G = FastGraph(edges.copy(), is_directed=True) - G2 = G.copy() - - self.assertFalse(G is G2) - assert_array_equal(G2.edges, G.edges) - self.assertFalse(G2.edges is G.edges) - - - def test_rewire1_double_edge_1(self): - edges = np.array([[0,1],[2,3]], dtype=np.uint32) - - G = FastGraph(edges.copy(), is_directed=True) - G.ensure_edges_prepared() - G.rewire(0, 1, seed=1, r=1) - assert_array_equal(G.edges, edges) - - edges2 = np.array([[0,3],[2,1]], dtype=np.uint32) - G = FastGraph(edges.copy(), is_directed=True) - G.ensure_edges_prepared() - G.rewire(0, method=1, seed=0, r=1) - assert_array_equal(G.edges, edges2) - - - - def test_rewire1_double_edge_2(self): - edges = np.array([[0,1],[2,3]], dtype=np.uint32) - - G = FastGraph(edges.copy(), is_directed=True) - G.ensure_edges_prepared() - G.rewire(0, method=2, seed=1, n_rewire=1) - assert_array_equal(G.edges, edges) - - edges2 = np.array([[0,3],[2,1]], dtype=np.uint32) - G.rewire(0, 1, seed=0, n_rewire=1) - assert_array_equal(G.edges, edges2) - - - def test_rewire1_double_edge(self): - edges_in = np.array([[0,1],[2,3]], dtype=np.uint32) - - result_edges = [ - np.array([[0, 3], [1, 2]], dtype=np.uint32), - np.array([[0, 1], [2, 3]], dtype=np.uint32), - np.array([[0, 2], [3, 1]], dtype=np.uint32), - np.array([[0, 1], [2, 3]], dtype=np.uint32), - np.array([[0, 1], [2, 3]], dtype=np.uint32), - np.array([[1, 0], [2, 3]], dtype=np.uint32), - np.array([[0, 2], [3, 1]], dtype=np.uint32), - np.array([[1, 2], [0, 3]], dtype=np.uint32), - np.array([[1, 3], [2, 0]], dtype=np.uint32), - np.array([[0, 3], [2, 1]], dtype=np.uint32) - ] - - for i, res_edges in enumerate(result_edges): - G = FastGraph(edges_in.copy(), is_directed=False) - G.ensure_edges_prepared() - - G.rewire(0, 1, seed=i, r=1) - assert_array_equal(G.edges, res_edges, f"{i}") - - def calc_base_wl_after_rewire_raises(self): - G = FastGraph(np.array([[0,1],[2,3]], dtype=np.uint32), is_directed=False) - G.ensure_edges_prepared() - G.rewire(0, 1, seed=0, r=1) - with self.assertRaises(ValueError): - G.calc_base_wl() - - - - def test_fast_graph_directed_triangle(self): - G = FastGraph(np.array([[0,1], [1,2], [2,0]], dtype=np.uint32), is_directed=True) - G.ensure_edges_prepared() - G.rewire(0, 1, seed=3, r=1) - assert_array_equal(G.edges, np.array([[1,0],[2,1], [0,2]])) - - def test_calc_wl(self): - edges = np.array([[0,1],[2,3], [2,4]], dtype=np.uint32) - G = FastGraph(edges, is_directed=True) - res1, res2 = [[0, 0, 0, 0, 0], [0, 1, 0, 1, 1]] - out1, out2 = G.calc_wl() - assert_array_equal(res1, out1) - assert_array_equal(res2, out2) - - def test_calc_wl_out_degree(self): - edges = np.array([[0,1],[2,3], [2,4]], dtype=np.uint32) - G = FastGraph(edges, is_directed=True) - res1, res2 = [[0, 1, 2, 1, 1], [0, 1, 2, 3, 3]] - out1, out2 = G.calc_wl("out_degree") - assert_array_equal(res1, out1) - assert_array_equal(res2, out2) - - def test_calc_wl_init_colors(self): - edges = np.array([[0,1],[2,3], [2,4]], dtype=np.uint32) - G = FastGraph(edges, is_directed=True) - res1, res2 = [[0, 1, 2, 1, 1], [0, 1, 2, 3, 3]] - out1, out2 = G.calc_wl(np.array([0, 1, 2, 1, 1], dtype=np.uint32)) - assert_array_equal(res1, out1) - assert_array_equal(res2, out2) - - def test_calc_wl_both(self): - edges = np.array([[0,1],[1,2], [3,4], [4,5], [4,6]], dtype=np.uint32) - G = FastGraph(edges.copy(), is_directed=True) - results = [np.zeros(7, dtype=np.uint32),[0, 1, 2, 0, 3, 2, 2], [0, 1, 2, 3, 4, 5, 5]] - res0, res1, res2 = results - start, out1, out2 = G.calc_wl_both() - assert_array_equal(res0, start) - assert_array_equal(res1, out1) - assert_array_equal(res2, out2) - - G.calc_base_wl(both=True) - self.assertEqual(G.wl_iterations, 3) - assert_array_equal(G.base_partitions, np.array(results)) - - G = FastGraph(edges.copy(), is_directed=True) - out1, out2 = G.calc_wl_both(initial_colors=np.array([0, 1, 2, 0, 3, 2, 2], dtype=np.uint32)) - assert_array_equal(res1, out1) - assert_array_equal(res2, out2) - - def test_rewire_large(self): - result = [[62, 40], [65, 2], [5, 30], [7, 71], [8, 13], [10, 85], [12, 9], [14, 15], - [16, 17], [18, 61], [20, 21], [22, 93], [3, 76], [26, 51], [29, 43], [19, 37], [32, 11], - [35, 89], [36, 31], [38, 28], [49, 84], [42, 96], [44, 45], [46, 4], [48, 34], [50, 27], - [52, 53], [54, 72], [56, 69], [99, 39], [60, 0], [25, 55], [64, 63], [66, 67], [68, 24], - [70, 83], [47, 23], [74, 75], [33, 1], [82, 59], [80, 6], [73, 77], [41, 57], [86, 81], - [88, 94], [90, 91], [92, 100], [95, 78], [97, 79], [98, 58], [101, 87]] - edges = np.array([[i,i+1] for i in range(0,102,2)], dtype=np.uint32) - G = FastGraph(edges, is_directed=False) - G.ensure_edges_prepared() - G.rewire(0, method=1, seed=0, r=1) - assert_array_equal(G.edges, result) - - def test_rewire_limited_depth(self): - G = FastGraph(np.array([(0,2), (1,2)], dtype=np.uint32), is_directed=False) - G.ensure_edges_prepared(max_depth=1) - self.assertEqual(G.wl_iterations, 1) - - def test_wl_limited_depth(self): - edges = np.array([(0,2), (1,2), (2,3)], dtype=np.uint32) - G = FastGraph(edges, is_directed=False) - with self.assertRaises(ValueError): - G.ensure_edges_prepared(max_depth=0) - - G = FastGraph(edges, is_directed=False) - G.ensure_edges_prepared(max_depth=1) - self.assertEqual(G.wl_iterations, 1) - - G = FastGraph(edges, is_directed=False) - G.ensure_edges_prepared(max_depth=2) - self.assertEqual(G.wl_iterations, 2) - - def test_wl_limited_depth_both(self): - edges = np.array([(0,2), (1,2), (2,3)], dtype=np.uint32) - G = FastGraph(edges, is_directed=False) - with self.assertRaises(ValueError): - G.ensure_edges_prepared(max_depth=0, both=True) - - G = FastGraph(edges, is_directed=False) - G.ensure_edges_prepared(max_depth=1, both=True) - self.assertEqual(G.wl_iterations, 1) - - G = FastGraph(edges, is_directed=False) - G.ensure_edges_prepared(max_depth=2, both=True) - self.assertEqual(G.wl_iterations, 2) - - def test_source_only_rewiring(self): - G = FastGraph(np.array([(0,1)], dtype=np.uint32), is_directed=True, num_nodes=3) - G.ensure_edges_prepared(sorting_strategy="source") - G.rewire(0, method=1, seed=3, r=1, source_only=True) - np.testing.assert_array_equal(G.edges, [[2, 1]]) - - def test_source_only_rewiring_parallel(self): - G = FastGraph(np.array([(0,1)], dtype=np.uint32), is_directed=True, num_nodes=3) - G.ensure_edges_prepared(sorting_strategy="source") - G.rewire(0, method=1, seed=3, r=1, source_only=True, parallel=True) - np.testing.assert_array_equal(G.edges, [[2, 1]]) - - - def test_prrewiring_only_rewiring(self): - G = FastGraph(np.array([(0,2), (0,3), (2,3), (1,4), (6,7), (1,5), (0,6), (0,7), (1,8), (1,9)], dtype=np.uint32), is_directed=False, num_nodes=10) - G.ensure_edges_prepared(sorting_strategy="source") - G.rewire(0, method=1, seed=3, r=1) - np.testing.assert_array_equal(G.block_indices[0], [[0, 10]]) - np.testing.assert_array_equal(G.block_indices[1], [[0, 8], [8,10]]) - np.testing.assert_array_equal(G.block_indices[2], [[0, 4], [4,8], [8,10]]) - - - def test_prrewiring_only_rewiring2(self): - """ - From the graph - 0 -> 2 - 1 -> 3 - to the graph - 0 -> 2 - 1 -> 3 - using initial colors to make node 2 and 3 different - which is only valid with the source only strategy - """ - G = FastGraph(np.array([(0,2), (1,3), ], dtype=np.uint32), is_directed=True, num_nodes=4) - - G.ensure_edges_prepared(initial_colors=np.array([0,0,1,2], np.uint32), sorting_strategy="source") - G.rewire(0, method=1, seed=5, r=1) - np.testing.assert_array_equal(G.edges, [[0, 3], [1, 2]]) - - def test_prrewiring_only_rewiring2_parallel(self): - G = FastGraph(np.array([(0,2), (1,3), ], dtype=np.uint32), is_directed=True, num_nodes=4) - - G.ensure_edges_prepared(initial_colors=np.array([0,0,1,2], np.uint32), sorting_strategy="source") - G.rewire(0, method=1, seed=5, r=1, parallel=True) - np.testing.assert_array_equal(G.edges, [[0, 3], [1, 2]]) - -from nestmodel.tests.utils_for_test import restore_numba, remove_numba - -class TestFastGraphNonCompiled(TestFastGraph): - def setUp(self): - import nestmodel - _, self.cleanup = remove_numba(nestmodel, allowed_packages=["nestmodel"]) - - def tearDown(self) -> None: - import nestmodel - restore_numba(nestmodel, self.cleanup) - - - - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/nestmodel/tests/test_fast_rewire.py b/nestmodel/tests/test_fast_rewire.py deleted file mode 100644 index 8c98945..0000000 --- a/nestmodel/tests/test_fast_rewire.py +++ /dev/null @@ -1,62 +0,0 @@ -# pylint: disable=missing-function-docstring, missing-class-docstring, wrong-import-position -import faulthandler -faulthandler.enable() -import unittest -import numpy as np - -#from nestmodel.load_datasets import * -from nestmodel.fast_rewire import sort_edges - -from numpy.testing import assert_array_equal - -def safe_diff(arr1, arr2): - return np.maximum(arr1,arr2) - np.minimum(arr1,arr2) - - - - -class TestSortingMethods(unittest.TestCase): - def test_sort_edges1(self): - edges = np.array((np.zeros(8), np.arange(1,9, dtype=np.uint32)), dtype=np.uint32).T - - labels = np.array([np.array([0,2,2,1,1,1,1,2,2]), np.array([0,4,4,3,3,2,2,1,1])]) - edges_ordered, edges_classes_arr, dead_indicator, is_mono, strategy = sort_edges(edges, labels, is_directed=True, sorting_strategy="both") - - assert_array_equal(edges_ordered[:,0], np.zeros(8, dtype=np.uint32)) - assert_array_equal(safe_diff(edges_ordered[:-1:2,1], edges_ordered[1::2,1]), [1,1,1,1]) - self.check_class_sizes(edges_classes_arr[:,0], [4,4]) - self.check_class_sizes( edges_classes_arr[:,1], [2,2,2,2]) - - - edges_ordered, edges_classes_arr, dead_indicator, is_mono, strategy = sort_edges(edges, labels, is_directed=True, sorting_strategy="source") - - assert_array_equal(edges_ordered[:,0], np.zeros(8, dtype=np.uint32)) - # compute difference of subsequent classes, it should be 1 - assert_array_equal(safe_diff(edges_ordered[:-1:2,1], edges_ordered[1::2,1]), [1,1,1,1]) - self.check_class_sizes(edges_classes_arr[:,0], [8]) - self.check_class_sizes( edges_classes_arr[:,1], [8]) - - - edges = np.array((np.arange(1,9, dtype=np.uint32), np.zeros(8)), dtype=np.uint32).T - edges_ordered, edges_classes_arr, dead_indicator, is_mono, strategy = sort_edges(edges, labels, is_directed=True, sorting_strategy="source") - - assert_array_equal(edges_ordered[:,1], np.zeros(8, dtype=np.uint32)) - assert_array_equal(safe_diff(edges_ordered[:-1:2,0], edges_ordered[1::2,0]), [1,1,1,1]) - - self.check_class_sizes(edges_classes_arr[:,0], [4,4]) - self.check_class_sizes( edges_classes_arr[:,1], [2,2,2,2]) - - - - def check_class_sizes(self, arr, sizes): - class_sizes = np.unique(arr) - self.assertEqual(len(sizes), len(class_sizes), "The number of classes mismatch") - n = 0 - for size in sizes: - assert_array_equal(np.diff(arr[n:n+size]), np.zeros(size-1)) - n+=size - - - -if __name__ == '__main__': - unittest.main() diff --git a/nestmodel/tests/test_pagerank.py b/nestmodel/tests/test_pagerank.py deleted file mode 100644 index c788ef2..0000000 --- a/nestmodel/tests/test_pagerank.py +++ /dev/null @@ -1,81 +0,0 @@ -# pylint: disable=missing-function-docstring, missing-class-docstring -import unittest -import numpy as np -from nestmodel.fast_graph import FastGraph -from nestmodel.centralities import calc_pagerank - - -karate_pagerank = [0.09699729, 0.05287692, 0.05707851, 0.03585986, 0.02197795, 0.02911115 -, 0.02911115, 0.0244905, 0.02976606, 0.0143094, 0.02197795, 0.00956475 -, 0.01464489, 0.02953646, 0.01453599, 0.01453599, 0.01678401, 0.01455868 -, 0.01453599, 0.01960464, 0.01453599, 0.01455868, 0.01453599, 0.03152251 -, 0.02107603, 0.0210062, 0.01504404, 0.02563977, 0.01957346, 0.02628854 -, 0.02459016, 0.03715809, 0.07169323, 0.10091918] - - -karate_edges = np.array([[0, 1], [0, 2], [0, 3], [0, 4], [0, 5], [0, 6], [0, 7], [0, 8], [0, 10], [0, 11], [0, 12], [0, 13], [0, 17], [0, 19], [0, 21], [0, 31], [1, 2], [1, 3], [1, 7], [1, 13], [1, 17], [1, 19], [1, 21], [1, 30], [2, 3], [2, 7], [2, 8], [2, 9], [2, 13], [2, 27], [2, 28], [2, 32], [3, 7], [3, 12], [3, 13], [4, 6], [4, 10], [5, 6], [5, 10], [5, 16], [6, 16], [8, 30], [8, 32], [8, 33], [9, 33], [13, 33], [14, 32], [14, 33], [15, 32], [15, 33], [18, 32], [18, 33], [19, 33], [20, 32], [20, 33], [22, 32], [22, 33], [23, 25], [23, 27], [23, 29], [23, 32], [23, 33], [24, 25], [24, 27], [24, 31], [25, 31], [26, 29], [26, 33], [27, 33], [28, 31], [28, 33], [29, 32], [29, 33], [30, 32], [30, 33], [31, 32], [31, 33], [32, 33]], dtype=np.uint32)# pylint: disable=line-too-long - - - -class TestFastWLMethods(unittest.TestCase): - - def test_pagerank_karate(self): - G = FastGraph(karate_edges, False) - p = calc_pagerank(G) - np.testing.assert_almost_equal(p, karate_pagerank) - - - def test_pagerank_directed_edge(self): - G = FastGraph(np.array([[0,1]], dtype=np.uint32), True) - p = calc_pagerank(G) # formerly "out" - alpha=0.85 - val1 = 1/(2+alpha) - val2 = (1+alpha)/(2+alpha) - np.testing.assert_almost_equal(p, [val1, val2]) - - p = calc_pagerank(FastGraph.switch_directions(G)) - np.testing.assert_almost_equal(p, [val2, val1]) - - - def test_pagerank_undirected_edge(self): - G = FastGraph(np.array([[0,1]], dtype=np.uint32), False) - p = calc_pagerank(G) - np.testing.assert_almost_equal(p, [0.5, 0.5]) - - p = calc_pagerank(FastGraph.switch_directions(G)) - np.testing.assert_almost_equal(p, [0.5, 0.5]) - - - def test_pagerank_undirected_line(self): - G = FastGraph(np.array([[0,1], [1,2]], dtype=np.uint32), False) - p = calc_pagerank(G) - np.testing.assert_almost_equal(p, [0.2567568, 0.4864865, 0.2567568]) - - p = calc_pagerank(FastGraph.switch_directions(G)) - np.testing.assert_almost_equal(p, [0.2567568, 0.4864865, 0.2567568]) - - - def test_pagerank_undirected_line2(self): - G = FastGraph(np.array([[0,1], [3,4]], dtype=np.uint32), False) - p = calc_pagerank(G) - np.testing.assert_almost_equal(p, [0.24096383, 0.24096383, 0.03614469, 0.24096383, 0.24096383]) - - - - - def test_networkx_pagerank(self): - def dict_to_arr(d): - arr = np.empty(len(d)) - for key, val in d.items(): - arr[key]=val - return arr - import networkx as nx # pylint: disable=import-outside-toplevel - G = nx.Graph() - G.add_nodes_from(range(34)) - G.add_edges_from(karate_edges) - p = dict_to_arr(nx.pagerank(G, tol=1e-14, max_iter=100)) - np.testing.assert_almost_equal(p, karate_pagerank) - - -if __name__ == '__main__': - unittest.main() diff --git a/nestmodel/tests/test_utils.py b/nestmodel/tests/test_utils.py deleted file mode 100644 index d0bf173..0000000 --- a/nestmodel/tests/test_utils.py +++ /dev/null @@ -1,33 +0,0 @@ -# pylint: disable=missing-function-docstring, missing-class-docstring -import unittest -import numpy as np -from nestmodel.fast_graph import FastGraph -from nestmodel.utils import calc_jaccard - -class TestFastWLMethods(unittest.TestCase): - - def test_unique_edges_dir(self): - G1 = FastGraph(np.array([[1,9],[4,3]], dtype=np.uint32), True) - G2 = FastGraph(np.array([[1,9],[6,2]], dtype=np.uint32), True) - G3 = FastGraph(np.array([[1,10],[5,2]], dtype=np.uint32), True) - G4 = FastGraph(np.array([[9,1],[3,4]], dtype=np.uint32), True) - - self.assertEqual(calc_jaccard(G1, G2), 1/3) - self.assertEqual(calc_jaccard(G1, G1), 1.0) - self.assertEqual(calc_jaccard(G1, G3), 0.0) - self.assertEqual(calc_jaccard(G1, G4), 0.0) - - def test_unique_edges_undir(self): - G1 = FastGraph(np.array([[1,9],[4,3]], dtype=np.uint32), False) - G4 = FastGraph(np.array([[9,1],[3,4]], dtype=np.uint32), False) - G2 = FastGraph(np.array([[1,9],[6,2]], dtype=np.uint32), False) - G3 = FastGraph(np.array([[1,10],[5,2]], dtype=np.uint32), False) - - self.assertEqual(calc_jaccard(G1, G2), 1/3) - self.assertEqual(calc_jaccard(G1, G1), 1.0) - self.assertEqual(calc_jaccard(G1, G3), 0.0) - self.assertEqual(calc_jaccard(G1, G4), 1.0) - - -if __name__ == '__main__': - unittest.main() diff --git a/nestmodel/wl_nlogn.py b/nestmodel/wl_nlogn.py deleted file mode 100644 index 2dd3f90..0000000 --- a/nestmodel/wl_nlogn.py +++ /dev/null @@ -1,601 +0,0 @@ -# pylint: disable=invalid-name, consider-using-enumerate, missing-function-docstring - -from numba import njit -from numba.types import uint32 -import numpy as np - -# from numba import objmode -# import time -@njit([(uint32[:], uint32[:])], cache=True) -def get_in_degree(end_neighbors, neighbors): - """Compute the in degree""" - #n_nodes = len(end_neighbors)-1 - in_degree = np.zeros(len(end_neighbors)-1, dtype=np.uint32) - - for i in range(len(neighbors)): - in_degree[neighbors[i]]+=1 - - return in_degree - - -@njit(cache=True) -def get_degree_partition(in_degree, max_degree): - n_nodes = len(in_degree) - num_per_degree = np.zeros(max_degree+2, dtype=np.uint32) - for i in range(len(in_degree)): - num_per_degree[in_degree[i]]+=1 - - - - start_nodes_by_class = np.empty(n_nodes, dtype=np.int32) - end_nodes_by_class = np.empty(n_nodes, dtype=np.int32) - prev_end = 0 - num_classes = 0 - for i in range(0, len(num_per_degree)): - num_nodes_this_class = num_per_degree[i] - if num_nodes_this_class == 0: - continue - num_per_degree[i] = prev_end - - start_nodes_by_class[num_classes] = prev_end - end_nodes_by_class[num_classes] = prev_end + num_nodes_this_class - prev_end += num_nodes_this_class - num_classes+=1 - - position_of_node = np.empty(n_nodes, dtype=np.uint32) - nodes_by_class = np.empty(n_nodes, dtype=np.uint32) - for i in range(n_nodes): - nodes_by_class[num_per_degree[in_degree[i]]]=i - position_of_node[i] = num_per_degree[in_degree[i]] - num_per_degree[in_degree[i]]+=1 - - - classes=np.empty(n_nodes, dtype=np.uint32) - for i in range(num_classes): - for j in range(start_nodes_by_class[i], end_nodes_by_class[i]): - classes[nodes_by_class[j]]=i - - - class_costs = np.zeros(num_classes, dtype=np.uint32) - for i in range(num_classes): - num_nodes = end_nodes_by_class[i] - start_nodes_by_class[i] - num_edges = in_degree[start_nodes_by_class[i]] - class_costs[i] = num_edges * num_nodes - - queue = np.empty((n_nodes,2), dtype=np.int32) - class_order = np.argsort(class_costs)[::-1] - for i in range(num_classes): - c = class_order[i] - queue[i,0] = start_nodes_by_class[c] - queue[i,1] = end_nodes_by_class[c] - - return num_classes, start_nodes_by_class, end_nodes_by_class, nodes_by_class, classes, position_of_node, queue - - - -@njit([(uint32[:], uint32[:], uint32[:])], cache=True) -def color_refinement_nlogn(end_neighbors, neighbors, initial_labels): - """Compute the coarsest WL refinement""" - # print("neighbprs") - # print(end_neighbors) - # print(neighbors) - n_nodes = len(end_neighbors)-1 - max_degree = n_nodes - starts_from_degree = np.all(initial_labels==initial_labels[0]) - if starts_from_degree: - # with objmode(t1='double'): # annotate return type - # t1 = time.process_time() - in_degree = get_in_degree(end_neighbors, neighbors) - num_classes, start_nodes_by_class, end_nodes_by_class, nodes_by_class, classes, position_of_node, queue = get_degree_partition(in_degree, in_degree.max()) - # with objmode(t2='double'): # annotate return type - # t2 = time.process_time() - # print(t2-t1) - depth=1 - else: - # print("initial", initial_labels) - num_classes, start_nodes_by_class, end_nodes_by_class, nodes_by_class, classes, position_of_node, queue = get_degree_partition(initial_labels, initial_labels.max()) - depth = 0 - # num_classes=1 - # start_nodes_by_class = np.empty(n_nodes, dtype=np.uint32) - # start_nodes_by_class[0] = 0 - # end_nodes_by_class = np.empty(n_nodes, dtype=np.uint32) - # end_nodes_by_class[0] = n_nodes - # position_of_node = np.arange(n_nodes, dtype=np.uint32) - # nodes_by_class = np.arange(n_nodes, dtype=np.uint32) - # queue = np.empty(n_nodes, dtype=np.uint32) - # queue[0]=0 - #original = np.arange(len(neighbors)) - #where = np.arange(len(neighbors)) - out_classes = np.empty((n_nodes,3), dtype=np.uint32) - for i in range(num_classes): - out_classes[i,0]=start_nodes_by_class[i] - out_classes[i,1]=end_nodes_by_class[i] - out_classes[i,2]=depth - - start_neighbors = end_neighbors[:-1].copy() - end_neighbors = end_neighbors[1:] - - - # per node characteristics - #classes = np.zeros(n_nodes, dtype=np.uint32) - receive_counts = np.zeros(n_nodes, dtype=np.uint32) - node_is_active = np.zeros(n_nodes, dtype=np.bool_) - #position_of_node = np.arange(n_nodes, dtype=np.uint32) - - # nodes per class - #start_nodes_by_class = np.empty(n_nodes, dtype=np.uint32) - #start_nodes_by_class[0] = 0 # first class contains all nodes - #end_nodes_by_class = np.empty(n_nodes, dtype=np.uint32) - #end_nodes_by_class[0] = n_nodes # first class contains all nodes - #nodes_by_class = np.arange(n_nodes) - received_nodes_by_class = np.empty(n_nodes, dtype=np.uint32) - - # queue - #queue = np.empty(n_nodes, dtype=np.uint32) - #queue[0:num_classes]=np.arange(num_classes) - queue_R = num_classes - queue_L = 0 - class_in_queue = np.zeros(n_nodes, dtype=np.bool_) - class_in_queue[:num_classes]=True - classes_processed = 0 - - - - - - #num_classes = 1 - active_nodes_in_class = np.zeros(n_nodes, dtype=np.uint32) # counts the number of nodes that are affected by message passing of current class - active_classes = np.empty(n_nodes, dtype=np.uint32)# max_size: if all nodes have unique degree active_classes = n_nodes-1 - num_active_classes = 0 - - # per group statistics - group_ids = np.zeros(max_degree+1, dtype=np.uint32)# max_size is upper bounded by max_degree+1 as no node can have count > max_degree - num_nodes_per_group_scattered = np.zeros(max_degree+1, dtype=np.uint32) # max_size: see above - num_nodes_per_group = np.zeros(max_degree+1, dtype=np.uint32) # max_size: see above - #nodes_in_group = np.zeros(n_nodes, dtype=np.uint32) # max_size=n_nodes because it is full in first iteration, afterwards could be max_i(num_nodes_with_degree=i * i) - nodes_indices_by_group = np.zeros(max_degree+1, dtype=np.uint32) - class_name_for_group = np.zeros(max_degree+1, dtype=np.uint32) - - # # performance metrics - # num_messages = 0 - # num_groups_1 = 0 - # num_groups_2 = 0 - # num_groups_3 = 0 - # num_groups_x = 0 - # min_group_id_0 =0 - # min_group_id_1 =0 - # min_group_id_x =0 - # max_group_id_1 =0 - # max_group_id_2 =0 - # max_group_id_x =0 - # min_max_group_id_0_1 = 0 - # largest_group_id_0 = 0 - # special_case = 0 - # normal_case = 0 - - # neighborhood_10 = 0 - # neighborhood_x = 0 - # all_unique = 0 - # num_active_1=0 - # num_active_2=0 - while queue_L < queue_R and num_classes < n_nodes: - # print("queue", queue, queue_L, queue_R) - # print(start_nodes_by_class) - # print(end_nodes_by_class) - # print(nodes_by_class) - depth+=1 - last_queue_R = queue_R - while queue_L < last_queue_R and num_classes < n_nodes: - # print("queue", queue, queue_L, queue_R, last_queue_R) - #print(queue_len) - start_class = queue[queue_L,0] - end_class = queue[queue_L,1] - #assert send_class < num_classes - #class_in_queue[send_class]=False - queue_L+=1 - - # Performance tracking - classes_processed +=1 - # if classes_processed == 2: - # print("initial_messages", num_messages) - - #print("class", classes) - #print("sending class", send_class) - #print("start_nodes_by_class", start_nodes_by_class[send_class]) - #print("end_nodes_by_class", end_nodes_by_class[send_class]) - #print("nodes_by_class", nodes_by_class[start_nodes_by_class[send_class]: end_nodes_by_class[send_class]]) - #print() - - #print("receive_counts==0", receive_counts) - #print("classes", classes) - num_active_classes = 0 - #if (end_nodes_by_class[send_class]-start_nodes_by_class[send_class]) == 1: - # single_node_propagation(send_class, nodes_by_class, start_nodes_by_class, end_neighbors, neighbors, - # receive_counts, classes, active_classes, position_of_node, class_in_queue, queue, num_classes, - # queue_R, received_nodes_by_class) - # continue - #print("max", active_nodes_in_class[:num_classes].max()) - if (start_class-end_class) == 1: - # print("special") - # special_case +=1 - # special case if the class has only one(!) node - sending_node = nodes_by_class[start_class] - #print("lonely_node", sending_node) - num_active_classes = 0 - for j in range(start_neighbors[sending_node], end_neighbors[sending_node]): - neigh = neighbors[j] - #num_messages+=1 - if not node_is_active[neigh]: - neigh_class = classes[neigh] - # mark node as needing processing - received_nodes_by_class[start_nodes_by_class[neigh_class] + active_nodes_in_class[neigh_class]] = neigh - active_nodes_in_class[neigh_class] +=1 - - if active_nodes_in_class[neigh_class] == 1: - # mark class as active - active_classes[num_active_classes] = neigh_class - num_active_classes+=1 - #if num_active_classes == 1: - # num_active_1 +=1 - #elif num_active_classes == 2: - # num_active_2 +=1 - for active_class_index in range(num_active_classes): - active_class = active_classes[active_class_index] - total_nodes_this_class = end_nodes_by_class[active_class] - start_nodes_by_class[active_class] - - if total_nodes_this_class==1: - # classes with only 1 node never need to be a receiving class again - # thus set the node to be active - i_node = start_nodes_by_class[active_class] - node = received_nodes_by_class[i_node] - node_is_active[node] = True - continue - - if total_nodes_this_class == active_nodes_in_class[active_class]: - active_nodes_in_class[active_class] = 0 - continue - else: - # prepare the two new classes - L = start_nodes_by_class[active_class] - R = start_nodes_by_class[active_class] + active_nodes_in_class[active_class] - active_nodes_in_class[active_class] = 0 - new_class = num_classes - num_classes+=1 - start_nodes_by_class[new_class] = L - end_nodes_by_class[new_class] = R - - out_classes[new_class,0] = L - out_classes[new_class,1] = R - out_classes[new_class,2] = depth - start_nodes_by_class[active_class] = R - #print("start", start_nodes_by_class[:num_classes]) - #print("end", end_nodes_by_class[:num_classes]) - index = L - for i_node in range(L, R): - node = received_nodes_by_class[i_node] - classes[node] = new_class - # swap node positions - start_pos_node = position_of_node[node] - swap_node = nodes_by_class[index] - nodes_by_class[index] = node - position_of_node[node] = index - nodes_by_class[start_pos_node] = swap_node - position_of_node[swap_node] = start_pos_node - #nodes_indices_by_group[group_id]+=1 - index+=1 - - #print("in queue A", active_class, class_in_queue[active_class]) - # if class_in_queue[active_class]: - # put_in = new_class - # else: - if active_nodes_in_class[active_class] > total_nodes_this_class//2: - put_in=new_class - else: - put_in=active_class - - queue[queue_R,0] = start_nodes_by_class[put_in] - queue[queue_R,1] = end_nodes_by_class[put_in] - #class_in_queue[put_in] = True - queue_R+=1 - #print(classes) - #classes_are_mono(start_nodes_by_class, end_nodes_by_class, num_classes, nodes_by_class, classes) - continue - - # normal_case +=1 - - - for i in range(start_class, end_class): - sending_node = nodes_by_class[i] - for j in range(start_neighbors[sending_node], end_neighbors[sending_node]): - neigh = neighbors[j] - # print("sending", sending_node, neigh) - # num_messages+=1 - #for sending_node in nodes_by_class[start_nodes_by_class[send_class]: end_nodes_by_class[send_class]]: - #for neigh in neighbors[end_neighbors[sending_node]:end_neighbors[sending_node+1]]: - receive_counts[neigh] += 1 - neigh_class = classes[neigh] - if not node_is_active[neigh]: - # mark node as needing processing - received_nodes_by_class[start_nodes_by_class[neigh_class] + active_nodes_in_class[neigh_class]] = neigh - active_nodes_in_class[neigh_class] +=1 - node_is_active[neigh] = True - - if active_nodes_in_class[neigh_class] == 1: # the current neigh node makes it's class active - # enque class into queue of active classes - active_classes[num_active_classes] = neigh_class - num_active_classes+=1 - #for class_ in active_classes[:num_active_classes]: - # active_nodes_in_class[class_] = 0 - #print() - #print() - #print("active classes") - #print(num_active_classes) - #print("active_classes", active_classes[:num_active_classes]) - #print("count", receive_counts) - #print("-------------------------------------------------------") - #print("ordered", classes[nodes_by_class]) - # print("receive", receive_counts) - for active_class_index in range(num_active_classes): # loop over all classes which were potentially split by this action - #classes_are_mono(start_nodes_by_class, end_nodes_by_class, num_classes, nodes_by_class, classes) - active_class = active_classes[active_class_index] - #print("class ranges", list( zip(start_nodes_by_class[:num_classes], end_nodes_by_class[:num_classes]))) - #s=[] - #for start, end in zip(start_nodes_by_class[:num_classes], end_nodes_by_class[:num_classes]): - # s.append("".join(str(classes[node]) for node in nodes_by_class[start: end])) - - - #print(" | ".join(s)) - #print(active_classes[:num_active_classes]) - - #print("active class", active_class) - #print("active range", start_nodes_by_class[active_class], end_nodes_by_class[active_class]) - num_active_nodes_this_class = active_nodes_in_class[active_class] - - - # resetting node information - for i_node in range(start_nodes_by_class[active_class], start_nodes_by_class[active_class] + num_active_nodes_this_class): - node = received_nodes_by_class[i_node] - node_is_active[node]=False - #print("activ", num_active_nodes_this_class) - active_nodes_in_class[active_class] = 0 - - total_nodes_this_class = end_nodes_by_class[active_class] - start_nodes_by_class[active_class] - #print("total", total_nodes_this_class) - #print() - #assert total_nodes_this_class >= num_active_nodes_this_class - if total_nodes_this_class==1: - # classes with only 1 node never need to be an receiving class again - i_node = start_nodes_by_class[active_class] - node = received_nodes_by_class[i_node] - node_is_active[node] = True # this ensures this node never lands in the active queue again - continue - - - - - #print(total_nodes_this_class, non_active_group_size) - num_groups = 0 - # find the groups and the number of nodes per group - for i_node in range(start_nodes_by_class[active_class], start_nodes_by_class[active_class] + num_active_nodes_this_class): - node = received_nodes_by_class[i_node] - group_id = receive_counts[node] - num_nodes_per_group_scattered[group_id]+= 1 # identify group sizes - if num_nodes_per_group_scattered[group_id] == 1: #add this group to queue of groups - group_ids[num_groups] = group_id - num_groups += 1 - #print("num_g", num_groups) - - # there might be nodes which are not adjacent to the currently sending class - # we are treating these incoming degree zero nodes here - non_active_group_size = total_nodes_this_class - num_active_nodes_this_class - if non_active_group_size > 0: - group_ids[num_groups] = 0 - num_nodes_per_group_scattered[0] = non_active_group_size - num_groups+=1 - #print("groups",group_ids[:num_groups]) - #print("ngrou", num_groups) - - # if num_groups==1: - # num_groups_1+=1 - # elif num_groups==2: - # num_groups_2+=1 - # elif num_groups==3: - # num_groups_3+=1 - # else: - # num_groups_x+=1 - - - if num_groups==1: # nothing to be done, active class is not split - # reset the counts - for node in nodes_by_class[start_nodes_by_class[active_class]: end_nodes_by_class[active_class]]: - receive_counts[node]=0 - for i in range(num_groups): - num_nodes_per_group_scattered[group_ids[i]] = 0 - continue - - # min_group_id = group_ids[:num_groups].min() - # if min_group_id == 0: - # min_group_id_0 +=1 - # elif min_group_id == 1: - # min_group_id_1 +=1 - # else: - # min_group_id_x +=1 - - # max_group_id = group_ids[:num_groups].max() - # if max_group_id == 1: - # max_group_id_1 +=1 - # elif max_group_id == 2: - # max_group_id_2 +=1 - # else: - # max_group_id_x+=1 - # if min_group_id == 0 and max_group_id == 1: - # min_max_group_id_0_1 +=1 - - - # collect num_nodes_per_group from scattered - for i in range(num_groups): - num_nodes_per_group[i] = num_nodes_per_group_scattered[group_ids[i]] - num_nodes_per_group_scattered[group_ids[i]] = 0 - #print(num_nodes_per_group) - - # in the following we determine two special classes: 1) the not_relabeled_group and 2) the not_enqueued aka the largest_group - # the not relabeled group is usually the degree zero node group, but in case there is no degree zero group, take the largest - # the largest group is simply one of the largest groups (doesn't matter which) - # collect largest group statistics - _largest_group_index = np.argmax(num_nodes_per_group[:num_groups]) - largest_group_size = num_nodes_per_group[_largest_group_index] - if largest_group_size == non_active_group_size: # in case that the largest and the zero partition are of identical size, just take 0 - largest_group_id = 0 - else: - largest_group_id = group_ids[_largest_group_index] - if non_active_group_size == 0: - not_relabeled_group_id = largest_group_id - else: - not_relabeled_group_id = 0 - # if largest_group_id == 0: - # largest_group_id_0 +=1 - - # ----- begin collecting nodes into groups ----- - # cumsum num_nodes_per_group - for i in range(1, num_groups): - num_nodes_per_group[i] = num_nodes_per_group[i-1] + num_nodes_per_group[i] - #print("largest", largest_group_id) - #print("start", start_nodes_by_class[:num_classes+1]) - #print("end ", end_nodes_by_class[:num_classes+1]) - #print("per_g", num_nodes_per_group[:num_groups]) - # scatter node indices to group locations - offset = start_nodes_by_class[active_class] - end_offset = start_nodes_by_class[active_class] + num_active_nodes_this_class - assert offset < end_offset - #print("groups", group_ids[:num_groups]) - for i in range(num_groups): - group_id = group_ids[i] - if i == 0: - nodes_indices_by_group[group_id] = 0 # nodes_indices_by_group will contain the position of this group in the order - else: - nodes_indices_by_group[group_id] = num_nodes_per_group[i-1] - - if group_id == not_relabeled_group_id: # there are some nodes that are not relabeled, they keep the active class - class_name_for_group[group_id] = active_class - else: - class_name_for_group[group_id] = num_classes # this group will become a new class - num_classes+=1 - #print("in queue B", active_class, class_in_queue[active_class]) - put_in = False - # if class_in_queue[active_class]: - # if group_id != not_relabeled_group_id: - # put_in = True - # else: - if group_id == largest_group_id: - put_in=False - else: - put_in=True - group_class = class_name_for_group[group_id] - - - if i == 0: - start_nodes_by_class[group_class] = offset - else: - start_nodes_by_class[group_class] = offset + num_nodes_per_group[i-1] - end_nodes_by_class[group_class] = offset + num_nodes_per_group[i] - - if put_in: - queue[queue_R,0] = start_nodes_by_class[group_class] - queue[queue_R,1] = end_nodes_by_class[group_class] - # class_in_queue[group_class] = True - queue_R+=1 - - if group_class != active_class: # active class is already present in out_classes - out_classes[group_class,0] = start_nodes_by_class[group_class] - out_classes[group_class,1] = end_nodes_by_class[group_class] - out_classes[group_class,2] = depth - - - #print("changing", new_group_ids[group_id]) - #assert end_nodes_by_class[class_name_for_group[group_id]]>start_nodes_by_class[class_name_for_group[group_id]] - - #print(nodes_indices_by_group[group_ids[:num_groups]]) - #print(class_name_for_group[group_ids[:num_groups]]) - #print("start", start_nodes_by_class[:num_classes+1]) - #print("end ", end_nodes_by_class[:num_classes+1]) - #print(nodes_by_class) - for i_node in range(offset, end_offset): - node = received_nodes_by_class[i_node] - #print("processing", node, classes[node], "->", class_name_for_group[group_id]) - group_id = receive_counts[node] - receive_counts[node] = 0 # reset this value, to be used again in the future - #print(group_id) - classes[node] = class_name_for_group[group_id] - group_class = class_name_for_group[group_id] - - #if (end_nodes_by_class[group_class]-start_nodes_by_class[group_class]) == 1: - #if True: - #print(neighbors) - #print(start_neighbors) - #print(end_neighbors) - # for i_remove in range(in_degree[node], in_degree[node+1]): - # to_remove_pos = in_neighbors_position[i_remove] - # affected_node = in_neighbors[i_remove] - - # def remove_from_out(to_delete, values, original, where, end_neighbors, node) - #print("affected", affected_node, to_remove_pos) - # remove_from_out(to_remove_pos, neighbors, original, where, end_neighbors, affected_node) - - # node_is_active[node] = True - #print(neighbors, end_neighbors) - - if group_id == not_relabeled_group_id: - continue - - # swap node positions - start_pos_node = position_of_node[node] - target_index = nodes_indices_by_group[group_id]+offset # target location of node - swap_node = nodes_by_class[target_index] # the node currently at the swap position - nodes_by_class[target_index] = node # place current node - position_of_node[node] = target_index # set position of current node - nodes_by_class[start_pos_node] = swap_node # place other node - position_of_node[swap_node] = start_pos_node # set position of other node - nodes_indices_by_group[group_id]+=1 # increase counter - #classes_are_mono(start_nodes_by_class, end_nodes_by_class, num_classes, nodes_by_class, classes) - #print("swap", node, swap_node, start_pos_node, index) - #print(nodes_by_class) - #print(nodes_by_class[position_of_node]) - - if starts_from_degree: - out_classes[0,0] = 0 - out_classes[0,1] = n_nodes - out_classes[0,2] = 0 - - # print("num_groups1", num_groups_1) - # print("num_groups2", num_groups_2) - # print("num_groups3", num_groups_3) - # print("num_groupsx", num_groups_x) - - # print("min_group_id_0", min_group_id_0) - # print("min_group_id_1", min_group_id_1) - # print("min_group_id_x", min_group_id_x) - # print("max_group_id_1", max_group_id_1) - # print("max_group_id_2", max_group_id_2) - # print("max_group_id_x", max_group_id_x) - # print("minmax_group_id_0_1", min_max_group_id_0_1) - # print("largest_group_id_0", largest_group_id_0) - # print("special_case", special_case) - # print("normal_case ", normal_case) - # print("neigh_10", neighborhood_10) - # print("neigh_X ", neighborhood_x) - # print("all_unqiue", all_unique) - # print("num_active_1", num_active_1) - # print("num_active_2", num_active_2) - # print("total_ messages", num_messages) - # print("classes_processed", classes_processed) - # start_nodes_by_class[:num_classes].sort() - # end_nodes_by_class[:num_classes].sort() - # print(start_nodes_by_class[:10]) - # print(end_nodes_by_class[:10]) - # print(out_classes[:20]) - - #print() - #print("result") - #print(classes) - #print(nodes_by_class) - return position_of_node, out_classes[0:num_classes,:] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f381bb2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,236 @@ +[build-system] +# AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD! +requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=5"] +build-backend = "setuptools.build_meta" + + +[tool.setuptools_scm] +# For smarter version schemes and other configuration options, +# check out https://github.com/pypa/setuptools_scm +version_scheme = "no-guess-dev" + + +[project] +name = "nestmodel" +dynamic = ["version"] +authors = [{ name = "Felix I. Stamm", email = "felix.stamm@rwth-aachen.de" }] +description = "A package to randomize graphs using the configuration or nest-model." +readme = "README.md" +requires-python = ">=3.9" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", +] +keywords = [ + "graph", + "randomization", + "network", + "configuration-model", + "fixed-degree-sequence-model", +] +license = { file = "LICENSE" } +dependencies = ['numpy >= 1.23.5', 'numba >= 0.56.4', 'scipy', 'networkx'] + +[project.optional-dependencies] +test = ['pytest', 'pytest-cov', 'coverage[toml]'] +execute = ['nbconvert', 'tqdm', 'threadpoolctl'] +ui = ['matplotlib', 'brokenaxes', 'tqdm'] + +[project.urls] +"Homepage" = "https://github.com/Feelx234/nestmodel" +"Bug Tracker" = "https://github.com/Feelx234/nestmodel/issues" + +[tool.hatch.build] +exclude = ["scripts", "demo", "old_notebooks"] + +[tool.pytest.ini_options] +addopts = "-ra --cov --cov-report html --cov-report term-missing" +testpaths = ["./tests"] +pythonpath = ["src"] + +[tool.coverage.run] +branch = true +source = ["src/nestmodel"] + + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "pragma: no branch okay", + # Don't complain about missing debug-only code: + "def __repr__", + "if self\\.debug", + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + # Don't complain if non-runnable code isn't run: + "if 0:", + "if __name__ == .__main__.:", + "pass", +] +omit = [ # Regexes for lines to exclude from consideration + "*/ergm.py", + "*/ERGM_experiments.py", + "*/dict_graph.py", + "*/io.py", + "*/long_refinement_graphs.py", + "*/visualization.py", + "*wl.py", +] + + +[tool.flake8] +# Some sane defaults for the code style checker flake8 +max_line_length = 200 +extend_ignore = ["E203", "W503", "E741"] +# ^ Black-compatible +# E203 and W503 have edge cases handled by black +exclude = [".tox", "build", "dist", ".eggs"] + + +[tool.isort] +profile = "black" +known_first_party = ["nestmodel"] + + +[tool.pylint.basic] +# Good variable names which should always be accepted, separated by a comma. +good-names = [ + "R", + "D", + "T", + "D_row", + "S", + "A", + "F", + "H", + "E", + "E2", + "H_vals", + "M", + "SMALL_VAL", + "LARGE_VAL", + "Gnp_row_first", + "_Gnp_row_first", + "SBM", + "setUp", + "tearDown", + "P", + "G_str", + "G_fg", +] + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs = ["^[_a-zGW][_a-z0-9L]?$"] + +[tool.pylint.format] +max-line-length = 225 + +[tool.pylint."messages control"] +disable = [ + "missing-module-docstring", + "missing-class-docstring", + "missing-function-docstring", + "missing-final-newline", + "superfluous-parens", +] + +[tool.pylint.main] +ignore = [".coveragerc"] +ignore-paths = [".coveragerc|.coveragerc"] + + +[tool.tox] +legacy_tox_ini = """ +[tox] +minversion = 3.24 +envlist = default +isolated_build = True + + +[testenv] +description = Invoke pytest to run automated tests +setenv = + TOXINIDIR = {toxinidir} +passenv = + HOME + SETUPTOOLS_* +extras = + test +commands = + pytest {posargs} + + +# # To run `tox -e lint` you need to make sure you have a +# # `.pre-commit-config.yaml` file. See https://pre-commit.com +# [testenv:lint] +# description = Perform static analysis and style checks +# skip_install = True +# deps = pre-commit +# passenv = +# HOMEPATH +# PROGRAMDATA +# SETUPTOOLS_* +# commands = +# pre-commit run --all-files {posargs:--show-diff-on-failure} + + +[testenv:{build,clean}] +description = + build: Build the package in isolation according to PEP517, see https://github.com/pypa/build + clean: Remove old distribution files and temporary build artifacts (./build and ./dist) +# https://setuptools.pypa.io/en/stable/build_meta.html#how-to-use-it +skip_install = True +changedir = {toxinidir} +deps = + build: build[virtualenv] +passenv = + SETUPTOOLS_* +commands = + clean: python -c 'import shutil; [shutil.rmtree(p, True) for p in ("build", "dist", "docs/_build")]' + clean: python -c 'import pathlib, shutil; [shutil.rmtree(p, True) for p in pathlib.Path("src").glob("*.egg-info")]' + build: python -m build {posargs} +# By default, both `sdist` and `wheel` are built. If your sdist is too big or you don't want +# to make it available, consider running: `tox -e build -- --wheel` + + +[testenv:{docs,doctests,linkcheck}] +description = + docs: Invoke sphinx-build to build the docs + doctests: Invoke sphinx-build to run doctests + linkcheck: Check for broken links in the documentation +passenv = + SETUPTOOLS_* +setenv = + DOCSDIR = {toxinidir}/docs + BUILDDIR = {toxinidir}/docs/_build + docs: BUILD = html + doctests: BUILD = doctest + linkcheck: BUILD = linkcheck +deps = + -r {toxinidir}/docs/requirements.txt + # ^ requirements.txt shared with Read The Docs +commands = + sphinx-build --color -b {env:BUILD} -d "{env:BUILDDIR}/doctrees" "{env:DOCSDIR}" "{env:BUILDDIR}/{env:BUILD}" {posargs} + + +[testenv:publish] +description = + Publish the package you have been developing to a package index server. + By default, it uses testpypi. If you really want to publish your package + to be publicly accessible in PyPI, use the `-- --repository pypi` option. +skip_install = True +changedir = {toxinidir} +passenv = + # See: https://twine.readthedocs.io/en/latest/ + TWINE_USERNAME + TWINE_PASSWORD + TWINE_REPOSITORY + TWINE_REPOSITORY_URL +deps = twine +commands = + python -m twine check dist/* + python -m twine upload {posargs:--repository {env:TWINE_REPOSITORY:testpypi}} dist/* +""" diff --git a/clean_errors.py b/scripts/clean_errors.py similarity index 51% rename from clean_errors.py rename to scripts/clean_errors.py index 60173d4..a58d71f 100644 --- a/clean_errors.py +++ b/scripts/clean_errors.py @@ -3,10 +3,10 @@ from pathlib import Path # <<< encoding UTF8 !!!!! >>> -#print(sys.argv) +# print(sys.argv) -j=None -if len(sys.argv[1])<2: +j = None +if len(sys.argv[1]) < 2: print("No file provided") exit() @@ -16,32 +16,32 @@ destination = path -#print(len()) +# print(len()) cells = j["cells"] deleted = 0 cells = j["cells"] deleted = 0 -for i in range(len(cells)-1, -1, -1): +for i in range(len(cells) - 1, -1, -1): cell = cells[i] if cell["cell_type"] == "code": - if len(cell["outputs"])>0: + if len(cell["outputs"]) > 0: to_remove = [] for i, output in enumerate(cell["outputs"]): - if "name" in output and output["name"]=="stderr": + if "name" in output and output["name"] == "stderr": to_remove.append(i) to_remove = reversed(to_remove) for i in to_remove: cell["outputs"].pop(i) - deleted+=1 -#print(f"deleted {deleted} cells") -destination = path# path.with_name(path.stem + "_stripped.ipynb") -#print(f"deleted {deleted} cells") -#destination = path.with_name(path.stem + "_stripped.ipynb") -#destination = path.with_stem(path.stem + "_stripped") -#print(f"writing to {destination}") + deleted += 1 +# print(f"deleted {deleted} cells") +destination = path # path.with_name(path.stem + "_stripped.ipynb") +# print(f"deleted {deleted} cells") +# destination = path.with_name(path.stem + "_stripped.ipynb") +# destination = path.with_stem(path.stem + "_stripped") +# print(f"writing to {destination}") if deleted > 0: - with open(destination, 'w', encoding="utf-8") as f: + with open(destination, "w", encoding="utf-8") as f: json.dump(j, f, indent=1) f.write("\n") -print(json.dumps(j, indent=1)) \ No newline at end of file +print(json.dumps(j, indent=1)) diff --git a/scripts/convergence2_katz.ipynb b/scripts/convergence2_katz.ipynb index 51c5107..7910992 100644 --- a/scripts/convergence2_katz.ipynb +++ b/scripts/convergence2_katz.ipynb @@ -6,7 +6,11 @@ "metadata": {}, "outputs": [], "source": [ - "from threadpoolctl import threadpool_limits\n", + "try:\n", + " from threadpoolctl import threadpool_limits\n", + " threadpool = True\n", + "except ImportError:\n", + " threadpool = False\n", "from nestmodel.centralities import calc_katz_iter, calc_katz\n", "from convergence_helper import MultiTracker, ParameterWrapper, save_results\n", "from convergence_helper import get_datasets, get_samples" @@ -18,10 +22,10 @@ "metadata": {}, "outputs": [], "source": [ - "datasets = [\"karate\", \n", + "datasets = [\"karate\",\n", " \"phonecalls\",\n", - " \"HepPh\", \n", - " \"AstroPh\", \n", + " \"HepPh\",\n", + " \"AstroPh\",\n", "# \"web-Google\",\n", "# \"soc-Pokec\"\n", " ]" @@ -63,7 +67,7 @@ "tracker = MultiTracker((\"katz\",))\n", "params = ParameterWrapper(\n", "dataset_path = None,\n", - "cent_func = calc_katz_iter, \n", + "cent_func = calc_katz_iter,\n", "cent_kwargs = {\"epsilon\":1e-15,\n", " \"max_iter\":100},\n", "rewire_kwargs = dict(method=1, source_only=True),\n", @@ -74,9 +78,12 @@ "params.verbosity=1\n", "params.tqdm=1\n", "tracker.verbosity=1\n", - "with threadpool_limits(limits=1, user_api='blas'):\n", - " with threadpool_limits(limits=1, user_api='openmp'):\n", - " compute_on_all_datasets(get_datasets(datasets), tracker, params)" + "if threadpool:\n", + " with threadpool_limits(limits=1, user_api='blas'):\n", + " with threadpool_limits(limits=1, user_api='openmp'):\n", + " compute_on_all_datasets(get_datasets(datasets), tracker, params)\n", + "else:\n", + " compute_on_all_datasets(get_datasets(datasets), tracker, params)" ] }, { diff --git a/scripts/convergence2_runner.py b/scripts/convergence2_runner.py index 13b87fd..760d88b 100644 --- a/scripts/convergence2_runner.py +++ b/scripts/convergence2_runner.py @@ -2,48 +2,55 @@ from pathlib import Path import subprocess import sys - +import io +import selectors +import re this_file = Path(__file__) this_folder = this_file.parent main_folder = this_file.parent.parent print(main_folder) -if sys.platform == 'win32': - import subprocess +if sys.platform == "win32": + result = subprocess.run("where.exe python", capture_output=True, text=True) python_path = result.stdout.split("\n")[0] -import io -import selectors -import subprocess def capture_subprocess_output(subprocess_args): - if sys.platform == 'win32': + if sys.platform == "win32": subprocess_args = subprocess_args.replace("python", python_path) print(">>>", subprocess_args) buf = io.StringIO() - with subprocess.Popen(subprocess_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=this_folder) as process: + with subprocess.Popen( + subprocess_args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=this_folder, + ) as process: for c in iter(lambda: process.stdout.read(1), b""): sys.stdout.buffer.write(c) sys.stdout.flush() buf.write(c.decode("utf-8")) output = buf.getvalue() buf.close() - #for line in process.stdout: - # print(line.decode('utf8')) + # for line in process.stdout: + # print(line.decode('utf8')) return None, output # Start subprocess # bufsize = 1 means output is line buffered # universal_newlines = True is required for line buffering - process = subprocess.Popen(subprocess_args, - bufsize=2, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - shell=True) + process = subprocess.Popen( + subprocess_args, + bufsize=2, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=True, + ) # Create callback function for process output buf = io.StringIO() + def handle_output(stream, mask): # Because the process' output is line buffered, there's only ever one # line to read when this function is called @@ -68,7 +75,7 @@ def handle_output(stream, mask): return_code = process.wait() selector.close() - success = (return_code == 0) + success = return_code == 0 # Store buffered output output = buf.getvalue() @@ -77,58 +84,50 @@ def handle_output(stream, mask): return (success, output) - - - - def encode_str(s): - if sys.platform == 'win32': + if sys.platform == "win32": print(s) - return s.replace("'", r'\"').replace(" ", "") + return s.replace("'", r"\"").replace(" ", "") else: return s.replace(" ", "").replace("'", r"\'") - - -import re - -to_run=["katz", "pagerank", "jaccard", "eigenvector", "hits"] -datasets = ["karate", - "phonecalls", - "HepPh", - "AstroPh", - "web-Google", - "soc-Pokec" - ] +to_run = ["katz", "pagerank", "jaccard", "eigenvector", "hits"] +datasets = ["karate", "phonecalls", "HepPh", "AstroPh", "web-Google", "soc-Pokec"] if "-s" in sys.argv: - datasets=[] + datasets = [] folder = Path(".") -outfile="convergence2_run_results.txt" -convert=True +outfile = "convergence2_run_results.txt" +convert = True if convert: for suffix in to_run: - file_name = Path("convergence2_"+suffix+".ipynb") - file = folder/file_name + file_name = Path("convergence2_" + suffix + ".ipynb") + file = folder / file_name if not file.is_file(): folder = Path("./scripts") - file = folder/file_name + file = folder / file_name if not file.is_file(): - print("did not find: "+str(file)) + print("did not find: " + str(file)) continue - command = "jupyter nbconvert --to script "+str(file) + command = "jupyter nbconvert --to script " + str(file) print(command) os.system(command) print("conversion done!") for suffix in to_run: - print("<<< testing: "+suffix +" >>>") - python_name = this_folder/Path("convergence2_"+suffix+".py") - - command = "python "+str(python_name)+" datasets "+encode_str(repr(["karate"]))+ " n 1" + print("<<< testing: " + suffix + " >>>") + python_name = this_folder / Path("convergence2_" + suffix + ".py") + + command = ( + "python " + + str(python_name) + + " datasets " + + encode_str(repr(["karate"])) + + " n 1" + ) print(command) - #os.system(command) + # os.system(command) _, result = capture_subprocess_output(command) print("tests done!") @@ -137,19 +136,21 @@ def encode_str(s): for suffix in to_run: if long_result: print("\n\n") - print("<<< "+suffix +" >>>") - python_name = folder/Path("convergence2_"+suffix+".py") + print("<<< " + suffix + " >>>") + python_name = folder / Path("convergence2_" + suffix + ".py") - command = "python "+str(python_name)+" datasets "+encode_str(repr(datasets)) + command = ( + "python " + str(python_name) + " datasets " + encode_str(repr(datasets)) + ) print(command) _, result = capture_subprocess_output(command) - #subprocess.run([command], capture_output=True, text=True, shell=True).stdout - #print(result) - long_result= result.count('\n') >5 + # subprocess.run([command], capture_output=True, text=True, shell=True).stdout + # print(result) + long_result = result.count("\n") > 5 res = re.search(r"\d\d\d\d_\d\d_\d\d__\d\d_\d\d_\d\d", result) if res: - with open(folder/outfile, "a", encoding="utf-8") as f: - f.write(suffix+ "\t"+str(res.group())+"\n") + with open(folder / outfile, "a", encoding="utf-8") as f: + f.write(suffix + "\t" + str(res.group()) + "\n") else: print("<<< no output file produced") - #print(repr(lines)) \ No newline at end of file + # print(repr(lines)) diff --git a/scripts/convergence_helper.py b/scripts/convergence_helper.py index 0a7c107..d4be8a5 100644 --- a/scripts/convergence_helper.py +++ b/scripts/convergence_helper.py @@ -11,17 +11,26 @@ from nestmodel.load_datasets import load_fg_dataset_cached + class ParameterWrapper: - def __init__(self, dataset_path, cent_func, cent_kwargs, rewire_kwargs, wl_kwargs, number_of_samples): - self.dataset_path=dataset_path - self._cent_func=cent_func - self.cent_kwargs=cent_kwargs - self.rewire_kwargs=rewire_kwargs - self.wl_kwargs=wl_kwargs - self.verbosity=1 - self.force_reload=False - self.number_of_samples=number_of_samples - self.tqdm=None + def __init__( + self, + dataset_path, + cent_func, + cent_kwargs, + rewire_kwargs, + wl_kwargs, + number_of_samples, + ): + self.dataset_path = dataset_path + self._cent_func = cent_func + self.cent_kwargs = cent_kwargs + self.rewire_kwargs = rewire_kwargs + self.wl_kwargs = wl_kwargs + self.verbosity = 1 + self.force_reload = False + self.number_of_samples = number_of_samples + self.tqdm = None def cent_func(self, *args, **kwargs): centralities = self._cent_func(*args, **kwargs) @@ -32,73 +41,78 @@ def cent_func(self, *args, **kwargs): def important_to_dict(self): return { - "cent_kwargs" : self.cent_kwargs, - "rewire_kwargs":self.rewire_kwargs, - "wl_kwargs" : self.wl_kwargs, - "number_of_samples": self.number_of_samples + "cent_kwargs": self.cent_kwargs, + "rewire_kwargs": self.rewire_kwargs, + "wl_kwargs": self.wl_kwargs, + "number_of_samples": self.number_of_samples, } def range_over_samples(self): the_range = range(self.number_of_samples) if self.tqdm: try: - from tqdm.auto import tqdm + from tqdm.auto import tqdm # pylint: disable=import-outside-toplevel + the_range = tqdm(the_range, leave=False, desc="samples") except ModuleNotFoundError: warnings.warn("Could not find tqdm, not displaying progressbar") return the_range + def wl_range(self, wl_iterations): - the_range = range(wl_iterations-1,-1,-1) + the_range = range(wl_iterations - 1, -1, -1) if self.tqdm: try: - from tqdm.auto import tqdm + from tqdm.auto import tqdm # pylint: disable=import-outside-toplevel + the_range = tqdm(the_range, desc="wl_rounds", leave=False) except ModuleNotFoundError: warnings.warn("Could not find tqdm, not displaying progressbar") return the_range - - def SAE(v0, v1): - return np.sum(np.abs(v0-v1)) + return np.sum(np.abs(v0 - v1)) + class Tracker: def __init__(self, tag=None, func=SAE, base_centrality=None): self.data = [] - self.curr_round=None - self.curr_dataset=None - self.tag=tag - self.func=func + self.curr_round = None + self.curr_dataset = None + self.tag = tag + self.func = func self.set_base_centrality(base_centrality) def set_base_centrality(self, base_centrality): self.base_centrality = base_centrality def new_round(self, round_id): - self.curr_round=round_id - + self.curr_round = round_id def new_dataset(self, dataset_id): - self.curr_round=None - self.curr_dataset=dataset_id + self.curr_round = None + self.curr_dataset = dataset_id def add_centrality(self, centrality): - assert not self.curr_round is None - assert not self.curr_dataset is None - - tpl = (self.curr_dataset, self.curr_round, self.func(centrality, self.base_centrality), self.tag) + assert self.curr_round is not None + assert self.curr_dataset is not None + + tpl = ( + self.curr_dataset, + self.curr_round, + self.func(centrality, self.base_centrality), + self.tag, + ) self.data.append(tpl) - class MultiTracker: def __init__(self, tags=None, funcs=None): if funcs is None: funcs = repeat(SAE) else: - assert len(funcs)==len(tags) - self.trackers = [Tracker(tag=tag, func=func) for tag ,func in zip(tags, funcs)] + assert len(funcs) == len(tags) + self.trackers = [Tracker(tag=tag, func=func) for tag, func in zip(tags, funcs)] self.verbosity = 0 def set_base_centrality(self, *centralities): @@ -129,16 +143,23 @@ def data(self): return chain.from_iterable(tracker.data for tracker in self.trackers) def data_to_pandas(self): - return pd.DataFrame.from_records(list(self.data), columns=("dataset", "wl_round", "value", "tag")) + return pd.DataFrame.from_records( + list(self.data), columns=("dataset", "wl_round", "value", "tag") + ) + + MultiTracker.to_df = MultiTracker.data_to_pandas MultiTracker.track = MultiTracker.add_centrality def load_dataset(dataset, params): - return load_fg_dataset_cached(params.dataset_path, - dataset, - verbosity=params.verbosity, - force_reload=params.force_reload) + return load_fg_dataset_cached( + params.dataset_path, + dataset, + verbosity=params.verbosity, + force_reload=params.force_reload, + ) + def process_dataset(dataset, tracker, params): tracker.new_dataset(dataset) @@ -158,8 +179,7 @@ def process_dataset(dataset, tracker, params): def compute_on_all_datasets(datasets, tracker, params): - """ computes - """ + """computes""" for dataset in datasets: process_dataset(dataset, tracker, params) @@ -172,10 +192,10 @@ def save_results(prefix, tracker, params, min_samples=50): folder = Path("./results/") if not folder.exists(): folder = Path("./scripts/results/") - out_name = folder/(f"{prefix}_"+date_suffix+".pkl") + out_name = folder / (f"{prefix}_" + date_suffix + ".pkl") data = tracker.data_to_pandas() - if len(data)6}: {arg}") origin = Path(sys.argv[1]).resolve() target = Path(sys.argv[2]).resolve() copy_py_file(origin, target, py_files) - copy_py_file(origin.parent/"scripts", target.parent/"scripts", script_files) + copy_py_file(origin.parent / "scripts", target.parent / "scripts", script_files) copy_py_file(origin.parent, target.parent, other_files) - - -# python copy_files.py ../colorful_configuration/cc_model ./nestmodel \ No newline at end of file +# python copy_files.py ../colorful_configuration/cc_model ./nestmodel diff --git a/scripts/find long refinement graphs.ipynb b/scripts/find long refinement graphs.ipynb new file mode 100644 index 0000000..d0e6b99 --- /dev/null +++ b/scripts/find long refinement graphs.ipynb @@ -0,0 +1,369 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 21, + "id": "76b3fa3b-969f-45f8-b88c-8ad72a7a62f3", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "5deb93b2-7514-4fca-96f4-325778ff695d", + "metadata": {}, + "outputs": [], + "source": [ + "from nestmodel.fast_graph import FastGraph" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "6c81a9d2-d3c7-4142-98e2-f7844bf33530", + "metadata": {}, + "outputs": [], + "source": [ + "from networkx.readwrite import from_graph6_bytes\n", + "from tqdm.notebook import tqdm" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "fb7bd5a6-0265-407f-bc7e-8223abc50627", + "metadata": {}, + "outputs": [], + "source": [ + "from nestmodel.io import g6_read_bytes, g6_bytes_to_edges" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "ec148bcb-7658-4b43-94a7-5ee9c74ea6d4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: total: 2.34 s\n", + "Wall time: 2.33 s\n" + ] + } + ], + "source": [ + "%%time\n", + "g6_bytes = g6_read_bytes(r\"L:/tmp/Isomorphism/graph10c.g6\")" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "cc5956c9-9880-425a-9fc4-fbaef31fdf04", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "11716571\n", + "CPU times: total: 19.3 s\n", + "Wall time: 19.4 s\n" + ] + } + ], + "source": [ + "%%time\n", + "g6_edges = g6_bytes_to_edges(g6_bytes)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "1fba459d-2b89-45c6-b172-0a99aea9b02c", + "metadata": {}, + "outputs": [], + "source": [ + "from collections import defaultdict, Counter\n", + "def calc_sequence(colors):\n", + " for c1, c2 in zip(colors, colors[1:]):\n", + " d = defaultdict(Counter)\n", + " for i, j in zip(c2, c1):\n", + " d[i][j]+=1\n", + " print(d)\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "11334e11-45e3-4b59-aff2-43be5677fdea", + "metadata": {}, + "outputs": [], + "source": [ + "from nestmodel.fast_wl import WL_fast\n", + "from nestmodel.utils import make_directed" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "e81065e4-c176-4fef-b2d8-d7a87c90c339", + "metadata": {}, + "outputs": [], + "source": [ + "from collections import defaultdict" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "be306263-ee05-4cd3-b21d-1d8d12ffafa3", + "metadata": {}, + "outputs": [], + "source": [ + "from tqdm.notebook import tqdm" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "727663e9-4e8e-43fa-9ec9-43945cdf6289", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e9efb4e45c664a48a1d41b2a709e2944", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/11716571 [00:00=8:\n", + " graphs_by_iteration_number[num_iterations].append(edges)" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "cdf366fd-a60e-452e-8df6-eedd42a25008", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "16" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(graphs_by_iteration_number[10])" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "eab8de92-2de6-43aa-bea7-a270e41ba5b3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(graphs_by_iteration_number[11])" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "0536cf0a-4f4c-47ee-8cb6-d9f6e102898c", + "metadata": {}, + "outputs": [], + "source": [ + "s=\"\"\n", + "for edges in graphs_by_iteration_number[10]:\n", + " s += repr(edges)+\"\\n\"" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "e1039ec8-3e0c-48a0-bf62-13d754a3fa8e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "array([[0, 4], [1, 5], [2, 5], [0, 6], [3, 6], [4, 6], [0, 7], [1, 7], [3, 7], [1, 8], [2, 8], [4, 8], [0, 9], [2, 9], [3, 9], [5, 9], [4, 0], [5, 1], [5, 2], [6, 0], [6, 3], [6, 4], [7, 0], [7, 1], [7, 3], [8, 1], [8, 2], [8, 4], [9, 0], [9, 2], [9, 3], [9, 5]], dtype=int16)\n", + "array([[0, 4], [1, 5], [2, 5], [0, 6], [1, 6], [3, 6], [0, 7], [2, 7], [4, 7], [0, 8], [1, 8], [2, 8], [3, 8], [1, 9], [2, 9], [6, 9], [7, 9], [4, 0], [5, 1], [5, 2], [6, 0], [6, 1], [6, 3], [7, 0], [7, 2], [7, 4], [8, 0], [8, 1], [8, 2], [8, 3], [9, 1], [9, 2], [9, 6], [9, 7]], dtype=int16)\n", + "array([[0, 4], [1, 5], [2, 5], [3, 5], [0, 6], [1, 6], [2, 6], [1, 7], [3, 7], [4, 7], [0, 8], [1, 8], [2, 8], [4, 8], [0, 9], [3, 9], [4, 9], [5, 9], [4, 0], [5, 1], [5, 2], [5, 3], [6, 0], [6, 1], [6, 2], [7, 1], [7, 3], [7, 4], [8, 0], [8, 1], [8, 2], [8, 4], [9, 0], [9, 3], [9, 4], [9, 5]], dtype=int16)\n", + "array([[0, 4], [1, 4], [2, 5], [3, 5], [0, 6], [1, 6], [2, 6], [0, 7], [1, 7], [3, 7], [0, 8], [2, 8], [4, 8], [5, 8], [2, 9], [3, 9], [4, 9], [6, 9], [4, 0], [4, 1], [5, 2], [5, 3], [6, 0], [6, 1], [6, 2], [7, 0], [7, 1], [7, 3], [8, 0], [8, 2], [8, 4], [8, 5], [9, 2], [9, 3], [9, 4], [9, 6]], dtype=int16)\n", + "array([[0, 4], [1, 4], [2, 5], [3, 5], [4, 5], [0, 6], [1, 6], [2, 6], [0, 7], [2, 7], [3, 7], [0, 8], [1, 8], [3, 8], [4, 8], [5, 8], [1, 9], [2, 9], [3, 9], [6, 9], [7, 9], [4, 0], [4, 1], [5, 2], [5, 3], [5, 4], [6, 0], [6, 1], [6, 2], [7, 0], [7, 2], [7, 3], [8, 0], [8, 1], [8, 3], [8, 4], [8, 5], [9, 1], [9, 2], [9, 3], [9, 6], [9, 7]], dtype=int16)\n", + "array([[0, 4], [1, 4], [2, 5], [3, 5], [4, 5], [0, 6], [1, 6], [2, 6], [0, 7], [2, 7], [3, 7], [0, 8], [1, 8], [3, 8], [4, 8], [5, 8], [1, 9], [2, 9], [3, 9], [6, 9], [7, 9], [8, 9], [4, 0], [4, 1], [5, 2], [5, 3], [5, 4], [6, 0], [6, 1], [6, 2], [7, 0], [7, 2], [7, 3], [8, 0], [8, 1], [8, 3], [8, 4], [8, 5], [9, 1], [9, 2], [9, 3], [9, 6], [9, 7], [9, 8]], dtype=int16)\n", + "array([[0, 4], [1, 4], [0, 5], [2, 5], [4, 5], [0, 6], [1, 6], [3, 6], [1, 7], [2, 7], [3, 7], [0, 8], [1, 8], [3, 8], [4, 8], [7, 8], [0, 9], [1, 9], [2, 9], [4, 9], [7, 9], [4, 0], [4, 1], [5, 0], [5, 2], [5, 4], [6, 0], [6, 1], [6, 3], [7, 1], [7, 2], [7, 3], [8, 0], [8, 1], [8, 3], [8, 4], [8, 7], [9, 0], [9, 1], [9, 2], [9, 4], [9, 7]], dtype=int16)\n", + "array([[0, 3], [1, 4], [0, 5], [2, 5], [0, 6], [1, 6], [3, 6], [4, 6], [0, 7], [1, 7], [2, 7], [4, 7], [1, 8], [2, 8], [3, 8], [5, 8], [6, 8], [2, 9], [3, 9], [4, 9], [5, 9], [7, 9], [3, 0], [4, 1], [5, 0], [5, 2], [6, 0], [6, 1], [6, 3], [6, 4], [7, 0], [7, 1], [7, 2], [7, 4], [8, 1], [8, 2], [8, 3], [8, 5], [8, 6], [9, 2], [9, 3], [9, 4], [9, 5], [9, 7]], dtype=int16)\n", + "array([[0, 3], [1, 4], [0, 5], [2, 5], [0, 6], [1, 6], [2, 6], [4, 6], [1, 7], [2, 7], [3, 7], [5, 7], [0, 8], [1, 8], [3, 8], [4, 8], [6, 8], [7, 8], [2, 9], [3, 9], [4, 9], [5, 9], [6, 9], [7, 9], [3, 0], [4, 1], [5, 0], [5, 2], [6, 0], [6, 1], [6, 2], [6, 4], [7, 1], [7, 2], [7, 3], [7, 5], [8, 0], [8, 1], [8, 3], [8, 4], [8, 6], [8, 7], [9, 2], [9, 3], [9, 4], [9, 5], [9, 6], [9, 7]], dtype=int16)\n", + "array([[0, 3], [1, 4], [2, 4], [0, 5], [1, 5], [4, 5], [0, 6], [2, 6], [3, 6], [0, 7], [1, 7], [2, 7], [3, 7], [1, 8], [2, 8], [3, 8], [4, 8], [6, 8], [2, 9], [3, 9], [4, 9], [5, 9], [6, 9], [3, 0], [4, 1], [4, 2], [5, 0], [5, 1], [5, 4], [6, 0], [6, 2], [6, 3], [7, 0], [7, 1], [7, 2], [7, 3], [8, 1], [8, 2], [8, 3], [8, 4], [8, 6], [9, 2], [9, 3], [9, 4], [9, 5], [9, 6]], dtype=int16)\n", + "array([[0, 3], [0, 4], [1, 4], [1, 5], [2, 5], [0, 6], [1, 6], [2, 6], [3, 6], [0, 7], [2, 7], [3, 7], [5, 7], [1, 8], [3, 8], [4, 8], [5, 8], [6, 8], [0, 9], [1, 9], [3, 9], [5, 9], [7, 9], [3, 0], [4, 0], [4, 1], [5, 1], [5, 2], [6, 0], [6, 1], [6, 2], [6, 3], [7, 0], [7, 2], [7, 3], [7, 5], [8, 1], [8, 3], [8, 4], [8, 5], [8, 6], [9, 0], [9, 1], [9, 3], [9, 5], [9, 7]], dtype=int16)\n", + "array([[0, 3], [0, 4], [1, 4], [1, 5], [2, 5], [3, 5], [0, 6], [1, 6], [2, 6], [4, 6], [0, 7], [1, 7], [2, 7], [3, 7], [1, 8], [2, 8], [3, 8], [4, 8], [5, 8], [0, 9], [3, 9], [4, 9], [5, 9], [6, 9], [3, 0], [4, 0], [4, 1], [5, 1], [5, 2], [5, 3], [6, 0], [6, 1], [6, 2], [6, 4], [7, 0], [7, 1], [7, 2], [7, 3], [8, 1], [8, 2], [8, 3], [8, 4], [8, 5], [9, 0], [9, 3], [9, 4], [9, 5], [9, 6]], dtype=int16)\n", + "array([[0, 3], [0, 4], [1, 4], [3, 4], [0, 5], [1, 5], [2, 5], [0, 6], [1, 6], [2, 6], [5, 6], [1, 7], [2, 7], [3, 7], [4, 7], [0, 8], [2, 8], [3, 8], [4, 8], [5, 8], [7, 8], [1, 9], [2, 9], [3, 9], [4, 9], [5, 9], [6, 9], [3, 0], [4, 0], [4, 1], [4, 3], [5, 0], [5, 1], [5, 2], [6, 0], [6, 1], [6, 2], [6, 5], [7, 1], [7, 2], [7, 3], [7, 4], [8, 0], [8, 2], [8, 3], [8, 4], [8, 5], [8, 7], [9, 1], [9, 2], [9, 3], [9, 4], [9, 5], [9, 6]], dtype=int16)\n", + "array([[0, 3], [1, 3], [0, 4], [2, 4], [1, 5], [2, 5], [0, 6], [1, 6], [3, 6], [4, 6], [0, 7], [1, 7], [2, 7], [4, 7], [5, 7], [1, 8], [2, 8], [3, 8], [4, 8], [5, 8], [6, 8], [0, 9], [2, 9], [3, 9], [5, 9], [6, 9], [7, 9], [3, 0], [3, 1], [4, 0], [4, 2], [5, 1], [5, 2], [6, 0], [6, 1], [6, 3], [6, 4], [7, 0], [7, 1], [7, 2], [7, 4], [7, 5], [8, 1], [8, 2], [8, 3], [8, 4], [8, 5], [8, 6], [9, 0], [9, 2], [9, 3], [9, 5], [9, 6], [9, 7]], dtype=int16)\n", + "array([[0, 3], [1, 3], [0, 4], [2, 4], [1, 5], [2, 5], [3, 5], [0, 6], [1, 6], [4, 6], [1, 7], [2, 7], [3, 7], [4, 7], [5, 7], [0, 8], [1, 8], [2, 8], [4, 8], [6, 8], [7, 8], [0, 9], [2, 9], [3, 9], [5, 9], [6, 9], [7, 9], [8, 9], [3, 0], [3, 1], [4, 0], [4, 2], [5, 1], [5, 2], [5, 3], [6, 0], [6, 1], [6, 4], [7, 1], [7, 2], [7, 3], [7, 4], [7, 5], [8, 0], [8, 1], [8, 2], [8, 4], [8, 6], [8, 7], [9, 0], [9, 2], [9, 3], [9, 5], [9, 6], [9, 7], [9, 8]], dtype=int16)\n", + "array([[0, 3], [1, 3], [0, 4], [2, 4], [1, 5], [2, 5], [3, 5], [4, 5], [0, 6], [1, 6], [2, 6], [3, 6], [0, 7], [1, 7], [2, 7], [4, 7], [6, 7], [0, 8], [1, 8], [2, 8], [3, 8], [4, 8], [6, 8], [1, 9], [2, 9], [3, 9], [4, 9], [5, 9], [7, 9], [3, 0], [3, 1], [4, 0], [4, 2], [5, 1], [5, 2], [5, 3], [5, 4], [6, 0], [6, 1], [6, 2], [6, 3], [7, 0], [7, 1], [7, 2], [7, 4], [7, 6], [8, 0], [8, 1], [8, 2], [8, 3], [8, 4], [8, 6], [9, 1], [9, 2], [9, 3], [9, 4], [9, 5], [9, 7]], dtype=int16)\n", + "\n" + ] + } + ], + "source": [ + "print(s.replace(\",\\n\", \",\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1282f96c-b1da-49c2-a654-e28aa49245f6", + "metadata": {}, + "outputs": [], + "source": [ + "def get_moves(colorings):\n", + " s = \"\"\n", + " \n", + " for colors, next_colors in zip(colorings, colorings[1:]):\n", + " count_prev = defaultdict(int)\n", + " count_now = defaultdict(int)\n", + " mapping = {c:prev_c for prev_c, c in zip(colors, next_colors) }\n", + " for prev_c, c in zip(colors, next_colors):\n", + " count_prev[prev_c]+=1\n", + " count_now[c]+=1\n", + " for c, prev_c in mapping.items():\n", + " if count_now[c] == count_prev[prev_c]:\n", + " continue\n", + " if count_prev[prev_c] != 2 and count_now[c] == 1:\n", + " s+=\"E\"\n", + " break\n", + " elif count_prev[prev_c] == 2 and count_now[c] == 1:\n", + " s+=\"A\"\n", + " break\n", + " else:\n", + " s+=\"R\"\n", + " break\n", + " return s" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5b6cf0d-910d-46aa-ba97-892bc618d255", + "metadata": {}, + "outputs": [], + "source": [ + "get_moves(WL_fast(G.edges, method=\"nlogn\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f222c149-aca7-4070-9e4b-2d8186f371d9", + "metadata": {}, + "outputs": [], + "source": [ + "get_moves(WL_fast(G2.edges, method=\"nlogn\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e08be468-50bf-4176-9d11-3260b154be60", + "metadata": {}, + "outputs": [], + "source": [ + "G.calc_wl()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ab9d5d0-e75c-42df-8780-d72b58443cf4", + "metadata": {}, + "outputs": [], + "source": [ + "calc_sequence(G.calc_wl())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a566eac3-d6f5-4300-b3b8-ea7b378e8550", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "edfe4dce-bb5d-4f6f-9d6e-5941da19fbff", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c6f9584-5178-46d7-a729-16a402250813", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/scripts/get_datasets.py b/scripts/get_datasets.py index fc5ded2..d77b5ba 100644 --- a/scripts/get_datasets.py +++ b/scripts/get_datasets.py @@ -4,23 +4,27 @@ import os -hep_link = r"http://snap.stanford.edu/data/cit-HepPh.txt.gz" -astro_link = r"http://snap.stanford.edu/data/ca-AstroPh.txt.gz" +hep_link = r"https://snap.stanford.edu/data/cit-HepPh.txt.gz" +astro_link = r"https://snap.stanford.edu/data/ca-AstroPh.txt.gz" # http://networksciencebook.com/translations/en/resources/data.html -networksciencebook_link = r"http://networksciencebook.com/translations/en/resources/networks.zip" +networksciencebook_link = ( + r"https://networksciencebook.com/translations/en/resources/networks.zip" +) google_link = r"https://snap.stanford.edu/data/web-Google.txt.gz" pokec_link = r"https://snap.stanford.edu/data/soc-pokec-relationships.txt.gz" -networkscience_files = ['collaboration.edgelist.txt', - 'powergrid.edgelist.txt', - 'actor.edgelist.txt', - 'www.edgelist.txt', - 'phonecalls.edgelist.txt', - 'internet.edgelist.txt', - 'metabolic.edgelist.txt', - 'email.edgelist.txt', - 'citation.edgelist.txt', - 'protein.edgelist.txt'] +networkscience_files = [ + "collaboration.edgelist.txt", + "powergrid.edgelist.txt", + "actor.edgelist.txt", + "www.edgelist.txt", + "phonecalls.edgelist.txt", + "internet.edgelist.txt", + "metabolic.edgelist.txt", + "email.edgelist.txt", + "citation.edgelist.txt", + "protein.edgelist.txt", +] print(Path(__file__).resolve()) print(Path(__file__).parent.absolute()) @@ -29,48 +33,66 @@ print(parent.name) if str(parent.name) == "scripts": parent = parent.parent -if str(parent.name)!="datasets": - parent = parent/"datasets" +if str(parent.name) != "datasets": + parent = parent / "datasets" if not parent.is_dir(): print("creating ", parent) parent.mkdir(exist_ok=True) -#assert parent.exists(), str(parent) +# assert parent.exists(), str(parent) links = [hep_link, astro_link, networksciencebook_link, google_link, pokec_link] -download_names = ["cit-HepPh.txt.gz", "ca-AstroPh.txt.gz", "networks.zip", "web-Google.txt.gz", "soc-pokec-relationships.txt.gz"] -final_files = [("cit-HepPh.txt",), ("ca-AstroPh.txt",), networkscience_files, ("web-Google.txt",), ("soc-pokec-relationships.txt",)] - -combinatorical_prefix = "http://users.cecs.anu.edu.au/~bdm/data/" -combinatorial_names = [f"ge{i}d1.g6" for i in range(2,16)] -combinatorial_links = [combinatorical_prefix+name for name in combinatorial_names] -combinatorial_final = [(name, name[:-3]+".npy") for name in combinatorial_names] +download_names = [ + "cit-HepPh.txt.gz", + "ca-AstroPh.txt.gz", + "networks.zip", + "web-Google.txt.gz", + "soc-pokec-relationships.txt.gz", +] +final_files = [ + ("cit-HepPh.txt",), + ("ca-AstroPh.txt",), + networkscience_files, + ("web-Google.txt",), + ("soc-pokec-relationships.txt",), +] + +combinatorical_prefix = "https://users.cecs.anu.edu.au/~bdm/data/" +combinatorial_names = [f"ge{i}d1.g6" for i in range(2, 16)] +combinatorial_links = [combinatorical_prefix + name for name in combinatorial_names] +combinatorial_final = [(name, name[:-3] + ".npy") for name in combinatorial_names] links.extend(combinatorial_links) download_names.extend(combinatorial_names) final_files.extend(combinatorial_final) -if os.name == 'nt': +if os.name == "nt": # if you want to know more about the download command for windows use # https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-webrequest?view=powershell-7.3 def download_command_windows(_link, _parent, file): - return "powershell wget " +"-Uri " + _link +" -OutFile " + str(_parent/file) + return "powershell wget " + "-Uri " + _link + " -OutFile " + str(_parent / file) + dowload_command = download_command_windows def unzip_command_windows(_parent, file): - return "unzip " + str(_parent/file) + " -d " + str((parent/file).stem) + return "unzip " + str(_parent / file) + " -d " + str((parent / file).stem) + unzip_command = unzip_command_windows else: - def download_command_linux(_link, _parent, file): # pylint: disable=unused-argument - return "wget " + _link +" -P " + str(_parent) + + def download_command_linux(_link, _parent, file): # pylint: disable=unused-argument + return "wget " + _link + " -P " + str(_parent) + dowload_command = download_command_linux def unzip_command_linux(_parent, file): # pylint: disable=unused-argument return "unzip " + str(file) + unzip_command = unzip_command_linux def process_g6(path): - import networkx as nx # pylint: disable=import-outside-toplevel - import numpy as np # pylint: disable=import-outside-toplevel + import networkx as nx # pylint: disable=import-outside-toplevel + import numpy as np # pylint: disable=import-outside-toplevel + Graphs = nx.readwrite.read_graph6(path) print(Graphs[0]) out = [np.array([e for e in G.edges], dtype=np.int32) for G in Graphs] @@ -86,31 +108,32 @@ def process_g6(path): if all(final in files for final in finals): continue - if not download_name in files: + if download_name not in files: # download file command = dowload_command(link, parent, download_name) print() print("<<< downloading " + download_name) + print(command) subprocess.call(command, shell=True, cwd=str(parent)) if download_name.endswith(".gz"): command = "gzip -d " + str(download_name) print("<<< extracting " + download_name) - subprocess.call(command, shell=True , cwd=str(parent)) + subprocess.call(command, shell=True, cwd=str(parent)) if download_name.endswith(".zip"): command = unzip_command(parent, download_name) print("<<< extracting " + download_name) - subprocess.call(command, shell=True , cwd=str(parent)) + subprocess.call(command, shell=True, cwd=str(parent)) if download_name.endswith(".g6"): - process_g6(Path(parent)/download_name) + process_g6(Path(parent) / download_name) print() print("done") print() -dataset_path_file = Path(__file__).parent.absolute()/"datasets_path.txt" +dataset_path_file = Path(__file__).parent.absolute() / "datasets_path.txt" if not dataset_path_file.is_file(): with open(dataset_path_file, "w", encoding="utf-8") as f: f.write(str(parent)) diff --git a/scripts/long refinement graphs.ipynb b/scripts/long refinement graphs.ipynb new file mode 100644 index 0000000..03f1cad --- /dev/null +++ b/scripts/long refinement graphs.ipynb @@ -0,0 +1,299 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "76b3fa3b-969f-45f8-b88c-8ad72a7a62f3", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5deb93b2-7514-4fca-96f4-325778ff695d", + "metadata": {}, + "outputs": [], + "source": [ + "from nestmodel.fast_graph import FastGraph\n", + "from nestmodel.long_refinement_graphs import long_refinement_12__1_5, long_refinement_14__1_3, long_refinement_10\n", + "from nestmodel.utils import make_directed" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "204b3c7c-80b4-4ba2-97b1-915406a4fe75", + "metadata": {}, + "outputs": [], + "source": [ + "G1 = long_refinement_14__1_3()\n", + "G2 = long_refinement_12__1_5()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ae30d0b4-1ae3-477a-86cc-da980c6d8684", + "metadata": {}, + "outputs": [], + "source": [ + "from nestmodel.fast_wl import WL_fast" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "eab8de92-2de6-43aa-bea7-a270e41ba5b3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),\n", + " array([0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),\n", + " array([0, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),\n", + " array([0, 2, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),\n", + " array([0, 2, 3, 3, 1, 1, 1, 1, 1, 1, 4, 4, 4, 4]),\n", + " array([0, 2, 3, 3, 1, 1, 1, 1, 1, 1, 4, 4, 5, 5]),\n", + " array([0, 2, 3, 3, 1, 1, 1, 1, 6, 6, 4, 4, 5, 5]),\n", + " array([0, 2, 3, 3, 1, 1, 7, 7, 6, 6, 4, 4, 5, 5]),\n", + " array([0, 2, 3, 3, 1, 1, 7, 7, 6, 6, 4, 8, 5, 5]),\n", + " array([0, 2, 9, 3, 1, 1, 7, 7, 6, 6, 4, 8, 5, 5]),\n", + " array([ 0, 2, 9, 3, 1, 1, 7, 7, 6, 6, 4, 8, 5, 10]),\n", + " array([ 0, 2, 9, 3, 1, 1, 7, 7, 11, 6, 4, 8, 5, 10]),\n", + " array([ 0, 2, 9, 3, 1, 1, 7, 12, 11, 6, 4, 8, 5, 10]),\n", + " array([ 0, 2, 9, 3, 13, 1, 7, 12, 11, 6, 4, 8, 5, 10])]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "WL_fast(make_directed(G1.edges), method=\"nlogn\")" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "1282f96c-b1da-49c2-a654-e28aa49245f6", + "metadata": {}, + "outputs": [], + "source": [ + "from collections import defaultdict\n", + "def get_moves(colorings):\n", + " \"\"\"Calculates the a move string corresponding to a long refinement graph\n", + " The three letters are\n", + " E : a single node is removed from a larger color\n", + " R : a color with more than 4 nodes is split into two colors where none of the colors has size 1\n", + " A : a color with two nodes is split into two singleton nodes\n", + " \"\"\"\n", + " s = \"\"\n", + " \n", + " for colors, next_colors in zip(colorings, colorings[1:]):\n", + " count_prev = defaultdict(int)\n", + " count_now = defaultdict(int)\n", + " mapping = {c:prev_c for prev_c, c in zip(colors, next_colors) }\n", + " for prev_c, c in zip(colors, next_colors):\n", + " count_prev[prev_c]+=1\n", + " count_now[c]+=1\n", + " found = False\n", + " for c, prev_c in mapping.items():\n", + " if count_now[c] == count_prev[prev_c]:\n", + " continue\n", + " if count_prev[prev_c] != 2 and count_now[c] == 1:\n", + " found=True\n", + " s+=\"E\"\n", + " break\n", + " elif count_prev[prev_c] == 2 and count_now[c] == 1:\n", + " found=True\n", + " s+=\"A\"\n", + " break\n", + " else:\n", + " continue\n", + " if not found:\n", + " s+=\"R\"\n", + " return s" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "a5b6cf0d-910d-46aa-ba97-892bc618d255", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'EERRRRRAAAAAA'" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "get_moves(WL_fast(make_directed(G1.edges), method=\"nlogn\", compact=False))" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "f222c149-aca7-4070-9e4b-2d8186f371d9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'EERRRRAAAAA'" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "get_moves(WL_fast(make_directed(G2.edges), method=\"nlogn\", compact=False))" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "a566eac3-d6f5-4300-b3b8-ea7b378e8550", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RRRRAAAAA\n", + "[4 3 3 3 3 3 3 3 3 4]\n", + "RERERAAAA\n", + "[4 4 4 2 2 2 4 4 4 4]\n", + "RRRRAAAAA\n", + "[4 4 3 3 4 4 3 3 4 4]\n", + "RRRRAAAAA\n", + "[4 3 4 3 4 3 4 3 4 4]\n", + "RRRRAAAAA\n", + "[4 4 4 4 4 4 4 4 5 5]\n", + "RRRRAAAAA\n", + "[4 4 4 4 4 4 4 4 6 6]\n", + "RRRRAAAAA\n", + "[5 5 3 3 5 3 3 5 5 5]\n", + "RRRRAAAAA\n", + "[4 4 4 4 4 4 5 5 5 5]\n", + "RRRRAAAAA\n", + "[4 4 4 4 4 4 6 6 6 6]\n", + "RRRRAAAAA\n", + "[4 4 5 5 5 4 5 4 5 5]\n", + "RRRRAAAAA\n", + "[5 5 3 5 3 5 5 5 5 5]\n", + "RRRRAAAAA\n", + "[5 5 4 5 5 5 5 4 5 5]\n", + "RRRRAAAAA\n", + "[5 5 5 5 6 6 5 5 6 6]\n", + "RRRRAAAAA\n", + "[5 5 5 5 5 5 6 6 6 6]\n", + "RERERAAAA\n", + "[5 5 5 5 5 5 5 7 7 7]\n", + "RRRRAAAAA\n", + "[5 6 6 6 6 5 6 6 6 6]\n" + ] + } + ], + "source": [ + "for i in range(16):\n", + " edges = long_refinement_10(i, return_graph=False)\n", + " print(get_moves(WL_fast(make_directed(edges), method=\"nlogn\", compact=False)))\n", + " print(np.bincount(edges.ravel())//2)" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "2c6f9584-5178-46d7-a729-16a402250813", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RERERAAAA\n", + "Counter({'A': 4, 'R': 3, 'E': 2})\n" + ] + }, + { + "data": { + "text/plain": [ + "[array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),\n", + " array([3, 3, 3, 0, 0, 0, 3, 3, 3, 3]),\n", + " array([3, 3, 3, 0, 0, 0, 3, 3, 3, 9]),\n", + " array([3, 4, 4, 0, 0, 0, 4, 4, 3, 9]),\n", + " array([3, 4, 4, 1, 1, 0, 4, 4, 3, 9]),\n", + " array([3, 5, 5, 1, 1, 0, 4, 4, 3, 9]),\n", + " array([3, 5, 5, 1, 1, 0, 4, 4, 8, 9]),\n", + " array([3, 5, 5, 2, 1, 0, 4, 4, 8, 9]),\n", + " array([3, 5, 5, 2, 1, 0, 7, 4, 8, 9]),\n", + " array([3, 6, 5, 2, 1, 0, 7, 4, 8, 9])]" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from collections import Counter\n", + "i=1\n", + "edges = long_refinement_10(i, return_graph=False)\n", + "s = get_moves(WL_fast(make_directed(edges), method=\"nlogn\", compact=False))\n", + "print(s)\n", + "print(Counter(s))\n", + "WL_fast(make_directed(edges), method=\"nlogn\", compact=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "faf8855c-1360-4aa9-abf6-a2ffdac91bbc", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba2c3950-c2d3-4ecc-99e8-8654b4a387bc", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/setup.py b/setup.py deleted file mode 100644 index cfc4d51..0000000 --- a/setup.py +++ /dev/null @@ -1,19 +0,0 @@ -from setuptools import setup - -with open("README.md", "r") as fh: - long_description = fh.read() - -setup( - name='nestmodel', - version='0.2.1', - packages=['nestmodel', 'nestmodel.tests'], - author='Felix Stamm', - author_email='felix.stamm@cssh.rwth-aachen.de', - description='This package contains the implementation of the neighborhood structure configuration model for python', - long_description=long_description, # Long description read from the the readme file - long_description_content_type="text/markdown", - install_requires=[ - 'pandas', 'scipy', 'numpy', 'matplotlib', 'numba', 'networkx', 'brokenaxes', 'nbconvert', 'tqdm', 'threadpoolctl' - ], - python_requires='>=3.8' -) diff --git a/nestmodel/ERGM_experiments.py b/src/nestmodel/ERGM_experiments.py similarity index 54% rename from nestmodel/ERGM_experiments.py rename to src/nestmodel/ERGM_experiments.py index 40e5e69..c5bb57f 100644 --- a/nestmodel/ERGM_experiments.py +++ b/src/nestmodel/ERGM_experiments.py @@ -1,49 +1,62 @@ - import time import numpy as np import networkx as nx from nestmodel.centralities import calc_pagerank -from nestmodel.ergm import _set_seed, pagerank_adjacency, edge_flip_ergm_pagerank_adjacency, edge_flip_ergm_pagerank_dict, Gnp_row_first, _set_seed -from nestmodel.dict_graph import edges_to_dict, pagerank_dict, calc_degrees_from_dict, edge_dict_to_edge_list +from nestmodel.ergm import ( + _set_seed, + pagerank_adjacency, + edge_flip_ergm_pagerank_adjacency, + edge_flip_ergm_pagerank_dict, +) +from nestmodel.dict_graph import ( + edges_to_dict, + pagerank_dict, + calc_degrees_from_dict, + edge_dict_to_edge_list, +) +from nestmodel.mutual_independent_models import Gnp_row_first from nestmodel.utils import calc_jaccard_edges from nestmodel.fast_graph import FastGraph # """ evaluation statistics helper """ -def SAE(a,b): + +def SAE(a, b): """Calculates the sum absolute error of a and b""" return np.sum(np.abs(a - b)) + def get_jaccard_adjacency(A0, A1): """Calculates Jaccard similartiy for adjacecency matrices A0 and A1""" - assert len(A0.shape)==2 - assert len(A1.shape)==2 - assert A0.shape[0]==A0.shape[1] - assert A1.shape[0]==A1.shape[1] + assert len(A0.shape) == 2 + assert len(A1.shape) == 2 + assert A0.shape[0] == A0.shape[1] + assert A1.shape[0] == A1.shape[1] assert A0.ravel().max() <= 1 assert A0.ravel().min() >= 0 assert A1.ravel().max() <= 1 assert A1.ravel().min() >= 0 - intersection = np.sum(A0 * A1) - union = np.sum(np.clip(A0+A1,0,1)) - return intersection/union + union = np.sum(np.clip(A0 + A1, 0, 1)) + return intersection / union + -class ERGM_RewireWrapper(): # pylint: disable = invalid-name +class ERGM_RewireWrapper: # pylint: disable = invalid-name """Wrapper to perform ERGM rewiring of G""" + def __init__(self, G, kind): assert kind in ("adjacency", "dict") if kind == "adjacency": assert G.is_directed() is False assert G.number_of_nodes() < 100, "to many nodes, dict mode recommended" - self.A0 = np.array(nx.to_numpy_array(G)) + self.A0 = np.array(nx.to_numpy_array(G)) self.target_p = pagerank_adjacency(self.A0.copy()) elif kind == "dict": assert not G.is_directed - self.A0 = edges_to_dict(G.edges) + self.A0 = edges_to_dict(G.edges) self.n = G.edges.max() + 1 self.degrees = calc_degrees_from_dict(self.A0, self.n) self.target_p = pagerank_dict(self.A0, self.n, self.degrees) @@ -51,11 +64,12 @@ def __init__(self, G, kind): self.kind = kind self.A = None - - def reset_graph(self): # pylint: disable=missing-function-docstring + def reset_graph(self): # pylint: disable=missing-function-docstring self.A = self.A0.copy() - def validate_params(self, phis): # pylint: disable=unused-argument, missing-function-docstring + def validate_params( + self, phis + ): # pylint: disable=unused-argument, missing-function-docstring return True def rewire(self, n_steps, phi, seed): @@ -63,55 +77,70 @@ def rewire(self, n_steps, phi, seed): target_p = self.target_p.copy() A_work = self.A0.copy() rew_t0 = time.process_time() + result_p = None + ratio = None if self.kind == "adjacency": - result_p, ratio = edge_flip_ergm_pagerank_adjacency(A_work, target_p, n_steps, phi, seed) + result_p, ratio = edge_flip_ergm_pagerank_adjacency( + A_work, target_p, n_steps, phi, seed + ) elif self.kind == "dict": - result_p, ratio = edge_flip_ergm_pagerank_dict(A_work, self.n, target_p, n_steps, phi, seed) + result_p, ratio = edge_flip_ergm_pagerank_dict( + A_work, self.n, target_p, n_steps, phi, seed + ) rew_total = time.process_time() - rew_t0 - return {"result_p" : result_p, - "target_p" : self.target_p.copy(), - "ratio" : ratio, - "result_graph" : A_work, - "initial_graph" : self.A0.copy(), - "rew_time" : rew_total, - "phi" : phi} - - def log_result(self, result, output):# pylint: disable=missing-function-docstring + return { + "result_p": result_p, + "target_p": self.target_p.copy(), + "ratio": ratio, + "result_graph": A_work, + "initial_graph": self.A0.copy(), + "rew_time": rew_total, + "phi": phi, + } + + def log_result(self, result, output): # pylint: disable=missing-function-docstring output.set_phi(result["phi"]) - #print(output._phi) + # print(output._phi) if self.kind == "adjacency": - output.J = get_jaccard_adjacency(result["initial_graph"], result["result_graph"]) - elif self.kind =="dict": + output.J = get_jaccard_adjacency( + result["initial_graph"], result["result_graph"] + ) + elif self.kind == "dict": def convert(edge_dict): edge_list = edge_dict_to_edge_list(edge_dict) - #edge_codes = get_unique_edges_from_edge_list(edge_list, False) + # edge_codes = get_unique_edges_from_edge_list(edge_list, False) return edge_list - output.J = calc_jaccard_edges(convert(result["initial_graph"]), - convert(result["result_graph"]), is_directed=False) + output.J = calc_jaccard_edges( + convert(result["initial_graph"]), + convert(result["result_graph"]), + is_directed=False, + ) output.rew_time = result["rew_time"] output.ratio = result["ratio"] output.result_p = result["result_p"] output.SAE = SAE(result["result_p"], result["target_p"]) - -class NeSt_RewireWrapper(): # pylint: disable = invalid-name +class NeSt_RewireWrapper: # pylint: disable = invalid-name """Wrapper to perform NeSt rewiring of G""" + def __init__(self, G): assert isinstance(G, FastGraph) G.ensure_edges_prepared() - self.G0 = G.copy() + self.G0 = G.copy() self.target_p = calc_pagerank(G) - self.G=None + self.G = None def reset_graph(self): # pylint: disable = missing-function-docstring self.G = self.G0.copy() def validate_params(self, phis): # pylint: disable = missing-function-docstring - if np.max(phis)>= len(self.G0.base_partitions): - raise ValueError(f"Max phi is to large {np.max(phis)} >= {len(self.G0.base_partitions)}") + if np.max(phis) >= len(self.G0.base_partitions): + raise ValueError( + f"Max phi is to large {np.max(phis)} >= {len(self.G0.base_partitions)}" + ) return True def rewire(self, n_steps, depth, seed): @@ -120,38 +149,40 @@ def rewire(self, n_steps, depth, seed): self.G.rewire(depth, 2, seed=seed, n_rewire=n_steps) result_p = calc_pagerank(self.G) rew_total = time.process_time() - rew_t0 - return {"result_p" : result_p, - "target_p" : self.target_p.copy(), - "ratio" : 0, - "result_edges" : self.G.edges.copy(), - "initial_edges" : self.G0.edges.copy(), - "rew_time" : rew_total, - "depth" : depth} - - def log_result(self, result, output): # pylint: disable = missing-function-docstring + return { + "result_p": result_p, + "target_p": self.target_p.copy(), + "ratio": 0, + "result_edges": self.G.edges.copy(), + "initial_edges": self.G0.edges.copy(), + "rew_time": rew_total, + "depth": depth, + } + + def log_result( + self, result, output + ): # pylint: disable = missing-function-docstring output.set_phi(result["depth"]) - #print(output._phi) - output.J = calc_jaccard_edges(result["initial_edges"], result["result_edges"], self.G0.is_directed) + # print(output._phi) + output.J = calc_jaccard_edges( + result["initial_edges"], result["result_edges"], self.G0.is_directed + ) output.rew_time = result["rew_time"] output.ratio = result["ratio"] output.result_p = result["result_p"] output.SAE = SAE(result["result_p"], result["target_p"]) - - - - - -class Erdos_RewireWrapper(): # pylint: disable = invalid-name +class Erdos_RewireWrapper: # pylint: disable = invalid-name """Wrapper to perform NeSt rewiring of G""" + def __init__(self, G): assert isinstance(G, FastGraph) G.ensure_edges_prepared() assert not G.is_directed n = G.num_nodes self.n = n - self.density = len(G.edges)/( (n * (n-1))//2 ) + self.density = len(G.edges) / ((n * (n - 1)) // 2) self.is_directed = G.is_directed self.target_p = calc_pagerank(G) self.initial_edges = G.edges.copy() @@ -167,23 +198,29 @@ def rewire(self, n_steps, depth, seed): rew_t0 = time.process_time() _set_seed(seed) edges = Gnp_row_first(self.n, self.density) - edges = np.array(edges, dtype=np.uint32) + edges = np.array(edges, dtype=np.int32) G = FastGraph(edges, self.is_directed, num_nodes=self.n) result_p = calc_pagerank(G) rew_total = time.process_time() - rew_t0 - return {"result_p" : result_p, - "target_p" : self.target_p.copy(), - "ratio" : 0, - "result_edges" : edges.copy(), - "initial_edges" : self.initial_edges, - "rew_time" : rew_total, - "depth" : depth} - - def log_result(self, result, output): # pylint: disable = missing-function-docstring + return { + "result_p": result_p, + "target_p": self.target_p.copy(), + "ratio": 0, + "result_edges": edges.copy(), + "initial_edges": self.initial_edges, + "rew_time": rew_total, + "depth": depth, + } + + def log_result( + self, result, output + ): # pylint: disable = missing-function-docstring output.set_phi(result["depth"]) - #print(output._phi) - output.J = calc_jaccard_edges(result["initial_edges"], result["result_edges"], self.is_directed) + # print(output._phi) + output.J = calc_jaccard_edges( + result["initial_edges"], result["result_edges"], self.is_directed + ) output.rew_time = result["rew_time"] output.ratio = result["ratio"] output.result_p = result["result_p"] - output.SAE = SAE(result["result_p"], result["target_p"]) \ No newline at end of file + output.SAE = SAE(result["result_p"], result["target_p"]) diff --git a/nestmodel/centralities.py b/src/nestmodel/centralities.py similarity index 59% rename from nestmodel/centralities.py rename to src/nestmodel/centralities.py index a1767fc..22e7d44 100644 --- a/nestmodel/centralities.py +++ b/src/nestmodel/centralities.py @@ -1,19 +1,24 @@ # pylint: disable=import-outside-toplevel, missing-function-docstring import numpy as np -from nestmodel.unified_functions import is_directed, get_sparse_adjacency, num_nodes, get_out_degree_array +from nestmodel.unified_functions import ( + is_directed, + get_sparse_adjacency, + num_nodes, + get_out_degree_array, +) def get_v0(n_nodes): - return np.full(n_nodes, 1/n_nodes) + return np.full(n_nodes, 1 / n_nodes) def normalize(v): v = np.real(v.flatten()) v_sum = np.sum(v) - if v_sum <0: - v*=-1 + if v_sum < 0: + v *= -1 v = np.maximum(v, 0) - v/=np.sum(np.abs(v)) + v /= np.sum(np.abs(v)) return v @@ -23,49 +28,54 @@ def get_adjacency_switched(G, switch=False): return get_sparse_adjacency(G) - def calc_eigenvector(G, *, epsilon=0, max_iter=None): """Compute the eigenvector centrality of this graph""" - from scipy.sparse.linalg import eigs, eigsh # pylint: disable=import-outside-toplevel + from scipy.sparse.linalg import ( + eigs, + eigsh, + ) # pylint: disable=import-outside-toplevel + A = get_adjacency_switched(G).T n_nodes = num_nodes(G) if is_directed(G): _, eigenvector = eigs(A, k=1, maxiter=max_iter, tol=epsilon, v0=get_v0(n_nodes)) else: - _, eigenvector = eigsh(A, k=1, maxiter=max_iter, tol=epsilon, v0=get_v0(n_nodes)) + _, eigenvector = eigsh( + A, k=1, maxiter=max_iter, tol=epsilon, v0=get_v0(n_nodes) + ) return normalize(eigenvector) - -def calc_pagerank(G, alpha = 0.85, epsilon=0, max_iter=None): +def calc_pagerank(G, alpha=0.85, epsilon=0, max_iter=None): """Compute pagerank using scipy.linalg.eigs""" - from scipy.sparse.linalg import eigs # pylint: disable=import-outside-toplevel - + from scipy.sparse.linalg import eigs # pylint: disable=import-outside-toplevel n_nodes = num_nodes(G) - if n_nodes==0: + if n_nodes == 0: raise ValueError("Pagerank undefined for empty graph") - elif n_nodes==1: + elif n_nodes == 1: pagerank_vector = np.array([1.0]) elif n_nodes == 2: pagerank_vector = _pagerank_2_nodes(G) else: op = get_pagerank_operator(G, alpha) - _, pagerank_vector = eigs(op, k=1, maxiter=max_iter, tol=epsilon, v0=get_v0(n_nodes)) + _, pagerank_vector = eigs( + op, k=1, maxiter=max_iter, tol=epsilon, v0=get_v0(n_nodes) + ) return normalize(pagerank_vector) def _pagerank_2_nodes(G): out_degrees = get_out_degree_array(G) - alpha=0.85 - val1 = 1/(2+alpha) - val2 = (1+alpha)/(2+alpha) - #val1 = 0.3508773619358619 - #val2 = 0.649122638064138 - if out_degrees[0]==1 and out_degrees[1]==0: + alpha = 0.85 + val1 = 1 / (2 + alpha) + val2 = (1 + alpha) / (2 + alpha) + # val1 = 0.3508773619358619 + # val2 = 0.649122638064138 + if out_degrees[0] == 1 and out_degrees[1] == 0: return np.array([val1, val2]) - elif out_degrees[0]==0 and out_degrees[1]==1: + elif out_degrees[0] == 0 and out_degrees[1] == 1: return np.array([val2, val1]) else: return np.array([0.5, 0.5]) @@ -75,35 +85,40 @@ def get_pagerank_operator(G, alpha): from scipy.sparse.linalg import LinearOperator from scipy.sparse import coo_array - n= num_nodes(G) + + n = num_nodes(G) A = get_adjacency_switched(G).T.tocoo() degree_arr = get_out_degree_array(G) degrees = np.array(degree_arr[A.col].ravel()) - nonzero_degrees = degrees!=0 + nonzero_degrees = degrees != 0 data = np.array(A.data) data[nonzero_degrees] = (alpha) / degrees[nonzero_degrees] m_int = coo_array((data, (A.col, A.row)), shape=(n, n)) is_dangling = np.where(degree_arr == 0)[0] - any_dangling = len(is_dangling) > 0 + any_dangling = len(is_dangling) > 0 def mv(v): v_sum = v.sum() vec = m_int.T @ v - vec += v_sum * (1 - alpha)/n + vec += v_sum * (1 - alpha) / n if any_dangling: - vec += alpha * v[is_dangling].sum()/n + vec += alpha * v[is_dangling].sum() / n return vec - return LinearOperator((n,n), matvec=mv) + return LinearOperator((n, n), matvec=mv) -def calc_hits(G, *, epsilon=0, max_iter=None): +def calc_hits(G, *, epsilon=0, max_iter=None): """Returns the hits scores of the current graph""" - from scipy.sparse.linalg import svds, eigsh # pylint: disable=import-outside-toplevel + from scipy.sparse.linalg import ( + svds, + eigsh, + ) # pylint: disable=import-outside-toplevel + A = get_adjacency_switched(G) n_nodes = num_nodes(G) @@ -119,35 +134,39 @@ def calc_hits(G, *, epsilon=0, max_iter=None): return hubs, auth - def calc_katz(G, alpha=0.1, epsilon=0, max_iter=None): """Returns the katz scores of the current graph""" - from scipy.sparse.linalg import spsolve # pylint: disable=import-outside-toplevel + from scipy.sparse.linalg import spsolve # pylint: disable=import-outside-toplevel from scipy.sparse import identity + A = get_adjacency_switched(G) n = num_nodes(G) A = identity(n) - alpha * A.T - b=np.ones(n) - katz = spsolve(A, b, ) - #np.linalg.solve(np.eye(n, n) - (alpha * A), b) + b = np.ones(n) + katz = spsolve( + A, + b, + ) + # np.linalg.solve(np.eye(n, n) - (alpha * A), b) return katz -def calc_katz_iter(G, alpha=0.1, epsilon=1e-15, max_iter=100): +def calc_katz_iter(G, alpha=0.1, epsilon=1e-15, max_iter=100, beta=None): """Returns the katz scores of the current graph""" A = get_adjacency_switched(G).T n = num_nodes(G) - beta=np.ones(n) - v=np.ones(n) - v_old=np.ones(n) - converged=False + if beta is None: + beta = np.ones(n) + v_old = beta.copy() + v = beta.copy() + converged = False for i in range(max_iter): - v = alpha*A@v_old + beta - if np.sum(np.abs(v- v_old))/n depth {depth}") # skip current class - range_index +=1 - if range_index == color_ranges.shape[0]:# after the last entry there is no next change but only the end + range_index += 1 + if ( + range_index == color_ranges.shape[0] + ): # after the last entry there is no next change but only the end next_change = curr_stop else: next_change = min(next_start, curr_stop) @@ -47,30 +48,32 @@ def get_depthx_colors_internal(color_ranges, num_nodes, depth, dtype=np.int32): # print(f"writing from {n} to {next_change} value {queue[queue_pointer]}") while n < next_change: out[n] = queue[queue_pointer] - n+=1 + n += 1 # assert n == next_change # pop things from queue whose end is lower or equal to n - while queue_pointer>=0 and color_ranges[queue[queue_pointer],1] <= n: + while queue_pointer >= 0 and color_ranges[queue[queue_pointer], 1] <= n: # print(f"popping", queue[queue_pointer],curr_stop,n) - queue_pointer-=1 - - if next_start == n: # finished writing current thing - while range_index < color_ranges.shape[0] and color_ranges[range_index,0]==n: - range_depth = color_ranges[range_index,2] - if range_depth<=depth: + queue_pointer -= 1 + + if next_start == n: # finished writing current thing + while ( + range_index < color_ranges.shape[0] + and color_ranges[range_index, 0] == n + ): + range_depth = color_ranges[range_index, 2] + if range_depth <= depth: # enque current class - queue_pointer+=1 + queue_pointer += 1 # print("enque", range_index,color_ranges[range_index,:], queue_pointer, range_depth) - queue[queue_pointer]=range_index - range_index+=1 + queue[queue_pointer] = range_index + range_index += 1 # print(queue_pointer) # print(out) # print() return out - @njit(cache=True) def advance_colors_one_round(labels_prev_depth, color_ranges, depth, out): """Produces a labeling of given depth from labels of previous depth @@ -81,40 +84,38 @@ def advance_colors_one_round(labels_prev_depth, color_ranges, depth, out): """ n = 0 for i in range(color_ranges.shape[0]): - if color_ranges[i,2] != depth: + if color_ranges[i, 2] != depth: continue - start = color_ranges[i,0] + start = color_ranges[i, 0] while n < start: - out[n] = labels_prev_depth[n] # we see previous rounds colors - n+=1 + out[n] = labels_prev_depth[n] # we see previous rounds colors + n += 1 - stop = color_ranges[i,1] - while n < stop: # we see current rounds color + stop = color_ranges[i, 1] + while n < stop: # we see current rounds color out[n] = i - n+=1 + n += 1 while n < len(out): out[n] = labels_prev_depth[n] - n+=1 + n += 1 return out - - -@njit([(uint32[:],), (uint64[:],), (int32[:],), (int64[:],)], cache=True) +@njit([(int32[:],), (int64[:],), (int32[:],), (int64[:],)], cache=True) def make_labeling_compact(labeling): """Converts a labeling to a labeling starting from zero consecutively""" max_val = labeling.max() assert labeling.min() >= 0 - fill_val = max_val+1 + fill_val = max_val + 1 - min_len = max(len(labeling), max_val+1) + min_len = max(len(labeling), max_val + 1) mapping = np.full(min_len, fill_val, dtype=labeling.dtype) num_labels = 0 - for i in range(len(labeling)): #pylint:disable=consider-using-enumerate + for i in range(len(labeling)): # pylint:disable=consider-using-enumerate val = labeling[i] - if mapping[val] <= max_val: # mapping of val is already a real value + if mapping[val] <= max_val: # mapping of val is already a real value labeling[i] = mapping[val] else: mapping[val] = num_labels @@ -122,7 +123,6 @@ def make_labeling_compact(labeling): num_labels += 1 - @njit(cache=True) def order_to_undo_order(order): """Transforms an order into the reverse order @@ -134,19 +134,19 @@ def order_to_undo_order(order): """ undo_order = np.empty_like(order) - for i in range(len(order)): #pylint:disable=consider-using-enumerate - undo_order[order[i]]=i + for i in range(len(order)): # pylint:disable=consider-using-enumerate + undo_order[order[i]] = i return undo_order - -class RefinementColors(): +class RefinementColors: """A class designed to hold the refinement colors in a memory efficient way""" + def __init__(self, color_ranges, /, order=None, undo_order=None, num_nodes=None): if order is None and undo_order is None: raise ValueError("either one of undo_order or order need to be provided") - range_order = np.lexsort((color_ranges[:,2], color_ranges[:,0])) - sorted_ranges = color_ranges[range_order,:] + range_order = np.lexsort((color_ranges[:, 2], color_ranges[:, 0])) + sorted_ranges = color_ranges[range_order, :] del color_ranges if order is None: @@ -163,7 +163,6 @@ def __init__(self, color_ranges, /, order=None, undo_order=None, num_nodes=None) num_nodes = len(self.order) self.num_nodes = num_nodes - def get_colors_all_depths(self, external=True, compact=True, dtype=np.int32): """Returns the colors of all nodes at a given depth If external is True, colors are provided in their external order @@ -175,16 +174,22 @@ def get_colors_all_depths(self, external=True, compact=True, dtype=np.int32): Returns: an array of shape (n, max_depth) indicating the wl partition of depths up to d """ - max_depth = np.max(self.color_ranges[:,2].ravel())+1 + max_depth = np.max(self.color_ranges[:, 2].ravel()) + 1 all_labelings = np.empty((max_depth, self.num_nodes), dtype=dtype) last_depth = 0 for depth in range(max_depth): - advance_colors_one_round(all_labelings[last_depth,:].ravel(), self.color_ranges, depth, all_labelings[depth,:].ravel()) + advance_colors_one_round( + all_labelings[last_depth, :].ravel(), + self.color_ranges, + depth, + all_labelings[depth, :].ravel(), + ) last_depth = depth - all_labelings = self.process_output(all_labelings, external=external, compact=compact).reshape(max_depth, self.num_nodes) + all_labelings = self.process_output( + all_labelings, external=external, compact=compact + ).reshape(max_depth, self.num_nodes) return all_labelings - def process_output(self, labeling, external, compact): """Changes labeling in place if external or compact labeling is desired""" if compact: @@ -193,15 +198,13 @@ def process_output(self, labeling, external, compact): self.reorder_partition_to_external(labeling) return labeling - def reorder_partition_to_external(self, arr): """Returns an array sorted such that it agrees with the external node order""" - if len(arr.shape)==1: - arr[:] = arr[self.undo_order] + if len(arr.shape) == 1: + arr[:] = arr[self.undo_order] return inplace_reorder_last_axis(arr, self.undo_order) - def get_colors_for_depth(self, depth, external=True, compact=True): """Returns the colors of all nodes at a fiven depth If external is True, colors are provided in their external order @@ -213,5 +216,7 @@ def get_colors_for_depth(self, depth, external=True, compact=True): Returns: an array of shape (n,) indicating the wl partition of depth d """ - labeling = get_depthx_colors_internal(self.color_ranges, num_nodes=self.num_nodes, depth=depth) + labeling = get_depthx_colors_internal( + self.color_ranges, num_nodes=self.num_nodes, depth=depth + ) return self.process_output(labeling, external=external, compact=compact) diff --git a/nestmodel/dict_graph.py b/src/nestmodel/dict_graph.py similarity index 59% rename from nestmodel/dict_graph.py rename to src/nestmodel/dict_graph.py index 7d8cdf8..458af52 100644 --- a/nestmodel/dict_graph.py +++ b/src/nestmodel/dict_graph.py @@ -1,29 +1,29 @@ import numpy as np from numba import njit, objmode -from numba.typed import Dict # pylint: disable=no-name-in-module +from numba.typed import Dict # pylint: disable=no-name-in-module from nestmodel.fast_graph import FastGraph from nestmodel.centralities import calc_pagerank -from numba.types import float32 @njit def edges_to_dict(edges): """Converts edge list into edge dict""" d = Dict() - d[np.uint32(0), np.uint32(0)] = True - del d[np.uint32(0), np.uint32(0)] - for i,j in edges: - d[(i,j)]=True + d[np.int32(0), np.int32(0)] = True + del d[np.int32(0), np.int32(0)] + for i, j in edges: + d[(i, j)] = True return d + @njit def edge_dict_to_edge_list(edge_dict): """Converts edge dict into edge list""" - edge_list = np.empty((len(edge_dict),2), dtype=np.uint32) - for n, (i,j) in enumerate(edge_dict.keys()): - edge_list[n, 0]=i - edge_list[n, 1]=j + edge_list = np.empty((len(edge_dict), 2), dtype=np.int32) + for n, (i, j) in enumerate(edge_dict.keys()): + edge_list[n, 0] = i + edge_list[n, 1] = j return edge_list @@ -31,23 +31,24 @@ def edge_dict_to_edge_list(edge_dict): def calc_degrees_from_dict(edges_dict, n): """Calculates the degrees from dict""" degrees = np.zeros(n) - for (i,j) in edges_dict: - degrees[i]+=1 - degrees[j]+=1 + for i, j in edges_dict: + degrees[i] += 1 + degrees[j] += 1 return degrees + @njit -def pagerank_dict2(edges_dict, n, degrees, alpha = 0.85, max_iter = 100, eps = 1e-14): +def pagerank_dict2(edges_dict, n, degrees, alpha=0.85, max_iter=100, eps=1e-14): # !!!! rewire time 118_829s !!!!!!! edges = edge_dict_to_edge_list(edges_dict) v = np.ones(n) - with objmode(v='float64[:]'): + with objmode(v="float64[:]"): v = calc_pagerank(FastGraph(edges, False)) return v -@njit -def pagerank_dict(edges_dict, n, degrees, alpha = 0.85, max_iter = 100, eps = 1e-14): +@njit +def pagerank_dict(edges_dict, n, degrees, alpha=0.85, max_iter=100, eps=1e-14): """ G: Graph beta: teleportation parameter @@ -58,36 +59,37 @@ def pagerank_dict(edges_dict, n, degrees, alpha = 0.85, max_iter = 100, eps = 1e """ # flip time approx 4300 s assert edges_dict is not None - v = np.ones(n)/n - v_new = np.ones(n)/n - + v = np.ones(n) / n + v_new = np.ones(n) / n dangling_nodes = np.empty(n, dtype=np.int32) n_dangling = 0 for i in range(n): if degrees[i] <= 1e-16: - dangling_nodes[n_dangling]=i + dangling_nodes[n_dangling] = i n_dangling += 1 - #print("number of dangling nodes", n_dangling, degrees) + # print("number of dangling nodes", n_dangling, degrees) n_steps = 0 while True: - v[:] = v_new/v_new.sum() + v[:] = v_new / v_new.sum() dangling_sum = 0.0 for i in range(n_dangling): dangling_sum += v[dangling_nodes[i]] - v_new[:] = (1.0 - alpha)/n + alpha * dangling_sum/n - for (i,j) in edges_dict.keys(): + v_new[:] = (1.0 - alpha) / n + alpha * dangling_sum / n + for i, j in edges_dict.keys(): v_new[i] += alpha * v[j] / degrees[j] v_new[j] += alpha * v[i] / degrees[i] - #v_new /= v_new.sum() + # v_new /= v_new.sum() n_steps += 1 if n_steps > max_iter: break err = np.linalg.norm(v - v_new, 1) - if not err > eps:# weird comparison for nan case # pylint: disable=unneeded-not + if ( + not err > eps + ): # weird comparison for nan case # pylint: disable=unneeded-not break return v_new diff --git a/nestmodel/ergm.py b/src/nestmodel/ergm.py similarity index 54% rename from nestmodel/ergm.py rename to src/nestmodel/ergm.py index ee09f2b..9ffeb4e 100644 --- a/nestmodel/ergm.py +++ b/src/nestmodel/ergm.py @@ -1,11 +1,10 @@ -from nestmodel.dict_graph import calc_degrees_from_dict, pagerank_dict, edges_to_dict +from nestmodel.dict_graph import calc_degrees_from_dict, pagerank_dict from numba import njit import numpy as np - @njit -def pagerank_adjacency(M_in, alpha = 0.85, max_iter = 1000, eps = 1e-14): +def pagerank_adjacency(M_in, alpha=0.85, max_iter=1000, eps=1e-14): """ G: Graph beta: teleportation parameter @@ -15,25 +14,24 @@ def pagerank_adjacency(M_in, alpha = 0.85, max_iter = 1000, eps = 1e-14): -> break iteration if L1-norm of difference between old and new pagerank vectors are smaller than eps """ n = M_in.shape[0] - norm = np.sum(M_in, axis=1).flatten() # get out degree + norm = np.sum(M_in, axis=1).flatten() # get out degree M = np.empty_like(M_in) for i in range(n): if norm[i] == 0: - M[i,:] = 1/n + M[i, :] = 1 / n else: - M[i,:] = M_in[i,:]/norm[i] - + M[i, :] = M_in[i, :] / norm[i] - v = np.ones(n)/n#v_0.copy() - v_new = np.ones(n)/n + v = np.ones(n) / n # v_0.copy() + v_new = np.ones(n) / n i = 0 while True: - v[:] = v_new/v_new.sum() - v_new[:] = alpha * v @ M + (1.0 - alpha)/n - v_new/=v_new.sum() + v[:] = v_new / v_new.sum() + v_new[:] = alpha * v @ M + (1.0 - alpha) / n + v_new /= v_new.sum() i += 1 if np.linalg.norm(v - v_new, 1) < eps or i > max_iter: break @@ -41,7 +39,6 @@ def pagerank_adjacency(M_in, alpha = 0.85, max_iter = 1000, eps = 1e-14): return v_new - @njit def edge_flip_ergm_pagerank_adjacency(A, target_p, n_steps, phi, seed): """Randomly rewires a network given by a symmetric adjacency matrix A @@ -53,49 +50,49 @@ def edge_flip_ergm_pagerank_adjacency(A, target_p, n_steps, phi, seed): M = A.copy() p_work1 = target_p.copy() - p=p_work1 + p = p_work1 previous_err = np.sum(np.abs(pagerank_adjacency(M) - target_p)) successes = 0 failures = 0 - for _ in range(n_steps): - i = np.random.randint(0,n) - j = np.random.randint(0,n) - if i==j: + i = np.random.randint(0, n) + j = np.random.randint(0, n) + if i == j: continue - if A[i,j]==0: - M[i,j] = 1 - M[j,i] = 1 + if A[i, j] == 0: + M[i, j] = 1 + M[j, i] = 1 else: - M[i,j] = 0 - M[j,i] = 0 + M[i, j] = 0 + M[j, i] = 0 p = pagerank_adjacency(M) err = np.sum(np.abs(p - target_p)) - delta = np.exp(- phi * (err - previous_err) ) + delta = np.exp(-phi * (err - previous_err)) if np.random.random() < min(1, delta): - if A[i,j]==0: - A[i,j]=1 - A[j,i]=1 - else: # A[i,j]==1 - A[i,j]=0 - A[j,i]=0 + if A[i, j] == 0: + A[i, j] = 1 + A[j, i] = 1 + else: # A[i,j]==1 + A[i, j] = 0 + A[j, i] = 0 previous_err = err - successes +=1 + successes += 1 else: - M[i,j] = A[i,j] - M[j,i] = A[j,i] - failures+=1 + M[i, j] = A[i, j] + M[j, i] = A[j, i] + failures += 1 p = pagerank_adjacency(A) if n_steps == 0: ratio = 0 else: - ratio = successes/(n_steps) + ratio = successes / (n_steps) return p, ratio + @njit def edge_flip_ergm_pagerank_dict(edge_dict, n, target_p, n_steps, phi, seed): """Randomly rewires a network given by a symmetric adjacency matrix A @@ -104,41 +101,49 @@ def edge_flip_ergm_pagerank_dict(edge_dict, n, target_p, n_steps, phi, seed): np.random.seed(seed) p_work1 = target_p.copy() - p=p_work1 + p = p_work1 degrees = calc_degrees_from_dict(edge_dict, n) - previous_err = np.sum(np.abs(pagerank_dict(edge_dict, n, degrees,) - target_p)) + previous_err = np.sum( + np.abs( + pagerank_dict( + edge_dict, + n, + degrees, + ) + - target_p + ) + ) successes = 0 failures = 0 - for _ in range(n_steps): - k = np.random.randint(0,n*n) - j = np.uint32(k % n) - i = np.uint32((k - j) //n) + k = np.random.randint(0, n * n) + j = np.int32(k % n) + i = np.int32((k - j) // n) - if i==j: + if i == j: continue - current_edge = (i,j) + current_edge = (i, j) added = False if current_edge in edge_dict: del edge_dict[current_edge] - degrees[i]-= 1 - degrees[j]-= 1 + degrees[i] -= 1 + degrees[j] -= 1 else: edge_dict[current_edge] = True - degrees[i]+= 1 - degrees[j]+= 1 + degrees[i] += 1 + degrees[j] += 1 added = True p = pagerank_dict(edge_dict, n, degrees) err = np.sum(np.abs(p - target_p)) - delta = np.exp(- phi * (err - previous_err) ) + delta = np.exp(-phi * (err - previous_err)) if np.random.random() < min(1, delta): previous_err = err - successes +=1 - else: # undo proposed changes + successes += 1 + else: # undo proposed changes if not added: edge_dict[current_edge] = True degrees[i] += 1 @@ -147,48 +152,20 @@ def edge_flip_ergm_pagerank_dict(edge_dict, n, target_p, n_steps, phi, seed): del edge_dict[current_edge] degrees[i] -= 1 degrees[j] -= 1 - failures+=1 - p = pagerank_dict(edge_dict, n, degrees,) + failures += 1 + p = pagerank_dict( + edge_dict, + n, + degrees, + ) if n_steps == 0: ratio = 0 else: - ratio = successes/(n_steps) + ratio = successes / (n_steps) return p, ratio -@njit -def Gnp_row_first(n, p): - """Generates a random graph drawn from the Gnp ensemble""" - approx = int(n*(n-1)*p) - E=np.empty((approx, 2), dtype=np.uint32) - - x = 0 - y = 0 - k = 1 - agg = 0 - upper_bound = ((n)*(n-1))//2 - i = 0 - while True: - k = np.random.geometric(p) - x += k - agg += k - if agg > upper_bound: - break - while x >= n: - x+=y+2-n - y+=1 - E[i,0]=y - E[i,1]=x - - i+=1 - if i >= len(E): - E2 = np.empty((len(E)+approx,2), dtype=np.uint32) - E2[:len(E)] = E[:] - E = E2 - return E[:i,:] - - -@njit +@njit(cache=True) def _set_seed(seed): """Set the need. This needs to be done within numba @njit function""" - np.random.seed(seed) \ No newline at end of file + np.random.seed(seed) diff --git a/src/nestmodel/fast_graph.py b/src/nestmodel/fast_graph.py new file mode 100644 index 0000000..bf4e714 --- /dev/null +++ b/src/nestmodel/fast_graph.py @@ -0,0 +1,448 @@ +from copy import copy +import warnings +from functools import partial +import numpy as np +from nestmodel.utils import ( + networkx_from_edges, + graph_tool_from_edges, + calc_color_histogram, + switch_in_out, + make_directed, +) +from nestmodel.fast_wl import WL_fast, WL_both + +from nestmodel.fast_rewire import ( + rewire_fast, + dir_rewire_source_only_fast, + sort_edges, + get_block_indices, + dir_sample_source_only_direct, +) +from nestmodel.fast_rewire2 import ( + fg_rewire_nest, +) + + +def ensure_is_numpy_or_none(arr, dtype=np.int64): + """Validates that sth is numpy array of sth iterable""" + if arr is None: + return arr + if isinstance(arr, (tuple, list)): + return np.array(arr, dtype=dtype) + if isinstance(arr, np.ndarray): + return arr + else: + raise ValueError(f"Unexpected input type received {type(arr)}. {arr}") + + +class FastGraph: + """A custom class representing Graphs through edge lists that can be used to efficiently be rewired""" + + def __init__(self, edges, is_directed, check_results=False, num_nodes=None): + assert edges.dtype == np.int32 or edges.dtype == np.int64 + assert isinstance( + is_directed, bool + ), f"wrong type of is_directed: {type(is_directed)}" + self._edges = edges.copy() + self.edges_ordered = None + self.is_directed = is_directed + self.base_partitions = None + self.latest_iteration_rewiring = None + if num_nodes is None: + self.num_nodes = edges.ravel().max() + 1 + else: + self.num_nodes = num_nodes + self.check_results = check_results + self.wl_iterations = None + + # these will be set in reset_edges_ordered + self.edges_classes = None + self.dead_arr = None + self.is_mono = None + self.block_indices = None + self.block_dead = None + + self.out_degree = np.array( + np.bincount( + edges[:, 0].ravel().astype(np.int64), minlength=np.int64(self.num_nodes) + ), + dtype=np.int32, + ) + self.in_degree = np.array( + np.bincount( + edges[:, 1].ravel().astype(np.int64), minlength=np.int64(self.num_nodes) + ), + dtype=np.int32, + ) + + if self.is_directed: + self.out_dead_ends = np.nonzero(self.out_degree == 0)[0] + self.corr_out_degree = self.out_degree.copy() + self.corr_out_degree[self.out_dead_ends] += 1 + + self.in_dead_ends = np.nonzero(self.in_degree == 0)[0] + self.corr_in_degree = self.in_degree.copy() + self.corr_in_degree[self.in_dead_ends] += 1 + + # print(len(self.out_dead_ends), len(self.in_dead_ends)) + else: + self.out_degree = self.out_degree + self.in_degree + self.in_degree = self.out_degree + self.out_dead_ends = np.nonzero(self.out_degree == 0)[0] + self.in_dead_ends = self.out_dead_ends + self.sorting_strategy = None + + @property + def edges( + self, + ): + """Return the current edges of the graph""" + if self.edges_ordered is None: + return self._edges + else: + return self.edges_ordered + + @staticmethod + def from_gt(G): # pragma: gt no cover + """Creates a FastGraph object from a graphtool graph""" + edges = np.array(G.get_edges(), dtype=np.int32) + is_directed = G.is_directed() + return FastGraph(edges, is_directed) + + @staticmethod + def from_nx(G, allow_advanced_node_labels=False): + """Creates a FastGraph object from a networkx graph + + if the networkx graph has non integer node labes you need to set allow_advanced_node_labels=True + + """ + unmapping = None + if allow_advanced_node_labels: + mapping = {node: index for index, node in enumerate(G.nodes)} + unmapping = {value: key for key, value in mapping.items()} + edges_nx = [(mapping[u], mapping[v]) for u, v in G.edges] + else: + edges_nx = G.edges + edges = np.array(edges_nx, dtype=np.int32) + is_directed = G.is_directed() + + if unmapping is None: + return FastGraph(edges, is_directed) + else: + return FastGraph(edges, is_directed), unmapping + + def switch_directions(self): + """Creates a FastGraph object from a graphtool graph""" + edges = switch_in_out(self.edges) + is_directed = self.is_directed + return FastGraph(edges, is_directed, num_nodes=self.num_nodes) + + def to_gt(self): + """Convert the graph to a graph-tool graph""" + edges = self.edges + return graph_tool_from_edges(edges, self.num_nodes, self.is_directed) + + def to_nx(self, is_multi=False): + """Convert the graph to a networkx graph""" + edges = self.edges + return networkx_from_edges( + edges, self.num_nodes, self.is_directed, is_multi=is_multi + ) + + def to_coo(self): + """Returns a sparse coo-matrix representation of the graph""" + from scipy.sparse import coo_matrix # pylint: disable=import-outside-toplevel + + edges = self.edges + if not self.is_directed: + edges = make_directed(edges) + + return coo_matrix( + (np.ones(edges.shape[0]), (edges[:, 0], edges[:, 1])), + shape=(self.num_nodes, self.num_nodes), + ) + + def to_csr(self): + """Returns a sparse csr-matrix representation of the graph""" + from scipy.sparse import csr_matrix # pylint: disable=import-outside-toplevel + + edges = self.edges + if not self.is_directed: + edges = make_directed(edges) + + return csr_matrix( + (np.ones(edges.shape[0]), (edges[:, 0], edges[:, 1])), + shape=(self.num_nodes, self.num_nodes), + ) + + def save_npz(self, outfile, include_wl=False): + """Save the FastGraph object as .npz""" + if not include_wl: + np.savez(outfile, edges=self.edges, is_directed=self.is_directed) + else: + if self.base_partitions is None or self.wl_iterations is None: + raise NotImplementedError( + "Saving without computing the information first makes no sense" + ) + kwargs = { + "edges": self.edges, + "is_directed": self.is_directed, + "base_partitions": self.base_partitions, + "wl_iterations": self.wl_iterations, + "edges_classes": self.edges_classes, + "mono_len": len(self.is_mono), + "block_len": len(self.block_indices), + } + for i, x in enumerate(self.is_mono): + kwargs[f"mono{i}_keys"] = np.array(list(x.keys()), np.int64) + kwargs[f"mono{i}_values"] = np.array(list(x.values()), np.bool_) + for i, x in enumerate(self.block_indices): + kwargs[f"block_indices{i}"] = x + np.savez(outfile, **kwargs) + + @staticmethod + def load_npz(file): + """Load a FastGraph object from a npz file""" + npzfile = np.load(file) + if len(npzfile) == 2: + return FastGraph(npzfile["edges"], bool(npzfile["is_directed"])) + else: + G = FastGraph(npzfile["edges"], bool(npzfile["is_directed"])) + G.base_partitions = npzfile["base_partitions"] + G.wl_iterations = npzfile["wl_iterations"] + G.edges_classes = npzfile["edges_classes"] + + from nestmodel.fast_rewire import ( + create_mono_from_arrs, + ) # pylint: disable=import-outside-toplevel + + G.is_mono = [] + for i in range(npzfile["mono_len"]): + G.is_mono.append( + create_mono_from_arrs( + npzfile[f"mono{i}_keys"], npzfile[f"mono{i}_values"] + ) + ) + G.block_indices = [] + for i in range(npzfile["block_len"]): + G.block_indices.append(npzfile[f"block_indices{i}"]) + return G + + def calc_wl(self, initial_colors=None, max_depth=None, algorithm="normal"): + """Compute the WL colors of this graph using the provided initial colors""" + wl_method = partial(WL_fast, method=algorithm) + return self._calc_wl(wl_method, initial_colors, max_depth=max_depth) + + def calc_wl_both(self, initial_colors=None, max_depth=None): + """Compute the WL partition over both the in and out neighborhood""" + return self._calc_wl(WL_both, initial_colors, max_depth=max_depth) + + def _calc_wl(self, method, initial_colors=None, max_depth=None): + edges = self.edges + if not self.is_directed: + edges = make_directed(edges) + if type(initial_colors).__module__ == np.__name__: # is numpy array + return method( + edges, self.num_nodes, labels=initial_colors, max_iter=max_depth + ) + elif isinstance(initial_colors, str): + assert initial_colors in ("out-degree", "out_degree") + return method( + edges, self.num_nodes, labels=self.out_degree, max_iter=max_depth + ) + else: + return method(edges, self.num_nodes, max_iter=max_depth) + + def ensure_base_wl(self, initial_colors=None, both=False, max_depth=None): + """Compute the base WL partition if they have not yet been computed""" + if self.base_partitions is None: + self.calc_base_wl( + initial_colors=initial_colors, both=both, max_depth=max_depth + ) + + def calc_base_wl(self, initial_colors=None, both=False, max_depth=None): + """Compute and store the base WL partition""" + if self.latest_iteration_rewiring is not None: + raise ValueError( + "Seems some rewiring already employed, cannot calc base WL" + ) + if both is False: + partitions = self.calc_wl( + initial_colors=initial_colors, max_depth=max_depth + ) + else: + partitions = self.calc_wl_both( + initial_colors=initial_colors, max_depth=max_depth + ) + + self.base_partitions = np.array(partitions, dtype=np.int32) + self.wl_iterations = len(self.base_partitions) + + def ensure_edges_prepared( + self, initial_colors=None, both=False, max_depth=None, sorting_strategy=None + ): + """Prepare the edges by first ensuring the base WL and then sorting edges by base WL""" + initial_colors = ensure_is_numpy_or_none(initial_colors, dtype=np.int32) + if self.base_partitions is None: + self.ensure_base_wl( + initial_colors=initial_colors, both=both, max_depth=max_depth + ) + if self.edges_ordered is None: + self.reset_edges_ordered(sorting_strategy) + + def reset_edges_ordered(self, sorting_strategy=None): + """Sort edges according to the partitions""" + ( + self.edges_ordered, + self.edges_classes, + self.dead_arr, + self.is_mono, + self.sorting_strategy, + ) = sort_edges( + self._edges, self.base_partitions, self.is_directed, sorting_strategy + ) + self.block_indices, self.block_dead = get_block_indices( + self.edges_classes, self.dead_arr + ) + + def copy(self): + """Returns a copy of this graph which has no data shared with the original graph""" + G = FastGraph(self._edges.copy(), self.is_directed) + for key, value in self.__dict__.items(): + setattr(G, key, copy(value)) + return G + + def rewire(self, depth, method, **kwargs): + """Rewire the edges of the graph in place, thereby preserving the colors of depth d + Note you cannot call this function with increasing depth, but rather with decreasing depth only + """ + assert ( + self.base_partitions is not None + ), "Base partitions are none. Call G.ensure_edges_prepared() first." + assert depth < len(self.base_partitions), f"{depth} {len(self.base_partitions)}" + assert ( + self.latest_iteration_rewiring is None + or depth <= self.latest_iteration_rewiring + ), f"{depth} {self.latest_iteration_rewiring}" + assert method in (1, 2, 3) + if kwargs is not None: + for key in kwargs: + assert key in ( + "seed", + "n_rewire", + "r", + "parallel", + "source_only", + ), "Invalid keyword provided {key}" + self.latest_iteration_rewiring = depth + + self.ensure_edges_prepared() + if self.check_results: # pragma: no cover + if self.is_directed: + ins, outs = calc_color_histogram( + self._edges, self.base_partitions[depth], self.is_directed + ) + else: + hist = calc_color_histogram( + self._edges, self.base_partitions[depth], self.is_directed + ) + res = None + if method == 1: + seed = kwargs.get("seed", None) + r = kwargs.get("r", 1) + parallel = kwargs.get("parallel", False) + source_only = kwargs.get("source_only", False) + if self.is_directed and source_only: + if self.sorting_strategy != "source": + warnings.warn( + message="source only rewiring should be performed but " + + f"sorting strategy is {self.sorting_strategy}!=source." + + " Dubious behaviour expected. Use sorting_strategy='source' when calling .ensure_edges_prepared", + category=RuntimeWarning, + ) + dir_rewire_source_only_fast( + self.edges_ordered, + self.base_partitions[depth], + self.block_indices[depth], + seed=seed, + num_flip_attempts_in=r, + parallel=parallel, + ) + else: + rewire_fast( + self.edges_ordered, + self.edges_classes[:, depth], + self.is_mono[depth], + self.block_indices[depth][ + np.logical_not(self.block_dead[depth]), : + ], + self.is_directed, + seed=seed, + num_flip_attempts_in=r, + parallel=parallel, + ) + res = None + elif method == 2: + + res = fg_rewire_nest(self, depth, kwargs["n_rewire"], kwargs["seed"]) + elif method == 3: + source_only = kwargs.get("source_only", False) + parallel = kwargs.get("parallel", False) + + if self.is_directed and source_only: + if parallel: + warnings.warn( + "Not running in parallel, direct sampling not yet implemented in parallel" + ) + seed = kwargs.get("seed", None) + dir_sample_source_only_direct( + self.edges_ordered, + self.base_partitions[depth], + self.block_indices[depth], + seed=seed, + ) + res = None + else: + raise NotImplementedError( + "No direct sampling algorithm is available for your configuration" + ) + + if self.check_results: # pragma: no cover + if self.is_directed: + from nestmodel.testing import ( + check_color_histograms_agree, + ) # pylint: disable=import-outside-toplevel + + ins2, outs2 = calc_color_histogram( + self.edges_ordered, self.base_partitions[depth], self.is_directed + ) + check_color_histograms_agree(ins, ins2) + check_color_histograms_agree(outs, outs2) + + assert np.all( + self.in_degree + == np.bincount(self.edges[:, 1].ravel(), minlength=self.num_nodes) + ) + assert np.all( + self.out_degree + == np.bincount(self.edges[:, 0].ravel(), minlength=self.num_nodes) + ) + + # check_colors_are_correct(self, depth) + + else: + # print("checking degree") + degree = self.in_degree + curr_degree1 = np.bincount( + self.edges[:, 0].ravel(), minlength=self.num_nodes + ) + curr_degree2 = np.bincount( + self.edges[:, 1].ravel(), minlength=self.num_nodes + ) + assert np.all(degree == (curr_degree1 + curr_degree2)) + + hist2 = calc_color_histogram( + self.edges, self.base_partitions[depth], self.is_directed + ) + check_color_histograms_agree(hist, hist2) + return res diff --git a/nestmodel/fast_rewire.py b/src/nestmodel/fast_rewire.py similarity index 61% rename from nestmodel/fast_rewire.py rename to src/nestmodel/fast_rewire.py index 90360b5..4a18e51 100644 --- a/nestmodel/fast_rewire.py +++ b/src/nestmodel/fast_rewire.py @@ -2,39 +2,45 @@ from itertools import chain import numpy as np from numba import njit, prange, get_num_threads -#from numba import uint32, bool_ -from numba.typed import List, Dict # pylint: disable=no-name-in-module + +# from numba import uint32, bool_ +from numba.typed import List, Dict # pylint: disable=no-name-in-module from nestmodel.utils import normalise_undirected_edges_by_labels -#from numpy.lib.arraysetops import unique + +# from numpy.lib.arraysetops import unique def get_dead_edges(labels, edges, dead_colors): """Computes the dead edges for given edges and labels""" - is_dead_end1 = dead_colors[labels[edges[:,0]]] - is_dead_end2 = dead_colors[labels[edges[:,1]]] + is_dead_end1 = dead_colors[labels[edges[:, 0]]] + is_dead_end2 = dead_colors[labels[edges[:, 1]]] return np.logical_or(is_dead_end1, is_dead_end2) - def get_dead_edges_full(edge_with_node_labels, edges, order): - """ returns arrays which indicate whether an edge is a dead edge + """returns arrays which indicate whether an edge is a dead edge edges are dead when in the subgraph there is only one node involved on either side """ - - num_labelings = edge_with_node_labels.shape[1]//2 + num_labelings = edge_with_node_labels.shape[1] // 2 dead_indicators = np.zeros((edges.shape[0], num_labelings), dtype=np.bool_) for i in range(num_labelings): - _get_dead_edges(edge_with_node_labels[:,i*2:i*2+2], edges, order, dead_indicators[:,i]) + _get_dead_edges( + edge_with_node_labels[:, i * 2 : i * 2 + 2], + edges, + order, + dead_indicators[:, i], + ) return dead_indicators + @njit(cache=True) def _get_dead_edges(edge_with_node_labels, edges, order, out): - """Computes dead edges, i.e. edges that will never be flipped """ - #print(edge_with_node_labels.shape) + """Computes dead edges, i.e. edges that will never be flipped""" + # print(edge_with_node_labels.shape) start_edge = order[0] last_label_0 = edge_with_node_labels[start_edge, 0] last_label_1 = edge_with_node_labels[start_edge, 1] @@ -56,7 +62,7 @@ def _get_dead_edges(edge_with_node_labels, edges, order, out): curr_id_1 = edges[curr_edge, 1] if curr_label_0 != last_label_0 or curr_label_1 != last_label_1: - if (last_group_is_dead_0 or last_group_is_dead_1) or len_last_group==1: + if (last_group_is_dead_0 or last_group_is_dead_1) or len_last_group == 1: for j in range(start_of_last_group, i): out[order[j]] = True last_group_is_dead_0 = True @@ -73,14 +79,14 @@ def _get_dead_edges(edge_with_node_labels, edges, order, out): last_group_is_dead_0 = False if last_id_1 != curr_id_1: last_group_is_dead_1 = False - len_last_group+=1 - if (last_group_is_dead_0 and last_group_is_dead_1) or len_last_group==1: + len_last_group += 1 + if (last_group_is_dead_0 and last_group_is_dead_1) or len_last_group == 1: for j in range(start_of_last_group, len(out)): out[order[j]] = True - return out + @njit(cache=True) def create_mono_from_arrs(keys, vals): """Turns numpy arrays back into numba dictionaries""" @@ -89,33 +95,35 @@ def create_mono_from_arrs(keys, vals): out[key] = val return out -#@njit + +# @njit def get_edge_id1(edge_with_node_labels, order): - """Compute labels starting from 0 consecutively """ - #order = np.lexsort(edge_with_node_labels.T) + """Compute labels starting from 0 consecutively""" + # order = np.lexsort(edge_with_node_labels.T) return _get_edge_id(edge_with_node_labels, order) + @njit(cache=True) def _get_edge_id(edge_with_node_labels, order): - """Compute labels starting from 0 consecutively """ - out = np.empty(len(edge_with_node_labels), dtype=np.uint32) - last_label_0 = edge_with_node_labels[order[0],0] - last_label_1 = edge_with_node_labels[order[0],1] + """Compute labels starting from 0 consecutively""" + out = np.empty(len(edge_with_node_labels), dtype=np.int32) + last_label_0 = edge_with_node_labels[order[0], 0] + last_label_1 = edge_with_node_labels[order[0], 1] - if last_label_0==last_label_1: - is_mono = {0 : True} + if last_label_0 == last_label_1: + is_mono = {0: True} else: - is_mono = {0 : False} + is_mono = {0: False} num_edge_colors = 0 for i in range(order.shape[0]): curr_edge = order[i] - node_label_0 = edge_with_node_labels[curr_edge,0] - node_label_1 = edge_with_node_labels[curr_edge,1] - if node_label_0!=last_label_0 or node_label_1!=last_label_1: + node_label_0 = edge_with_node_labels[curr_edge, 0] + node_label_1 = edge_with_node_labels[curr_edge, 1] + if node_label_0 != last_label_0 or node_label_1 != last_label_1: num_edge_colors += 1 - last_label_0=node_label_0 - last_label_1=node_label_1 - if node_label_0==node_label_1: + last_label_0 = node_label_0 + last_label_1 = node_label_1 + if node_label_0 == node_label_1: is_mono[num_edge_colors] = True out[curr_edge] = num_edge_colors @@ -125,19 +133,18 @@ def _get_edge_id(edge_with_node_labels, order): @njit(cache=True) def get_edge_id_sourceonly(source_labels, order): - """Compute labels starting from 0 consecutively """ + """Compute labels starting from 0 consecutively""" - out = np.empty(len(order), dtype=np.uint32) + out = np.empty(len(order), dtype=np.int32) last_label_source = source_labels[order[0]] - - is_mono = {0 : False} + is_mono = {0: False} num_edge_colors = 0 for i in range(order.shape[0]): node_label_source = source_labels[order[i]] - if node_label_source!=last_label_source: + if node_label_source != last_label_source: num_edge_colors += 1 - last_label_source=node_label_source + last_label_source = node_label_source out[order[i]] = num_edge_colors @@ -150,11 +157,11 @@ def assign_node_labels_both(labels, edges, out, is_directed): if not is_directed: edges = normalise_undirected_edges_by_labels(edges, labels) - for i in range(edges.shape[0]):#pylint: disable=not-an-iterable - node_0 = edges[i,0] - node_1 = edges[i,1] - out[i,0]=labels[node_0] - out[i,1]=labels[node_1] + for i in range(edges.shape[0]): # pylint: disable=not-an-iterable + node_0 = edges[i, 0] + node_1 = edges[i, 1] + out[i, 0] = labels[node_0] + out[i, 1] = labels[node_1] @njit(cache=True) @@ -162,28 +169,28 @@ def assign_node_labels_sourceonly(labels, edges, out, is_directed): """Assign to out the node labels of the edges""" if not is_directed: edges = normalise_undirected_edges_by_labels(edges, labels) - for i in range(edges.shape[0]):#pylint: disable=not-an-iterable - node_0 = edges[i,0] - out[i]=labels[node_0] + for i in range(edges.shape[0]): # pylint: disable=not-an-iterable + node_0 = edges[i, 0] + out[i] = labels[node_0] @njit(cache=True) def _switch_edges_according_to_labels(edges, labels, unprocessed): """Switches endpoints of edges u-v such that for labels[u] <= labels[v] and removes edges with labels[u] != labels[v] from unprocessed - """ + """ n = 0 - for i in range(len(unprocessed)):#pylint: disable=consider-using-enumerate + for i in range(len(unprocessed)): # pylint: disable=consider-using-enumerate edge_id = unprocessed[i] - u,v = edges[edge_id,:] + u, v = edges[edge_id, :] l_u = labels[u] l_v = labels[v] if l_u == l_v: unprocessed[n] = edge_id n += 1 elif l_u > l_v: - edges[edge_id,0] = v - edges[edge_id,1] = u + edges[edge_id, 0] = v + edges[edge_id, 1] = u return unprocessed[:n] @@ -198,7 +205,8 @@ def normalize_edge_directions(edges, labelings): while len(unprocessed) > 0 and i < len(labelings): labels = labelings[i] unprocessed = _switch_edges_according_to_labels(edges, labels, unprocessed) - i+=1 + i += 1 + def sorting_is_both(sorting_strategy): return sorting_strategy in ("fboth", "f_both", "force_both") @@ -212,89 +220,116 @@ def sort_edges(edges, labelings, is_directed, sorting_strategy): # default if sorting_strategy is None or sorting_strategy in ("in-out", "inout"): sorting_strategy = "both" - if not is_directed and sorting_strategy != "both" and sorting_is_both(sorting_strategy): - warnings.warn(f"Using sorting_strategy='{sorting_strategy}'. sorting_strategy='both' is the only valid choice for correct NeSt") + if ( + not is_directed + and sorting_strategy != "both" + and sorting_is_both(sorting_strategy) + ): + warnings.warn( + f"Using sorting_strategy='{sorting_strategy}'. sorting_strategy='both' is the only valid choice for correct NeSt" + ) assert sorting_strategy in ("both", "source") - edges=edges.copy() - if not is_directed:#inplace modify edges to make sure directions are aligned + edges = edges.copy() + if not is_directed: # inplace modify edges to make sure directions are aligned normalize_edge_directions(edges, labelings) - if sorting_strategy == "both": return *sort_edges_both(edges, labelings, is_directed), sorting_strategy else: return *sort_edges_sourceonly(edges, labelings, is_directed), sorting_strategy + def sort_edges_both(edges, labelings, is_directed): edges_classes = [] is_mono = [] - edge_with_node_labels = np.empty((edges.shape[0], 2*labelings.shape[0]), dtype=labelings.dtype) + edge_with_node_labels = np.empty( + (edges.shape[0], 2 * labelings.shape[0]), dtype=labelings.dtype + ) for i in range(labelings.shape[0]): - assign_node_labels_both(labelings[i,:], edges , edge_with_node_labels[:,i*2:i*2+2], is_directed) + assign_node_labels_both( + labelings[i, :], + edges, + edge_with_node_labels[:, i * 2 : i * 2 + 2], + is_directed, + ) - order = np.lexsort(edge_with_node_labels[:,::-1].T) + order = np.lexsort(edge_with_node_labels[:, ::-1].T) for i in range(labelings.shape[0]): - edge_class, mono = get_edge_id1(edge_with_node_labels[:,i*2:i*2+2], order) + edge_class, mono = get_edge_id1( + edge_with_node_labels[:, i * 2 : i * 2 + 2], order + ) edges_classes.append(edge_class) is_mono.append(mono) - dead_indicator = get_dead_edges_full(edge_with_node_labels, edges, order).T # create alternating array of edge classes and dead idicator alternating_list = list(chain.from_iterable(zip(edges_classes, dead_indicator))) edges_classes_arr = np.vstack(edges_classes) - to_sort_arr = np.vstack(alternating_list)#[dead_ids]+ edges_classes) + to_sort_arr = np.vstack(alternating_list) # [dead_ids]+ edges_classes) # sort edges such that each of the classes are in order - edge_order = np.lexsort(to_sort_arr[::-1,:]) - edges_ordered = edges[edge_order,:] - - return edges_ordered, edges_classes_arr[:, edge_order].T, dead_indicator[:, edge_order], is_mono + edge_order = np.lexsort(to_sort_arr[::-1, :]) + edges_ordered = edges[edge_order, :] + return ( + edges_ordered, + edges_classes_arr[:, edge_order].T, + dead_indicator[:, edge_order], + is_mono, + ) def sort_edges_sourceonly(edges, labelings, is_directed): edges_classes = [] is_mono = [] num_edges = edges.shape[0] - source_labels_per_round = np.empty((labelings.shape[0], num_edges), dtype=labelings.dtype) + source_labels_per_round = np.empty( + (labelings.shape[0], num_edges), dtype=labelings.dtype + ) for i in range(labelings.shape[0]): - assign_node_labels_sourceonly(labelings[i,:], edges , source_labels_per_round[i,:].ravel(), is_directed) + assign_node_labels_sourceonly( + labelings[i, :], edges, source_labels_per_round[i, :].ravel(), is_directed + ) - order = np.lexsort(source_labels_per_round[::-1,:]) + order = np.lexsort(source_labels_per_round[::-1, :]) for i in range(labelings.shape[0]): - edge_class, mono = get_edge_id_sourceonly(source_labels_per_round[i,:], order) + edge_class, mono = get_edge_id_sourceonly(source_labels_per_round[i, :], order) edges_classes.append(edge_class) is_mono.append(mono) - dead_indicator = np.array([np.zeros(num_edges, dtype=np.uint32) for _ in range(labelings.shape[0])]) + dead_indicator = np.array( + [np.zeros(num_edges, dtype=np.int32) for _ in range(labelings.shape[0])] + ) # create alternating array of edge classes and dead idicator alternating_list = list(chain.from_iterable(zip(edges_classes, dead_indicator))) edges_classes_arr = np.vstack(edges_classes) - to_sort_arr = np.vstack(alternating_list)#[dead_ids]+ edges_classes) + to_sort_arr = np.vstack(alternating_list) # [dead_ids]+ edges_classes) # sort edges such that each of the classes are in order - edge_order = np.lexsort(to_sort_arr[::-1,:]) - edges_ordered = edges[edge_order,:] - - return edges_ordered, edges_classes_arr[:, edge_order].T, dead_indicator[:, edge_order], is_mono - + edge_order = np.lexsort(to_sort_arr[::-1, :]) + edges_ordered = edges[edge_order, :] + return ( + edges_ordered, + edges_classes_arr[:, edge_order].T, + dead_indicator[:, edge_order], + is_mono, + ) @njit(cache=True) @@ -308,14 +343,13 @@ def undir_rewire_smallest(edges, n_rewire, is_mono): """ delta = len(edges) - for _ in range(n_rewire): index1 = np.random.randint(0, delta) index2 = np.random.randint(0, delta) - if index1==index2: + if index1 == index2: continue - e1_l, e1_r = edges[index1,:] + e1_l, e1_r = edges[index1, :] if is_mono: i2_1 = np.random.randint(0, 2) i2_2 = 1 - i2_1 @@ -324,18 +358,21 @@ def undir_rewire_smallest(edges, n_rewire, is_mono): else: e2_l, e2_r = edges[index2, :] - - if (e1_r == e2_r) or (e1_l == e2_l): # swap would do nothing + if (e1_r == e2_r) or (e1_l == e2_l): # swap would do nothing continue - if (e1_l == e2_r) or (e1_r == e2_l): # no self loops after swab + if (e1_l == e2_r) or (e1_r == e2_l): # no self loops after swab continue can_flip = True for i in range(len(edges)): - ei_l, ei_r = edges[i,:] - if ((ei_l == e1_l and ei_r == e2_r) or (ei_l == e2_l and ei_r == e1_r) - or (ei_l == e1_r and ei_r == e2_l) or (ei_l == e2_r and ei_r == e1_l)): + ei_l, ei_r = edges[i, :] + if ( + (ei_l == e1_l and ei_r == e2_r) + or (ei_l == e2_l and ei_r == e1_r) + or (ei_l == e1_r and ei_r == e2_l) + or (ei_l == e2_r and ei_r == e1_l) + ): can_flip = False break if can_flip: @@ -343,6 +380,7 @@ def undir_rewire_smallest(edges, n_rewire, is_mono): edges[index2, 0] = e2_l edges[index2, 1] = e1_r + @njit(cache=True) def undir_create_neighborhood_dict(edges): """Converts the edges into a dict which maps each node onto a list of its neighbors @@ -361,7 +399,7 @@ def undir_create_neighborhood_dict(edges): neigh = Dict() neigh[0] = List([-1]) del neigh[0] - for l,r in edges: + for l, r in edges: if l not in neigh: tmp = List([-1]) tmp.pop() @@ -375,7 +413,6 @@ def undir_create_neighborhood_dict(edges): return neigh - @njit(cache=True) def undir_create_neighborhood_dict_dict(edges): """Converts the edges into a dict which maps each node onto a list of its neighbors @@ -393,22 +430,22 @@ def undir_create_neighborhood_dict_dict(edges): """ neigh = Dict() tmp = Dict() - tmp[0]=0 + tmp[0] = 0 neigh[0] = tmp del neigh[0] - for l,r in edges: + for l, r in edges: if l not in neigh: tmp = Dict() - tmp[r]=1 + tmp[r] = 1 del tmp[r] neigh[l] = tmp if r not in neigh: tmp = Dict() - tmp[l]=1 + tmp[l] = 1 del tmp[l] neigh[r] = tmp - neigh[l][r]=1 - neigh[r][l]=1 + neigh[l][r] = 1 + neigh[r][l] = 1 return neigh @@ -436,9 +473,9 @@ def undir_rewire_small(edges, n_rewire, is_mono): for _ in range(n_rewire): index1 = np.random.randint(0, num_edges) index2 = np.random.randint(0, num_edges) - if index1==index2: + if index1 == index2: continue - e1_l, e1_r = edges[index1,:] + e1_l, e1_r = edges[index1, :] if is_mono: i2_1 = np.random.randint(0, 2) i2_2 = 1 - i2_1 @@ -447,15 +484,13 @@ def undir_rewire_small(edges, n_rewire, is_mono): else: e2_l, e2_r = edges[index2, :] - - if (e1_r == e2_r) or (e1_l == e2_l): # swap would do nothing + if (e1_r == e2_r) or (e1_l == e2_l): # swap would do nothing continue - if (e1_l == e2_r) or (e1_r == e2_l): # no self loops after swap + if (e1_l == e2_r) or (e1_r == e2_l): # no self loops after swap continue - - if e2_r in neigh[e1_l] or e1_r in neigh[e2_l]: # no multi_edges after swap + if e2_r in neigh[e1_l] or e1_r in neigh[e2_l]: # no multi_edges after swap continue edges[index1, 1] = e2_r @@ -474,8 +509,6 @@ def undir_rewire_small(edges, n_rewire, is_mono): neigh[e1_r].append(e2_l) - - @njit(cache=True) def undir_rewire_large(edges, n_rewire, is_mono): """ @@ -500,9 +533,9 @@ def undir_rewire_large(edges, n_rewire, is_mono): for _ in range(n_rewire): index1 = np.random.randint(0, num_edges) index2 = np.random.randint(0, num_edges) - if index1==index2: + if index1 == index2: continue - e1_l, e1_r = edges[index1,:] + e1_l, e1_r = edges[index1, :] if is_mono: i2_1 = np.random.randint(0, 2) i2_2 = 1 - i2_1 @@ -511,15 +544,13 @@ def undir_rewire_large(edges, n_rewire, is_mono): else: e2_l, e2_r = edges[index2, :] - - if (e1_r == e2_r) or (e1_l == e2_l): # swap would do nothing + if (e1_r == e2_r) or (e1_l == e2_l): # swap would do nothing continue - if (e1_l == e2_r) or (e1_r == e2_l): # no self loops after swap + if (e1_l == e2_r) or (e1_r == e2_l): # no self loops after swap continue - - if e2_r in neigh[e1_l] or e1_r in neigh[e2_l]: # no multi_edges after swap + if e2_r in neigh[e1_l] or e1_r in neigh[e2_l]: # no multi_edges after swap continue edges[index1, 1] = e2_r @@ -531,12 +562,11 @@ def undir_rewire_large(edges, n_rewire, is_mono): del neigh[e2_l][e2_r] del neigh[e2_r][e2_l] - neigh[e1_l][e2_r]=1 - neigh[e2_r][e1_l]=1 - - neigh[e2_l][e1_r]=1 - neigh[e1_r][e2_l]=1 + neigh[e1_l][e2_r] = 1 + neigh[e2_r][e1_l] = 1 + neigh[e2_l][e1_r] = 1 + neigh[e1_r][e2_l] = 1 @njit(cache=True) @@ -555,7 +585,7 @@ def dir_create_successor_dict(edges): neigh = Dict() neigh[0] = List([-1]) del neigh[0] - for l,r in edges: + for l, r in edges: if l not in neigh: tmp = List([-1]) tmp.pop() @@ -564,7 +594,6 @@ def dir_create_successor_dict(edges): return neigh - @njit(cache=True) def dir_create_successor_dict_dict(edges): """Converts the edges into a dict which maps each node onto a list of its successors @@ -580,18 +609,19 @@ def dir_create_successor_dict_dict(edges): """ neigh = Dict() tmp = Dict() - tmp[0]=0 + tmp[0] = 0 neigh[0] = tmp del neigh[0] - for l,r in edges: + for l, r in edges: if l not in neigh: tmp = Dict() - tmp[r]=1 + tmp[r] = 1 del tmp[r] neigh[l] = tmp - neigh[l][r]=1 + neigh[l][r] = 1 return neigh + @njit(cache=True) def dir_create_predecessor_dict(edges): """Converts the edges into a dict which maps each node onto a list of its successors @@ -608,7 +638,7 @@ def dir_create_predecessor_dict(edges): neigh = Dict() neigh[0] = List([-1]) del neigh[0] - for r,l in edges: + for r, l in edges: if l not in neigh: tmp = List([-1]) tmp.pop() @@ -616,9 +646,10 @@ def dir_create_predecessor_dict(edges): neigh[l].append(r) return neigh + @njit(cache=True) def dir_rewire_large(edges, n_rewire, is_mono): - """ Rewires any bipartite network + """Rewires any bipartite network This is optimized for larger networks and uses a dictionary lookup to avoid multi edges """ @@ -631,33 +662,32 @@ def dir_rewire_large(edges, n_rewire, is_mono): triangle_flip_large(edges, succ) edge_index1 = np.random.randint(0, delta) edge_index2 = np.random.randint(0, delta) - if edge_index1==edge_index2: # same edge means self loop + if edge_index1 == edge_index2: # same edge means self loop continue - e1_l, e1_r = edges[edge_index1,:] - e2_l, e2_r = edges[edge_index2 ,:] + e1_l, e1_r = edges[edge_index1, :] + e2_l, e2_r = edges[edge_index2, :] - if (e1_r == e2_r) or (e1_l == e2_l): # swap would do nothing + if (e1_r == e2_r) or (e1_l == e2_l): # swap would do nothing continue - if (e1_l == e2_r) or (e1_r == e2_l): # no self loops after swab + if (e1_l == e2_r) or (e1_r == e2_l): # no self loops after swab continue - - if e2_r in succ[e1_l] or e1_r in succ[e2_l]: # no multiedge after swap + if e2_r in succ[e1_l] or e1_r in succ[e2_l]: # no multiedge after swap continue - edges[edge_index1, 1] = e2_r edges[edge_index2, 1] = e1_r del succ[e1_l][e1_r] del succ[e2_l][e2_r] - succ[e1_l][e2_r]=1 - succ[e2_l][e1_r]=1 + succ[e1_l][e2_r] = 1 + succ[e2_l][e1_r] = 1 + @njit(cache=True) def dir_rewire_small(edges, n_rewire, is_mono): - """ Rewires any bipartite network + """Rewires any bipartite network This is optimized for larger networks and uses a dictionary lookup to avoid multi edges """ @@ -670,22 +700,20 @@ def dir_rewire_small(edges, n_rewire, is_mono): triangle_flip_small(edges, succ) edge_index1 = np.random.randint(0, delta) edge_index2 = np.random.randint(0, delta) - if edge_index1==edge_index2: # same edge means self loop + if edge_index1 == edge_index2: # same edge means self loop continue - e1_l, e1_r = edges[edge_index1,:] - e2_l, e2_r = edges[edge_index2 ,:] + e1_l, e1_r = edges[edge_index1, :] + e2_l, e2_r = edges[edge_index2, :] - if (e1_r == e2_r) or (e1_l == e2_l): # swap would do nothing + if (e1_r == e2_r) or (e1_l == e2_l): # swap would do nothing continue - if (e1_l == e2_r) or (e1_r == e2_l): # no self loops after swab + if (e1_l == e2_r) or (e1_r == e2_l): # no self loops after swab continue - - if e2_r in succ[e1_l] or e1_r in succ[e2_l]: # no multiedge after swap + if e2_r in succ[e1_l] or e1_r in succ[e2_l]: # no multiedge after swap continue - edges[edge_index1, 1] = e2_r edges[edge_index2, 1] = e1_r @@ -695,10 +723,9 @@ def dir_rewire_small(edges, n_rewire, is_mono): succ[e2_l].append(e1_r) - @njit(cache=True) def dir_rewire_source_only_large(edges, source_nodes, n_rewire): - """ Rewires any bipartite network + """Rewires any bipartite network This is optimized for larger networks and uses a dictionary lookup to avoid multi edges """ @@ -713,23 +740,20 @@ def dir_rewire_source_only_large(edges, source_nodes, n_rewire): other_node = source_nodes[node_index] - e1_l, e1_r = edges[edge_index,:] + e1_l, e1_r = edges[edge_index, :] - if (e1_l == other_node) or (e1_r == other_node): # avoid creating self loop + if (e1_l == other_node) or (e1_r == other_node): # avoid creating self loop continue - if other_node in predecessors[e1_r]: # avoid creating multi-edge + if other_node in predecessors[e1_r]: # avoid creating multi-edge continue - edges[edge_index, 0] = other_node predecessors[e1_r].remove(e1_l) predecessors[e1_r].append(other_node) - - @njit(cache=True) def triangle_flip_small(edges, neigh): """ @@ -739,17 +763,17 @@ def triangle_flip_small(edges, neigh): num_edges = len(edges) index1 = np.random.randint(0, num_edges) index2 = np.random.randint(0, num_edges) - if index1==index2: + if index1 == index2: return - e1_l, e1_r = edges[index1,:] - e2_l, e2_r = edges[index2,:] + e1_l, e1_r = edges[index1, :] + e2_l, e2_r = edges[index2, :] if e1_r != e2_l: return index3 = np.random.randint(0, num_edges) - if index1==index3 or index3==index2: + if index1 == index3 or index3 == index2: return - e3_l, e3_r = edges[index3,:] + e3_l, e3_r = edges[index3, :] if e2_r != e3_l or e3_r != e1_l: return @@ -764,14 +788,14 @@ def triangle_flip_small(edges, neigh): neigh[e2_r].append(e2_l) neigh[e3_r].append(e3_l) - edges[index1,0] = e1_r - edges[index1,1] = e1_l + edges[index1, 0] = e1_r + edges[index1, 1] = e1_l - edges[index2,0] = e2_r - edges[index2,1] = e2_l + edges[index2, 0] = e2_r + edges[index2, 1] = e2_l - edges[index3,0] = e3_r - edges[index3,1] = e3_l + edges[index3, 0] = e3_r + edges[index3, 1] = e3_l @njit(cache=True) @@ -783,17 +807,17 @@ def triangle_flip_large(edges, neigh): num_edges = len(edges) index1 = np.random.randint(0, num_edges) index2 = np.random.randint(0, num_edges) - if index1==index2: + if index1 == index2: return - e1_l, e1_r = edges[index1,:] - e2_l, e2_r = edges[index2,:] + e1_l, e1_r = edges[index1, :] + e2_l, e2_r = edges[index2, :] if e1_r != e2_l: return index3 = np.random.randint(0, num_edges) - if index1==index3 or index3==index2: + if index1 == index3 or index3 == index2: return - e3_l, e3_r = edges[index3,:] + e3_l, e3_r = edges[index3, :] if e2_r != e3_l or e3_r != e1_l: return @@ -808,14 +832,15 @@ def triangle_flip_large(edges, neigh): neigh[e2_r][e2_l] = 1 neigh[e3_r][e3_l] = 1 - edges[index1,0] = e1_r - edges[index1,1] = e1_l + edges[index1, 0] = e1_r + edges[index1, 1] = e1_l + + edges[index2, 0] = e2_r + edges[index2, 1] = e2_l - edges[index2,0] = e2_r - edges[index2,1] = e2_l + edges[index3, 0] = e3_r + edges[index3, 1] = e3_l - edges[index3,0] = e3_r - edges[index3,1] = e3_l @njit(cache=True) def _get_block_indices(uids, is_dead): @@ -829,52 +854,53 @@ def _get_block_indices(uids, is_dead): n = 0 current_uid = uids[0] i = 0 - out = np.empty((len(uids),2), dtype=np.int64) + out = np.empty((len(uids), 2), dtype=np.int64) dead_out = np.zeros(len(uids), dtype=np.bool_) while i < len(is_dead): current_uid = uids[i] if is_dead[i]: - dead_out[n]=True - out[n,0]=i - i+=1 - while i < len(is_dead) and uids[i]==current_uid: - i+=1 - out[n,1] = i - n+=1 + dead_out[n] = True + out[n, 0] = i + i += 1 + while i < len(is_dead) and uids[i] == current_uid: + i += 1 + out[n, 1] = i + n += 1 return out[:n, :].copy(), dead_out[:n] -#def check_blocks(out_arr): + +# def check_blocks(out_arr): # block_lengths = out_arr[1:]-out_arr[0:len(out_arr)-1] # inds = block_lengths <= 1 # assert np.all(block_lengths>1), f"{block_lengths[inds]} {out_arr[1:][inds]}" -#@njit +# @njit def get_block_indices(edges_classes, dead_arrs): """Returns an arr that contains the start and end of blocks""" out = [] deads = [] for arr, dead_arr in zip(edges_classes.T, dead_arrs): - out_arr, dead_out_arr =_get_block_indices(arr, dead_arr) - #print(arr) - #print(dead_arr) - #c=45673 - #d=3 - #print(arr[c-d:c+d]) - #print(dead_arr[c-d:c+d]) - #print(out_arr) - - #check_blocks(out_arr) - #print(dead_arr.sum()+np.sum(out_arr[:,1]-out_arr[:,0])) - #print("block", np.sum(out_arr[:,1]-out_arr[:,0])) - #print(len(edges_classes)) + out_arr, dead_out_arr = _get_block_indices(arr, dead_arr) + # print(arr) + # print(dead_arr) + # c=45673 + # d=3 + # print(arr[c-d:c+d]) + # print(dead_arr[c-d:c+d]) + # print(out_arr) + + # check_blocks(out_arr) + # print(dead_arr.sum()+np.sum(out_arr[:,1]-out_arr[:,0])) + # print("block", np.sum(out_arr[:,1]-out_arr[:,0])) + # print(len(edges_classes)) out.append(out_arr) deads.append(dead_out_arr) - return out, deads + @njit(cache=True) def _set_seed(seed): """Set the need. This needs to be done within numba @njit function""" @@ -884,34 +910,49 @@ def _set_seed(seed): def get_flip_attempts_from_input(block, num_flip_attempts_in): """Converts an int or tuple of flip attempts values into a single number""" if isinstance(num_flip_attempts_in, (int, np.integer)): - num_edges = (block[:,1]-block[:,0]).ravel() + num_edges = (block[:, 1] - block[:, 0]).ravel() num_flip_attempts = num_flip_attempts_in * num_edges elif isinstance(num_flip_attempts_in, tuple): lower, upper = num_flip_attempts_in assert isinstance(lower, (int, np.integer)) assert isinstance(upper, (int, np.integer)) assert lower < upper - num_edges = (block[:,1]-block[:,0]).ravel() - num_flip_attempts = np.random.randint(lower * num_edges, upper*num_edges) + num_edges = (block[:, 1] - block[:, 0]).ravel() + num_flip_attempts = np.random.randint(lower * num_edges, upper * num_edges) + else: + raise ValueError("num_flip_attempts_in should be integer or tuple") return num_flip_attempts -#@njit -def rewire_fast(edges, edge_class, is_mono_color, block, is_directed, seed=None, num_flip_attempts_in=1, parallel=False): +# @njit +def rewire_fast( + edges, + edge_class, + is_mono_color, + block, + is_directed, + seed=None, + num_flip_attempts_in=1, + parallel=False, +): """This function rewires the edges in place thereby preserving the WL classes This function assumes edges to be ordered according to the classes """ # assumes edges to be ordered - if not seed is None: - _set_seed(seed) # numba seed - np.random.seed(seed) # numpy seed, seperate from numba seed + if seed is not None: + _set_seed(seed) # numba seed + np.random.seed(seed) # numpy seed, seperate from numba seed num_flip_attempts = get_flip_attempts_from_input(block, num_flip_attempts_in) if parallel: - return _rewire_fast_parallel(edges, edge_class, is_mono_color, block, is_directed, num_flip_attempts) + return _rewire_fast_parallel( + edges, edge_class, is_mono_color, block, is_directed, num_flip_attempts + ) else: - return _rewire_fast(edges, edge_class, is_mono_color, block, is_directed, num_flip_attempts) + return _rewire_fast( + edges, edge_class, is_mono_color, block, is_directed, num_flip_attempts + ) @njit(cache=True) @@ -928,17 +969,16 @@ def collect_nodes_by_color_class(partition): nodes_by_color[color].append(i) out = Dict() - out[0] = np.array([0], dtype=np.uint32) + out[0] = np.array([0], dtype=np.int32) del out[0] for key, value in nodes_by_color.items(): - arr = np.zeros(len(value), dtype=np.uint32) + arr = np.zeros(len(value), dtype=np.int32) for i in range(len(value)): # pylint: disable=consider-using-enumerate arr[i] = value[i] out[key] = arr return out - @njit(cache=True) def count_nodes_by_color_class(partition): """COunts all nodes in the same partition""" @@ -948,39 +988,129 @@ def count_nodes_by_color_class(partition): for i, color in enumerate(partition): if color not in nodes_by_color: nodes_by_color[color] = 0 - nodes_by_color[color]+=1 + nodes_by_color[color] += 1 return nodes_by_color - -#@njit -def dir_rewire_source_only_fast(edges, partition, block, seed=None, num_flip_attempts_in=1, parallel=False): +# @njit +def dir_rewire_source_only_fast( + edges, partition, block, seed=None, num_flip_attempts_in=1, parallel=False +): """This function rewires the edges in place thereby preserving the WL classes This function assumes edges to be ordered according to the classes """ # assumes edges to be ordered - if not seed is None: - _set_seed(seed) # numba seed - np.random.seed(seed) # numpy seed, seperate from numba seed + if seed is not None: + _set_seed(seed) # numba seed + np.random.seed(seed) # numpy seed, seperate from numba seed num_flip_attempts = get_flip_attempts_from_input(block, num_flip_attempts_in) nodes_by_class = collect_nodes_by_color_class(partition) if parallel: - return _dir_rewire_source_only_fast_parallel(edges, nodes_by_class, partition, block, num_flip_attempts) + return _dir_rewire_source_only_fast_parallel( + edges, nodes_by_class, partition, block, num_flip_attempts + ) else: - return _dir_rewire_source_only_fast(edges, nodes_by_class, partition, block, num_flip_attempts) + return _dir_rewire_source_only_fast( + edges, nodes_by_class, partition, block, num_flip_attempts + ) + + +def dir_sample_source_only_direct(edges, partition, block, seed=None): + """Perform direct sampling from the in-NeSt model""" + # assumes edges to be ordered + if seed is not None: + _set_seed(seed) # numba seed + np.random.seed(seed) # numpy seed, seperate from numba seed + + nodes_by_class = collect_nodes_by_color_class(partition) + _dir_sample_source_only_direct(edges, nodes_by_class, partition, block) + + +@njit +def count_in_degree(edges) -> Dict: + """Compute a dictionary of in degrees from edges""" + degree_counts = Dict() + for i in range(edges.shape[0]): + v = edges[i, 1] + if v in degree_counts: + degree_counts[v] += 1 + else: + degree_counts[v] = 1 + return degree_counts @njit(cache=True) -def _dir_rewire_source_only_fast(edges, nodes_by_class, partition, block, num_flip_attempts): - """Rewires only the source node i.e. u in u -> v +def _dir_sample_source_only_direct(edges, nodes_by_class, partition, block): + """Perform direct sampling of the in-NeSt model""" + + for i in range(len(block)): + lower = block[i, 0] + upper = block[i, 1] + node1 = edges[lower, 0] + source_nodes = nodes_by_class[partition[node1]] + degree_counts = count_in_degree(edges[lower:upper]) + n = 0 + for v, degree in degree_counts.items(): + tmp = sample_without_replacement(source_nodes, degree, avoid=v) + for u in tmp: + edges[n, 0] = u + edges[n, 1] = v + n += 1 + + +@njit +def sample_without_replacement(arr, k, avoid): + """Sample k values without replacement from arr avoiding to sample avoid + + Avoid is assumed to appear no more than once in arr. + + This mutates arr! + The algorithm used is a variant of the Fisher-Yates shuffle """ - #deltas=[] + n = len(arr) + + if k == len(arr): + return arr.copy() + if 2 * k <= n: + num_select = k # choose k elements and put them to the front + else: + num_select = n - k # choose n-k elements and put them to the front + # these elements will be excluded + j0 = n + for j in range(num_select): + val = j + np.random.randint(0, n - j) + if arr[val] == avoid: + arr[val] = arr[n - 1] + arr[n - 1] = avoid + n -= 1 # pretend the array is shorted + j0 = j + break + tmp = arr[j] + arr[j] = arr[val] + arr[val] = tmp + for j in range(j0, num_select): + val = j + np.random.randint(0, n - j) + tmp = arr[j] + arr[j] = arr[val] + arr[val] = tmp + if k <= n // 2: + return arr[:k].copy() + else: + return arr[k:n].copy() # return the included elements + + +@njit(cache=True) +def _dir_rewire_source_only_fast( + edges, nodes_by_class, partition, block, num_flip_attempts +): + """Rewires only the source node i.e. u in u -> v""" + # deltas=[] for i in range(len(block)): - lower = block[i,0] - upper = block[i,1] + lower = block[i, 0] + upper = block[i, 1] node1 = edges[lower, 0] source_nodes = nodes_by_class[partition[node1]] @@ -989,59 +1119,59 @@ def _dir_rewire_source_only_fast(edges, nodes_by_class, partition, block, num_fl @njit(parallel=True) -def _dir_rewire_source_only_fast_parallel(edges, nodes_by_class, partition, block, num_flip_attempts): +def _dir_rewire_source_only_fast_parallel( + edges, nodes_by_class, partition, block, num_flip_attempts +): # the next lines hack some "load balancing" - chunks = to_chunks(block[:,1]-block[:,0], get_num_threads()*10) - to_iter = np.arange(len(chunks)-1) - np.random.shuffle(to_iter) # randomly assign chunks to threads - - #print("parallel " + str(get_num_threads())) - for i_iter in prange(len(to_iter)): # pylint: disable=not-an-iterable - for i in range(chunks[to_iter[i_iter]], chunks[to_iter[i_iter]+1]): - #i = to_iter[u] - lower = block[i,0] - upper = block[i,1] + chunks = to_chunks(block[:, 1] - block[:, 0], get_num_threads() * 10) + to_iter = np.arange(len(chunks) - 1) + np.random.shuffle(to_iter) # randomly assign chunks to threads + + # print("parallel " + str(get_num_threads())) + for i_iter in prange(len(to_iter)): # pylint: disable=not-an-iterable + for i in range(chunks[to_iter[i_iter]], chunks[to_iter[i_iter] + 1]): + # i = to_iter[u] + lower = block[i, 0] + upper = block[i, 1] node1 = edges[lower, 0] source_nodes = nodes_by_class[partition[node1]] current_flips = num_flip_attempts[i] - dir_rewire_source_only_large(edges[lower:upper], source_nodes, current_flips) - - - - - + dir_rewire_source_only_large( + edges[lower:upper], source_nodes, current_flips + ) @njit(cache=True) def to_chunks(arr, n_chunks): """Chunks a given workload into approximately equally large chunks""" total = arr.sum() - per_chunk = max(total//n_chunks, 1) - chunks = np.zeros(n_chunks+2, dtype=np.uint32) - s=0 - i=0 - u=0 + per_chunk = max(total // n_chunks, 1) + chunks = np.zeros(n_chunks + 2, dtype=np.int32) + s = 0 + i = 0 + u = 0 while i < len(arr): - s=0 - u+=1 + s = 0 + u += 1 while s < per_chunk and i < len(arr): - s+=arr[i] - i+=1 + s += arr[i] + i += 1 chunks[u] = i - return chunks[:u+1] - + return chunks[: u + 1] @njit(cache=True) -def _rewire_fast(edges, edge_class, is_mono_color, block, is_directed, num_flip_attempts): - #deltas=[] +def _rewire_fast( + edges, edge_class, is_mono_color, block, is_directed, num_flip_attempts +): + # deltas=[] for i in range(len(block)): - lower = block[i,0] - upper = block[i,1] - block_size=upper-lower + lower = block[i, 0] + upper = block[i, 1] + block_size = upper - lower current_flips = num_flip_attempts[i] current_class = edge_class[lower] @@ -1060,21 +1190,28 @@ def _rewire_fast(edges, edge_class, is_mono_color, block, is_directed, num_flip_ else: undir_rewire_large(edges[lower:upper], current_flips, is_mono) + @njit(parallel=True) -def _rewire_fast_parallel(edges, edge_class, is_mono_color, block, is_directed, num_flip_attempts): +def _rewire_fast_parallel( + edges, edge_class, is_mono_color, block, is_directed, num_flip_attempts +): # the next lines hack some "load balancing" - chunks = to_chunks(block[:,1]-block[:,0], get_num_threads()*10) - to_iter = np.arange(len(chunks)-1) - np.random.shuffle(to_iter) # randomly assign chunks to threads + chunks = to_chunks(block[:, 1] - block[:, 0], get_num_threads() * 10) + to_iter = np.arange(len(chunks) - 1) + np.random.shuffle(to_iter) # randomly assign chunks to threads + base_seed = np.random.randint(0, np.iinfo(np.int32).max - 1) - #print("parallel " + str(get_num_threads())) + # print("parallel " + str(get_num_threads())) for i_iter in prange(len(to_iter)): # pylint: disable=not-an-iterable - for i in range(chunks[to_iter[i_iter]], chunks[to_iter[i_iter]+1]): - #i = to_iter[u] - lower = block[i,0] - upper = block[i,1] - block_size=upper-lower + chunk_id = to_iter[i_iter] + for i in range(chunks[chunk_id], chunks[chunk_id + 1]): + chunk_seed = np.bitwise_xor(base_seed, chunk_id) + _set_seed(chunk_seed) + # i = to_iter[u] + lower = block[i, 0] + upper = block[i, 1] + block_size = upper - lower current_flips = num_flip_attempts[i] current_class = edge_class[lower] is_mono = is_mono_color.get(current_class, False) diff --git a/nestmodel/fast_rewire2.py b/src/nestmodel/fast_rewire2.py similarity index 68% rename from nestmodel/fast_rewire2.py rename to src/nestmodel/fast_rewire2.py index f828546..2ccb1fc 100644 --- a/nestmodel/fast_rewire2.py +++ b/src/nestmodel/fast_rewire2.py @@ -1,33 +1,35 @@ import numpy as np -from numba.typed import List, Dict # pylint: disable=no-name-in-module +from numba.typed import List, Dict # pylint: disable=no-name-in-module from numba import njit @njit(cache=True) def _create_neighborhood_dict2(edges, offset): - if edges.shape[0]==0: + if edges.shape[0] == 0: raise ValueError("cannot handly empty edge sets") - if edges.shape[1]!=2: + if edges.shape[1] != 2: raise ValueError("edge set should be of shape n_edges x 2") neigh = Dict() - neigh[(edges[0,0], edges[0,0])] = offset - del neigh[(edges[0,0], edges[0,0])] + neigh[(edges[0, 0], edges[0, 0])] = offset + del neigh[(edges[0, 0], edges[0, 0])] - for i, (l,r) in enumerate(edges): + for i, (l, r) in enumerate(edges): if (l, r) not in neigh: neigh[(l, r)] = i + offset return neigh - def get_subgraphs(G, depth): """Returns a List of subgraphs for graph G and depth""" if depth >= len(G.block_indices): - raise ValueError(f"depth of {depth} is larger than depth in Graph ({len(G.block_indices)})") + raise ValueError( + f"depth of {depth} is larger than depth in Graph ({len(G.block_indices)})" + ) assert G.edges is G.edges_ordered - return _get_subgraphs(G.edges, G.block_indices[depth], G.edges_classes[:,depth], G.is_mono[depth]) - + return _get_subgraphs( + G.edges, G.block_indices[depth], G.edges_classes[:, depth], G.is_mono[depth] + ) @njit(cache=True) @@ -38,8 +40,8 @@ def _get_subgraphs(edges, blocks, edges_classes, is_mono): subgraphs = List() for block in blocks: edge_range = (block[0], block[1]) - neigh_dict = _create_neighborhood_dict2(edges[block[0]:block[1],:], block[0]) - list_of_nodes = np.unique(edges[block[0]:block[1],:]) + neigh_dict = _create_neighborhood_dict2(edges[block[0] : block[1], :], block[0]) + list_of_nodes = np.unique(edges[block[0] : block[1], :]) is_mono_val = False if edges_classes[block[0]] in is_mono: @@ -50,7 +52,6 @@ def _get_subgraphs(edges, blocks, edges_classes, is_mono): return subgraphs - @njit(cache=True) def normal_flip_subgraphs(edge_list, subgraphs, is_directed): """Perform the normal edge flip @@ -58,12 +59,15 @@ def normal_flip_subgraphs(edge_list, subgraphs, is_directed): """ subgraph_id = np.random.randint(0, len(subgraphs)) (target_start_id, target_end_id), edge_dict, _, is_mono = subgraphs[subgraph_id] - return normal_flip(edge_list, target_start_id, target_end_id, edge_dict, is_directed, is_mono) - + return normal_flip( + edge_list, target_start_id, target_end_id, edge_dict, is_directed, is_mono + ) @njit(cache=True) -def normal_flip(edge_list, target_start_id, target_end_id, edge_dict, is_directed, is_mono): +def normal_flip( + edge_list, target_start_id, target_end_id, edge_dict, is_directed, is_mono +): """Perform a normal edge flip on a specific subgraph Therefor choose two edges from the subgraph edges """ @@ -71,7 +75,7 @@ def normal_flip(edge_list, target_start_id, target_end_id, edge_dict, is_directe second_edge_id = np.random.randint(target_start_id, target_end_id) if first_edge_id == second_edge_id: - #print("same edges") + # print("same edges") return False u1_o, v1_o = edge_list[first_edge_id] @@ -80,27 +84,33 @@ def normal_flip(edge_list, target_start_id, target_end_id, edge_dict, is_directe v1 = v1_o if not is_directed: - if u1 == u2 or u1 == v2 or v1 == u2 or v1 == v2:# assuming u1 != v1 and u2 != v2 - #print("same nodes", u1, u2, v1, v2, first_edge_id, second_edge_id) + if ( + u1 == u2 or u1 == v2 or v1 == u2 or v1 == v2 + ): # assuming u1 != v1 and u2 != v2 + # print("same nodes", u1, u2, v1, v2, first_edge_id, second_edge_id) return False if is_mono and np.random.randint(2) == 1: - u1, v1 = v1, u1 # reverse direction of edge - - if (u1, v2) in edge_dict or (v2, u1) in edge_dict or \ - (u2, v1) in edge_dict or (v1, u2) in edge_dict: - #print("other edge already present") + u1, v1 = v1, u1 # reverse direction of edge + + if ( + (u1, v2) in edge_dict + or (v2, u1) in edge_dict + or (u2, v1) in edge_dict + or (v1, u2) in edge_dict + ): + # print("other edge already present") return False else: if u1 == u2 or v1 == v2: - #print("same nodes") + # print("same nodes") return False if (u1, v2) in edge_dict or (u2, v1) in edge_dict: - #print("other edge already present 2") + # print("other edge already present 2") return False - #print("old", (u1_o, v1_o), (u2, v2)) - #print("new", (u1, v2), (u2, v1)) + # print("old", (u1_o, v1_o), (u2, v2)) + # print("new", (u1, v2), (u2, v1)) del edge_dict[(u1_o, v1_o)] del edge_dict[(u2, v2)] @@ -112,13 +122,14 @@ def normal_flip(edge_list, target_start_id, target_end_id, edge_dict, is_directe return True - @njit(cache=True) def triangle_flip(edge_list, subgraphs): """Choose a subgraph and if the subgraph is mono, flip the subgraph""" subgraph_index = np.random.randint(0, len(subgraphs)) - (target_start_id, target_end_id), edge_dict, list_of_nodes, is_mono = subgraphs[subgraph_index] - if is_mono: # mono colored graph + (target_start_id, target_end_id), edge_dict, list_of_nodes, is_mono = subgraphs[ + subgraph_index + ] + if is_mono: # mono colored graph first_edge_id = np.random.randint(target_start_id, target_end_id) third_node_id = np.random.randint(0, len(list_of_nodes)) u1, u2 = edge_list[first_edge_id] @@ -128,7 +139,6 @@ def triangle_flip(edge_list, subgraphs): return False - @njit(cache=True) def _triangle_flip(edge_list, edge_dict, u1, u2, u3): """Flip the direction of the potential triangle u1->u2->u3->u1""" @@ -146,8 +156,12 @@ def _triangle_flip(edge_list, edge_dict, u1, u2, u3): e2_n = (u2, u1) e3_n = (u1, u3) - if e1_o in edge_dict and e2_o in edge_dict and e3_o in edge_dict and not ( - e1_n in edge_dict or e2_n in edge_dict or e3_n in edge_dict): + if ( + e1_o in edge_dict + and e2_o in edge_dict + and e3_o in edge_dict + and not (e1_n in edge_dict or e2_n in edge_dict or e3_n in edge_dict) + ): index1 = edge_dict[e1_o] index2 = edge_dict[e2_o] index3 = edge_dict[e3_o] @@ -168,41 +182,37 @@ def _triangle_flip(edge_list, edge_dict, u1, u2, u3): return False - @njit(cache=True) def _sg_flip_directed(edges, subgraphs, n_rewire): - n=0 - t=0 + n = 0 + t = 0 for _ in range(n_rewire): - n+=normal_flip_subgraphs(edges, subgraphs, is_directed=True) - t+=triangle_flip(edges, subgraphs) + n += normal_flip_subgraphs(edges, subgraphs, is_directed=True) + t += triangle_flip(edges, subgraphs) return n, t - @njit(cache=True) def _sg_flip_undirected(edges, subgraphs, n_rewire): - n=0 + n = 0 for _ in range(n_rewire): - n+=normal_flip_subgraphs(edges, subgraphs, is_directed=False) + n += normal_flip_subgraphs(edges, subgraphs, is_directed=False) return n - @njit(cache=True) def _set_seed(seed): """Set the need. This needs to be done within numba @njit function""" np.random.seed(seed) - def fg_rewire_nest(G, depth, n_rewire, seed=None): """Rewire the graph G for n_rewire steps by using WL colors of depth d""" - if not seed is None: + if seed is not None: _set_seed(seed) subgraphs = get_subgraphs(G, depth) - if len(subgraphs)==0: + if len(subgraphs) == 0: print("Nothing to rewire") return 0, 0 if G.is_directed: diff --git a/nestmodel/fast_wl.py b/src/nestmodel/fast_wl.py similarity index 54% rename from nestmodel/fast_wl.py rename to src/nestmodel/fast_wl.py index df71281..659cdf4 100644 --- a/nestmodel/fast_wl.py +++ b/src/nestmodel/fast_wl.py @@ -1,51 +1,47 @@ # pylint: disable=consider-using-enumerate from numba import njit -from numba.types import uint32, uint64 +from numba.types import int16, int32, int64 import numpy as np from nestmodel.colorings import make_labeling_compact, RefinementColors from nestmodel.wl_nlogn import color_refinement_nlogn - - @njit(cache=True) def primesfrom2to(n): - """ Input n>=6, Returns an array of primes, 2 <= p < n + """Input n>=6, Returns an array of primes, 2 <= p < n taken from Stackoverflow """ - size = int(n//3 + (n%6==2)) + size = int(n // 3 + (n % 6 == 2)) sieve = np.ones(size, dtype=np.bool_) - for i in range(1,int(n**0.5)//3+1): + for i in range(1, int(n**0.5) // 3 + 1): if sieve[i]: - k=3*i+1|1 - sieve[ k*k//3 ::2*k] = False - sieve[k*(k-2*(i&1)+4)//3::2*k] = False - arr = (3*np.nonzero(sieve)[0][1:]+1) | 1 - output = np.empty(len(arr)+2, dtype=arr.dtype) - output[0]=2 - output[1]=3 - output[2:]=arr + k = 3 * i + 1 | 1 + sieve[k * k // 3 :: 2 * k] = False + sieve[k * (k - 2 * (i & 1) + 4) // 3 :: 2 * k] = False + arr = (3 * np.nonzero(sieve)[0][1:] + 1) | 1 + output = np.empty(len(arr) + 2, dtype=arr.dtype) + output[0] = 2 + output[1] = 3 + output[2:] = arr return output - -@njit([(uint32[:], uint32), (uint64[:], uint64)],cache=True) +@njit([(int16[:], int32), (int32[:], int32), (int64[:], int64)], cache=True) def my_bincount(arr, min_lenght): """The same as numpy bincount, but works on unsigned integers as well""" if min_lenght <= 0: - m=arr.max() # pragma: no cover + m = arr.max() # pragma: no cover else: m = min_lenght - out = np.zeros(m+1, dtype=arr.dtype) + out = np.zeros(m + 1, dtype=arr.dtype) for i in range(len(arr)): - out[arr[i]]+=np.uint32(1) + out[arr[i]] += np.int32(1) return out - -@njit([(uint32[:,:], uint32), (uint64[:,:], uint64)], cache=True) +@njit([(int16[:, :], int32), (int32[:, :], int32), (int64[:, :], int64)], cache=True) def to_in_neighbors(edges, num_nodes): - """ transforms the edges into two arrays the first arrays indicates ranges into the second array + """transforms the edges into two arrays the first arrays indicates ranges into the second array the second array contains the in neighbors of those nodes indicated in array 1 input : takes in edges in the format a -> b (first column a, second column b) @@ -56,32 +52,39 @@ def to_in_neighbors(edges, num_nodes): starting_positions[i] ..starting_positions[i+1] contains all in neighbors of node i in_neighbors """ - if num_nodes ==0: + if num_nodes == 0: num_nodes = edges.ravel().max() else: assert num_nodes > 0 num_nodes -= 1 - in_degrees = my_bincount(edges[:,1].ravel(), min_lenght=np.uint32(num_nodes)) + in_degrees = my_bincount(edges[:, 1].ravel(), min_lenght=np.int32(num_nodes)) # starting_positions[i] ..starting_positions[i+1] contains all in neighbors of node i - starting_positions = np.empty(in_degrees.shape[0]+1, dtype=np.uint32) - starting_positions[0]=0 + starting_positions = np.empty(in_degrees.shape[0] + 1, dtype=np.int32) + starting_positions[0] = 0 starting_positions[1:] = in_degrees.cumsum() current_index = starting_positions.copy() - in_neighbors = np.zeros(edges.shape[0], dtype=np.uint32) + in_neighbors = np.zeros(edges.shape[0], dtype=np.int32) for i in range(edges.shape[0]): - l = edges[i,0] - r = edges[i,1] + l = edges[i, 0] + r = edges[i, 1] in_neighbors[current_index[r]] = l current_index[r] += 1 return starting_positions, in_neighbors, in_degrees.max() - -def WL_fast(edges, num_nodes : int = None, labels = None, max_iter : int = None, return_all=False, method="normal"): +def WL_fast( + edges, + num_nodes: int = None, + labels=None, + max_iter: int = None, + return_all=False, + method="normal", + compact=True, +): """Computes the in-WL very fast for the input edges edges : array like, shape (num_edges, 2) indicates the graph as a set of directed edges @@ -95,42 +98,44 @@ def WL_fast(edges, num_nodes : int = None, labels = None, max_iter : int = None, runtime is O(max_depth * (E + log(N)N) ) memory requirement is O(E + max_depth * N) """ - assert edges.dtype==np.uint32 or edges.dtype==np.uint64 - if not labels is None: - assert labels.dtype==np.uint32 or labels.dtype==np.uint64 + assert edges.dtype == np.int16 or edges.dtype == np.int32 or edges.dtype == np.int64 + if labels is not None: + assert labels.dtype == np.int32 or labels.dtype == np.int64 assert method in ("normal", "nlogn") if num_nodes is None: - num_nodes = int(edges.max()+1) + num_nodes = int(edges.max() + 1) if max_iter is None: max_iter = num_nodes - if max_iter <=0: + if max_iter <= 0: raise ValueError("Need at least max_iter/max_depth of 1") if labels is None: - labels = np.zeros(num_nodes, dtype=np.uint32) + labels = np.zeros(num_nodes, dtype=np.int32) else: labels = np.array(labels.copy(), dtype=labels.dtype) make_labeling_compact(labels) - out = [labels] if max_iter == 1: return out edges2 = np.empty_like(edges) - edges2[:,0] = edges[:,1] - edges2[:,1] = edges[:,0] + edges2[:, 0] = edges[:, 1] + edges2[:, 1] = edges[:, 0] startings, neighbors, _ = to_in_neighbors(edges2, num_nodes) if method == "normal": - labelings, order, partitions = _wl_fast2(startings, neighbors, labels.copy(), max_iter) + labelings, order, partitions = _wl_fast2( + startings, neighbors, labels.copy(), max_iter + ) for labeling in labelings: make_labeling_compact(labeling) - out.extend(labelings[:-1]) # the stable color is doubled, so omit it + out.extend(labelings[:-1]) # the stable color is doubled, so omit it ref_colors = RefinementColors(partitions, order=order) else: - undo_order, partitions = color_refinement_nlogn(startings, neighbors, labels.copy()) + undo_order, partitions = color_refinement_nlogn( + startings, neighbors, labels.copy() + ) ref_colors = RefinementColors(partitions, undo_order=undo_order) - out = list(ref_colors.get_colors_all_depths()) - + out = list(ref_colors.get_colors_all_depths(compact=compact)) if return_all: return out, ref_colors.order, partitions @@ -138,48 +143,51 @@ def WL_fast(edges, num_nodes : int = None, labels = None, max_iter : int = None, return out - -@njit([(uint32[:],uint32[:]), (uint64[:], uint64[:])], locals={'num_entries': uint32}, cache=True) +@njit( + [(int32[:], int32[:]), (int64[:], int64[:])], + locals={"num_entries": int32}, + cache=True, +) def injective_combine(labels1, labels2): """Combine two labelings to create a new labeling that respects both labelings""" -# assert labels1.dtype==np.uint32 -# assert labels2.dtype==np.uint32 - assert len(labels1)==len(labels2) + # assert labels1.dtype==np.int32 + # assert labels2.dtype==np.int32 + assert len(labels1) == len(labels2) - out_labels= np.empty_like(labels1) - d = {(labels1[0], labels1[0]): labels1[0]}#(0,0):0} + out_labels = np.empty_like(labels1) + d = {(labels1[0], labels1[0]): labels1[0]} # (0,0):0} del d[(labels1[0], labels1[0])] num_entries = 0 for i in range(len(labels1)): a = labels1[i] b = labels2[i] - x = (a,b) + x = (a, b) if x in d: - out_labels[i]=d[x] + out_labels[i] = d[x] else: - out_labels[i]=num_entries - d[x]=num_entries - num_entries+=1 + out_labels[i] = num_entries + d[x] = num_entries + num_entries += 1 return out_labels, len(d) - -def WL_both(edges, num_nodes=None, labels = None, max_iter = None): # pylint:disable=invalid-name - """A very simple implementation of WL both - """ - assert edges.dtype==np.uint32 or edges.dtype==np.uint64 - if not labels is None: - assert labels.dtype==np.uint32 +def WL_both( + edges, num_nodes=None, labels=None, max_iter=None +): # pylint:disable=invalid-name + """A very simple implementation of WL both""" + assert edges.dtype == np.int32 or edges.dtype == np.int64 + if labels is not None: + assert labels.dtype == np.int32 if max_iter is None: - max_iter=201 # + max_iter = 201 # if num_nodes is None: - num_nodes = int(edges.max()+1) - if max_iter <=0: + num_nodes = int(edges.max() + 1) + if max_iter <= 0: raise ValueError("Need at least max_iter/max_depth of 1") out = [] if labels is None: - labels = np.zeros(num_nodes, dtype=np.uint32) + labels = np.zeros(num_nodes, dtype=np.int32) else: labels = np.array(labels.copy(), dtype=labels.dtype) make_labeling_compact(labels) @@ -189,20 +197,20 @@ def WL_both(edges, num_nodes=None, labels = None, max_iter = None): # pylint:dis return out edges2 = np.empty_like(edges) - edges2[:,0] = edges[:,1] - edges2[:,1] = edges[:,0] + edges2[:, 0] = edges[:, 1] + edges2[:, 1] = edges[:, 0] startings, neighbors, _ = to_in_neighbors(edges2, num_nodes) startings2, neighbors2, _ = to_in_neighbors(edges, num_nodes) last_num_colors = len(np.unique(labels)) - labelings=[] + labelings = [] for _ in range(max_iter): - labelings1, _, _ = _wl_fast2(startings, neighbors, labels.copy(), 1) + labelings1, _, _ = _wl_fast2(startings, neighbors, labels.copy(), 1) labelings2, _, _ = _wl_fast2(startings2, neighbors2, labels.copy(), 1) labels, num_colors = injective_combine(labelings1[-1], labelings2[-1]) labelings.append(labels) - if last_num_colors==num_colors: + if last_num_colors == num_colors: break - last_num_colors=num_colors + last_num_colors = num_colors for labeling in labelings: make_labeling_compact(labeling) @@ -211,24 +219,21 @@ def WL_both(edges, num_nodes=None, labels = None, max_iter = None): # pylint:dis return out - - - - - @njit(cache=True) def is_sorted_fast(vals, order): """Checks whether the values in vals are sorted ascedingly when using order""" last_val = vals[order[0]] for i in range(1, len(order)): - if vals[order[i]]=6: - ln=np.log - n=num_nodes - correction = np.ceil(ln(n)+ln(ln(n))) + assert len(labels) == num_nodes + if num_nodes >= 6: + ln = np.log + n = num_nodes + correction = np.ceil(ln(n) + ln(ln(n))) else: correction = 5 - primes = primesfrom2to(num_nodes*correction) + primes = primesfrom2to(num_nodes * correction) log_primes = np.log(primes) deltas = log_primes.copy() @@ -262,17 +267,17 @@ def _wl_fast2(startings, neighbors, labels, max_iter=201): vals = np.empty(num_nodes, dtype=np.float64) partitions = np.empty(num_nodes + 1, dtype=labels.dtype) - out_partitions = np.empty((num_nodes,3), dtype=labels.dtype) - if np.all(labels==0): + out_partitions = np.empty((num_nodes, 3), dtype=labels.dtype) + if np.all(labels == 0): order = np.arange(num_nodes) partitions[0] = 0 partitions[1] = num_nodes - out_partitions[0,0] = 0 - out_partitions[0,1] = num_nodes - out_partitions[0,2] = 0 + out_partitions[0, 0] = 0 + out_partitions[0, 1] = num_nodes + out_partitions[0, 2] = 0 num_colors = 1 total_num_colors = 1 - vals[:]=1 + vals[:] = 1 else: order = np.argsort(labels) partitions[0] = 0 @@ -285,21 +290,22 @@ def _wl_fast2(startings, neighbors, labels, max_iter=201): if labels[node_id] != last_old_label: last_old_label = labels[node_id] partitions[num_colors] = i - out_partitions[total_num_colors,0] = last_new_label - out_partitions[total_num_colors,1] = i - out_partitions[total_num_colors,2] = 0 + out_partitions[total_num_colors, 0] = last_new_label + out_partitions[total_num_colors, 1] = i + out_partitions[total_num_colors, 2] = 0 num_colors += 1 - total_num_colors +=1 + total_num_colors += 1 last_new_label = i - labels[node_id]=last_new_label - vals[node_id] = labels[node_id] + 1 # vals cannot be zero, so start counting from one + labels[node_id] = last_new_label + vals[node_id] = ( + labels[node_id] + 1 + ) # vals cannot be zero, so start counting from one partitions[num_colors] = len(order) - out_partitions[total_num_colors,0] = last_new_label - out_partitions[total_num_colors,1] = len(order) - out_partitions[total_num_colors,2] = 0 - total_num_colors+=1 - + out_partitions[total_num_colors, 0] = last_new_label + out_partitions[total_num_colors, 1] = len(order) + out_partitions[total_num_colors, 2] = 0 + total_num_colors += 1 last_num_colors = num_colors out = [] @@ -308,21 +314,25 @@ def _wl_fast2(startings, neighbors, labels, max_iter=201): for depth in range(max_iter): # propagate label aka deltas to neighboring nodes - for index in range(num_updates):# loop over all nodes that changed in last iter + for index in range( + num_updates + ): # loop over all nodes that changed in last iter i = order_updates[index] lb = startings[i] - ub = startings[i+1] - for j in range(lb, ub): # propagate label of i to neighbor j - vals[neighbors[j]]+=deltas[labels[i]] + ub = startings[i + 1] + for j in range(lb, ub): # propagate label of i to neighbor j + vals[neighbors[j]] += deltas[labels[i]] # sort partitions such that the same values come after one another for i in range(num_colors): lb = partitions[i] - ub = partitions[i+1] + ub = partitions[i + 1] - if ub <= 1 + lb: # only need sorting if more than one node is involved + if ub <= 1 + lb: # only need sorting if more than one node is involved continue - if is_sorted_fast(vals, order[lb:ub]): # no need to do any sorting if already sorted + if is_sorted_fast( + vals, order[lb:ub] + ): # no need to do any sorting if already sorted continue partition_order = np.argsort(vals[order[lb:ub]]) order[lb:ub] = order[lb:ub][partition_order] @@ -331,46 +341,47 @@ def _wl_fast2(startings, neighbors, labels, max_iter=201): last_index = 0 num_updates = 0 num_new_colors = 0 - last_changed = -1 # need to keep track of last changed color + last_changed = -1 # need to keep track of last changed color last_val = vals[order[0]] for i in range(len(order)): node_id = order[i] val = vals[node_id] - if val != last_val:# yes we are using equality for floats, - # but floats which were obtained from sorted operations - # so no issues with a+b+c != b+c+a + if val != last_val: # yes we are using equality for floats, + # but floats which were obtained from sorted operations + # so no issues with a+b+c != b+c+a if num_new_colors > 0: - out_partitions[total_num_colors-1,1] = i - num_new_colors-=1 - if labels[node_id] != i: # create out partition if necessary - out_partitions[total_num_colors,0] = i - out_partitions[total_num_colors,2] = depth+1 - total_num_colors+=1 - num_new_colors+=1 + out_partitions[total_num_colors - 1, 1] = i + num_new_colors -= 1 + if labels[node_id] != i: # create out partition if necessary + out_partitions[total_num_colors, 0] = i + out_partitions[total_num_colors, 2] = depth + 1 + total_num_colors += 1 + num_new_colors += 1 last_index = i - partitions[num_colors]=i # create new partition + partitions[num_colors] = i # create new partition num_colors += 1 last_val = val - deltas[last_index] = log_primes[last_index]-log_primes[labels[node_id]] + deltas[last_index] = ( + log_primes[last_index] - log_primes[labels[node_id]] + ) - - if labels[node_id] != last_index: # there is a need for updates + if labels[node_id] != last_index: # there is a need for updates last_changed = i - order_updates[num_updates]=node_id - num_updates+=1 - vals[node_id] += last_index-labels[node_id] + order_updates[num_updates] = node_id + num_updates += 1 + vals[node_id] += last_index - labels[node_id] labels[node_id] = last_index partitions[num_colors] = len(order) if num_new_colors > 0: - if last_changed>=0: - out_partitions[total_num_colors-1,1] = last_changed+1 + if last_changed >= 0: + out_partitions[total_num_colors - 1, 1] = last_changed + 1 else: - out_partitions[total_num_colors-1,1] = len(order) + out_partitions[total_num_colors - 1, 1] = len(order) out.append(labels.copy()) @@ -379,5 +390,4 @@ def _wl_fast2(startings, neighbors, labels, max_iter=201): else: last_num_colors = num_colors - return out, order, out_partitions[0:total_num_colors, :] diff --git a/nestmodel/graph_properties.py b/src/nestmodel/graph_properties.py similarity index 59% rename from nestmodel/graph_properties.py rename to src/nestmodel/graph_properties.py index c085122..48ec320 100644 --- a/nestmodel/graph_properties.py +++ b/src/nestmodel/graph_properties.py @@ -2,54 +2,60 @@ from nestmodel.fast_rewire import count_nodes_by_color_class import numpy as np from numba import njit +from collections import defaultdict +from itertools import product, combinations + @njit def __compute_degrees(E, start, stop, d_source, d_target): """Increments the degree of edges in [start,stop)""" for i in range(start, stop): - u = E[i,0] - v = E[i,1] - d_source[u]+=1 - d_target[v]+=1 + u = E[i, 0] + v = E[i, 1] + d_source[u] += 1 + d_target[v] += 1 @njit def __reset_degrees(E, start, stop, d_source, d_target): """Resets all degrees of edges in [start,stop) to zero""" for i in range(start, stop): - u = E[i,0] - v = E[i,1] - d_source[u]=0 - d_target[v]=0 + u = E[i, 0] + v = E[i, 1] + d_source[u] = 0 + d_target[v] = 0 + @njit(cache=True) def sum_degrees_at_endpoints_both(E, start, stop, d_source, d_target): out = np.int64(0) __compute_degrees(E, start, stop, d_source, d_target) for i in range(start, stop): - u = E[i,0] - v = E[i,1] + u = E[i, 0] + v = E[i, 1] out += np.int64(d_source[u]) out += np.int64(d_target[v]) __reset_degrees(E, start, stop, d_source, d_target) return out + @njit(cache=True) -def sum_degrees_at_endpoints(E, num_nodes, block_indices, partition, source=True, target=True, is_directed=True): +def sum_degrees_at_endpoints( + E, num_nodes, block_indices, partition, source=True, target=True, is_directed=True +): """For each edge sums the degree of the enpoints in that particular block""" buf_source = np.zeros(num_nodes, dtype=np.int32) buf_target = np.zeros(num_nodes, dtype=np.int32) d_source = buf_source - out = np.int64(0) for i in range(len(block_indices)): - - start = block_indices[i,0] - stop = block_indices[i,1] - is_mono = partition[E[start,0]]==partition[E[start,1]] - #print("mono", is_mono) - #print(E[start:stop,:]) + + start = block_indices[i, 0] + stop = block_indices[i, 1] + is_mono = partition[E[start, 0]] == partition[E[start, 1]] + # print("mono", is_mono) + # print(E[start:stop,:]) if is_directed: d_target = buf_target else: @@ -58,60 +64,64 @@ def sum_degrees_at_endpoints(E, num_nodes, block_indices, partition, source=True d_target = d_source else: d_target = buf_target - + __compute_degrees(E, start, stop, d_source, d_target) for i in range(start, stop): - u = E[i,0] - v = E[i,1] + u = E[i, 0] + v = E[i, 1] if source: out += np.int64(d_source[u]) if target: out += np.int64(d_target[v]) __reset_degrees(E, start, stop, d_source, d_target) - #print(out) + # print(out) return out + @njit(cache=True) def count_mono_color_edges(E, colors, block_indices): """Counts the number of edges where source and target have the same color""" num_mono = np.int64(0) for i in range(len(block_indices)): - start = block_indices[i,0] - stop = block_indices[i,1] + start = block_indices[i, 0] + stop = block_indices[i, 1] for i in range(start, stop): - u = E[i,0] - v = E[i,1] - if colors[u]==colors[v]: - num_mono+=np.int64(1) + u = E[i, 0] + v = E[i, 1] + if colors[u] == colors[v]: + num_mono += np.int64(1) return num_mono + @njit(cache=True) def count_source_node_per_block(E, partition, number_of_nodes_by_class, block_indices): """Counts the number of nodes with the same color as the source for each block""" num_nodes = np.empty(len(block_indices), dtype=np.int64) for i in range(len(block_indices)): - start = block_indices[i,0] - stop = block_indices[i,1] - source_node = E[start,0] - target_node = E[start,1] + start = block_indices[i, 0] + # stop = block_indices[i, 1] + source_node = E[start, 0] + # target_node = E[start, 1] source_color = partition[source_node] - target_color = partition[source_node] + # target_color = partition[source_node] num_other_nodes_with_color = number_of_nodes_by_class[np.int64(source_color)] num_nodes[i] = num_other_nodes_with_color return num_nodes + @njit(cache=True) def block_sizes(block_indices): """Computes an array that contains the sizes of each block""" out_block_sizes = np.empty(len(block_indices), dtype=np.int64) for i in range(len(block_indices)): - start = block_indices[i,0] - stop = block_indices[i,1] - out_block_sizes[i]=stop-start + start = block_indices[i, 0] + stop = block_indices[i, 1] + out_block_sizes[i] = stop - start return out_block_sizes -def number_of_flips_possible(G : FastGraph, kind = "normal"): + +def number_of_flips_possible(G: FastGraph, kind="normal"): """Computes the number of valid 1-hop flips for G G : FastGraph kind : ['normal', 'source_only'] how the flips are performed @@ -123,8 +133,7 @@ def number_of_flips_possible(G : FastGraph, kind = "normal"): num_rounds = len(G.block_indices) out = np.zeros(num_rounds, dtype=np.int64) - - if kind=="normal": + if kind == "normal": for d in range(num_rounds): out[d] = _normal_number_of_flips_possible(G, d) elif kind == "source_only": @@ -135,147 +144,156 @@ def number_of_flips_possible(G : FastGraph, kind = "normal"): return out +def _normal_number_of_flips_possible(G: FastGraph, d: int): -def _normal_number_of_flips_possible(G : FastGraph, d: int): - - allowed_flips = allowed_flips_quad_undir(G.edges, G.num_nodes, G.block_indices[d], G.base_partitions[d], G.is_directed) + allowed_flips = allowed_flips_quad_undir( + G.edges, G.num_nodes, G.block_indices[d], G.base_partitions[d], G.is_directed + ) return allowed_flips - -def _source_only_number_of_flips_possible(G : FastGraph, d: int, num_nodes_by_class =None): +def _source_only_number_of_flips_possible( + G: FastGraph, d: int, num_nodes_by_class=None +): """Compute the number of flips for a directed graph and the source only strategy""" - degrees_sum = sum_degrees_at_endpoints(G.edges, G.num_nodes, G.block_indices[d], G.base_partitions[d], source=False) + degrees_sum = sum_degrees_at_endpoints( + G.edges, G.num_nodes, G.block_indices[d], G.base_partitions[d], source=False + ) partition = G.base_partitions[d] if num_nodes_by_class is None: num_nodes_by_class = count_nodes_by_color_class(partition) - num_nodes = count_source_node_per_block(G.edges, partition, num_nodes_by_class, G.block_indices[d]) - ##print("partitions", G.base_partitions) - #print(G.block_indices[d]) + num_nodes = count_source_node_per_block( + G.edges, partition, num_nodes_by_class, G.block_indices[d] + ) + # print("partitions", G.base_partitions) + # print(G.block_indices[d]) num_edges = block_sizes(G.block_indices[d]) - #print(num_nodes) - #print(num_edges) + # print(num_nodes) + # print(num_edges) block_squared_sum = np.inner(num_nodes, num_edges) num_mono_colored = count_mono_color_edges(G.edges, partition, G.block_indices[d]) - #print(block_squared_sum) - ##print("degrees_sum", degrees_sum) - #print(num_mono_colored) - final_value = (int(block_squared_sum) - int(degrees_sum) - int(num_mono_colored)) + # print(block_squared_sum) + # print("degrees_sum", degrees_sum) + # print(num_mono_colored) + final_value = int(block_squared_sum) - int(degrees_sum) - int(num_mono_colored) return final_value -from collections import defaultdict -from itertools import product, combinations - def allowed_flips_quad_undir(edges, num_nodes, block_indices, partition, is_directed): num_allowed = 0 buf_source = np.zeros(num_nodes, dtype=np.int32) buf_target = np.zeros(num_nodes, dtype=np.int32) - #print() - for i,j in block_indices: - u = edges[i,0] - v = edges[i,1] - #print() - if partition[u]==partition[v]: - #print("mono") - total_pairs = (j-i)*(j-i) # total num pairs - #print("total", total_pairs) + # print() + for i, j in block_indices: + u = edges[i, 0] + v = edges[i, 1] + # print() + if partition[u] == partition[v]: + # print("mono") + total_pairs = (j - i) * (j - i) # total num pairs + # print("total", total_pairs) if is_directed: - degrees_sum = sum_degrees_at_endpoints_both(edges, i, j, buf_source, buf_target)-(j-i) + degrees_sum = sum_degrees_at_endpoints_both( + edges, i, j, buf_source, buf_target + ) - (j - i) else: - degrees_sum = sum_degrees_at_endpoints_both(edges, i, j, buf_source, buf_source)-(j-i) - #print("degrees_sum",degrees_sum) - on_four_nodes = total_pairs-degrees_sum - #print("on four", on_four_nodes) - impossible_flips_due_to_triangles_etc = count_forbidden_moves_by_substructures(edges[i:j,:]) - #print("impossibles", impossible_flips_due_to_triangles_etc) + degrees_sum = sum_degrees_at_endpoints_both( + edges, i, j, buf_source, buf_source + ) - (j - i) + # print("degrees_sum",degrees_sum) + on_four_nodes = total_pairs - degrees_sum + # print("on four", on_four_nodes) + impossible_flips_due_to_triangles_etc = ( + count_forbidden_moves_by_substructures(edges[i:j, :]) + ) + # print("impossibles", impossible_flips_due_to_triangles_etc) allowed_flips = on_four_nodes - impossible_flips_due_to_triangles_etc - #print("added", allowed_flips//2) - num_allowed += allowed_flips//2 + # print("added", allowed_flips//2) + num_allowed += allowed_flips // 2 else: - #num_allowed += (j-i)*(j-i-1) # total num pairs - #print("bipartite") - #print(edges[i:j,:]) - result = bipartite_count_pairwise_allowed_moves(edges[i:j,:]) - #print("added", result) + # num_allowed += (j-i)*(j-i-1) # total num pairs + # print("bipartite") + # print(edges[i:j,:]) + result = bipartite_count_pairwise_allowed_moves(edges[i:j, :]) + # print("added", result) num_allowed += result return num_allowed def mono_count_pairwise_forbidden_moves(edges, also_count_single_color=True): """Count the number of forbidden flips in a bipartite graph - + The three parts of this have runtime O(E)+O(V*deg^2)+O(V*deg) The space requirements are O(E)+O(V*deg) - + """ neighborhoods = defaultdict(set) - for u,v in edges: + for u, v in edges: neighborhoods[u].add(v) neighborhoods[v].add(u) - + # count for each node the number of pairwise overlapping edges overlap_count = defaultdict(int) for v in neighborhoods: neighs = list(sorted(neighborhoods[v])) - for i,j in combinations(neighs,2): - overlap_count[(i,j)]+=1 + for i, j in combinations(neighs, 2): + overlap_count[(i, j)] += 1 _forbidden_flips = 0 - for (u,v), s in overlap_count.items(): + for (u, v), s in overlap_count.items(): d_u = len(neighborhoods[u]) d_v = len(neighborhoods[v]) # allowed = (d_u-s)*(d_v-s) - print(u,v,(d_u+d_v)*s-s*s) - _forbidden_flips += (d_u+d_v)*s-s*s - #print("forbidden pairwise", _forbidden_flips) + print(u, v, (d_u + d_v) * s - s * s) + _forbidden_flips += (d_u + d_v) * s - s * s + # print("forbidden pairwise", _forbidden_flips) if also_count_single_color: - for u,n in neighborhoods.items(): - _forbidden_flips+=len(n)*(len(n)-1) - #print("forbidden all", _forbidden_flips) + for u, n in neighborhoods.items(): + _forbidden_flips += len(n) * (len(n) - 1) + # print("forbidden all", _forbidden_flips) return _forbidden_flips + def bipartite_count_pairwise_allowed_moves(edges, also_count_single_color=True): """Count the number of forbidden flips in a bipartite graph - + The three parts of this have runtime O(E)+O(V*deg^2)+O(V*deg) The space requirements are O(E)+O(V*deg) - + """ neighborhoods = defaultdict(set) partition_l = set() partition_r = set() - for u,v in edges: + for u, v in edges: neighborhoods[u].add(v) neighborhoods[v].add(u) partition_l.add(u) partition_r.add(v) - + # count for each node the number of pairwise overlapping edges overlap_count = defaultdict(int) for v in partition_r: neighs = list(sorted(neighborhoods[v])) - for i,j in combinations(neighs,2): - overlap_count[(i,j)]+=1 + for i, j in combinations(neighs, 2): + overlap_count[(i, j)] += 1 _forbidden_flips = 0 - for (u,v), s in overlap_count.items(): + for (u, v), s in overlap_count.items(): d_u = len(neighborhoods[u]) d_v = len(neighborhoods[v]) - #allowed_flips += (d_u-s)*(d_v-s) - _forbidden_flips += (d_u+d_v)*s-s*s - _forbidden_flips*=2 - #print("forbidden pairwise", _forbidden_flips) + # allowed_flips += (d_u-s)*(d_v-s) + _forbidden_flips += (d_u + d_v) * s - s * s + _forbidden_flips *= 2 + # print("forbidden pairwise", _forbidden_flips) if also_count_single_color: for u in partition_l: neigh = neighborhoods[u] - _forbidden_flips+=len(neigh)*(len(neigh)-1) - #print("forbidden all", _forbidden_flips) - #print("allowed all", len(edges)*(len(edges)-1)) - allowed = len(edges)*(len(edges)-1) - _forbidden_flips - return allowed//2 - + _forbidden_flips += len(neigh) * (len(neigh) - 1) + # print("forbidden all", _forbidden_flips) + # print("allowed all", len(edges)*(len(edges)-1)) + allowed = len(edges) * (len(edges) - 1) - _forbidden_flips + return allowed // 2 def count_forbidden_moves_by_substructures(edges): @@ -300,43 +318,47 @@ def count_forbidden_moves_by_substructures(edges): four_cliques = 0 edge_set = set() neighborhoods = defaultdict(set) - for u,v in edges: + for u, v in edges: neighborhoods[u].add(v) neighborhoods[v].add(u) - edge_set.add((min(u,v), max(u,v))) + edge_set.add((min(u, v), max(u, v))) degrees = {v: len(neigh) for v, neigh in neighborhoods.items()} - for u,v in edges: - low = min(u,v) - high = max(u,v) + for u, v in edges: + low = min(u, v) + high = max(u, v) lows = [] highs = [] num_triangles = 0 - if degrees[u]< degrees[v]: + if degrees[u] < degrees[v]: for tip_node in neighborhoods[u]: if tip_node in neighborhoods[v]: - impossible_flips_due_to_triangles += degrees[tip_node]-2 + impossible_flips_due_to_triangles += degrees[tip_node] - 2 if tip_node < low: lows.append(tip_node) elif tip_node > high: highs.append(tip_node) - #print(u,v, degrees[tip_node]-2) - num_triangles+=1 + # print(u,v, degrees[tip_node]-2) + num_triangles += 1 else: for tip_node in neighborhoods[v]: if tip_node in neighborhoods[u]: - impossible_flips_due_to_triangles += degrees[tip_node]-2 + impossible_flips_due_to_triangles += degrees[tip_node] - 2 if tip_node < low: lows.append(tip_node) elif tip_node > high: highs.append(tip_node) - #print(u,v, degrees[tip_node]-2) - num_triangles+=1 - for i,j in product(lows, highs): - if (i,j) in edge_set: - four_cliques+=1 - four_cycle_with_cord_plus += num_triangles*(num_triangles-1)//2 - #print("AAA", four_cycle_with_cord_plus, four_cliques) - four_cycle_with_cord_exact = four_cycle_with_cord_plus - 6*four_cliques - #print("forbid, ", impossible_flips_due_to_triangles, four_cycle_with_cord_exact, four_cliques) - impossible_flips = 2* impossible_flips_due_to_triangles - 18*four_cliques-4*four_cycle_with_cord_exact - return impossible_flips \ No newline at end of file + # print(u,v, degrees[tip_node]-2) + num_triangles += 1 + for i, j in product(lows, highs): + if (i, j) in edge_set: + four_cliques += 1 + four_cycle_with_cord_plus += num_triangles * (num_triangles - 1) // 2 + # print("AAA", four_cycle_with_cord_plus, four_cliques) + four_cycle_with_cord_exact = four_cycle_with_cord_plus - 6 * four_cliques + # print("forbid, ", impossible_flips_due_to_triangles, four_cycle_with_cord_exact, four_cliques) + impossible_flips = ( + 2 * impossible_flips_due_to_triangles + - 18 * four_cliques + - 4 * four_cycle_with_cord_exact + ) + return impossible_flips diff --git a/src/nestmodel/io.py b/src/nestmodel/io.py new file mode 100644 index 0000000..292cd70 --- /dev/null +++ b/src/nestmodel/io.py @@ -0,0 +1,84 @@ +import numpy as np +from numba import njit, bool_ + + +def g6_read_bytes(p): + """Read the file at path p into a byte array""" + with open(p, "rb") as file: + lines = file.read() + arr = np.fromiter(lines, np.uint8) + with open(p, "rb") as f: + first_line = f.readline() + line_length = len(first_line) + m, r = divmod(len(arr), line_length) + if r != 0: + raise ValueError("not all lines have the same length") + arr = arr.reshape(m, line_length) + assert np.all(arr[:, line_length - 1] == 10) # 10 in ASCII is linefeed + arr = arr[:, : line_length - 1] + return arr + + +@njit(cache=True) +def _g6_data_to_n(data): + """Read initial one-, four- or eight-unit value from graph6 + integer sequence. + + Return (value, rest of seq.) + + From https://networkx.org/documentation/stable/_modules/networkx/readwrite/graph6.html + """ + if data[0] <= 62: + return data[0], data[1:] + if data[1] <= 62: + return (data[1] << 12) + (data[2] << 6) + data[3], data[4:] + return ( + (data[2] << 30) + + (data[3] << 24) + + (data[4] << 18) + + (data[5] << 12) + + (data[6] << 6) + + data[7], + data[8:], + ) + + +@njit(cache=True) +def g6_bytes_to_edges(input_arr): + """Convert a g6 bytes array into a list of graph edges""" + arr = input_arr - 63 + out = [] + + print(arr.shape[0]) + bits_buf = np.empty(arr.shape[1] * 6, dtype=bool_) + for i in range(arr.shape[0]): + edges = _g6_line_to_edges(arr[i, :], bits_buf) + out.append(edges) + return out + + +@njit(cache=True) +def _g6_bits(data, bits_buf): + """Returns sequence of individual bits from 6-bit-per-value + list of data values. From https://networkx.org/documentation/stable/_modules/networkx/readwrite/graph6.html#read_graph6 + """ + n = 0 + for d in data: + for i in [5, 4, 3, 2, 1, 0]: + bits_buf[n] = (d >> i) & 1 + n += 1 + + +@njit(cache=True) +def _g6_line_to_edges(input_line, bits_buf): + """Convert the g6 byte representation on the input line into an array of edges""" + n, data = _g6_data_to_n(input_line) + _g6_bits(data, bits_buf) + m = 0 + edges = [] + for j in range(1, n): + for i in range(j): + if bits_buf[m]: + edges.append((i, j)) + m += 1 + return np.array(edges, dtype=np.int16) diff --git a/nestmodel/load_datasets.py b/src/nestmodel/load_datasets.py similarity index 64% rename from nestmodel/load_datasets.py rename to src/nestmodel/load_datasets.py index 1e427f7..44e3ba8 100644 --- a/nestmodel/load_datasets.py +++ b/src/nestmodel/load_datasets.py @@ -1,11 +1,10 @@ from pathlib import Path import numpy as np -import pandas as pd + from nestmodel.utils import graph_tool_from_edges from nestmodel.fast_graph import FastGraph - def get_dataset_folder(should_assert=True): """Get a link to the dataset folder""" @@ -17,46 +16,54 @@ def get_dataset_folder(should_assert=True): path = path.parent if str(path.name) == "src": path = path.parent - if str(path.name)!="datasets": - path = path/"datasets" + if str(path.name) != "datasets": + path = path / "datasets" if should_assert: assert path.is_dir() return path + def relabel_edges(edges): """relabels nodes such that they start from 0 consecutively""" unique = np.unique(edges.ravel()) - mapping = {key:val for key, val in zip(unique, range(len(unique)))} + mapping = {key: val for key, val in zip(unique, range(len(unique)))} out_edges = np.empty_like(edges) - for i,(e1,e2) in enumerate(edges): - out_edges[i,0] = mapping[e1] - out_edges[i,1] = mapping[e2] + for i, (e1, e2) in enumerate(edges): + out_edges[i, 0] = mapping[e1] + out_edges[i, 1] = mapping[e2] return out_edges + def check_is_directed(edges): """Checks whether for all edges u-v the edge v-u is also in edges""" - d = {(a,b) for a,b in edges} - for a,b in edges: - assert (b,a) in d + d = {(a, b) for a, b in edges} + for a, b in edges: + assert (b, a) in d + class Dataset: """Simple structure to store information on datasets""" + def __init__(self, name, file_name, is_directed=False, delimiter=None): - self.name=name + self.name = name self.file_name = file_name self.get_edges = self.get_edges_pandas self.skip_rows = 0 - self.is_directed=is_directed + self.is_directed = is_directed self.delimiter = delimiter - self.requires_node_renaming=False - - + self.requires_node_renaming = False def get_edges_pandas(self, datasets_dir): """Reads edges using pands read_csv function""" - df = pd.read_csv(datasets_dir/self.file_name, skiprows=self.skip_rows, header=None, sep=self.delimiter) - edges = np.array([df[0].to_numpy(), df[1].to_numpy()],dtype=np.uint64).T + import pandas as pd # pylint: disable=import-outside-toplevel + df = pd.read_csv( + datasets_dir / self.file_name, + skiprows=self.skip_rows, + header=None, + sep=self.delimiter, + ) + edges = np.array([df[0].to_numpy(), df[1].to_numpy()], dtype=np.int64).T if self.requires_node_renaming: return relabel_edges(edges) @@ -67,43 +74,52 @@ def __eq__(self, other): if isinstance(other, str): return self.name == other elif isinstance(other, Dataset): - return self.name==other.name + return self.name == other.name else: raise ValueError() - def get_edges_karate(self, datasets_dir): # pylint: disable=unused-argument, missing-function-docstring - import networkx as nx # pylint: disable=import-outside-toplevel + def get_edges_karate( + self, datasets_dir + ): # pylint: disable=unused-argument, missing-function-docstring + import networkx as nx # pylint: disable=import-outside-toplevel + G = nx.karate_club_graph() edges = np.array(list(G.edges), dtype=int) return edges + Phonecalls = Dataset("phonecalls", "phonecalls.edgelist.txt", delimiter="\t") AstroPh = Dataset("AstroPh", "ca-AstroPh.txt", delimiter="\t", is_directed=False) -AstroPh.skip_rows=4 -AstroPh.requires_node_renaming=True +AstroPh.skip_rows = 4 +AstroPh.requires_node_renaming = True HepPh = Dataset("HepPh", "cit-HepPh.txt", delimiter="\t", is_directed=True) -HepPh.skip_rows=4 -HepPh.requires_node_renaming=True +HepPh.skip_rows = 4 +HepPh.requires_node_renaming = True Karate = Dataset("karate", "karate") Karate.get_edges = Karate.get_edges_karate -Google= Dataset("web-Google", "web-Google.txt", delimiter="\t", is_directed=True) -Google.skip_rows=4 -Google.requires_node_renaming=True +Google = Dataset("web-Google", "web-Google.txt", delimiter="\t", is_directed=True) +Google.skip_rows = 4 +Google.requires_node_renaming = True -Pokec= Dataset("soc-Pokec", "soc-pokec-relationships.txt", delimiter="\t", is_directed=True) -Pokec.skip_rows=0 -Pokec.requires_node_renaming=True +Pokec = Dataset( + "soc-Pokec", "soc-pokec-relationships.txt", delimiter="\t", is_directed=True +) +Pokec.skip_rows = 0 +Pokec.requires_node_renaming = True -Netscience= Dataset("netscience", "ca-netscience.edges", delimiter=" ", is_directed=False) -Netscience.skip_rows=0 -Netscience.requires_node_renaming=True +Netscience = Dataset( + "netscience", "ca-netscience.edges", delimiter=" ", is_directed=False +) +Netscience.skip_rows = 0 +Netscience.requires_node_renaming = True all_datasets = [Karate, Phonecalls, AstroPh, HepPh, Google, Pokec, Netscience] + def find_dataset(dataset_name): """Finds dataset object by dataset_name as str""" dataset = None @@ -111,23 +127,24 @@ def find_dataset(dataset_name): if potential_dataset == dataset_name: dataset = potential_dataset break - assert not dataset is None, f"You have specified an unknown dataset {dataset}" + assert dataset is not None, f"You have specified an unknown dataset {dataset}" return dataset + def load_fast_graph(dataset_path, dataset, verbosity=0): """Loads the dataset as specified by the dataset_str""" - g_base = load_gt_dataset_cached(dataset_path, - dataset, - verbosity=verbosity, - force_reload=True) - edges = np.array(g_base.get_edges(), dtype=np.uint32) + g_base = load_gt_dataset_cached( + dataset_path, dataset, verbosity=verbosity, force_reload=True + ) + edges = np.array(g_base.get_edges(), dtype=np.int32) G = FastGraph(edges, g_base.is_directed()) return G + def load_dataset(datasets_dir, dataset_name): - """Loads dataset in as edge_list """ - #"deezer_HR", "deezer_HU", "deezer_RO","tw_musae_DE", + """Loads dataset in as edge_list""" + # "deezer_HR", "deezer_HU", "deezer_RO","tw_musae_DE", # "tw_musae_ENGB","tw_musae_FR","lastfm_asia","fb_ath", # "fb_pol","phonecalls", "facebook_sc"] @@ -135,16 +152,17 @@ def load_dataset(datasets_dir, dataset_name): edges = dataset.get_edges(datasets_dir) if dataset.is_directed is False: - edges = edges[edges[:,0] < edges[:,1],:] - #[(e1, e2) for e1, e2 in edges if e1 < e2] - #print("A", dataset.is_directed) + edges = edges[edges[:, 0] < edges[:, 1], :] + # [(e1, e2) for e1, e2 in edges if e1 < e2] + # print("A", dataset.is_directed) return edges, dataset.is_directed + def get_datasets_path(): """Try to read dataset path from file""" folders = [".", "./scripts", "./nest_model/scripts"] for folder in folders: - p = Path(folder)/"datasets_path.txt" + p = Path(folder) / "datasets_path.txt" if p.is_file(): with open(p, "r", encoding="utf-8") as f: return Path(f.read()) @@ -159,37 +177,36 @@ def load_fg_dataset_cached(datasets_dir, dataset_name, verbosity=0, force_reload else: datasets_dir = Path(datasets_dir) dataset = find_dataset(dataset_name) - cache_file = datasets_dir/(dataset.file_name+".npz") + cache_file = datasets_dir / (dataset.file_name + ".npz") if cache_file.is_file() and not force_reload: - if verbosity>1: + if verbosity > 1: print("loading cached") npzfile = np.load(cache_file) return FastGraph(npzfile["edges"], bool(npzfile["is_directed"])) else: - if verbosity>1: + if verbosity > 1: print("loading raw") edges, is_directed = load_dataset(datasets_dir, dataset_name) - if edges.max() < np.iinfo(np.uint32).max: - edges = edges.astype(np.uint32) + if edges.max() < np.iinfo(np.int32).max: + edges = edges.astype(np.int32) print(edges.dtype) g = FastGraph(edges, is_directed) g.save_npz(str(cache_file.absolute())) return g - - def load_gt_dataset_cached(datasets_dir, dataset_name, verbosity=0, force_reload=False): """Loads a dataset using the binary file format from graph-tool""" dataset = find_dataset(dataset_name) - cache_file = datasets_dir/(dataset.file_name+".gt") + cache_file = datasets_dir / (dataset.file_name + ".gt") if cache_file.is_file() and not force_reload: - if verbosity>1: + if verbosity > 1: print("loading cached") - import graph_tool.all as gt # pylint: disable=import-outside-toplevel, import-error # type: ignore + import graph_tool.all as gt # pylint: disable=import-outside-toplevel, import-error # type: ignore + return gt.load_graph(str(cache_file.absolute())) else: - if verbosity>1: + if verbosity > 1: print("loading raw") edges, is_directed = load_dataset(datasets_dir, dataset_name) g = graph_tool_from_edges(edges, None, is_directed=is_directed) diff --git a/src/nestmodel/long_refinement_graphs.py b/src/nestmodel/long_refinement_graphs.py new file mode 100644 index 0000000..78e3a3c --- /dev/null +++ b/src/nestmodel/long_refinement_graphs.py @@ -0,0 +1,871 @@ +import numpy as np +from nestmodel.fast_graph import FastGraph + + +def _dict_to_list(d): + l = [] + for key, values in d.items(): + for val in values: + l.append((min(key, val), max(key, val))) + return list(set(l)) + + +def long_refinement_12__1_5(): + """Returns the first example long refinement graph from 'The Iteration Number of Colour Refinement' + It has 12 nodes and nodes have degrees either 1 or 5 + """ + d = { + 0: [1], + 1: [0, 2, 3, 4, 5], + 2: [1, 3, 5, 7, 10], + 3: [1, 2, 4, 6, 10], + 4: [1, 3, 5, 9, 11], + 5: [1, 2, 4, 8, 11], + 6: [3, 7, 8, 9, 11], + 7: [2, 6, 8, 9, 10], + 8: [5, 6, 7, 10, 11], + 9: [4, 6, 7, 10, 11], + 10: [2, 3, 7, 8, 9], + 11: [4, 5, 6, 8, 9], + } + edges = np.array(_dict_to_list(d)) + return FastGraph(edges, is_directed=False, num_nodes=12) + + +def long_refinement_14__1_3(): + """Returns the second example long refinement graph from 'The Iteration Number of Colour Refinement' + It has 14 nodes and nodes have degrees either 1 or 3 + """ + d = { + 0: [1], + 1: [0, 2, 3], + 2: [1, 11, 13], + 3: [1, 10, 12], + 4: [ + 5, + 7, + 10, + ], + 5: [4, 6, 10], + 6: [5, 9, 11], + 7: [4, 8, 11], + 8: [7, 9, 13], + 9: [6, 8, 12], + 10: [3, 4, 5], + 11: [2, 6, 7], + 12: [3, 9, 13], + 13: [2, 8, 12], + } + edges = np.array(_dict_to_list(d)) + return FastGraph(edges, is_directed=False, num_nodes=14) + + +def long_refinement_10(i, return_graph=True): + """Returns one of the 16 long refinement graph on 10 nodes""" + all_edges = [ + np.array( + [ + [0, 4], + [1, 5], + [2, 5], + [0, 6], + [3, 6], + [4, 6], + [0, 7], + [1, 7], + [3, 7], + [1, 8], + [2, 8], + [4, 8], + [0, 9], + [2, 9], + [3, 9], + [5, 9], + [4, 0], + [5, 1], + [5, 2], + [6, 0], + [6, 3], + [6, 4], + [7, 0], + [7, 1], + [7, 3], + [8, 1], + [8, 2], + [8, 4], + [9, 0], + [9, 2], + [9, 3], + [9, 5], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 4], + [1, 5], + [2, 5], + [0, 6], + [1, 6], + [3, 6], + [0, 7], + [2, 7], + [4, 7], + [0, 8], + [1, 8], + [2, 8], + [3, 8], + [1, 9], + [2, 9], + [6, 9], + [7, 9], + [4, 0], + [5, 1], + [5, 2], + [6, 0], + [6, 1], + [6, 3], + [7, 0], + [7, 2], + [7, 4], + [8, 0], + [8, 1], + [8, 2], + [8, 3], + [9, 1], + [9, 2], + [9, 6], + [9, 7], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 4], + [1, 5], + [2, 5], + [3, 5], + [0, 6], + [1, 6], + [2, 6], + [1, 7], + [3, 7], + [4, 7], + [0, 8], + [1, 8], + [2, 8], + [4, 8], + [0, 9], + [3, 9], + [4, 9], + [5, 9], + [4, 0], + [5, 1], + [5, 2], + [5, 3], + [6, 0], + [6, 1], + [6, 2], + [7, 1], + [7, 3], + [7, 4], + [8, 0], + [8, 1], + [8, 2], + [8, 4], + [9, 0], + [9, 3], + [9, 4], + [9, 5], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 4], + [1, 4], + [2, 5], + [3, 5], + [0, 6], + [1, 6], + [2, 6], + [0, 7], + [1, 7], + [3, 7], + [0, 8], + [2, 8], + [4, 8], + [5, 8], + [2, 9], + [3, 9], + [4, 9], + [6, 9], + [4, 0], + [4, 1], + [5, 2], + [5, 3], + [6, 0], + [6, 1], + [6, 2], + [7, 0], + [7, 1], + [7, 3], + [8, 0], + [8, 2], + [8, 4], + [8, 5], + [9, 2], + [9, 3], + [9, 4], + [9, 6], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 4], + [1, 4], + [2, 5], + [3, 5], + [4, 5], + [0, 6], + [1, 6], + [2, 6], + [0, 7], + [2, 7], + [3, 7], + [0, 8], + [1, 8], + [3, 8], + [4, 8], + [5, 8], + [1, 9], + [2, 9], + [3, 9], + [6, 9], + [7, 9], + [4, 0], + [4, 1], + [5, 2], + [5, 3], + [5, 4], + [6, 0], + [6, 1], + [6, 2], + [7, 0], + [7, 2], + [7, 3], + [8, 0], + [8, 1], + [8, 3], + [8, 4], + [8, 5], + [9, 1], + [9, 2], + [9, 3], + [9, 6], + [9, 7], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 4], + [1, 4], + [2, 5], + [3, 5], + [4, 5], + [0, 6], + [1, 6], + [2, 6], + [0, 7], + [2, 7], + [3, 7], + [0, 8], + [1, 8], + [3, 8], + [4, 8], + [5, 8], + [1, 9], + [2, 9], + [3, 9], + [6, 9], + [7, 9], + [8, 9], + [4, 0], + [4, 1], + [5, 2], + [5, 3], + [5, 4], + [6, 0], + [6, 1], + [6, 2], + [7, 0], + [7, 2], + [7, 3], + [8, 0], + [8, 1], + [8, 3], + [8, 4], + [8, 5], + [9, 1], + [9, 2], + [9, 3], + [9, 6], + [9, 7], + [9, 8], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 4], + [1, 4], + [0, 5], + [2, 5], + [4, 5], + [0, 6], + [1, 6], + [3, 6], + [1, 7], + [2, 7], + [3, 7], + [0, 8], + [1, 8], + [3, 8], + [4, 8], + [7, 8], + [0, 9], + [1, 9], + [2, 9], + [4, 9], + [7, 9], + [4, 0], + [4, 1], + [5, 0], + [5, 2], + [5, 4], + [6, 0], + [6, 1], + [6, 3], + [7, 1], + [7, 2], + [7, 3], + [8, 0], + [8, 1], + [8, 3], + [8, 4], + [8, 7], + [9, 0], + [9, 1], + [9, 2], + [9, 4], + [9, 7], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 3], + [1, 4], + [0, 5], + [2, 5], + [0, 6], + [1, 6], + [3, 6], + [4, 6], + [0, 7], + [1, 7], + [2, 7], + [4, 7], + [1, 8], + [2, 8], + [3, 8], + [5, 8], + [6, 8], + [2, 9], + [3, 9], + [4, 9], + [5, 9], + [7, 9], + [3, 0], + [4, 1], + [5, 0], + [5, 2], + [6, 0], + [6, 1], + [6, 3], + [6, 4], + [7, 0], + [7, 1], + [7, 2], + [7, 4], + [8, 1], + [8, 2], + [8, 3], + [8, 5], + [8, 6], + [9, 2], + [9, 3], + [9, 4], + [9, 5], + [9, 7], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 3], + [1, 4], + [0, 5], + [2, 5], + [0, 6], + [1, 6], + [2, 6], + [4, 6], + [1, 7], + [2, 7], + [3, 7], + [5, 7], + [0, 8], + [1, 8], + [3, 8], + [4, 8], + [6, 8], + [7, 8], + [2, 9], + [3, 9], + [4, 9], + [5, 9], + [6, 9], + [7, 9], + [3, 0], + [4, 1], + [5, 0], + [5, 2], + [6, 0], + [6, 1], + [6, 2], + [6, 4], + [7, 1], + [7, 2], + [7, 3], + [7, 5], + [8, 0], + [8, 1], + [8, 3], + [8, 4], + [8, 6], + [8, 7], + [9, 2], + [9, 3], + [9, 4], + [9, 5], + [9, 6], + [9, 7], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 3], + [1, 4], + [2, 4], + [0, 5], + [1, 5], + [4, 5], + [0, 6], + [2, 6], + [3, 6], + [0, 7], + [1, 7], + [2, 7], + [3, 7], + [1, 8], + [2, 8], + [3, 8], + [4, 8], + [6, 8], + [2, 9], + [3, 9], + [4, 9], + [5, 9], + [6, 9], + [3, 0], + [4, 1], + [4, 2], + [5, 0], + [5, 1], + [5, 4], + [6, 0], + [6, 2], + [6, 3], + [7, 0], + [7, 1], + [7, 2], + [7, 3], + [8, 1], + [8, 2], + [8, 3], + [8, 4], + [8, 6], + [9, 2], + [9, 3], + [9, 4], + [9, 5], + [9, 6], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 3], + [0, 4], + [1, 4], + [1, 5], + [2, 5], + [0, 6], + [1, 6], + [2, 6], + [3, 6], + [0, 7], + [2, 7], + [3, 7], + [5, 7], + [1, 8], + [3, 8], + [4, 8], + [5, 8], + [6, 8], + [0, 9], + [1, 9], + [3, 9], + [5, 9], + [7, 9], + [3, 0], + [4, 0], + [4, 1], + [5, 1], + [5, 2], + [6, 0], + [6, 1], + [6, 2], + [6, 3], + [7, 0], + [7, 2], + [7, 3], + [7, 5], + [8, 1], + [8, 3], + [8, 4], + [8, 5], + [8, 6], + [9, 0], + [9, 1], + [9, 3], + [9, 5], + [9, 7], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 3], + [0, 4], + [1, 4], + [1, 5], + [2, 5], + [3, 5], + [0, 6], + [1, 6], + [2, 6], + [4, 6], + [0, 7], + [1, 7], + [2, 7], + [3, 7], + [1, 8], + [2, 8], + [3, 8], + [4, 8], + [5, 8], + [0, 9], + [3, 9], + [4, 9], + [5, 9], + [6, 9], + [3, 0], + [4, 0], + [4, 1], + [5, 1], + [5, 2], + [5, 3], + [6, 0], + [6, 1], + [6, 2], + [6, 4], + [7, 0], + [7, 1], + [7, 2], + [7, 3], + [8, 1], + [8, 2], + [8, 3], + [8, 4], + [8, 5], + [9, 0], + [9, 3], + [9, 4], + [9, 5], + [9, 6], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 3], + [0, 4], + [1, 4], + [3, 4], + [0, 5], + [1, 5], + [2, 5], + [0, 6], + [1, 6], + [2, 6], + [5, 6], + [1, 7], + [2, 7], + [3, 7], + [4, 7], + [0, 8], + [2, 8], + [3, 8], + [4, 8], + [5, 8], + [7, 8], + [1, 9], + [2, 9], + [3, 9], + [4, 9], + [5, 9], + [6, 9], + [3, 0], + [4, 0], + [4, 1], + [4, 3], + [5, 0], + [5, 1], + [5, 2], + [6, 0], + [6, 1], + [6, 2], + [6, 5], + [7, 1], + [7, 2], + [7, 3], + [7, 4], + [8, 0], + [8, 2], + [8, 3], + [8, 4], + [8, 5], + [8, 7], + [9, 1], + [9, 2], + [9, 3], + [9, 4], + [9, 5], + [9, 6], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 3], + [1, 3], + [0, 4], + [2, 4], + [1, 5], + [2, 5], + [0, 6], + [1, 6], + [3, 6], + [4, 6], + [0, 7], + [1, 7], + [2, 7], + [4, 7], + [5, 7], + [1, 8], + [2, 8], + [3, 8], + [4, 8], + [5, 8], + [6, 8], + [0, 9], + [2, 9], + [3, 9], + [5, 9], + [6, 9], + [7, 9], + [3, 0], + [3, 1], + [4, 0], + [4, 2], + [5, 1], + [5, 2], + [6, 0], + [6, 1], + [6, 3], + [6, 4], + [7, 0], + [7, 1], + [7, 2], + [7, 4], + [7, 5], + [8, 1], + [8, 2], + [8, 3], + [8, 4], + [8, 5], + [8, 6], + [9, 0], + [9, 2], + [9, 3], + [9, 5], + [9, 6], + [9, 7], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 3], + [1, 3], + [0, 4], + [2, 4], + [1, 5], + [2, 5], + [3, 5], + [0, 6], + [1, 6], + [4, 6], + [1, 7], + [2, 7], + [3, 7], + [4, 7], + [5, 7], + [0, 8], + [1, 8], + [2, 8], + [4, 8], + [6, 8], + [7, 8], + [0, 9], + [2, 9], + [3, 9], + [5, 9], + [6, 9], + [7, 9], + [8, 9], + [3, 0], + [3, 1], + [4, 0], + [4, 2], + [5, 1], + [5, 2], + [5, 3], + [6, 0], + [6, 1], + [6, 4], + [7, 1], + [7, 2], + [7, 3], + [7, 4], + [7, 5], + [8, 0], + [8, 1], + [8, 2], + [8, 4], + [8, 6], + [8, 7], + [9, 0], + [9, 2], + [9, 3], + [9, 5], + [9, 6], + [9, 7], + [9, 8], + ], + dtype=np.int16, + ), + np.array( + [ + [0, 3], + [1, 3], + [0, 4], + [2, 4], + [1, 5], + [2, 5], + [3, 5], + [4, 5], + [0, 6], + [1, 6], + [2, 6], + [3, 6], + [0, 7], + [1, 7], + [2, 7], + [4, 7], + [6, 7], + [0, 8], + [1, 8], + [2, 8], + [3, 8], + [4, 8], + [6, 8], + [1, 9], + [2, 9], + [3, 9], + [4, 9], + [5, 9], + [7, 9], + [3, 0], + [3, 1], + [4, 0], + [4, 2], + [5, 1], + [5, 2], + [5, 3], + [5, 4], + [6, 0], + [6, 1], + [6, 2], + [6, 3], + [7, 0], + [7, 1], + [7, 2], + [7, 4], + [7, 6], + [8, 0], + [8, 1], + [8, 2], + [8, 3], + [8, 4], + [8, 6], + [9, 1], + [9, 2], + [9, 3], + [9, 4], + [9, 5], + [9, 7], + ], + dtype=np.int16, + ), + ] + edges = all_edges[i] + if return_graph: + return FastGraph(edges, is_directed=False, num_nodes=10) + else: + return edges diff --git a/src/nestmodel/mutual_independent_models.py b/src/nestmodel/mutual_independent_models.py new file mode 100644 index 0000000..7865cb3 --- /dev/null +++ b/src/nestmodel/mutual_independent_models.py @@ -0,0 +1,122 @@ +from numba import njit +import numpy as np + + +def Gnp_row_first(n, p, seed=0): + """Generates a random graph drawn from the Gnp ensemble""" + _set_seed(seed=seed) + return _Gnp_row_first(n, p) + + +@njit(cache=True) +def _Gnp_row_first(n, p): + """Generates a random graph drawn from the Gnp ensemble""" + approx = int(n * (n - 1) * p) + E = np.empty((approx, 2), dtype=np.int32) + + x = 0 + y = 0 + k = 1 + agg = 0 + upper_bound = ((n) * (n - 1)) // 2 + i = 0 + while True: + k = np.random.geometric(p) + agg += k + if agg > upper_bound: + break + x += k + while x >= n: + x += y + 2 - n # = n-1 -(r+1) + y += 1 + E[i, 0] = y + E[i, 1] = x + + i += 1 + if i >= len(E): + E2 = np.empty((len(E) + approx, 2), dtype=np.int32) + E2[: len(E)] = E[:] + E = E2 + return E[:i, :] + + +@njit(cache=True) +def _set_seed(seed): + """Set the need. This needs to be done within numba @njit function""" + np.random.seed(seed) + + +@njit(cache=True) +def random_matrix(n_rows, n_columns, p, seed): + """Returns the nonzero entries of a matrix of shape (n_rows, n_columns) + each element is 1 with probability p and 0 with probability 1-p""" + np.random.seed(seed) + approx = int(n_rows * n_columns * p) + E = np.empty((approx, 2), dtype=np.int32) + i = 0 + + x = -1 + y = 0 + k = 1 + agg = 0 + upper_bound = n_rows * n_columns + while True: + k = np.random.geometric(p) + x += k + agg += k + if agg > upper_bound: + break + while x >= n_columns: + x -= n_columns + y += 1 + + E[i, 0] = y + E[i, 1] = x + + i += 1 + if i >= len(E): + E2 = np.empty((len(E) + approx, 2), dtype=np.int32) + E2[: len(E)] = E[:] + E = E2 + + return E[:i, :] + + +def SBM(partition_sizes, P, seed=0): + """Returns the edges corresponding to an SBM + partition sizes: + array of length num_blocks desired sizes of each parttion + P: + matrix of shape (num_blocks, num_blocks) indicating the connecting probabilities of each block. + + """ + + def offset_edges(a, b, edge): + edges[:, 0] += a + edges[:, 1] += b + return edges + + all_edges = [] + n_partitions = len(partition_sizes) + ns_cumsum = np.array([0, *np.cumsum(partition_sizes)], dtype=np.int64) + + for i in range(n_partitions): + for j in range(i + 1): + ij_seed = seed + i * n_partitions + j + p = P[i, j] + + # print(p) + if i == j: + edges = Gnp_row_first(partition_sizes[j], p, seed=ij_seed) + else: + edges = random_matrix( + partition_sizes[j], partition_sizes[i], p, seed=ij_seed + ) + # print(edges) + di = ns_cumsum[i] + dj = ns_cumsum[j] + + offset_edges(dj, di, edges) + all_edges.append(edges) + + return np.vstack(all_edges) diff --git a/nestmodel/tests/testing.py b/src/nestmodel/testing.py similarity index 63% rename from nestmodel/tests/testing.py rename to src/nestmodel/testing.py index 452595f..79e1ecf 100644 --- a/nestmodel/tests/testing.py +++ b/src/nestmodel/testing.py @@ -1,4 +1,3 @@ - from collections import Counter, defaultdict import numpy as np @@ -6,33 +5,44 @@ from nestmodel.fast_wl import WL_fast from nestmodel.utils import calc_color_histogram + def check_counters_agree(c1, c2): """Checks whether two Counters agree on all keys and values, order is irrelevant""" - assert c1.total()==c2.total(), "Counters have different totals" - assert len(c1)==len(c2), "Counters have different number of keys" + assert c1.total() == c2.total(), "Counters have different totals" + assert len(c1) == len(c2), "Counters have different number of keys" for key in c1: assert key in c2, "counters do not share all keys" for key, val1 in c1.items(): - assert c2[key] == val1, f"counters have different values for the same key {key} maps onto {val1} and {c2[key]}" + assert ( + c2[key] == val1 + ), f"counters have different values for the same key {key} maps onto {val1} and {c2[key]}" + def check_colorings_agree(coloring1, coloring2): """Checks whether two colorings agree (i.e. they are equivalent)""" assert len(coloring1) == len(coloring2) d_a = defaultdict(set) d_b = defaultdict(set) - for a,b in zip(coloring1, coloring2): + for a, b in zip(coloring1, coloring2): d_a[a].add(b) d_b[b].add(a) - assert len(d_a) == len(d_b), f"Number of colors disagrees {len(d_a)}!={len(d_b)}, {coloring1}, {coloring2}" + assert len(d_a) == len( + d_b + ), f"Number of colors disagrees {len(d_a)}!={len(d_b)}, {coloring1}, {coloring2}" for key, value in d_a.items(): - assert len(value)==1, f"found multiple matches for {key} i.e. {value}, {coloring1}, {coloring2}" + assert ( + len(value) == 1 + ), f"found multiple matches for {key} i.e. {value}, {coloring1}, {coloring2}" for key, value in d_b.items(): - assert len(value)==1, f"found multiple matches for {key} i.e. {value}, {coloring1}, {coloring2}" + assert ( + len(value) == 1 + ), f"found multiple matches for {key} i.e. {value}, {coloring1}, {coloring2}" + def check_color_histograms_agree(hist1, hist2): """Assert that two color histograms agree""" - assert len(hist1)==len(hist2) + assert len(hist1) == len(hist2) for key in hist1: assert key in hist2, f"{key} not in hist2" @@ -54,21 +64,27 @@ def check_two_edge_sets_are_identical(edges1, edges2, is_directed): check_counters_agree(C1, C2) - def check_wl_colors_agree(edges1, edges2, is_directed): """Checks whether two edge lists agree on their color""" - from nestmodel.fast_graph import make_directed # pylint:disable=import-outside-toplevel + from nestmodel.fast_graph import ( + make_directed, + ) # pylint:disable=import-outside-toplevel + if not is_directed: edges1 = make_directed(edges1) edges2 = make_directed(edges2) WL1 = WL_fast(edges1) WL2 = WL_fast(edges2) - assert len(WL1)==len(WL2), f"stages until stable WL colors disagree {len(WL1)} != {len(WL2)}" + assert len(WL1) == len( + WL2 + ), f"stages until stable WL colors disagree {len(WL1)} != {len(WL2)}" for arr1, arr2 in zip(WL1, WL2): np.testing.assert_array_equal(arr1, arr2) -def compare_color_histograms_for_edges(edges1, edges2, base_partitions, max_depth, is_directed): +def compare_color_histograms_for_edges( + edges1, edges2, base_partitions, max_depth, is_directed +): """Compare color histograms for depth up to max_depth""" for depth in range(len(max_depth)): hist1 = calc_color_histogram(edges1, base_partitions[depth], is_directed) @@ -78,15 +94,15 @@ def compare_color_histograms_for_edges(edges1, edges2, base_partitions, max_dept def check_blocks_are_oriented(all_blocks, G): - """Asserts that bounds for each block all edges are either i-j or j-i but not both """ + """Asserts that bounds for each block all edges are either i-j or j-i but not both""" for blocks, labels in zip(all_blocks, G.base_partitions): for block in blocks: start, end = block - edges_this_block = G.edges_ordered[start: end,:] + edges_this_block = G.edges_ordered[start:end, :] - l = Counter((labels[u],labels[v]) for u,v in edges_this_block) + l = Counter((labels[u], labels[v]) for u, v in edges_this_block) - assert len(l)==1 + assert len(l) == 1 def check_blocks_are_unique_class(all_blocks, edges_classes): @@ -94,19 +110,19 @@ def check_blocks_are_unique_class(all_blocks, edges_classes): for blocks, classes in zip(all_blocks, edges_classes): for block in blocks: start, end = block - classes_this_block = classes[start: end] + classes_this_block = classes[start:end] if len(np.unique(classes_this_block)): - #print(np.unique(classes_this_block)) - assert len(np.unique(classes_this_block))==1 + # print(np.unique(classes_this_block)) + assert len(np.unique(classes_this_block)) == 1 def check_blocks_dont_overlap(blocks, n_edges): """Asserts that there is no overlap in the blocks""" - number_of_block_edge_is_part_of = np.zeros(n_edges, dtype=np.uint32) + number_of_block_edge_is_part_of = np.zeros(n_edges, dtype=np.int32) for start, end in blocks: - number_of_block_edge_is_part_of[start:end]+=1 - #print("free edges", np.count_nonzero(counts)) + number_of_block_edge_is_part_of[start:end] += 1 + # print("free edges", np.count_nonzero(counts)) assert np.all(number_of_block_edge_is_part_of <= 1) @@ -115,20 +131,20 @@ def check_outside_of_blocks(all_blocks, G): for blocks, labels in zip(all_blocks, G.base_partitions): n_edges = len(G.edges_ordered) - number_of_block_edge_is_part_of = np.zeros(n_edges, dtype=np.uint32) + number_of_block_edge_is_part_of = np.zeros(n_edges, dtype=np.int32) for start, end in blocks: - number_of_block_edge_is_part_of[start:end]+=1 + number_of_block_edge_is_part_of[start:end] += 1 block_labels = set() for start, end in blocks: - for u,v in G.edges_ordered[start:end,:]: + for u, v in G.edges_ordered[start:end, :]: lu = labels[u] lv = labels[v] - block_labels.add((min(lu,lv), max(lu,lv))) + block_labels.add((min(lu, lv), max(lu, lv))) - for i, (u,v) in enumerate(G.edges_ordered): - if number_of_block_edge_is_part_of[i]>0: + for i, (u, v) in enumerate(G.edges_ordered): + if number_of_block_edge_is_part_of[i] > 0: continue lu = labels[u] lv = labels[v] - assert not (min(lu,lv), max(lu,lv)) in block_labels + assert not (min(lu, lv), max(lu, lv)) in block_labels diff --git a/nestmodel/unified_functions.py b/src/nestmodel/unified_functions.py similarity index 69% rename from nestmodel/unified_functions.py rename to src/nestmodel/unified_functions.py index 183f896..0b76d16 100644 --- a/nestmodel/unified_functions.py +++ b/src/nestmodel/unified_functions.py @@ -1,15 +1,19 @@ -"""This file should contain functions that work independent of the underlying graph structure used (e.g. networkx or graph-tool)""" +"""This file should contain functions that work independent of the underlying graph structure used + (e.g. networkx or graph-tool)""" + import numpy as np + def is_networkx_str(G_str): """Checks whether a repr string is from networkx Graph""" - if (G_str.startswith("2: + import graph_tool.all as gt # pylint:disable=import-error; # type: ignore + + if verbosity > 2: print(repr(G), len(G.nodes), len(G.edges)) - if verbosity>3: + if verbosity > 3: print("creating edge list") edge_list = np.array(list(G.edges), dtype=int) - while edge_list.min()>0: - edge_list-=1 - if verbosity>3: + while edge_list.min() > 0: + edge_list -= 1 + if verbosity > 3: print("done creating edge list") print("creating graph") time.sleep(0.0001) - g = gt.Graph(directed = False) + g = gt.Graph(directed=False) g.add_vertex(len(G.nodes)) g.add_edge_list(edge_list) - if verbosity>3: + if verbosity > 3: print("done creating graph") time.sleep(0.0001) return g @@ -33,12 +34,13 @@ def nx_to_gt(G, verbosity=0): def graph_tool_from_edges(edges, size, is_directed): """Create a new graph-tool graph from an edge list""" - import graph_tool.all as gt # pylint:disable=import-error; # type: ignore + import graph_tool.all as gt # pylint:disable=import-error; # type: ignore + if size is None: unique = np.unique(edges.flatten()) - assert unique[0]==0, "expecting to start from 0 " + str(unique[:10]) + assert unique[0] == 0, "expecting to start from 0 " + str(unique[:10]) size = len(unique) - graph = gt.Graph(directed=is_directed) + graph = gt.Graph(directed=is_directed) graph.add_vertex(size) graph.add_edge_list(edges) return graph @@ -47,9 +49,10 @@ def graph_tool_from_edges(edges, size, is_directed): def networkx_from_edges(edges, size, is_directed, is_multi=False): """Create a networkx graph from an edge list""" import networkx as nx + if size is None: unique = np.unique(edges.flatten()) - assert unique[0]==0, "expecting to start from 0 " + str(unique[:10]) + assert unique[0] == 0, "expecting to start from 0 " + str(unique[:10]) size = len(unique) if is_directed: if is_multi: @@ -66,82 +69,75 @@ def networkx_from_edges(edges, size, is_directed, is_multi=False): return G - - - def calc_color_histogram(edges, labels, is_directed): """Compute the color histogram (multiset) of colors given an edge list""" if is_directed: outs = defaultdict(Counter) ins = defaultdict(Counter) - for e1,e2 in edges: + for e1, e2 in edges: l1 = labels[e1] l2 = labels[e2] - outs[e1][l2]+=1 - ins[e2][l1]+=1 + outs[e1][l2] += 1 + ins[e2][l1] += 1 return outs, ins else: hist = defaultdict(Counter) - for u,v in edges: - hist[u][labels[v]]+=1 - hist[v][labels[u]]+=1 + for u, v in edges: + hist[u][labels[v]] += 1 + hist[v][labels[u]] += 1 return hist - - - def compare_partitions(p1s, p2s): """Prints the difference of two partitions""" - #print(p1s.shape) + # print(p1s.shape) for depth, (p1, p2) in enumerate(zip(p1s, p2s)): - same = p1==p2 + same = p1 == p2 if not np.all(same): print("current depth", depth) - for i, (a,b) in enumerate(zip(p1, p2)): - if a!=b: - print(i, a,b) + for i, (a, b) in enumerate(zip(p1, p2)): + if a != b: + print(i, a, b) print(np.vstack((p1[~same], p2[~same]))) print() + def compare_edges(edges1, edges2): """Copares two edge lists and prints the differences if any""" o1 = np.lexsort(edges1.T) o2 = np.lexsort(edges2.T) - edges1 = edges1[o1,:] - edges2 = edges2[o2,:] - diffs = np.all(edges1==edges2,axis=1) + edges1 = edges1[o1, :] + edges2 = edges2[o2, :] + diffs = np.all(edges1 == edges2, axis=1) if not np.all(diffs): - print(np.hstack((edges1[~diffs,:], edges2[~diffs,:]))) + print(np.hstack((edges1[~diffs, :], edges2[~diffs, :]))) print() - def check_colors_are_correct(G, max_depth): """Cheks whether the base partitions for FastGraph G are the same - as the partitions computed by the other LW algorithm""" - _, labelings = WL(G.to_gt(False))#pylint: disable=unbalanced-tuple-unpacking - assert len(labelings)==len(G.base_partitions)-1 - for i, (p1,p2) in enumerate(zip(labelings, G.base_partitions)): + as the partitions computed by the other LW algorithm""" + _, labelings = WL(G.to_gt(False)) # pylint: disable=unbalanced-tuple-unpacking + assert len(labelings) == len(G.base_partitions) - 1 + for i, (p1, p2) in enumerate(zip(labelings, G.base_partitions)): if i > max_depth: print(f"skipped {i}") continue - if labelings_are_equivalent(p1,p2): + if labelings_are_equivalent(p1, p2): continue - print(labelings_are_equivalent(p1,p2)) + print(labelings_are_equivalent(p1, p2)) print(p1) print(p2) - agree = p1==p2 - #print(np.unique(p1.ravel())) - #print(np.unique(p2.ravel())) - print(len(np.unique(p1.ravel())),len(np.unique(p2.ravel()))) - print("uniques", np.all((np.unique(p1.ravel())==np.unique(p2.ravel())))) + agree = p1 == p2 + # print(np.unique(p1.ravel())) + # print(np.unique(p2.ravel())) + print(len(np.unique(p1.ravel())), len(np.unique(p2.ravel()))) + print("uniques", np.all((np.unique(p1.ravel()) == np.unique(p2.ravel())))) print(agree.sum()) - print(p1[~agree]) print(p2[~agree]) - assert np.all(p1==p2) + assert np.all(p1 == p2) print("WL colors agree") @@ -151,12 +147,12 @@ def calc_jaccard(G1, G2): return calc_jaccard_edges(G1.edges, G2.edges, G1.is_directed) -def calc_jaccard_edges(edges1, edges2, is_directed): +def calc_jaccard_edges(edges1, edges2, is_directed): """Calc Jaccard similarity of two edge lists""" - assert len(edges1.shape)==2 - assert edges1.shape[1]==2 - assert len(edges2.shape)==2 - assert edges2.shape[1]==2 + assert len(edges1.shape) == 2 + assert edges1.shape[1] == 2 + assert len(edges2.shape) == 2 + assert edges2.shape[1] == 2 u_edges1 = get_unique_edges_from_edge_list(edges1, is_directed) u_edges2 = get_unique_edges_from_edge_list(edges2, is_directed) @@ -166,56 +162,52 @@ def calc_jaccard_edges(edges1, edges2, is_directed): def calc_jaccard_unique_edges(edges1, edges2): """computes the jaccard of two edge lists""" if len(edges1.shape) > 1: - assert edges1.shape[1]==1 + assert edges1.shape[1] == 1 if len(edges2.shape) > 1: - assert edges2.shape[1]==1 - l1=len(edges1) - l2=len(edges2) + assert edges2.shape[1] == 1 + l1 = len(edges1) + l2 = len(edges2) intersection = len(np.intersect1d(edges1, edges2)) - return intersection/(l1+l2-intersection) - + return intersection / (l1 + l2 - intersection) @njit def normalise_undirected_edges_by_labels(edges, labels): """Makes sure that edges u-v always have l[u]<=l[v]""" - edges2=np.empty_like(edges) - for i,(u,v) in enumerate(edges): + edges2 = np.empty_like(edges) + for i, (u, v) in enumerate(edges): l_u = labels[u] l_v = labels[v] if l_u <= l_v: - edges2[i,0]=u - edges2[i,1]=v - else: #reverse order - edges2[i,0]=v - edges2[i,1]=u + edges2[i, 0] = u + edges2[i, 1] = v + else: # reverse order + edges2[i, 0] = v + edges2[i, 1] = u return edges2 - def normalise_undirected_edges(edges, labels=None): """Normlises undirected edges""" if labels is None: - edges2=np.empty_like(edges) - edges2[:,0]=np.minimum(edges[:,0],edges[:,1]) - edges2[:,1]=np.maximum(edges[:,0],edges[:,1]) + edges2 = np.empty_like(edges) + edges2[:, 0] = np.minimum(edges[:, 0], edges[:, 1]) + edges2[:, 1] = np.maximum(edges[:, 0], edges[:, 1]) else: edges2 = normalise_undirected_edges_by_labels(edges, labels) return edges2 - def get_unique_edges_from_edge_list(edges, is_directed): """Returns unique code per edge for edgelist edges""" - edges = np.array(edges, dtype=np.uint64).copy() + edges = np.array(edges, dtype=np.int64).copy() if not is_directed: # need to "sort edges" - edges=normalise_undirected_edges(edges) - return edges[:,0]*np.iinfo(np.uint32).max + edges[:,1] - + edges = normalise_undirected_edges(edges) + return edges[:, 0] * np.iinfo(np.int32).max + edges[:, 1] def get_unique_edges(G): @@ -225,20 +217,33 @@ def get_unique_edges(G): def edges_to_str(edges): """Returns an edge list as a string that can be copy and pasted elsewhere""" - return str(edges).replace("\n", "" ).replace(" ", ", ").replace(", ,", ", ").replace("[, ", "[") + return ( + str(edges) + .replace("\n", "") + .replace(" ", ", ") + .replace(", ,", ", ") + .replace("[, ", "[") + ) class AutoList: """A class that appends on assigning values to any of the designated attributes""" + def __init__(self, names): self.__names = names - self.__dict = defaultdict(lambda : defaultdict(list)) + self.__dict = defaultdict(lambda: defaultdict(list)) self._phi = None - def __setattr__(self, name:str, value): - if name in ("__names", "__dicts", "_AutoList__names", "_AutoList__dicts", "phi"): + def __setattr__(self, name: str, value): + if name in ( + "__names", + "__dicts", + "_AutoList__names", + "_AutoList__dicts", + "phi", + ): return super().__setattr__(name, value) - #print(name, value) + # print(name, value) if name in self.__names: self.__dict[name][self._phi].append(value) else: @@ -248,8 +253,8 @@ def set_phi(self, phi): # pylint: disable = missing-function-docstring self._phi = phi def __getattr__(self, name): - #print("getattr", name) - #if name == "_auto_list__phi": + # print("getattr", name) + # if name == "_auto_list__phi": # return self._phi if name in self.__names: return self.__dict[name] @@ -257,25 +262,24 @@ def __getattr__(self, name): return self.__getattribute__(name) - def make_directed(edges): """Converts undirected edges to directed edges i.e. when edges contains only either 0-1 or 1-0 then the result contains both 1->0 and 0->1 """ n = edges.shape[0] - out_edges = np.empty((n*2,2),dtype = edges.dtype) - out_edges[:n,:] = edges - out_edges[n:,0] = edges[:, 1] - out_edges[n:,1] = edges[:, 0] + out_edges = np.empty((n * 2, 2), dtype=edges.dtype) + out_edges[:n, :] = edges + out_edges[n:, 0] = edges[:, 1] + out_edges[n:, 1] = edges[:, 0] return out_edges def switch_in_out(edges): """small helper function that switches in edges to out edges and vice versa""" edges_tmp = np.empty_like(edges) - edges_tmp[:,0]=edges[:,1] - edges_tmp[:,1]=edges[:,0] + edges_tmp[:, 0] = edges[:, 1] + edges_tmp[:, 1] = edges[:, 0] return edges_tmp @@ -292,26 +296,26 @@ def find_all_cycles(perm): the element at position 5 is its own cycle """ n = len(perm) - cycle_arr = np.empty(n,dtype=perm.dtype) - cycle_lengths = np.empty(n,dtype=perm.dtype) + cycle_arr = np.empty(n, dtype=perm.dtype) + cycle_lengths = np.empty(n, dtype=perm.dtype) num_cycles = 0 seen = np.zeros(n, dtype=np.bool_) - length_cycle=0 + length_cycle = 0 for start in range(n): if seen[start]: continue current_pos = start - #if perm[current_pos]==start: + # if perm[current_pos]==start: # continue while True: - seen[current_pos]=True - cycle_arr[length_cycle]=current_pos - length_cycle+=1 + seen[current_pos] = True + cycle_arr[length_cycle] = current_pos + length_cycle += 1 current_pos = perm[current_pos] if current_pos == start: break cycle_lengths[num_cycles] = length_cycle - num_cycles+=1 + num_cycles += 1 return cycle_arr, cycle_lengths[0:num_cycles] @@ -322,15 +326,15 @@ def inplace_reorder_last_axis(arr, order): The reordering happens inplace (almost) For an array of shape m,n we need O(n) additional memory """ - m,n = arr.shape + m, n = arr.shape cycles, lengths = find_all_cycles(order) - tmp = arr[0,0] # make sure variable persists throughout loops - start = 0 # make sure variable persists throughout loops + tmp = arr[0, 0] # make sure variable persists throughout loops + start = 0 # make sure variable persists throughout loops for i in range(m): start = 0 for stop in lengths: tmp = arr[i, cycles[start]] - for cycle_index in range(start, stop-1): - arr[i,cycles[cycle_index]] = arr[i,cycles[cycle_index+1]] - arr[i, cycles[stop-1]] = tmp - start=stop + for cycle_index in range(start, stop - 1): + arr[i, cycles[cycle_index]] = arr[i, cycles[cycle_index + 1]] + arr[i, cycles[stop - 1]] = tmp + start = stop diff --git a/nestmodel/tests/utils_for_test.py b/src/nestmodel/utils_for_test.py similarity index 59% rename from nestmodel/tests/utils_for_test.py rename to src/nestmodel/utils_for_test.py index fe0e280..7f517ab 100644 --- a/nestmodel/tests/utils_for_test.py +++ b/src/nestmodel/utils_for_test.py @@ -1,23 +1,28 @@ import numba import numpy as np + + def remove_numba(func, seen=None, allowed_packages=tuple()): clean_up = {} if seen is None: seen = {} if hasattr(func, "py_func"): - #clean_up["self"] = func - seen[func]=func.py_func + # clean_up["self"] = func + seen[func] = func.py_func func = func.py_func - if isinstance(func, type(remove_numba)): to_iter = func.__globals__ + def set_func(key, value): to_iter[key] = value + elif str(type(func)) == "" and func.__package__ in allowed_packages: + def set_func(key, value): setattr(func, key, value) - to_iter = {key : getattr(func, key) for key in dir(func)} + + to_iter = {key: getattr(func, key) for key in dir(func)} else: raise NotImplementedError(type(func)) for key, maybe_func in to_iter.items(): @@ -25,54 +30,70 @@ def set_func(key, value): continue if isinstance(maybe_func, (int, float, str)): continue - if str(type(maybe_func)) == "" and maybe_func.__package__ in allowed_packages: - #print("module", maybe_func) + if ( + str(type(maybe_func)) == "" + and maybe_func.__package__ in allowed_packages + ): + # print("module", maybe_func) if maybe_func in seen: continue seen[maybe_func] = None - #print(seen) - non_numba_handle, handle_cleanup = remove_numba(maybe_func, seen, allowed_packages) - clean_up["__module__"+key] = handle_cleanup + # print(seen) + non_numba_handle, handle_cleanup = remove_numba( + maybe_func, seen, allowed_packages + ) + clean_up["__module__" + key] = handle_cleanup continue - if not isinstance(maybe_func, (type(remove_numba), numba.core.registry.CPUDispatcher)): + if not isinstance( + maybe_func, (type(remove_numba), numba.core.registry.CPUDispatcher) + ): continue if maybe_func in seen: - #print("Seen") - #print(maybe_func) + # print("Seen") + # print(maybe_func) clean_up[key] = maybe_func - clean_up["__children__"+key] = {} + clean_up["__children__" + key] = {} set_func(key, seen[maybe_func]) continue if hasattr(maybe_func, "py_func"): - non_numba_handle, handle_cleanup = remove_numba(maybe_func, seen, allowed_packages) + non_numba_handle, handle_cleanup = remove_numba( + maybe_func, seen, allowed_packages + ) clean_up[key] = maybe_func - clean_up["__children__"+key] = handle_cleanup + clean_up["__children__" + key] = handle_cleanup set_func(key, non_numba_handle) return func, clean_up -def restore_numba(func, clean_up, parents = []): - #print(parents, func) + +def restore_numba(func, clean_up, parents=[]): + # print(parents, func) if isinstance(func, type(remove_numba)): + def set_func(key, value): func.__globals__[key] = value + def get_func(key): return func.__globals__[key] + elif str(type(func)) == "": + def set_func(key, value): setattr(func, key, value) + def get_func(key): return getattr(func, key) + else: - raise NotImplementedError(str(type(func)) + " "+ str(func)) + raise NotImplementedError(str(type(func)) + " " + str(func)) for key, value in clean_up.items(): if key.startswith("__children__"): continue if key.startswith("__module__"): - short_key = key[len("__module__"):] - restore_numba(get_func(short_key), clean_up[key], parents+[func]) + short_key = key[len("__module__") :] + restore_numba(get_func(short_key), clean_up[key], parents + [func]) continue if get_func(key) is value: continue - restore_numba(get_func(key), clean_up["__children__"+key], parents+[func]) - set_func(key, value) \ No newline at end of file + restore_numba(get_func(key), clean_up["__children__" + key], parents + [func]) + set_func(key, value) diff --git a/nestmodel/visualization.py b/src/nestmodel/visualization.py similarity index 56% rename from nestmodel/visualization.py rename to src/nestmodel/visualization.py index f7ef922..105ba17 100644 --- a/nestmodel/visualization.py +++ b/src/nestmodel/visualization.py @@ -3,6 +3,8 @@ from collections import defaultdict, Counter + + def relative_colors(partitions, start, stop, shorten=True): """Computes relative colors instead of the global colors for the following input colors per round @@ -26,15 +28,15 @@ def relative_colors(partitions, start, stop, shorten=True): i_tpl = defaultdict(tuple) parent_counter = defaultdict(list) num_nodes = Counter() - num_nodes[tuple()]= len(partitions[0]) + num_nodes[tuple()] = len(partitions[0]) for depth in range(start, stop): for i in range(len(partitions[depth])): if depth == start: i_tpl[i] += (partitions[depth][i],) - num_nodes[i_tpl[i]]+=1 + num_nodes[i_tpl[i]] += 1 else: - new_tpl =i_tpl[i]+(partitions[depth][i],) + new_tpl = i_tpl[i] + (partitions[depth][i],) try: val = parent_counter[i_tpl[i]].index(new_tpl) @@ -42,127 +44,132 @@ def relative_colors(partitions, start, stop, shorten=True): val = len(parent_counter[i_tpl[i]]) parent_counter[i_tpl[i]].append(new_tpl) i_tpl[i] += (val,) - num_nodes[i_tpl[i]]+=1 + num_nodes[i_tpl[i]] += 1 if shorten: while True: changes = False new_i_tpl = i_tpl.copy() for i, tpl in i_tpl.items(): - if num_nodes[tpl]==num_nodes[tpl[:-1]]: - changes=True - new_i_tpl[i]= tpl[:-1] + if num_nodes[tpl] == num_nodes[tpl[:-1]]: + changes = True + new_i_tpl[i] = tpl[:-1] if changes: - i_tpl =new_i_tpl + i_tpl = new_i_tpl else: break return i_tpl def to_base_10_tpl(h): - return tuple(int(h[i:i+2], 16) for i in (1, 3, 5)) + return tuple(int(h[i : i + 2], 16) for i in (1, 3, 5)) + def to_base_10_arr(h): - return np.array(tuple(int(h[i:i+2], 16) for i in (1, 3, 5))) + return np.array(tuple(int(h[i : i + 2], 16) for i in (1, 3, 5))) + def to_base_16(tpl): out = "#" for i in range(3): tmp = hex(tpl[i])[2:] - if len(tmp)==1: - out+="0"+tmp + if len(tmp) == 1: + out += "0" + tmp else: - out+=tmp - assert len(out)==7, (out, tpl) + out += tmp + assert len(out) == 7, (out, tpl) return out -def modify_color(start_hex, vals=(40,40)): + +def modify_color(start_hex, vals=(40, 40)): if isinstance(start_hex, str): in_colors = {} - in_colors[(0,)]=start_hex + in_colors[(0,)] = start_hex elif isinstance(start_hex, dict): - in_colors=start_hex + in_colors = start_hex - colors=in_colors.copy() + colors = in_colors.copy() for name, h in in_colors.items(): base_color = to_base_10_arr(h) - for i, m in enumerate( [ - [0,0,0], - [1,0,0], - [0,1,0], - [0,0,1], - [1,1,0], - [0,1,1], - [1,0,1], - [1,1,1] - ]): - mod =np.array(m, dtype=float) - #mod/=mod.sum() + for i, m in enumerate( + [ + [0, 0, 0], + [1, 0, 0], + [0, 1, 0], + [0, 0, 1], + [1, 1, 0], + [0, 1, 1], + [1, 0, 1], + [1, 1, 1], + ] + ): + mod = np.array(m, dtype=float) + # mod/=mod.sum() # blue 15, 40 # red 15, 30 - #vals = (15,40) - if len(name)!= 1: - mod*=vals[0] + # vals = (15,40) + if len(name) != 1: + mod *= vals[0] else: - mod*=vals[1] - mod=mod.astype(int) + mod *= vals[1] + mod = mod.astype(int) tmp = base_color.copy() tmp -= np.array(mod, dtype=int) tmp = np.maximum(tmp, 0) hex_col = to_base_16(tmp) - colors[name+(i,)]=hex_col + colors[name + (i,)] = hex_col return colors def get_family_node_colors(G_nx, depth, strength): - """ Obtain colors that represent the wl colors of the graph G_nx - - - """ + """Obtain colors that represent the wl colors of the graph G_nx""" G = to_fast_graph(G_nx) G.ensure_edges_prepared() return get_familiy_node_colors_for_partition(G.base_partitions, depth, strength) - def get_familiy_node_colors_for_partition(partitions, depth, strength): """Obtain colors that represent the families present in partitions returns a list of hex color codes that should resemble the family tree of the partitions """ max_depth = len(partitions) - depth = min(max_depth-1, depth) + depth = min(max_depth - 1, depth) color_arr = partitions[depth] colors = partitions[depth] num_nodes = len(partitions[0]) - if depth==0 and np.all(color_arr == color_arr[0]): - colors="gray" + if depth == 0 and np.all(color_arr == color_arr[0]): + colors = ["gray" for _ in range(num_nodes)] else: colors_map = { - (0,): '#a2cffe', # turquoise '#AFEEEE', - (2,): '#aaff32', # green #'#90EE90', - (1,): '#fe7b7c', # red - (3,): '#ffff14', # yellow # '#fffe71', - (4,): '#DDA0DD', # purple - (5,): '#8cffdb', # green/blue - (6,): '#ffa756', # orange - (7,): '#ffb2d0', # pink - } + (0,): "#a2cffe", # turquoise '#AFEEEE', + (2,): "#aaff32", # green #'#90EE90', + (1,): "#fe7b7c", # red + (3,): "#ffff14", # yellow # '#fffe71', + (4,): "#DDA0DD", # purple + (5,): "#8cffdb", # green/blue + (6,): "#ffa756", # orange + (7,): "#ffb2d0", # pink + } for _ in range(depth): - colors_map = modify_color(colors_map, vals=(strength,strength)) - depth_zero_is_uniform = np.all( partitions[0] == color_arr[0]) - node_tpls = relative_colors(partitions, int(depth_zero_is_uniform), depth+depth_zero_is_uniform) - #print(node_tpls) + colors_map = modify_color(colors_map, vals=(strength, strength)) + depth_zero_is_uniform = np.all(partitions[0] == color_arr[0]) + node_tpls = relative_colors( + partitions, int(depth_zero_is_uniform), depth + depth_zero_is_uniform + ) + + # print(node_tpls) def get_color(i): tpl = node_tpls[i] return colors_map[tpl] + colors = [get_color(i) for i in range(num_nodes)] return colors - -def draw_network_with_colors(G_nx, depth=0, pos=None, strength = 40, **kwargs): +def draw_network_with_colors(G_nx, depth=0, pos=None, strength=40, **kwargs): import networkx as nx + colors = get_family_node_colors(G_nx, depth, strength=strength) - #print(colors) - nx.draw_networkx(G_nx, pos=pos, node_color = colors, **kwargs) \ No newline at end of file + # print(colors) + nx.draw_networkx(G_nx, pos=pos, node_color=colors, **kwargs) diff --git a/nestmodel/wl.py b/src/nestmodel/wl.py similarity index 66% rename from nestmodel/wl.py rename to src/nestmodel/wl.py index 5f1c6fb..2cf861c 100644 --- a/nestmodel/wl.py +++ b/src/nestmodel/wl.py @@ -5,34 +5,39 @@ import numpy as np - -def labelings_are_equivalent(labels1 : List[int], labels2 : List[int], verbosity=0) -> bool: +def labelings_are_equivalent( + labels1: List[int], labels2: List[int], verbosity=0 +) -> bool: """Check whether two labeling Lists are equivalent""" if not len(set(labels1)) == len(set(labels2)): - if verbosity>2: + if verbosity > 2: print("number of labels disagree") return False - if not sorted(list(Counter(labels1).values()))== sorted(list(Counter(labels2).values())): - if verbosity>2: + if not sorted(list(Counter(labels1).values())) == sorted( + list(Counter(labels2).values()) + ): + if verbosity > 2: print("counts per color disagree") return False - map1 = {} # maps labels from 1 onto + map1 = {} # maps labels from 1 onto for label1, label2 in zip(labels1, labels2): if label1 in map1: - if not map1[label1]==label2: + if not map1[label1] == label2: if verbosity > 2: - print(f"label conflict for {label1} mapped to {map1[label1]} and {label2}") + print( + f"label conflict for {label1} mapped to {map1[label1]} and {label2}" + ) return False else: - map1[label1]=label2 + map1[label1] = label2 return True - def get_neighbors_labels_nx(G, node_id, last_labeling): """Return the neighbors_labels in networkx""" return tuple(sorted([last_labeling[j] for j in G.neighbors(node_id)])) + def get_in_neighbors_labels_nx(G, node_id, last_labeling): """Return the IN neighbors_labels in networkx""" return tuple(sorted([last_labeling[j] for j in G.predecessors(node_id)])) @@ -45,12 +50,18 @@ def get_neighbors_labels_gt(G, node_id, last_labeling): return tuple(neighbor_labels) - -def WL(g, k_max=30, use_components=False, verbosity=0, return_meanings=False, add_labelings=False): +def WL( + g, + k_max=30, + use_components=False, + verbosity=0, + return_meanings=False, + add_labelings=False, +): """Compute the WL colors for the given Graph g""" is_nx = isinstance(g, (nx.Graph, nx.DiGraph)) if is_nx: - labelings = [np.zeros(len(g.nodes()),dtype=int)] + labelings = [np.zeros(len(g.nodes()), dtype=int)] if isinstance(g, nx.DiGraph): get_neighbor_labels = get_in_neighbors_labels_nx else: @@ -60,15 +71,18 @@ def WL(g, k_max=30, use_components=False, verbosity=0, return_meanings=False, ad get_neighbor_labels = get_neighbors_labels_gt node_iter = g.get_vertices() if use_components: - import graph_tool.all as gt # pylint: disable=import-outside-toplevel # type: ignore - component_colors,_ = gt.label_components(g)#pylint: disable=unbalanced-tuple-unpacking + import graph_tool.all as gt # pylint: disable=import-outside-toplevel # type: ignore + + component_colors, _ = gt.label_components( + g + ) # pylint: disable=unbalanced-tuple-unpacking labelings = [np.array(component_colors.get_array())] else: - labelings = [np.zeros(len(g.get_vertices()),dtype=int)] + labelings = [np.zeros(len(g.get_vertices()), dtype=int)] - meanings=[] + meanings = [] for k in range(k_max): - if verbosity>1: + if verbosity > 1: print(k) colors = {} labeling = np.empty_like(labelings[0]) @@ -79,16 +93,16 @@ def WL(g, k_max=30, use_components=False, verbosity=0, return_meanings=False, ad color = colors[color_meaning] else: color = len(colors) - colors[color_meaning]=color + colors[color_meaning] = color labeling[i] = color meanings.append(colors) labelings.append(labeling) - if verbosity>0: + if verbosity > 0: print("number of colors", len(colors)) if labelings_are_equivalent(labeling, labelings[-2]): - labelings=labelings[:-1] - if verbosity>0: + labelings = labelings[:-1] + if verbosity > 0: print(f"converged in {k} iterations") break @@ -119,55 +133,52 @@ def add_labelings_gt(g, labelings, verbosity=False): vp = g.new_vertex_property("int") g.vertex_properties[f"color_{i}"] = vp - if verbosity>3: + if verbosity > 3: print(f"adding colors {i}") - vp.get_array()[:]=labeling + vp.get_array()[:] = labeling - if verbosity>3: + if verbosity > 3: print("done adding color") - - def get_edge_block_memberships(g, colors) -> Counter: """Calculate the number of edges that fall between the classes defined by colors""" edges = g.get_edges() - color_arr = np.vstack((colors[edges[:,0]], colors[edges[:,1]])).T + color_arr = np.vstack((colors[edges[:, 0]], colors[edges[:, 1]])).T if not g.is_directed(): # sort colors such that (Blue,Red) and (Red,Blue) edges are treated the same - e1 = np.min(color_arr,axis=1) - e2 = np.max(color_arr,axis=1) + e1 = np.min(color_arr, axis=1) + e2 = np.max(color_arr, axis=1) else: - e1=color_arr[:,0] - e2=color_arr[:,1] + e1 = color_arr[:, 0] + e2 = color_arr[:, 1] l = sorted(zip(e1, e2)) - return Counter((a,b) for a,b in l) + return Counter((a, b) for a, b in l) -def assert_block_memberships_agree(b1 : Counter, b2 : Counter): - """ check that the number of edges between blocks agree""" - assert len(b1)==len(b2) +def assert_block_memberships_agree(b1: Counter, b2: Counter): + """check that the number of edges between blocks agree""" + assert len(b1) == len(b2) for key, val in b1.items(): - assert b2[key]==val + assert b2[key] == val -def check_block_colorings_are_preserved(g1,g2, labels): +def check_block_colorings_are_preserved(g1, g2, labels): """Checks that the number of edges that fall between the blocks defined by labels agree for g1 and g2""" b1 = get_edge_block_memberships(g1, labels) b2 = get_edge_block_memberships(g2, labels) - assert_block_memberships_agree(b1,b2) - + assert_block_memberships_agree(b1, b2) def check_neighbor_color_histograms_agree(g1, g2, labels1, labels2=None): - """ function to validate that all nodes in g1 has similarly + """function to validate that all nodes in g1 has similarly colored neighbors as the same node in g2""" if labels2 is None: - labels2=labels1 - assert g1.num_vertices()==g1.num_vertices(), "number of nodes don't agree" + labels2 = labels1 + assert g1.num_vertices() == g1.num_vertices(), "number of nodes don't agree" for i in g1.vertices(): c1 = np.sort(labels1[g1.get_all_neighbors(i)]) c2 = np.sort(labels2[g2.get_all_neighbors(i)]) - assert np.all(c1==c2), f"histogram of neighbors of {i} disagree {c1} {c2}" + assert np.all(c1 == c2), f"histogram of neighbors of {i} disagree {c1} {c2}" diff --git a/src/nestmodel/wl_nlogn.py b/src/nestmodel/wl_nlogn.py new file mode 100644 index 0000000..756f88f --- /dev/null +++ b/src/nestmodel/wl_nlogn.py @@ -0,0 +1,691 @@ +# pylint: disable=invalid-name, consider-using-enumerate, missing-function-docstring + +from numba import njit +from numba.types import int32 +import numpy as np + + +# from numba import objmode +# import time +@njit([(int32[:], int32[:])], cache=True) +def get_in_degree(end_neighbors, neighbors): + """Compute the in degree""" + # n_nodes = len(end_neighbors)-1 + in_degree = np.zeros(len(end_neighbors) - 1, dtype=np.int32) + + for i in range(len(neighbors)): + in_degree[neighbors[i]] += 1 + + return in_degree + + +@njit(cache=True) +def get_degree_partition(in_degree, max_degree): + n_nodes = len(in_degree) + num_per_degree = np.zeros(max_degree + 2, dtype=np.int32) + for i in range(len(in_degree)): + num_per_degree[in_degree[i]] += 1 + + start_nodes_by_class = np.empty(n_nodes, dtype=np.int32) + end_nodes_by_class = np.empty(n_nodes, dtype=np.int32) + prev_end = 0 + num_classes = 0 + for i in range(0, len(num_per_degree)): + num_nodes_this_class = num_per_degree[i] + if num_nodes_this_class == 0: + continue + num_per_degree[i] = prev_end + + start_nodes_by_class[num_classes] = prev_end + end_nodes_by_class[num_classes] = prev_end + num_nodes_this_class + prev_end += num_nodes_this_class + num_classes += 1 + + position_of_node = np.empty(n_nodes, dtype=np.int32) + nodes_by_class = np.empty(n_nodes, dtype=np.int32) + for i in range(n_nodes): + nodes_by_class[num_per_degree[in_degree[i]]] = i + position_of_node[i] = num_per_degree[in_degree[i]] + num_per_degree[in_degree[i]] += 1 + + classes = np.empty(n_nodes, dtype=np.int32) + for i in range(num_classes): + for j in range(start_nodes_by_class[i], end_nodes_by_class[i]): + classes[nodes_by_class[j]] = i + + class_costs = np.zeros(num_classes, dtype=np.int32) + for i in range(num_classes): + num_nodes = end_nodes_by_class[i] - start_nodes_by_class[i] + num_edges = in_degree[start_nodes_by_class[i]] + class_costs[i] = num_edges * num_nodes + + queue = np.empty((n_nodes, 2), dtype=np.int32) + class_order = np.argsort(class_costs)[::-1] + for i in range(num_classes): + c = class_order[i] + queue[i, 0] = start_nodes_by_class[c] + queue[i, 1] = end_nodes_by_class[c] + + return ( + num_classes, + start_nodes_by_class, + end_nodes_by_class, + nodes_by_class, + classes, + position_of_node, + queue, + ) + + +@njit([(int32[:], int32[:], int32[:])], cache=True) +def color_refinement_nlogn(end_neighbors, neighbors, initial_labels): + """Compute the coarsest WL refinement""" + # print("neighbprs") + # print(end_neighbors) + # print(neighbors) + n_nodes = len(end_neighbors) - 1 + max_degree = n_nodes + starts_from_degree = np.all(initial_labels == initial_labels[0]) + if starts_from_degree: + # with objmode(t1='double'): # annotate return type + # t1 = time.process_time() + in_degree = get_in_degree(end_neighbors, neighbors) + ( + num_classes, + start_nodes_by_class, + end_nodes_by_class, + nodes_by_class, + classes, + position_of_node, + queue, + ) = get_degree_partition(in_degree, in_degree.max()) + # with objmode(t2='double'): # annotate return type + # t2 = time.process_time() + # print(t2-t1) + depth = 1 + else: + # print("initial", initial_labels) + ( + num_classes, + start_nodes_by_class, + end_nodes_by_class, + nodes_by_class, + classes, + position_of_node, + queue, + ) = get_degree_partition(initial_labels, initial_labels.max()) + depth = 0 + # num_classes=1 + # start_nodes_by_class = np.empty(n_nodes, dtype=np.int32) + # start_nodes_by_class[0] = 0 + # end_nodes_by_class = np.empty(n_nodes, dtype=np.int32) + # end_nodes_by_class[0] = n_nodes + # position_of_node = np.arange(n_nodes, dtype=np.int32) + # nodes_by_class = np.arange(n_nodes, dtype=np.int32) + # queue = np.empty(n_nodes, dtype=np.int32) + # queue[0]=0 + # original = np.arange(len(neighbors)) + # where = np.arange(len(neighbors)) + out_classes = np.empty((n_nodes, 3), dtype=np.int32) + for i in range(num_classes): + out_classes[i, 0] = start_nodes_by_class[i] + out_classes[i, 1] = end_nodes_by_class[i] + out_classes[i, 2] = depth + + start_neighbors = end_neighbors[:-1].copy() + end_neighbors = end_neighbors[1:] + + # per node characteristics + # classes = np.zeros(n_nodes, dtype=np.int32) + receive_counts = np.zeros(n_nodes, dtype=np.int32) + node_is_active = np.zeros(n_nodes, dtype=np.bool_) + # position_of_node = np.arange(n_nodes, dtype=np.int32) + + # nodes per class + # start_nodes_by_class = np.empty(n_nodes, dtype=np.int32) + # start_nodes_by_class[0] = 0 # first class contains all nodes + # end_nodes_by_class = np.empty(n_nodes, dtype=np.int32) + # end_nodes_by_class[0] = n_nodes # first class contains all nodes + # nodes_by_class = np.arange(n_nodes) + received_nodes_by_class = np.empty(n_nodes, dtype=np.int32) + + # queue + # queue = np.empty(n_nodes, dtype=np.int32) + # queue[0:num_classes]=np.arange(num_classes) + queue_R = num_classes + queue_L = 0 + class_in_queue = np.zeros(n_nodes, dtype=np.bool_) + class_in_queue[:num_classes] = True + classes_processed = 0 + + # num_classes = 1 + active_nodes_in_class = np.zeros( + n_nodes, dtype=np.int32 + ) # counts the number of nodes that are affected by message passing of current class + active_classes = np.empty( + n_nodes, dtype=np.int32 + ) # max_size: if all nodes have unique degree active_classes = n_nodes-1 + num_active_classes = 0 + + # per group statistics + group_ids = np.zeros( + max_degree + 1, dtype=np.int32 + ) # max_size is upper bounded by max_degree+1 as no node can have count > max_degree + num_nodes_per_group_scattered = np.zeros( + max_degree + 1, dtype=np.int32 + ) # max_size: see above + num_nodes_per_group = np.zeros( + max_degree + 1, dtype=np.int32 + ) # max_size: see above + # nodes_in_group = np.zeros(n_nodes, dtype=np.int32) + # # max_size=n_nodes because it is full in first iteration, afterwards could be max_i(num_nodes_with_degree=i * i) + nodes_indices_by_group = np.zeros(max_degree + 1, dtype=np.int32) + class_name_for_group = np.zeros(max_degree + 1, dtype=np.int32) + + # # performance metrics + # num_messages = 0 + # num_groups_1 = 0 + # num_groups_2 = 0 + # num_groups_3 = 0 + # num_groups_x = 0 + # min_group_id_0 =0 + # min_group_id_1 =0 + # min_group_id_x =0 + # max_group_id_1 =0 + # max_group_id_2 =0 + # max_group_id_x =0 + # min_max_group_id_0_1 = 0 + # largest_group_id_0 = 0 + # special_case = 0 + # normal_case = 0 + + # neighborhood_10 = 0 + # neighborhood_x = 0 + # all_unique = 0 + # num_active_1=0 + # num_active_2=0 + while queue_L < queue_R and num_classes < n_nodes: + # print("queue", queue, queue_L, queue_R) + # print(start_nodes_by_class) + # print(end_nodes_by_class) + # print(nodes_by_class) + depth += 1 + last_queue_R = queue_R + while queue_L < last_queue_R and num_classes < n_nodes: + # print("queue", queue, queue_L, queue_R, last_queue_R) + # print(queue_len) + start_class = queue[queue_L, 0] + end_class = queue[queue_L, 1] + # assert send_class < num_classes + # class_in_queue[send_class]=False + queue_L += 1 + + # Performance tracking + classes_processed += 1 + # if classes_processed == 2: + # print("initial_messages", num_messages) + + # print("class", classes) + # print("sending class", send_class) + # print("start_nodes_by_class", start_nodes_by_class[send_class]) + # print("end_nodes_by_class", end_nodes_by_class[send_class]) + # print("nodes_by_class", nodes_by_class[start_nodes_by_class[send_class]: end_nodes_by_class[send_class]]) + # print() + + # print("receive_counts==0", receive_counts) + # print("classes", classes) + num_active_classes = 0 + # if (end_nodes_by_class[send_class]-start_nodes_by_class[send_class]) == 1: + # single_node_propagation(send_class, nodes_by_class, start_nodes_by_class, + # end_neighbors, neighbors, + # receive_counts, classes, active_classes, position_of_node, class_in_queue, queue, + # num_classes, + # queue_R, received_nodes_by_class) + # continue + # print("max", active_nodes_in_class[:num_classes].max()) + if (start_class - end_class) == 1: + # print("special") + # special_case +=1 + # special case if the class has only one(!) node + sending_node = nodes_by_class[start_class] + # print("lonely_node", sending_node) + num_active_classes = 0 + for j in range( + start_neighbors[sending_node], end_neighbors[sending_node] + ): + neigh = neighbors[j] + # num_messages+=1 + if not node_is_active[neigh]: + neigh_class = classes[neigh] + # mark node as needing processing + received_nodes_by_class[ + start_nodes_by_class[neigh_class] + + active_nodes_in_class[neigh_class] + ] = neigh + active_nodes_in_class[neigh_class] += 1 + + if active_nodes_in_class[neigh_class] == 1: + # mark class as active + active_classes[num_active_classes] = neigh_class + num_active_classes += 1 + # if num_active_classes == 1: + # num_active_1 +=1 + # elif num_active_classes == 2: + # num_active_2 +=1 + for active_class_index in range(num_active_classes): + active_class = active_classes[active_class_index] + total_nodes_this_class = ( + end_nodes_by_class[active_class] + - start_nodes_by_class[active_class] + ) + + if total_nodes_this_class == 1: + # classes with only 1 node never need to be a receiving class again + # thus set the node to be active + i_node = start_nodes_by_class[active_class] + node = received_nodes_by_class[i_node] + node_is_active[node] = True + continue + + if total_nodes_this_class == active_nodes_in_class[active_class]: + active_nodes_in_class[active_class] = 0 + continue + else: + # prepare the two new classes + L = start_nodes_by_class[active_class] + R = ( + start_nodes_by_class[active_class] + + active_nodes_in_class[active_class] + ) + active_nodes_in_class[active_class] = 0 + new_class = num_classes + num_classes += 1 + start_nodes_by_class[new_class] = L + end_nodes_by_class[new_class] = R + + out_classes[new_class, 0] = L + out_classes[new_class, 1] = R + out_classes[new_class, 2] = depth + start_nodes_by_class[active_class] = R + # print("start", start_nodes_by_class[:num_classes]) + # print("end", end_nodes_by_class[:num_classes]) + index = L + for i_node in range(L, R): + node = received_nodes_by_class[i_node] + classes[node] = new_class + # swap node positions + start_pos_node = position_of_node[node] + swap_node = nodes_by_class[index] + nodes_by_class[index] = node + position_of_node[node] = index + nodes_by_class[start_pos_node] = swap_node + position_of_node[swap_node] = start_pos_node + # nodes_indices_by_group[group_id]+=1 + index += 1 + + # print("in queue A", active_class, class_in_queue[active_class]) + # if class_in_queue[active_class]: + # put_in = new_class + # else: + if ( + active_nodes_in_class[active_class] + > total_nodes_this_class // 2 + ): + put_in = new_class + else: + put_in = active_class + + queue[queue_R, 0] = start_nodes_by_class[put_in] + queue[queue_R, 1] = end_nodes_by_class[put_in] + # class_in_queue[put_in] = True + queue_R += 1 + # print(classes) + # classes_are_mono(start_nodes_by_class, end_nodes_by_class, num_classes, nodes_by_class, classes) + continue + + # normal_case +=1 + + for i in range(start_class, end_class): + sending_node = nodes_by_class[i] + for j in range( + start_neighbors[sending_node], end_neighbors[sending_node] + ): + neigh = neighbors[j] + # print("sending", sending_node, neigh) + # num_messages+=1 + # for sending_node in nodes_by_class[start_nodes_by_class[send_class]: end_nodes_by_class[send_class]]: + # for neigh in neighbors[end_neighbors[sending_node]:end_neighbors[sending_node+1]]: + receive_counts[neigh] += 1 + neigh_class = classes[neigh] + if not node_is_active[neigh]: + # mark node as needing processing + received_nodes_by_class[ + start_nodes_by_class[neigh_class] + + active_nodes_in_class[neigh_class] + ] = neigh + active_nodes_in_class[neigh_class] += 1 + node_is_active[neigh] = True + + if ( + active_nodes_in_class[neigh_class] == 1 + ): # the current neigh node makes it's class active + # enque class into queue of active classes + active_classes[num_active_classes] = neigh_class + num_active_classes += 1 + # for class_ in active_classes[:num_active_classes]: + # active_nodes_in_class[class_] = 0 + # print() + # print() + # print("active classes") + # print(num_active_classes) + # print("active_classes", active_classes[:num_active_classes]) + # print("count", receive_counts) + # print("-------------------------------------------------------") + # print("ordered", classes[nodes_by_class]) + # print("receive", receive_counts) + for active_class_index in range( + num_active_classes + ): # loop over all classes which were potentially split by this action + # classes_are_mono(start_nodes_by_class, end_nodes_by_class, num_classes, nodes_by_class, classes) + active_class = active_classes[active_class_index] + # print("class ranges", list( zip(start_nodes_by_class[:num_classes], end_nodes_by_class[:num_classes]))) + # s=[] + # for start, end in zip(start_nodes_by_class[:num_classes], end_nodes_by_class[:num_classes]): + # s.append("".join(str(classes[node]) for node in nodes_by_class[start: end])) + + # print(" | ".join(s)) + # print(active_classes[:num_active_classes]) + + # print("active class", active_class) + # print("active range", start_nodes_by_class[active_class], end_nodes_by_class[active_class]) + num_active_nodes_this_class = active_nodes_in_class[active_class] + + # resetting node information + for i_node in range( + start_nodes_by_class[active_class], + start_nodes_by_class[active_class] + num_active_nodes_this_class, + ): + node = received_nodes_by_class[i_node] + node_is_active[node] = False + # print("activ", num_active_nodes_this_class) + active_nodes_in_class[active_class] = 0 + + total_nodes_this_class = ( + end_nodes_by_class[active_class] + - start_nodes_by_class[active_class] + ) + # print("total", total_nodes_this_class) + # print() + # assert total_nodes_this_class >= num_active_nodes_this_class + if total_nodes_this_class == 1: + # classes with only 1 node never need to be an receiving class again + i_node = start_nodes_by_class[active_class] + node = received_nodes_by_class[i_node] + node_is_active[node] = ( + True # this ensures this node never lands in the active queue again + ) + continue + + # print(total_nodes_this_class, non_active_group_size) + num_groups = 0 + # find the groups and the number of nodes per group + for i_node in range( + start_nodes_by_class[active_class], + start_nodes_by_class[active_class] + num_active_nodes_this_class, + ): + node = received_nodes_by_class[i_node] + group_id = receive_counts[node] + num_nodes_per_group_scattered[group_id] += 1 # identify group sizes + if ( + num_nodes_per_group_scattered[group_id] == 1 + ): # add this group to queue of groups + group_ids[num_groups] = group_id + num_groups += 1 + # print("num_g", num_groups) + + # there might be nodes which are not adjacent to the currently sending class + # we are treating these incoming degree zero nodes here + non_active_group_size = ( + total_nodes_this_class - num_active_nodes_this_class + ) + if non_active_group_size > 0: + group_ids[num_groups] = 0 + num_nodes_per_group_scattered[0] = non_active_group_size + num_groups += 1 + # print("groups",group_ids[:num_groups]) + # print("ngrou", num_groups) + + # if num_groups==1: + # num_groups_1+=1 + # elif num_groups==2: + # num_groups_2+=1 + # elif num_groups==3: + # num_groups_3+=1 + # else: + # num_groups_x+=1 + + if num_groups == 1: # nothing to be done, active class is not split + # reset the counts + for node in nodes_by_class[ + start_nodes_by_class[active_class] : end_nodes_by_class[ + active_class + ] + ]: + receive_counts[node] = 0 + for i in range(num_groups): + num_nodes_per_group_scattered[group_ids[i]] = 0 + continue + + # min_group_id = group_ids[:num_groups].min() + # if min_group_id == 0: + # min_group_id_0 +=1 + # elif min_group_id == 1: + # min_group_id_1 +=1 + # else: + # min_group_id_x +=1 + + # max_group_id = group_ids[:num_groups].max() + # if max_group_id == 1: + # max_group_id_1 +=1 + # elif max_group_id == 2: + # max_group_id_2 +=1 + # else: + # max_group_id_x+=1 + # if min_group_id == 0 and max_group_id == 1: + # min_max_group_id_0_1 +=1 + + # collect num_nodes_per_group from scattered + for i in range(num_groups): + num_nodes_per_group[i] = num_nodes_per_group_scattered[group_ids[i]] + num_nodes_per_group_scattered[group_ids[i]] = 0 + # print(num_nodes_per_group) + + # in the following we determine two special classes: + # 1) the not_relabeled_group and 2) the not_enqueued aka the largest_group + # the not relabeled group is usually the degree zero node group, + # but in case there is no degree zero group, take the largest + # the largest group is simply one of the largest groups (doesn't matter which) + # collect largest group statistics + _largest_group_index = np.argmax(num_nodes_per_group[:num_groups]) + largest_group_size = num_nodes_per_group[_largest_group_index] + if ( + largest_group_size == non_active_group_size + ): # in case that the largest and the zero partition are of identical size, just take 0 + largest_group_id = 0 + else: + largest_group_id = group_ids[_largest_group_index] + if non_active_group_size == 0: + not_relabeled_group_id = largest_group_id + else: + not_relabeled_group_id = 0 + # if largest_group_id == 0: + # largest_group_id_0 +=1 + + # ----- begin collecting nodes into groups ----- + # cumsum num_nodes_per_group + for i in range(1, num_groups): + num_nodes_per_group[i] = ( + num_nodes_per_group[i - 1] + num_nodes_per_group[i] + ) + # print("largest", largest_group_id) + # print("start", start_nodes_by_class[:num_classes+1]) + # print("end ", end_nodes_by_class[:num_classes+1]) + # print("per_g", num_nodes_per_group[:num_groups]) + # scatter node indices to group locations + offset = start_nodes_by_class[active_class] + end_offset = ( + start_nodes_by_class[active_class] + num_active_nodes_this_class + ) + assert offset < end_offset + # print("groups", group_ids[:num_groups]) + for i in range(num_groups): + group_id = group_ids[i] + if i == 0: + nodes_indices_by_group[group_id] = ( + 0 # nodes_indices_by_group will contain the position of this group in the order + ) + else: + nodes_indices_by_group[group_id] = num_nodes_per_group[i - 1] + + if ( + group_id == not_relabeled_group_id + ): # there are some nodes that are not relabeled, they keep the active class + class_name_for_group[group_id] = active_class + else: + class_name_for_group[group_id] = ( + num_classes # this group will become a new class + ) + num_classes += 1 + # print("in queue B", active_class, class_in_queue[active_class]) + put_in = False + # if class_in_queue[active_class]: + # if group_id != not_relabeled_group_id: + # put_in = True + # else: + if group_id == largest_group_id: + put_in = False + else: + put_in = True + group_class = class_name_for_group[group_id] + start_nodes_by_class[group_class] = ( + offset + nodes_indices_by_group[group_id] + ) + + # if i == 0: + # start_nodes_by_class[group_class] = offset + # else: + # start_nodes_by_class[group_class] = offset + num_nodes_per_group[i-1] + end_nodes_by_class[group_class] = offset + num_nodes_per_group[i] + + if put_in: + queue[queue_R, 0] = start_nodes_by_class[group_class] + queue[queue_R, 1] = end_nodes_by_class[group_class] + # class_in_queue[group_class] = True + queue_R += 1 + + if ( + group_class != active_class + ): # active class is already present in out_classes + out_classes[group_class, 0] = start_nodes_by_class[group_class] + out_classes[group_class, 1] = end_nodes_by_class[group_class] + out_classes[group_class, 2] = depth + + # print("changing", new_group_ids[group_id]) + # assert end_nodes_by_class[class_name_for_group[group_id]] + # >start_nodes_by_class[class_name_for_group[group_id]] + + # print(nodes_indices_by_group[group_ids[:num_groups]]) + # print(class_name_for_group[group_ids[:num_groups]]) + # print("start", start_nodes_by_class[:num_classes+1]) + # print("end ", end_nodes_by_class[:num_classes+1]) + # print(nodes_by_class) + for i_node in range(offset, end_offset): + node = received_nodes_by_class[i_node] + # print("processing", node, classes[node], "->", class_name_for_group[group_id]) + group_id = receive_counts[node] + receive_counts[node] = ( + 0 # reset this value, to be used again in the future + ) + # print(group_id) + classes[node] = class_name_for_group[group_id] + group_class = class_name_for_group[group_id] + + # if (end_nodes_by_class[group_class]-start_nodes_by_class[group_class]) == 1: + # if True: + # print(neighbors) + # print(start_neighbors) + # print(end_neighbors) + # for i_remove in range(in_degree[node], in_degree[node+1]): + # to_remove_pos = in_neighbors_position[i_remove] + # affected_node = in_neighbors[i_remove] + + # def remove_from_out(to_delete, values, original, where, end_neighbors, node) + # print("affected", affected_node, to_remove_pos) + # remove_from_out(to_remove_pos, neighbors, original, where, end_neighbors, affected_node) + + # node_is_active[node] = True + # print(neighbors, end_neighbors) + + if group_id == not_relabeled_group_id: + continue + + # swap node positions + start_pos_node = position_of_node[node] + target_index = ( + nodes_indices_by_group[group_id] + offset + ) # target location of node + swap_node = nodes_by_class[ + target_index + ] # the node currently at the swap position + nodes_by_class[target_index] = node # place current node + position_of_node[node] = ( + target_index # set position of current node + ) + nodes_by_class[start_pos_node] = swap_node # place other node + position_of_node[swap_node] = ( + start_pos_node # set position of other node + ) + nodes_indices_by_group[group_id] += 1 # increase counter + # classes_are_mono(start_nodes_by_class, end_nodes_by_class, num_classes, nodes_by_class, classes) + # print("swap", node, swap_node, start_pos_node, index) + # print(nodes_by_class) + # print(nodes_by_class[position_of_node]) + + if starts_from_degree: + out_classes[0, 0] = 0 + out_classes[0, 1] = n_nodes + out_classes[0, 2] = 0 + + # print("num_groups1", num_groups_1) + # print("num_groups2", num_groups_2) + # print("num_groups3", num_groups_3) + # print("num_groupsx", num_groups_x) + + # print("min_group_id_0", min_group_id_0) + # print("min_group_id_1", min_group_id_1) + # print("min_group_id_x", min_group_id_x) + # print("max_group_id_1", max_group_id_1) + # print("max_group_id_2", max_group_id_2) + # print("max_group_id_x", max_group_id_x) + # print("minmax_group_id_0_1", min_max_group_id_0_1) + # print("largest_group_id_0", largest_group_id_0) + # print("special_case", special_case) + # print("normal_case ", normal_case) + # print("neigh_10", neighborhood_10) + # print("neigh_X ", neighborhood_x) + # print("all_unqiue", all_unique) + # print("num_active_1", num_active_1) + # print("num_active_2", num_active_2) + # print("total_ messages", num_messages) + # print("classes_processed", classes_processed) + # start_nodes_by_class[:num_classes].sort() + # end_nodes_by_class[:num_classes].sort() + # print(start_nodes_by_class[:10]) + # print(end_nodes_by_class[:10]) + # print(out_classes[:20]) + + # print() + # print("result") + # print(classes) + # print(nodes_by_class) + return position_of_node, out_classes[0:num_classes, :] diff --git a/nestmodel/tests/test_centralities.py b/tests/test_centralities.py similarity index 62% rename from nestmodel/tests/test_centralities.py rename to tests/test_centralities.py index 24c20c0..8c2d3fd 100644 --- a/nestmodel/tests/test_centralities.py +++ b/tests/test_centralities.py @@ -4,86 +4,107 @@ import networkx as nx from nestmodel.fast_graph import FastGraph -from nestmodel.centralities import calc_pagerank, calc_eigenvector, calc_hits, calc_katz, calc_katz_iter - +from nestmodel.centralities import ( + calc_pagerank, + calc_eigenvector, + calc_hits, + calc_katz, + calc_katz_iter, +) def verify_nx(G, v_base, centrality): v = centrality(G.to_nx()) np.testing.assert_almost_equal(v, v_base) + def verify_fg(G, v_base, centrality): v = centrality(G) np.testing.assert_almost_equal(v, v_base) + def verify_gt(G, v_base, centrality): v = centrality(G.to_gt()) np.testing.assert_almost_equal(v, v_base) + def verify_all(G, v_base, calc_centrality): verify_nx(G, v_base, calc_centrality) verify_fg(G, v_base, calc_centrality) - try : + try: verify_gt(G, v_base, calc_centrality) except ModuleNotFoundError: - import warnings # pylint: disable=import-outside-toplevel + import warnings # pylint: disable=import-outside-toplevel + warnings.warn("graph_tool not found", Warning) def hubs_wrapper(G): return calc_hits(G)[0] + def auth_wrapper(G): return calc_hits(G)[1] + def to_vec(v): if isinstance(v, dict): - v = np.array(list({i : v[i] for i in range(len(v))}.values())) + v = np.array(list({i: v[i] for i in range(len(v))}.values())) return v -def normalize(v): - v=to_vec(v) - return v/v.sum() +def normalize(v): + v = to_vec(v) + return v / v.sum() + class TestCentralities(unittest.TestCase): - def test_pagerank_simple(self, ): - G = FastGraph(np.array([[0,1], [2,1]], dtype=np.uint32), True) + def test_pagerank_simple( + self, + ): + G = FastGraph(np.array([[0, 1], [2, 1]], dtype=np.int32), True) v_base = nx.pagerank(G.to_nx(), tol=1e-15, max_iter=300) v_base = normalize(v_base) verify_all(G, v_base, calc_pagerank) - - def test_pagerank_karate(self, ): + def test_pagerank_karate( + self, + ): G = FastGraph.from_nx(nx.karate_club_graph()) v_base = nx.pagerank(G.to_nx(), tol=1e-15, max_iter=300) v_base = normalize(v_base) verify_all(G, v_base, calc_pagerank) - - def test_eigenvector_simple(self, ): - G = FastGraph(np.array([[0,1], [1,2], [2,0]], dtype=np.uint32), True) + def test_eigenvector_simple( + self, + ): + G = FastGraph(np.array([[0, 1], [1, 2], [2, 0]], dtype=np.int32), True) v_base = nx.eigenvector_centrality(G.to_nx(), tol=1e-15, max_iter=300) v_base = normalize(v_base) verify_all(G, v_base, calc_eigenvector) - - def test_eigenvector_simple2(self, ): - G = FastGraph(np.array([[0,1], [0,2], [1,3], [2,3], [3,0]], dtype=np.uint32), True) + def test_eigenvector_simple2( + self, + ): + G = FastGraph( + np.array([[0, 1], [0, 2], [1, 3], [2, 3], [3, 0]], dtype=np.int32), True + ) v_base = nx.eigenvector_centrality(G.to_nx(), tol=1e-15, max_iter=300) v_base = normalize(v_base) verify_all(G, v_base, calc_eigenvector) - - def test_eigenvector_karate(self, ): + def test_eigenvector_karate( + self, + ): G = FastGraph.from_nx(nx.karate_club_graph()) v_base = nx.eigenvector_centrality(G.to_nx(), tol=1e-15, max_iter=300) v_base = normalize(v_base) verify_all(G, v_base, calc_eigenvector) - - def test_hits_karate(self, ): + def test_hits_karate( + self, + ): G = FastGraph.from_nx(nx.karate_club_graph()) h, a = nx.hits(G.to_nx(), tol=1e-15, max_iter=600) h = normalize(h) @@ -92,9 +113,10 @@ def test_hits_karate(self, ): verify_all(G, h, hubs_wrapper) verify_all(G, a, auth_wrapper) - - def test_hits_simple(self, ): - G = FastGraph(np.array([[0,1], [2,1]], dtype=np.uint32), True) + def test_hits_simple( + self, + ): + G = FastGraph(np.array([[0, 1], [2, 1]], dtype=np.int32), True) h, a = nx.hits(G.to_nx(), tol=1e-15, max_iter=600) h = normalize(h) a = normalize(a) @@ -102,31 +124,43 @@ def test_hits_simple(self, ): verify_all(G, h, hubs_wrapper) verify_all(G, a, auth_wrapper) - - def test_katz_simple(self, ): - G = FastGraph(np.array([[0,1], [2,1]], dtype=np.uint32), True) - v_base = nx.katz_centrality(G.to_nx(), tol=1e-15, max_iter=300, normalized=False) + def test_katz_simple( + self, + ): + G = FastGraph(np.array([[0, 1], [2, 1]], dtype=np.int32), True) + v_base = nx.katz_centrality( + G.to_nx(), tol=1e-15, max_iter=300, normalized=False + ) v_base = to_vec(v_base) verify_all(G, v_base, calc_katz) - - def test_katz_karate(self, ): + def test_katz_karate( + self, + ): G = FastGraph.from_nx(nx.karate_club_graph()) - v_base = nx.katz_centrality(G.to_nx(), tol=1e-15, max_iter=300, normalized=False) + v_base = nx.katz_centrality( + G.to_nx(), tol=1e-15, max_iter=300, normalized=False + ) v_base = to_vec(v_base) verify_all(G, v_base, calc_katz) - - def test_katz2_simple(self, ): - G = FastGraph(np.array([[0,1], [2,1]], dtype=np.uint32), True) - v_base = nx.katz_centrality(G.to_nx(), tol=1e-15, max_iter=300, normalized=False) + def test_katz2_simple( + self, + ): + G = FastGraph(np.array([[0, 1], [2, 1]], dtype=np.int32), True) + v_base = nx.katz_centrality( + G.to_nx(), tol=1e-15, max_iter=300, normalized=False + ) v_base = to_vec(v_base) verify_all(G, v_base, calc_katz_iter) - - def test_katz2_karate(self, ): + def test_katz2_karate( + self, + ): G = FastGraph.from_nx(nx.karate_club_graph()) - v_base = nx.katz_centrality(G.to_nx(), tol=1e-15, max_iter=300, normalized=False) + v_base = nx.katz_centrality( + G.to_nx(), tol=1e-15, max_iter=300, normalized=False + ) v_base = to_vec(v_base) verify_all(G, v_base, calc_katz_iter) @@ -142,6 +176,5 @@ def test_pagerank_1_node(self): np.testing.assert_array_equal(arr, [1.0]) - -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/nestmodel/tests/test_colorings.py b/tests/test_colorings.py similarity index 63% rename from nestmodel/tests/test_colorings.py rename to tests/test_colorings.py index e08f652..974d46c 100644 --- a/nestmodel/tests/test_colorings.py +++ b/tests/test_colorings.py @@ -1,51 +1,62 @@ # pylint: disable=missing-function-docstring, missing-class-docstring, wrong-import-position -import faulthandler -faulthandler.enable() +# import faulthandler + +# faulthandler.enable() import unittest from itertools import product import numpy as np from numpy.testing import assert_array_equal -from nestmodel.colorings import get_depthx_colors_internal, make_labeling_compact, RefinementColors -from nestmodel.tests.testing import check_colorings_agree +from nestmodel.colorings import ( + get_depthx_colors_internal, + make_labeling_compact, + RefinementColors, +) +from nestmodel.testing import check_colorings_agree def safe_diff(arr1, arr2): - return np.maximum(arr1,arr2) - np.minimum(arr1,arr2) - + return np.maximum(arr1, arr2) - np.minimum(arr1, arr2) class TestColorings(unittest.TestCase): def test_make_labeling_compact_1(self): - arr = np.zeros(10, dtype=np.uint32) + arr = np.zeros(10, dtype=np.int32) make_labeling_compact(arr) - np.testing.assert_array_equal(arr, np.zeros(10, dtype=np.uint32)) - + np.testing.assert_array_equal(arr, np.zeros(10, dtype=np.int32)) def test_make_labeling_compact_2(self): - arr = np.arange(11, dtype=np.uint32) + arr = np.arange(11, dtype=np.int32) make_labeling_compact(arr) - np.testing.assert_array_equal(arr, np.arange(11, dtype=np.uint32)) - + np.testing.assert_array_equal(arr, np.arange(11, dtype=np.int32)) def test_make_labeling_compact_3(self): - arr = np.arange(11, dtype=np.uint32) + arr = np.arange(11, dtype=np.int32) arr[1] = 1000 make_labeling_compact(arr) - np.testing.assert_array_equal(arr, np.arange(11, dtype=np.uint32)) - - + np.testing.assert_array_equal(arr, np.arange(11, dtype=np.int32)) def test_get_colors_internal(self): # the triples are (start, stop, depth) # triples need to be sorted first increasing by start and then increasing by depth - color_ranges = np.array([(0,5,0), (0,1,2), (1,3,1), (2,3,3), (3,4,4), (5,7,0), (5,6,2)], dtype=np.uint32) + color_ranges = np.array( + [ + (0, 5, 0), + (0, 1, 2), + (1, 3, 1), + (2, 3, 3), + (3, 4, 4), + (5, 7, 0), + (5, 6, 2), + ], + dtype=np.int32, + ) solutions = [ - [0,0,0,0,0,5,5], - [0,2,2,0,0,5,5], - [1,2,2,0,0,6,5], - [1,2,3,0,0,6,5], - [1,2,3,4,0,6,5] + [0, 0, 0, 0, 0, 5, 5], + [0, 2, 2, 0, 0, 5, 5], + [1, 2, 2, 0, 0, 6, 5], + [1, 2, 3, 0, 0, 6, 5], + [1, 2, 3, 4, 0, 6, 5], ] for depth, sol in enumerate(solutions): colors = get_depthx_colors_internal(color_ranges, len(sol), depth=depth) @@ -67,57 +78,51 @@ def validate_refinement_colors_object(self, order, ranges, solutions): for depth, sol in enumerate(solutions): if external: - check_colorings_agree(colors[depth,:], sol) - corlor_d = color_obj.get_colors_for_depth(depth, external=external, compact=compact) + check_colorings_agree(colors[depth, :], sol) + corlor_d = color_obj.get_colors_for_depth( + depth, external=external, compact=compact + ) check_colorings_agree(corlor_d, sol) else: - check_colorings_agree(colors[depth,:], sol[order]) - corlor_d = color_obj.get_colors_for_depth(depth, external=external, compact=compact) + check_colorings_agree(colors[depth, :], sol[order]) + corlor_d = color_obj.get_colors_for_depth( + depth, external=external, compact=compact + ) check_colorings_agree(corlor_d, sol[order]) - def test_refinement_colors_1(self): """Test on a line of length three""" order = np.array([0, 2, 1]) - ranges = np.array([[0, 3, 0], - [2, 3, 1]], dtype=np.uint32) + ranges = np.array([[0, 3, 0], [2, 3, 1]], dtype=np.int32) solutions = [ np.zeros(len(order)), - [0,1,0], + [0, 1, 0], ] self.validate_refinement_colors_object(order, ranges, solutions) - def test_refinement_colors_2(self): """Test on a line of length 5""" order = np.array([0, 4, 1, 3, 2], dtype=np.int64) - ranges = np.array([[0, 5, 0], - [2, 5, 1], - [4, 5, 2]], dtype=np.uint32) + ranges = np.array([[0, 5, 0], [2, 5, 1], [4, 5, 2]], dtype=np.int32) solutions = [ np.zeros(len(order)), - [0,1,1,1,0], - [0,1,2,1,0], + [0, 1, 1, 1, 0], + [0, 1, 2, 1, 0], ] self.validate_refinement_colors_object(order, ranges, solutions) - def test_refinement_colors_3(self): """Test on a line of length 8""" order = np.array([0, 7, 1, 6, 2, 5, 3, 4], dtype=np.int64) - ranges = np.array([[0, 8, 0], - [2, 8, 1], - [4, 8, 2], - [6, 8, 3]], dtype=np.uint32) + ranges = np.array([[0, 8, 0], [2, 8, 1], [4, 8, 2], [6, 8, 3]], dtype=np.int32) solutions = [ np.zeros(8), [0, 1, 1, 1, 1, 1, 1, 0], [0, 1, 2, 2, 2, 2, 1, 0], - [0, 1, 2, 3, 3, 2, 1, 0] + [0, 1, 2, 3, 3, 2, 1, 0], ] self.validate_refinement_colors_object(order, ranges, solutions) - -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_fast_graph.py b/tests/test_fast_graph.py new file mode 100644 index 0000000..e1c6cbd --- /dev/null +++ b/tests/test_fast_graph.py @@ -0,0 +1,691 @@ +# pylint: disable=missing-function-docstring, missing-class-docstring +import unittest +import networkx as nx + +from numpy.testing import assert_array_equal +import numpy as np +import os +import nestmodel +import platform +from nestmodel.fast_graph import FastGraph + +from nestmodel.utils_for_test import restore_numba, remove_numba + + +def arr_to_tuple(arr, should_sort=True): + if should_sort: + l = [tuple(sorted(list(a))) for a in arr] + return tuple(sorted(list(l))) + else: + l = [tuple(a) for a in arr] + return tuple(l) + + +class TestFastGraph(unittest.TestCase): + def test_edges(self): + edges = np.array([[0, 1]], dtype=np.int32) + G = FastGraph(edges.copy(), is_directed=True) + assert_array_equal(edges, G.edges) + + # ----------------- Testing IO -------------------- + + def test_save_npz(self): + edges = np.array([[0, 1]], dtype=np.int32) + G = FastGraph(edges.copy(), is_directed=True) + G.save_npz("./out.npz") + + G2 = FastGraph.load_npz("./out.npz") + assert_array_equal(edges, G2.edges) + + os.remove("./out.npz") + + def test_save_npz_wl(self): + edges = np.array([[0, 1], [1, 2]], dtype=np.int32) + G = FastGraph(edges.copy(), is_directed=False) + G.ensure_edges_prepared() + G.save_npz("./out.npz", include_wl=True) + + G2 = FastGraph.load_npz("./out.npz") + preserved_attrs = [ + "edges", + "base_partitions", + "wl_iterations", + "edges_classes", + "is_mono", + "block_indices", + ] + for attr in preserved_attrs: + assert_array_equal(getattr(G, attr), getattr(G2, attr)) + + os.remove("./out.npz") + + def test_copy(self): + edges = np.array([[0, 1]], dtype=np.int32) + G = FastGraph(edges.copy(), is_directed=True) + G2 = G.copy() + + self.assertFalse(G is G2) + assert_array_equal(G2.edges, G.edges) + self.assertFalse(G2.edges is G.edges) + + def test_from_nx(self): + with self.subTest(advanced_labels=True): + a = "A" + b = "B" + c = "C" + G = nx.Graph() + G.add_nodes_from([a, b, c]) + G.add_edges_from([(a, b), (b, c)]) + G_nx, mapping = FastGraph.from_nx(G, allow_advanced_node_labels=True) + self.assertDictEqual(mapping, {0: "A", 1: "B", 2: "C"}) + assert_array_equal(G_nx.edges, [[0, 1], [1, 2]]) + with self.subTest(advanced_labels=False): + a = 0 + b = 1 + c = 2 + G = nx.Graph() + G.add_nodes_from([a, b, c]) + G.add_edges_from([(a, b), (b, c)]) + G_nx = FastGraph.from_nx(G) + assert_array_equal(G_nx.edges, [[0, 1], [1, 2]]) + + def test_to_coo(self): + edges = np.array([[0, 1], [1, 2]], dtype=np.int32) + G = FastGraph(edges.copy(), is_directed=False) + arr = G.to_csr() + assert_array_equal(arr.data, np.array([1.0, 1.0, 1.0, 1.0])) + assert_array_equal(arr.indices, np.array([1, 0, 2, 1])) + assert_array_equal(arr.indptr, np.array([0, 1, 3, 4])) + + # ----------------- End Testing IO -------------------- + + # ----------------- Testing raises -------------------- + + def test_rewire3_raises(self): + edges = np.array([[0, 1], [2, 3]], dtype=np.int32) + G = FastGraph(edges.copy(), is_directed=True) + G.ensure_edges_prepared() + with self.assertRaises(NotImplementedError): + G.rewire(0, method=3, seed=1, r=1, source_only=False) + + G = FastGraph(edges.copy(), is_directed=False) + G.ensure_edges_prepared() + with self.assertRaises(NotImplementedError): + G.rewire(0, method=3, seed=1, r=1, source_only=True) + + with self.assertWarns(UserWarning): + G = FastGraph(edges.copy(), is_directed=True) + G.ensure_edges_prepared() + G.rewire(0, method=3, seed=1, r=1, source_only=True, parallel=True) + + def test_base_wl_wrong_colors_raises(self): + edges = np.array([[0, 1], [2, 3]], dtype=np.int32) + + G = FastGraph(edges.copy(), is_directed=True) + with self.assertRaises(ValueError): + G.ensure_edges_prepared(initial_colors="banana") + + def test_base_wl_after_rewire_raises(self): + edges = np.array([[0, 1], [2, 3]], dtype=np.int32) + + G = FastGraph(edges.copy(), is_directed=True) + G.ensure_edges_prepared() + G.rewire(0, method=1, seed=0, r=1) + with self.assertRaises(ValueError): + G.calc_base_wl() + + def calc_base_wl_after_rewire_raises(self): + G = FastGraph(np.array([[0, 1], [2, 3]], dtype=np.int32), is_directed=False) + G.ensure_edges_prepared() + G.rewire(0, 1, seed=0, r=1) + with self.assertRaises(ValueError): + G.calc_base_wl() + + def test_source_only_warns(self): + G = FastGraph(np.array([(0, 1)], dtype=np.int32), is_directed=True, num_nodes=3) + G.ensure_edges_prepared() + with self.assertWarns(RuntimeWarning): + G.rewire(0, method=1, seed=3, r=1, source_only=True) + # np.testing.assert_array_equal(G.edges, [[2, 1]]) + + # ----------------- End Testing raises -------------------- + + def test_rewire1_double_edge_2(self): + edges = np.array([[0, 1], [2, 3]], dtype=np.int32) + for parallel in [False, True]: + with self.subTest(parallel=parallel): + G = FastGraph(edges.copy(), is_directed=True) + G.ensure_edges_prepared() + G.rewire(0, method=2, seed=1, n_rewire=1, parallel=True) + assert_array_equal(G.edges, edges) + + edges2 = np.array([[0, 3], [2, 1]], dtype=np.int32) + G.rewire(0, 1, seed=0, n_rewire=1, parallel=True) + assert_array_equal(G.edges, edges2) + + def test_rewire1_double_edge(self): + edges_in = np.array([[0, 1], [2, 3]], dtype=np.int32) + + result_edges = [ + np.array([[0, 3], [1, 2]], dtype=np.int32), + np.array([[0, 1], [2, 3]], dtype=np.int32), + np.array([[0, 2], [3, 1]], dtype=np.int32), + np.array([[0, 1], [2, 3]], dtype=np.int32), + np.array([[0, 1], [2, 3]], dtype=np.int32), + np.array([[1, 0], [2, 3]], dtype=np.int32), + np.array([[0, 2], [3, 1]], dtype=np.int32), + np.array([[1, 2], [0, 3]], dtype=np.int32), + np.array([[1, 3], [2, 0]], dtype=np.int32), + np.array([[0, 3], [2, 1]], dtype=np.int32), + ] + + # from collections import defaultdict + # d = defaultdict(list) + # for seed in range(100_00): + # G = FastGraph(edges_in.copy(), is_directed=False) + # G.ensure_edges_prepared() + # G.rewire(0, 1, seed=seed, r=2, parallel=False) + # d[arr_to_tuple(G.edges, should_sort=True)].append(seed) + # for key, value in d.items(): + # print(key, value[0], " ", len(value)) + + for i, res_edges in enumerate(result_edges): + for parallel, seeds in zip( + [False, True], [list(range(10)), [5, 1, 4, 1, 1, 18, 4, 49, 6, 0]] + ): + with self.subTest(parallel=parallel, seed_index=i, seed=seeds[i]): + G = FastGraph(edges_in.copy(), is_directed=False) + G.ensure_edges_prepared() + + G.rewire(0, 1, seed=seeds[i], r=1, parallel=parallel) + assert_array_equal(G.edges, res_edges, f"{i}") + + def test_rewire1_double_edge_1(self): + edges = np.array([[0, 1], [2, 3]], dtype=np.int32) + for parallel, seed in zip([False, True], [1, 2]): + with self.subTest(parallel=parallel): + G = FastGraph(edges.copy(), is_directed=True) + G.ensure_edges_prepared() + G.rewire(0, 1, seed=seed, r=1, parallel=parallel) + assert_array_equal(G.edges, edges) + + edges2 = np.array([[0, 3], [2, 1]], dtype=np.int32) + for parallel, seed in zip([False, True], [0, 1]): + with self.subTest(parallel=parallel): + G = FastGraph(edges.copy(), is_directed=True) + G.ensure_edges_prepared() + G.rewire(0, method=1, seed=seed, r=1, parallel=parallel) + assert_array_equal(G.edges, edges2) + + def test_rewire3_double_edge_all(self): + # check all nine possible outcomes with direct sampling method for source only + edges = np.array([[0, 1], [2, 3]], dtype=np.int32) + + cases = [ + (((0, 1), (1, 3)), 0), + (((0, 1), (0, 3)), 1), + (((2, 1), (2, 3)), 3), + (((2, 1), (0, 3)), 4), + (((3, 1), (2, 3)), 5), + (((2, 1), (1, 3)), 6), + (((3, 1), (1, 3)), 7), + (((3, 1), (0, 3)), 12), + (((0, 1), (2, 3)), 25), + ] + for result, seed in cases: + with self.subTest(seed=seed): + G = FastGraph(edges.copy(), is_directed=True) + G.ensure_edges_prepared() + G.rewire(0, method=3, seed=seed, r=1, source_only=True) + assert_array_equal(G.edges, np.array(result, dtype=np.int32)) + + def test_fast_graph_directed_triangle(self): + """Test that a directed triangle is appropriately flipped""" + result = np.array([[1, 0], [2, 1], [0, 2]]) + + for parallel, seed in zip([False, True], [3, 4]): + with self.subTest(parallel=parallel): + G = FastGraph( + np.array([[0, 1], [1, 2], [2, 0]], dtype=np.int32), is_directed=True + ) + G.ensure_edges_prepared() + + G.rewire(0, 1, seed=seed, r=1, parallel=parallel) + assert_array_equal(G.edges, result) + + def test_rewire_large(self): + result = { + False: [ + [62, 40], + [65, 2], + [5, 30], + [7, 71], + [8, 13], + [10, 85], + [12, 9], + [14, 15], + [16, 17], + [18, 61], + [20, 21], + [22, 93], + [3, 76], + [26, 51], + [29, 43], + [19, 37], + [32, 11], + [35, 89], + [36, 31], + [38, 28], + [49, 84], + [42, 96], + [44, 45], + [46, 4], + [48, 34], + [50, 27], + [52, 53], + [54, 72], + [56, 69], + [99, 39], + [60, 0], + [25, 55], + [64, 63], + [66, 67], + [68, 24], + [70, 83], + [47, 23], + [74, 75], + [33, 1], + [82, 59], + [80, 6], + [73, 77], + [41, 57], + [86, 81], + [88, 94], + [90, 91], + [92, 100], + [95, 78], + [97, 79], + [98, 58], + [101, 87], + ], + True: np.array( + [ + [65, 67], + [43, 63], + [4, 3], + [6, 41], + [8, 7], + [10, 94], + [13, 0], + [15, 99], + [16, 17], + [18, 2], + [20, 21], + [22, 29], + [52, 24], + [26, 27], + [28, 23], + [30, 72], + [78, 61], + [34, 79], + [36, 37], + [39, 25], + [51, 58], + [42, 101], + [56, 11], + [1, 59], + [48, 49], + [50, 19], + [38, 57], + [54, 55], + [45, 5], + [9, 85], + [60, 91], + [77, 69], + [47, 40], + [66, 44], + [68, 75], + [70, 89], + [31, 95], + [74, 62], + [76, 88], + [97, 53], + [80, 81], + [82, 83], + [84, 46], + [86, 87], + [71, 32], + [90, 64], + [92, 93], + [73, 33], + [96, 35], + [98, 14], + [100, 12], + ] + ), + } + edges = np.array([[i, i + 1] for i in range(0, 102, 2)], dtype=np.int32) + + for parallel in [False, True]: + with self.subTest(parallel=parallel): + G = FastGraph(edges, is_directed=False) + G.ensure_edges_prepared() + + G.rewire(0, method=1, seed=0, r=1, parallel=parallel) + assert_array_equal(G.edges, result[parallel]) + + def test_rewire_large_dir(self): + result = { + False: np.array( + [ + [0, 69], + [2, 77], + [4, 53], + [6, 33], + [8, 95], + [10, 3], + [12, 13], + [14, 15], + [16, 31], + [18, 27], + [20, 21], + [22, 41], + [24, 49], + [26, 23], + [28, 63], + [30, 9], + [32, 39], + [34, 87], + [36, 71], + [38, 5], + [40, 29], + [42, 97], + [44, 99], + [46, 79], + [48, 25], + [50, 51], + [52, 1], + [54, 55], + [56, 57], + [58, 17], + [60, 61], + [62, 45], + [64, 81], + [66, 67], + [68, 37], + [70, 7], + [72, 83], + [74, 35], + [76, 59], + [78, 65], + [80, 85], + [82, 73], + [84, 43], + [86, 47], + [88, 89], + [90, 91], + [92, 93], + [94, 11], + [96, 19], + [98, 101], + [100, 75], + ] + ), + True: np.array( + [ + [0, 57], + [2, 47], + [4, 17], + [6, 41], + [8, 31], + [10, 79], + [12, 29], + [14, 99], + [16, 3], + [18, 19], + [20, 25], + [22, 95], + [24, 45], + [26, 27], + [28, 33], + [30, 73], + [32, 61], + [34, 35], + [36, 15], + [38, 39], + [40, 21], + [42, 97], + [44, 59], + [46, 43], + [48, 53], + [50, 51], + [52, 75], + [54, 69], + [56, 5], + [58, 55], + [60, 9], + [62, 63], + [64, 23], + [66, 89], + [68, 81], + [70, 71], + [72, 7], + [74, 49], + [76, 13], + [78, 67], + [80, 101], + [82, 87], + [84, 1], + [86, 85], + [88, 91], + [90, 77], + [92, 65], + [94, 83], + [96, 11], + [98, 37], + [100, 93], + ] + ), + } + edges = np.array([[i, i + 1] for i in range(0, 102, 2)], dtype=np.int32) + + for parallel in [False, True]: + with self.subTest(parallel=parallel): + G = FastGraph(edges, is_directed=True) + G.ensure_edges_prepared() + G.rewire(0, method=1, seed=0, r=1, parallel=parallel) + assert_array_equal(G.edges, result[parallel]) + + def test_source_only_rewiring(self): + """Test that for a graph with 3 nodes but one edge the rewiring seems correct""" + for parallel in [False, True]: + with self.subTest(parallel=parallel): + G = FastGraph( + np.array([(0, 1)], dtype=np.int32), is_directed=True, num_nodes=3 + ) + G.ensure_edges_prepared(sorting_strategy="source") + G.rewire(0, method=1, seed=3, r=1, source_only=True, parallel=parallel) + if platform.system() == "Darwin": + np.testing.assert_array_equal(G.edges, [[0, 1]]) + else: + np.testing.assert_array_equal(G.edges, [[2, 1]]) + + def test_prrewiring_only_rewiring(self): + G = FastGraph( + np.array( + [ + (0, 2), + (0, 3), + (2, 3), + (1, 4), + (6, 7), + (1, 5), + (0, 6), + (0, 7), + (1, 8), + (1, 9), + ], + dtype=np.int32, + ), + is_directed=False, + num_nodes=10, + ) + G.ensure_edges_prepared(sorting_strategy="source") + G.rewire(0, method=1, seed=3, r=1) + np.testing.assert_array_equal(G.block_indices[0], [[0, 10]]) + np.testing.assert_array_equal(G.block_indices[1], [[0, 8], [8, 10]]) + np.testing.assert_array_equal(G.block_indices[2], [[0, 4], [4, 8], [8, 10]]) + + def test_prrewiring_only_rewiring2(self): + """ + From the graph + 0 -> 2 + 1 -> 3 + to the graph + 0 -> 2 + 1 -> 3 + using initial colors to make node 2 and 3 different + which is only valid with the source only strategy + """ + result = [[0, 3], [1, 2]] + + for parallel, seed in zip([False, True], [5, 4]): + with self.subTest(parallel=parallel): + G = FastGraph( + np.array( + [ + (0, 2), + (1, 3), + ], + dtype=np.int32, + ), + is_directed=True, + num_nodes=4, + ) + + G.ensure_edges_prepared( + initial_colors=np.array([0, 0, 1, 2], np.int32), + sorting_strategy="source", + ) + G.rewire(0, method=1, seed=seed, r=1, parallel=parallel) + np.testing.assert_array_equal(G.edges, result) + + # ----------------- Begin Testing WL -------------------- + + def test_rewire_limited_depth(self): + G = FastGraph(np.array([(0, 2), (1, 2)], dtype=np.int32), is_directed=False) + G.ensure_edges_prepared(max_depth=1) + self.assertEqual(G.wl_iterations, 1) + + def test_wl_limited_depth(self): + edges = np.array([(0, 2), (1, 2), (2, 3)], dtype=np.int32) + G = FastGraph(edges, is_directed=False) + with self.assertRaises(ValueError): + G.ensure_edges_prepared(max_depth=0) + + G = FastGraph(edges, is_directed=False) + G.ensure_edges_prepared(max_depth=1) + self.assertEqual(G.wl_iterations, 1) + + G = FastGraph(edges, is_directed=False) + G.ensure_edges_prepared(max_depth=2) + self.assertEqual(G.wl_iterations, 2) + + def test_calc_wl(self): + edges = np.array([[0, 1], [2, 3], [2, 4]], dtype=np.int32) + G = FastGraph(edges, is_directed=True) + res1, res2 = [[0, 0, 0, 0, 0], [0, 1, 0, 1, 1]] + for algorithm in ["normal", "nlogn"]: + with self.subTest(algorithm=algorithm): + out1, out2 = G.calc_wl(algorithm=algorithm) + assert_array_equal(res1, out1) + assert_array_equal(res2, out2) + + def test_calc_wl_out_degree(self): + edges = np.array([[0, 1], [2, 3], [2, 4]], dtype=np.int32) + G = FastGraph(edges, is_directed=True) + for algorithm in ["normal", "nlogn"]: + with self.subTest(algorithm=algorithm): + res1, res2 = [[0, 1, 2, 1, 1], [0, 1, 2, 3, 3]] + out1, out2 = G.calc_wl("out_degree", algorithm=algorithm) + assert_array_equal(res1, out1) + assert_array_equal(res2, out2) + + def test_calc_wl_init_colors(self): + edges = np.array([[0, 1], [2, 3], [2, 4]], dtype=np.int32) + G = FastGraph(edges, is_directed=True) + for algorithm in ["normal", "nlogn"]: + with self.subTest(algorithm=algorithm): + res1, res2 = [[0, 1, 2, 1, 1], [0, 1, 2, 3, 3]] + out1, out2 = G.calc_wl( + np.array([0, 1, 2, 1, 1], dtype=np.int32), algorithm=algorithm + ) + assert_array_equal(res1, out1) + assert_array_equal(res2, out2) + + def test_calc_wl_both(self): + edges = np.array([[0, 1], [1, 2], [3, 4], [4, 5], [4, 6]], dtype=np.int32) + G = FastGraph(edges.copy(), is_directed=True) + results = [ + np.zeros(7, dtype=np.int32), + [0, 1, 2, 0, 3, 2, 2], + [0, 1, 2, 3, 4, 5, 5], + ] + res0, res1, res2 = results + start, out1, out2 = G.calc_wl_both() + assert_array_equal(res0, start) + assert_array_equal(res1, out1) + assert_array_equal(res2, out2) + + G.calc_base_wl(both=True) + self.assertEqual(G.wl_iterations, 3) + assert_array_equal(G.base_partitions, np.array(results)) + + G = FastGraph(edges.copy(), is_directed=True) + out1, out2 = G.calc_wl_both( + initial_colors=np.array([0, 1, 2, 0, 3, 2, 2], dtype=np.int32) + ) + assert_array_equal(res1, out1) + assert_array_equal(res2, out2) + + def test_wl_limited_depth_both(self): + edges = np.array([(0, 2), (1, 2), (2, 3)], dtype=np.int32) + G = FastGraph(edges, is_directed=False) + with self.assertRaises(ValueError): + G.ensure_edges_prepared(max_depth=0, both=True) + + G = FastGraph(edges, is_directed=False) + G.ensure_edges_prepared(max_depth=1, both=True) + self.assertEqual(G.wl_iterations, 1) + + G = FastGraph(edges, is_directed=False) + G.ensure_edges_prepared(max_depth=2, both=True) + self.assertEqual(G.wl_iterations, 2) + + # ----------------- End Testing WL -------------------- + + def test_smoke_erdos(self): + from nestmodel.mutual_independent_models import Gnp_row_first + + for p in [0.1, 0.3, 0.5]: + for seed in [1, 1337, 1234124]: + for n in [10, 20, 50]: + for is_directed in [False, True]: + edges = Gnp_row_first(n, p, seed=seed) + G = FastGraph(edges, is_directed=is_directed, num_nodes=n) + G.ensure_edges_prepared() + for d in range(len(G.base_partitions) - 1, -1, -1): + G.rewire(d, method=1, r=4) + G.rewire(d, method=1, r=4, parallel=True) + + +class TestFastGraphNonCompiled(TestFastGraph): + def setUp(self): + _, self.cleanup = remove_numba(nestmodel, allowed_packages=["nestmodel"]) + + def tearDown(self) -> None: + restore_numba(nestmodel, self.cleanup) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_fast_rewire.py b/tests/test_fast_rewire.py new file mode 100644 index 0000000..34c2949 --- /dev/null +++ b/tests/test_fast_rewire.py @@ -0,0 +1,77 @@ +# pylint: disable=missing-function-docstring, missing-class-docstring, wrong-import-position +# import faulthandler + +# faulthandler.enable() +import unittest +import numpy as np +from numpy.testing import assert_array_equal + +# from nestmodel.load_datasets import * +from nestmodel.fast_rewire import sort_edges + + +def safe_diff(arr1, arr2): + return np.maximum(arr1, arr2) - np.minimum(arr1, arr2) + + +class TestSortingMethods(unittest.TestCase): + def test_sort_edges1(self): + edges = np.array( + (np.zeros(8), np.arange(1, 9, dtype=np.int32)), dtype=np.int32 + ).T + + labels = np.array( + [ + np.array([0, 2, 2, 1, 1, 1, 1, 2, 2]), + np.array([0, 4, 4, 3, 3, 2, 2, 1, 1]), + ] + ) + edges_ordered, edges_classes_arr, dead_indicator, is_mono, strategy = ( + sort_edges(edges, labels, is_directed=True, sorting_strategy="both") + ) + + assert_array_equal(edges_ordered[:, 0], np.zeros(8, dtype=np.int32)) + assert_array_equal( + safe_diff(edges_ordered[:-1:2, 1], edges_ordered[1::2, 1]), [1, 1, 1, 1] + ) + self.check_class_sizes(edges_classes_arr[:, 0], [4, 4]) + self.check_class_sizes(edges_classes_arr[:, 1], [2, 2, 2, 2]) + + edges_ordered, edges_classes_arr, dead_indicator, is_mono, strategy = ( + sort_edges(edges, labels, is_directed=True, sorting_strategy="source") + ) + + assert_array_equal(edges_ordered[:, 0], np.zeros(8, dtype=np.int32)) + # compute difference of subsequent classes, it should be 1 + assert_array_equal( + safe_diff(edges_ordered[:-1:2, 1], edges_ordered[1::2, 1]), [1, 1, 1, 1] + ) + self.check_class_sizes(edges_classes_arr[:, 0], [8]) + self.check_class_sizes(edges_classes_arr[:, 1], [8]) + + edges = np.array( + (np.arange(1, 9, dtype=np.int32), np.zeros(8)), dtype=np.int32 + ).T + edges_ordered, edges_classes_arr, dead_indicator, is_mono, strategy = ( + sort_edges(edges, labels, is_directed=True, sorting_strategy="source") + ) + + assert_array_equal(edges_ordered[:, 1], np.zeros(8, dtype=np.int32)) + assert_array_equal( + safe_diff(edges_ordered[:-1:2, 0], edges_ordered[1::2, 0]), [1, 1, 1, 1] + ) + + self.check_class_sizes(edges_classes_arr[:, 0], [4, 4]) + self.check_class_sizes(edges_classes_arr[:, 1], [2, 2, 2, 2]) + + def check_class_sizes(self, arr, sizes): + class_sizes = np.unique(arr) + self.assertEqual(len(sizes), len(class_sizes), "The number of classes mismatch") + n = 0 + for size in sizes: + assert_array_equal(np.diff(arr[n : n + size]), np.zeros(size - 1)) + n += size + + +if __name__ == "__main__": + unittest.main() diff --git a/nestmodel/tests/test_fast_wl.py b/tests/test_fast_wl.py similarity index 52% rename from nestmodel/tests/test_fast_wl.py rename to tests/test_fast_wl.py index 2460aed..b092464 100644 --- a/nestmodel/tests/test_fast_wl.py +++ b/tests/test_fast_wl.py @@ -1,47 +1,79 @@ # pylint: disable=missing-function-docstring, missing-class-docstring, wrong-import-position -import faulthandler +# import faulthandler +# faulthandler.enable() + from itertools import combinations -faulthandler.enable() import unittest import numpy as np -#from nestmodel.load_datasets import * +# from nestmodel.load_datasets import * from nestmodel.fast_wl import WL_fast, to_in_neighbors, my_bincount -from nestmodel.tests.testing import check_colorings_agree +from nestmodel.testing import check_colorings_agree from nestmodel.colorings import RefinementColors from nestmodel.load_datasets import get_dataset_folder - def create_line_graph(n): - edges = np.empty((2*(n-1), 2), dtype=np.uint64) + edges = np.empty((2 * (n - 1), 2), dtype=np.int64) - edges[:n-1,0]= np.arange(n-1) - edges[:n-1,1]= np.arange(1,n) - edges[n-1:,1]= np.arange(n-1) - edges[n-1:,0]= np.arange(1,n) + edges[: n - 1, 0] = np.arange(n - 1) + edges[: n - 1, 1] = np.arange(1, n) + edges[n - 1 :, 1] = np.arange(n - 1) + edges[n - 1 :, 0] = np.arange(1, n) return edges + class TestFastWLMethods(unittest.TestCase): def test_to_in_neighbors(self): - edges = np.array([[0,1,0], [1,2,2]], dtype=np.uint32).T + edges = np.array([[0, 1, 0], [1, 2, 2]], dtype=np.int32).T arr1, arr2, _ = to_in_neighbors(edges, 0) - np.testing.assert_array_equal(arr1, [0,0,1,3]) - np.testing.assert_array_equal(arr2, [0,1,0]) + np.testing.assert_array_equal(arr1, [0, 0, 1, 3]) + np.testing.assert_array_equal(arr2, [0, 1, 0]) - def verify_wl_all(self, edges, number_of_nodes, solutions, order_sol, partitions_sol, labels=None, subtest=None): + def verify_wl_all( + self, + edges, + number_of_nodes, + solutions, + order_sol, + partitions_sol, + labels=None, + subtest=None, + ): if subtest is None: - self._verify_wl_all(edges, number_of_nodes, solutions, order_sol, partitions_sol, labels=labels) + self._verify_wl_all( + edges, + number_of_nodes, + solutions, + order_sol, + partitions_sol, + labels=labels, + ) else: with self.subTest(subtest=subtest): - self._verify_wl_all(edges, number_of_nodes, solutions, order_sol, partitions_sol, labels=labels) + self._verify_wl_all( + edges, + number_of_nodes, + solutions, + order_sol, + partitions_sol, + labels=labels, + ) - def _verify_wl_all(self, edges, number_of_nodes, solutions, order_sol, partitions_sol, labels=None): # pylint:disable=unused-argument + def _verify_wl_all( + self, edges, number_of_nodes, solutions, order_sol, partitions_sol, labels=None + ): # pylint:disable=unused-argument for method in ("normal", "nlogn"): with self.subTest(method=method): # print(method) - out, order, ranges = WL_fast(edges, number_of_nodes, labels=labels, return_all=True, method=method) + out, order, ranges = WL_fast( + edges, + number_of_nodes, + labels=labels, + return_all=True, + method=method, + ) # print(out) # print("order", order) @@ -59,102 +91,95 @@ def _verify_wl_all(self, edges, number_of_nodes, solutions, order_sol, partition with self.subTest(depth=depth): check_colorings_agree(arr, sol) - def verify_agreement(self, edges, number_of_nodes, labels=None, subtest=None): if subtest is None: self._verify_agreement(edges, number_of_nodes, labels=labels) else: with self.subTest(subtest=subtest): self._verify_agreement(edges, number_of_nodes, labels=labels) + def _verify_agreement(self, edges, number_of_nodes, labels=None): outs = [] for method in ("normal", "nlogn"): with self.subTest(method=method): # print(method) - out, order, ranges = WL_fast(edges, number_of_nodes, labels=labels, return_all=True, method=method) - outs.append((method+"_direct", out)) + out, order, ranges = WL_fast( + edges, + number_of_nodes, + labels=labels, + return_all=True, + method=method, + ) + outs.append((method + "_direct", out)) with self.subTest(kind="RefinementColors"): out2 = RefinementColors(ranges, order=order).get_colors_all_depths() - outs.append((method+"_indirect",out2)) + outs.append((method + "_indirect", out2)) - for ((label1, out1), (label2, out2)) in combinations(outs, 2): + for (label1, out1), (label2, out2) in combinations(outs, 2): with self.subTest(label1=label1, label2=label2): for depth, (arr1, arr2) in enumerate(zip(out1, out2)): with self.subTest(depth=depth): check_colorings_agree(arr1, arr2) def test_wl_line_3(self): - n=3 + n = 3 edges = create_line_graph(n) - solutions = [ - np.zeros(n, dtype=np.uint32), - [0, 1, 0] - ] - order_sol = [0,2,1] - partitions_sol = [[0,3,0], [2,3,1]] + solutions = [np.zeros(n, dtype=np.int32), [0, 1, 0]] + order_sol = [0, 2, 1] + partitions_sol = [[0, 3, 0], [2, 3, 1]] self.verify_wl_all(edges, n, solutions, order_sol, partitions_sol) def test_wl_line_5(self): - n=5 + n = 5 edges = create_line_graph(n) - solutions = [ - np.zeros(n, dtype=np.uint32), - [0, 1, 1, 1, 0], - [0, 1, 2, 1, 0] - - ] + solutions = [np.zeros(n, dtype=np.int32), [0, 1, 1, 1, 0], [0, 1, 2, 1, 0]] order_sol = [0, 4, 1, 3, 2] - partitions_sol = [[0,5,0], [2,5,1], [4,5,2]] + partitions_sol = [[0, 5, 0], [2, 5, 1], [4, 5, 2]] self.verify_wl_all(edges, n, solutions, order_sol, partitions_sol) def test_wl_line_8(self): - n=8 + n = 8 edges = create_line_graph(n) solutions = [ - np.zeros(n, dtype=np.uint32), - [0, 1, 1, 1, 1, 1, 1, 0], - [0, 1, 2, 2, 2, 2, 1, 0], - [0, 1, 2, 3, 3, 2, 1, 0] + np.zeros(n, dtype=np.int32), + [0, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 2, 2, 2, 2, 1, 0], + [0, 1, 2, 3, 3, 2, 1, 0], ] order_sol = [0, 7, 1, 6, 2, 5, 3, 4] - partitions_sol = [[0, 8, 0], - [2, 8, 1], - [4, 8, 2], - [6, 8, 3]] + partitions_sol = [[0, 8, 0], [2, 8, 1], [4, 8, 2], [6, 8, 3]] self.verify_wl_all(edges, n, solutions, order_sol, partitions_sol) def test_wl_line_7(self): - n=7 + n = 7 edges = create_line_graph(n) solutions = [ - np.zeros(n, dtype=np.uint32), - [0, 1, 1, 1, 1, 1, 0], - [0, 1, 2, 2, 2, 1, 0], - [0, 1, 2, 3, 2, 1, 0] + np.zeros(n, dtype=np.int32), + [0, 1, 1, 1, 1, 1, 0], + [0, 1, 2, 2, 2, 1, 0], + [0, 1, 2, 3, 2, 1, 0], ] order_sol = [0, 6, 1, 5, 2, 4, 3] - partitions_sol = [[0, 7, 0], - [2, 7, 1], - [4, 7, 2], - [6, 7, 3]] + partitions_sol = [[0, 7, 0], [2, 7, 1], [4, 7, 2], [6, 7, 3]] self.verify_wl_all(edges, n, solutions, order_sol, partitions_sol) def test_wl_line_7_1(self): """Now with imperfection""" - n=7 + n = 7 edges = create_line_graph(n) solutions = [ - [0,0,0,1,0,0,0], - [0, 1, 2, 3, 2, 1, 0], + [0, 0, 0, 1, 0, 0, 0], + [0, 1, 2, 3, 2, 1, 0], ] - starting_labels = np.array([0,0,0,100,0,0,0], dtype=np.uint32) + starting_labels = np.array([0, 0, 0, 100, 0, 0, 0], dtype=np.int32) self.verify_wl_all(edges, n, solutions, None, None, labels=starting_labels) - def test_wl_4(self): - edges = np.array([[0, 3], + edges = np.array( + [ + [0, 3], [1, 2], [2, 4], [2, 5], @@ -171,19 +196,25 @@ def test_wl_4(self): [7, 3], [8, 4], [8, 5], - [7, 6]], dtype=np.uint32) - + [7, 6], + ], + dtype=np.int32, + ) - solutions = [[0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 1, 2, 2, 2, 2, 2], - [0, 0, 1, 1, 2, 2, 2, 2, 3], - [0, 0, 1, 1, 2, 2, 3, 3, 4], - [0, 0, 1, 2, 3, 3, 4, 4, 5], - [0, 1, 2, 3, 4, 4, 5, 5, 6]] + solutions = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 2, 2, 2, 2, 2], + [0, 0, 1, 1, 2, 2, 2, 2, 3], + [0, 0, 1, 1, 2, 2, 3, 3, 4], + [0, 0, 1, 2, 3, 3, 4, 4, 5], + [0, 1, 2, 3, 4, 4, 5, 5, 6], + ] self.verify_wl_all(edges, 9, solutions, None, None) def test_wl_other_graph(self): - edges = np.array([[0, 5], + edges = np.array( + [ + [0, 5], [0, 6], [0, 9], [1, 6], @@ -193,10 +224,15 @@ def test_wl_other_graph(self): [3, 8], [4, 8], [5, 7], - [6, 9]], dtype=np.uint32) - solutions = [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 1, 2, 2, 3, 3], - [0, 0, 0, 0, 0, 1, 2, 3, 4, 5],] + [6, 9], + ], + dtype=np.int32, + ) + solutions = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 2, 2, 3, 3], + [0, 0, 0, 0, 0, 1, 2, 3, 4, 5], + ] self.verify_wl_all(edges, 10, solutions, None, None) def test_agree_on_many(self): @@ -205,22 +241,25 @@ def test_agree_on_many(self): except AssertionError: self.skipTest("Could not find dataset folder") try: - from tqdm import tqdm # pylint: disable=import-outside-toplevel + from tqdm import tqdm # pylint: disable=import-outside-toplevel except ImportError: - def tqdm(obj, **kwargs): # pylint: disable=unused-argument + + def tqdm(obj, **kwargs): # pylint: disable=unused-argument return obj - for num_edges in tqdm(range(2,13), leave=False): - list_edges = np.load(folder/f"ge{num_edges}d1.npy") + + for num_edges in tqdm(range(2, 13), leave=False): + list_edges = np.load(folder / f"ge{num_edges}d1.npy") for i in tqdm(range(list_edges.shape[0]), leave=False): - edges = np.array(list_edges[i,:,:],dtype=np.uint32) - self.verify_agreement(edges, edges.ravel().max()+1, subtest= (num_edges, i)) + edges = np.array(list_edges[i, :, :], dtype=np.int32) + self.verify_agreement( + edges, edges.ravel().max() + 1, subtest=(num_edges, i) + ) def test_bincount(self): - arr = np.array([2,1,0,0], dtype=np.uint32) + arr = np.array([2, 1, 0, 0], dtype=np.int32) out = my_bincount(arr, 0) - np.testing.assert_array_equal(out, [2,1,1]) - + np.testing.assert_array_equal(out, [2, 1, 1]) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/nestmodel/tests/test_graph_properties.py b/tests/test_graph_properties.py similarity index 58% rename from nestmodel/tests/test_graph_properties.py rename to tests/test_graph_properties.py index 78e4b0e..abef6f2 100644 --- a/nestmodel/tests/test_graph_properties.py +++ b/tests/test_graph_properties.py @@ -1,108 +1,114 @@ # pylint: disable=missing-function-docstring, missing-class-docstring import unittest import numpy as np -import networkx as nx from nestmodel.fast_graph import FastGraph from nestmodel.graph_properties import number_of_flips_possible from numpy.testing import assert_array_equal + class TestFlipsPossible(unittest.TestCase): def test_number_of_flips_possible_1(self): - G = FastGraph(np.array([(0,1), (2,3)], dtype=np.uint32), is_directed=False) + G = FastGraph(np.array([(0, 1), (2, 3)], dtype=np.int32), is_directed=False) G.ensure_edges_prepared() assert_array_equal(number_of_flips_possible(G), [1]) - G = FastGraph(np.array([(0,1), (2,3)], dtype=np.uint32), is_directed=True) + G = FastGraph(np.array([(0, 1), (2, 3)], dtype=np.int32), is_directed=True) G.ensure_edges_prepared() - assert_array_equal(number_of_flips_possible(G), [1,1]) + assert_array_equal(number_of_flips_possible(G), [1, 1]) def test_number_of_flips_possible_2(self): - edges = np.array([(0,1), (2,3), (2,4)], dtype=np.uint32) + edges = np.array([(0, 1), (2, 3), (2, 4)], dtype=np.int32) G = FastGraph(edges.copy(), is_directed=False) G.ensure_edges_prepared() - assert_array_equal(number_of_flips_possible(G), [2,0,0]) + assert_array_equal(number_of_flips_possible(G), [2, 0, 0]) G = FastGraph(edges.copy(), is_directed=True) G.ensure_edges_prepared() - assert_array_equal(number_of_flips_possible(G), [2,2]) - + assert_array_equal(number_of_flips_possible(G), [2, 2]) def test_number_of_flips_possible_3(self): - edges = np.array([(0,1), (2,3), (2,4), (0,3)], dtype=np.uint32) + edges = np.array([(0, 1), (2, 3), (2, 4), (0, 3)], dtype=np.int32) G = FastGraph(edges.copy(), is_directed=False) G.ensure_edges_prepared() - assert_array_equal(number_of_flips_possible(G), [3,1,1]) + assert_array_equal(number_of_flips_possible(G), [3, 1, 1]) G = FastGraph(edges.copy(), is_directed=True) G.ensure_edges_prepared() - assert_array_equal(number_of_flips_possible(G), [3,1]) - - + assert_array_equal(number_of_flips_possible(G), [3, 1]) def test_number_of_flips_possible_4(self): - edges = np.array([(0,1), (2,3), (2,4), (0,3)], dtype=np.uint32) - edges = np.vstack((edges, edges+5)) + edges = np.array([(0, 1), (2, 3), (2, 4), (0, 3)], dtype=np.int32) + edges = np.vstack((edges, edges + 5)) G = FastGraph(edges.copy(), is_directed=False) G.ensure_edges_prepared() - assert_array_equal(number_of_flips_possible(G), [22,10,10]) + assert_array_equal(number_of_flips_possible(G), [22, 10, 10]) G = FastGraph(edges.copy(), is_directed=True) G.ensure_edges_prepared() - assert_array_equal(number_of_flips_possible(G), [22,10]) - + assert_array_equal(number_of_flips_possible(G), [22, 10]) def test_number_of_flips_possible_5(self): """A case with a triangle where no flip is possible""" - edges = np.array([(0,1), (1,2), (2,0), (2,3)], dtype=np.uint32) + edges = np.array([(0, 1), (1, 2), (2, 0), (2, 3)], dtype=np.int32) G = FastGraph(edges.copy(), is_directed=False) G.ensure_edges_prepared() - assert_array_equal(number_of_flips_possible(G), [0,0]) - + assert_array_equal(number_of_flips_possible(G), [0, 0]) def test_number_of_flips_possible_6(self): """A case with a triangle where no flip is possible""" - edges = np.array([(0,1), (1,2), (2,0), (2,3), (1,3), (3,4)], dtype=np.uint32) + edges = np.array( + [(0, 1), (1, 2), (2, 0), (2, 3), (1, 3), (3, 4)], dtype=np.int32 + ) G = FastGraph(edges.copy(), is_directed=False) G.ensure_edges_prepared() - assert_array_equal(number_of_flips_possible(G), [2,0,0]) + assert_array_equal(number_of_flips_possible(G), [2, 0, 0]) def test_number_of_flips_possible_7(self): - """A case with of four clique """ - edges = np.array([(0,1), (1,2), (2,0), (2,3), (1,3),(0,3)], dtype=np.uint32) + """A case with of four clique""" + edges = np.array( + [(0, 1), (1, 2), (2, 0), (2, 3), (1, 3), (0, 3)], dtype=np.int32 + ) G = FastGraph(edges.copy(), is_directed=False) G.ensure_edges_prepared() assert_array_equal(number_of_flips_possible(G), [0]) def test_number_of_flips_possible_8(self): """A case with of four clique with an extra edge""" - edges = np.array([(0,1), (1,2), (2,0), (2,3), (1,3), (3,4),(0,3)], dtype=np.uint32) + edges = np.array( + [(0, 1), (1, 2), (2, 0), (2, 3), (1, 3), (3, 4), (0, 3)], dtype=np.int32 + ) G = FastGraph(edges.copy(), is_directed=False) G.ensure_edges_prepared() - assert_array_equal(number_of_flips_possible(G), [0,0]) + assert_array_equal(number_of_flips_possible(G), [0, 0]) def test_number_of_flips_possible_9(self): """A case with of four clique with an two extra edges""" - edges = np.array([(0,1), (1,2), (2,0), (2,3), (1,3), (3,4), (3,5),(0,3)], dtype=np.uint32) + edges = np.array( + [(0, 1), (1, 2), (2, 0), (2, 3), (1, 3), (3, 4), (3, 5), (0, 3)], + dtype=np.int32, + ) G = FastGraph(edges.copy(), is_directed=False) G.ensure_edges_prepared() - assert_array_equal(number_of_flips_possible(G), [0,0]) + assert_array_equal(number_of_flips_possible(G), [0, 0]) def test_number_of_flips_possible_10(self): """A case with of four clique with an two extra edges""" - edges = np.array([(0,1), (0,2), (0,3), (1,2), (1,3), (2,3), (1,4), (0,5)], dtype=np.uint32) + edges = np.array( + [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3), (1, 4), (0, 5)], + dtype=np.int32, + ) G = FastGraph(edges.copy(), is_directed=False) G.ensure_edges_prepared() - assert_array_equal(number_of_flips_possible(G), [1,1]) - + assert_array_equal(number_of_flips_possible(G), [1, 1]) def test_number_of_flips_possible_11(self): """A case with of four clique with an two extra edges""" - edges = np.array([(0,1), (0,2), (3,2), (3,1)], dtype=np.uint32) + edges = np.array([(0, 1), (0, 2), (3, 2), (3, 1)], dtype=np.int32) G = FastGraph(edges.copy(), is_directed=False) G.ensure_edges_prepared() @@ -110,49 +116,46 @@ def test_number_of_flips_possible_11(self): def test_number_of_flips_possible_12(self): """A case with of four clique with an two extra edges""" - edges = np.array([(0,1), (0,2), (3,2), (3,1)], dtype=np.uint32) + edges = np.array([(0, 1), (0, 2), (3, 2), (3, 1)], dtype=np.int32) G = FastGraph(edges.copy(), is_directed=False) - G.ensure_edges_prepared(initial_colors=[0,1,1,0]) + G.ensure_edges_prepared(initial_colors=[0, 1, 1, 0]) assert_array_equal(number_of_flips_possible(G), [0]) - - - - - - def test_number_of_flips_possible_source_1(self): - edges = np.array([(0,1), (2,1)], dtype=np.uint32) + edges = np.array([(0, 1), (2, 1)], dtype=np.int32) G = FastGraph(edges.copy(), is_directed=True, num_nodes=4) G.ensure_edges_prepared() - assert_array_equal(number_of_flips_possible(G, kind="source_only"), [2,2]) - + assert_array_equal(number_of_flips_possible(G, kind="source_only"), [2, 2]) def test_number_of_flips_possible_source_2(self): - edges = np.array([(0,1), (2,1)], dtype=np.uint32) + edges = np.array([(0, 1), (2, 1)], dtype=np.int32) G = FastGraph(edges.copy(), is_directed=True, num_nodes=5) G.ensure_edges_prepared() - assert_array_equal(number_of_flips_possible(G, kind="source_only"), [4,4]) - + assert_array_equal(number_of_flips_possible(G, kind="source_only"), [4, 4]) def test_number_of_flips_possible_source_3(self): - edges = np.array([(0,1), (2,1)], dtype=np.uint32) + edges = np.array([(0, 1), (2, 1)], dtype=np.int32) G = FastGraph(edges.copy(), is_directed=True, num_nodes=5) - G.ensure_edges_prepared(initial_colors=[0,0,1,0,1], sorting_strategy="source") - assert_array_equal(number_of_flips_possible(G, kind="source_only"), [2,2]) + G.ensure_edges_prepared( + initial_colors=[0, 0, 1, 0, 1], sorting_strategy="source" + ) + assert_array_equal(number_of_flips_possible(G, kind="source_only"), [2, 2]) def test_number_of_flips_possible_source_4(self): - edges = np.array([(0,1), (2,1)], dtype=np.uint32) + edges = np.array([(0, 1), (2, 1)], dtype=np.int32) G = FastGraph(edges.copy(), is_directed=True, num_nodes=6) - G.ensure_edges_prepared(initial_colors=[1,0,0,0,1,0], sorting_strategy="source") - assert_array_equal(number_of_flips_possible(G, kind="source_only"), [3,3]) + G.ensure_edges_prepared( + initial_colors=[1, 0, 0, 0, 1, 0], sorting_strategy="source" + ) + assert_array_equal(number_of_flips_possible(G, kind="source_only"), [3, 3]) def test_number_of_flips_possible_source_5(self): - edges = np.array([(0,1), (0,5), (4,1)], dtype=np.uint32) + edges = np.array([(0, 1), (0, 5), (4, 1)], dtype=np.int32) G = FastGraph(edges.copy(), is_directed=True, num_nodes=6) G.ensure_edges_prepared(sorting_strategy="source") - assert_array_equal(number_of_flips_possible(G, kind="source_only"), [10,7]) + assert_array_equal(number_of_flips_possible(G, kind="source_only"), [10, 7]) + -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_pagerank.py b/tests/test_pagerank.py new file mode 100644 index 0000000..c217892 --- /dev/null +++ b/tests/test_pagerank.py @@ -0,0 +1,189 @@ +# pylint: disable=missing-function-docstring, missing-class-docstring +import unittest +import numpy as np +from nestmodel.fast_graph import FastGraph +from nestmodel.centralities import calc_pagerank + + +karate_pagerank = [ + 0.09699729, + 0.05287692, + 0.05707851, + 0.03585986, + 0.02197795, + 0.02911115, + 0.02911115, + 0.0244905, + 0.02976606, + 0.0143094, + 0.02197795, + 0.00956475, + 0.01464489, + 0.02953646, + 0.01453599, + 0.01453599, + 0.01678401, + 0.01455868, + 0.01453599, + 0.01960464, + 0.01453599, + 0.01455868, + 0.01453599, + 0.03152251, + 0.02107603, + 0.0210062, + 0.01504404, + 0.02563977, + 0.01957346, + 0.02628854, + 0.02459016, + 0.03715809, + 0.07169323, + 0.10091918, +] + + +karate_edges = np.array( + [ + [0, 1], + [0, 2], + [0, 3], + [0, 4], + [0, 5], + [0, 6], + [0, 7], + [0, 8], + [0, 10], + [0, 11], + [0, 12], + [0, 13], + [0, 17], + [0, 19], + [0, 21], + [0, 31], + [1, 2], + [1, 3], + [1, 7], + [1, 13], + [1, 17], + [1, 19], + [1, 21], + [1, 30], + [2, 3], + [2, 7], + [2, 8], + [2, 9], + [2, 13], + [2, 27], + [2, 28], + [2, 32], + [3, 7], + [3, 12], + [3, 13], + [4, 6], + [4, 10], + [5, 6], + [5, 10], + [5, 16], + [6, 16], + [8, 30], + [8, 32], + [8, 33], + [9, 33], + [13, 33], + [14, 32], + [14, 33], + [15, 32], + [15, 33], + [18, 32], + [18, 33], + [19, 33], + [20, 32], + [20, 33], + [22, 32], + [22, 33], + [23, 25], + [23, 27], + [23, 29], + [23, 32], + [23, 33], + [24, 25], + [24, 27], + [24, 31], + [25, 31], + [26, 29], + [26, 33], + [27, 33], + [28, 31], + [28, 33], + [29, 32], + [29, 33], + [30, 32], + [30, 33], + [31, 32], + [31, 33], + [32, 33], + ], + dtype=np.int32, +) # pylint: disable=line-too-long + + +class TestFastWLMethods(unittest.TestCase): + + def test_pagerank_karate(self): + G = FastGraph(karate_edges, False) + p = calc_pagerank(G) + np.testing.assert_almost_equal(p, karate_pagerank) + + def test_pagerank_directed_edge(self): + G = FastGraph(np.array([[0, 1]], dtype=np.int32), True) + p = calc_pagerank(G) # formerly "out" + alpha = 0.85 + val1 = 1 / (2 + alpha) + val2 = (1 + alpha) / (2 + alpha) + np.testing.assert_almost_equal(p, [val1, val2]) + + p = calc_pagerank(FastGraph.switch_directions(G)) + np.testing.assert_almost_equal(p, [val2, val1]) + + def test_pagerank_undirected_edge(self): + G = FastGraph(np.array([[0, 1]], dtype=np.int32), False) + p = calc_pagerank(G) + np.testing.assert_almost_equal(p, [0.5, 0.5]) + + p = calc_pagerank(FastGraph.switch_directions(G)) + np.testing.assert_almost_equal(p, [0.5, 0.5]) + + def test_pagerank_undirected_line(self): + G = FastGraph(np.array([[0, 1], [1, 2]], dtype=np.int32), False) + p = calc_pagerank(G) + np.testing.assert_almost_equal(p, [0.2567568, 0.4864865, 0.2567568]) + + p = calc_pagerank(FastGraph.switch_directions(G)) + np.testing.assert_almost_equal(p, [0.2567568, 0.4864865, 0.2567568]) + + def test_pagerank_undirected_line2(self): + G = FastGraph(np.array([[0, 1], [3, 4]], dtype=np.int32), False) + p = calc_pagerank(G) + np.testing.assert_almost_equal( + p, [0.24096383, 0.24096383, 0.03614469, 0.24096383, 0.24096383] + ) + + def test_networkx_pagerank(self): + def dict_to_arr(d): + arr = np.empty(len(d)) + for key, val in d.items(): + arr[key] = val + return arr + + import networkx as nx # pylint: disable=import-outside-toplevel + + G = nx.Graph() + G.add_nodes_from(range(34)) + G.add_edges_from(karate_edges) + p = dict_to_arr(nx.pagerank(G, tol=1e-14, max_iter=100)) + np.testing.assert_almost_equal(p, karate_pagerank) + + +if __name__ == "__main__": + unittest.main() diff --git a/nestmodel/tests/test_unified_functions.py b/tests/test_unified_functions.py similarity index 61% rename from nestmodel/tests/test_unified_functions.py rename to tests/test_unified_functions.py index c90f461..21b2f58 100644 --- a/nestmodel/tests/test_unified_functions.py +++ b/tests/test_unified_functions.py @@ -6,6 +6,7 @@ import networkx as nx import numpy as np + class TestUnifiedFunctions(unittest.TestCase): def test_fastgraph_failed(self): G = nx.Graph() @@ -13,70 +14,64 @@ def test_fastgraph_failed(self): def test_to_fast_graph_nx(self): G = nx.Graph() - G.add_edges_from([(0,1), (1,2)]) + G.add_edges_from([(0, 1), (1, 2)]) G_fg = to_fast_graph(G) self.assertFalse(G_fg.is_directed) - assert_array_equal(G.edges, [(0,1), (1,2)]) + assert_array_equal(G.edges, [(0, 1), (1, 2)]) def test_to_fast_graph_nx2(self): G = nx.DiGraph() - G.add_edges_from([(0,1), (1,2)]) + G.add_edges_from([(0, 1), (1, 2)]) G_fg = to_fast_graph(G) self.assertTrue(G_fg.is_directed) - assert_array_equal(G.edges, [(0,1), (1,2)]) - + assert_array_equal(G.edges, [(0, 1), (1, 2)]) def test_to_fast_graph_fg(self): from nestmodel.fast_graph import FastGraph - G = FastGraph(np.array([(0,1), (1,2)], dtype=np.uint32), is_directed=True) + + G = FastGraph(np.array([(0, 1), (1, 2)], dtype=np.int32), is_directed=True) G_fg = to_fast_graph(G) self.assertTrue(G_fg.is_directed) - assert_array_equal(G.edges, [(0,1), (1,2)]) - - - + assert_array_equal(G.edges, [(0, 1), (1, 2)]) def test_rewire1_double_edge_1(self): - edges = np.array([[0,1],[2,3]], dtype=np.uint32) + edges = np.array([[0, 1], [2, 3]], dtype=np.int32) G = FastGraph(edges.copy(), is_directed=True) G_rew = rewire_graph(G, depth=0, method=1, seed=1, r=1) assert_array_equal(G_rew.edges, edges) - edges2 = np.array([[0,3],[2,1]], dtype=np.uint32) + edges2 = np.array([[0, 3], [2, 1]], dtype=np.int32) G = FastGraph(edges.copy(), is_directed=True) G_rew = rewire_graph(G, depth=0, method=1, seed=0, r=1) assert_array_equal(G_rew.edges, edges2) - - def test_rewire1_double_edge_2(self): - edges = np.array([[0,1],[2,3]], dtype=np.uint32) + edges = np.array([[0, 1], [2, 3]], dtype=np.int32) G = FastGraph(edges.copy(), is_directed=True) G_rew = rewire_graph(G, depth=0, method=2, seed=1, n_rewire=1) assert_array_equal(G_rew.edges, edges) - edges2 = np.array([[0,3],[2,1]], dtype=np.uint32) + edges2 = np.array([[0, 3], [2, 1]], dtype=np.int32) G_rew = rewire_graph(G, depth=0, method=2, seed=2, n_rewire=1) assert_array_equal(G_rew.edges, edges2) - def test_rewire1_double_edge(self): - edges_in = np.array([[0,1],[2,3]], dtype=np.uint32) + edges_in = np.array([[0, 1], [2, 3]], dtype=np.int32) G = FastGraph(edges_in.copy(), is_directed=False) result_edges = [ - np.array([[0, 3], [1, 2]], dtype=np.uint32), - np.array([[0, 1], [2, 3]], dtype=np.uint32), - np.array([[0, 2], [3, 1]], dtype=np.uint32), - np.array([[0, 1], [2, 3]], dtype=np.uint32), - np.array([[0, 1], [2, 3]], dtype=np.uint32), - np.array([[1, 0], [2, 3]], dtype=np.uint32), - np.array([[0, 2], [3, 1]], dtype=np.uint32), - np.array([[1, 2], [0, 3]], dtype=np.uint32), - np.array([[1, 3], [2, 0]], dtype=np.uint32), - np.array([[0, 3], [2, 1]], dtype=np.uint32) + np.array([[0, 3], [1, 2]], dtype=np.int32), + np.array([[0, 1], [2, 3]], dtype=np.int32), + np.array([[0, 2], [3, 1]], dtype=np.int32), + np.array([[0, 1], [2, 3]], dtype=np.int32), + np.array([[0, 1], [2, 3]], dtype=np.int32), + np.array([[1, 0], [2, 3]], dtype=np.int32), + np.array([[0, 2], [3, 1]], dtype=np.int32), + np.array([[1, 2], [0, 3]], dtype=np.int32), + np.array([[1, 3], [2, 0]], dtype=np.int32), + np.array([[0, 3], [2, 1]], dtype=np.int32), ] for i, res_edges in enumerate(result_edges): @@ -84,5 +79,5 @@ def test_rewire1_double_edge(self): assert_array_equal(G_rew.edges, res_edges, f"{i}") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..9d09051 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,34 @@ +# pylint: disable=missing-function-docstring, missing-class-docstring +import unittest +import numpy as np +from nestmodel.fast_graph import FastGraph +from nestmodel.utils import calc_jaccard + + +class TestFastWLMethods(unittest.TestCase): + + def test_unique_edges_dir(self): + G1 = FastGraph(np.array([[1, 9], [4, 3]], dtype=np.int32), True) + G2 = FastGraph(np.array([[1, 9], [6, 2]], dtype=np.int32), True) + G3 = FastGraph(np.array([[1, 10], [5, 2]], dtype=np.int32), True) + G4 = FastGraph(np.array([[9, 1], [3, 4]], dtype=np.int32), True) + + self.assertEqual(calc_jaccard(G1, G2), 1 / 3) + self.assertEqual(calc_jaccard(G1, G1), 1.0) + self.assertEqual(calc_jaccard(G1, G3), 0.0) + self.assertEqual(calc_jaccard(G1, G4), 0.0) + + def test_unique_edges_undir(self): + G1 = FastGraph(np.array([[1, 9], [4, 3]], dtype=np.int32), False) + G4 = FastGraph(np.array([[9, 1], [3, 4]], dtype=np.int32), False) + G2 = FastGraph(np.array([[1, 9], [6, 2]], dtype=np.int32), False) + G3 = FastGraph(np.array([[1, 10], [5, 2]], dtype=np.int32), False) + + self.assertEqual(calc_jaccard(G1, G2), 1 / 3) + self.assertEqual(calc_jaccard(G1, G1), 1.0) + self.assertEqual(calc_jaccard(G1, G3), 0.0) + self.assertEqual(calc_jaccard(G1, G4), 1.0) + + +if __name__ == "__main__": + unittest.main()