diff --git a/.all-contributorsrc b/.all-contributorsrc index 78c931202..d175b8231 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -531,6 +531,123 @@ "contributions": [ "code" ] + }, + { + "login": "JDPowell648", + "name": "JDPowell648", + "avatar_url": "https://avatars.githubusercontent.com/u/41934552?v=4", + "profile": "https://github.com/JDPowell648", + "contributions": [ + "doc" + ] + }, + { + "login": "Adriankhl", + "name": "k.h.lai", + "avatar_url": "https://avatars.githubusercontent.com/u/16377650?v=4", + "profile": "https://github.com/Adriankhl", + "contributions": [ + "code" + ] + }, + { + "login": "gruebel", + "name": "Anton Grübel", + "avatar_url": "https://avatars.githubusercontent.com/u/33207684?v=4", + "profile": "https://github.com/gruebel", + "contributions": [ + "code" + ] + }, + { + "login": "flange-ipb", + "name": "flange-ipb", + "avatar_url": "https://avatars.githubusercontent.com/u/34936695?v=4", + "profile": "https://github.com/flange-ipb", + "contributions": [ + "code" + ] + }, + { + "login": "pmp-p", + "name": "Paul m. p. Peny", + "avatar_url": "https://avatars.githubusercontent.com/u/16009100?v=4", + "profile": "https://discuss.afpy.org/", + "contributions": [ + "code" + ] + }, + { + "login": "DavidRConnell", + "name": "David R. Connell", + "avatar_url": "https://avatars.githubusercontent.com/u/35470740?v=4", + "profile": "https://davidrconnell.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "rmmaf", + "name": "Rodrigo Monteiro de Moraes de Arruda Falcão", + "avatar_url": "https://avatars.githubusercontent.com/u/23747884?v=4", + "profile": "https://www.linkedin.com/in/rmmaf/", + "contributions": [ + "code" + ] + }, + { + "login": "Kreijstal", + "name": "Kreijstal", + "avatar_url": "https://avatars.githubusercontent.com/u/2415206?v=4", + "profile": "https://github.com/Kreijstal", + "contributions": [ + "code" + ] + }, + { + "login": "m1-s", + "name": "Michael Schneider", + "avatar_url": "https://avatars.githubusercontent.com/u/94642227?v=4", + "profile": "https://github.com/m1-s", + "contributions": [ + "code" + ] + }, + { + "login": "aothms", + "name": "Thomas Krijnen", + "avatar_url": "https://avatars.githubusercontent.com/u/1096535?v=4", + "profile": "http://thomaskrijnen.com/", + "contributions": [ + "code" + ] + }, + { + "login": "GenieTim", + "name": "Tim Bernhard", + "avatar_url": "https://avatars.githubusercontent.com/u/8596965?v=4", + "profile": "https://github.com/GenieTim", + "contributions": [ + "code" + ] + }, + { + "login": "BeaMarton13", + "name": "Bea Márton", + "avatar_url": "https://avatars.githubusercontent.com/u/204701577?v=4", + "profile": "https://github.com/BeaMarton13", + "contributions": [ + "code" + ] + }, + { + "login": "SKG24", + "name": "Sanat Kumar Gupta", + "avatar_url": "https://avatars.githubusercontent.com/u/123228827?v=4", + "profile": "https://github.com/SKG24", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..686944ff0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,5 @@ + + + + +- [ ] By submitting this pull request, I assign the copyright of my contribution to _The igraph development team_. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59ffdea0b..2192d7c72 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,102 +1,85 @@ -name: Build and test, upload to PyPI on release +# Name cannot contain commas because of setup-emsdk job +name: Build and test on: [push, pull_request] env: + CIBW_ENABLE: pypy CIBW_ENVIRONMENT_PASS_LINUX: PYTEST_TIMEOUT + CIBW_PROJECT_REQUIRES_PYTHON: ">=3.9" CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test]' && python -m pytest -v tests" - CIBW_SKIP: "cp36-* cp37-* pp37-*" + # Free-threaded builds excluded for Python 3.14 because they do not support the limited API + CIBW_SKIP: "cp314t-*" PYTEST_TIMEOUT: 60 - MACOSX_DEPLOYMENT_TARGET: "10.9" jobs: build_wheel_linux: - name: Build wheels on Linux (${{ matrix.wheel_arch }}) - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - wheel_arch: [x86_64, i686] - + name: Build wheels on Linux (x86_64) + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 - - uses: actions/setup-python@v4 - name: Install Python - with: - python-version: '3.8' - - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v3.3.0 env: - CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip wheel && python setup.py build_c_core" - CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" - # Skip tests for Python 3.10 onwards because SciPy does not have - # 32-bit wheels for Linux - CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686" + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" + CIBW_BUILD: "*-manylinux_x86_64" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v3.3.0 env: - CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip wheel && python setup.py build_c_core" - CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" + CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" + CIBW_BUILD: "*-musllinux_x86_64" CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test-musl]' && python -m pytest -v tests" - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v6 with: + name: wheels-linux-x86_64 path: ./wheelhouse/*.whl build_wheel_linux_aarch64_manylinux: name: Build wheels on Linux (aarch64/manylinux) - runs-on: ubuntu-20.04 - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + runs-on: ubuntu-22.04-arm steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 - - name: Set up QEMU - id: qemu - uses: docker/setup-qemu-action@v3 - - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v3.3.0 env: - CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip wheel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 CIBW_BUILD: "*-manylinux_aarch64" - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v6 with: + name: wheels-linux-aarch64-manylinux path: ./wheelhouse/*.whl build_wheel_linux_aarch64_musllinux: name: Build wheels on Linux (aarch64/musllinux) - runs-on: ubuntu-20.04 - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + runs-on: ubuntu-22.04-arm steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 - - name: Set up QEMU - id: qemu - uses: docker/setup-qemu-action@v3 - - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v3.3.0 env: - CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip wheel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 CIBW_BUILD: "*-musllinux_aarch64" CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test-musl]' && python -m pytest -v tests" - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v6 with: + name: wheels-linux-aarch64-musllinux path: ./wheelhouse/*.whl build_wheel_macos: @@ -104,6 +87,7 @@ jobs: runs-on: macos-latest env: LLVM_VERSION: "14.0.5" + MACOSX_DEPLOYMENT_TARGET: "10.15" strategy: matrix: include: @@ -114,34 +98,28 @@ jobs: wheel_arch: arm64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 - name: Cache installed C core id: cache-c-core - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: vendor/install key: C-core-cache-${{ runner.os }}-${{ matrix.cmake_arch }}-llvm${{ env.LLVM_VERSION }}-${{ hashFiles('.git/modules/**/HEAD') }} - name: Cache C core dependencies id: cache-c-deps - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: ~/local key: deps-cache-v2-${{ runner.os }}-${{ matrix.cmake_arch }}-llvm${{ env.LLVM_VERSION }} - - uses: actions/setup-python@v4 - name: Install Python - with: - python-version: '3.8' - - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' || steps.cache-c-deps.outputs.cache-hit != 'true' # Only needed when building the C core or libomp - run: - brew install ninja autoconf automake libtool cmake + run: brew install autoconf automake libtool - name: Install OpenMP library if: steps.cache-c-deps.outputs.cache-hit != 'true' @@ -155,44 +133,44 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v3.3.0 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" - CIBW_BEFORE_BUILD: "python setup.py build_c_core" + CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_ENVIRONMENT: "LDFLAGS=-L$HOME/local/lib" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_arch }} ${{ matrix.cmake_extra_args }} -DCMAKE_PREFIX_PATH=$HOME/local - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v6 with: + name: wheels-macos-${{ matrix.wheel_arch }} path: ./wheelhouse/*.whl build_wheel_wasm: name: Build wheels for WebAssembly - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v6 name: Install Python with: - python-version: '3.11.2' + python-version: "3.12.1" - name: Install OS dependencies - run: - sudo apt install ninja-build cmake flex bison + run: sudo apt install ninja-build cmake flex bison - - uses: mymindstorm/setup-emsdk@v12 + - uses: mymindstorm/setup-emsdk@v14 with: - version: '3.1.45' - actions-cache-folder: 'emsdk-cache' + version: "3.1.58" + actions-cache-folder: "emsdk-cache" - name: Build wheel run: | - pip install pyodide-build==0.24.1 + pip install pyodide-build==0.26.2 python3 scripts/fix_pyodide_build.py pyodide build @@ -203,43 +181,48 @@ jobs: limit-access-to-actor: true wait-timeout-minutes: 5 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v6 with: + name: wheels-wasm path: ./dist/*.whl build_wheel_win: name: Build wheels on Windows (${{ matrix.cmake_arch }}) - runs-on: windows-2019 strategy: matrix: include: - cmake_arch: Win32 wheel_arch: win32 vcpkg_arch: x86 + os: windows-2022 + test_extra: test - cmake_arch: x64 wheel_arch: win_amd64 vcpkg_arch: x64 + os: windows-2022 + test_extra: test + - cmake_arch: ARM64 + wheel_arch: win_arm64 + vcpkg_arch: arm64 + os: windows-11-arm + test_extra: test-win-arm64 + runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 - - uses: actions/setup-python@v4 - name: Install Python - with: - python-version: '3.8' - - name: Cache installed C core id: cache-c-core - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: vendor/install key: C-core-cache-${{ runner.os }}-${{ matrix.cmake_arch }}-${{ hashFiles('.git/modules/**/HEAD') }} - name: Cache VCPKG - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: C:/vcpkg/installed/ key: vcpkg-${{ runner.os }}-${{ matrix.vcpkg_arch }} @@ -251,40 +234,41 @@ jobs: - name: Install VCPKG libraries run: | %VCPKG_INSTALLATION_ROOT%\vcpkg.exe integrate install - %VCPKG_INSTALLATION_ROOT%\vcpkg.exe install libxml2:${{ matrix.vcpkg_arch }}-windows-static-md + %VCPKG_INSTALLATION_ROOT%\vcpkg.exe install liblzma:${{ matrix.vcpkg_arch }}-windows-static-md libxml2:${{ matrix.vcpkg_arch }}-windows-static-md shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v3.3.0 env: - CIBW_BEFORE_BUILD: "python setup.py build_c_core" + CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" - CIBW_TEST_COMMAND: "cd /d {project} && pip install --prefer-binary \".[test]\" && python -m pytest tests" + CIBW_TEST_COMMAND: 'cd /d {project} && pip install --prefer-binary ".[${{ matrix.test_extra }}]" && python -m pytest tests' # Skip tests for Python 3.10 onwards because SciPy does not have - # 32-bit wheels for Windows - CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32" + # 32-bit wheels for Windows any more + CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32 cp313-win32 cp314-win32" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=${{ matrix.vcpkg_arch }}-windows-static-md -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -A ${{ matrix.cmake_arch }} IGRAPH_EXTRA_LIBRARY_PATH: C:/vcpkg/installed/${{ matrix.vcpkg_arch }}-windows-static-md/lib/ IGRAPH_STATIC_EXTENSION: True - IGRAPH_EXTRA_LIBRARIES: libxml2,lzma,zlib,iconv,charset + IGRAPH_EXTRA_LIBRARIES: libxml2,lzma,zlib,iconv,charset,bcrypt IGRAPH_EXTRA_DYNAMIC_LIBRARIES: wsock32,ws2_32 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v6 with: + name: wheels-win-${{ matrix.wheel_arch }} path: ./wheelhouse/*.whl build_sdist: name: Build sdist and test extra dependencies runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 - name: Cache installed C core id: cache-c-core - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: | vendor/install @@ -292,45 +276,45 @@ jobs: - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' # Only needed when building the C core - run: - sudo apt install ninja-build cmake flex bison + run: sudo apt install ninja-build cmake flex bison - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v6 name: Install Python with: - python-version: '3.8' + python-version: "3.9" - name: Build sdist run: | python setup.py build_c_core python setup.py sdist - python setup.py install + pip install . - name: Test run: | - pip install --prefer-binary cairocffi numpy scipy pandas networkx pytest pytest-timeout + pip install '.[test]' python -m pytest -v tests - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v6 with: + name: sdist path: dist/*.tar.gz # When updating 'runs-on', the ASan/UBSan library paths/versions must also be updated for LD_PRELOAD # for the "Test" step below. build_with_sanitizer: name: Build with sanitizers for debugging purposes - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: IGRAPH_CMAKE_EXTRA_ARGS: -DFORCE_COLORED_OUTPUT=ON steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 - name: Cache installed C core id: cache-c-core - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: | vendor/build @@ -339,30 +323,21 @@ jobs: - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' # Only needed when building the C core - run: - sudo apt install ninja-build cmake flex bison + run: sudo apt install ninja-build cmake flex bison - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v6 name: Install Python with: - python-version: '3.12' - - - name: Install test dependencies - run: | - pip install --prefer-binary pytest pytest-timeout setuptools - - - name: Build C core - env: - IGRAPH_USE_SANITIZERS: 1 - run: | - python setup.py build_c_core + python-version: "3.12" - name: Build and install Python extension env: IGRAPH_USE_SANITIZERS: 1 run: | - # NOTE: install calls "build" first - python setup.py install + # We cannot install the test dependency group because many test dependencies cause + # false positives in the sanitizer + pip install --prefer-binary networkx pytest pytest-timeout + pip install -e . # Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD. # The ASan/UBSan library versions need to be updated when switching to a newer Ubuntu/GCC. @@ -373,4 +348,5 @@ jobs: ASAN_OPTIONS: "detect_stack_use_after_return=1" LSAN_OPTIONS: "suppressions=etc/lsan-suppr.txt:print_suppressions=false" run: | - LD_PRELOAD=/lib/x86_64-linux-gnu/libasan.so.5:/lib/x86_64-linux-gnu/libubsan.so.1 python -m pytest --capture=sys tests + sudo sysctl vm.mmap_rnd_bits=28 + LD_PRELOAD=/lib/x86_64-linux-gnu/libasan.so.8:/lib/x86_64-linux-gnu/libubsan.so.1 python -m pytest --capture=sys tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 516ea19ca..e67e6d125 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,21 +1,15 @@ fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.6.0 hooks: - - id: check-ast - id: end-of-file-fixer + exclude: ^tests/drawing/plotly/baseline_images - id: trailing-whitespace - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.275 + rev: v0.3.5 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - - - repo: https://github.com/psf/black - rev: 22.3.0 - hooks: - - id: black - exclude: ^doc/examples_sphinx-gallery/ - language_version: python3 + - id: ruff-format diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b323c94be..7de01065a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -21,7 +21,7 @@ build: - zlib1g-dev tools: - python: "3.9" + python: "3.11" # You can also specify other tool versions: # nodejs: "16" # rust: "1.55" diff --git a/CHANGELOG.md b/CHANGELOG.md index a0fbeef3b..f2424f4fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,178 @@ # igraph Python interface changelog -## [main] +## 1.0.1 - 2025-12-26 + +### Changed + +- The C core of igraph was updated to version 1.0.1. + +## [1.0.0] - 2025-10-23 + +### Added + +- Added `Graph.Nearest_Neighbor_Graph()`. + +- Added `node_in_weights` argument to `Graph.community_leiden()`. + +- Added `align_layout()` to align the principal axes of a layout nicely + with screen dimensions. + +- Added `Graph.commnity_voronoi()`. + +- Added `Graph.commnity_fluid_communities()`. + +### Changed + +- The C core of igraph was updated to version 1.0.0. + +- Most layouts are now auto-aligned using `align_layout()`. + +- Dropped support for PyPy 3.9 and PyPy 3.10 as they are now EOL. + +### Miscellaneous + +- Documentation improvements. + +- This is the last version that supports Python 3.9 as it will reach its + end of life at the end of October 2025. + +## [0.11.9] - 2025-06-11 + +### Changed + +- Dropped support for Python 3.8 as it has now reached its end of life. + +- The C core of igraph was updated to version 0.10.16. + +- Added `Graph.simple_cycles()` to find simple cycles in the graph. + +## [0.11.8] - 2024-10-25 + +### Fixed + +- Fixed documentation build on Read The Docs. No other changes compared to + 0.11.7. + +## [0.11.7] - 2024-10-24 + +### Added + +- Added `Graph.feedback_vertex_set()` to calculate a feedback vertex set of the + graph. + +- Added new methods to `Graph.feedback_arc_set()` that allows the user to + select the specific integer problem formulation used by the underlying + solver. + +### Changed + +- Ensured compatibility with Python 3.13. + +- The C core of igraph was updated to an interim commit (3dd336a) between + version 0.10.13 and version 0.10.15. Earlier versions of this changelog + mistakenly marked this revision as version 0.10.14. + +### Fixed + +- Fixed a potential memory leak in the `Graph.get_shortest_path_astar()` heuristic + function callback + +## [0.11.6] - 2024-07-08 + +### Added + +- Added `Graph.Hypercube()` for creating n-dimensional hypercube graphs. + +- Added `Graph.Chung_Lu()` for sampling from the Chung-Lu model as well as + several related models. + +- Added `Graph.is_complete()` to test if there is a connection between all + distinct pairs of vertices. + +- Added `Graph.is_clique()` to test if a set of vertices forms a clique. + +- Added `Graph.is_independent_vertex_set()` to test if some vertices form an + independent set. + +- Added `Graph.mean_degree()` for a convenient way to compute the average + degree of a graph. + +### Changed + +- The C core of igraph was updated to version 0.10.13. + +- `Graph.rewire()` now attempts to perform edge swaps 10 times the number of + edges by default. + +- Error messages issued when an attribute is not found now mention the name + and type of that attribute. + +## [0.11.5] - 2024-05-07 + +### Added + +- Added a `prefixattr=...` keyword argument to `Graph.write_graphml()` that + allows the user to strip the `g_`, `v_` and `e_` prefixes from GraphML files + written by igraph. + +### Changed + +- `Graph.are_connected()` has now been renamed to `Graph.are_adjacent()`, + following up a similar change in the C core. The old name of the function + is deprecated but will be kept around until at least 0.12.0. + +- The C core of igraph was updated to version 0.10.12. + +- Deprecated `PyCObject` API calls in the C code were replaced by calls to + `PyCapsule`, thanks to @DavidRConnell in + + +- `get_shortest_path()` documentation was clarified by @JDPowell648 in + + +- It is now possible to link to an existing igraph C core on MSYS2, thanks to + @Kreijstal in + +### Fixed + +- Bugfix in the NetworkX graph conversion code by @rmmaf in + + +## [0.11.4] + +### Added + +- Added `Graph.Prufer()` to construct a graph from a Prüfer sequence. + +- Added `Graph.Bipartite_Degree_Sequence()` to construct a bipartite graph from + a bidegree sequence. + +- Added `Graph.is_biconnected()` to check if a graph is biconnected. + +### Fixed + +- Fixed import of `graph-tool` graphs for vertex properties where each property + has a vector value. + +- `Graph.Adjacency()` now accepts `Matrix` instances and other sequences as an + input, it is not limited to lists-of-lists-of-ints any more. + +## [0.11.3] - 2023-11-19 ### Added - Added `Graph.__invalidate_cache()` for debugging and benchmarking purposes. +### Changed + +- The C core of igraph was updated to version 0.10.8. + ### Fixed - Removed incorrectly added `loops=...` argument of `Graph.is_bigraphical()`. +- Fixed a bug in the Matplotlib graph drawing backend that filled the interior of undirected curved edges. + ## [0.11.2] - 2023-10-12 ### Fixed @@ -594,11 +757,18 @@ ## [0.8.3] This is the last released version of `python-igraph` without a changelog file. -Please refer to the commit logs at https://github.com/igraph/python-igraph for +Please refer to the commit logs at for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[main]: https://github.com/igraph/python-igraph/compare/0.11.2...main +[1.0.0]: https://github.com/igraph/python-igraph/compare/0.11.9...1.0.0 +[0.11.9]: https://github.com/igraph/python-igraph/compare/0.11.8...0.11.9 +[0.11.8]: https://github.com/igraph/python-igraph/compare/0.11.7...0.11.8 +[0.11.7]: https://github.com/igraph/python-igraph/compare/0.11.6...0.11.7 +[0.11.6]: https://github.com/igraph/python-igraph/compare/0.11.5...0.11.6 +[0.11.5]: https://github.com/igraph/python-igraph/compare/0.11.4...0.11.5 +[0.11.4]: https://github.com/igraph/python-igraph/compare/0.11.3...0.11.4 +[0.11.3]: https://github.com/igraph/python-igraph/compare/0.11.2...0.11.3 [0.11.2]: https://github.com/igraph/python-igraph/compare/0.11.0...0.11.2 [0.11.0]: https://github.com/igraph/python-igraph/compare/0.10.8...0.11.0 [0.10.8]: https://github.com/igraph/python-igraph/compare/0.10.7...0.10.8 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 37b369f5d..d22d5bd6f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -6,82 +6,101 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Tamás Nepusz

💻

Fabio Zanini

💻

Kevin Zhu

💻

Gábor Csárdi

💻

Szabolcs Horvát

💻

Vincent Traag

💻

deeenes

💻

Seungoh Han

💻

Artem V L

💻

Yesung(Isaac) Lee

💻

John Boy

💻

Casper da Costa-Luis

💻

Alberto Alcolea

💻

Árpád Horváth

💻

ebraminio

💻

Fabian Witter

💻

Jan Katins

💻

Nick Eubank

💻

Peter Scott

💻

Sriram-Pattabiraman

💻

Sviatoslav

💻

Ah-Young Nho

💻

Frederik Harwath

💻

Navid Dianati

💻

abe-winter

💻

Alejandro Rivero

💻

Ariki

💻

Casper van Elteren

💻

Charles Tapley Hoyt

💻

Christoph Gohlke

💻

Christopher Falter

💻

FredInChina

💻

Friso van Vollenhoven

💻

Gabor Szarnyas

💻

Gao Fangshu

💻

Grzegorz Chilczuk

💻

Gwyn Ciesla

💻

Hong Xu

💻

Jay Smith

💻

MapleCCC

💻

Marco Köpcke

💻

Markus Elfring

💻

Martino Mensio

💻

Matas

💻

Mike Lissner

💻

Philipp A.

💻

Puneetha Pai

💻

S Murthy

💻

Scott Gigante

💻

Thierry Thomas

💻

Willem van den Boom

💻

Yisu Remy Wang

💻

YY Ahn

💻

kmankinen

💻

odidev

💻

sombreslames

💻

szcf-weiya

💻

tristanlatr

💻
Tamás Nepusz
Tamás Nepusz

💻
Fabio Zanini
Fabio Zanini

💻
Kevin Zhu
Kevin Zhu

💻
Gábor Csárdi
Gábor Csárdi

💻
Szabolcs Horvát
Szabolcs Horvát

💻
Vincent Traag
Vincent Traag

💻
deeenes
deeenes

💻
Seungoh Han
Seungoh Han

💻
Artem V L
Artem V L

💻
Yesung(Isaac) Lee
Yesung(Isaac) Lee

💻
John Boy
John Boy

💻
Casper da Costa-Luis
Casper da Costa-Luis

💻
Alberto Alcolea
Alberto Alcolea

💻
Árpád Horváth
Árpád Horváth

💻
ebraminio
ebraminio

💻
Fabian Witter
Fabian Witter

💻
Jan Katins
Jan Katins

💻
Nick Eubank
Nick Eubank

💻
Peter Scott
Peter Scott

💻
Sriram-Pattabiraman
Sriram-Pattabiraman

💻
Sviatoslav
Sviatoslav

💻
Ah-Young Nho
Ah-Young Nho

💻
Frederik Harwath
Frederik Harwath

💻
Navid Dianati
Navid Dianati

💻
abe-winter
abe-winter

💻
Alejandro Rivero
Alejandro Rivero

💻
Ariki
Ariki

💻
Casper van Elteren
Casper van Elteren

💻
Charles Tapley Hoyt
Charles Tapley Hoyt

💻
Christoph Gohlke
Christoph Gohlke

💻
Christopher Falter
Christopher Falter

💻
FredInChina
FredInChina

💻
Friso van Vollenhoven
Friso van Vollenhoven

💻
Gabor Szarnyas
Gabor Szarnyas

💻
Gao Fangshu
Gao Fangshu

💻
Grzegorz Chilczuk
Grzegorz Chilczuk

💻
Gwyn Ciesla
Gwyn Ciesla

💻
Hong Xu
Hong Xu

💻
Jay Smith
Jay Smith

💻
MapleCCC
MapleCCC

💻
Marco Köpcke
Marco Köpcke

💻
Markus Elfring
Markus Elfring

💻
Martino Mensio
Martino Mensio

💻
Matas
Matas

💻
Mike Lissner
Mike Lissner

💻
Philipp A.
Philipp A.

💻
Puneetha Pai
Puneetha Pai

💻
S Murthy
S Murthy

💻
Scott Gigante
Scott Gigante

💻
Thierry Thomas
Thierry Thomas

💻
Willem van den Boom
Willem van den Boom

💻
Yisu Remy Wang
Yisu Remy Wang

💻
YY Ahn
YY Ahn

💻
kmankinen
kmankinen

💻
odidev
odidev

💻
sombreslames
sombreslames

💻
szcf-weiya
szcf-weiya

💻
tristanlatr
tristanlatr

💻
JDPowell648
JDPowell648

📖
k.h.lai
k.h.lai

💻
Anton Grübel
Anton Grübel

💻
flange-ipb
flange-ipb

💻
Paul m. p. Peny
Paul m. p. Peny

💻
David R. Connell
David R. Connell

💻
Rodrigo Monteiro de Moraes de Arruda Falcão
Rodrigo Monteiro de Moraes de Arruda Falcão

💻
Kreijstal
Kreijstal

💻
Michael Schneider
Michael Schneider

💻
Thomas Krijnen
Thomas Krijnen

💻
Tim Bernhard
Tim Bernhard

💻
Bea Márton
Bea Márton

💻
Sanat Kumar Gupta
Sanat Kumar Gupta

💻
diff --git a/MANIFEST.in b/MANIFEST.in index 5c68b5e82..39e2ee9fe 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,6 +8,10 @@ include scripts/*.sh include scripts/*.py include tests/*.py +include CHANGELOG.md +include CONTRIBUTORS.md +include CITATION.cff + graft vendor/source/igraph graft doc diff --git a/README.md b/README.md index a94f33f5a..1f7162788 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,10 @@ Also, when building in MSYS2, you need to set the `SETUPTOOLS_USE_DISTUTILS` environment variable to `stdlib`; this is because MSYS2 uses a patched version of `distutils` that conflicts with `setuptools >= 60.0`. +> [!TIP] +> You need the following packages: +> `$MINGW_PACKAGE_PREFIX-python-pip $MINGW_PACKAGE_PREFIX-python-setuptools $MINGW_PACKAGE_PREFIX-cc $MINGW_PACKAGE_PREFIX-cmake` + ### Enabling GraphML By default, GraphML is disabled, because `libxml2` is not available on Windows in @@ -158,8 +162,16 @@ the packaged igraph library instead of bringing its own copy. It is also useful on macOS if you want to link to the igraph library installed from Homebrew. -Due to the lack of support of `pkg-config` on Windows, it is currently not -possible to build against an external library on Windows. +Due to the lack of support of `pkg-config` on MSVC, it is currently not +possible to build against an external library on MSVC. + +In case you are already using a MSYS2/[MinGW](https://www.mingw-w64.org/) and already have +[mingw-w64-igraph](https://packages.msys2.org/base/mingw-w64-igraph) installed, +simply type: +``` +IGRAPH_USE_PKG_CONFIG=1 SETUPTOOLS_USE_DISTUTILS=stdlib pip install igraph +``` +to build. **Warning:** the Python interface is guaranteed to work only with the same version of the C core that is vendored inside the `vendor/source/igraph` @@ -290,7 +302,7 @@ faster than the first one as the C core does not need to be recompiled. We aim to keep up with the development cycle of Python and support all official Python versions that have not reached their end of life yet. Currently this -means that we support Python 3.8 to 3.12, inclusive. Please refer to [this +means that we support Python 3.9 to 3.13, inclusive. Please refer to [this page](https://devguide.python.org/versions/) for the status of Python branches and let us know if you encounter problems with `igraph` on any of the non-EOL Python versions. diff --git a/doc/examples_sphinx-gallery/articulation_points.py b/doc/examples_sphinx-gallery/articulation_points.py index 0dadded36..6492f1f6f 100644 --- a/doc/examples_sphinx-gallery/articulation_points.py +++ b/doc/examples_sphinx-gallery/articulation_points.py @@ -8,6 +8,7 @@ This example shows how to compute and visualize the `articulation points `_ in a graph using :meth:`igraph.GraphBase.articulation_points`. For an example on bridges instead, see :ref:`tutorials-bridges`. """ + import igraph as ig import matplotlib.pyplot as plt @@ -30,9 +31,9 @@ vertex_size=30, vertex_color="lightblue", vertex_label=range(g.vcount()), - vertex_frame_color = ["red" if v in articulation_points else "black" for v in g.vs], - vertex_frame_width = [3 if v in articulation_points else 1 for v in g.vs], + vertex_frame_color=["red" if v in articulation_points else "black" for v in g.vs], + vertex_frame_width=[3 if v in articulation_points else 1 for v in g.vs], edge_width=0.8, - edge_color='gray' + edge_color="gray", ) plt.show() diff --git a/doc/examples_sphinx-gallery/betweenness.py b/doc/examples_sphinx-gallery/betweenness.py index 29ed9b3e3..7c52bdb08 100644 --- a/doc/examples_sphinx-gallery/betweenness.py +++ b/doc/examples_sphinx-gallery/betweenness.py @@ -24,14 +24,14 @@ # :meth:`igraph.utils.rescale` to rescale the betweennesses in the interval # ``[0, 1]``. def plot_betweenness(g, vertex_betweenness, edge_betweenness, ax, cax1, cax2): - '''Plot vertex/edge betweenness, with colorbars + """Plot vertex/edge betweenness, with colorbars Args: g: the graph to plot. ax: the Axes for the graph cax1: the Axes for the vertex betweenness colorbar cax2: the Axes for the edge betweenness colorbar - ''' + """ # Rescale betweenness to be between 0.0 and 1.0 scaled_vertex_betweenness = ig.rescale(vertex_betweenness, clamp=True) @@ -45,7 +45,7 @@ def plot_betweenness(g, vertex_betweenness, edge_betweenness, ax, cax1, cax2): # Plot graph g.vs["color"] = [cmap1(betweenness) for betweenness in scaled_vertex_betweenness] - g.vs["size"] = ig.rescale(vertex_betweenness, (10, 50)) + g.vs["size"] = ig.rescale(vertex_betweenness, (10, 50)) g.es["color"] = [cmap2(betweenness) for betweenness in scaled_edge_betweenness] g.es["width"] = ig.rescale(edge_betweenness, (0.5, 1.0)) ig.plot( @@ -58,8 +58,8 @@ def plot_betweenness(g, vertex_betweenness, edge_betweenness, ax, cax1, cax2): # Color bars norm1 = ScalarMappable(norm=Normalize(0, max(vertex_betweenness)), cmap=cmap1) norm2 = ScalarMappable(norm=Normalize(0, max(edge_betweenness)), cmap=cmap2) - plt.colorbar(norm1, cax=cax1, orientation="horizontal", label='Vertex Betweenness') - plt.colorbar(norm2, cax=cax2, orientation="horizontal", label='Edge Betweenness') + plt.colorbar(norm1, cax=cax1, orientation="horizontal", label="Vertex Betweenness") + plt.colorbar(norm2, cax=cax2, orientation="horizontal", label="Edge Betweenness") # %% diff --git a/doc/examples_sphinx-gallery/bipartite_matching.py b/doc/examples_sphinx-gallery/bipartite_matching.py index ad7538fa2..1046e1322 100644 --- a/doc/examples_sphinx-gallery/bipartite_matching.py +++ b/doc/examples_sphinx-gallery/bipartite_matching.py @@ -7,6 +7,7 @@ This example demonstrates an efficient way to find and visualise a maximum biparite matching using :meth:`igraph.Graph.maximum_bipartite_matching`. """ + import igraph as ig import matplotlib.pyplot as plt @@ -16,7 +17,7 @@ # - nodes 5-8 to the other side g = ig.Graph.Bipartite( [0, 0, 0, 0, 0, 1, 1, 1, 1], - [(0, 5), (1, 6), (1, 7), (2, 5), (2, 8), (3, 6), (4, 5), (4, 6)] + [(0, 5), (1, 6), (1, 7), (2, 5), (2, 8), (3, 6), (4, 5), (4, 6)], ) # %% diff --git a/doc/examples_sphinx-gallery/bipartite_matching_maxflow.py b/doc/examples_sphinx-gallery/bipartite_matching_maxflow.py index 28b06e0b7..eec39a81b 100644 --- a/doc/examples_sphinx-gallery/bipartite_matching_maxflow.py +++ b/doc/examples_sphinx-gallery/bipartite_matching_maxflow.py @@ -9,6 +9,7 @@ .. note:: :meth:`igraph.Graph.maximum_bipartite_matching` is usually a better way to find the maximum bipartite matching. For a demonstration on how to use that method instead, check out :ref:`tutorials-bipartite-matching`. """ + import igraph as ig import matplotlib.pyplot as plt @@ -17,7 +18,7 @@ g = ig.Graph( 9, [(0, 4), (0, 5), (1, 4), (1, 6), (1, 7), (2, 5), (2, 7), (2, 8), (3, 6), (3, 7)], - directed=True + directed=True, ) # %% @@ -62,6 +63,6 @@ vertex_size=30, vertex_label=range(g.vcount()), vertex_color=["lightblue" if i < 9 else "orange" for i in range(11)], - edge_width=[1.0 + flow.flow[i] for i in range(g.ecount())] + edge_width=[1.0 + flow.flow[i] for i in range(g.ecount())], ) plt.show() diff --git a/doc/examples_sphinx-gallery/bridges.py b/doc/examples_sphinx-gallery/bridges.py index 26b8dce6a..08dc47d48 100644 --- a/doc/examples_sphinx-gallery/bridges.py +++ b/doc/examples_sphinx-gallery/bridges.py @@ -7,6 +7,7 @@ This example shows how to compute and visualize the `bridges `_ in a graph using :meth:`igraph.GraphBase.bridges`. For an example on articulation points instead, see :ref:`tutorials-articulation-points`. """ + import igraph as ig import matplotlib.pyplot as plt @@ -37,7 +38,7 @@ target=ax, vertex_size=30, vertex_color="lightblue", - vertex_label=range(g.vcount()) + vertex_label=range(g.vcount()), ) plt.show() @@ -72,9 +73,9 @@ vertex_size=30, vertex_color="lightblue", vertex_label=range(g.vcount()), - edge_background="#FFF0", # transparent background color - edge_align_label=True, # make sure labels are aligned with the edge + edge_background="#FFF0", # transparent background color + edge_align_label=True, # make sure labels are aligned with the edge edge_label=g.es["label"], - edge_label_color="red" + edge_label_color="red", ) plt.show() diff --git a/doc/examples_sphinx-gallery/cluster_contraction.py b/doc/examples_sphinx-gallery/cluster_contraction.py index aa7cf38a7..fc02fc293 100644 --- a/doc/examples_sphinx-gallery/cluster_contraction.py +++ b/doc/examples_sphinx-gallery/cluster_contraction.py @@ -7,12 +7,13 @@ This example shows how to find the communities in a graph, then contract each community into a single node using :class:`igraph.clustering.VertexClustering`. For this tutorial, we'll use the *Donald Knuth's Les Miserables Network*, which shows the coapperances of characters in the novel *Les Miserables*. """ + import igraph as ig import matplotlib.pyplot as plt # %% # We begin by load the graph from file. The file containing this network can be -# downloaded `here `_. +# downloaded `here `_. g = ig.load("./lesmis/lesmis.gml") # %% @@ -106,7 +107,10 @@ # Finally, we can assign colors to the clusters and plot the cluster graph, # including a legend to make things clear: palette2 = ig.GradientPalette("gainsboro", "black") -g.es["color"] = [palette2.get(int(i)) for i in ig.rescale(cluster_graph.es["size"], (0, 255), clamp=True)] +g.es["color"] = [ + palette2.get(int(i)) + for i in ig.rescale(cluster_graph.es["size"], (0, 255), clamp=True) +] fig2, ax2 = plt.subplots() ig.plot( @@ -123,7 +127,8 @@ legend_handles = [] for i in range(num_communities): handle = ax2.scatter( - [], [], + [], + [], s=100, facecolor=palette1.get(i), edgecolor="k", @@ -133,7 +138,7 @@ ax2.legend( handles=legend_handles, - title='Community:', + title="Community:", bbox_to_anchor=(0, 1.0), bbox_transform=ax2.transAxes, ) diff --git a/doc/examples_sphinx-gallery/complement.py b/doc/examples_sphinx-gallery/complement.py index 4436ae372..b4f2f17a0 100644 --- a/doc/examples_sphinx-gallery/complement.py +++ b/doc/examples_sphinx-gallery/complement.py @@ -7,6 +7,7 @@ This example shows how to generate the `complement graph `_ of a graph (sometimes known as the anti-graph) using :meth:`igraph.GraphBase.complementer`. """ + import igraph as ig import matplotlib.pyplot as plt import random @@ -49,14 +50,14 @@ layout="circle", vertex_color="black", ) -axs[0, 0].set_title('Original graph') +axs[0, 0].set_title("Original graph") ig.plot( g2, target=axs[0, 1], layout="circle", vertex_color="black", ) -axs[0, 1].set_title('Complement graph') +axs[0, 1].set_title("Complement graph") ig.plot( g_full, @@ -64,12 +65,12 @@ layout="circle", vertex_color="black", ) -axs[1, 0].set_title('Union graph') +axs[1, 0].set_title("Union graph") ig.plot( g_empty, target=axs[1, 1], layout="circle", vertex_color="black", ) -axs[1, 1].set_title('Complement of union graph') +axs[1, 1].set_title("Complement of union graph") plt.show() diff --git a/doc/examples_sphinx-gallery/configuration.py b/doc/examples_sphinx-gallery/configuration.py index ab408886d..4e9c077eb 100644 --- a/doc/examples_sphinx-gallery/configuration.py +++ b/doc/examples_sphinx-gallery/configuration.py @@ -7,6 +7,7 @@ This example shows how to use igraph's :class:`configuration instance ` to set default igraph settings. This is useful for setting global settings so that they don't need to be explicitly stated at the beginning of every igraph project you work on. """ + import igraph as ig import matplotlib.pyplot as plt import random @@ -18,16 +19,16 @@ ig.config["plotting.palette"] = "rainbow" # %% -# Then, we save them. By default, ``ig.config.save()`` will save files to -# ``~/.igraphrc`` on Linux and Max OS X systems, or in -# ``%USERPROFILE%\.igraphrc`` for Windows systems: -ig.config.save() +# The updated configuration affects only the current session. Optionally, it +# can be saved using ``ig.config.save()``. By default, this function writes the +# configuration to ``~/.igraphrc`` on Linux and Max OS X systems, and in +# ``%USERPROFILE%\.igraphrc`` on Windows systems. # %% -# The code above only needs to be run once (to store the new config options -# into the ``.igraphrc`` file). Whenever you use igraph and this file exists, -# igraph will read its content and use those options as defaults. For -# example, let's create and plot a new graph to demonstrate: +# The configuration only needs to be saved to `.igraphrc` once, and it will +# be automatically used in all future sessions. Whenever you use igraph and +# this file exists, igraph will read its content and use those options as +# defaults. For example, let's create and plot a new graph to demonstrate: random.seed(1) g = ig.Graph.Barabasi(n=100, m=1) diff --git a/doc/examples_sphinx-gallery/connected_components.py b/doc/examples_sphinx-gallery/connected_components.py index 008f66cec..33c341fee 100644 --- a/doc/examples_sphinx-gallery/connected_components.py +++ b/doc/examples_sphinx-gallery/connected_components.py @@ -7,6 +7,7 @@ This example demonstrates how to visualise the connected components in a graph using :meth:`igraph.GraphBase.connected_components`. """ + import igraph as ig import matplotlib.pyplot as plt import random @@ -21,7 +22,7 @@ # %% # Now we can cluster the graph into weakly connected components, i.e. subgraphs # that have no edges connecting them to one another: -components = g.connected_components(mode='weak') +components = g.connected_components(mode="weak") # %% # Finally, we can visualize the distinct connected components of the graph: diff --git a/doc/examples_sphinx-gallery/delaunay-triangulation.py b/doc/examples_sphinx-gallery/delaunay-triangulation.py index ba6ce5a85..3a25eebe1 100644 --- a/doc/examples_sphinx-gallery/delaunay-triangulation.py +++ b/doc/examples_sphinx-gallery/delaunay-triangulation.py @@ -8,6 +8,7 @@ This example demonstrates how to calculate the `Delaunay triangulation `_ of an input graph. We start by generating a set of points on a 2D grid using random ``numpy`` arrays and a graph with those vertex coordinates and no edges. """ + import numpy as np from scipy.spatial import Delaunay import igraph as ig @@ -20,8 +21,8 @@ np.random.seed(0) x, y = np.random.rand(2, 30) g = ig.Graph(30) -g.vs['x'] = x -g.vs['y'] = y +g.vs["x"] = x +g.vs["y"] = y # %% # Because we already set the `x` and `y` vertex attributes, we can use diff --git a/doc/examples_sphinx-gallery/erdos_renyi.py b/doc/examples_sphinx-gallery/erdos_renyi.py index c4f2879f7..aeb808992 100644 --- a/doc/examples_sphinx-gallery/erdos_renyi.py +++ b/doc/examples_sphinx-gallery/erdos_renyi.py @@ -12,6 +12,7 @@ We generate two graphs of each, so we can confirm that our graph generator is truly random. """ + import igraph as ig import matplotlib.pyplot as plt import random @@ -48,33 +49,11 @@ # differences: fig, axs = plt.subplots(2, 2) # Probability -ig.plot( - g1, - target=axs[0, 0], - layout="circle", - vertex_color="lightblue" -) -ig.plot( - g2, - target=axs[0, 1], - layout="circle", - vertex_color="lightblue" -) -axs[0, 0].set_ylabel('Probability') +ig.plot(g1, target=axs[0, 0], layout="circle", vertex_color="lightblue") +ig.plot(g2, target=axs[0, 1], layout="circle", vertex_color="lightblue") +axs[0, 0].set_ylabel("Probability") # N edges -ig.plot( - g3, - target=axs[1, 0], - layout="circle", - vertex_color="lightblue", - vertex_size=15 -) -ig.plot( - g4, - target=axs[1, 1], - layout="circle", - vertex_color="lightblue", - vertex_size=15 -) -axs[1, 0].set_ylabel('N. edges') +ig.plot(g3, target=axs[1, 0], layout="circle", vertex_color="lightblue", vertex_size=15) +ig.plot(g4, target=axs[1, 1], layout="circle", vertex_color="lightblue", vertex_size=15) +axs[1, 0].set_ylabel("N. edges") plt.show() diff --git a/doc/examples_sphinx-gallery/generate_dag.py b/doc/examples_sphinx-gallery/generate_dag.py index addbc661c..1565a59ed 100644 --- a/doc/examples_sphinx-gallery/generate_dag.py +++ b/doc/examples_sphinx-gallery/generate_dag.py @@ -8,6 +8,7 @@ This example demonstrates how to create a random directed acyclic graph (DAG), which is useful in a number of contexts including for Git commit history. """ + import igraph as ig import matplotlib.pyplot as plt import random diff --git a/doc/examples_sphinx-gallery/isomorphism.py b/doc/examples_sphinx-gallery/isomorphism.py index 7f98bc053..57c6034f4 100644 --- a/doc/examples_sphinx-gallery/isomorphism.py +++ b/doc/examples_sphinx-gallery/isomorphism.py @@ -7,6 +7,7 @@ This example shows how to check for `isomorphism `_ between small graphs using :meth:`igraph.GraphBase.isomorphic`. """ + import igraph as ig import matplotlib.pyplot as plt @@ -66,6 +67,20 @@ target=axs[2], **visual_style, ) -fig.text(0.38, 0.5, '$\\simeq$' if g1.isomorphic(g2) else '$\\neq$', fontsize=15, ha='center', va='center') -fig.text(0.65, 0.5, '$\\simeq$' if g2.isomorphic(g3) else '$\\neq$', fontsize=15, ha='center', va='center') +fig.text( + 0.38, + 0.5, + "$\\simeq$" if g1.isomorphic(g2) else "$\\neq$", + fontsize=15, + ha="center", + va="center", +) +fig.text( + 0.65, + 0.5, + "$\\simeq$" if g2.isomorphic(g3) else "$\\neq$", + fontsize=15, + ha="center", + va="center", +) plt.show() diff --git a/doc/examples_sphinx-gallery/maxflow.py b/doc/examples_sphinx-gallery/maxflow.py index 246d4c451..eb76684a4 100644 --- a/doc/examples_sphinx-gallery/maxflow.py +++ b/doc/examples_sphinx-gallery/maxflow.py @@ -8,16 +8,13 @@ This example shows how to construct a max flow on a directed graph with edge capacities using :meth:`igraph.Graph.maxflow`. """ + import igraph as ig import matplotlib.pyplot as plt # %% # First, we generate a graph and assign a "capacity" to each edge: -g = ig.Graph( - 6, - [(3, 2), (3, 4), (2, 1), (4,1), (4, 5), (1, 0), (5, 0)], - directed=True -) +g = ig.Graph(6, [(3, 2), (3, 4), (2, 1), (4, 1), (4, 5), (1, 0), (5, 0)], directed=True) g.es["capacity"] = [7, 8, 1, 2, 3, 4, 5] # %% @@ -39,6 +36,6 @@ target=ax, layout="circle", vertex_label=range(g.vcount()), - vertex_color="lightblue" + vertex_color="lightblue", ) plt.show() diff --git a/doc/examples_sphinx-gallery/minimum_spanning_trees.py b/doc/examples_sphinx-gallery/minimum_spanning_trees.py index 60c320bf6..9177e976a 100644 --- a/doc/examples_sphinx-gallery/minimum_spanning_trees.py +++ b/doc/examples_sphinx-gallery/minimum_spanning_trees.py @@ -8,6 +8,7 @@ This example shows how to generate a `minimum spanning tree `_ from an input graph using :meth:`igraph.Graph.spanning_tree`. If you only need a regular spanning tree, check out :ref:`tutorials-spanning-trees`. """ + import random import igraph as ig import matplotlib.pyplot as plt diff --git a/doc/examples_sphinx-gallery/online_user_actions.py b/doc/examples_sphinx-gallery/online_user_actions.py index 1da528570..47cc00ff5 100644 --- a/doc/examples_sphinx-gallery/online_user_actions.py +++ b/doc/examples_sphinx-gallery/online_user_actions.py @@ -18,23 +18,24 @@ # indicates a certain action taken by a user (e.g. click on a button within a # website). Actual user data usually come with time stamp, but that's not # essential for this example. -action_dataframe = pd.DataFrame([ - ['dsj3239asadsa3', 'createPage', 'greatProject'], - ['2r09ej221sk2k5', 'editPage', 'greatProject'], - ['dsj3239asadsa3', 'editPage', 'greatProject'], - ['789dsadafj32jj', 'editPage', 'greatProject'], - ['oi32ncwosap399', 'editPage', 'greatProject'], - ['4r4320dkqpdokk', 'createPage', 'miniProject'], - ['320eljl3lk3239', 'editPage', 'miniProject'], - ['dsj3239asadsa3', 'editPage', 'miniProject'], - ['3203ejew332323', 'createPage', 'private'], - ['3203ejew332323', 'editPage', 'private'], - ['40m11919332msa', 'createPage', 'private2'], - ['40m11919332msa', 'editPage', 'private2'], - ['dsj3239asadsa3', 'createPage', 'anotherGreatProject'], - ['2r09ej221sk2k5', 'editPage', 'anotherGreatProject'], +action_dataframe = pd.DataFrame( + [ + ["dsj3239asadsa3", "createPage", "greatProject"], + ["2r09ej221sk2k5", "editPage", "greatProject"], + ["dsj3239asadsa3", "editPage", "greatProject"], + ["789dsadafj32jj", "editPage", "greatProject"], + ["oi32ncwosap399", "editPage", "greatProject"], + ["4r4320dkqpdokk", "createPage", "miniProject"], + ["320eljl3lk3239", "editPage", "miniProject"], + ["dsj3239asadsa3", "editPage", "miniProject"], + ["3203ejew332323", "createPage", "private"], + ["3203ejew332323", "editPage", "private"], + ["40m11919332msa", "createPage", "private2"], + ["40m11919332msa", "editPage", "private2"], + ["dsj3239asadsa3", "createPage", "anotherGreatProject"], + ["2r09ej221sk2k5", "editPage", "anotherGreatProject"], ], - columns=['userid', 'action', 'project'], + columns=["userid", "action", "project"], ) # %% @@ -42,7 +43,7 @@ # We choose to use a weighted adjacency matrix for this, i.e. a table with rows # and columns indexes by the users that has nonzero entries whenever folks # collaborate. First, let's get the users and prepare an empty matrix: -users = action_dataframe['userid'].unique() +users = action_dataframe["userid"].unique() adjacency_matrix = pd.DataFrame( np.zeros((len(users), len(users)), np.int32), index=users, @@ -51,8 +52,8 @@ # %% # Then, let's iterate over all projects one by one, and add all collaborations: -for _project, project_data in action_dataframe.groupby('project'): - project_users = project_data['userid'].values +for _project, project_data in action_dataframe.groupby("project"): + project_users = project_data["userid"].values for i1, user1 in enumerate(project_users): for user2 in project_users[:i1]: adjacency_matrix.at[user1, user2] += 1 @@ -60,12 +61,12 @@ # %% # There are many ways to achieve the above matrix, so don't be surprised if you # came up with another algorithm ;-) Now it's time to make the graph: -g = ig.Graph.Weighted_Adjacency(adjacency_matrix, mode='plus') +g = ig.Graph.Weighted_Adjacency(adjacency_matrix, mode="plus") # %% # We can take a look at the graph via plotting functions. We can first make a # layout: -layout = g.layout('circle') +layout = g.layout("circle") # %% # Then we can prepare vertex sizes based on their closeness to other vertices @@ -79,7 +80,7 @@ g, target=ax, layout=layout, - vertex_label=g.vs['name'], + vertex_label=g.vs["name"], vertex_color="lightblue", vertex_size=vertex_size, edge_width=g.es["weight"], @@ -89,14 +90,14 @@ # %% # Loops indicate "self-collaborations", which are not very meaningful. To # filter out loops without losing the edge weights, we can use: -g = g.simplify(combine_edges='first') +g = g.simplify(combine_edges="first") fig, ax = plt.subplots() ig.plot( g, target=ax, layout=layout, - vertex_label=g.vs['name'], + vertex_label=g.vs["name"], vertex_color="lightblue", vertex_size=vertex_size, edge_width=g.es["weight"], diff --git a/doc/examples_sphinx-gallery/personalized_pagerank.py b/doc/examples_sphinx-gallery/personalized_pagerank.py new file mode 100644 index 000000000..f7e2f9e2b --- /dev/null +++ b/doc/examples_sphinx-gallery/personalized_pagerank.py @@ -0,0 +1,98 @@ +""" +.. _tutorials-personalized_pagerank: + +=============================== +Personalized PageRank on a grid +=============================== + +This example demonstrates how to calculate and visualize personalized PageRank on a grid. We use the :meth:`igraph.Graph.personalized_pagerank` method, and demonstrate the effects on a grid graph. +""" + +# %% +# .. note:: +# +# The PageRank score of a vertex reflects the probability that a random walker will be at that vertex over the long run. At each step the walker has a 1 - damping chance to restart the walk and pick a starting vertex according to the probabilities defined in the reset vector. + +import igraph as ig +import matplotlib.cm as cm +import matplotlib.pyplot as plt +import numpy as np + +# %% +# We define a function that plots the graph on a Matplotlib axis, along with +# its personalized PageRank values. The function also generates a +# color bar on the side to see how the values change. +# We use `Matplotlib's Normalize class `_ +# to set the colors and ensure that our color bar range is correct. + + +def plot_pagerank(graph: ig.Graph, p_pagerank: list[float]): + """Plots personalized PageRank values on a grid graph with a colorbar. + + Parameters + ---------- + graph : ig.Graph + graph to plot + p_pagerank : list[float] + calculated personalized PageRank values + """ + # Create the axis for matplotlib + _, ax = plt.subplots(figsize=(8, 8)) + + # Create a matplotlib colormap + # coolwarm goes from blue (lowest value) to red (highest value) + cmap = cm.coolwarm + + # Normalize the PageRank values for colormap + normalized_pagerank = ig.rescale(p_pagerank) + + graph.vs["color"] = [cmap(pr) for pr in normalized_pagerank] + graph.vs["size"] = ig.rescale(p_pagerank, (20, 40)) + graph.es["color"] = "gray" + graph.es["width"] = 1.5 + + # Plot the graph + ig.plot(graph, target=ax, layout=graph.layout_grid()) + + # Add a colorbar + sm = cm.ScalarMappable( + norm=plt.Normalize(min(p_pagerank), max(p_pagerank)), cmap=cmap + ) + plt.colorbar(sm, ax=ax, label="Personalized PageRank") + + plt.title("Graph with Personalized PageRank") + plt.axis("equal") + plt.show() + + +# %% +# First, we generate a graph, e.g. a Lattice Graph, which basically is a ``dim x dim`` grid: +dim = 5 +grid_size = (dim, dim) # dim rows, dim columns +g = ig.Graph.Lattice(dim=grid_size, circular=False) + +# %% +# Then we initialize the ``reset_vector`` (it's length should be equal to the number of vertices in the graph): +reset_vector = np.zeros(g.vcount()) + +# %% +# Then we set the nodes to prioritize, for example nodes with indices ``0`` and ``18``: +reset_vector[0] = 1 +reset_vector[18] = 0.65 + +# %% +# Then we calculate the personalized PageRank: +personalized_page_rank = g.personalized_pagerank(damping=0.85, reset=reset_vector) + +# %% +# Finally, we plot the graph with the personalized PageRank values: +plot_pagerank(g, personalized_page_rank) + + +# %% +# Alternatively, we can play around with the ``damping`` parameter: +personalized_page_rank = g.personalized_pagerank(damping=0.45, reset=reset_vector) + +# %% +# Here we can see the same plot with the new damping parameter: +plot_pagerank(g, personalized_page_rank) diff --git a/doc/examples_sphinx-gallery/plot_iplotx.py b/doc/examples_sphinx-gallery/plot_iplotx.py new file mode 100644 index 000000000..5c3c12c43 --- /dev/null +++ b/doc/examples_sphinx-gallery/plot_iplotx.py @@ -0,0 +1,63 @@ +""" +.. _tutorials-iplotx: + +============================== +Visualising graphs with iplotx +============================== +``iplotx`` (https://iplotx.readthedocs.io) is a library for visualisation of graphs/networks +with direct compatibility with both igraph and NetworkX. It uses ``matplotlib`` behind the +scenes so the results are compatible with the current igraph matplotlib backend and many +additional chart types (e.g. bar charts, annotations). + +Compared to the standard visualisations shipped with igraph, ``iplotx`` offers: + +- More styling options +- More consistent behaviour across DPI resolutions and backends +- More consistent matplotlib artists for plot editing and animation + +""" + +import igraph as ig +import iplotx as ipx + +# Construct a graph with 5 vertices +n_vertices = 5 +edges = [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (3, 4)] +g = ig.Graph(n_vertices, edges) + +# Set attributes for the graph, nodes, and edges +g["title"] = "Small Social Network" +g.vs["name"] = [ + "Daniel Morillas", + "Kathy Archer", + "Kyle Ding", + "Joshua Walton", + "Jana Hoyer", +] +g.vs["gender"] = ["M", "F", "F", "M", "F"] +g.es["married"] = [False, False, False, False, False, False, False, True] + +# Set individual attributes +g.vs[1]["name"] = "Kathy Morillas" +g.es[0]["married"] = True + +# Plot using iplotx +ipx.network( + g, + layout="circle", # print nodes in a circular layout + vertex_marker="s", + vertex_size=45, + vertex_linewidth=2, + vertex_facecolor=[ + "lightblue" if gender == "M" else "deeppink" for gender in g.vs["gender"] + ], + vertex_label_color=[ + "black" if gender == "M" else "white" for gender in g.vs["gender"] + ], + vertex_edgecolor="black", + vertex_labels=[name.replace(" ", "\n") for name in g.vs["name"]], + edge_linewidth=[2 if married else 1 for married in g.es["married"]], + edge_color=["#7142cf" if married else "#AAA" for married in g.es["married"]], + edge_padding=3, + aspect=1.0, +) diff --git a/doc/examples_sphinx-gallery/quickstart.py b/doc/examples_sphinx-gallery/quickstart.py index ec6f7ddb7..0735768e5 100644 --- a/doc/examples_sphinx-gallery/quickstart.py +++ b/doc/examples_sphinx-gallery/quickstart.py @@ -15,6 +15,7 @@ To find out more features that igraph has to offer, check out the :ref:`gallery`! """ + import igraph as ig import matplotlib.pyplot as plt @@ -25,7 +26,13 @@ # Set attributes for the graph, nodes, and edges g["title"] = "Small Social Network" -g.vs["name"] = ["Daniel Morillas", "Kathy Archer", "Kyle Ding", "Joshua Walton", "Jana Hoyer"] +g.vs["name"] = [ + "Daniel Morillas", + "Kathy Archer", + "Kyle Ding", + "Joshua Walton", + "Jana Hoyer", +] g.vs["gender"] = ["M", "F", "F", "M", "F"] g.es["married"] = [False, False, False, False, False, False, False, True] @@ -35,27 +42,29 @@ # Plot in matplotlib # Note that attributes can be set globally (e.g. vertex_size), or set individually using arrays (e.g. vertex_color) -fig, ax = plt.subplots(figsize=(5,5)) +fig, ax = plt.subplots(figsize=(5, 5)) ig.plot( g, target=ax, - layout="circle", # print nodes in a circular layout + layout="circle", # print nodes in a circular layout vertex_size=30, - vertex_color=["steelblue" if gender == "M" else "salmon" for gender in g.vs["gender"]], + vertex_color=[ + "steelblue" if gender == "M" else "salmon" for gender in g.vs["gender"] + ], vertex_frame_width=4.0, vertex_frame_color="white", vertex_label=g.vs["name"], vertex_label_size=7.0, edge_width=[2 if married else 1 for married in g.es["married"]], - edge_color=["#7142cf" if married else "#AAA" for married in g.es["married"]] + edge_color=["#7142cf" if married else "#AAA" for married in g.es["married"]], ) plt.show() # Save the graph as an image file -fig.savefig('social_network.png') -fig.savefig('social_network.jpg') -fig.savefig('social_network.pdf') +fig.savefig("social_network.png") +fig.savefig("social_network.jpg") +fig.savefig("social_network.pdf") # Export and import a graph as a GML file. g.save("social_network.gml") diff --git a/doc/examples_sphinx-gallery/ring_animation.py b/doc/examples_sphinx-gallery/ring_animation.py index 25daa2918..33bd6a109 100644 --- a/doc/examples_sphinx-gallery/ring_animation.py +++ b/doc/examples_sphinx-gallery/ring_animation.py @@ -9,6 +9,7 @@ order to animate a ring graph sequentially being revealed. """ + import igraph as ig import matplotlib.pyplot as plt import matplotlib.animation as animation @@ -23,6 +24,7 @@ # Compute a 2D ring layout that looks like an actual ring layout = g.layout_circle() + # %% # Prepare an update function. This "callback" function will be run at every # frame and takes as a single argument the frame number. For simplicity, at @@ -71,6 +73,7 @@ def _update_graph(frame): handles = ax.get_children()[:nhandles] return handles + # %% # Run the animation fig, ax = plt.subplots() diff --git a/doc/examples_sphinx-gallery/shortest_path_visualisation.py b/doc/examples_sphinx-gallery/shortest_path_visualisation.py index 87fa01523..fedad160a 100644 --- a/doc/examples_sphinx-gallery/shortest_path_visualisation.py +++ b/doc/examples_sphinx-gallery/shortest_path_visualisation.py @@ -8,15 +8,13 @@ This example demonstrates how to find the shortest distance between two vertices of a weighted or an unweighted graph. """ + import igraph as ig import matplotlib.pyplot as plt # %% # To find the shortest path or distance between two nodes, we can use :meth:`igraph.GraphBase.get_shortest_paths`. If we're only interested in counting the unweighted distance, then we can do the following: -g = ig.Graph( - 6, - [(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)] -) +g = ig.Graph(6, [(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)]) results = g.get_shortest_paths(1, to=4, output="vpath") # results = [[1, 0, 2, 4]] @@ -25,7 +23,7 @@ # We can print the result of the computation: if len(results[0]) > 0: # The distance is the number of vertices in the shortest path minus one. - print("Shortest distance is: ", len(results[0])-1) + print("Shortest distance is: ", len(results[0]) - 1) else: print("End node could not be reached!") @@ -60,20 +58,20 @@ # %% # In case you are wondering how the visualization figure was done, here's the code: -g.es['width'] = 0.5 -g.es[results[0]]['width'] = 2.5 +g.es["width"] = 0.5 +g.es[results[0]]["width"] = 2.5 fig, ax = plt.subplots() ig.plot( g, target=ax, - layout='circle', - vertex_color='steelblue', + layout="circle", + vertex_color="steelblue", vertex_label=range(g.vcount()), - edge_width=g.es['width'], + edge_width=g.es["width"], edge_label=g.es["weight"], - edge_color='#666', + edge_color="#666", edge_align_label=True, - edge_background='white' + edge_background="white", ) plt.show() diff --git a/doc/examples_sphinx-gallery/simplify.py b/doc/examples_sphinx-gallery/simplify.py index 893b67fae..ed36b2da5 100644 --- a/doc/examples_sphinx-gallery/simplify.py +++ b/doc/examples_sphinx-gallery/simplify.py @@ -5,25 +5,28 @@ This example shows how to remove self loops and multiple edges using :meth:`igraph.GraphBase.simplify`. """ + import igraph as ig import matplotlib.pyplot as plt # %% # We start with a graph that includes loops and multiedges: -g1 = ig.Graph([ - (0, 1), - (1, 2), - (2, 3), - (3, 4), - (4, 0), - (0, 0), - (1, 4), - (1, 4), - (0, 2), - (2, 4), - (2, 4), - (2, 4), - (3, 3)], +g1 = ig.Graph( + [ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 0), + (0, 0), + (1, 4), + (1, 4), + (0, 2), + (2, 4), + (2, 4), + (2, 4), + (3, 3), + ], ) # %% @@ -59,13 +62,29 @@ target=axs[1], **visual_style, ) -axs[0].set_title('Multigraph...') -axs[1].set_title('...simplified') +axs[0].set_title("Multigraph...") +axs[1].set_title("...simplified") # Draw rectangles around axes -axs[0].add_patch(plt.Rectangle( - (0, 0), 1, 1, fc='none', ec='k', lw=4, transform=axs[0].transAxes, - )) -axs[1].add_patch(plt.Rectangle( - (0, 0), 1, 1, fc='none', ec='k', lw=4, transform=axs[1].transAxes, - )) +axs[0].add_patch( + plt.Rectangle( + (0, 0), + 1, + 1, + fc="none", + ec="k", + lw=4, + transform=axs[0].transAxes, + ) +) +axs[1].add_patch( + plt.Rectangle( + (0, 0), + 1, + 1, + fc="none", + ec="k", + lw=4, + transform=axs[1].transAxes, + ) +) plt.show() diff --git a/doc/examples_sphinx-gallery/spanning_trees.py b/doc/examples_sphinx-gallery/spanning_trees.py index 82f190a8a..be98501af 100644 --- a/doc/examples_sphinx-gallery/spanning_trees.py +++ b/doc/examples_sphinx-gallery/spanning_trees.py @@ -7,6 +7,7 @@ This example shows how to generate a spanning tree from an input graph using :meth:`igraph.Graph.spanning_tree`. For the related idea of finding a *minimum spanning tree*, see :ref:`tutorials-minimum-spanning-trees`. """ + import igraph as ig import matplotlib.pyplot as plt import random @@ -29,7 +30,7 @@ g = g.permute_vertices(permutation) new_layout = g.layout("grid") for i in range(36): - new_layout[permutation[i]] = layout[i] + new_layout[i] = layout[permutation[i]] layout = new_layout # %% @@ -47,13 +48,7 @@ g.es[spanning_tree]["width"] = 3.0 fig, ax = plt.subplots() -ig.plot( - g, - target=ax, - layout=layout, - vertex_color="lightblue", - edge_width=g.es["width"] -) +ig.plot(g, target=ax, layout=layout, vertex_color="lightblue", edge_width=g.es["width"]) plt.show() # %% diff --git a/doc/examples_sphinx-gallery/stochastic_variability.py b/doc/examples_sphinx-gallery/stochastic_variability.py new file mode 100644 index 000000000..2afc01153 --- /dev/null +++ b/doc/examples_sphinx-gallery/stochastic_variability.py @@ -0,0 +1,171 @@ +""" +.. _tutorials-stochastic-variability: + +========================================================= +Stochastic Variability in Community Detection Algorithms +========================================================= + +This example demonstrates the use of stochastic community detection methods to check whether a network possesses a strong community structure, and whether the partitionings we obtain are meaningul. Many community detection algorithms are randomized, and return somewhat different results after each run, depending on the random seed that was set. When there is a robust community structure, we expect these results to be similar to each other. When the community structure is weak or non-existent, the results may be noisy and highly variable. We will employ several partion similarity measures to analyse the consistency of the results, including the normalized mutual information (NMI), the variation of information (VI), and the Rand index (RI). + +""" + +# %% +import igraph as ig +import matplotlib.pyplot as plt +import itertools +import random + +# %% +# .. note:: +# We set a random seed to ensure that the results look exactly the same in +# the gallery. You don't need to do this when exploring randomness. +random.seed(42) + +# %% +# We will use Zachary's karate club dataset [1]_, a classic example of a network +# with a strong community structure: +karate = ig.Graph.Famous("Zachary") + +# %% +# We will compare it to an an Erdős-Rényi :math:`G(n, m)` random network having +# the same number of vertices and edges. The parameters 'n' and 'm' refer to the +# vertex and edge count, respectively. Since this is a random network, it should +# have no community structure. +random_graph = ig.Graph.Erdos_Renyi(n=karate.vcount(), m=karate.ecount()) + +# %% +# First, let us plot the two networks for a visual comparison: + +# Create subplots +fig, axes = plt.subplots(1, 2, figsize=(12, 6), subplot_kw={"aspect": "equal"}) + +# Karate club network +ig.plot( + karate, + target=axes[0], + vertex_color="lightblue", + vertex_size=30, + vertex_label=range(karate.vcount()), + vertex_label_size=10, + edge_width=1, +) +axes[0].set_title("Karate club network") + +# Random network +ig.plot( + random_graph, + target=axes[1], + vertex_color="lightcoral", + vertex_size=30, + vertex_label=range(random_graph.vcount()), + vertex_label_size=10, + edge_width=1, +) +axes[1].set_title("Erdős-Rényi random network") + +plt.show() + + +# %% +# Function to compute similarity between partitions using various methods: +def compute_pairwise_similarity(partitions, method): + similarities = [] + + for p1, p2 in itertools.combinations(partitions, 2): + similarity = ig.compare_communities(p1, p2, method=method) + similarities.append(similarity) + + return similarities + + +# %% +# The Leiden method, accessible through :meth:`igraph.Graph.community_leiden()`, +# is a modularity maximization approach for community detection. Since exact +# modularity maximization is NP-hard, the algorithm employs a greedy heuristic +# that processes vertices in a random order. This randomness leads to +# variation in the detected communities across different runs, which is why +# results may differ each time the method is applied. The following function +# runs the Leiden algorithm multiple times: +def run_experiment(graph, iterations=100): + partitions = [ + graph.community_leiden(objective_function="modularity").membership + for _ in range(iterations) + ] + nmi_scores = compute_pairwise_similarity(partitions, method="nmi") + vi_scores = compute_pairwise_similarity(partitions, method="vi") + ri_scores = compute_pairwise_similarity(partitions, method="rand") + return nmi_scores, vi_scores, ri_scores + + +# %% +# Run the experiment on both networks: +nmi_karate, vi_karate, ri_karate = run_experiment(karate) +nmi_random, vi_random, ri_random = run_experiment(random_graph) + +# %% +# Finally, let us plot histograms of the pairwise similarities of the obtained +# partitionings to understand the result: +fig, axes = plt.subplots(2, 3, figsize=(12, 6)) +measures = [ + # Normalized Mutual Information (0-1, higher = more similar) + (nmi_karate, nmi_random, "NMI", 0, 1), + # Variation of Information (0+, lower = more similar) + (vi_karate, vi_random, "VI", 0, max(vi_karate + vi_random)), + # Rand Index (0-1, higher = more similar) + (ri_karate, ri_random, "RI", 0, 1), +] +colors = ["red", "blue", "green"] + +for i, (karate_scores, random_scores, measure, lower, upper) in enumerate(measures): + # Karate club histogram + axes[0][i].hist( + karate_scores, + bins=20, + range=(lower, upper), + density=True, # Probability density + alpha=0.7, + color=colors[i], + edgecolor="black", + ) + axes[0][i].set_title(f"{measure} - Karate club network") + axes[0][i].set_xlabel(f"{measure} score") + axes[0][i].set_ylabel("PDF") + + # Random network histogram + axes[1][i].hist( + random_scores, + bins=20, + range=(lower, upper), + density=True, + alpha=0.7, + color=colors[i], + edgecolor="black", + ) + axes[1][i].set_title(f"{measure} - Random network") + axes[1][i].set_xlabel(f"{measure} score") + axes[0][i].set_ylabel("PDF") + +plt.tight_layout() +plt.show() + +# %% +# We have compared the pairwise similarities using the NMI, VI, and RI measures +# between partitonings obtained for the karate club network (strong community +# structure) and a comparable random graph (which lacks communities). +# +# The Normalized Mutual Information (NMI) and Rand Index (RI) both quantify +# similarity, and take values from :math:`[0,1]`. Higher values indicate more +# similar partitionings, with a value of 1 attained when the partitionings are +# identical. +# +# The Variation of Information (VI) is a distance measure. It takes values from +# :math:`[0,\infty]`, with lower values indicating higher similarities. Identical +# partitionings have a distance of zero. +# +# For the karate club network, NMI and RI value are concentrated near 1, while +# VI is concentrated near 0, suggesting a robust community structure. In contrast +# the values obtained for the random network are much more spread out, showing +# inconsistent partitionings due to the lack of a clear community structure. + +# %% +# .. [1] W. Zachary: "An Information Flow Model for Conflict and Fission in Small Groups". Journal of Anthropological Research 33, no. 4 (1977): 452–73. https://www.jstor.org/stable/3629752 diff --git a/doc/examples_sphinx-gallery/topological_sort.py b/doc/examples_sphinx-gallery/topological_sort.py index 7b0a05481..3c558f90b 100644 --- a/doc/examples_sphinx-gallery/topological_sort.py +++ b/doc/examples_sphinx-gallery/topological_sort.py @@ -7,6 +7,7 @@ This example demonstrates how to get a topological sorting on a directed acyclic graph (DAG). A topological sorting of a directed graph is a linear ordering based on the precedence implied by the directed edges. It exists iff the graph doesn't have any cycle. In ``igraph``, we can use :meth:`igraph.GraphBase.topological_sorting` to get a topological ordering of the vertices. """ + import igraph as ig import matplotlib.pyplot as plt @@ -26,21 +27,21 @@ # A topological sorting can be computed quite easily by calling # :meth:`igraph.GraphBase.topological_sorting`, which returns a list of vertex IDs. # If the given graph is not DAG, the error will occur. -results = g.topological_sorting(mode='out') -print('Topological sort of g (out):', *results) +results = g.topological_sorting(mode="out") +print("Topological sort of g (out):", *results) # %% # In fact, there are two modes of :meth:`igraph.GraphBase.topological_sorting`, # ``'out'`` ``'in'``. ``'out'`` is the default and starts from a node with # indegree equal to 0. Vice versa, ``'in'`` starts from a node with outdegree # equal to 0. To call the other mode, we can simply use: -results = g.topological_sorting(mode='in') -print('Topological sort of g (in):', *results) +results = g.topological_sorting(mode="in") +print("Topological sort of g (in):", *results) # %% # We can use :meth:`igraph.Vertex.indegree` to find the indegree of the node. for i in range(g.vcount()): - print('degree of {}: {}'.format(i, g.vs[i].indegree())) + print("degree of {}: {}".format(i, g.vs[i].indegree())) # % # Finally, we can plot the graph to make the situation a little clearer. @@ -51,7 +52,7 @@ ig.plot( g, target=ax, - layout='kk', + layout="kk", vertex_size=25, edge_width=4, vertex_label=range(g.vcount()), diff --git a/doc/examples_sphinx-gallery/visual_style.py b/doc/examples_sphinx-gallery/visual_style.py index 4ac7d05da..30c27d73b 100644 --- a/doc/examples_sphinx-gallery/visual_style.py +++ b/doc/examples_sphinx-gallery/visual_style.py @@ -6,6 +6,7 @@ This example shows how to change the visual style of network plots. """ + import igraph as ig import matplotlib.pyplot as plt import random @@ -17,7 +18,7 @@ "edge_width": 0.3, "vertex_size": 15, "palette": "heat", - "layout": "fruchterman_reingold" + "layout": "fruchterman_reingold", } # %% diff --git a/doc/examples_sphinx-gallery/visualize_cliques.py b/doc/examples_sphinx-gallery/visualize_cliques.py index 15f149360..c1ebd49d6 100644 --- a/doc/examples_sphinx-gallery/visualize_cliques.py +++ b/doc/examples_sphinx-gallery/visualize_cliques.py @@ -8,12 +8,13 @@ This example shows how to compute and visualize cliques of a graph using :meth:`igraph.GraphBase.cliques`. """ + import igraph as ig import matplotlib.pyplot as plt # %% # First, let's create a graph, for instance the famous karate club graph: -g = ig.Graph.Famous('Zachary') +g = ig.Graph.Famous("Zachary") # %% # Computing cliques can be done as follows: @@ -27,12 +28,13 @@ for clique, ax in zip(cliques, axs): ig.plot( ig.VertexCover(g, [clique]), - mark_groups=True, palette=ig.RainbowPalette(), + mark_groups=True, + palette=ig.RainbowPalette(), vertex_size=5, edge_width=0.5, target=ax, ) -plt.axis('off') +plt.axis("off") plt.show() @@ -45,16 +47,16 @@ axs = axs.ravel() for clique, ax in zip(cliques, axs): # Color vertices yellow/red based on whether they are in this clique - g.vs['color'] = 'yellow' - g.vs[clique]['color'] = 'red' + g.vs["color"] = "yellow" + g.vs[clique]["color"] = "red" # Color edges black/red based on whether they are in this clique clique_edges = g.es.select(_within=clique) - g.es['color'] = 'black' - clique_edges['color'] = 'red' + g.es["color"] = "black" + clique_edges["color"] = "red" # also increase thickness of clique edges - g.es['width'] = 0.3 - clique_edges['width'] = 1 + g.es["width"] = 0.3 + clique_edges["width"] = 1 ig.plot( ig.VertexCover(g, [clique]), @@ -63,5 +65,5 @@ vertex_size=5, target=ax, ) -plt.axis('off') +plt.axis("off") plt.show() diff --git a/doc/examples_sphinx-gallery/visualize_communities.py b/doc/examples_sphinx-gallery/visualize_communities.py index a13edbfa9..9ab696e8e 100644 --- a/doc/examples_sphinx-gallery/visualize_communities.py +++ b/doc/examples_sphinx-gallery/visualize_communities.py @@ -7,6 +7,7 @@ This example shows how to visualize communities or clusters of a graph. """ + import igraph as ig import matplotlib.pyplot as plt @@ -47,7 +48,8 @@ legend_handles = [] for i in range(num_communities): handle = ax.scatter( - [], [], + [], + [], s=100, facecolor=palette.get(i), edgecolor="k", @@ -56,7 +58,7 @@ legend_handles.append(handle) ax.legend( handles=legend_handles, - title='Community:', + title="Community:", bbox_to_anchor=(0, 1.0), bbox_transform=ax.transAxes, ) diff --git a/doc/source/_static/custom.css b/doc/source/_static/custom.css new file mode 100644 index 000000000..7c1a52022 --- /dev/null +++ b/doc/source/_static/custom.css @@ -0,0 +1,10 @@ +/* override table width restrictions */ +.wy-table-responsive table td, .wy-table-responsive table th { + white-space: normal; +} + +.wy-table-responsive { + margin-bottom: 24px; + max-width: 100%; + overflow: visible; +} diff --git a/doc/source/_static/other.css b/doc/source/_static/other.css deleted file mode 100644 index 54aa675fd..000000000 --- a/doc/source/_static/other.css +++ /dev/null @@ -1,19 +0,0 @@ -.highlight { - border-radius: 8px; -} - -.highlight pre { - padding: 8px; -} - -.navigation-header { - text-align: center; -} - -.bs-docs-section h1 { - display: none; -} - -.bs-docs-section h1.real { - display: block; -} diff --git a/doc/source/analysis.rst b/doc/source/analysis.rst index 943f719cc..bb911efad 100644 --- a/doc/source/analysis.rst +++ b/doc/source/analysis.rst @@ -63,7 +63,7 @@ To get the vertices at the two ends of an edge, use :attr:`Edge.source` and :att >>> v1, v2 = e.source, e.target Vice versa, to get the edge if from the source and target vertices, you can use :meth:`Graph.get_eid` or, for multiple pairs of source/targets, -:meth:`Graph.get_eids`. The boolean version, asking whether two vertices are directly connected, is :meth:`Graph.are_connected`. +:meth:`Graph.get_eids`. The boolean version, asking whether two vertices are directly connected, is :meth:`Graph.are_adjacent`. To get the edges incident on a vertex, you can use :meth:`Vertex.incident`, :meth:`Vertex.out_edges` and :meth:`Vertex.in_edges`. The three are equivalent on undirected graphs but not directed ones of course:: diff --git a/doc/source/conf.py b/doc/source/conf.py index 17a25c204..2dcdfa5fe 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -136,7 +136,8 @@ def get_igraph_version(): # Inspired by pydoctor's RTD page itself # https://github.com/twisted/pydoctor/blob/master/docs/source/conf.py html_theme = "sphinx_rtd_theme" -html_static_path = [] +html_static_path = ["_static"] +html_css_files = ["custom.css"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -147,7 +148,7 @@ def get_igraph_version(): # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = "_static/logo-black.svg" +# html_logo = "_static/logo-black.svg" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 @@ -186,6 +187,14 @@ def get_igraph_version(): # Output file base name for HTML help builder. htmlhelp_basename = "igraphdoc" +# Integration with Read the Docs since RTD is not manipulating the Sphinx +# config files on its own any more. +# This is according to: +# https://about.readthedocs.com/blog/2024/07/addons-by-default/ +html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "") +html_context = {} +if os.environ.get("READTHEDOCS", "") == "True": + html_context["READTHEDOCS"] = True # -- Options for pydoctor ------------------------------------------------------ diff --git a/doc/source/icon.png b/doc/source/icon.png new file mode 100644 index 000000000..0735f005b Binary files /dev/null and b/doc/source/icon.png differ diff --git a/doc/source/icon@2x.png b/doc/source/icon@2x.png new file mode 100644 index 000000000..a9260d97b Binary files /dev/null and b/doc/source/icon@2x.png differ diff --git a/doc/source/install.rst b/doc/source/install.rst index 5f8fc07ea..65c985bb9 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -50,7 +50,7 @@ Package managers on Linux and other systems ------------------------------------------- |igraph|'s Python interface and its dependencies are included in several package management systems, including those of the most popular Linux distributions (Arch Linux, -Debian and Ubuntu, Fedora, GNU Guix, etc.) as well as some cross-platform systems like +Debian and Ubuntu, Fedora, etc.) as well as some cross-platform systems like NixPkgs or MacPorts. .. note:: |igraph| is updated quite often: if you need a more recent version than your @@ -131,7 +131,7 @@ Fourth, call ``pip`` to compile and install the package from source:: $ pip install . Alternatively, you can call ``build`` or another PEP 517-compliant build frontend -to build an installable Python wheel. Here we use `pipx `_ +to build an installable Python wheel. Here we use `pipx `_ to invoke ``build`` in a separate virtualenv:: $ pipx run build @@ -140,7 +140,7 @@ Testing your installation ------------------------- Use ``tox`` or another standard test runner tool to run all the unit tests. -Here we use `pipx `_` to invoke ``tox``:: +Here we use `pipx `_ to invoke ``tox``:: $ pipx run tox @@ -174,13 +174,6 @@ you need to install Cairo headers using your package manager (Linux) or `homebre $ pip install pycairo -The Cairo project does not provide pre-compiled binaries for Windows, but Christoph Gohlke -maintains a site containing unofficial Windows binaries for several Python extension packages, -including Cairo. Therefore, the easiest way to install Cairo on Windows along with its Python bindings -is simply to download it from `Christoph's site `_. -Make sure you use an installer that is suitable for your Windows platform (32-bit or 64-bit) and the -version of Python you are using. - To check if Cairo is installed correctly on your system, run the following example:: >>> import igraph as ig diff --git a/doc/source/requirements.txt b/doc/source/requirements.txt index 869964a7d..ef825d09f 100644 --- a/doc/source/requirements.txt +++ b/doc/source/requirements.txt @@ -2,9 +2,11 @@ pip wheel requests>=2.28.1 +sphinx==7.4.7 sphinx-gallery>=0.14.0 sphinx-rtd-theme>=1.3.0 pydoctor>=23.4.0 +iplotx>=0.6.8 numpy scipy diff --git a/doc/source/tutorial.es.rst b/doc/source/tutorial.es.rst index 9ca6f88d5..b5758c413 100644 --- a/doc/source/tutorial.es.rst +++ b/doc/source/tutorial.es.rst @@ -543,7 +543,7 @@ Hmm, esto no es demasiado bonito hasta ahora. Una adición trivial sería usar l >>> color_dict = {"m": "blue", "f": "pink"} >>> g.vs["color"] = [color_dict[gender] for gender in g.vs["gender"]] >>> ig.plot(g, layout=layout, bbox=(300, 300), margin=20) # Cairo backend - >>> ig.plot(g, layout=layout, bbox=(300, 300), margin=20, target=ax) # matplotlib backend + >>> ig.plot(g, layout=layout, target=ax) # matplotlib backend Tenga en cuenta que aquí simplemente estamos reutilizando el objeto de diseño anterior, pero también hemos especificado que necesitamos un gráfico más pequeño (300 x 300 píxeles) y un margen mayor alrededor del grafo para que quepan las etiquetas (20 píxeles). El resultado es: @@ -701,7 +701,7 @@ Especificación de colores en los gráficos ***Nombres de colores X11*** -Consulta la `lista de nombres de colores X11 `_ en Wikipedia para ver la lista completa. Los nombres de los colores no distinguen entre mayúsculas y minúsculas en |igraph|, por lo que "DarkBLue" puede escribirse también como "darkblue". +Consulta la `lista de nombres de colores X11 `_ en Wikipedia para ver la lista completa. Los nombres de los colores no distinguen entre mayúsculas y minúsculas en |igraph|, por lo que ``"DarkBLue"`` puede escribirse también como ``"darkblue"``. ***Especificación del color en la sintaxis CSS*** diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 49d113594..7ba2de653 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -8,7 +8,7 @@ Tutorial ======== -This page is a detailed tutorial of |igraph|'s Python capabilities. To get an quick impression of what |igraph| can do, check out the :doc:`tutorials/quickstart`. If you have not installed |igraph| yet, follow the :doc:`install`. +This page is a detailed tutorial of |igraph|'s Python capabilities. To get an quick impression of what |igraph| can do, check out the :doc:`tutorials/quickstart`. If you have not installed |igraph| yet, follow the section titled :doc:`install`. .. note:: For the impatient reader, see the :doc:`tutorials/index` page for short, self-contained examples. @@ -16,7 +16,7 @@ This page is a detailed tutorial of |igraph|'s Python capabilities. To get an qu Starting |igraph| ================= -The most common way to use |igraph| is as a named import within a Python environment (e.g. a bare Python shell, a `IPython`_ shell, a `Jupyter`_ notebook or JupyterLab instance, `Google Colab `_, or an `IDE `_):: +The most common way to use |igraph| is as a named import within a Python environment (e.g. a bare Python shell, an `IPython`_ shell, a `Jupyter`_ notebook or JupyterLab instance, `Google Colab `_, or an `IDE `_):: $ python Python 3.9.6 (default, Jun 29 2021, 05:25:02) @@ -79,10 +79,9 @@ This means: **U** ndirected graph with **10** vertices and **2** edges, with the .. note:: - :meth:`Graph.summary` is similar to ``print`` but does not list the edges, which is - convenient for large graphs with millions of edges:: + |igraph| also has a :func:`igraph.summary()` function, which is similar to ``print()`` but it does not list the edges. This is convenient for large graphs with millions of edges:: - >>> summary(g) + >>> ig.summary(g) IGRAPH U--- 10 2 -- @@ -206,7 +205,7 @@ answer can quickly be given by checking the degree distributions of the two grap Setting and retrieving attributes ================================= -As mentioned above, in |igraph| each vertex and each edge have a numeric id from ``0`` upwards. Deleting vertices or edges can therefore cause reassignments of vertex and/or edge IDs. In addition to IDs, vertex and edges can have *attributes* such as a name, coordinates for plotting, metadata, and weights. The graph itself can have such attributes too (e.g. a name, which will show in ``print`` or :meth:`Graph.summary`). In a sense, every :class:`Graph`, vertex and edge can be used as a Python dictionary to store and retrieve these attributes. +As mentioned above, vertices and edges of a graph in |igraph| have numeric IDs from ``0`` upwards. Deleting vertices or edges can therefore cause reassignments of vertex and/or edge IDs. In addition to IDs, vertices and edges can have *attributes* such as a name, coordinates for plotting, metadata, and weights. The graph itself can have such attributes too (e.g. a name, which will show in ``print`` or :meth:`Graph.summary`). In a sense, every :class:`Graph`, vertex and edge can be used as a Python dictionary to store and retrieve these attributes. To demonstrate the use of attributes, let us create a simple social network:: @@ -604,6 +603,8 @@ Method name Short name Algorithm description ``layout_circle`` ``circle``, Deterministic layout that places the ``circular`` vertices on a circle ------------------------------------ --------------- --------------------------------------------- +``layout_davidson_harel`` ``dh`` Davidson-Harel simulated annealing algorithm +------------------------------------ --------------- --------------------------------------------- ``layout_drl`` ``drl`` The `Distributed Recursive Layout`_ algorithm for large graphs ------------------------------------ --------------- --------------------------------------------- @@ -612,6 +613,10 @@ Method name Short name Algorithm description ``layout_fruchterman_reingold_3d`` ``fr3d``, Fruchterman-Reingold force-directed algorithm ``fr_3d`` in three dimensions ------------------------------------ --------------- --------------------------------------------- +``layout_graphopt`` ``graphopt`` The GraphOpt algorithm for large graphs +------------------------------------ --------------- --------------------------------------------- +``layout_grid`` ``grid`` Regular grid layout +------------------------------------ --------------- --------------------------------------------- ``layout_kamada_kawai`` ``kk`` Kamada-Kawai force-directed algorithm ------------------------------------ --------------- --------------------------------------------- ``layout_kamada_kawai_3d`` ``kk3d``, Kamada-Kawai force-directed algorithm @@ -621,6 +626,8 @@ Method name Short name Algorithm description ``lgl``, large graphs ``large_graph`` ------------------------------------ --------------- --------------------------------------------- +``layout_mds`` ``mds`` Multidimensional scaling layout +------------------------------------ --------------- --------------------------------------------- ``layout_random`` ``random`` Places the vertices completely randomly ------------------------------------ --------------- --------------------------------------------- ``layout_random_3d`` ``random_3d`` Places the vertices completely randomly in 3D @@ -630,7 +637,7 @@ Method name Short name Algorithm description ------------------------------------ --------------- --------------------------------------------- ``layout_reingold_tilford_circular`` ``rt_circular`` Reingold-Tilford tree layout with a polar coordinate post-transformation, useful for - ``tree`` (almost) tree-like graphs + (almost) tree-like graphs ------------------------------------ --------------- --------------------------------------------- ``layout_sphere`` ``sphere``, Deterministic layout that places the vertices ``spherical``, evenly on the surface of a sphere @@ -652,9 +659,9 @@ are passed intact to the chosen layout method. For instance, the following two c completely equivalent:: >>> layout = g.layout_reingold_tilford(root=[2]) - >>> layout = g.layout("rt", [2]) + >>> layout = g.layout("rt", root=[2]) -Layout methods return a :class:`~layout.Layout` object which behaves mostly like a list of lists. +Layout methods return a :class:`~layout.Layout` object, which behaves mostly like a list of lists. Each list entry in a :class:`~layout.Layout` object corresponds to a vertex in the original graph and contains the vertex coordinates in the 2D or 3D space. :class:`~layout.Layout` objects also contain some useful methods to translate, scale or rotate the coordinates in a batch. @@ -705,11 +712,12 @@ from the ``label`` attribute by default and vertex colors are determined by the >>> color_dict = {"m": "blue", "f": "pink"} >>> g.vs["color"] = [color_dict[gender] for gender in g.vs["gender"]] >>> ig.plot(g, layout=layout, bbox=(300, 300), margin=20) # Cairo backend - >>> ig.plot(g, layout=layout, bbox=(300, 300), margin=20, target=ax) # matplotlib backend + >>> ig.plot(g, layout=layout, target=ax) # matplotlib backend -Note that we are simply re-using the previous layout object here, but we also specified -that we need a smaller plot (300 x 300 pixels) and a larger margin around the graph -to fit the labels (20 pixels). The result is: +Note that we are simply re-using the previous layout object here, but for the Cairo backend +we also specified that we need a smaller plot (300 x 300 pixels) and a larger margin around +the graph to fit the labels (20 pixels). These settings would be ignored for the Matplotlib +backend. The result is: .. figure:: figures/tutorial_social_network_2.png :alt: The visual representation of our social network - with names and genders @@ -792,8 +800,9 @@ Attribute name Keyword argument Purpose drawn first. --------------- ---------------------- ------------------------------------------ ``shape`` ``vertex_shape`` Shape of the vertex. Known shapes are: - ``rectangle``, ``circle``, ``hidden``, - ``triangle-up``, ``triangle-down``. + ``rectangle``, ``circle``, ``diamond``, + ``hidden``, ``triangle-up``, + ``triangle-down``. Several aliases are also accepted, see :data:`drawing.known_shapes`. --------------- ---------------------- ------------------------------------------ @@ -868,7 +877,8 @@ Keyword argument Purpose ---------------- ---------------------------------------------------------------- ``bbox`` The bounding box of the plot. This must be a tuple containing the desired width and height of the plot. The default plot is - 600 pixels wide and 600 pixels high. + 600 pixels wide and 600 pixels high. Ignored for the + Matplotlib backend. ---------------- ---------------------------------------------------------------- ``layout`` The layout to be used. It can be an instance of :class:`~layout.Layout`, @@ -880,7 +890,7 @@ Keyword argument Purpose ``margin`` The top, right, bottom and left margins of the plot in pixels. This argument must be a list or tuple and its elements will be re-used if you specify a list or tuple with less than four - elements. + elements. Ignored for the Matplotlib backend. ================ ================================================================ Specifying colors in plots @@ -892,9 +902,9 @@ color (e.g., edge, vertex or label colors in the respective attributes): X11 color names See the `list of X11 color names `_ in Wikipedia for the complete list. Alternatively you can see the - keys of the igraph.drawing.colors.known_colors dictionary. Color - names are case insensitive in igraph so "DarkBlue" can be written as - "darkblue" as well. + keys of the ``igraph.drawing.colors.known_colors`` dictionary. Color + names are case insensitive in igraph so ``"DarkBlue"`` can be written as + ``"darkblue"`` as well. Color specification in CSS syntax This is a string according to one of the following formats (where *R*, *G* and @@ -910,6 +920,23 @@ Color specification in CSS syntax Lists or tuples of RGB values in the range 0-1 Example: ``(1.0, 0.5, 0)`` or ``[1.0, 0.5, 0]``. +Note that when specifying the same color for all vertices or edges, you can use +a string as-is but not the tuple or list syntax as tuples or lists would be +interpreted as if the *items* in the tuple are for individual vertices or +edges. So, this would work:: + + >>> ig.plot(g, vertex_color="green") + +But this would not, as it would treat the items in the tuple as palette indices +for the first, second and third vertoces:: + + >>> ig.plot(g, vertex_color=(1, 0, 0)) + +In this latter case, you need to expand the color specification for each vertex +explicitly:: + + >>> ig.plot(g, vertex_color=[(1, 0, 0)] * g.vcount()) + Saving plots ^^^^^^^^^^^^ diff --git a/doc/source/visualisation.rst b/doc/source/visualisation.rst index 1963b85e4..cf32dbd90 100644 --- a/doc/source/visualisation.rst +++ b/doc/source/visualisation.rst @@ -177,7 +177,7 @@ Plotting graphs in Jupyter notebooks |igraph| supports inline plots within a `Jupyter`_ notebook via both the `Cairo`_ and `matplotlib`_ backend. If you are calling `igraph.plot` from a notebook cell without a `matplotlib`_ axes, the image will be shown inline in the corresponding output cell. Use the `bbox` argument to scale the image while preserving the size of the vertices, text, and other artists. -For instance, to get a compact plot:: +For instance, to get a compact plot (using the Cairo backend only):: >>> ig.plot(g, bbox=(0, 0, 100, 100)) @@ -234,6 +234,6 @@ See the :ref:`tutorial ` for examples and a full list .. _matplotlib: https://matplotlib.org .. _Jupyter: https://jupyter.org/ .. _Cairo: https://www.cairographics.org -.. _graphviz: http://www.graphviz.org +.. _graphviz: https://www.graphviz.org .. _networkx: https://networkx.org/ .. _graph-tool: https://graph-tool.skewed.de/ diff --git a/pyproject.toml b/pyproject.toml index 0d5932b7b..db932f633 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,17 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = [ + # pin setuptools: + # https://github.com/airspeed-velocity/asv/pull/1426#issuecomment-2290658198 + # Most likely cause: + # https://github.com/pypa/distutils/issues/283 + # Workaround based on this commit: + # https://github.com/harfbuzz/uharfbuzz/commit/9b607bd06fb17fcb4abe3eab5c4f342ad08309d7 + "setuptools>=64,<72.2.0; platform_python_implementation == 'PyPy'", + "setuptools>=64; platform_python_implementation != 'PyPy'", + "cmake>=3.18" +] build-backend = "setuptools.build_meta" [tool.ruff] -ignore = ["B905", "C901", "E402", "E501"] -line-length = 80 -select = ["B", "C", "E", "F", "W"] +lint.ignore = ["B905", "C901", "E402", "E501"] +lint.select = ["B", "C", "E", "F", "W"] diff --git a/scripts/fix_pyodide_build.py b/scripts/fix_pyodide_build.py index 79d451969..9e239f973 100644 --- a/scripts/fix_pyodide_build.py +++ b/scripts/fix_pyodide_build.py @@ -4,10 +4,12 @@ from pathlib import Path from urllib.request import urlretrieve -target_dir = Path(pyodide_build.__file__).parent / "tools" / "cmake" / "Modules" / "Platform" +target_dir = ( + Path(pyodide_build.__file__).parent / "tools" / "cmake" / "Modules" / "Platform" +) target_dir.mkdir(exist_ok=True, parents=True) target_file = target_dir / "Emscripten.cmake" if not target_file.is_file(): - url = "https://raw.githubusercontent.com/pyodide/pyodide/main/pyodide-build/pyodide_build/tools/cmake/Modules/Platform/Emscripten.cmake" + url = "https://raw.githubusercontent.com/pyodide/pyodide-build/main/pyodide_build/tools/cmake/Modules/Platform/Emscripten.cmake" urlretrieve(url, str(target_file)) diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index 3a65cc264..9309987f6 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -15,29 +15,33 @@ DOC2DASH=0 LINKCHECK=0 CLEAN=0 -while getopts ":scjdl" OPTION; do +while getopts ":cdl" OPTION; do case $OPTION in - c) - CLEAN=1 - ;; - d) - DOC2DASH=1 - ;; - l) - LINKCHECK=1 - ;; - \?) - echo "Usage: $0 [-sjd]" - exit 1 - ;; - esac + c) + CLEAN=1 + ;; + d) + DOC2DASH=1 + ;; + l) + LINKCHECK=1 + ;; + \?) + echo "Usage: $0 [-cdl]" + echo "" + echo "-c: clean and force a full rebuild of the documentation" + echo "-d: generate Dash docset with doc2dash" + echo "-l: check the generated documentation for broken links" + exit 1 + ;; + esac done -shift $((OPTIND -1)) +shift $((OPTIND - 1)) -SCRIPTS_FOLDER=`dirname $0` +SCRIPTS_FOLDER=$(dirname $0) cd ${SCRIPTS_FOLDER}/.. -ROOT_FOLDER=`pwd` +ROOT_FOLDER=$(pwd) DOC_SOURCE_FOLDER=${ROOT_FOLDER}/doc/source DOC_HTML_FOLDER=${ROOT_FOLDER}/doc/html DOC_LINKCHECK_FOLDER=${ROOT_FOLDER}/doc/linkcheck @@ -47,21 +51,27 @@ cd ${ROOT_FOLDER} # Create a virtual environment if [ ! -d ".venv" ]; then - python3 -m venv .venv + echo "Creating virtualenv..." + ${PYTHON:-python3} -m venv .venv - # Install sphinx, matplotlib, pandas, scipy, wheel and pydoctor into the venv. + # Install documentation dependencies into the venv. # doc2dash is optional; it will be installed when -d is given - .venv/bin/pip install -U pip wheel sphinx matplotlib pandas scipy pydoctor sphinx-rtd-theme + .venv/bin/pip install -q -U pip wheel sphinx==7.4.7 matplotlib pandas scipy pydoctor sphinx-rtd-theme iplotx +else + # Upgrade pip in the virtualenv + echo "Upgrading pip in virtualenv..." + .venv/bin/pip install -q -U pip wheel fi -# Make sure that Sphinx, PyDoctor (and maybe doc2dash) are up-to-date in the virtualenv -.venv/bin/pip install -U sphinx pydoctor sphinx-gallery sphinxcontrib-jquery sphinx-rtd-theme +# Make sure that documentation dependencies are up-to-date in the virtualenv +echo "Making sure that all dependencies are up-to-date..." +.venv/bin/pip install -q -U sphinx==7.4.7 pydoctor sphinx-gallery sphinxcontrib-jquery sphinx-rtd-theme iplotx if [ x$DOC2DASH = x1 ]; then - .venv/bin/pip install -U doc2dash + .venv/bin/pip install -U doc2dash fi echo "Removing existing igraph and python-igraph eggs from virtualenv..." -SITE_PACKAGES_DIR=`.venv/bin/python3 -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])'` +SITE_PACKAGES_DIR=$(.venv/bin/python3 -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') rm -rf "${SITE_PACKAGES_DIR}"/igraph*.egg rm -rf "${SITE_PACKAGES_DIR}"/igraph*.egg-link rm -rf "${SITE_PACKAGES_DIR}"/python_igraph*.egg @@ -84,47 +94,47 @@ fi if [ "x$LINKCHECK" = "x1" ]; then echo "Check for broken links" .venv/bin/python -m sphinx \ - -T \ - -b linkcheck \ - -Dtemplates_path='' \ - -Dhtml_theme='alabaster' \ - ${DOC_SOURCE_FOLDER} ${DOC_LINKCHECK_FOLDER} + -T \ + -b linkcheck \ + -Dtemplates_path='' \ + -Dhtml_theme='alabaster' \ + ${DOC_SOURCE_FOLDER} ${DOC_LINKCHECK_FOLDER} fi - echo "Generating HTML documentation..." -.venv/bin/pip install -U sphinx-rtd-theme +.venv/bin/pip install -q -U sphinx-rtd-theme .venv/bin/python -m sphinx -T -b html ${DOC_SOURCE_FOLDER} ${DOC_HTML_FOLDER} echo "HTML documentation generated in ${DOC_HTML_FOLDER}" - # doc2dash if [ "x$DOC2DASH" = "x1" ]; then - PWD=`pwd` + PWD=$(pwd) # Output folder of sphinx (before Jekyll if requested) DOC_API_FOLDER=${ROOT_FOLDER}/doc/html/api - DOC2DASH=`which doc2dash 2>/dev/null || true` + DOC2DASH=.venv/bin/doc2dash DASH_FOLDER=${ROOT_FOLDER}/doc/dash if [ "x$DOC2DASH" != x ]; then - echo "Generating Dash docset..." - "$DOC2DASH" \ - --online-redirect-url "https://igraph.org/python/api/latest" \ - --name "python-igraph" \ - -d "${DASH_FOLDER}" \ - -f \ - -j \ - -I "index.html" \ - "${DOC_API_FOLDER}" - DASH_READY=1 + echo "Generating Dash docset..." + "$DOC2DASH" \ + --online-redirect-url "https://python.igraph.org/en/latest/api/" \ + --name "python-igraph" \ + -d "${DASH_FOLDER}" \ + -f \ + -j \ + -I "index.html" \ + --icon ${ROOT_FOLDER}/doc/source/icon.png \ + --icon-2x ${ROOT_FOLDER}/doc/source/icon@2x.png \ + "${DOC_API_FOLDER}" + DASH_READY=1 else - echo "WARNING: doc2dash not installed, skipping Dash docset generation." - DASH_READY=0 + echo "WARNING: doc2dash not installed, skipping Dash docset generation." + DASH_READY=0 fi echo "" if [ "x${DASH_READY}" = x1 ]; then - echo "Dash docset generated in ${DASH_FOLDER}/python-igraph.docset" + echo "Dash docset generated in ${DASH_FOLDER}/python-igraph.docset" fi cd "$PWD" diff --git a/scripts/patch_modularized_graph_methods.py b/scripts/patch_modularized_graph_methods.py index 9e188b6b5..081ab73a5 100644 --- a/scripts/patch_modularized_graph_methods.py +++ b/scripts/patch_modularized_graph_methods.py @@ -13,7 +13,6 @@ def main(): - # Get instance and classmethods g = igraph.Graph() methods = inspect.getmembers(g, predicate=inspect.ismethod) @@ -43,7 +42,7 @@ def main(): newmodule = igraph.__file__ + ".new" with open(newmodule, "wt") as fout: # FIXME: whitelisting all cases is not great, try to improve - for (origin, value) in auxiliary_imports: + for origin, value in auxiliary_imports: fout.write(f"from {origin} import {value}\n") with open(igraph.__file__, "rt") as f: diff --git a/scripts/rtd_prebuild.sh b/scripts/rtd_prebuild.sh index 9b41bab13..461cf2cc4 100755 --- a/scripts/rtd_prebuild.sh +++ b/scripts/rtd_prebuild.sh @@ -12,4 +12,4 @@ echo "Modularize pure Python modules" echo "NOTE: Patch pydoctor to trigger build-finished before RTD extension" # see https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx.application.Sphinx.connect # see also https://github.com/readthedocs/readthedocs.org/pull/4054 - might or might not be exactly what we are seeing here -sed -i 's/on_build_finished)/on_build_finished, priority=490)/' /home/docs/checkouts/readthedocs.org/user_builds/igraph/envs/${READTHEDOCS_VERSION}/lib/python3.9/site-packages/pydoctor/sphinx_ext/build_apidocs.py +sed -i 's/on_build_finished)/on_build_finished, priority=490)/' /home/docs/checkouts/readthedocs.org/user_builds/igraph/envs/${READTHEDOCS_VERSION}/lib/python3.11/site-packages/pydoctor/sphinx_ext/build_apidocs.py diff --git a/setup.py b/setup.py index 11d4cd967..c919d3507 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,8 @@ ########################################################################### # Check Python's version info and exit early if it is too old -if sys.version_info < (3, 8): - print("This module requires Python >= 3.8") +if sys.version_info < (3, 9): + print("This module requires Python >= 3.9") sys.exit(0) ########################################################################### @@ -16,7 +16,7 @@ from setuptools import find_packages, setup, Command, Extension try: - from wheel.bdist_wheel import bdist_wheel + from setuptools.command.bdist_wheel import bdist_wheel except ImportError: bdist_wheel = None @@ -280,9 +280,12 @@ def _compile_in( args.append("-DIGRAPH_GRAPHML_SUPPORT:BOOL=OFF") # Build the Python interface with vendored libraries - for deps in "ARPACK BLAS GLPK GMP LAPACK".split(): + for deps in "ARPACK BLAS GLPK GMP LAPACK PLFIT".split(): args.append("-DIGRAPH_USE_INTERNAL_" + deps + "=ON") + # Use link-time optinization if available + args.append("-DIGRAPH_ENABLE_LTO=AUTO") + # -fPIC is needed on Linux so we can link to a static igraph lib from a # Python shared library args.append("-DCMAKE_POSITION_INDEPENDENT_CODE=ON") @@ -913,8 +916,6 @@ def get_tag(self): bdist_wheel_abi3 = None # We are going to build an abi3 wheel if we are at least on CPython 3.9. -# This is because the C code contains conditionals for CPython 3.8 so we cannot -# use an abi3 wheel built with CPython 3.8 on CPython 3.9 should_build_abi3_wheel = ( bdist_wheel_abi3 and platform.python_implementation() == "CPython" @@ -952,7 +953,7 @@ def get_tag(self): Graph plotting functionality is provided by the Cairo library, so make sure you install the Python bindings of Cairo if you want to generate publication-quality graph plots. You can try either `pycairo -`_ or `cairocffi `_, +`_ or `cairocffi `_, ``cairocffi`` is recommended because there were bug reports affecting igraph graph plots in Jupyter notebooks when using ``pycairo`` (but not with ``cairocffi``). @@ -982,7 +983,7 @@ def get_tag(self): "Bug Tracker": "https://github.com/igraph/python-igraph/issues", "Changelog": "https://github.com/igraph/python-igraph/blob/main/CHANGELOG.md", "CI": "https://github.com/igraph/python-igraph/actions", - "Documentation": "https://igraph.readthedocs.io", + "Documentation": "https://python.igraph.org", "Source Code": "https://github.com/igraph/python-igraph", }, "ext_modules": [igraph_extension], @@ -999,9 +1000,7 @@ def get_tag(self): # Dependencies needed for plotting with Cairo "cairo": ["cairocffi>=1.2.0"], # Dependencies needed for plotting with Matplotlib - "matplotlib": [ - "matplotlib>=3.6.0; platform_python_implementation != 'PyPy'" - ], + "matplotlib": ["matplotlib>=3.6.0; platform_python_implementation != 'PyPy'"], # Dependencies needed for plotting with Plotly "plotly": ["plotly>=5.3.0"], # Compatibility alias to 'cairo' for python-igraph <= 0.9.6 @@ -1019,6 +1018,15 @@ def get_tag(self): "plotly>=5.3.0", "Pillow>=9; platform_python_implementation != 'PyPy'", ], + # Dependencies needed for testing on Windows ARM64; only those that are either + # pure Python or have Windows ARM64 wheels as we don't want to compile wheels + # in CI + "test-win-arm64": [ + "cairocffi>=1.2.0", + "networkx>=2.5", + "pytest>=7.0.1", + "pytest-timeout>=2.1.0", + ], # Dependencies needed for testing on musllinux; only those that are either # pure Python or have musllinux wheels as we don't want to compile wheels # in CI @@ -1036,7 +1044,7 @@ def get_tag(self): "pydoctor>=23.4.0", ], }, - "python_requires": ">=3.8", + "python_requires": ">=3.9", "headers": headers, "platforms": "ALL", "keywords": [ @@ -1054,11 +1062,11 @@ def get_tag(self): "Operating System :: OS Independent", "Programming Language :: C", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Information Analysis", diff --git a/src/_igraph/arpackobject.c b/src/_igraph/arpackobject.c index 86de32363..69d8841ab 100644 --- a/src/_igraph/arpackobject.c +++ b/src/_igraph/arpackobject.c @@ -115,7 +115,7 @@ static PyObject* igraphmodule_ARPACKOptions_getattr( static int igraphmodule_ARPACKOptions_setattr( igraphmodule_ARPACKOptionsObject* self, char* attrname, PyObject* value) { - igraph_integer_t igraph_int; + igraph_int_t igraph_int; if (value == 0) { PyErr_SetString(PyExc_TypeError, "attribute can not be deleted"); diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 7079fa19f..1bc39a0eb 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -183,7 +183,7 @@ int igraphmodule_PyObject_matches_attribute_record(PyObject* object, igraph_attr return 0; } -int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_integer_t* vid) { +int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_int_t* vid) { igraphmodule_i_attribute_struct* attrs = ATTR_STRUCT(graph); PyObject* o_vid = NULL; @@ -347,9 +347,11 @@ PyObject* igraphmodule_create_or_get_edge_attribute_values(const igraph_t* graph /* Attribute handlers for the Python interface */ /* Initialization */ -static igraph_error_t igraphmodule_i_attribute_init(igraph_t *graph, igraph_vector_ptr_t *attr) { +static igraph_error_t igraphmodule_i_attribute_init( + igraph_t *graph, const igraph_attribute_record_list_t *attr +) { igraphmodule_i_attribute_struct* attrs; - igraph_integer_t i, n; + igraph_int_t i, n; attrs = (igraphmodule_i_attribute_struct*)calloc(1, sizeof(igraphmodule_i_attribute_struct)); if (!attrs) { @@ -368,27 +370,26 @@ static igraph_error_t igraphmodule_i_attribute_init(igraph_t *graph, igraph_vect PyObject *dict = attrs->attrs[0], *value; const char *s; - n = igraph_vector_ptr_size(attr); + n = igraph_attribute_record_list_size(attr); for (i = 0; i < n; i++) { - igraph_attribute_record_t *attr_rec; - attr_rec = VECTOR(*attr)[i]; + igraph_attribute_record_t *attr_rec = igraph_attribute_record_list_get_ptr(attr, i); switch (attr_rec->type) { case IGRAPH_ATTRIBUTE_NUMERIC: - value = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[0]); + value = PyFloat_FromDouble((double)VECTOR(*attr_rec->value.as_vector)[0]); if (!value) { PyErr_PrintEx(0); } break; case IGRAPH_ATTRIBUTE_STRING: - s = igraph_strvector_get((igraph_strvector_t*)attr_rec->value, 0); + s = igraph_strvector_get(attr_rec->value.as_strvector, 0); value = PyUnicode_FromString(s ? s : ""); if (!value) { PyErr_PrintEx(0); } break; case IGRAPH_ATTRIBUTE_BOOLEAN: - value = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[0] ? Py_True : Py_False; + value = VECTOR(*attr_rec->value.as_vector_bool)[0] ? Py_True : Py_False; Py_INCREF(value); break; default: @@ -515,10 +516,12 @@ static igraph_error_t igraphmodule_i_attribute_copy(igraph_t *to, const igraph_t } /* Adding vertices */ -static igraph_error_t igraphmodule_i_attribute_add_vertices(igraph_t *graph, igraph_integer_t nv, igraph_vector_ptr_t *attr) { +static igraph_error_t igraphmodule_i_attribute_add_vertices( + igraph_t *graph, igraph_int_t nv, const igraph_attribute_record_list_t *attr +) { /* Extend the end of every value in the vertex hash with nv pieces of None */ PyObject *key, *value, *dict; - igraph_integer_t i, j, k, num_attr_entries; + igraph_int_t i, j, k, num_attr_entries; igraph_attribute_record_t *attr_rec; igraph_vector_bool_t added_attrs; Py_ssize_t pos = 0; @@ -531,7 +534,7 @@ static igraph_error_t igraphmodule_i_attribute_add_vertices(igraph_t *graph, igr return IGRAPH_SUCCESS; } - num_attr_entries = attr ? igraph_vector_ptr_size(attr) : 0; + num_attr_entries = attr ? igraph_attribute_record_list_size(attr) : 0; IGRAPH_VECTOR_BOOL_INIT_FINALLY(&added_attrs, num_attr_entries); dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; @@ -547,7 +550,7 @@ static igraph_error_t igraphmodule_i_attribute_add_vertices(igraph_t *graph, igr /* Check if we have specific values for the given attribute */ attr_rec = NULL; for (i = 0; i < num_attr_entries; i++) { - attr_rec = VECTOR(*attr)[i]; + attr_rec = igraph_attribute_record_list_get_ptr(attr, i); if (igraphmodule_PyObject_matches_attribute_record(key, attr_rec)) { VECTOR(added_attrs)[i] = 1; break; @@ -564,14 +567,14 @@ static igraph_error_t igraphmodule_i_attribute_add_vertices(igraph_t *graph, igr switch (attr_rec->type) { case IGRAPH_ATTRIBUTE_NUMERIC: - o = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); + o = PyFloat_FromDouble((double)VECTOR(*attr_rec->value.as_vector)[i]); break; case IGRAPH_ATTRIBUTE_STRING: - s = igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i); + s = igraph_strvector_get(attr_rec->value.as_strvector, i); o = PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: - o = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; + o = VECTOR(*attr_rec->value.as_vector_bool)[i] ? Py_True : Py_False; Py_INCREF(o); break; default: @@ -620,7 +623,7 @@ static igraph_error_t igraphmodule_i_attribute_add_vertices(igraph_t *graph, igr continue; } - attr_rec = (igraph_attribute_record_t*)VECTOR(*attr)[k]; + attr_rec = igraph_attribute_record_list_get_ptr(attr, k); value = PyList_New(j + nv); if (!value) { @@ -637,14 +640,14 @@ static igraph_error_t igraphmodule_i_attribute_add_vertices(igraph_t *graph, igr PyObject *o = NULL; switch (attr_rec->type) { case IGRAPH_ATTRIBUTE_NUMERIC: - o = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); + o = PyFloat_FromDouble((double)VECTOR(*attr_rec->value.as_vector)[i]); break; case IGRAPH_ATTRIBUTE_STRING: - s = igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i); + s = igraph_strvector_get(attr_rec->value.as_strvector, i); o = PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: - o = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; + o = VECTOR(*attr_rec->value.as_vector_bool)[i] ? Py_True : Py_False; Py_INCREF(o); break; default: @@ -687,7 +690,7 @@ static igraph_error_t igraphmodule_i_attribute_add_vertices(igraph_t *graph, igr /* Permuting vertices */ static igraph_error_t igraphmodule_i_attribute_permute_vertices(const igraph_t *graph, igraph_t *newgraph, const igraph_vector_int_t *idx) { - igraph_integer_t i, n; + igraph_int_t i, n; PyObject *key, *value, *dict, *newdict, *newlist, *o; Py_ssize_t pos = 0; @@ -739,11 +742,13 @@ static igraph_error_t igraphmodule_i_attribute_permute_vertices(const igraph_t * } /* Adding edges */ -static igraph_error_t igraphmodule_i_attribute_add_edges(igraph_t *graph, const igraph_vector_int_t *edges, igraph_vector_ptr_t *attr) { +static igraph_error_t igraphmodule_i_attribute_add_edges( + igraph_t *graph, const igraph_vector_int_t *edges, const igraph_attribute_record_list_t* attr +) { /* Extend the end of every value in the edge hash with ne pieces of None */ PyObject *key, *value, *dict; Py_ssize_t pos = 0; - igraph_integer_t i, j, k, ne, num_attr_entries; + igraph_int_t i, j, k, ne, num_attr_entries; igraph_vector_bool_t added_attrs; igraph_attribute_record_t *attr_rec; @@ -756,7 +761,7 @@ static igraph_error_t igraphmodule_i_attribute_add_edges(igraph_t *graph, const return IGRAPH_SUCCESS; } - num_attr_entries = attr ? igraph_vector_ptr_size(attr) : 0; + num_attr_entries = attr ? igraph_attribute_record_list_size(attr) : 0; IGRAPH_VECTOR_BOOL_INIT_FINALLY(&added_attrs, num_attr_entries); dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; @@ -772,7 +777,7 @@ static igraph_error_t igraphmodule_i_attribute_add_edges(igraph_t *graph, const /* Check if we have specific values for the given attribute */ attr_rec = NULL; for (i = 0; i < num_attr_entries; i++) { - attr_rec = VECTOR(*attr)[i]; + attr_rec = igraph_attribute_record_list_get_ptr(attr, i); if (igraphmodule_PyObject_matches_attribute_record(key, attr_rec)) { VECTOR(added_attrs)[i] = 1; break; @@ -788,14 +793,14 @@ static igraph_error_t igraphmodule_i_attribute_add_edges(igraph_t *graph, const PyObject *o; switch (attr_rec->type) { case IGRAPH_ATTRIBUTE_NUMERIC: - o = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); + o = PyFloat_FromDouble((double)VECTOR(*attr_rec->value.as_vector)[i]); break; case IGRAPH_ATTRIBUTE_STRING: - s = igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i); + s = igraph_strvector_get(attr_rec->value.as_strvector, i); o = PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: - o = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; + o = VECTOR(*attr_rec->value.as_vector_bool)[i] ? Py_True : Py_False; Py_INCREF(o); break; default: @@ -837,7 +842,7 @@ static igraph_error_t igraphmodule_i_attribute_add_edges(igraph_t *graph, const if (VECTOR(added_attrs)[k]) { continue; } - attr_rec=(igraph_attribute_record_t*)VECTOR(*attr)[k]; + attr_rec = igraph_attribute_record_list_get_ptr(attr, k); value = PyList_New(j + ne); if (!value) { @@ -854,14 +859,14 @@ static igraph_error_t igraphmodule_i_attribute_add_edges(igraph_t *graph, const PyObject *o; switch (attr_rec->type) { case IGRAPH_ATTRIBUTE_NUMERIC: - o = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); + o = PyFloat_FromDouble((double)VECTOR(*attr_rec->value.as_vector)[i]); break; case IGRAPH_ATTRIBUTE_STRING: - s = igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i); + s = igraph_strvector_get(attr_rec->value.as_strvector, i); o = PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: - o = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; + o = VECTOR(*attr_rec->value.as_vector_bool)[i] ? Py_True : Py_False; Py_INCREF(o); break; default: @@ -899,7 +904,7 @@ static igraph_error_t igraphmodule_i_attribute_add_edges(igraph_t *graph, const /* Permuting edges */ static igraph_error_t igraphmodule_i_attribute_permute_edges(const igraph_t *graph, igraph_t *newgraph, const igraph_vector_int_t *idx) { - igraph_integer_t i, n; + igraph_int_t i, n; PyObject *key, *value, *dict, *newdict, *newlist, *o; Py_ssize_t pos=0; @@ -956,14 +961,14 @@ static igraph_error_t igraphmodule_i_attribute_permute_edges(const igraph_t *gra */ static PyObject* igraphmodule_i_ac_func(PyObject* values, const igraph_vector_int_list_t *merges, PyObject* func) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *list, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); - igraph_integer_t j, n = igraph_vector_int_size(v); + igraph_int_t j, n = igraph_vector_int_size(v); list = PyList_New(n); for (j = 0; j < n; j++) { @@ -1039,14 +1044,14 @@ static PyObject* igraphmodule_i_ac_builtin_func(PyObject* values, */ static PyObject* igraphmodule_i_ac_sum(PyObject* values, const igraph_vector_int_list_t *merges) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); igraph_real_t num = 0.0, sum = 0.0; - igraph_integer_t j, n = igraph_vector_int_size(v); + igraph_int_t j, n = igraph_vector_int_size(v); for (j = 0; j < n; j++) { item = PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -1082,14 +1087,14 @@ static PyObject* igraphmodule_i_ac_sum(PyObject* values, */ static PyObject* igraphmodule_i_ac_prod(PyObject* values, const igraph_vector_int_list_t *merges) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); igraph_real_t num = 1.0, prod = 1.0; - igraph_integer_t j, n = igraph_vector_int_size(v); + igraph_int_t j, n = igraph_vector_int_size(v); for (j = 0; j < n; j++) { item = PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -1126,13 +1131,13 @@ static PyObject* igraphmodule_i_ac_prod(PyObject* values, */ static PyObject* igraphmodule_i_ac_first(PyObject* values, const igraph_vector_int_list_t *merges) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); - igraph_integer_t n = igraph_vector_int_size(v); + igraph_int_t n = igraph_vector_int_size(v); item = n > 0 ? PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[0]) : Py_None; if (item == 0) { @@ -1159,7 +1164,7 @@ static PyObject* igraphmodule_i_ac_first(PyObject* values, */ static PyObject* igraphmodule_i_ac_random(PyObject* values, const igraph_vector_int_list_t *merges) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item, *num; PyObject *random_module = PyImport_ImportModule("random"); PyObject *random_func; @@ -1176,7 +1181,7 @@ static PyObject* igraphmodule_i_ac_random(PyObject* values, res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); - igraph_integer_t n = igraph_vector_int_size(v); + igraph_int_t n = igraph_vector_int_size(v); if (n > 0) { num = PyObject_CallObject(random_func, 0); @@ -1187,7 +1192,7 @@ static PyObject* igraphmodule_i_ac_random(PyObject* values, } item = PyList_GetItem( - values, VECTOR(*v)[(igraph_integer_t)(n * PyFloat_AsDouble(num))] + values, VECTOR(*v)[(igraph_int_t)(n * PyFloat_AsDouble(num))] ); if (item == 0) { Py_DECREF(random_func); @@ -1222,13 +1227,13 @@ static PyObject* igraphmodule_i_ac_random(PyObject* values, */ static PyObject* igraphmodule_i_ac_last(PyObject* values, const igraph_vector_int_list_t *merges) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); - igraph_integer_t n = igraph_vector_int_size(v); + igraph_int_t n = igraph_vector_int_size(v); item = (n > 0) ? PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[n-1]) : Py_None; if (item == 0) { @@ -1256,14 +1261,14 @@ static PyObject* igraphmodule_i_ac_last(PyObject* values, */ static PyObject* igraphmodule_i_ac_mean(PyObject* values, const igraph_vector_int_list_t *merges) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); igraph_real_t num = 0.0, mean = 0.0; - igraph_integer_t j, n = igraph_vector_int_size(v); + igraph_int_t j, n = igraph_vector_int_size(v); for (j = 0; j < n; ) { item = PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -1302,13 +1307,13 @@ static PyObject* igraphmodule_i_ac_mean(PyObject* values, */ static PyObject* igraphmodule_i_ac_median(PyObject* values, const igraph_vector_int_list_t *merges) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *list, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); - igraph_integer_t j, n = igraph_vector_int_size(v); + igraph_int_t j, n = igraph_vector_int_size(v); list = PyList_New(n); for (j = 0; j < n; j++) { item = PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -1683,12 +1688,13 @@ igraph_error_t igraphmodule_i_attribute_get_type(const igraph_t *graph, int is_numeric, is_string, is_boolean; Py_ssize_t i, j; PyObject *o, *dict; + const char *attr_type_name; switch (elemtype) { - case IGRAPH_ATTRIBUTE_GRAPH: attrnum = ATTRHASH_IDX_GRAPH; break; - case IGRAPH_ATTRIBUTE_VERTEX: attrnum = ATTRHASH_IDX_VERTEX; break; - case IGRAPH_ATTRIBUTE_EDGE: attrnum = ATTRHASH_IDX_EDGE; break; - default: IGRAPH_ERROR("No such attribute type", IGRAPH_EINVAL); break; + case IGRAPH_ATTRIBUTE_GRAPH: attrnum = ATTRHASH_IDX_GRAPH; attr_type_name = "graph"; break; + case IGRAPH_ATTRIBUTE_VERTEX: attrnum = ATTRHASH_IDX_VERTEX; attr_type_name = "vertex"; break; + case IGRAPH_ATTRIBUTE_EDGE: attrnum = ATTRHASH_IDX_EDGE; attr_type_name = "edge"; break; + default: IGRAPH_ERROR("No such attribute type.", IGRAPH_EINVAL); break; } /* Get the attribute dict */ @@ -1697,12 +1703,12 @@ igraph_error_t igraphmodule_i_attribute_get_type(const igraph_t *graph, /* Check whether the attribute exists */ o = PyDict_GetItemString(dict, name); if (o == 0) { - IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + IGRAPH_ERRORF("No %s attribute named \"%s\" exists.", IGRAPH_EINVAL, attr_type_name, name); } /* Basic type check */ if (attrnum != ATTRHASH_IDX_GRAPH && !PyList_Check(o)) { - IGRAPH_ERROR("attribute hash type mismatch", IGRAPH_EINVAL); + IGRAPH_ERROR("Attribute hash type mismatch.", IGRAPH_EINVAL); } j = PyList_Size(o); @@ -1765,36 +1771,38 @@ igraph_error_t igraphmodule_i_get_boolean_graph_attr(const igraph_t *graph, attribute handler calls... hopefully :) Same applies for the other handlers. */ o = PyDict_GetItemString(dict, name); if (!o) { - IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + IGRAPH_ERRORF("No boolean graph attribute named \"%s\" exists.", IGRAPH_EINVAL, name); } - IGRAPH_CHECK(igraph_vector_bool_resize(value, 1)); - VECTOR(*value)[0] = PyObject_IsTrue(o); - return IGRAPH_SUCCESS; + return igraph_vector_bool_push_back(value, PyObject_IsTrue(o)); } /* Getting numeric graph attributes */ igraph_error_t igraphmodule_i_get_numeric_graph_attr(const igraph_t *graph, const char *name, igraph_vector_t *value) { PyObject *dict, *o, *result; + double num; + dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_GRAPH]; /* No error checking, if we get here, the type has already been checked by previous attribute handler calls... hopefully :) Same applies for the other handlers. */ o = PyDict_GetItemString(dict, name); if (!o) { - IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + IGRAPH_ERRORF("No numeric graph attribute named \"%s\" exists.", IGRAPH_EINVAL, name); } - IGRAPH_CHECK(igraph_vector_resize(value, 1)); + if (o == Py_None) { - VECTOR(*value)[0] = IGRAPH_NAN; - return IGRAPH_SUCCESS; + num = IGRAPH_NAN; + } else { + result = PyNumber_Float(o); + if (result) { + num = PyFloat_AsDouble(o); + Py_DECREF(result); + } else { + IGRAPH_ERROR("Internal error in PyNumber_Float", IGRAPH_EINVAL); + } } - result = PyNumber_Float(o); - if (result) { - VECTOR(*value)[0] = PyFloat_AsDouble(o); - Py_DECREF(result); - } else IGRAPH_ERROR("Internal error in PyFloat_AsDouble", IGRAPH_EINVAL); - return IGRAPH_SUCCESS; + return igraph_vector_push_back(value, num); } /* Getting string graph attributes */ @@ -1806,9 +1814,8 @@ igraph_error_t igraphmodule_i_get_string_graph_attr(const igraph_t *graph, dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_GRAPH]; o = PyDict_GetItemString(dict, name); if (!o) { - IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + IGRAPH_ERRORF("No string graph attribute named \"%s\" exists.", IGRAPH_EINVAL, name); } - IGRAPH_CHECK(igraph_strvector_resize(value, 1)); /* For Python 3.x, we simply call PyObject_Str, which produces a * Unicode string, then encode it into UTF-8, except when we @@ -1836,7 +1843,7 @@ igraph_error_t igraphmodule_i_get_string_graph_attr(const igraph_t *graph, IGRAPH_ERROR("Internal error in PyBytes_AsString", IGRAPH_EINVAL); } - IGRAPH_CHECK(igraph_strvector_set(value, 0, c_str)); + IGRAPH_CHECK(igraph_strvector_push_back(value, c_str)); Py_XDECREF(str); @@ -1853,19 +1860,24 @@ igraph_error_t igraphmodule_i_get_numeric_vertex_attr(const igraph_t *graph, dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; list = PyDict_GetItemString(dict, name); - if (!list) IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + if (!list) { + IGRAPH_ERRORF("No numeric vertex attribute named \"%s\" exists.", IGRAPH_EINVAL, name); + } if (igraph_vs_is_all(&vs)) { - if (igraphmodule_PyObject_float_to_vector_t(list, &newvalue)) + if (igraphmodule_PyObject_float_to_vector_t(list, &newvalue)) { IGRAPH_ERROR("Internal error", IGRAPH_EINVAL); - igraph_vector_update(value, &newvalue); + } + IGRAPH_FINALLY(igraph_vector_destroy, &newvalue); + IGRAPH_CHECK(igraph_vector_append(value, &newvalue)); + IGRAPH_FINALLY_CLEAN(1); igraph_vector_destroy(&newvalue); } else { igraph_vit_t it; - igraph_integer_t i = 0; + igraph_int_t i = igraph_vector_size(value); IGRAPH_CHECK(igraph_vit_create(graph, vs, &it)); IGRAPH_FINALLY(igraph_vit_destroy, &it); - IGRAPH_CHECK(igraph_vector_resize(value, IGRAPH_VIT_SIZE(it))); + IGRAPH_CHECK(igraph_vector_resize(value, i + IGRAPH_VIT_SIZE(it))); while (!IGRAPH_VIT_END(it)) { o = PyList_GetItem(list, (Py_ssize_t)IGRAPH_VIT_GET(it)); if (o != Py_None) { @@ -1895,22 +1907,26 @@ igraph_error_t igraphmodule_i_get_string_vertex_attr(const igraph_t *graph, dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; list = PyDict_GetItemString(dict, name); - if (!list) - IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + if (!list) { + IGRAPH_ERRORF("No string vertex attribute named \"%s\" exists.", IGRAPH_EINVAL, name); + } if (igraph_vs_is_all(&vs)) { - if (igraphmodule_PyList_to_strvector_t(list, &newvalue)) + if (igraphmodule_PyList_to_strvector_t(list, &newvalue)) { IGRAPH_ERROR("Internal error", IGRAPH_EINVAL); - igraph_strvector_destroy(value); - *value=newvalue; + } + IGRAPH_FINALLY(igraph_strvector_destroy, &newvalue); + IGRAPH_CHECK(igraph_strvector_append(value, &newvalue)); + IGRAPH_FINALLY_CLEAN(1); + igraph_strvector_destroy(&newvalue); } else { igraph_vit_t it; - igraph_integer_t i = 0; + igraph_int_t i = igraph_strvector_size(value); IGRAPH_CHECK(igraph_vit_create(graph, vs, &it)); IGRAPH_FINALLY(igraph_vit_destroy, &it); - IGRAPH_CHECK(igraph_strvector_resize(value, IGRAPH_VIT_SIZE(it))); + IGRAPH_CHECK(igraph_strvector_resize(value, i + IGRAPH_VIT_SIZE(it))); while (!IGRAPH_VIT_END(it)) { - igraph_integer_t v = IGRAPH_VIT_GET(it); + igraph_int_t v = IGRAPH_VIT_GET(it); char* str; result = PyList_GetItem(list, v); @@ -1918,16 +1934,19 @@ igraph_error_t igraphmodule_i_get_string_vertex_attr(const igraph_t *graph, IGRAPH_ERROR("null element in PyList", IGRAPH_EINVAL); str = igraphmodule_PyObject_ConvertToCString(result); - if (str == 0) + if (str == 0) { IGRAPH_ERROR("error while calling igraphmodule_PyObject_ConvertToCString", IGRAPH_EINVAL); + } + IGRAPH_FINALLY(free, str); /* Note: this is a bit inefficient here, igraphmodule_PyObject_ConvertToCString * allocates a new string which could be copied into the string * vector straight away. Instead of that, the string vector makes * another copy. Probably the performance hit is not too severe. */ - igraph_strvector_set(value, i, str); + IGRAPH_CHECK(igraph_strvector_set(value, i, str)); free(str); + IGRAPH_FINALLY_CLEAN(1); IGRAPH_VIT_NEXT(it); i++; @@ -1949,19 +1968,24 @@ igraph_error_t igraphmodule_i_get_boolean_vertex_attr(const igraph_t *graph, dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; list = PyDict_GetItemString(dict, name); - if (!list) IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + if (!list) { + IGRAPH_ERRORF("No boolean vertex attribute named \"%s\" exists.", IGRAPH_EINVAL, name); + } if (igraph_vs_is_all(&vs)) { - if (igraphmodule_PyObject_to_vector_bool_t(list, &newvalue)) + if (igraphmodule_PyObject_to_vector_bool_t(list, &newvalue)) { IGRAPH_ERROR("Internal error", IGRAPH_EINVAL); - igraph_vector_bool_update(value, &newvalue); + } + IGRAPH_FINALLY(igraph_vector_bool_destroy, &newvalue); + IGRAPH_CHECK(igraph_vector_bool_append(value, &newvalue)); + IGRAPH_FINALLY_CLEAN(1); igraph_vector_bool_destroy(&newvalue); } else { igraph_vit_t it; - igraph_integer_t i = 0; + igraph_int_t i = igraph_vector_bool_size(value); IGRAPH_CHECK(igraph_vit_create(graph, vs, &it)); IGRAPH_FINALLY(igraph_vit_destroy, &it); - IGRAPH_CHECK(igraph_vector_bool_resize(value, IGRAPH_VIT_SIZE(it))); + IGRAPH_CHECK(igraph_vector_bool_resize(value, i + IGRAPH_VIT_SIZE(it))); while (!IGRAPH_VIT_END(it)) { o = PyList_GetItem(list, (Py_ssize_t)IGRAPH_VIT_GET(it)); VECTOR(*value)[i] = PyObject_IsTrue(o); @@ -1985,26 +2009,37 @@ igraph_error_t igraphmodule_i_get_numeric_edge_attr(const igraph_t *graph, dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; list = PyDict_GetItemString(dict, name); - if (!list) IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + if (!list) { + IGRAPH_ERRORF("No numeric edge attribute named \"%s\" exists.", IGRAPH_EINVAL, name); + } if (igraph_es_is_all(&es)) { - if (igraphmodule_PyObject_float_to_vector_t(list, &newvalue)) + if (igraphmodule_PyObject_float_to_vector_t(list, &newvalue)) { IGRAPH_ERROR("Internal error", IGRAPH_EINVAL); - igraph_vector_update(value, &newvalue); + } + IGRAPH_FINALLY(igraph_vector_destroy, &newvalue); + IGRAPH_CHECK(igraph_vector_append(value, &newvalue)); + IGRAPH_FINALLY_CLEAN(1); igraph_vector_destroy(&newvalue); } else { igraph_eit_t it; - igraph_integer_t i = 0; + igraph_int_t i = igraph_vector_size(value); IGRAPH_CHECK(igraph_eit_create(graph, es, &it)); IGRAPH_FINALLY(igraph_eit_destroy, &it); - IGRAPH_CHECK(igraph_vector_resize(value, IGRAPH_EIT_SIZE(it))); + IGRAPH_CHECK(igraph_vector_resize(value, i + IGRAPH_EIT_SIZE(it))); while (!IGRAPH_EIT_END(it)) { o = PyList_GetItem(list, (Py_ssize_t)IGRAPH_EIT_GET(it)); if (o != Py_None) { result = PyNumber_Float(o); - VECTOR(*value)[i] = PyFloat_AsDouble(result); - Py_XDECREF(result); - } else VECTOR(*value)[i] = IGRAPH_NAN; + if (result) { + VECTOR(*value)[i] = PyFloat_AsDouble(result); + Py_XDECREF(result); + } else { + IGRAPH_ERROR("Internal error in PyNumber_Float", IGRAPH_EINVAL); + } + } else { + VECTOR(*value)[i] = IGRAPH_NAN; + } IGRAPH_EIT_NEXT(it); i++; } @@ -2025,16 +2060,21 @@ igraph_error_t igraphmodule_i_get_string_edge_attr(const igraph_t *graph, dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; list = PyDict_GetItemString(dict, name); - if (!list) IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + if (!list) { + IGRAPH_ERRORF("No string edge attribute named \"%s\" exists.", IGRAPH_EINVAL, name); + } if (igraph_es_is_all(&es)) { - if (igraphmodule_PyList_to_strvector_t(list, &newvalue)) + if (igraphmodule_PyList_to_strvector_t(list, &newvalue)) { IGRAPH_ERROR("Internal error", IGRAPH_EINVAL); - igraph_strvector_destroy(value); - *value=newvalue; + } + IGRAPH_FINALLY(igraph_strvector_destroy, &newvalue); + IGRAPH_CHECK(igraph_strvector_append(value, &newvalue)); + IGRAPH_FINALLY_CLEAN(1); + igraph_strvector_destroy(&newvalue); } else { igraph_eit_t it; - igraph_integer_t i = 0; + igraph_int_t i = igraph_strvector_size(value); IGRAPH_CHECK(igraph_eit_create(graph, es, &it)); IGRAPH_FINALLY(igraph_eit_destroy, &it); IGRAPH_CHECK(igraph_strvector_resize(value, IGRAPH_EIT_SIZE(it))); @@ -2054,7 +2094,9 @@ igraph_error_t igraphmodule_i_get_string_edge_attr(const igraph_t *graph, * vector straight away. Instead of that, the string vector makes * another copy. Probably the performance hit is not too severe. */ - igraph_strvector_set(value, i, str); + IGRAPH_FINALLY(free, str); + IGRAPH_CHECK(igraph_strvector_set(value, i, str)); + IGRAPH_FINALLY_CLEAN(1); free(str); IGRAPH_EIT_NEXT(it); @@ -2077,19 +2119,24 @@ igraph_error_t igraphmodule_i_get_boolean_edge_attr(const igraph_t *graph, dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; list = PyDict_GetItemString(dict, name); - if (!list) IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + if (!list) { + IGRAPH_ERRORF("No boolean edge attribute named \"%s\" exists.", IGRAPH_EINVAL, name); + } if (igraph_es_is_all(&es)) { - if (igraphmodule_PyObject_to_vector_bool_t(list, &newvalue)) + if (igraphmodule_PyObject_to_vector_bool_t(list, &newvalue)) { IGRAPH_ERROR("Internal error", IGRAPH_EINVAL); - igraph_vector_bool_update(value, &newvalue); + } + IGRAPH_FINALLY(igraph_vector_bool_destroy, &newvalue); + IGRAPH_CHECK(igraph_vector_bool_append(value, &newvalue)); + IGRAPH_FINALLY_CLEAN(1); igraph_vector_bool_destroy(&newvalue); } else { igraph_eit_t it; - igraph_integer_t i=0; + igraph_int_t i = igraph_vector_bool_size(value); IGRAPH_CHECK(igraph_eit_create(graph, es, &it)); IGRAPH_FINALLY(igraph_eit_destroy, &it); - IGRAPH_CHECK(igraph_vector_bool_resize(value, IGRAPH_EIT_SIZE(it))); + IGRAPH_CHECK(igraph_vector_bool_resize(value, i + IGRAPH_EIT_SIZE(it))); while (!IGRAPH_EIT_END(it)) { o = PyList_GetItem(list, (Py_ssize_t)IGRAPH_EIT_GET(it)); VECTOR(*value)[i] = PyObject_IsTrue(o); diff --git a/src/_igraph/attributes.h b/src/_igraph/attributes.h index 6d3da9c90..d02d55239 100644 --- a/src/_igraph/attributes.h +++ b/src/_igraph/attributes.h @@ -83,7 +83,7 @@ int igraphmodule_attribute_name_check(PyObject* obj); void igraphmodule_initialize_attribute_handler(void); void igraphmodule_index_vertex_names(igraph_t *graph, igraph_bool_t force); void igraphmodule_invalidate_vertex_name_index(igraph_t *graph); -int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_integer_t* id); +int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_int_t* id); PyObject* igraphmodule_create_or_get_edge_attribute_values(const igraph_t* graph, const char* name); diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index cf7cafdfe..c8b814f88 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -44,7 +44,7 @@ PyTypeObject* igraphmodule_BFSIterType; */ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_neimode_t mode, igraph_bool_t advanced) { igraphmodule_BFSIterObject* self; - igraph_integer_t no_of_nodes, r; + igraph_int_t no_of_nodes, r; self = (igraphmodule_BFSIterObject*) PyType_GenericNew(igraphmodule_BFSIterType, 0, 0); if (!self) { @@ -121,10 +121,7 @@ static int igraphmodule_BFSIter_traverse(igraphmodule_BFSIterObject *self, visitproc visit, void *arg) { RC_TRAVERSE("BFSIter", self); Py_VISIT(self->gref); -#if PY_VERSION_HEX >= 0x03090000 - // This was not needed before Python 3.9 (Python issue 35810 and 40217) Py_VISIT(Py_TYPE(self)); -#endif return 0; } @@ -165,19 +162,19 @@ static PyObject* igraphmodule_BFSIter_iter(igraphmodule_BFSIterObject* self) { static PyObject* igraphmodule_BFSIter_iternext(igraphmodule_BFSIterObject* self) { if (!igraph_dqueue_int_empty(&self->queue)) { - igraph_integer_t vid = igraph_dqueue_int_pop(&self->queue); - igraph_integer_t dist = igraph_dqueue_int_pop(&self->queue); - igraph_integer_t parent = igraph_dqueue_int_pop(&self->queue); - igraph_integer_t i, n; + igraph_int_t vid = igraph_dqueue_int_pop(&self->queue); + igraph_int_t dist = igraph_dqueue_int_pop(&self->queue); + igraph_int_t parent = igraph_dqueue_int_pop(&self->queue); + igraph_int_t i, n; - if (igraph_neighbors(self->graph, &self->neis, vid, self->mode)) { + if (igraph_neighbors(self->graph, &self->neis, vid, self->mode, /* loops = */ 0, /* multiple = */ 0)) { igraphmodule_handle_igraph_error(); return NULL; } n = igraph_vector_int_size(&self->neis); for (i = 0; i < n; i++) { - igraph_integer_t neighbor = VECTOR(self->neis)[i]; + igraph_int_t neighbor = VECTOR(self->neis)[i]; if (self->visited[neighbor] == 0) { self->visited[neighbor] = 1; if (igraph_dqueue_int_push(&self->queue, neighbor) || diff --git a/src/_igraph/common.c b/src/_igraph/common.c index bf888adfc..a099ef030 100644 --- a/src/_igraph/common.c +++ b/src/_igraph/common.c @@ -46,27 +46,3 @@ PyObject* igraphmodule_unimplemented(PyObject* self, PyObject* args, PyObject* k PyErr_SetString(PyExc_NotImplementedError, "This method is unimplemented."); return NULL; } - -/** - * \ingroup python_interface - * \brief Resolves a weak reference to an \c igraph.Graph - * \return the \c igraph.Graph object or NULL if the weak reference is dead. - * Sets an exception in the latter case. - */ -PyObject* igraphmodule_resolve_graph_weakref(PyObject* ref) { - PyObject *o; - -#ifndef PYPY_VERSION - /* PyWeakref_Check is not implemented in PyPy yet */ - if (!PyWeakref_Check(ref)) { - PyErr_SetString(PyExc_TypeError, "weak reference expected"); - return NULL; - } -#endif /* PYPY_VERSION */ - o=PyWeakref_GetObject(ref); - if (o == Py_None) { - PyErr_SetString(PyExc_TypeError, "underlying graph has already been destroyed"); - return NULL; - } - return o; -} diff --git a/src/_igraph/common.h b/src/_igraph/common.h index 77514e2fd..fda85c948 100644 --- a/src/_igraph/common.h +++ b/src/_igraph/common.h @@ -51,5 +51,4 @@ #define ATTRIBUTE_TYPE_EDGE 2 PyObject* igraphmodule_unimplemented(PyObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_resolve_graph_weakref(PyObject* ref); #endif diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index c05a4eba2..a36d0558d 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -46,9 +46,13 @@ * throws an exception if necessary. This variant is needed for enum conversions * because we assume that enums fit into an int. * + * Note that Python 3.13 also provides a PyLong_AsInt() function, hence we need + * a different name for this function. The difference is that PyLong_AsInt() + * needs an extra call to PyErr_Occurred() to disambiguate in case of errors. + * * Returns -1 if there was an error, 0 otherwise. */ -int PyLong_AsInt(PyObject* obj, int* result) { +int PyLong_AsInt_OutArg(PyObject* obj, int* result) { long dummy = PyLong_AsLong(obj); if (dummy < INT_MIN) { PyErr_SetString(PyExc_OverflowError, "long integer too small for conversion to C int"); @@ -92,7 +96,7 @@ int igraphmodule_PyObject_to_enum(PyObject *o, return 0; if (PyLong_Check(o)) - return PyLong_AsInt(o, result); + return PyLong_AsInt_OutArg(o, result); s = PyUnicode_CopyAsString(o); if (s == 0) { @@ -174,7 +178,7 @@ int igraphmodule_PyObject_to_enum_strict(PyObject *o, } if (PyLong_Check(o)) { - return PyLong_AsInt(o, result); + return PyLong_AsInt_OutArg(o, result); } s = PyUnicode_CopyAsString(o); @@ -370,7 +374,7 @@ int igraphmodule_PyObject_to_eigen_which_t(PyObject *o, w->pos = IGRAPH_EIGEN_LM; w->howmany = 1; w->il = w->iu = -1; - w->vl = IGRAPH_NEGINFINITY; + w->vl = -IGRAPH_INFINITY; w->vu = IGRAPH_INFINITY; w->vestimate = 0; w->balance = IGRAPH_LAPACK_DGEEVX_BALANCE_NONE; @@ -410,15 +414,15 @@ int igraphmodule_PyObject_to_eigen_which_t(PyObject *o, } w->pos = w_pos_int; } else if (!strcasecmp(kv, "howmany")) { - if (PyLong_AsInt(value, &w->howmany)) { + if (PyLong_AsInt_OutArg(value, &w->howmany)) { return -1; } } else if (!strcasecmp(kv, "il")) { - if (PyLong_AsInt(value, &w->il)) { + if (PyLong_AsInt_OutArg(value, &w->il)) { return -1; } } else if (!strcasecmp(kv, "iu")) { - if (PyLong_AsInt(value, &w->iu)) { + if (PyLong_AsInt_OutArg(value, &w->iu)) { return -1; } } else if (!strcasecmp(kv, "vl")) { @@ -426,7 +430,7 @@ int igraphmodule_PyObject_to_eigen_which_t(PyObject *o, } else if (!strcasecmp(kv, "vu")) { w->vu = PyFloat_AsDouble(value); } else if (!strcasecmp(kv, "vestimate")) { - if (PyLong_AsInt(value, &w->vestimate)) { + if (PyLong_AsInt_OutArg(value, &w->vestimate)) { return -1; } } else if (!strcasecmp(kv, "balance")) { @@ -514,6 +518,20 @@ int igraphmodule_PyObject_to_bliss_sh_t(PyObject *o, TRANSLATE_ENUM_WITH(bliss_sh_tt); } +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_chung_lu_t + */ +int igraphmodule_PyObject_to_chung_lu_t(PyObject *o, igraph_chung_lu_t *result) { + static igraphmodule_enum_translation_table_entry_t chung_lu_tt[] = { + {"original", IGRAPH_CHUNG_LU_ORIGINAL}, + {"maxent", IGRAPH_CHUNG_LU_MAXENT}, + {"nr", IGRAPH_CHUNG_LU_NR}, + {0,0} + }; + TRANSLATE_ENUM_WITH(chung_lu_tt); +} + /** * \ingroup python_interface_conversion * \brief Converts a Python object to an igraph \c igraph_coloring_greedy_t @@ -582,11 +600,26 @@ int igraphmodule_PyObject_to_fas_algorithm_t(PyObject *o, {"exact", IGRAPH_FAS_EXACT_IP}, {"exact_ip", IGRAPH_FAS_EXACT_IP}, {"ip", IGRAPH_FAS_EXACT_IP}, + {"ip_ti", IGRAPH_FAS_EXACT_IP_TI}, + {"ip_cg", IGRAPH_FAS_EXACT_IP_CG}, {0,0} }; TRANSLATE_ENUM_WITH(fas_algorithm_tt); } +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_fvs_algorithm_t + */ +int igraphmodule_PyObject_to_fvs_algorithm_t(PyObject *o, + igraph_fvs_algorithm_t *result) { + static igraphmodule_enum_translation_table_entry_t fvs_algorithm_tt[] = { + {"ip", IGRAPH_FVS_EXACT_IP}, + {0,0} + }; + TRANSLATE_ENUM_WITH(fvs_algorithm_tt); +} + /** * \ingroup python_interface_conversion * \brief Converts a Python object to an igraph \c igraph_get_adjacency_t @@ -677,6 +710,36 @@ int igraphmodule_PyObject_to_loops_t(PyObject *o, igraph_loops_t *result) { TRANSLATE_ENUM_WITH(loops_tt); } +/** + * \brief Converts a Python object to an igraph \c igraph_lpa_variant_t + */ +int igraphmodule_PyObject_to_lpa_variant_t(PyObject *o, igraph_lpa_variant_t *result) { + static igraphmodule_enum_translation_table_entry_t lpa_variant_tt[] = { + {"dominance", IGRAPH_LPA_DOMINANCE}, + {"retention", IGRAPH_LPA_RETENTION}, + {"fast", IGRAPH_LPA_FAST}, + {0,0} + }; + + TRANSLATE_ENUM_WITH(lpa_variant_tt); +} + +/** + * \brief Converts a Python object to an igraph \c igraph_mst_algorithm_t + */ +int igraphmodule_PyObject_to_mst_algorithm_t(PyObject *o, igraph_mst_algorithm_t *result) { + static igraphmodule_enum_translation_table_entry_t mst_algorithm_tt[] = { + {"auto", IGRAPH_MST_AUTOMATIC}, + {"automatic", IGRAPH_MST_AUTOMATIC}, + {"unweighted", IGRAPH_MST_UNWEIGHTED}, + {"prim", IGRAPH_MST_PRIM}, + {"kruskal", IGRAPH_MST_KRUSKAL}, + {0,0} + }; + + TRANSLATE_ENUM_WITH(mst_algorithm_tt); +} + /** * \ingroup python_interface_conversion * \brief Converts a Python object to an igraph \c igraph_random_walk_stuck_t @@ -703,19 +766,6 @@ int igraphmodule_PyObject_to_reciprocity_t(PyObject *o, igraph_reciprocity_t *re TRANSLATE_ENUM_WITH(reciprocity_tt); } -/** - * \brief Converts a Python object to an igraph \c igraph_rewiring_t - */ -int igraphmodule_PyObject_to_rewiring_t(PyObject *o, igraph_rewiring_t *result) { - static igraphmodule_enum_translation_table_entry_t rewiring_tt[] = { - {"simple", IGRAPH_REWIRING_SIMPLE}, - {"simple_loops", IGRAPH_REWIRING_SIMPLE_LOOPS}, - {"loops", IGRAPH_REWIRING_SIMPLE_LOOPS}, - {0,0} - }; - TRANSLATE_ENUM_WITH(rewiring_tt); -} - /** * \brief Converts a Python object to an igraph \c igraphmodule_shortest_path_algorithm_t */ @@ -901,7 +951,7 @@ int igraphmodule_PyObject_to_igraph_t(PyObject *o, igraph_t **result) { } /** - * \brief Converts a PyLong to an igraph \c igraph_integer_t + * \brief Converts a PyLong to an igraph \c igraph_int_t * * Raises suitable Python exceptions when needed. * @@ -912,7 +962,7 @@ int igraphmodule_PyObject_to_igraph_t(PyObject *o, igraph_t **result) { * \param v the result is stored here * \return 0 if everything was OK, 1 otherwise */ -int PyLong_to_integer_t(PyObject* obj, igraph_integer_t* v) { +int PyLong_to_integer_t(PyObject* obj, igraph_int_t* v) { if (IGRAPH_INTEGER_SIZE == 64) { /* here the assumption is that sizeof(long long) == 64 bits; anyhow, this * is the widest integer type that we can convert a PyLong to so we cannot @@ -937,7 +987,7 @@ int PyLong_to_integer_t(PyObject* obj, igraph_integer_t* v) { } /** - * \brief Converts a Python object to an igraph \c igraph_integer_t + * \brief Converts a Python object to an igraph \c igraph_int_t * * Raises suitable Python exceptions when needed. * @@ -945,9 +995,9 @@ int PyLong_to_integer_t(PyObject* obj, igraph_integer_t* v) { * \param v the result is stored here * \return 0 if everything was OK, 1 otherwise */ -int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v) { +int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_int_t *v) { int retval; - igraph_integer_t num; + igraph_int_t num; if (object == NULL) { } else if (PyLong_Check(object)) { @@ -973,6 +1023,60 @@ int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v) { return 1; } +/** + * \brief Converts a Python object to an igraph \c igraph_int_t when it is + * used as a limit on the number of results for some function. + * + * This is different from \ref igraphmodule_PyObject_to_integer_t such that it + * converts None and positive infinity to \c IGRAPH_UNLIMITED, and it does not + * accept negative values. + * + * Raises suitable Python exceptions when needed. + * + * \param object the Python object to be converted + * \param v the result is stored here + * \return 0 if everything was OK, 1 otherwise + */ +int igraphmodule_PyObject_to_max_results_t(PyObject *object, igraph_int_t *v) { + int retval; + igraph_int_t num; + + if (object != NULL) { + if (object == Py_None) { + *v = IGRAPH_UNLIMITED; + return 0; + } + + if (PyNumber_Check(object)) { + PyObject *flt = PyNumber_Float(object); + if (flt == NULL) { + return 1; + } + + if (PyFloat_AsDouble(flt) == IGRAPH_INFINITY) { + Py_DECREF(flt); + *v = IGRAPH_UNLIMITED; + return 0; + } + + Py_DECREF(flt); + } + } + + retval = igraphmodule_PyObject_to_integer_t(object, &num); + if (retval) { + return retval; + } + + if (num < 0) { + PyErr_SetString(PyExc_ValueError, "expected non-negative integer, None or infinity"); + return 1; + } + + *v = num; + return 0; +} + /** * \brief Converts a Python object to an igraph \c igraph_real_t * @@ -1027,7 +1131,7 @@ int igraphmodule_PyObject_to_vector_t(PyObject *list, igraph_vector_t *v, igraph PyObject *item, *it; Py_ssize_t size_hint; int ok; - igraph_integer_t number; + igraph_int_t number; if (PyBaseString_Check(list)) { /* It is highly unlikely that a string (although it is a sequence) will @@ -1226,7 +1330,7 @@ int igraphmodule_PyObject_float_to_vector_t(PyObject *list, igraph_vector_t *v) */ int igraphmodule_PyObject_to_vector_int_t(PyObject *list, igraph_vector_int_t *v) { PyObject *it = 0, *item; - igraph_integer_t value = 0; + igraph_int_t value = 0; Py_ssize_t i, j, k; int ok; @@ -1388,13 +1492,13 @@ int igraphmodule_PyObject_to_vector_bool_t(PyObject *list, /** * \ingroup python_interface_conversion - * \brief Converts an igraph \c igraph_integer_t to a Python integer + * \brief Converts an igraph \c igraph_int_t to a Python integer * - * \param value the \c igraph_integer_t value to be converted + * \param value the \c igraph_int_t value to be converted * \return the Python integer as a \c PyObject*, or \c NULL if an * error occurred */ -PyObject* igraphmodule_integer_t_to_PyObject(igraph_integer_t value) { +PyObject* igraphmodule_integer_t_to_PyObject(igraph_int_t value) { #if IGRAPH_INTEGER_SIZE == 32 /* minimum size of a long is 32 bits so we are okay */ return PyLong_FromLong(value); @@ -1402,7 +1506,7 @@ PyObject* igraphmodule_integer_t_to_PyObject(igraph_integer_t value) { /* minimum size of a long long is 64 bits so we are okay */ return PyLong_FromLongLong(value); #else -# error "Unknown igraph_integer_t size" +# error "Unknown igraph_int_t size" #endif } @@ -1541,10 +1645,10 @@ PyObject* igraphmodule_vector_int_t_to_PyList(const igraph_vector_int_t *v) { * \param v the \c igraph_vector_int_t containing the vector to be converted * \return the Python integer list as a \c PyObject*, or \c NULL if an error occurred */ -PyObject* igraphmodule_vector_int_t_to_PyList_with_nan(const igraph_vector_int_t *v, const igraph_integer_t nanvalue) { +PyObject* igraphmodule_vector_int_t_to_PyList_with_nan(const igraph_vector_int_t *v, const igraph_int_t nanvalue) { PyObject *list, *item; Py_ssize_t n, i; - igraph_integer_t val; + igraph_int_t val; n = igraph_vector_int_size(v); if (n < 0) { @@ -1725,7 +1829,7 @@ int igraphmodule_PyObject_to_edgelist( ) { PyObject *item, *i1, *i2, *it, *expected; int ok; - igraph_integer_t idx1=0, idx2=0; + igraph_int_t idx1=0, idx2=0; if (PyBaseString_Check(list)) { /* It is highly unlikely that a string (although it is a sequence) will @@ -1740,13 +1844,13 @@ int igraphmodule_PyObject_to_edgelist( * detail that we don't want to commit ourselves to */ if (PyMemoryView_Check(list)) { item = PyObject_GetAttrString(list, "itemsize"); - expected = PyLong_FromSize_t(sizeof(igraph_integer_t)); + expected = PyLong_FromSize_t(sizeof(igraph_int_t)); ok = item && PyObject_RichCompareBool(item, expected, Py_EQ); Py_XDECREF(expected); Py_XDECREF(item); if (!ok) { PyErr_SetString( - PyExc_TypeError, "item size of buffer must match the size of igraph_integer_t" + PyExc_TypeError, "item size of buffer must match the size of igraph_int_t" ); return 1; } @@ -1900,7 +2004,7 @@ int igraphmodule_attrib_to_vector_t(PyObject *o, igraphmodule_GraphObject *self, /* Check whether the attribute exists and is numeric */ igraph_attribute_type_t at; igraph_attribute_elemtype_t et; - igraph_integer_t n; + igraph_int_t n; char *name = PyUnicode_CopyAsString(o); if (attr_type == ATTRIBUTE_TYPE_VERTEX) { @@ -1929,7 +2033,19 @@ int igraphmodule_attrib_to_vector_t(PyObject *o, igraphmodule_GraphObject *self, free(name); return 1; } - igraph_vector_init(result, n); + if (igraph_vector_init(result, 0)) { + igraphmodule_handle_igraph_error(); + free(name); + free(result); + return 1; + } + if (igraph_vector_reserve(result, n)) { + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(result); + free(name); + free(result); + return 1; + } if (attr_type == ATTRIBUTE_TYPE_VERTEX) { if (igraphmodule_i_get_numeric_vertex_attr(&self->g, name, igraph_vss_all(), result)) { @@ -2001,7 +2117,7 @@ int igraphmodule_attrib_to_vector_int_t(PyObject *o, igraphmodule_GraphObject *s if (PyUnicode_Check(o)) { igraph_vector_t* dummy = 0; - igraph_integer_t i, n; + igraph_int_t i, n; if (igraphmodule_attrib_to_vector_t(o, self, &dummy, attr_type)) { return 1; @@ -2029,7 +2145,7 @@ int igraphmodule_attrib_to_vector_int_t(PyObject *o, igraphmodule_GraphObject *s } for (i = 0; i < n; i++) { - VECTOR(*result)[i] = (igraph_integer_t) VECTOR(*dummy)[i]; + VECTOR(*result)[i] = (igraph_int_t) VECTOR(*dummy)[i]; } igraph_vector_destroy(dummy); free(dummy); @@ -2094,7 +2210,7 @@ int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject * return 0; if (PyUnicode_Check(o)) { - igraph_integer_t i, n; + igraph_int_t i, n; /* First, check if the attribute is a "real" boolean */ igraph_attribute_type_t at; @@ -2124,7 +2240,19 @@ int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject * free(name); return 1; } - igraph_vector_bool_init(result, n); + if (igraph_vector_bool_init(result, 0)) { + igraphmodule_handle_igraph_error(); + free(name); + free(result); + return 1; + } + if (igraph_vector_bool_reserve(result, n)) { + igraph_vector_bool_destroy(result); + igraphmodule_handle_igraph_error(); + free(name); + free(result); + return 1; + } if (attr_type == ATTRIBUTE_TYPE_VERTEX) { if (igraphmodule_i_get_boolean_vertex_attr(&self->g, name, igraph_vss_all(), result)) { @@ -2160,12 +2288,17 @@ int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject * n = igraph_vector_size(dummy); result = (igraph_vector_bool_t*)calloc(1, sizeof(igraph_vector_bool_t)); - igraph_vector_bool_init(result, n); if (result == 0) { igraph_vector_destroy(dummy); free(dummy); PyErr_NoMemory(); return 1; } + if (igraph_vector_bool_init(result, n)) { + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(dummy); free(dummy); + igraph_vector_bool_destroy(result); free(result); + return 1; + } for (i = 0; i < n; i++) { VECTOR(*result)[i] = (VECTOR(*dummy)[i] != 0 && VECTOR(*dummy)[i] == VECTOR(*dummy)[i]); @@ -2357,6 +2490,40 @@ PyObject* igraphmodule_matrix_int_t_to_PyList(const igraph_matrix_int_t *m) { return list; } +/** + * \ingroup python_interface_conversion + * \brief Converts an igraph \c igraph_matrix_list_t to a Python list of lists of lists + * + * \param v the \c igraph_matrix_list_t containing the matrix list to be converted + * \return the Python list as a \c PyObject*, or \c NULL if an error occurred + */ +PyObject* igraphmodule_matrix_list_t_to_PyList(const igraph_matrix_list_t *m) { + PyObject *list, *item; + Py_ssize_t n, i; + + n = igraph_matrix_list_size(m); + if (n < 0) { + return igraphmodule_handle_igraph_error(); + } + + list = PyList_New(n); + if (!list) { + return NULL; + } + + for (i = 0; i < n; i++) { + item = igraphmodule_matrix_t_to_PyList(igraph_matrix_list_get_ptr(m, i), + IGRAPHMODULE_TYPE_FLOAT); + if (item == NULL) { + Py_DECREF(list); + return NULL; + } + PyList_SetItem(list, i, item); /* will not fail */ + } + + return list; +} + /** * \ingroup python_interface_conversion * \brief Converts an igraph \c igraph_vector_ptr_t to a Python list of lists @@ -2594,10 +2761,10 @@ PyObject* igraphmodule_graph_list_t_to_PyList(igraph_graph_list_t *v, PyTypeObje * applicable. May be used in error messages. * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. */ -int igraphmodule_PyList_to_matrix_t( +int igraphmodule_PyObject_to_matrix_t( PyObject* o, igraph_matrix_t *m, const char *arg_name ) { - return igraphmodule_PyList_to_matrix_t_with_minimum_column_count(o, m, 0, arg_name); + return igraphmodule_PyObject_to_matrix_t_with_minimum_column_count(o, m, 0, arg_name); } /** @@ -2612,7 +2779,7 @@ int igraphmodule_PyList_to_matrix_t( * applicable. May be used in error messages. * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. */ -int igraphmodule_PyList_to_matrix_t_with_minimum_column_count( +int igraphmodule_PyObject_to_matrix_t_with_minimum_column_count( PyObject *o, igraph_matrix_t *m, int min_cols, const char *arg_name ) { Py_ssize_t nr, nc, n, i, j; @@ -2630,6 +2797,10 @@ int igraphmodule_PyList_to_matrix_t_with_minimum_column_count( } nr = PySequence_Size(o); + if (nr < 0) { + return 1; + } + nc = min_cols > 0 ? min_cols : 0; for (i = 0; i < nr; i++) { row = PySequence_GetItem(o, i); @@ -2644,18 +2815,30 @@ int igraphmodule_PyList_to_matrix_t_with_minimum_column_count( } n = PySequence_Size(row); Py_DECREF(row); + if (n < 0) { + return 1; + } if (n > nc) { nc = n; } } - igraph_matrix_init(m, nr, nc); + if (igraph_matrix_init(m, nr, nc)) { + igraphmodule_handle_igraph_error(); + return 1; + } + for (i = 0; i < nr; i++) { row = PySequence_GetItem(o, i); n = PySequence_Size(row); for (j = 0; j < n; j++) { item = PySequence_GetItem(row, j); + if (!item) { + igraph_matrix_destroy(m); + return 1; + } if (igraphmodule_PyObject_to_real_t(item, &value)) { + igraph_matrix_destroy(m); Py_DECREF(item); return 1; } @@ -2678,10 +2861,10 @@ int igraphmodule_PyList_to_matrix_t_with_minimum_column_count( * applicable. May be used in error messages. * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. */ -int igraphmodule_PyList_to_matrix_int_t( +int igraphmodule_PyObject_to_matrix_int_t( PyObject* o, igraph_matrix_int_t *m, const char* arg_name ) { - return igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(o, m, 0, arg_name); + return igraphmodule_PyObject_to_matrix_int_t_with_minimum_column_count(o, m, 0, arg_name); } /** @@ -2696,12 +2879,12 @@ int igraphmodule_PyList_to_matrix_int_t( * applicable. May be used in error messages. * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. */ -int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count( +int igraphmodule_PyObject_to_matrix_int_t_with_minimum_column_count( PyObject *o, igraph_matrix_int_t *m, int min_cols, const char* arg_name ) { Py_ssize_t nr, nc, n, i, j; PyObject *row, *item; - igraph_integer_t value; + igraph_int_t value; /* calculate the matrix dimensions */ if (!PySequence_Check(o) || PyUnicode_Check(o)) { @@ -2714,6 +2897,10 @@ int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count( } nr = PySequence_Size(o); + if (nr < 0) { + return 1; + } + nc = min_cols > 0 ? min_cols : 0; for (i = 0; i < nr; i++) { row = PySequence_GetItem(o, i); @@ -2728,6 +2915,9 @@ int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count( } n = PySequence_Size(row); Py_DECREF(row); + if (n < 0) { + return 1; + } if (n > nc) { nc = n; } @@ -2743,7 +2933,12 @@ int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count( n = PySequence_Size(row); for (j = 0; j < n; j++) { item = PySequence_GetItem(row, j); + if (!item) { + igraph_matrix_int_destroy(m); + return 1; + } if (igraphmodule_PyObject_to_integer_t(item, &value)) { + igraph_matrix_int_destroy(m); Py_DECREF(item); return 1; } @@ -3208,7 +3403,7 @@ int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t_with_type(PyObject *it, * if we don't need name lookups. * \return 0 if everything was OK, 1 otherwise */ -int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *graph) { +int igraphmodule_PyObject_to_vid(PyObject *o, igraph_int_t *vid, igraph_t *graph) { if (o == 0) { PyErr_SetString(PyExc_TypeError, "only non-negative integers, strings or igraph.Vertex objects can be converted to vertex IDs"); return 1; @@ -3267,7 +3462,7 @@ int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *g * if we don't need name lookups. * \return 0 if everything was OK, 1 otherwise */ -int igraphmodule_PyObject_to_optional_vid(PyObject *o, igraph_integer_t *vid, igraph_t *graph) { +int igraphmodule_PyObject_to_optional_vid(PyObject *o, igraph_int_t *vid, igraph_t *graph) { if (o == 0 || o == Py_None) { return 0; } else { @@ -3289,7 +3484,7 @@ int igraphmodule_PyObject_to_optional_vid(PyObject *o, igraph_integer_t *vid, ig int igraphmodule_PyObject_to_vid_list(PyObject* o, igraph_vector_int_t* result, igraph_t* graph) { PyObject *iterator; PyObject *item; - igraph_integer_t vid; + igraph_int_t vid; if (PyBaseString_Check(o)) { /* exclude strings; they are iterable but cannot yield meaningful vertex IDs */ @@ -3353,8 +3548,8 @@ int igraphmodule_PyObject_to_vid_list(PyObject* o, igraph_vector_int_t* result, * \return 0 if everything was OK, 1 otherwise */ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, - igraph_t *graph, igraph_bool_t *return_single, igraph_integer_t *single_vid) { - igraph_integer_t vid; + igraph_t *graph, igraph_bool_t *return_single, igraph_int_t *single_vid) { + igraph_int_t vid; igraph_vector_int_t vector; if (o == 0 || o == Py_None) { @@ -3471,9 +3666,9 @@ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, * if we don't want to handle tuples. * \return 0 if everything was OK, 1 otherwise */ -int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *graph) { +int igraphmodule_PyObject_to_eid(PyObject *o, igraph_int_t *eid, igraph_t *graph) { int retval; - igraph_integer_t vid1, vid2; + igraph_int_t vid1, vid2; if (!o) { PyErr_SetString(PyExc_TypeError, @@ -3575,7 +3770,7 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g */ int igraphmodule_PyObject_to_es_t(PyObject *o, igraph_es_t *es, igraph_t *graph, igraph_bool_t *return_single) { - igraph_integer_t eid; + igraph_int_t eid; igraph_vector_int_t vector; if (o == 0 || o == Py_None) { @@ -3962,6 +4157,21 @@ int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t TRANSLATE_ENUM_WITH(pagerank_algo_tt); } +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_metric_t + */ +int igraphmodule_PyObject_to_metric_t(PyObject *o, igraph_metric_t *result) { + static igraphmodule_enum_translation_table_entry_t metric_tt[] = { + {"euclidean", IGRAPH_METRIC_EUCLIDEAN}, + {"l2", IGRAPH_METRIC_L2}, /* alias to the previous */ + {"manhattan", IGRAPH_METRIC_MANHATTAN}, + {"l1", IGRAPH_METRIC_L1}, /* alias to the previous */ + {0,0} + }; + TRANSLATE_ENUM_WITH(metric_tt); +} + /** * \ingroup python_interface_conversion * \brief Converts a Python object to an igraph \c igraph_edge_type_sw_t diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 006f0b652..39fbf5619 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -65,16 +65,21 @@ int igraphmodule_PyObject_to_attribute_combination_type_t(PyObject* o, int igraphmodule_PyObject_to_barabasi_algorithm_t(PyObject *o, igraph_barabasi_algorithm_t *result); int igraphmodule_PyObject_to_bliss_sh_t(PyObject *o, igraph_bliss_sh_t *result); +int igraphmodule_PyObject_to_chung_lu_t(PyObject *o, igraph_chung_lu_t *result); int igraphmodule_PyObject_to_coloring_greedy_t(PyObject *o, igraph_coloring_greedy_t *result); int igraphmodule_PyObject_to_community_comparison_t(PyObject *obj, igraph_community_comparison_t *result); int igraphmodule_PyObject_to_connectedness_t(PyObject *o, igraph_connectedness_t *result); int igraphmodule_PyObject_to_degseq_t(PyObject *o, igraph_degseq_t *result); int igraphmodule_PyObject_to_fas_algorithm_t(PyObject *o, igraph_fas_algorithm_t *result); +int igraphmodule_PyObject_to_fvs_algorithm_t(PyObject *o, igraph_fvs_algorithm_t *result); int igraphmodule_PyObject_to_get_adjacency_t(PyObject *o, igraph_get_adjacency_t *result); int igraphmodule_PyObject_to_laplacian_normalization_t(PyObject *o, igraph_laplacian_normalization_t *result); int igraphmodule_PyObject_to_layout_grid_t(PyObject *o, igraph_layout_grid_t *result); +int igraphmodule_PyObject_to_lpa_variant_t(PyObject *o, igraph_lpa_variant_t *result); int igraphmodule_PyObject_to_loops_t(PyObject *o, igraph_loops_t *result); +int igraphmodule_PyObject_to_metric_t(PyObject *o, igraph_metric_t *result); +int igraphmodule_PyObject_to_mst_algorithm_t(PyObject *o, igraph_mst_algorithm_t *result); int igraphmodule_PyObject_to_neimode_t(PyObject *o, igraph_neimode_t *result); int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t *result); int igraphmodule_PyObject_to_edge_type_sw_t(PyObject *o, igraph_edge_type_sw_t *result); @@ -82,7 +87,6 @@ int igraphmodule_PyObject_to_realize_degseq_t(PyObject *o, igraph_realize_degseq int igraphmodule_PyObject_to_random_tree_t(PyObject *o, igraph_random_tree_t *result); int igraphmodule_PyObject_to_random_walk_stuck_t(PyObject *o, igraph_random_walk_stuck_t *result); int igraphmodule_PyObject_to_reciprocity_t(PyObject *o, igraph_reciprocity_t *result); -int igraphmodule_PyObject_to_rewiring_t(PyObject *o, igraph_rewiring_t *result); int igraphmodule_PyObject_to_shortest_path_algorithm_t(PyObject *o, igraphmodule_shortest_path_algorithm_t *result); int igraphmodule_PyObject_to_spinglass_implementation_t(PyObject *o, igraph_spinglass_implementation_t *result); int igraphmodule_PyObject_to_spincomm_update_t(PyObject *o, igraph_spincomm_update_t *result); @@ -96,10 +100,12 @@ int igraphmodule_PyObject_to_vconn_nei_t(PyObject *o, igraph_vconn_nei_t *result /* Conversion from PyObject to igraph types */ -int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v); +int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_int_t *v); int igraphmodule_PyObject_to_real_t(PyObject *object, igraph_real_t *v); int igraphmodule_PyObject_to_igraph_t(PyObject *o, igraph_t **result); +int igraphmodule_PyObject_to_max_results_t(PyObject *object, igraph_int_t *v); + int igraphmodule_PyObject_to_vector_t(PyObject *list, igraph_vector_t *v, igraph_bool_t need_non_negative); int igraphmodule_PyObject_float_to_vector_t(PyObject *list, igraph_vector_t *v); @@ -116,10 +122,15 @@ int igraphmodule_PyObject_to_edgelist( igraph_bool_t *list_is_owned ); -int igraphmodule_PyList_to_matrix_t(PyObject *o, igraph_matrix_t *m, const char *arg_name); -int igraphmodule_PyList_to_matrix_t_with_minimum_column_count(PyObject *o, igraph_matrix_t *m, int min_cols, const char *arg_name); -int igraphmodule_PyList_to_matrix_int_t(PyObject *o, igraph_matrix_int_t *m, const char *arg_name); -int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(PyObject *o, igraph_matrix_int_t *m, int min_cols, const char *arg_name); +int igraphmodule_PyObject_to_matrix_t( + PyObject *o, igraph_matrix_t *m, const char *arg_name); +int igraphmodule_PyObject_to_matrix_t_with_minimum_column_count( + PyObject *o, igraph_matrix_t *m, int min_cols, const char *arg_name); +int igraphmodule_PyObject_to_matrix_int_t( + PyObject *o, igraph_matrix_int_t *m, const char *arg_name); +int igraphmodule_PyObject_to_matrix_int_t_with_minimum_column_count( + PyObject *o, igraph_matrix_int_t *m, int min_cols, const char *arg_name); + PyObject* igraphmodule_strvector_t_to_PyList(igraph_strvector_t *v); int igraphmodule_PyList_to_strvector_t(PyObject* v, igraph_strvector_t *result); int igraphmodule_PyList_to_existing_strvector_t(PyObject* v, igraph_strvector_t *result); @@ -127,14 +138,14 @@ int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t(PyObject *it, igraph_vector_ptr_t *v); int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t_with_type(PyObject *it, igraph_vector_ptr_t *v, PyTypeObject **g_type); -int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *graph); -int igraphmodule_PyObject_to_optional_vid(PyObject *o, igraph_integer_t *vid, igraph_t *graph); +int igraphmodule_PyObject_to_vid(PyObject *o, igraph_int_t *vid, igraph_t *graph); +int igraphmodule_PyObject_to_optional_vid(PyObject *o, igraph_int_t *vid, igraph_t *graph); int igraphmodule_PyObject_to_vid_list(PyObject *o, igraph_vector_int_t *vids, igraph_t *graph); int igraphmodule_PyObject_to_vs_t( PyObject *o, igraph_vs_t *vs, igraph_t *graph, - igraph_bool_t *return_single, igraph_integer_t *single_vid + igraph_bool_t *return_single, igraph_int_t *single_vid ); -int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *graph); +int igraphmodule_PyObject_to_eid(PyObject *o, igraph_int_t *eid, igraph_t *graph); int igraphmodule_PyObject_to_es_t(PyObject *o, igraph_es_t *es, igraph_t *graph, igraph_bool_t *return_single); int igraphmodule_PyObject_to_attribute_values(PyObject *o, @@ -163,7 +174,7 @@ int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject * /* Conversion from igraph types to PyObjects */ -PyObject* igraphmodule_integer_t_to_PyObject(igraph_integer_t value); +PyObject* igraphmodule_integer_t_to_PyObject(igraph_int_t value); PyObject* igraphmodule_real_t_to_PyObject(igraph_real_t value, igraphmodule_conv_t type); PyObject* igraphmodule_vector_bool_t_to_PyList(const igraph_vector_bool_t *v); @@ -174,7 +185,7 @@ PyObject* igraphmodule_vector_int_t_pair_to_PyList(const igraph_vector_int_t *v1 const igraph_vector_int_t *v2); PyObject* igraphmodule_vector_int_t_to_PyList_of_fixed_length_tuples( const igraph_vector_int_t *v, Py_ssize_t tuple_len); -PyObject* igraphmodule_vector_int_t_to_PyList_with_nan(const igraph_vector_int_t *v, const igraph_integer_t nanvalue); +PyObject* igraphmodule_vector_int_t_to_PyList_with_nan(const igraph_vector_int_t *v, const igraph_int_t nanvalue); PyObject* igraphmodule_vector_ptr_t_to_PyList(const igraph_vector_ptr_t *v, igraphmodule_conv_t type); PyObject* igraphmodule_vector_int_ptr_t_to_PyList(const igraph_vector_ptr_t *v); @@ -186,4 +197,5 @@ PyObject* igraphmodule_vector_int_t_to_PyList(const igraph_vector_int_t *v); PyObject* igraphmodule_matrix_t_to_PyList(const igraph_matrix_t *m, igraphmodule_conv_t type); PyObject* igraphmodule_matrix_int_t_to_PyList(const igraph_matrix_int_t *m); +PyObject* igraphmodule_matrix_list_t_to_PyList(const igraph_matrix_list_t *m); #endif diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c index 6064a9862..08a7fedea 100644 --- a/src/_igraph/dfsiter.c +++ b/src/_igraph/dfsiter.c @@ -44,7 +44,7 @@ PyTypeObject* igraphmodule_DFSIterType; */ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_neimode_t mode, igraph_bool_t advanced) { igraphmodule_DFSIterObject* self; - igraph_integer_t no_of_nodes, r; + igraph_int_t no_of_nodes, r; self = (igraphmodule_DFSIterObject*) PyType_GenericNew(igraphmodule_DFSIterType, 0, 0); if (!self) { @@ -122,10 +122,7 @@ static int igraphmodule_DFSIter_traverse(igraphmodule_DFSIterObject *self, visitproc visit, void *arg) { RC_TRAVERSE("DFSIter", self); Py_VISIT(self->gref); -#if PY_VERSION_HEX >= 0x03090000 - // This was not needed before Python 3.9 (Python issue 35810 and 40217) Py_VISIT(Py_TYPE(self)); -#endif return 0; } @@ -165,7 +162,7 @@ static PyObject* igraphmodule_DFSIter_iter(igraphmodule_DFSIterObject* self) { static PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) { /* the design is to return the top of the stack and then proceed until * we have found an unvisited neighbor and push that on top */ - igraph_integer_t parent_out, dist_out, vid_out; + igraph_int_t parent_out, dist_out, vid_out; igraph_bool_t any = false; /* nothing on the stack, end of iterator */ @@ -184,25 +181,25 @@ static PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) /* look for neighbors until we find one or until we have exhausted the graph */ while (!any && !igraph_stack_int_empty(&self->stack)) { - igraph_integer_t parent = igraph_stack_int_pop(&self->stack); - igraph_integer_t dist = igraph_stack_int_pop(&self->stack); - igraph_integer_t vid = igraph_stack_int_pop(&self->stack); + igraph_int_t parent = igraph_stack_int_pop(&self->stack); + igraph_int_t dist = igraph_stack_int_pop(&self->stack); + igraph_int_t vid = igraph_stack_int_pop(&self->stack); igraph_stack_int_push(&self->stack, vid); igraph_stack_int_push(&self->stack, dist); igraph_stack_int_push(&self->stack, parent); - igraph_integer_t i, n; + igraph_int_t i, n; /* the values above are returned at at this stage. However, we must * prepare for the next iteration by putting the next unvisited * neighbor onto the stack */ - if (igraph_neighbors(self->graph, &self->neis, vid, self->mode)) { + if (igraph_neighbors(self->graph, &self->neis, vid, self->mode, /* loops = */ 0, /* multiple = */ 0)) { igraphmodule_handle_igraph_error(); return NULL; } n = igraph_vector_int_size(&self->neis); for (i = 0; i < n; i++) { - igraph_integer_t neighbor = VECTOR(self->neis)[i]; + igraph_int_t neighbor = VECTOR(self->neis)[i]; /* new neighbor, push the next item onto the stack */ if (self->visited[neighbor] == 0) { any = 1; diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index ca26c7009..0f5e54171 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -54,7 +54,7 @@ int igraphmodule_Edge_Check(PyObject* obj) { * exception and returns zero if the edge object is invalid. */ int igraphmodule_Edge_Validate(PyObject* obj) { - igraph_integer_t n; + igraph_int_t n; igraphmodule_EdgeObject *self; igraphmodule_GraphObject *graph; @@ -98,7 +98,7 @@ int igraphmodule_Edge_Validate(PyObject* obj) { * changes, your existing edge objects will point to elsewhere * (or they might even get invalidated). */ -PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t idx) { +PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_int_t idx) { return PyObject_CallFunction((PyObject*) igraphmodule_EdgeType, "On", gref, (Py_ssize_t) idx); } @@ -110,7 +110,7 @@ PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t static int igraphmodule_Edge_init(igraphmodule_EdgeObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "graph", "eid", NULL }; PyObject *g, *index_o = Py_None; - igraph_integer_t eid; + igraph_int_t eid; igraph_t *graph; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|O", kwlist, @@ -400,7 +400,7 @@ int igraphmodule_Edge_set_attribute(igraphmodule_EdgeObject* self, PyObject* k, /* result is NULL, check whether there was an error */ if (!PyErr_Occurred()) { /* no, there wasn't, so we must simply add the attribute */ - igraph_integer_t i, n = igraph_ecount(&o->g); + igraph_int_t i, n = igraph_ecount(&o->g); result = PyList_New(n); for (i = 0; i < n; i++) { if (i != self->idx) { @@ -437,7 +437,7 @@ int igraphmodule_Edge_set_attribute(igraphmodule_EdgeObject* self, PyObject* k, */ PyObject* igraphmodule_Edge_get_from(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; if (!igraphmodule_Edge_Validate((PyObject*)self)) { return NULL; @@ -456,7 +456,7 @@ PyObject* igraphmodule_Edge_get_from(igraphmodule_EdgeObject* self, void* closur */ PyObject* igraphmodule_Edge_get_source_vertex(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; if (!igraphmodule_Edge_Validate((PyObject*)self)) return NULL; @@ -474,7 +474,7 @@ PyObject* igraphmodule_Edge_get_source_vertex(igraphmodule_EdgeObject* self, voi */ PyObject* igraphmodule_Edge_get_to(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; if (!igraphmodule_Edge_Validate((PyObject*)self)) { return NULL; @@ -493,7 +493,7 @@ PyObject* igraphmodule_Edge_get_to(igraphmodule_EdgeObject* self, void* closure) */ PyObject* igraphmodule_Edge_get_target_vertex(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; if (!igraphmodule_Edge_Validate((PyObject*)self)) return NULL; @@ -515,9 +515,9 @@ PyObject* igraphmodule_Edge_get_index(igraphmodule_EdgeObject* self, void* closu /** * \ingroup python_interface_edge - * Returns the edge index as an igraph_integer_t + * Returns the edge index as an igraph_int_t */ -igraph_integer_t igraphmodule_Edge_get_index_as_igraph_integer(igraphmodule_EdgeObject* self) { +igraph_int_t igraphmodule_Edge_get_index_as_igraph_integer(igraphmodule_EdgeObject* self) { return self->idx; } @@ -527,7 +527,7 @@ igraph_integer_t igraphmodule_Edge_get_index_as_igraph_integer(igraphmodule_Edge */ PyObject* igraphmodule_Edge_get_tuple(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; PyObject *from_o, *to_o, *result; if (!igraphmodule_Edge_Validate((PyObject*)self)) { @@ -562,7 +562,7 @@ PyObject* igraphmodule_Edge_get_tuple(igraphmodule_EdgeObject* self, void* closu */ PyObject* igraphmodule_Edge_get_vertex_tuple(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; PyObject *from_o, *to_o; if (!igraphmodule_Edge_Validate((PyObject*)self)) @@ -724,6 +724,15 @@ PyDoc_STRVAR( " >>> e[\"weight\"] = 2 #doctest: +SKIP\n" " >>> print(e[\"weight\"]) #doctest: +SKIP\n" " 2\n" + "\n" + "@ivar source: Source vertex index of this edge\n" + "@ivar source_vertex: Source vertex of this edge\n" + "@ivar target: Target vertex index of this edge\n" + "@ivar target_vertex: Target vertex of this edge\n" + "@ivar tuple: Source and target vertex index of this edge as a tuple\n" + "@ivar vertex_tuple: Source and target vertex of this edge as a tuple\n" + "@ivar index: Index of this edge\n" + "@ivar graph: The graph the edge belongs to\t" ); int igraphmodule_Edge_register_type() { diff --git a/src/_igraph/edgeobject.h b/src/_igraph/edgeobject.h index f42862fe3..039a6e20f 100644 --- a/src/_igraph/edgeobject.h +++ b/src/_igraph/edgeobject.h @@ -34,7 +34,7 @@ typedef struct { PyObject_HEAD igraphmodule_GraphObject* gref; - igraph_integer_t idx; + igraph_int_t idx; long hash; } igraphmodule_EdgeObject; @@ -43,7 +43,7 @@ extern PyTypeObject* igraphmodule_EdgeType; int igraphmodule_Edge_register_type(void); int igraphmodule_Edge_Check(PyObject* obj); -PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t idx); -igraph_integer_t igraphmodule_Edge_get_index_as_igraph_integer(igraphmodule_EdgeObject* self); +PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_int_t idx); +igraph_int_t igraphmodule_Edge_get_index_as_igraph_integer(igraphmodule_EdgeObject* self); #endif diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index fcdbce41a..13f4364f9 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -109,7 +109,7 @@ int igraphmodule_EdgeSeq_init(igraphmodule_EdgeSeqObject *self, PyObject *args, igraph_es_all(&es, IGRAPH_EDGEORDER_ID); } else if (PyLong_Check(esobj)) { /* We selected a single edge */ - igraph_integer_t idx; + igraph_int_t idx; if (igraphmodule_PyObject_to_integer_t(esobj, &idx)) { return -1; @@ -124,7 +124,7 @@ int igraphmodule_EdgeSeq_init(igraphmodule_EdgeSeqObject *self, PyObject *args, } else { /* We selected multiple edges */ igraph_vector_int_t v; - igraph_integer_t n = igraph_ecount(&((igraphmodule_GraphObject*)g)->g); + igraph_int_t n = igraph_ecount(&((igraphmodule_GraphObject*)g)->g); if (igraphmodule_PyObject_to_vector_int_t(esobj, &v)) { return -1; } @@ -173,7 +173,7 @@ void igraphmodule_EdgeSeq_dealloc(igraphmodule_EdgeSeqObject* self) { */ Py_ssize_t igraphmodule_EdgeSeq_sq_length(igraphmodule_EdgeSeqObject* self) { igraph_t *g; - igraph_integer_t result; + igraph_int_t result; g = &GET_GRAPH(self); @@ -192,7 +192,7 @@ Py_ssize_t igraphmodule_EdgeSeq_sq_length(igraphmodule_EdgeSeqObject* self) { PyObject* igraphmodule_EdgeSeq_sq_item(igraphmodule_EdgeSeqObject* self, Py_ssize_t i) { igraph_t *g; - igraph_integer_t idx = -1; + igraph_int_t idx = -1; if (!self->gref) { return NULL; @@ -433,7 +433,7 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject igraphmodule_GraphObject *gr; igraph_vector_int_t es; Py_ssize_t i, j, n; - igraph_integer_t no_of_edges; + igraph_int_t no_of_edges; gr = self->gref; dict = ATTR_STRUCT_DICT(&gr->g)[ATTRHASH_IDX_EDGE]; @@ -570,7 +570,7 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject /* We don't have attributes with the given name yet. Create an entry * in the dict, create a new list, fill with None for vertices not in the * sequence and copy the rest */ - igraph_integer_t n2 = igraph_ecount(&gr->g); + igraph_int_t n2 = igraph_ecount(&gr->g); list = PyList_New(n2); if (list == 0) { igraph_vector_int_destroy(&es); @@ -682,7 +682,7 @@ PyObject* igraphmodule_EdgeSeq_find(igraphmodule_EdgeSeqObject *self, PyObject * PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject *args) { igraphmodule_EdgeSeqObject *result; igraphmodule_GraphObject *gr; - igraph_integer_t igraph_idx; + igraph_int_t igraph_idx; igraph_bool_t working_on_whole_graph = igraph_es_is_all(&self->es); igraph_vector_int_t v, v2; Py_ssize_t i, j, n, m; @@ -786,7 +786,7 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject for (; i < n; i++) { PyObject *item2 = PyTuple_GetItem(args, i); - igraph_integer_t idx; + igraph_int_t idx; if (item2 == 0) { Py_DECREF(result); @@ -849,7 +849,7 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject igraph_vector_int_destroy(&v); } else { /* Iterators and everything that was not handled directly */ - PyObject *iter, *item2; + PyObject *iter = NULL, *item2 = NULL; /* Allocate stuff */ if (igraph_vector_int_init(&v, 0)) { diff --git a/src/_igraph/error.c b/src/_igraph/error.c index 41b2b879e..b6940a27e 100644 --- a/src/_igraph/error.c +++ b/src/_igraph/error.c @@ -61,7 +61,20 @@ PyObject* igraphmodule_handle_igraph_error() { void igraphmodule_igraph_warning_hook(const char *reason, const char *file, int line) { char buf[4096]; - snprintf(buf, sizeof(buf), "%s at %s:%i", reason, file, line); + char end; + size_t len = strlen(reason); + const char* separator = " "; + + if (len == 0) { + separator = ""; + } else { + end = reason[len - 1]; + if (end != '.' && end != '?' && end != '!') { + separator = ". "; + } + } + + snprintf(buf, sizeof(buf), "%s%sLocation: %s:%i", reason, separator, file, line); PY_IGRAPH_WARN(buf); } diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 842cd84e3..b6c14a340 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -33,7 +33,6 @@ #include "indexing.h" #include "memory.h" #include "pyhelpers.h" -#include "utils.h" #include "vertexseqobject.h" #include @@ -141,10 +140,7 @@ int igraphmodule_Graph_traverse(igraphmodule_GraphObject * self, } } -#if PY_VERSION_HEX >= 0x03090000 - // This was not needed before Python 3.9 (Python issue 35810 and 40217) Py_VISIT(Py_TYPE(self)); -#endif return 0; } @@ -200,7 +196,7 @@ int igraphmodule_Graph_init(igraphmodule_GraphObject * self, void* ptr = 0; Py_ssize_t n = 0; igraph_vector_int_t edges_vector; - igraph_integer_t vcount; + igraph_int_t vcount; igraph_bool_t edges_vector_owned = false; int retval = 0; @@ -537,7 +533,7 @@ PyObject *igraphmodule_Graph_is_maximal_matching(igraphmodule_GraphObject* self, PyObject *igraphmodule_Graph_is_simple(igraphmodule_GraphObject* self, PyObject* Py_UNUSED(_null)) { igraph_bool_t res; - if (igraph_is_simple(&self->g, &res)) { + if (igraph_is_simple(&self->g, &res, IGRAPH_DIRECTED)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -548,6 +544,96 @@ PyObject *igraphmodule_Graph_is_simple(igraphmodule_GraphObject* self, PyObject* } +/** \ingroup python_interface_graph + * \brief Checks whether an \c igraph.Graph object is a complete graph. + * \return \c True if the graph is complete, \c False otherwise. + * \sa igraph_is_complete + */ +PyObject *igraphmodule_Graph_is_complete(igraphmodule_GraphObject* self, PyObject* Py_UNUSED(_null)) { + igraph_bool_t res; + + if (igraph_is_complete(&self->g, &res)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (res) + Py_RETURN_TRUE; + Py_RETURN_FALSE; +} + + +/** \ingroup python_interface_graph + * \brief Checks whether a given vertex set forms a clique + */ +PyObject *igraphmodule_Graph_is_clique(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) +{ + PyObject *list = Py_None; + PyObject *directed = Py_False; + igraph_bool_t res; + igraph_vs_t vs; + + static char *kwlist[] = { "vertices", "directed", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &list, &directed)) { + return NULL; + } + + if (igraphmodule_PyObject_to_vs_t(list, &vs, &self->g, NULL, NULL)) { + return NULL; + } + + if (igraph_is_clique(&self->g, vs, PyObject_IsTrue(directed), &res)) { + igraphmodule_handle_igraph_error(); + igraph_vs_destroy(&vs); + return NULL; + } + + igraph_vs_destroy(&vs); + + if (res) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; +} + + +/** \ingroup python_interface_graph + * \brief Checks whether a the given vertices form an independent set + */ +PyObject *igraphmodule_Graph_is_independent_vertex_set(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) +{ + PyObject *list = Py_None; + igraph_bool_t res; + igraph_vs_t vs; + + static char *kwlist[] = { "vertices", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &list)) { + return NULL; + } + + if (igraphmodule_PyObject_to_vs_t(list, &vs, &self->g, NULL, NULL)) { + return NULL; + } + + if (igraph_is_independent_vertex_set(&self->g, vs, &res)) { + igraphmodule_handle_igraph_error(); + igraph_vs_destroy(&vs); + return NULL; + } + + igraph_vs_destroy(&vs); + + if (res) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; +} + + /** \ingroup python_interface_graph * \brief Determines whether a graph is a (directed or undirected) tree * \sa igraph_is_tree @@ -790,7 +876,7 @@ PyObject *igraphmodule_Graph_diversity(igraphmodule_GraphObject * self, igraph_vector_t res, *weights = 0; igraph_vs_t vs; igraph_bool_t return_single = false; - igraph_integer_t no_of_nodes; + igraph_int_t no_of_nodes; static char *kwlist[] = { "vertices", "weights", NULL }; @@ -927,15 +1013,50 @@ PyObject *igraphmodule_Graph_strength(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_density(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) +{ + char *kwlist[] = { "loops", "weights", NULL }; + igraph_real_t res; + PyObject *loops = Py_False, *weights_o = Py_None; + igraph_vector_t *weights = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &loops, &weights_o)) + return NULL; + + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } + + if (igraph_density(&self->g, weights, &res, PyObject_IsTrue(loops))) { + igraphmodule_handle_igraph_error(); + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + return NULL; + } + + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); +} + +/** \ingroup python_interface_graph + * \brief Calculates the mean degree + * \return the mean degree + * \sa igraph_mean_degree + */ +PyObject *igraphmodule_Graph_mean_degree(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) { char *kwlist[] = { "loops", NULL }; igraph_real_t res; - PyObject *loops = Py_False; + PyObject *loops = Py_True; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &loops)) return NULL; - if (igraph_density(&self->g, &res, PyObject_IsTrue(loops))) { + if (igraph_mean_degree(&self->g, &res, PyObject_IsTrue(loops))) { igraphmodule_handle_igraph_error(); return NULL; } @@ -955,7 +1076,7 @@ PyObject *igraphmodule_Graph_maxdegree(igraphmodule_GraphObject * self, igraph_neimode_t dmode = IGRAPH_ALL; PyObject *dmode_o = Py_None; PyObject *loops = Py_False; - igraph_integer_t res; + igraph_int_t res; igraph_vs_t vs; igraph_bool_t return_single = false; @@ -1207,20 +1328,28 @@ PyObject *igraphmodule_Graph_count_multiple(igraphmodule_GraphObject *self, PyObject *igraphmodule_Graph_neighbors(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *list, *dmode_o = Py_None, *index_o; + PyObject *list, *dmode_o = Py_None, *index_o, *loops_o = Py_True, *multiple_o = Py_True; igraph_neimode_t dmode = IGRAPH_ALL; - igraph_integer_t idx; + igraph_loops_t loops = IGRAPH_LOOPS; + igraph_bool_t multiple = 1; + igraph_int_t idx; igraph_vector_int_t res; - static char *kwlist[] = { "vertex", "mode", NULL }; + static char *kwlist[] = { "vertex", "mode", "loops", "multiple", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &index_o, &dmode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, &index_o, &dmode_o, &loops_o, &multiple_o)) return NULL; if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) { return NULL; } + if (igraphmodule_PyObject_to_loops_t(loops_o, &loops)) { + return NULL; + } + + multiple = PyObject_IsTrue(multiple_o); + if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) { return NULL; } @@ -1230,7 +1359,7 @@ PyObject *igraphmodule_Graph_neighbors(igraphmodule_GraphObject * self, return NULL; } - if (igraph_neighbors(&self->g, &res, idx, dmode)) { + if (igraph_neighbors(&self->g, &res, idx, dmode, loops, multiple)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&res); return NULL; @@ -1256,20 +1385,25 @@ PyObject *igraphmodule_Graph_neighbors(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_incident(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *list, *dmode_o = Py_None, *index_o; + PyObject *list, *dmode_o = Py_None, *index_o, *loops_o = Py_True; igraph_neimode_t dmode = IGRAPH_OUT; - igraph_integer_t idx; + igraph_loops_t loops = IGRAPH_LOOPS; + igraph_int_t idx; igraph_vector_int_t res; - static char *kwlist[] = { "vertex", "mode", NULL }; + static char *kwlist[] = { "vertex", "mode", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &index_o, &dmode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, &index_o, &dmode_o, &loops_o)) return NULL; if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) { return NULL; } + if (igraphmodule_PyObject_to_loops_t(loops_o, &loops)) { + return NULL; + } + if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) { return NULL; } @@ -1279,7 +1413,7 @@ PyObject *igraphmodule_Graph_incident(igraphmodule_GraphObject * self, return NULL; } - if (igraph_incident(&self->g, &res, idx, dmode)) { + if (igraph_incident(&self->g, &res, idx, dmode, loops)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&res); return NULL; @@ -1318,80 +1452,6 @@ PyObject *igraphmodule_Graph_reciprocity(igraphmodule_GraphObject * self, return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); } -/** \ingroup python_interface_graph - * \brief The successors of a given vertex in an \c igraph.Graph - * This method accepts a single vertex ID as a parameter, and returns the - * successors of the given vertex in the form of an integer list. It - * is equivalent to calling \c igraph.Graph.neighbors with \c type=OUT - * - * \return the successor list as a Python list object - * \sa igraph_neighbors - */ -PyObject *igraphmodule_Graph_successors(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) -{ - PyObject *list, *index_o; - igraph_integer_t idx; - igraph_vector_int_t res; - - static char *kwlist[] = { "vertex", NULL }; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &index_o)) - return NULL; - - if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) - return NULL; - - igraph_vector_int_init(&res, 0); - if (igraph_neighbors(&self->g, &res, idx, IGRAPH_OUT)) { - igraphmodule_handle_igraph_error(); - igraph_vector_int_destroy(&res); - return NULL; - } - - list = igraphmodule_vector_int_t_to_PyList(&res); - igraph_vector_int_destroy(&res); - - return list; -} - -/** \ingroup python_interface_graph - * \brief The predecessors of a given vertex in an \c igraph.Graph - * This method accepts a single vertex ID as a parameter, and returns the - * predecessors of the given vertex in the form of an integer list. It - * is equivalent to calling \c igraph.Graph.neighbors with \c type=IN - * - * \return the predecessor list as a Python list object - * \sa igraph_neighbors - */ -PyObject *igraphmodule_Graph_predecessors(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) -{ - PyObject *list, *index_o; - igraph_integer_t idx; - igraph_vector_int_t res; - - static char *kwlist[] = { "vertex", NULL }; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &index_o)) - return NULL; - - if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) - return NULL; - - igraph_vector_int_init(&res, 1); - if (igraph_neighbors(&self->g, &res, idx, IGRAPH_IN)) { - igraphmodule_handle_igraph_error(); - igraph_vector_int_destroy(&res); - return NULL; - } - - list = igraphmodule_vector_int_t_to_PyList(&res); - igraph_vector_int_destroy(&res); - - return list; -} - /** \ingroup python_interface_graph * \brief Decides whether a graph is connected. * \return Py_True if the graph is connected, Py_False otherwise @@ -1421,17 +1481,38 @@ PyObject *igraphmodule_Graph_is_connected(igraphmodule_GraphObject * self, Py_RETURN_FALSE; } +/** \ingroup python_interface_graph + * \brief Decides whether a graph is biconnected. + * \return Py_True if the graph is biconnected, Py_False otherwise + * \sa igraph_is_biconnected + */ +PyObject *igraphmodule_Graph_is_biconnected(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) +{ + igraph_bool_t res; + + if (igraph_is_biconnected(&self->g, &res)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (res) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + /** \ingroup python_interface_graph * \brief Decides whether there is an edge from a given vertex to an other one. * \return Py_True if the vertices are directly connected, Py_False otherwise - * \sa igraph_are_connected + * \sa igraph_are_adjacent */ -PyObject *igraphmodule_Graph_are_connected(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) +PyObject *igraphmodule_Graph_are_adjacent(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) { static char *kwlist[] = { "v1", "v2", NULL }; PyObject *v1, *v2; - igraph_integer_t idx1, idx2; + igraph_int_t idx1, idx2; igraph_bool_t res; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, &v1, &v2)) @@ -1443,7 +1524,7 @@ PyObject *igraphmodule_Graph_are_connected(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_vid(v2, &idx2, &self->g)) return NULL; - if (igraph_are_connected(&self->g, idx1, idx2, &res)) + if (igraph_are_adjacent(&self->g, idx1, idx2, &res)) return igraphmodule_handle_igraph_error(); if (res) @@ -1462,8 +1543,8 @@ PyObject *igraphmodule_Graph_get_eid(igraphmodule_GraphObject * self, PyObject *v1, *v2; PyObject *directed = Py_True; PyObject *error = Py_True; - igraph_integer_t idx1, idx2; - igraph_integer_t res; + igraph_int_t idx1, idx2; + igraph_int_t res; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OO", kwlist, &v1, &v2, &directed, &error)) @@ -1558,30 +1639,24 @@ PyObject *igraphmodule_Graph_diameter(igraphmodule_GraphObject * self, if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) return NULL; - if (weights) { - if (igraph_diameter_dijkstra(&self->g, weights, &diameter, - /* from, to, vertex_path, edge_path */ - 0, 0, 0, 0, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); + if ( + igraph_diameter(&self->g, weights, &diameter, + /* from, to, vertex_path, edge_path */ + 0, 0, 0, 0, + PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected)) + ) { + igraphmodule_handle_igraph_error(); + if (weights) { igraph_vector_destroy(weights); free(weights); - return NULL; - } - igraph_vector_destroy(weights); free(weights); - return igraphmodule_real_t_to_PyObject(diameter, IGRAPHMODULE_TYPE_FLOAT); - } else { - if (igraph_diameter(&self->g, &diameter, - /* from, to, vertex_path, edge_path */ - 0, 0, 0, 0, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); - return NULL; } + return NULL; + } - /* The diameter is integer in this case, except if igraph_diameter() - * returned NaN or infinity for some reason */ - return igraphmodule_real_t_to_PyObject(diameter, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); + if (weights) { + igraph_vector_destroy(weights); free(weights); } + + return igraphmodule_real_t_to_PyObject(diameter, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); } /** \ingroup python_interface_graph @@ -1606,30 +1681,33 @@ PyObject *igraphmodule_Graph_get_diameter(igraphmodule_GraphObject * self, if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) return NULL; - igraph_vector_int_init(&res, 0); - if (weights) { - if (igraph_diameter_dijkstra(&self->g, weights, 0, - /* from, to, vertex_path, edge_path */ - 0, 0, &res, 0, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); + if (igraph_vector_int_init(&res, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if ( + igraph_diameter(&self->g, weights, 0, + /* from, to, vertex_path, edge_path */ + 0, 0, &res, 0, + PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected) + ) + ) { + igraphmodule_handle_igraph_error(); + if (weights) { igraph_vector_destroy(weights); free(weights); - igraph_vector_int_destroy(&res); - return NULL; } + igraph_vector_int_destroy(&res); + return NULL; + } + + if (weights) { igraph_vector_destroy(weights); free(weights); - } else { - if (igraph_diameter(&self->g, 0, - /* from, to, vertex_path, edge_path */ - 0, 0, &res, 0, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); - return NULL; - } } result_o = igraphmodule_vector_int_t_to_PyList(&res); igraph_vector_int_destroy(&res); + return result_o; } @@ -1643,7 +1721,7 @@ PyObject *igraphmodule_Graph_farthest_points(igraphmodule_GraphObject * self, PyObject *dir = Py_True, *vcount_if_unconnected = Py_True; PyObject *weights_o = Py_None; igraph_vector_t *weights = 0; - igraph_integer_t from, to; + igraph_int_t from, to; igraph_real_t len; static char *kwlist[] = { "directed", "unconn", "weights", NULL }; @@ -1656,46 +1734,29 @@ PyObject *igraphmodule_Graph_farthest_points(igraphmodule_GraphObject * self, if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) return NULL; - if (weights) { - if (igraph_diameter_dijkstra(&self->g, weights, &len, - /* from, to, vertex_path, edge_path */ - &from, &to, 0, 0, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); + if ( + igraph_diameter( + &self->g, weights, &len, + /* from, to, vertex_path, edge_path */ + &from, &to, 0, 0, + PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected) + ) + ) { + igraphmodule_handle_igraph_error(); + if (weights) { igraph_vector_destroy(weights); free(weights); - return NULL; } + return NULL; + } + + if (weights) { igraph_vector_destroy(weights); free(weights); - if (from >= 0) { - return Py_BuildValue("nnd", (Py_ssize_t)from, (Py_ssize_t)to, (double)len); - } else { - return Py_BuildValue("OOd", Py_None, Py_None, (double)len); - } - } else { - if (igraph_diameter(&self->g, &len, - /* from, to, vertex_path, edge_path */ - &from, &to, 0, 0, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); - return NULL; - } + } - /* if len is finite and integer (which it typically is, unless it's - * infinite), then return a Python int as the third value; otherwise - * return a float */ - if (ceil(len) == len && isfinite(len)) { - if (from >= 0) { - return Py_BuildValue("nnn", (Py_ssize_t)from, (Py_ssize_t)to, (Py_ssize_t)len); - } else { - return Py_BuildValue("OOn", Py_None, Py_None, (Py_ssize_t)len); - } - } else { - if (from >= 0) { - return Py_BuildValue("nnd", (Py_ssize_t)from, (Py_ssize_t)to, (double)len); - } else { - return Py_BuildValue("OOd", Py_None, Py_None, (double)len); - } - } + if (from >= 0) { + return Py_BuildValue("nnd", (Py_ssize_t)from, (Py_ssize_t)to, (double)len); + } else { + return Py_BuildValue("OOd", Py_None, Py_None, (double)len); } } @@ -1888,7 +1949,7 @@ PyObject *igraphmodule_Graph_radius(igraphmodule_GraphObject * self, return NULL; } - if (igraph_radius_dijkstra(&self->g, weights, &radius, mode)) { + if (igraph_radius(&self->g, weights, &radius, mode)) { if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -1904,8 +1965,8 @@ PyObject *igraphmodule_Graph_radius(igraphmodule_GraphObject * self, } /** \ingroup python_interface_graph - * \brief Converts a tree graph into a Prufer sequence - * \return the Prufer sequence as a Python object + * \brief Converts a tree graph into a Prüfer sequence + * \return the Prüfer sequence as a Python object * \sa igraph_to_prufer */ PyObject *igraphmodule_Graph_to_prufer(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) @@ -1943,14 +2004,14 @@ PyObject *igraphmodule_Graph_Adjacency(PyTypeObject * type, igraphmodule_GraphObject *self; igraph_t g; igraph_matrix_t m; - PyObject *matrix, *mode_o = Py_None, *loops_o = Py_None; + PyObject *matrix_o, *mode_o = Py_None, *loops_o = Py_None; igraph_adjacency_t mode = IGRAPH_ADJ_DIRECTED; igraph_loops_t loops = IGRAPH_LOOPS_ONCE; static char *kwlist[] = { "matrix", "mode", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OO", kwlist, - &PyList_Type, &matrix, &mode_o, &loops_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, + &matrix_o, &mode_o, &loops_o)) return NULL; if (igraphmodule_PyObject_to_adjacency_t(mode_o, &mode)) @@ -1959,7 +2020,7 @@ PyObject *igraphmodule_Graph_Adjacency(PyTypeObject * type, if (igraphmodule_PyObject_to_loops_t(loops_o, &loops)) return NULL; - if (igraphmodule_PyList_to_matrix_t(matrix, &m, "matrix")) { + if (igraphmodule_PyObject_to_matrix_t(matrix_o, &m, "matrix")) { return NULL; } @@ -2003,7 +2064,7 @@ PyObject *igraphmodule_Graph_Atlas(PyTypeObject * type, PyObject * args) } /** \ingroup python_interface_graph - * \brief Generates a graph based on the Barabasi-Albert model + * \brief Generates a graph based on the Barabási-Albert model * This is intended to be a class method in Python, so the first argument * is the type object and not the Python igraph object (because we have * to allocate that in this method). @@ -2018,8 +2079,9 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, igraph_t g; Py_ssize_t n; float power = 1.0f, zero_appeal = 1.0f; - igraph_integer_t m = 1; + igraph_int_t m = 1; igraph_vector_int_t outseq; + igraph_bool_t has_outseq = false; igraph_t *start_from = 0; igraph_barabasi_algorithm_t algo = IGRAPH_BARABASI_PSUMTREE; PyObject *m_obj = 0, *outpref = Py_False, *directed = Py_False; @@ -2045,36 +2107,36 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, CHECK_SSIZE_T_RANGE(n, "vertex count"); if (m_obj == 0) { - igraph_vector_int_init(&outseq, 0); m = 1; - } else if (m_obj != 0) { - /* let's check whether we have a constant out-degree or a list */ - if (PyLong_Check(m_obj)) { - if (igraphmodule_PyObject_to_integer_t(m_obj, &m)) { - return NULL; - } - igraph_vector_int_init(&outseq, 0); - } else if (PyList_Check(m_obj)) { - if (igraphmodule_PyObject_to_vector_int_t(m_obj, &outseq)) { - return NULL; - } - } else { - PyErr_SetString(PyExc_TypeError, "m must be an integer or a list of integers"); + } else if (PyLong_Check(m_obj)) { + if (igraphmodule_PyObject_to_integer_t(m_obj, &m)) { return NULL; } + } else if (PySequence_Check(m_obj)) { + if (igraphmodule_PyObject_to_vector_int_t(m_obj, &outseq)) { + return NULL; + } + has_outseq = true; + } else { + PyErr_SetString(PyExc_TypeError, "m must be an integer or a sequence of integers"); + return NULL; } if (igraph_barabasi_game(&g, n, power, m, - &outseq, PyObject_IsTrue(outpref), + has_outseq ? &outseq : NULL, PyObject_IsTrue(outpref), zero_appeal, PyObject_IsTrue(directed), algo, start_from)) { igraphmodule_handle_igraph_error(); - igraph_vector_int_destroy(&outseq); + if (has_outseq) { + igraph_vector_int_destroy(&outseq); + } return NULL; } - igraph_vector_int_destroy(&outseq); + if (has_outseq) { + igraph_vector_int_destroy(&outseq); + } CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -2129,6 +2191,57 @@ PyObject *igraphmodule_Graph_Bipartite(PyTypeObject * type, return (PyObject *) self; } +/** \ingroup python_interface_graph + * \brief Generates a Chung-Lu random graph + * This is intended to be a class method in Python, so the first argument + * is the type object and not the Python igraph object (because we have + * to allocate that in this method). + * + * \return a reference to the newly generated Python igraph object + * \sa igraph_chung_lu_game + */ +PyObject *igraphmodule_Graph_Chung_Lu(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + igraphmodule_GraphObject *self; + igraph_t g; + igraph_vector_t outw, inw; + igraph_chung_lu_t var = IGRAPH_CHUNG_LU_ORIGINAL; + igraph_bool_t has_inw = false; + PyObject *weight_out = NULL, *weight_in = NULL, *loops = Py_True, *variant = NULL; + + static char *kwlist[] = { "out", "in_", "loops", "variant", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, + &weight_out, &weight_in, &loops, &variant)) + return NULL; + + if (igraphmodule_PyObject_to_chung_lu_t(variant, &var)) return NULL; + if (igraphmodule_PyObject_to_vector_t(weight_out, &outw, /* need_non_negative */ true)) return NULL; + if (weight_in) { + if (igraphmodule_PyObject_to_vector_t(weight_in, &inw, /* need_non_negative */ true)) { + igraph_vector_destroy(&outw); + return NULL; + } + has_inw=true; + } + + if (igraph_chung_lu_game(&g, &outw, has_inw ? &inw : NULL, PyObject_IsTrue(loops), var)) { + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(&outw); + if (has_inw) + igraph_vector_destroy(&inw); + return NULL; + } + + igraph_vector_destroy(&outw); + if (has_inw) + igraph_vector_destroy(&inw); + + CREATE_GRAPH_FROM_TYPE(self, g, type); + + return (PyObject *) self; +} + /** \ingroup python_interface_graph * \brief Generates a De Bruijn graph * \sa igraph_kautz @@ -2220,15 +2333,16 @@ PyObject *igraphmodule_Graph_Erdos_Renyi(PyTypeObject * type, igraph_t g; Py_ssize_t n, m = -1; double p = -1.0; - PyObject *loops = Py_False, *directed = Py_False; + PyObject *loops = Py_False, *directed = Py_False, *edge_labeled = Py_False; int retval; - static char *kwlist[] = { "n", "p", "m", "directed", "loops", NULL }; + static char *kwlist[] = { "n", "p", "m", "directed", "loops", "edge_labeled", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|dnOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|dnOOO", kwlist, &n, &p, &m, &directed, - &loops)) + &loops, + &edge_labeled)) return NULL; CHECK_SSIZE_T_RANGE(n, "vertex count"); @@ -2247,10 +2361,18 @@ PyObject *igraphmodule_Graph_Erdos_Renyi(PyTypeObject * type, if (m == -1) { /* GNP model */ - retval = igraph_erdos_renyi_game_gnp(&g, n, p, PyObject_IsTrue(directed), PyObject_IsTrue(loops)); + retval = igraph_erdos_renyi_game_gnp( + &g, n, p, PyObject_IsTrue(directed), + PyObject_IsTrue(loops) ? IGRAPH_LOOPS_SW : IGRAPH_SIMPLE_SW, + PyObject_IsTrue(edge_labeled) + ); } else { /* GNM model */ - retval = igraph_erdos_renyi_game_gnm(&g, n, m, PyObject_IsTrue(directed), PyObject_IsTrue(loops)); + retval = igraph_erdos_renyi_game_gnm( + &g, n, m, PyObject_IsTrue(directed), + PyObject_IsTrue(loops) ? IGRAPH_LOOPS_SW : IGRAPH_SIMPLE_SW, + PyObject_IsTrue(edge_labeled) + ); } if (retval) { @@ -2281,9 +2403,8 @@ PyObject *igraphmodule_Graph_Establishment(PyTypeObject * type, char *kwlist[] = { "n", "k", "type_dist", "pref_matrix", "directed", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nnO!O!|O", kwlist, - &n, &k, &PyList_Type, &type_dist, - &PyList_Type, &pref_matrix, &directed)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nnOO|O", kwlist, + &n, &k, &type_dist, &pref_matrix, &directed)) return NULL; if (n <= 0 || k <= 0) { @@ -2295,29 +2416,32 @@ PyObject *igraphmodule_Graph_Establishment(PyTypeObject * type, CHECK_SSIZE_T_RANGE(n, "vertex count"); CHECK_SSIZE_T_RANGE(k, "connection trials per set"); - types = PyList_Size(type_dist); + if (igraphmodule_PyObject_to_vector_t(type_dist, &td, 1)) { + PyErr_SetString(PyExc_ValueError, + "Error while converting type distribution vector"); + return NULL; + } - if (igraphmodule_PyList_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { + if (igraphmodule_PyObject_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { + igraph_vector_destroy(&td); return NULL; } + + types = igraph_vector_size(&td); + if (igraph_matrix_nrow(&pm) != igraph_matrix_ncol(&pm) || igraph_matrix_nrow(&pm) != types) { PyErr_SetString(PyExc_ValueError, "Preference matrix must have exactly the same rows and columns as the number of types"); - igraph_matrix_destroy(&pm); - return NULL; - } - if (igraphmodule_PyObject_to_vector_t(type_dist, &td, 1)) { - PyErr_SetString(PyExc_ValueError, - "Error while converting type distribution vector"); + igraph_vector_destroy(&td); igraph_matrix_destroy(&pm); return NULL; } if (igraph_establishment_game(&g, n, types, k, &td, &pm, PyObject_IsTrue(directed), 0)) { igraphmodule_handle_igraph_error(); - igraph_matrix_destroy(&pm); igraph_vector_destroy(&td); + igraph_matrix_destroy(&pm); return NULL; } @@ -2642,6 +2766,41 @@ PyObject *igraphmodule_Graph_Hexagonal_Lattice(PyTypeObject * type, return (PyObject *) self; } + +/** \ingroup python_interface_graph + * \brief Generates hypercube graph + * \return a reference to the newly generated Python igraph object + * \sa igraph_hypercube + */ +PyObject *igraphmodule_Graph_Hypercube(PyTypeObject * type, + PyObject * args, PyObject * kwds) +{ + Py_ssize_t n; + igraph_bool_t directed; + PyObject *o_directed = Py_False; + igraphmodule_GraphObject *self; + igraph_t g; + + static char *kwlist[] = { "n", "directed", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|O", kwlist, &n, &o_directed)) { + return NULL; + } + + CHECK_SSIZE_T_RANGE(n, "vertex count"); + + directed = PyObject_IsTrue(o_directed); + + if (igraph_hypercube(&g, n, directed)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + CREATE_GRAPH_FROM_TYPE(self, g, type); + + return (PyObject *) self; +} + /** \ingroup python_interface_graph * \brief Generates a bipartite graph from a bipartite adjacency matrix * \return a reference to the newly generated Python igraph object @@ -2659,7 +2818,7 @@ PyObject *igraphmodule_Graph_Biadjacency(PyTypeObject * type, static char *kwlist[] = { "matrix", "directed", "mode", "multiple", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOO", kwlist, &PyList_Type, &matrix_o, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, &matrix_o, &directed, &mode_o, &multiple)) return NULL; @@ -2670,7 +2829,7 @@ PyObject *igraphmodule_Graph_Biadjacency(PyTypeObject * type, return NULL; } - if (igraphmodule_PyList_to_matrix_t(matrix_o, &matrix, "matrix")) { + if (igraphmodule_PyObject_to_matrix_t(matrix_o, &matrix, "matrix")) { igraph_vector_bool_destroy(&vertex_types); return NULL; } @@ -2875,7 +3034,7 @@ PyObject *igraphmodule_Graph_LCF(PyTypeObject *type, if (igraphmodule_PyObject_to_vector_int_t(shifts_o, &shifts)) return NULL; - if (igraph_lcf_vector(&g, n, &shifts, repeats)) { + if (igraph_lcf(&g, n, &shifts, repeats)) { igraph_vector_int_destroy(&shifts); igraphmodule_handle_igraph_error(); return NULL; @@ -2952,6 +3111,61 @@ PyObject *igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, } +/** \ingroup python_interface_graph + * \brief Generates a graph with a specified degree sequence + * \return a reference to the newly generated Python igraph object + * \sa igraph_realize_bipartite_degree_sequence + */ +PyObject *igraphmodule_Graph_Realize_Bipartite_Degree_Sequence(PyTypeObject *type, + PyObject *args, PyObject *kwds) { + + igraph_vector_int_t degrees1, degrees2; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; + igraph_realize_degseq_t method = IGRAPH_REALIZE_DEGSEQ_SMALLEST; + PyObject *degrees1_o, *degrees2_o; + PyObject *edge_types_o = Py_None, *method_o = Py_None; + igraphmodule_GraphObject *self; + igraph_t g; + + static char *kwlist[] = { "degrees1", "degrees2", "allowed_edge_types", "method", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OO", kwlist, + °rees1_o, °rees2_o, &edge_types_o, &method_o)) + return NULL; + + /* allowed edge types */ + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; + + /* methods */ + if (igraphmodule_PyObject_to_realize_degseq_t(method_o, &method)) + return NULL; + + /* First degree vector */ + if (igraphmodule_PyObject_to_vector_int_t(degrees1_o, °rees1)) + return NULL; + + /* Second degree vector */ + if (igraphmodule_PyObject_to_vector_int_t(degrees2_o, °rees2)) { + igraph_vector_int_destroy(°rees1); + return NULL; + } + + if (igraph_realize_bipartite_degree_sequence(&g, °rees1, °rees2, allowed_edge_types, method)) { + igraph_vector_int_destroy(°rees1); + igraph_vector_int_destroy(°rees2); + igraphmodule_handle_igraph_error(); + return NULL; + } + + igraph_vector_int_destroy(°rees1); + igraph_vector_int_destroy(°rees2); + + CREATE_GRAPH_FROM_TYPE(self, g, type); + + return (PyObject *) self; +} + + /** \ingroup python_interface_graph * \brief Generates a graph based on vertex types and connection preferences * \return a reference to the newly generated Python igraph object @@ -2977,16 +3191,15 @@ PyObject *igraphmodule_Graph_Preference(PyTypeObject * type, { "n", "type_dist", "pref_matrix", "attribute", "directed", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nO!O!|OOO", kwlist, - &n, &PyList_Type, &type_dist, - &PyList_Type, &pref_matrix, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nOO|OOO", kwlist, + &n, &type_dist, &pref_matrix, &attribute_key, &directed, &loops)) return NULL; CHECK_SSIZE_T_RANGE(n, "vertex count"); types = PyList_Size(type_dist); - if (igraphmodule_PyList_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { + if (igraphmodule_PyObject_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { return NULL; } if (igraphmodule_PyObject_float_to_vector_t(type_dist, &td)) { @@ -3070,18 +3283,18 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, char *kwlist[] = { "n", "type_dist_matrix", "pref_matrix", "attribute", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nO!O!|OO", kwlist, - &n, &PyList_Type, &type_dist_matrix, - &PyList_Type, &pref_matrix, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nOO|OO", kwlist, + &n, &type_dist_matrix, + &pref_matrix, &attribute_key, &loops)) return NULL; CHECK_SSIZE_T_RANGE(n, "vertex count"); - if (igraphmodule_PyList_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { + if (igraphmodule_PyObject_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { return NULL; } - if (igraphmodule_PyList_to_matrix_t(type_dist_matrix, &td, "type_dist_matrix")) { + if (igraphmodule_PyObject_to_matrix_t(type_dist_matrix, &td, "type_dist_matrix")) { igraph_matrix_destroy(&pm); return NULL; } @@ -3154,6 +3367,43 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, return (PyObject *) self; } + +/** \ingroup python_interface_graph + * \brief Generates a tree graph based on a Prufer sequence + * \return a reference to the newly generated Python igraph object + * \sa igraph_from_prufer + */ +PyObject *igraphmodule_Graph_Prufer( + PyTypeObject * type, PyObject * args, PyObject * kwds +) { + igraphmodule_GraphObject *self; + igraph_t g; + PyObject *seq_o; + igraph_vector_int_t seq; + + static char *kwlist[] = { "seq", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &seq_o)) { + return NULL; + } + + if (igraphmodule_PyObject_to_vector_int_t(seq_o, &seq)) { + return NULL; + } + + if (igraph_from_prufer(&g, &seq)) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&seq); + return NULL; + } + + CREATE_GRAPH_FROM_TYPE(self, g, type); + + igraph_vector_int_destroy(&seq); + + return (PyObject *) self; +} + /** \ingroup python_interface_graph * \brief Generates a bipartite graph based on the Erdos-Renyi model * \return a reference to the newly generated Python igraph object @@ -3167,15 +3417,20 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, Py_ssize_t n1, n2, m = -1; double p = -1.0; igraph_neimode_t neimode = IGRAPH_ALL; - PyObject *directed_o = Py_False, *neimode_o = NULL; + PyObject *directed_o = Py_False, *neimode_o = NULL, *edge_labeled_o = Py_False; + PyObject *edge_types_o = Py_None; igraph_vector_bool_t vertex_types; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; PyObject *vertex_types_o; igraph_error_t retval; - static char *kwlist[] = { "n1", "n2", "p", "m", "directed", "neimode", NULL }; + static char *kwlist[] = { + "n1", "n2", "p", "m", "directed", "neimode", "allowed_edge_types", "edge_labeled", NULL + }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|dnOO", kwlist, - &n1, &n2, &p, &m, &directed_o, &neimode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|dnOOOO", kwlist, + &n1, &n2, &p, &m, &directed_o, &neimode_o, + &edge_types_o, &edge_labeled_o)) return NULL; CHECK_SSIZE_T_RANGE(n1, "number of vertices in first partition"); @@ -3195,6 +3450,9 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, if (igraphmodule_PyObject_to_neimode_t(neimode_o, &neimode)) return NULL; + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; + if (igraph_vector_bool_init(&vertex_types, n1+n2)) { igraphmodule_handle_igraph_error(); return NULL; @@ -3203,12 +3461,14 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, if (m == -1) { /* GNP model */ retval = igraph_bipartite_game_gnp( - &g, &vertex_types, n1, n2, p, PyObject_IsTrue(directed_o), neimode + &g, &vertex_types, n1, n2, p, PyObject_IsTrue(directed_o), neimode, + allowed_edge_types, PyObject_IsTrue(edge_labeled_o) ); } else { /* GNM model */ retval = igraph_bipartite_game_gnm( - &g, &vertex_types, n1, n2, m, PyObject_IsTrue(directed_o), neimode + &g, &vertex_types, n1, n2, m, PyObject_IsTrue(directed_o), neimode, + allowed_edge_types, PyObject_IsTrue(edge_labeled_o) ); } @@ -3231,7 +3491,7 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, } /** \ingroup python_interface_graph - * \brief Generates a graph based on sort of a "windowed" Barabasi-Albert model + * \brief Generates a graph based on sort of a "windowed" Barabási-Albert model * \return a reference to the newly generated Python igraph object * \sa igraph_recent_degree_game */ @@ -3242,8 +3502,9 @@ PyObject *igraphmodule_Graph_Recent_Degree(PyTypeObject * type, igraph_t g; Py_ssize_t n, window = 0; float power = 0.0f, zero_appeal = 0.0f; - igraph_integer_t m = 0; + igraph_int_t m = 0; igraph_vector_int_t outseq; + igraph_bool_t has_outseq = false; PyObject *m_obj, *outpref = Py_False, *directed = Py_False; char *kwlist[] = @@ -3263,24 +3524,28 @@ NULL }; if (igraphmodule_PyObject_to_integer_t(m_obj, &m)) { return NULL; } - igraph_vector_int_init(&outseq, 0); } else if (PyList_Check(m_obj)) { if (igraphmodule_PyObject_to_vector_int_t(m_obj, &outseq)) { // something bad happened during conversion return NULL; } + has_outseq = true; } - if (igraph_recent_degree_game(&g, n, power, window, m, &outseq, + if (igraph_recent_degree_game(&g, n, power, window, m, has_outseq ? &outseq : NULL, PyObject_IsTrue(outpref), zero_appeal, PyObject_IsTrue(directed))) { igraphmodule_handle_igraph_error(); - igraph_vector_int_destroy(&outseq); + if (has_outseq) { + igraph_vector_int_destroy(&outseq); + } return NULL; } - igraph_vector_int_destroy(&outseq); + if (has_outseq) { + igraph_vector_int_destroy(&outseq); + } CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -3328,25 +3593,24 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - Py_ssize_t n; - PyObject *block_sizes_o, *pref_matrix_o; + PyObject *block_sizes_o, *pref_matrix_o, *edge_types_o = Py_None; PyObject *directed_o = Py_False; - PyObject *loops_o = Py_False; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; igraph_matrix_t pref_matrix; igraph_vector_int_t block_sizes; - static char *kwlist[] = { "n", "pref_matrix", "block_sizes", "directed", - "loops", NULL }; + static char *kwlist[] = { "pref_matrix", "block_sizes", "directed", "allowed_edge_types", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nO!O!|OO", kwlist, - &n, &PyList_Type, &pref_matrix_o, - &PyList_Type, &block_sizes_o, - &directed_o, &loops_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OO", kwlist, + &pref_matrix_o, + &block_sizes_o, + &directed_o, &edge_types_o)) return NULL; - CHECK_SSIZE_T_RANGE(n, "vertex count"); + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; - if (igraphmodule_PyList_to_matrix_t(pref_matrix_o, &pref_matrix, "pref_matrix")) { + if (igraphmodule_PyObject_to_matrix_t(pref_matrix_o, &pref_matrix, "pref_matrix")) { return NULL; } @@ -3355,7 +3619,7 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, return NULL; } - if (igraph_sbm_game(&g, n, &pref_matrix, &block_sizes, PyObject_IsTrue(directed_o), PyObject_IsTrue(loops_o))) { + if (igraph_sbm_game(&g, &pref_matrix, &block_sizes, PyObject_IsTrue(directed_o), allowed_edge_types)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&pref_matrix); igraph_vector_int_destroy(&block_sizes); @@ -3424,15 +3688,16 @@ PyObject *igraphmodule_Graph_Static_Fitness(PyTypeObject *type, Py_ssize_t m; PyObject *fitness_out_o = Py_None, *fitness_in_o = Py_None; PyObject *fitness_o = Py_None; - PyObject *multiple = Py_False, *loops = Py_False; + PyObject *edge_types_o = Py_None; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; igraph_vector_t fitness_out, fitness_in; static char *kwlist[] = { "m", "fitness_out", "fitness_in", - "loops", "multiple", "fitness", NULL }; + "allowed_edge_types", "fitness", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|OOOOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|OOOO", kwlist, &m, &fitness_out_o, &fitness_in_o, - &loops, &multiple, &fitness_o)) + &edge_types_o, &fitness_o)) return NULL; CHECK_SSIZE_T_RANGE(m, "edge count"); @@ -3448,6 +3713,9 @@ PyObject *igraphmodule_Graph_Static_Fitness(PyTypeObject *type, return NULL; } + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; + if (igraphmodule_PyObject_float_to_vector_t(fitness_out_o, &fitness_out)) return NULL; @@ -3460,7 +3728,7 @@ PyObject *igraphmodule_Graph_Static_Fitness(PyTypeObject *type, if (igraph_static_fitness_game(&g, m, &fitness_out, fitness_in_o == Py_None ? 0 : &fitness_in, - PyObject_IsTrue(loops), PyObject_IsTrue(multiple))) { + allowed_edge_types)) { igraph_vector_destroy(&fitness_out); if (fitness_in_o != Py_None) igraph_vector_destroy(&fitness_in); @@ -3489,21 +3757,25 @@ PyObject *igraphmodule_Graph_Static_Power_Law(PyTypeObject *type, igraph_t g; Py_ssize_t n, m; float exponent_out = -1.0f, exponent_in = -1.0f, exponent = -1.0f; - PyObject *multiple = Py_False, *loops = Py_False; + PyObject *edge_types_o = Py_None; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; PyObject *finite_size_correction = Py_True; static char *kwlist[] = { "n", "m", "exponent_out", "exponent_in", - "loops", "multiple", "finite_size_correction", "exponent", NULL }; + "allowed_edge_types", "finite_size_correction", "exponent", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|ffOOOf", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|ffOOf", kwlist, &n, &m, &exponent_out, &exponent_in, - &loops, &multiple, &finite_size_correction, + &edge_types_o, &finite_size_correction, &exponent)) return NULL; CHECK_SSIZE_T_RANGE(n, "vertex count"); CHECK_SSIZE_T_RANGE(m, "edge count"); + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; + /* This trickery allows us to use "exponent" or "exponent_out" as * keyword argument, with "exponent_out" taking precedence over * "exponent" */ @@ -3516,7 +3788,7 @@ PyObject *igraphmodule_Graph_Static_Power_Law(PyTypeObject *type, } if (igraph_static_power_law_game(&g, n, m, exponent_out, exponent_in, - PyObject_IsTrue(loops), PyObject_IsTrue(multiple), + allowed_edge_types, PyObject_IsTrue(finite_size_correction))) { igraphmodule_handle_igraph_error(); return NULL; @@ -3577,7 +3849,7 @@ PyObject *igraphmodule_Graph_Tree(PyTypeObject * type, * - directed is a bool that specifies if the edges should be directed. If so, they * point away from the root. * - method is one of: - * - 'Prufer' aka sample Pruefer sequences and convert to trees. + * - 'prufer' aka sample Prüfer sequences and convert to trees. * - 'lerw' aka loop-erased random walk on the complete graph to sample spanning * trees. * @@ -3666,22 +3938,25 @@ PyObject *igraphmodule_Graph_Watts_Strogatz(PyTypeObject * type, { Py_ssize_t dim, size, nei; double p; - PyObject* loops = Py_False; - PyObject* multiple = Py_False; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; + PyObject* edge_types_o = Py_None; igraphmodule_GraphObject *self; igraph_t g; - static char *kwlist[] = { "dim", "size", "nei", "p", "loops", "multiple", NULL }; + static char *kwlist[] = { "dim", "size", "nei", "p", "allowed_edge_types", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nnnd|OO", kwlist, - &dim, &size, &nei, &p, &loops, &multiple)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nnnd|O", kwlist, + &dim, &size, &nei, &p, &edge_types_o)) return NULL; CHECK_SSIZE_T_RANGE(dim, "dimensionality"); CHECK_SSIZE_T_RANGE(size, "size"); CHECK_SSIZE_T_RANGE(nei, "number of neighbors"); - if (igraph_watts_strogatz_game(&g, dim, size, nei, p, PyObject_IsTrue(loops), PyObject_IsTrue(multiple))) { + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; + + if (igraph_watts_strogatz_game(&g, dim, size, nei, p, allowed_edge_types)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -3709,8 +3984,8 @@ PyObject *igraphmodule_Graph_Weighted_Adjacency(PyTypeObject * type, static char *kwlist[] = { "matrix", "mode", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OO", kwlist, - &PyList_Type, &matrix, &mode_o, &loops_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, + &matrix, &mode_o, &loops_o)) return NULL; if (igraphmodule_PyObject_to_adjacency_t(mode_o, &mode)) @@ -3723,7 +3998,7 @@ PyObject *igraphmodule_Graph_Weighted_Adjacency(PyTypeObject * type, } else if (igraphmodule_PyObject_to_loops_t(loops_o, &loops)) return NULL; - if (igraphmodule_PyList_to_matrix_t(matrix, &m, "matrix")) { + if (igraphmodule_PyObject_to_matrix_t(matrix, &m, "matrix")) { return NULL; } @@ -3794,25 +4069,35 @@ PyObject *igraphmodule_Graph_articulation_points(igraphmodule_GraphObject* self, */ PyObject *igraphmodule_Graph_assortativity_nominal(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = { "types", "directed", "normalized", NULL }; - PyObject *types_o = Py_None, *directed = Py_True, *normalized = Py_True; + static char *kwlist[] = { "types", "directed", "normalized", "weights", NULL }; + PyObject *types_o = Py_None, *weights_o = Py_None, *directed = Py_True, *normalized = Py_True; igraph_real_t res; igraph_error_t ret; igraph_vector_int_t *types = 0; + igraph_vector_t *weights = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, &types_o, &directed, &normalized)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, &types_o, &directed, &normalized, &weights_o)) return NULL; if (igraphmodule_attrib_to_vector_int_t(types_o, self, &types, ATTRIBUTE_TYPE_VERTEX)) return NULL; - ret = igraph_assortativity_nominal(&self->g, types, &res, PyObject_IsTrue(directed), + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + if (types) { igraph_vector_int_destroy(types); free(types); } + return NULL; + } + + ret = igraph_assortativity_nominal(&self->g, weights, types, &res, PyObject_IsTrue(directed), PyObject_IsTrue(normalized)); if (types) { igraph_vector_int_destroy(types); free(types); } + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + if (ret) { igraphmodule_handle_igraph_error(); return NULL; @@ -3827,26 +4112,36 @@ PyObject *igraphmodule_Graph_assortativity_nominal(igraphmodule_GraphObject *sel */ PyObject *igraphmodule_Graph_assortativity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = { "types1", "types2", "directed", "normalized", NULL }; + static char *kwlist[] = { "types1", "types2", "directed", "normalized", "weights", NULL }; PyObject *types1_o = Py_None, *types2_o = Py_None, *directed = Py_True, *normalized = Py_True; + PyObject *weights_o = Py_None; igraph_real_t res; igraph_error_t ret; - igraph_vector_t *types1 = 0, *types2 = 0; + igraph_vector_t *types1 = 0, *types2 = 0, *weights = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, &types1_o, &types2_o, &directed, &normalized)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOOO", kwlist, &types1_o, &types2_o, &directed, &normalized, &weights_o)) return NULL; - if (igraphmodule_attrib_to_vector_t(types1_o, self, &types1, ATTRIBUTE_TYPE_VERTEX)) + if (igraphmodule_attrib_to_vector_t(types1_o, self, &types1, ATTRIBUTE_TYPE_VERTEX)) { return NULL; + } + if (igraphmodule_attrib_to_vector_t(types2_o, self, &types2, ATTRIBUTE_TYPE_VERTEX)) { if (types1) { igraph_vector_destroy(types1); free(types1); } return NULL; } - ret = igraph_assortativity(&self->g, types1, types2, &res, PyObject_IsTrue(directed), PyObject_IsTrue(normalized)); + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + if (types1) { igraph_vector_destroy(types1); free(types1); } + if (types2) { igraph_vector_destroy(types2); free(types2); } + return NULL; + } + + ret = igraph_assortativity(&self->g, weights, types1, types2, &res, PyObject_IsTrue(directed), PyObject_IsTrue(normalized)); if (types1) { igraph_vector_destroy(types1); free(types1); } if (types2) { igraph_vector_destroy(types2); free(types2); } + if (weights) { igraph_vector_destroy(weights); free(weights); } if (ret) { igraphmodule_handle_igraph_error(); @@ -3893,6 +4188,8 @@ PyObject *igraphmodule_Graph_authority_score( igraph_real_t value; igraph_vector_t res, *weights = 0; + /* scale is deprecated but kept for backward compatibility reasons */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO!O", kwlist, &weights_o, &scale_o, igraphmodule_ARPACKOptionsType, &arpack_options_o, &return_eigenvalue)) @@ -3904,7 +4201,7 @@ PyObject *igraphmodule_Graph_authority_score( ATTRIBUTE_TYPE_EDGE)) return NULL; arpack_options = (igraphmodule_ARPACKOptionsObject*)arpack_options_o; - if (igraph_hub_and_authority_scores(&self->g, NULL, &res, &value, PyObject_IsTrue(scale_o), + if (igraph_hub_and_authority_scores(&self->g, NULL, &res, &value, weights, igraphmodule_ARPACKOptions_get(arpack_options))) { igraphmodule_handle_igraph_error(); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -3952,19 +4249,16 @@ PyObject *igraphmodule_Graph_average_path_length(igraphmodule_GraphObject * if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) return NULL; - if (weights) { - if (igraph_average_path_length_dijkstra(&self->g, &res, 0, weights, PyObject_IsTrue(directed), PyObject_IsTrue(unconn))) { + if (igraph_average_path_length(&self->g, weights, &res, 0, PyObject_IsTrue(directed), PyObject_IsTrue(unconn))) { + if (weights) { igraph_vector_destroy(weights); free(weights); - igraphmodule_handle_igraph_error(); - return NULL; } + igraphmodule_handle_igraph_error(); + return NULL; + } + if (weights) { igraph_vector_destroy(weights); free(weights); - } else { - if (igraph_average_path_length(&self->g, &res, 0, PyObject_IsTrue(directed), PyObject_IsTrue(unconn))) { - igraphmodule_handle_igraph_error(); - return NULL; - } } return PyFloat_FromDouble(res); @@ -3979,8 +4273,9 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "vertices", "directed", "cutoff", "weights", - "sources", "targets", NULL }; + "sources", "targets", "normalized", NULL }; PyObject *directed = Py_True; + PyObject *normalized = Py_False; PyObject *vobj = Py_None, *list; PyObject *cutoff = Py_None; PyObject *weights_o = Py_None; @@ -3992,9 +4287,9 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, igraph_vs_t vs, sources, targets; igraph_error_t retval; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOO", kwlist, &vobj, &directed, &cutoff, &weights_o, - &sources_o, &targets_o)) { + &sources_o, &targets_o, &normalized)) { return NULL; } @@ -4035,10 +4330,10 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, if (cutoff == Py_None) { if (is_subsetted) { retval = igraph_betweenness_subset( - &self->g, &res, vs, PyObject_IsTrue(directed), sources, targets, weights + &self->g, weights, &res, sources, targets, vs, PyObject_IsTrue(directed), PyObject_IsTrue(normalized) ); } else { - retval = igraph_betweenness(&self->g, &res, vs, PyObject_IsTrue(directed), weights); + retval = igraph_betweenness(&self->g, weights, &res, vs, PyObject_IsTrue(directed), PyObject_IsTrue(normalized)); } if (retval) { @@ -4072,8 +4367,8 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, return NULL; } - if (igraph_betweenness_cutoff(&self->g, &res, vs, PyObject_IsTrue(directed), - weights, PyFloat_AsDouble(cutoff_num))) { + if (igraph_betweenness_cutoff(&self->g, weights, &res, vs, PyObject_IsTrue(directed), + PyObject_IsTrue(normalized), PyFloat_AsDouble(cutoff_num))) { igraph_vs_destroy(&vs); igraph_vs_destroy(&targets); igraph_vs_destroy(&sources); @@ -4161,7 +4456,7 @@ PyObject *igraphmodule_Graph_biconnected_components(igraphmodule_GraphObject *se igraph_vector_int_list_t components; igraph_vector_int_t points; igraph_bool_t return_articulation_points; - igraph_integer_t no; + igraph_int_t no; PyObject *result_o, *aps=Py_False; static char* kwlist[] = {"return_articulation_points", NULL}; @@ -4340,7 +4635,7 @@ PyObject *igraphmodule_Graph_bipartite_projection_size(igraphmodule_GraphObject PyObject* args, PyObject* kwds) { PyObject *types_o = Py_None; igraph_vector_bool_t* types = 0; - igraph_integer_t vcount1, vcount2, ecount1, ecount2; + igraph_int_t vcount1, vcount2, ecount1, ecount2; static char* kwlist[] = {"types", NULL}; @@ -4602,7 +4897,7 @@ PyObject *igraphmodule_Graph_harmonic_centrality(igraphmodule_GraphObject * self return NULL; } if (igraph_harmonic_centrality_cutoff(&self->g, &res, vs, mode, weights, - PyFloat_AsDouble(cutoff_num), PyObject_IsTrue(normalized_o))) { + PyObject_IsTrue(normalized_o), PyFloat_AsDouble(cutoff_num))) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -4637,7 +4932,7 @@ PyObject *igraphmodule_Graph_connected_components( static char *kwlist[] = { "mode", NULL }; igraph_connectedness_t mode = IGRAPH_STRONG; igraph_vector_int_t res1, res2; - igraph_integer_t no; + igraph_int_t no; PyObject *list, *mode_o = Py_None; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &mode_o)) @@ -4907,7 +5202,7 @@ PyObject *igraphmodule_Graph_eccentricity(igraphmodule_GraphObject* self, return NULL; } - if (igraph_eccentricity_dijkstra(&self->g, weights, &res, vs, mode)) { + if (igraph_eccentricity(&self->g, weights, &res, vs, mode)) { if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -4996,9 +5291,9 @@ PyObject *igraphmodule_Graph_edge_betweenness(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "directed", "cutoff", "weights", "sources", "targets", NULL }; + static char *kwlist[] = { "directed", "cutoff", "weights", "sources", "targets", "normalized", NULL }; igraph_vector_t res, *weights = 0; - PyObject *list, *directed = Py_True, *cutoff = Py_None; + PyObject *list, *directed = Py_True, *cutoff = Py_None, *normalized = Py_False; PyObject *weights_o = Py_None; PyObject *sources_o = Py_None; PyObject *targets_o = Py_None; @@ -5007,8 +5302,8 @@ PyObject *igraphmodule_Graph_edge_betweenness(igraphmodule_GraphObject * self, igraph_error_t retval; igraph_bool_t is_subsetted = false; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOO", kwlist, - &directed, &cutoff, &weights_o, &sources_o, &targets_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOO", kwlist, + &directed, &cutoff, &weights_o, &sources_o, &targets_o, &normalized)) return NULL; if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, @@ -5039,12 +5334,14 @@ PyObject *igraphmodule_Graph_edge_betweenness(igraphmodule_GraphObject * self, if (cutoff == Py_None) { if (is_subsetted) { retval = igraph_edge_betweenness_subset( - &self->g, &res, igraph_ess_all(IGRAPH_EDGEORDER_ID), - PyObject_IsTrue(directed), sources, targets, weights + &self->g, weights, &res, + sources, targets, igraph_ess_all(IGRAPH_EDGEORDER_ID), + PyObject_IsTrue(directed), PyObject_IsTrue(normalized) ); } else { retval = igraph_edge_betweenness( - &self->g, &res, PyObject_IsTrue(directed), weights + &self->g, weights, &res, igraph_ess_all(IGRAPH_EDGEORDER_ID), + PyObject_IsTrue(directed), PyObject_IsTrue(normalized) ); } if (retval) { @@ -5074,8 +5371,10 @@ PyObject *igraphmodule_Graph_edge_betweenness(igraphmodule_GraphObject * self, return NULL; } - if (igraph_edge_betweenness_cutoff(&self->g, &res, PyObject_IsTrue(directed), - weights, PyFloat_AsDouble(cutoff_num))) { + if (igraph_edge_betweenness_cutoff( + &self->g, weights, &res, igraph_ess_all(IGRAPH_EDGEORDER_ID), + PyObject_IsTrue(directed), PyObject_IsTrue(normalized), PyFloat_AsDouble(cutoff_num) + )) { igraph_vector_destroy(&res); igraph_vs_destroy(&targets); igraph_vs_destroy(&sources); @@ -5110,8 +5409,8 @@ PyObject *igraphmodule_Graph_edge_connectivity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "source", "target", "checks", NULL }; PyObject *checks = Py_True, *source_o = Py_None, *target_o = Py_None; - igraph_integer_t source = -1, target = -1; - igraph_integer_t res; + igraph_int_t source = -1, target = -1; + igraph_int_t res; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &source_o, &target_o, &checks)) return NULL; @@ -5161,6 +5460,8 @@ PyObject *igraphmodule_Graph_eigenvector_centrality( igraph_real_t value; igraph_vector_t *weights=0, res; + /* scale is deprecated but kept for backward compatibility reasons */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO!O", kwlist, &directed_o, &scale_o, &weights_o, igraphmodule_ARPACKOptionsType, @@ -5177,7 +5478,7 @@ PyObject *igraphmodule_Graph_eigenvector_centrality( arpack_options = (igraphmodule_ARPACKOptionsObject*)arpack_options_o; if (igraph_eigenvector_centrality(&self->g, &res, &value, - PyObject_IsTrue(directed_o), PyObject_IsTrue(scale_o), + PyObject_IsTrue(directed_o), weights, igraphmodule_ARPACKOptions_get(arpack_options))) { igraphmodule_handle_igraph_error(); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -5208,29 +5509,71 @@ PyObject *igraphmodule_Graph_eigenvector_centrality( * \return a list containing the indices in the chosen feedback arc set * \sa igraph_feedback_arc_set */ -PyObject *igraphmodule_Graph_feedback_arc_set( +PyObject *igraphmodule_Graph_feedback_arc_set( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { + static char *kwlist[] = { "weights", "method", NULL }; + igraph_vector_t* weights = 0; + igraph_vector_int_t res; + igraph_fas_algorithm_t algo = IGRAPH_FAS_APPROX_EADES; + PyObject *weights_o = Py_None, *result_o = NULL, *algo_o = NULL; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &weights_o, &algo_o)) + return NULL; + + if (igraphmodule_PyObject_to_fas_algorithm_t(algo_o, &algo)) + return NULL; + + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, + ATTRIBUTE_TYPE_EDGE)) + return NULL; + + if (igraph_vector_int_init(&res, 0)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + } + + if (igraph_feedback_arc_set(&self->g, &res, weights, algo)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraph_vector_int_destroy(&res); + return NULL; + } + + if (weights) { igraph_vector_destroy(weights); free(weights); } + + result_o = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); + + return result_o; +} + + +/** \ingroup python_interface_graph + * \brief Calculates a feedback vertex set for a graph + * \return a list containing the indices in the chosen feedback vertex set + * \sa igraph_feedback_vertex_set + */ +PyObject *igraphmodule_Graph_feedback_vertex_set( igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "weights", "method", NULL }; igraph_vector_t* weights = 0; igraph_vector_int_t res; - igraph_fas_algorithm_t algo = IGRAPH_FAS_APPROX_EADES; + igraph_fvs_algorithm_t algo = IGRAPH_FVS_EXACT_IP; PyObject *weights_o = Py_None, *result_o = NULL, *algo_o = NULL; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &weights_o, &algo_o)) return NULL; - if (igraphmodule_PyObject_to_fas_algorithm_t(algo_o, &algo)) + if (igraphmodule_PyObject_to_fvs_algorithm_t(algo_o, &algo)) return NULL; if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) + ATTRIBUTE_TYPE_VERTEX)) return NULL; if (igraph_vector_int_init(&res, 0)) { if (weights) { igraph_vector_destroy(weights); free(weights); } } - if (igraph_feedback_arc_set(&self->g, &res, weights, algo)) { + if (igraph_feedback_vertex_set(&self->g, &res, weights, algo)) { if (weights) { igraph_vector_destroy(weights); free(weights); } igraph_vector_int_destroy(&res); return NULL; @@ -5256,7 +5599,7 @@ PyObject *igraphmodule_Graph_get_shortest_path( static char *kwlist[] = { "v", "to", "weights", "mode", "output", "algorithm", NULL }; igraph_vector_t *weights=0; igraph_neimode_t mode = IGRAPH_OUT; - igraph_integer_t from, to; + igraph_int_t from, to; PyObject *list, *mode_o=Py_None, *weights_o=Py_None, *output_o=Py_None, *from_o = Py_None, *to_o=Py_None, *algorithm_o=Py_None; @@ -5292,14 +5635,14 @@ PyObject *igraphmodule_Graph_get_shortest_path( return NULL; } - if (algorithm == IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO) { - algorithm = igraphmodule_select_shortest_path_algorithm( - &self->g, weights, NULL, mode, /* allow_johnson = */ false - ); - } - /* Call the C function */ switch (algorithm) { + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO: + retval = igraph_get_shortest_path( + &self->g, weights, use_edges ? NULL : &vec, use_edges ? &vec : NULL, from, to, mode + ); + break; + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA: retval = igraph_get_shortest_path_dijkstra( &self->g, use_edges ? NULL : &vec, use_edges ? &vec : NULL, from, to, weights, mode @@ -5340,7 +5683,7 @@ typedef struct { } igraphmodule_i_Graph_get_shortest_path_astar_callback_data_t; igraph_error_t igraphmodule_i_Graph_get_shortest_path_astar_callback( - igraph_real_t *result, igraph_integer_t from, igraph_integer_t to, + igraph_real_t *result, igraph_int_t from, igraph_int_t to, void *extra ) { igraphmodule_i_Graph_get_shortest_path_astar_callback_data_t* data = @@ -5358,6 +5701,7 @@ igraph_error_t igraphmodule_i_Graph_get_shortest_path_astar_callback( to_o = igraphmodule_integer_t_to_PyObject(to); if (to_o == NULL) { /* Error in conversion, return 1 */ + Py_DECREF(from_o); return IGRAPH_FAILURE; } @@ -5372,9 +5716,11 @@ igraph_error_t igraphmodule_i_Graph_get_shortest_path_astar_callback( if (igraphmodule_PyObject_to_real_t(result_o, result)) { /* Error in conversion, return 1 */ + Py_DECREF(result_o); return IGRAPH_FAILURE; } + Py_DECREF(result_o); return IGRAPH_SUCCESS; } @@ -5389,7 +5735,7 @@ PyObject *igraphmodule_Graph_get_shortest_path_astar( static char *kwlist[] = { "v", "to", "heuristics", "weights", "mode", "output", NULL }; igraph_vector_t *weights=0; igraph_neimode_t mode = IGRAPH_OUT; - igraph_integer_t from, to; + igraph_int_t from, to; PyObject *list, *mode_o=Py_None, *weights_o=Py_None, *output_o=Py_None, *from_o = Py_None, *to_o=Py_None, *heuristics_o; @@ -5460,7 +5806,7 @@ PyObject *igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject * igraph_vector_t *weights = NULL; igraph_neimode_t mode = IGRAPH_OUT; igraphmodule_shortest_path_algorithm_t algorithm = IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO; - igraph_integer_t from, no_of_target_nodes; + igraph_int_t from, no_of_target_nodes; igraph_vs_t to; PyObject *list, *mode_o=Py_None, *weights_o=Py_None, *output_o=Py_None, *from_o = Py_None, *to_o=Py_None, @@ -5511,14 +5857,15 @@ PyObject *igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject * return NULL; } - if (algorithm == IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO) { - algorithm = igraphmodule_select_shortest_path_algorithm( - &self->g, weights, NULL, mode, /* allow_johnson = */ false - ); - } - /* Call the C function */ switch (algorithm) { + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO: + retval = igraph_get_shortest_paths( + &self->g, weights, use_edges ? NULL : &veclist, use_edges ? &veclist : NULL, + from, to, mode, NULL, NULL + ); + break; + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA: retval = igraph_get_shortest_paths_dijkstra( &self->g, use_edges ? NULL : &veclist, use_edges ? &veclist : NULL, @@ -5569,7 +5916,7 @@ PyObject *igraphmodule_Graph_get_all_shortest_paths(igraphmodule_GraphObject * igraph_vector_int_list_t res; igraph_vector_t *weights = 0; igraph_neimode_t mode = IGRAPH_OUT; - igraph_integer_t from; + igraph_int_t from; igraph_vs_t to; PyObject *list, *from_o, *mode_o=Py_None, *to_o=Py_None, *weights_o=Py_None; @@ -5629,9 +5976,9 @@ PyObject *igraphmodule_Graph_get_k_shortest_paths( igraph_vector_int_list_t res; igraph_vector_t *weights = 0; igraph_neimode_t mode = IGRAPH_OUT; - igraph_integer_t from; - igraph_integer_t to; - igraph_integer_t k = 1; + igraph_int_t from; + igraph_int_t to; + igraph_int_t k = 1; PyObject *list, *from_o, *to_o; PyObject *output_o = Py_None, *mode_o = Py_None, *weights_o = Py_None, *k_o = NULL; igraph_bool_t use_edges = false; @@ -5697,22 +6044,26 @@ PyObject *igraphmodule_Graph_get_all_simple_paths(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "v", "to", "cutoff", "mode", NULL }; - igraph_vector_int_t res; + static char *kwlist[] = { "v", "to", "minlen", "maxlen", "mode", "max_results", NULL }; + igraph_vector_int_list_t res; igraph_neimode_t mode = IGRAPH_OUT; - igraph_integer_t from; + igraph_int_t from; igraph_vs_t to; - igraph_integer_t cutoff; - PyObject *list, *from_o, *mode_o=Py_None, *to_o=Py_None, *cutoff_o=Py_None; + igraph_int_t minlen, maxlen, max_results = IGRAPH_UNLIMITED; + PyObject *list, *from_o, *mode_o = Py_None, *to_o = Py_None, *max_results_o = Py_None; + PyObject *minlen_o = Py_None, *maxlen_o = Py_None; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, &from_o, - &to_o, &cutoff_o, &mode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOOOO", kwlist, &from_o, + &to_o, &minlen_o, &maxlen_o, &mode_o, &max_results_o)) return NULL; if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; - if (igraphmodule_PyObject_to_integer_t(cutoff_o, &cutoff)) + if (igraphmodule_PyObject_to_integer_t(minlen_o, &minlen)) + return NULL; + + if (igraphmodule_PyObject_to_integer_t(maxlen_o, &maxlen)) return NULL; if (igraphmodule_PyObject_to_vid(from_o, &from, &self->g)) @@ -5721,24 +6072,27 @@ PyObject *igraphmodule_Graph_get_all_simple_paths(igraphmodule_GraphObject * if (igraphmodule_PyObject_to_vs_t(to_o, &to, &self->g, 0, 0)) return NULL; - if (igraph_vector_int_init(&res, 0)) { + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) + return NULL; + + if (igraph_vector_int_list_init(&res, 0)) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&to); return NULL; } - if (igraph_get_all_simple_paths(&self->g, &res, from, to, cutoff, mode)) { + if (igraph_get_all_simple_paths(&self->g, &res, from, to, mode, minlen, maxlen, max_results)) { igraphmodule_handle_igraph_error(); - igraph_vector_int_destroy(&res); + igraph_vector_int_list_destroy(&res); igraph_vs_destroy(&to); return NULL; } igraph_vs_destroy(&to); - list = igraphmodule_vector_int_t_to_PyList(&res); + list = igraphmodule_vector_int_list_t_to_PyList(&res); - igraph_vector_int_destroy(&res); + igraph_vector_int_list_destroy(&res); return list; } @@ -5759,6 +6113,8 @@ PyObject *igraphmodule_Graph_hub_score( igraph_real_t value; igraph_vector_t res, *weights = 0; + /* scale is deprecated but kept for backward compatibility reasons */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO!O", kwlist, &weights_o, &scale_o, igraphmodule_ARPACKOptionsType, &arpack_options, &return_eigenvalue)) @@ -5770,7 +6126,7 @@ PyObject *igraphmodule_Graph_hub_score( ATTRIBUTE_TYPE_EDGE)) return NULL; arpack_options = (igraphmodule_ARPACKOptionsObject*)arpack_options_o; - if (igraph_hub_and_authority_scores(&self->g, &res, NULL, &value, PyObject_IsTrue(scale_o), + if (igraph_hub_and_authority_scores(&self->g, &res, NULL, &value, weights, igraphmodule_ARPACKOptions_get(arpack_options))) { igraphmodule_handle_igraph_error(); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -6127,11 +6483,11 @@ PyObject *igraphmodule_Graph_personalized_pagerank(igraphmodule_GraphObject *sel } if (rvsobj != Py_None) - retval = igraph_personalized_pagerank_vs(&self->g, algo, &res, 0, vs, - PyObject_IsTrue(directed), damping, reset_vs, &weights, opts); + retval = igraph_personalized_pagerank_vs(&self->g, &weights, &res, 0, reset_vs, + damping, PyObject_IsTrue(directed), vs, algo, opts); else - retval = igraph_personalized_pagerank(&self->g, algo, &res, 0, vs, - PyObject_IsTrue(directed), damping, reset, &weights, opts); + retval = igraph_personalized_pagerank(&self->g, &weights, &res, 0, reset, + damping, PyObject_IsTrue(directed), vs, algo, opts); if (retval) { igraphmodule_handle_igraph_error(); @@ -6197,7 +6553,7 @@ PyObject *igraphmodule_Graph_permute_vertices(igraphmodule_GraphObject *self, igraphmodule_GraphObject *result_o; PyObject *list; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!", kwlist, &PyList_Type, &list)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &list)) return NULL; if (igraphmodule_PyObject_to_vector_int_t(list, &perm)) @@ -6224,21 +6580,26 @@ PyObject *igraphmodule_Graph_permute_vertices(igraphmodule_GraphObject *self, PyObject *igraphmodule_Graph_rewire(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "n", "mode", NULL }; - Py_ssize_t n = 1000; - PyObject *mode_o = Py_None; - igraph_rewiring_t mode = IGRAPH_REWIRING_SIMPLE; + static char *kwlist[] = { "n", "allowed_edge_types", NULL }; + PyObject *n_o = Py_None, *allowed_edge_types_o = Py_None; + igraph_int_t n = 10 * igraph_ecount(&self->g); /* TODO overflow check */ + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nO", kwlist, &n, &mode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &n_o, &allowed_edge_types_o)) { return NULL; + } - CHECK_SSIZE_T_RANGE(n, "number of rewiring attempts"); + if (n_o != Py_None) { + if (igraphmodule_PyObject_to_integer_t(n_o, &n)) { + return NULL; + } + } - if (igraphmodule_PyObject_to_rewiring_t(mode_o, &mode)) { + if (igraphmodule_PyObject_to_edge_type_sw_t(allowed_edge_types_o, &allowed_edge_types)) { return NULL; } - if (igraph_rewire(&self->g, n, mode)) { + if (igraph_rewire(&self->g, n, allowed_edge_types, /* rewiring_stats = */ NULL)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -6254,16 +6615,20 @@ PyObject *igraphmodule_Graph_rewire(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_rewire_edges(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "prob", "loops", "multiple", NULL }; + static char *kwlist[] = { "prob", "allowed_edge_types", NULL }; double prob; - PyObject *loops_o = Py_False, *multiple_o = Py_False; + PyObject *edge_types_o = Py_None; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "d|O", kwlist, + &prob, &edge_types_o)) + return NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "d|OO", kwlist, - &prob, &loops_o, &multiple_o)) + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) { return NULL; + } - if (igraph_rewire_edges(&self->g, prob, PyObject_IsTrue(loops_o), - PyObject_IsTrue(multiple_o))) { + if (igraph_rewire_edges(&self->g, prob, allowed_edge_types)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -6326,19 +6691,12 @@ PyObject *igraphmodule_Graph_distances( return igraphmodule_handle_igraph_error(); } - if (algorithm == IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO) { - algorithm = igraphmodule_select_shortest_path_algorithm( - &self->g, weights, &from_vs, mode, /* allow_johnson = */ true - ); - } - - if (algorithm == IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_JOHNSON && mode != IGRAPH_OUT) { - PyErr_SetString(PyExc_ValueError, "Johnson's algorithm is supported for mode=\"out\" only"); - goto cleanup; - } - /* Call the C function */ switch (algorithm) { + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO: + retval = igraph_distances(&self->g, weights, &res, from_vs, to_vs, mode); + break; + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA: retval = igraph_distances_dijkstra(&self->g, &res, from_vs, to_vs, weights, mode); break; @@ -6348,7 +6706,7 @@ PyObject *igraphmodule_Graph_distances( break; case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_JOHNSON: - retval = igraph_distances_johnson(&self->g, &res, from_vs, to_vs, weights); + retval = igraph_distances_johnson(&self->g, &res, from_vs, to_vs, weights, mode); break; default: @@ -6405,25 +6763,35 @@ PyObject *igraphmodule_Graph_similarity_jaccard(igraphmodule_GraphObject * self, if (pairs_o == Py_None) { /* Case #1: vertices, returning matrix */ igraph_matrix_t res; - igraph_vs_t vs; + igraph_vs_t vs_from; + igraph_vs_t vs_to; igraph_bool_t return_single = false; - if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs, &self->g, &return_single, 0)) + if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs_from, &self->g, &return_single, 0)) + return NULL; + + /* TODO(ntamas): support separate vs_from and vs_to arguments */ + if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs_to, &self->g, &return_single, 0)) { + igraph_vs_destroy(&vs_from); return NULL; + } if (igraph_matrix_init(&res, 0, 0)) { - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); return igraphmodule_handle_igraph_error(); } - if (igraph_similarity_jaccard(&self->g, &res, vs, mode, PyObject_IsTrue(loops))) { + if (igraph_similarity_jaccard(&self->g, &res, vs_from, vs_to, mode, PyObject_IsTrue(loops))) { igraph_matrix_destroy(&res); - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); igraphmodule_handle_igraph_error(); return NULL; } - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); list = igraphmodule_matrix_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&res); @@ -6491,25 +6859,35 @@ PyObject *igraphmodule_Graph_similarity_dice(igraphmodule_GraphObject * self, if (pairs_o == Py_None) { /* Case #1: vertices, returning matrix */ igraph_matrix_t res; - igraph_vs_t vs; + igraph_vs_t vs_from; + igraph_vs_t vs_to; igraph_bool_t return_single = false; - if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs, &self->g, &return_single, 0)) + if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs_from, &self->g, &return_single, 0)) + return NULL; + + /* TODO(ntamas): support separate vs_from and vs_to arguments */ + if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs_to, &self->g, &return_single, 0)) { + igraph_vs_destroy(&vs_from); return NULL; + } if (igraph_matrix_init(&res, 0, 0)) { - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); return igraphmodule_handle_igraph_error(); } - if (igraph_similarity_dice(&self->g, &res, vs, mode, PyObject_IsTrue(loops))) { + if (igraph_similarity_dice(&self->g, &res, vs_from, vs_to, mode, PyObject_IsTrue(loops))) { igraph_matrix_destroy(&res); - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); igraphmodule_handle_igraph_error(); return NULL; } - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); list = igraphmodule_matrix_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&res); @@ -6602,12 +6980,16 @@ PyObject *igraphmodule_Graph_similarity_inverse_log_weighted( PyObject *igraphmodule_Graph_spanning_tree(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "weights", NULL }; + static char *kwlist[] = { "weights", "method", NULL }; igraph_vector_t* ws = 0; igraph_vector_int_t res; - PyObject *weights_o = Py_None, *result_o = NULL; + igraph_mst_algorithm_t method = IGRAPH_MST_AUTOMATIC; + PyObject *weights_o = Py_None, *result_o = NULL, *method_o = NULL; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &weights_o, &method_o)) + return NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &weights_o)) + if (igraphmodule_PyObject_to_mst_algorithm_t(method_o, &method)) return NULL; if (igraph_vector_int_init(&res, 0)) { @@ -6620,14 +7002,19 @@ PyObject *igraphmodule_Graph_spanning_tree(igraphmodule_GraphObject * self, return NULL; } - if (igraph_minimum_spanning_tree(&self->g, &res, ws)) { - if (ws != 0) { igraph_vector_destroy(ws); free(ws); } + if (igraph_minimum_spanning_tree(&self->g, &res, ws, method)) { + if (ws != 0) { + igraph_vector_destroy(ws); free(ws); + } igraph_vector_int_destroy(&res); igraphmodule_handle_igraph_error(); return NULL; } - if (ws != 0) { igraph_vector_destroy(ws); free(ws); } + if (ws != 0) { + igraph_vector_destroy(ws); free(ws); + } + result_o = igraphmodule_vector_int_t_to_PyList(&res); igraph_vector_int_destroy(&res); return result_o; @@ -6676,7 +7063,7 @@ PyObject *igraphmodule_Graph_subcomponent(igraphmodule_GraphObject * self, static char *kwlist[] = { "v", "mode", NULL }; igraph_vector_int_t res; igraph_neimode_t mode = IGRAPH_ALL; - igraph_integer_t from; + igraph_int_t from; PyObject *list = NULL, *mode_o = Py_None, *from_o = Py_None; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &from_o, &mode_o)) @@ -6954,7 +7341,7 @@ PyObject *igraphmodule_Graph_vertex_connectivity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "source", "target", "checks", "neighbors", NULL }; PyObject *checks = Py_True, *neis = Py_None, *source_o = Py_None, *target_o = Py_None; - igraph_integer_t source = -1, target = -1, res; + igraph_int_t source = -1, target = -1, res; igraph_vconn_nei_t neighbors = IGRAPH_VCONN_NEI_ERROR; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, @@ -7118,7 +7505,7 @@ typedef struct { } igraphmodule_i_Graph_motifs_randesu_callback_data_t; igraph_error_t igraphmodule_i_Graph_motifs_randesu_callback(const igraph_t *graph, - igraph_vector_int_t *vids, igraph_integer_t isoclass, void* extra) { + const igraph_vector_int_t *vids, igraph_int_t isoclass, void* extra) { igraphmodule_i_Graph_motifs_randesu_callback_data_t* data = (igraphmodule_i_Graph_motifs_randesu_callback_data_t*)extra; PyObject* vector; @@ -7221,7 +7608,7 @@ PyObject *igraphmodule_Graph_motifs_randesu(igraphmodule_GraphObject *self, PyObject *igraphmodule_Graph_motifs_randesu_no(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { igraph_vector_t cut_prob; - igraph_integer_t res; + igraph_real_t res; Py_ssize_t size = 3; PyObject* cut_prob_list=Py_None; static char* kwlist[] = {"size", "cut_prob", NULL}; @@ -7248,7 +7635,7 @@ PyObject *igraphmodule_Graph_motifs_randesu_no(igraphmodule_GraphObject *self, } igraph_vector_destroy(&cut_prob); - return igraphmodule_integer_t_to_PyObject(res); + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); } /** \ingroup python_interface_graph @@ -7259,7 +7646,7 @@ PyObject *igraphmodule_Graph_motifs_randesu_no(igraphmodule_GraphObject *self, PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { igraph_vector_t cut_prob; - igraph_integer_t res; + igraph_real_t res; Py_ssize_t size = 3; PyObject* cut_prob_list=Py_None; PyObject *sample=Py_None; @@ -7289,7 +7676,7 @@ PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *s if (PyLong_Check(sample)) { /* samples chosen randomly */ - igraph_integer_t ns; + igraph_int_t ns; if (igraphmodule_PyObject_to_integer_t(sample, &ns)) { igraph_vector_destroy(&cut_prob); return NULL; @@ -7317,7 +7704,7 @@ PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *s } igraph_vector_destroy(&cut_prob); - return igraphmodule_integer_t_to_PyObject(res); + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); } /** \ingroup python_interface_graph @@ -7383,13 +7770,15 @@ PyObject *igraphmodule_Graph_fundamental_cycles( ) { PyObject *cutoff_o = Py_None; PyObject *start_vid_o = Py_None; + PyObject *weights_o = Py_None; PyObject *result_o; - igraph_integer_t cutoff = -1, start_vid = -1; + igraph_int_t cutoff = -1, start_vid = -1; igraph_vector_int_list_t result; + igraph_vector_t *weights = 0; - static char *kwlist[] = { "start_vid", "cutoff", NULL }; + static char *kwlist[] = { "start_vid", "cutoff", "weights", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &start_vid_o, &cutoff_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &start_vid_o, &cutoff_o, &weights_o)) return NULL; if (igraphmodule_PyObject_to_optional_vid(start_vid_o, &start_vid, &self->g)) @@ -7398,17 +7787,31 @@ PyObject *igraphmodule_Graph_fundamental_cycles( if (cutoff_o != Py_None && igraphmodule_PyObject_to_integer_t(cutoff_o, &cutoff)) return NULL; + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } + if (igraph_vector_int_list_init(&result, 0)) { + if (weights) { + igraph_vector_destroy(weights); free(weights); + } igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_fundamental_cycles(&self->g, &result, start_vid, cutoff, /* weights = */ NULL)) { + if (igraph_fundamental_cycles(&self->g, weights, &result, start_vid, cutoff)) { igraph_vector_int_list_destroy(&result); + if (weights) { + igraph_vector_destroy(weights); free(weights); + } igraphmodule_handle_igraph_error(); return NULL; } + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + result_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&result); igraph_vector_int_list_destroy(&result); @@ -7421,38 +7824,134 @@ PyObject *igraphmodule_Graph_minimum_cycle_basis( PyObject *cutoff_o = Py_None; PyObject *complete_o = Py_True; PyObject *use_cycle_order_o = Py_True; + PyObject *weights_o = Py_None; PyObject *result_o; - igraph_integer_t cutoff = -1; + igraph_int_t cutoff = -1; igraph_vector_int_list_t result; + igraph_vector_t *weights; - static char *kwlist[] = { "cutoff", "complete", "use_cycle_order", NULL }; + static char *kwlist[] = { "cutoff", "complete", "use_cycle_order", "weights", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &cutoff_o, &complete_o, &use_cycle_order_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, &cutoff_o, &complete_o, &use_cycle_order_o, &weights_o)) return NULL; if (cutoff_o != Py_None && igraphmodule_PyObject_to_integer_t(cutoff_o, &cutoff)) return NULL; + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } + if (igraph_vector_int_list_init(&result, 0)) { + if (weights) { + igraph_vector_destroy(weights); free(weights); + } igraphmodule_handle_igraph_error(); return NULL; } if (igraph_minimum_cycle_basis( - &self->g, &result, cutoff, PyObject_IsTrue(complete_o), - PyObject_IsTrue(use_cycle_order_o), /* weights = */ NULL + &self->g, weights, &result, cutoff, PyObject_IsTrue(complete_o), + PyObject_IsTrue(use_cycle_order_o) )) { igraph_vector_int_list_destroy(&result); + if (weights) { + igraph_vector_destroy(weights); free(weights); + } igraphmodule_handle_igraph_error(); return NULL; } + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + result_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&result); igraph_vector_int_list_destroy(&result); return result_o; } + +PyObject *igraphmodule_Graph_simple_cycles( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds +) { + PyObject *mode_o = Py_None; + PyObject *output_o = Py_None; + PyObject *min_cycle_length_o = Py_None; + PyObject *max_cycle_length_o = Py_None; + PyObject *max_results_o = Py_None; + + // argument defaults: no cycle limits + igraph_int_t mode = IGRAPH_OUT; + igraph_int_t min_cycle_length = -1; + igraph_int_t max_cycle_length = -1; + igraph_int_t max_results = IGRAPH_UNLIMITED; + igraph_bool_t use_edges = false; + + static char *kwlist[] = { "mode", "min", "max", "output", "max_results", NULL }; + + if ( + !PyArg_ParseTupleAndKeywords( + args, kwds, "|OOOOO", kwlist, + &mode_o, &min_cycle_length_o, &max_cycle_length_o, &output_o, &max_results_o + ) + ) + return NULL; + + if (mode_o != Py_None && igraphmodule_PyObject_to_integer_t(mode_o, &mode)) + return NULL; + + if (min_cycle_length_o != Py_None && igraphmodule_PyObject_to_integer_t(min_cycle_length_o, &min_cycle_length)) + return NULL; + + if (max_cycle_length_o != Py_None && igraphmodule_PyObject_to_integer_t(max_cycle_length_o, &max_cycle_length)) + return NULL; + + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) + return NULL; + + if (igraphmodule_PyObject_to_vpath_or_epath(output_o, &use_edges)) + return NULL; + + igraph_vector_int_list_t vertices; + if (igraph_vector_int_list_init(&vertices, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + igraph_vector_int_list_t edges; + if (igraph_vector_int_list_init(&edges, 0)) { + igraph_vector_int_list_destroy(&vertices); + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_simple_cycles( + &self->g, + use_edges ? NULL : &vertices, + use_edges ? &edges : NULL, + mode, min_cycle_length, max_cycle_length, + max_results + )) { + igraph_vector_int_list_destroy(&vertices); + igraph_vector_int_list_destroy(&edges); + igraphmodule_handle_igraph_error(); + return NULL; + } + + PyObject *result_o; + + if (use_edges) { + result_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&edges); + } else { + result_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&vertices); + } + igraph_vector_int_list_destroy(&edges); + igraph_vector_int_list_destroy(&vertices); + + return result_o; +} + /********************************************************************** * Graph layout algorithms * **********************************************************************/ @@ -7629,7 +8128,7 @@ PyObject *igraphmodule_Graph_layout_star(igraphmodule_GraphObject* self, igraph_matrix_t m; PyObject *result_o, *order_o = Py_None, *center_o = Py_None; - igraph_integer_t center = 0; + igraph_int_t center = 0; igraph_vector_int_t* order = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, @@ -7689,7 +8188,7 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * igraph_matrix_t m; igraph_bool_t use_seed = false; igraph_error_t ret; - igraph_integer_t maxiter; + igraph_int_t maxiter; Py_ssize_t dim = 2; igraph_real_t kkconst; double epsilon = 0.0; @@ -7753,7 +8252,7 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * } } else { use_seed = 1; - if (igraphmodule_PyList_to_matrix_t(seed_o, &m, "seed")) { + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { return NULL; } } @@ -7805,14 +8304,15 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * return NULL; } - if (dim == 2) + if (dim == 2) { ret = igraph_layout_kamada_kawai (&self->g, &m, use_seed, maxiter, epsilon, kkconst, weights, /*bounds*/ minx, maxx, miny, maxy); - else + } else { ret = igraph_layout_kamada_kawai_3d (&self->g, &m, use_seed, maxiter, epsilon, kkconst, weights, /*bounds*/ minx, maxx, miny, maxy, minz, maxz); + } DESTROY_VECTORS; @@ -7824,6 +8324,19 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * return NULL; } + /* Align layout, but only if no bounding box was specified. */ + if (minx == NULL && maxx == NULL && + miny == NULL && maxy == NULL && + minz == NULL && maxz == NULL && + igraph_vcount(&self->g) <= 1000) { + ret = igraph_layout_align(&self->g, &m); + if (ret) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; @@ -7871,7 +8384,7 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel } if (weight_edge_lengths < 0 || weight_edge_crossings < 0 || weight_node_edge_dist < 0) { - if (igraph_density(&self->g, &density, 0)) { + if (igraph_density(&self->g, 0, &density, 0)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -7899,7 +8412,7 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel return NULL; } } else { - if (igraphmodule_PyList_to_matrix_t(seed_o, &m, "seed")) { + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { return NULL; } use_seed = 1; @@ -7915,6 +8428,16 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel return NULL; } + /* Align layout */ + if (igraph_vcount(&self->g)) { + retval = igraph_layout_align(&self->g, &m); + if (retval) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; @@ -7968,7 +8491,7 @@ PyObject* igraphmodule_Graph_layout_drl(igraphmodule_GraphObject *self, return NULL; } } else { - if (igraphmodule_PyList_to_matrix_t(seed_o, &m, "seed")) { + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { return NULL; } use_seed = 1; @@ -8067,7 +8590,7 @@ PyObject return NULL; } } else { - if (igraphmodule_PyList_to_matrix_t(seed_o, &m, "seed")) { + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { return NULL; } use_seed = 1; @@ -8137,6 +8660,19 @@ PyObject return NULL; } + /* Align layout, but only if no bounding box was specified. */ + if (minx == NULL && maxx == NULL && + miny == NULL && maxy == NULL && + minz == NULL && maxz == NULL && + igraph_vcount(&self->g) <= 1000) { + ret = igraph_layout_align(&self->g, &m); + if (ret) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + #undef DESTROY_VECTORS result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); @@ -8178,7 +8714,7 @@ PyObject *igraphmodule_Graph_layout_graphopt(igraphmodule_GraphObject *self, } } else { use_seed = 1; - if (igraphmodule_PyList_to_matrix_t(seed_o, &m, "seed")) { + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { return NULL; } } @@ -8191,6 +8727,15 @@ PyObject *igraphmodule_Graph_layout_graphopt(igraphmodule_GraphObject *self, return NULL; } + /* Align layout */ + if (igraph_vcount(&self->g) <= 1000) { + if (igraph_layout_align(&self->g, &m)) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; @@ -8210,7 +8755,7 @@ PyObject *igraphmodule_Graph_layout_lgl(igraphmodule_GraphObject * self, igraph_matrix_t m; PyObject *result_o, *root_o = Py_None; Py_ssize_t maxiter = 150; - igraph_integer_t proot = -1; + igraph_int_t proot = -1; double maxdelta, area, coolexp, repulserad, cellsize; maxdelta = igraph_vcount(&self->g); @@ -8249,6 +8794,15 @@ PyObject *igraphmodule_Graph_layout_lgl(igraphmodule_GraphObject * self, return NULL; } + /* Align layout */ + if (igraph_vcount(&self->g) <= 1000) { + if (igraph_layout_align(&self->g, &m)) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; @@ -8287,7 +8841,7 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self, PyErr_NoMemory(); return NULL; } - if (igraphmodule_PyList_to_matrix_t(dist_o, dist, "dist")) { + if (igraphmodule_PyObject_to_matrix_t(dist_o, dist, "dist")) { free(dist); return NULL; } @@ -8314,6 +8868,15 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self, igraph_matrix_destroy(dist); free(dist); } + /* Align layout */ + if (igraph_vcount(&self->g) <= 1000) { + if (igraph_layout_align(&self->g, &m)) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; @@ -8447,40 +9010,36 @@ PyObject *igraphmodule_Graph_layout_reingold_tilford_circular( PyObject *igraphmodule_Graph_layout_sugiyama( igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "layers", "weights", "hgap", "vgap", "maxiter", - "return_extended_graph", NULL }; + static char *kwlist[] = { "layers", "weights", "hgap", "vgap", "maxiter", NULL }; igraph_matrix_t m; - igraph_t extd_graph; - igraph_vector_int_t extd_to_orig_eids; igraph_vector_t *weights = 0; igraph_vector_int_t *layers = 0; double hgap = 1, vgap = 1; Py_ssize_t maxiter = 100; - PyObject *layers_o = Py_None, *weights_o = Py_None, *extd_to_orig_eids_o = Py_None; - PyObject *return_extended_graph = Py_False; - PyObject *result_o; - igraphmodule_GraphObject *graph_o; + PyObject *layers_o = Py_None, *weights_o = Py_None; + PyObject *layout_o, *routing_o; + igraph_matrix_list_t routing; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOddnO", kwlist, - &layers_o, &weights_o, &hgap, &vgap, &maxiter, &return_extended_graph)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOddn", kwlist, + &layers_o, &weights_o, &hgap, &vgap, &maxiter)) return NULL; CHECK_SSIZE_T_RANGE_POSITIVE(maxiter, "maximum number of iterations"); - if (igraph_vector_int_init(&extd_to_orig_eids, 0)) { + if (igraph_matrix_list_init(&routing, 0)) { igraphmodule_handle_igraph_error(); return NULL; } if (igraph_matrix_init(&m, 1, 1)) { - igraph_vector_int_destroy(&extd_to_orig_eids); + igraph_matrix_list_destroy(&routing); igraphmodule_handle_igraph_error(); return NULL; } if (igraphmodule_attrib_to_vector_int_t(layers_o, self, &layers, ATTRIBUTE_TYPE_VERTEX)) { - igraph_vector_int_destroy(&extd_to_orig_eids); + igraph_matrix_list_destroy(&routing); igraph_matrix_destroy(&m); return NULL; } @@ -8488,46 +9047,39 @@ PyObject *igraphmodule_Graph_layout_sugiyama( if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { if (layers != 0) { igraph_vector_int_destroy(layers); free(layers); } - igraph_vector_int_destroy(&extd_to_orig_eids); + igraph_matrix_list_destroy(&routing); igraph_matrix_destroy(&m); return NULL; } - if (igraph_layout_sugiyama(&self->g, &m, - (PyObject_IsTrue(return_extended_graph) ? &extd_graph : 0), - (PyObject_IsTrue(return_extended_graph) ? &extd_to_orig_eids : 0), + if (igraph_layout_sugiyama(&self->g, &m, &routing, layers, hgap, vgap, maxiter, weights)) { if (layers != 0) { igraph_vector_int_destroy(layers); free(layers); } if (weights != 0) { igraph_vector_destroy(weights); free(weights); } - igraph_vector_int_destroy(&extd_to_orig_eids); + igraph_matrix_list_destroy(&routing); igraph_matrix_destroy(&m); igraphmodule_handle_igraph_error(); return NULL; } - if (layers != 0) { igraph_vector_int_destroy(layers); free(layers); } - if (weights != 0) { igraph_vector_destroy(weights); free(weights); } - - result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); - if (result_o == NULL) { - igraph_vector_int_destroy(&extd_to_orig_eids); + layout_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + if (layout_o == NULL) { + igraph_matrix_list_destroy(&routing); igraph_matrix_destroy(&m); return NULL; } igraph_matrix_destroy(&m); - if (PyObject_IsTrue(return_extended_graph)) { - CREATE_GRAPH(graph_o, extd_graph); - if (graph_o == NULL) { - Py_DECREF(result_o); - } - extd_to_orig_eids_o = igraphmodule_vector_int_t_to_PyList(&extd_to_orig_eids); - result_o = Py_BuildValue("NNN", result_o, graph_o, extd_to_orig_eids_o); + routing_o = igraphmodule_matrix_list_t_to_PyList(&routing); + if (routing_o == NULL) { + igraph_matrix_list_destroy(&routing); + return NULL; } - igraph_vector_int_destroy(&extd_to_orig_eids); - return (PyObject *) result_o; + igraph_matrix_list_destroy(&routing); + + return Py_BuildValue("NN", layout_o, routing_o); } /** \ingroup python_interface_graph @@ -8577,7 +9129,7 @@ PyObject *igraphmodule_Graph_layout_umap( } } else { use_seed = 1; - if (igraphmodule_PyList_to_matrix_t(seed_o, &m, "seed")) { + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { return NULL; } } @@ -8617,7 +9169,7 @@ PyObject *igraphmodule_Graph_layout_umap( use_seed, dist, (igraph_real_t)min_dist, - (igraph_integer_t)epochs, + (igraph_int_t)epochs, distances_are_weights)) { if (dist) { igraph_vector_destroy(dist); free(dist); @@ -8631,7 +9183,7 @@ PyObject *igraphmodule_Graph_layout_umap( use_seed, dist, (igraph_real_t)min_dist, - (igraph_integer_t)epochs, + (igraph_int_t)epochs, distances_are_weights)) { if (dist) { igraph_vector_destroy(dist); free(dist); @@ -8758,13 +9310,14 @@ PyObject *igraphmodule_Graph_get_adjacency(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_get_biadjacency(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "types", NULL }; + static char *kwlist[] = { "types", "weights", NULL }; igraph_matrix_t matrix; igraph_vector_int_t row_ids, col_ids; igraph_vector_bool_t *types; - PyObject *matrix_o, *row_ids_o, *col_ids_o, *types_o; + igraph_vector_t *weights = 0; + PyObject *matrix_o, *row_ids_o, *col_ids_o, *types_o, *weights_o = Py_None; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &types_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &types_o)) return NULL; if (igraph_vector_int_init(&row_ids, 0)) @@ -8781,24 +9334,34 @@ PyObject *igraphmodule_Graph_get_biadjacency(igraphmodule_GraphObject * self, return NULL; } + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + igraph_vector_int_destroy(&row_ids); + igraph_vector_int_destroy(&col_ids); + if (types) { igraph_vector_bool_destroy(types); free(types); } + return NULL; + } + if (igraph_matrix_init(&matrix, 1, 1)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&row_ids); igraph_vector_int_destroy(&col_ids); if (types) { igraph_vector_bool_destroy(types); free(types); } + if (weights) { igraph_vector_destroy(weights); free(weights); } return NULL; } - if (igraph_get_biadjacency(&self->g, types, &matrix, &row_ids, &col_ids)) { + if (igraph_get_biadjacency(&self->g, types, weights, &matrix, &row_ids, &col_ids)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&row_ids); igraph_vector_int_destroy(&col_ids); if (types) { igraph_vector_bool_destroy(types); free(types); } + if (weights) { igraph_vector_destroy(weights); free(weights); } igraph_matrix_destroy(&matrix); return NULL; } if (types) { igraph_vector_bool_destroy(types); free(types); } + if (weights) { igraph_vector_destroy(weights); free(weights); } matrix_o = igraphmodule_matrix_t_to_PyList(&matrix, IGRAPHMODULE_TYPE_INT); igraph_matrix_destroy(&matrix); @@ -8981,7 +9544,7 @@ PyObject *igraphmodule_Graph_Read_DIMACS(PyTypeObject * type, { igraphmodule_GraphObject *self; igraphmodule_filehandle_t fobj; - igraph_integer_t source = 0, target = 0; + igraph_int_t source = 0, target = 0; igraph_vector_t capacity; igraph_t g; PyObject *fname = NULL, *directed = Py_False, *capacity_obj; @@ -9332,7 +9895,7 @@ PyObject *igraphmodule_Graph_write_dimacs(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { PyObject *capacity_obj = Py_None, *fname = NULL, *source_o, *target_o; - igraph_integer_t source, target; + igraph_int_t source, target; igraphmodule_filehandle_t fobj; igraph_vector_t* capacity = 0; @@ -9614,18 +10177,19 @@ PyObject *igraphmodule_Graph_write_pajek(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_write_graphml(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *fname = NULL; - static char *kwlist[] = { "f", NULL }; + PyObject *fname = NULL, *prefixattr_o = Py_True; + static char *kwlist[] = { "f", "prefixattr", NULL }; igraphmodule_filehandle_t fobj; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &fname)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &fname, &prefixattr_o)) return NULL; if (igraphmodule_filehandle_init(&fobj, fname, "w")) return NULL; - if (igraph_write_graph_graphml(&self->g, igraphmodule_filehandle_get(&fobj), - /*prefixattr=*/ 1)) { + if (igraph_write_graph_graphml( + &self->g, igraphmodule_filehandle_get(&fobj), PyObject_IsTrue(prefixattr_o) + )) { igraphmodule_handle_igraph_error(); igraphmodule_filehandle_destroy(&fobj); return NULL; @@ -9703,7 +10267,7 @@ PyObject *igraphmodule_Graph_automorphism_group( if (igraphmodule_attrib_to_vector_int_t(color_o, self, &color, ATTRIBUTE_TYPE_VERTEX)) return NULL; - retval = igraph_automorphism_group(&self->g, color, &generators, sh, 0); + retval = igraph_automorphism_group_bliss(&self->g, color, &generators, sh, 0); if (color) { igraph_vector_int_destroy(color); free(color); } @@ -9749,7 +10313,7 @@ PyObject *igraphmodule_Graph_canonical_permutation( if (igraphmodule_attrib_to_vector_int_t(color_o, self, &color, ATTRIBUTE_TYPE_VERTEX)) return NULL; - retval = igraph_canonical_permutation(&self->g, color, &labeling, sh, 0); + retval = igraph_canonical_permutation(&self->g, color, &labeling); if (color) { igraph_vector_int_destroy(color); free(color); } @@ -9791,7 +10355,7 @@ PyObject *igraphmodule_Graph_count_automorphisms( if (igraphmodule_attrib_to_vector_int_t(color_o, self, &color, ATTRIBUTE_TYPE_VERTEX)) return NULL; - retval = igraph_count_automorphisms(&self->g, color, sh, &info); + retval = igraph_count_automorphisms_bliss(&self->g, color, sh, &info); if (color) { igraph_vector_int_destroy(color); free(color); } @@ -9817,25 +10381,25 @@ PyObject *igraphmodule_Graph_count_automorphisms( PyObject *igraphmodule_Graph_isoclass(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - igraph_integer_t isoclass = 0; + igraph_int_t isoclass = 0; PyObject *vids = 0; char *kwlist[] = { "vertices", NULL }; if (!PyArg_ParseTupleAndKeywords - (args, kwds, "|O!", kwlist, &PyList_Type, &vids)) + (args, kwds, "|O", kwlist, &vids)) return NULL; if (vids) { - igraph_vector_int_t vidsvec; - if (igraphmodule_PyObject_to_vid_list(vids, &vidsvec, &self->g)) { + igraph_vs_t vs; + if (igraphmodule_PyObject_to_vs_t(vids, &vs, &self->g, NULL, NULL)) { return NULL; } - if (igraph_isoclass_subgraph(&self->g, &vidsvec, &isoclass)) { - igraph_vector_int_destroy(&vidsvec); + if (igraph_isoclass_subgraph(&self->g, vs, &isoclass)) { + igraph_vs_destroy(&vs); igraphmodule_handle_igraph_error(); return NULL; } - igraph_vector_int_destroy(&vidsvec); + igraph_vs_destroy(&vs); } else { if (igraph_isoclass(&self->g, &isoclass)) { igraphmodule_handle_igraph_error(); @@ -10018,7 +10582,7 @@ igraph_error_t igraphmodule_i_Graph_isomorphic_vf2_callback_fn( igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn( const igraph_t *graph1, const igraph_t *graph2, - const igraph_integer_t cand1, const igraph_integer_t cand2, + const igraph_int_t cand1, const igraph_int_t cand2, void* extra) { igraphmodule_i_Graph_isomorphic_vf2_callback_data_t* data = (igraphmodule_i_Graph_isomorphic_vf2_callback_data_t*)extra; @@ -10042,7 +10606,7 @@ igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn( igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_edge_compat_fn( const igraph_t *graph1, const igraph_t *graph2, - const igraph_integer_t cand1, const igraph_integer_t cand2, + const igraph_int_t cand1, const igraph_int_t cand2, void* extra) { igraphmodule_i_Graph_isomorphic_vf2_callback_data_t* data = (igraphmodule_i_Graph_isomorphic_vf2_callback_data_t*)extra; @@ -10217,7 +10781,7 @@ PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_count_isomorphisms_vf2(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - igraph_integer_t res = 0; + igraph_int_t res = 0; PyObject *o = Py_None; PyObject *color1_o=Py_None, *color2_o=Py_None; PyObject *edge_color1_o=Py_None, *edge_color2_o=Py_None; @@ -10555,7 +11119,7 @@ PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_count_subisomorphisms_vf2(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - igraph_integer_t res = 0; + igraph_int_t res = 0; PyObject *o = Py_None; PyObject *color1_o = Py_None, *color2_o = Py_None; PyObject *edge_color1_o=Py_None, *edge_color2_o=Py_None; @@ -10743,18 +11307,18 @@ PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, { igraph_bool_t res = false; PyObject *o, *return_mapping=Py_False, *domains_o=Py_None, *induced=Py_False; - float time_limit = 0; igraphmodule_GraphObject *other; igraph_vector_int_list_t domains; igraph_vector_int_list_t* p_domains = 0; igraph_vector_int_t mapping, *map=0; - static char *kwlist[] = { "pattern", "domains", "induced", "time_limit", - "return_mapping", NULL }; + static char *kwlist[] = { + "pattern", "domains", "induced", "return_mapping", NULL + }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOfO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOO", kwlist, igraphmodule_GraphType, &o, &domains_o, &induced, - &time_limit, &return_mapping)) + &return_mapping)) return NULL; other=(igraphmodule_GraphObject*)o; @@ -10768,8 +11332,9 @@ PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, if (PyObject_IsTrue(return_mapping)) { if (igraph_vector_int_init(&mapping, 0)) { - if (p_domains) + if (p_domains) { igraph_vector_int_list_destroy(p_domains); + } igraphmodule_handle_igraph_error(); return NULL; } @@ -10777,9 +11342,10 @@ PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, } if (igraph_subisomorphic_lad(&other->g, &self->g, p_domains, &res, - map, 0, PyObject_IsTrue(induced), (igraph_integer_t) time_limit)) { - if (p_domains) + map, 0, PyObject_IsTrue(induced))) { + if (p_domains) { igraph_vector_int_list_destroy(p_domains); + } igraphmodule_handle_igraph_error(); return NULL; } @@ -10810,16 +11376,15 @@ PyObject *igraphmodule_Graph_get_subisomorphisms_lad( igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { PyObject *o, *domains_o=Py_None, *induced=Py_False, *result_o; - float time_limit = 0; igraphmodule_GraphObject *other; igraph_vector_int_list_t domains; igraph_vector_int_list_t* p_domains = 0; igraph_vector_int_list_t mappings; - static char *kwlist[] = { "pattern", "domains", "induced", "time_limit", NULL }; + static char *kwlist[] = { "pattern", "domains", "induced", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOf", kwlist, - igraphmodule_GraphType, &o, &domains_o, &induced, &time_limit)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OO", kwlist, + igraphmodule_GraphType, &o, &domains_o, &induced)) return NULL; other=(igraphmodule_GraphObject*)o; @@ -10838,8 +11403,9 @@ PyObject *igraphmodule_Graph_get_subisomorphisms_lad( return NULL; } - if (igraph_subisomorphic_lad(&other->g, &self->g, p_domains, 0, 0, &mappings, - PyObject_IsTrue(induced), (igraph_integer_t) time_limit)) { + if (igraph_subisomorphic_lad( + &other->g, &self->g, p_domains, 0, 0, &mappings, PyObject_IsTrue(induced) + )) { igraphmodule_handle_igraph_error(); igraph_vector_int_list_destroy(&mappings); if (p_domains) @@ -11155,7 +11721,7 @@ PyObject *igraphmodule_Graph_bfs(igraphmodule_GraphObject * self, { static char *kwlist[] = { "vid", "mode", NULL }; PyObject *l1, *l2, *l3, *result_o, *mode_o = Py_None, *vid_o; - igraph_integer_t vid; + igraph_int_t vid; igraph_neimode_t mode = IGRAPH_OUT; igraph_vector_int_t vids; igraph_vector_int_t layers; @@ -11321,7 +11887,7 @@ PyObject *igraphmodule_Graph_dominator(igraphmodule_GraphObject * self, { static char *kwlist[] = { "vid", "mode", NULL }; PyObject *list = Py_None, *mode_o = Py_None, *root_o; - igraph_integer_t root; + igraph_int_t root; igraph_vector_int_t dom; igraph_neimode_t mode = IGRAPH_OUT; igraph_error_t res; @@ -11375,7 +11941,7 @@ PyObject *igraphmodule_Graph_maxflow_value(igraphmodule_GraphObject * self, PyObject *capacity_object = Py_None, *v1_o, *v2_o; igraph_vector_t capacity_vector; igraph_real_t res; - igraph_integer_t v1, v2; + igraph_int_t v1, v2; igraph_maxflow_stats_t stats; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|O", kwlist, @@ -11414,7 +11980,7 @@ PyObject *igraphmodule_Graph_maxflow(igraphmodule_GraphObject * self, PyObject *capacity_object = Py_None, *flow_o, *cut_o, *partition_o, *v1_o, *v2_o; igraph_vector_t capacity_vector; igraph_real_t res; - igraph_integer_t v1, v2; + igraph_int_t v1, v2; igraph_vector_t flow; igraph_vector_int_t cut, partition; igraph_maxflow_stats_t stats; @@ -11500,7 +12066,7 @@ PyObject *igraphmodule_Graph_all_st_cuts(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "source", "target", NULL }; - igraph_integer_t source, target; + igraph_int_t source, target; igraph_vector_int_list_t cuts, partition1s; PyObject *source_o, *target_o; PyObject *cuts_o, *partition1s_o; @@ -11551,7 +12117,7 @@ PyObject *igraphmodule_Graph_all_st_mincuts(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "source", "target", "capacity", NULL }; - igraph_integer_t source, target; + igraph_int_t source, target; igraph_real_t value; igraph_vector_int_list_t cuts, partition1s; igraph_vector_t capacity_vector; @@ -11618,7 +12184,7 @@ PyObject *igraphmodule_Graph_mincut_value(igraphmodule_GraphObject * self, PyObject *capacity_object = Py_None, *v1_o = Py_None, *v2_o = Py_None; igraph_vector_t capacity_vector; igraph_real_t res, mincut; - igraph_integer_t n, v1 = -1, v2 = -1; + igraph_int_t n, v1 = -1, v2 = -1; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &v1_o, &v2_o, &capacity_object)) @@ -11699,7 +12265,7 @@ PyObject *igraphmodule_Graph_mincut(igraphmodule_GraphObject * self, igraph_vector_t capacity_vector; igraph_real_t value; igraph_vector_int_t partition, partition2, cut; - igraph_integer_t source = -1, target = -1; + igraph_int_t source = -1, target = -1; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &source_o, &target_o, &capacity_object)) @@ -11840,7 +12406,7 @@ PyObject *igraphmodule_Graph_st_mincut(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "source", "target", "capacity", NULL }; - igraph_integer_t source, target; + igraph_int_t source, target; PyObject *cut_o, *part_o, *part2_o, *result_o; PyObject *source_o, *target_o, *capacity_o = Py_None; igraph_vector_t capacity_vector; @@ -12156,13 +12722,15 @@ PyObject *igraphmodule_Graph_vertex_coloring_greedy( PyObject *igraphmodule_Graph_cliques(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "min", "max", NULL }; + static char *kwlist[] = { "min", "max", "max_results", NULL }; PyObject *list; + PyObject *max_results_o = Py_None; Py_ssize_t min_size = 0, max_size = 0; igraph_vector_int_list_t res; + igraph_int_t max_results; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nn", kwlist, - &min_size, &max_size)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnO", kwlist, + &min_size, &max_size, &max_results_o)) return NULL; if (min_size >= 0) { @@ -12177,12 +12745,16 @@ PyObject *igraphmodule_Graph_cliques(igraphmodule_GraphObject * self, max_size = -1; } + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) { + return NULL; + } + if (igraph_vector_int_list_init(&res, 0)) { igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_cliques(&self->g, &res, min_size, max_size)) { + if (igraph_cliques(&self->g, &res, min_size, max_size, max_results)) { igraph_vector_int_list_destroy(&res); return igraphmodule_handle_igraph_error(); } @@ -12272,25 +12844,30 @@ PyObject *igraphmodule_Graph_maximum_bipartite_matching(igraphmodule_GraphObject */ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, PyObject* args, PyObject* kwds) { - static char* kwlist[] = { "min", "max", "file", NULL }; - PyObject *list, *file = Py_None; + static char* kwlist[] = { "min", "max", "file", "max_results", NULL }; + PyObject *list, *file = Py_None, *max_results_o = Py_None; Py_ssize_t min = 0, max = 0; igraph_vector_int_list_t res; igraphmodule_filehandle_t filehandle; + igraph_int_t max_results = IGRAPH_UNLIMITED; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnO", kwlist, &min, &max, &file)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnOO", kwlist, &min, &max, &file, &max_results_o)) return NULL; CHECK_SSIZE_T_RANGE(min, "minimum size"); CHECK_SSIZE_T_RANGE(max, "maximum size"); + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) { + return NULL; + } + if (file == Py_None) { if (igraph_vector_int_list_init(&res, 0)) { PyErr_SetString(PyExc_MemoryError, ""); return NULL; } - if (igraph_maximal_cliques(&self->g, &res, min, max)) { + if (igraph_maximal_cliques(&self->g, &res, min, max, max_results)) { igraph_vector_int_list_destroy(&res); return igraphmodule_handle_igraph_error(); } @@ -12304,7 +12881,7 @@ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, return igraphmodule_handle_igraph_error(); } if (igraph_maximal_cliques_file(&self->g, - igraphmodule_filehandle_get(&filehandle), min, max)) { + igraphmodule_filehandle_get(&filehandle), min, max, max_results)) { igraphmodule_filehandle_destroy(&filehandle); return igraphmodule_handle_igraph_error(); } @@ -12318,7 +12895,7 @@ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_clique_number(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { - igraph_integer_t i; + igraph_int_t i; if (igraph_clique_number(&self->g, &i)) { return igraphmodule_handle_igraph_error(); @@ -12334,13 +12911,14 @@ PyObject *igraphmodule_Graph_independent_vertex_sets(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "min", "max", NULL }; - PyObject *list; + static char *kwlist[] = { "min", "max", "max_results", NULL }; + PyObject *list, *max_results_o = Py_None; Py_ssize_t min_size = 0, max_size = 0; igraph_vector_int_list_t res; + igraph_int_t max_results = IGRAPH_UNLIMITED; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nn", kwlist, - &min_size, &max_size)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnO", kwlist, + &min_size, &max_size, &max_results_o)) return NULL; if (min_size >= 0) { @@ -12355,12 +12933,16 @@ PyObject *igraphmodule_Graph_independent_vertex_sets(igraphmodule_GraphObject max_size = -1; } + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) { + return NULL; + } + if (igraph_vector_int_list_init(&res, 0)) { PyErr_SetString(PyExc_MemoryError, ""); return NULL; } - if (igraph_independent_vertex_sets(&self->g, &res, min_size, max_size)) { + if (igraph_independent_vertex_sets(&self->g, &res, min_size, max_size, max_results)) { igraph_vector_int_list_destroy(&res); return igraphmodule_handle_igraph_error(); } @@ -12398,17 +12980,39 @@ PyObject *igraphmodule_Graph_largest_independent_vertex_sets( * \brief Find all maximal independent vertex sets in a graph */ PyObject *igraphmodule_Graph_maximal_independent_vertex_sets( - igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null) + igraphmodule_GraphObject *self, PyObject* args, PyObject *kwds ) { - PyObject *list; + static char *kwlist[] = { "min", "max", "max_results", NULL }; + PyObject *list, *max_results_o = Py_None; + Py_ssize_t min_size = 0, max_size = 0; igraph_vector_int_list_t res; + igraph_int_t max_results = IGRAPH_UNLIMITED; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnO", kwlist, &min_size, &max_size, &max_results_o)) + return NULL; + + if (min_size >= 0) { + CHECK_SSIZE_T_RANGE(min_size, "minimum size"); + } else { + min_size = -1; + } + + if (max_size >= 0) { + CHECK_SSIZE_T_RANGE(max_size, "maximum size"); + } else { + max_size = -1; + } + + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) { + return NULL; + } if (igraph_vector_int_list_init(&res, 0)) { PyErr_SetString(PyExc_MemoryError, ""); return NULL; } - if (igraph_maximal_independent_vertex_sets(&self->g, &res)) { + if (igraph_maximal_independent_vertex_sets(&self->g, &res, min_size, max_size, max_results)) { igraph_vector_int_list_destroy(&res); return igraphmodule_handle_igraph_error(); } @@ -12424,7 +13028,7 @@ PyObject *igraphmodule_Graph_maximal_independent_vertex_sets( PyObject *igraphmodule_Graph_independence_number( igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null) ) { - igraph_integer_t i; + igraph_int_t i; if (igraph_independence_number(&self->g, &i)) { return igraphmodule_handle_igraph_error(); @@ -12567,16 +13171,18 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject PyObject *res, *qs, *ms; igraph_matrix_int_t merges; igraph_vector_t q; - igraph_vector_t *weights = 0; + igraph_vector_t *weights = NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &directed, &weights_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &directed, &weights_o)) { return NULL; + } - if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) return NULL; + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } if (igraph_matrix_int_init(&merges, 0, 0)) { - if (weights != 0) { + if (weights) { igraph_vector_destroy(weights); free(weights); } return igraphmodule_handle_igraph_error(); @@ -12584,50 +13190,44 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject if (igraph_vector_init(&q, 0)) { igraph_matrix_int_destroy(&merges); - if (weights != 0) { + if (weights) { igraph_vector_destroy(weights); free(weights); } return igraphmodule_handle_igraph_error(); } if (igraph_community_edge_betweenness(&self->g, - /* removed_edges = */ 0, - /* edge_betweenness = */ 0, + /* removed_edges = */ NULL, + /* edge_betweenness = */ NULL, /* merges = */ &merges, - /* bridges = */ 0, - /* modularity = */ weights ? 0 : &q, - /* membership = */ 0, + /* bridges = */ NULL, + /* modularity = */ &q, + /* membership = */ NULL, PyObject_IsTrue(directed), - weights)) { - igraphmodule_handle_igraph_error(); - if (weights != 0) { + weights, + /* lengths = */ NULL)) { + + igraph_vector_destroy(&q); + igraph_matrix_int_destroy(&merges); + if (weights) { igraph_vector_destroy(weights); free(weights); } - igraph_matrix_int_destroy(&merges); - igraph_vector_destroy(&q); - return NULL; + + return igraphmodule_handle_igraph_error();; } - if (weights != 0) { + if (weights) { igraph_vector_destroy(weights); free(weights); } - if (weights == 0) { - /* Calculate modularity vector only in the unweighted case as we don't - * calculate modularities for the weighted case */ - qs=igraphmodule_vector_t_to_PyList(&q, IGRAPHMODULE_TYPE_FLOAT); - igraph_vector_destroy(&q); - if (!qs) { - igraph_matrix_int_destroy(&merges); - return NULL; - } - } else { - qs = Py_None; - Py_INCREF(qs); - igraph_vector_destroy(&q); + qs = igraphmodule_vector_t_to_PyList(&q, IGRAPHMODULE_TYPE_FLOAT); + igraph_vector_destroy(&q); + if (!qs) { + igraph_matrix_int_destroy(&merges); + return NULL; } - ms=igraphmodule_matrix_int_t_to_PyList(&merges); + ms = igraphmodule_matrix_int_t_to_PyList(&merges); igraph_matrix_int_destroy(&merges); if (ms == NULL) { @@ -12808,10 +13408,12 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, return NULL; } - if (igraph_community_infomap(/*in */ &self->g, - /*e_weight=*/ e_ws, /*v_weight=*/ v_ws, - /*nb_trials=*/nb_trials, - /*out*/ &membership, &codelength)) { + if (igraph_community_infomap( + /*in */ &self->g, /*e_weight=*/ e_ws, /*v_weight=*/ v_ws, + /*nb_trials=*/nb_trials, /*is_regularized=*/0, + /*regularization_strength=*/ 0, + /*out*/ &membership, &codelength) + ) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&membership); if (e_ws) { @@ -12851,17 +13453,23 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_community_label_propagation( igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = { "weights", "initial", "fixed", NULL }; + static char *kwlist[] = { "weights", "initial", "fixed", "variant", NULL }; PyObject *weights_o = Py_None, *initial_o = Py_None, *fixed_o = Py_None; + PyObject *variant_o = NULL; PyObject *result_o; igraph_vector_int_t membership, *initial = 0; igraph_vector_t *ws = 0; igraph_vector_bool_t fixed; + igraph_lpa_variant_t variant = IGRAPH_LPA_DOMINANCE; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &weights_o, &initial_o, &fixed_o)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, &weights_o, &initial_o, &fixed_o, &variant_o)) { return NULL; } + if (igraphmodule_PyObject_to_lpa_variant_t(variant_o, &variant)) { + return NULL; + } + if (fixed_o != Py_None) { if (igraphmodule_PyObject_to_vector_bool_t(fixed_o, &fixed)) return NULL; @@ -12880,7 +13488,7 @@ PyObject *igraphmodule_Graph_community_label_propagation( igraph_vector_int_init(&membership, igraph_vcount(&self->g)); if (igraph_community_label_propagation(&self->g, &membership, - IGRAPH_OUT, ws, initial, (fixed_o != Py_None ? &fixed : 0))) { + IGRAPH_OUT, ws, initial, (fixed_o != Py_None ? &fixed : 0), variant)) { if (fixed_o != Py_None) igraph_vector_bool_destroy(&fixed); if (ws) { igraph_vector_destroy(ws); free(ws); } if (initial) { igraph_vector_int_destroy(initial); free(initial); } @@ -12965,15 +13573,16 @@ PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self */ PyObject *igraphmodule_Graph_community_optimal_modularity( igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"weights", NULL}; + static char *kwlist[] = {"weights", "resolution", NULL}; PyObject *weights_o = Py_None; igraph_real_t modularity; igraph_vector_int_t membership; igraph_vector_t* weights = 0; + double resolution = 1; PyObject *res; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, - &weights_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Od", kwlist, + &weights_o, &resolution)) return NULL; if (igraph_vector_int_init(&membership, igraph_vcount(&self->g))) { @@ -12987,8 +13596,8 @@ PyObject *igraphmodule_Graph_community_optimal_modularity( return NULL; } - if (igraph_community_optimal_modularity(&self->g, &modularity, - &membership, weights)) { + if (igraph_community_optimal_modularity(&self->g, weights, resolution, &modularity, + &membership)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&membership); if (weights != 0) { @@ -13147,11 +13756,12 @@ PyObject *igraphmodule_Graph_community_walktrap(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"edge_weights", "node_weights", "resolution", + static char *kwlist[] = {"edge_weights", "node_weights", "node_in_weights", "resolution", "normalize_resolution", "beta", "initial_membership", "n_iterations", NULL}; PyObject *edge_weights_o = Py_None; PyObject *node_weights_o = Py_None; + PyObject *node_in_weights_o = Py_None; PyObject *initial_membership_o = Py_None; PyObject *normalize_resolution = Py_False; PyObject *res = Py_None; @@ -13160,14 +13770,14 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, Py_ssize_t n_iterations = 2; double resolution = 1.0; double beta = 0.01; - igraph_vector_t *edge_weights = NULL, *node_weights = NULL; + igraph_vector_t *edge_weights = NULL, *node_weights = NULL, *node_in_weights = NULL; igraph_vector_int_t *membership = NULL; igraph_bool_t start = true; - igraph_integer_t nb_clusters = 0; + igraph_int_t nb_clusters = 0; igraph_real_t quality = 0.0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOdOdOn", kwlist, - &edge_weights_o, &node_weights_o, &resolution, &normalize_resolution, &beta, &initial_membership_o, &n_iterations)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOdOdOn", kwlist, + &edge_weights_o, &node_weights_o, &node_in_weights_o, &resolution, &normalize_resolution, &beta, &initial_membership_o, &n_iterations)) return NULL; if (n_iterations >= 0) { @@ -13190,6 +13800,13 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, error = -1; } + /* Get node in-weights (directed case) */ + if (!error && igraphmodule_attrib_to_vector_t(node_in_weights_o, self, &node_in_weights, + ATTRIBUTE_TYPE_VERTEX)) { + igraphmodule_handle_igraph_error(); + error = -1; + } + /* Get initial membership */ if (!error && igraphmodule_attrib_to_vector_int_t(initial_membership_o, self, &membership, ATTRIBUTE_TYPE_VERTEX)) { @@ -13200,30 +13817,35 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, if (!error && membership == 0) { start = 0; membership = (igraph_vector_int_t*)calloc(1, sizeof(igraph_vector_int_t)); - if (membership==0) { + if (membership == 0) { PyErr_NoMemory(); error = -1; - } else { - igraph_vector_int_init(membership, 0); + } else if (igraph_vector_int_init(membership, 0)) { + igraphmodule_handle_igraph_error(); + error = -1; } } - if (PyObject_IsTrue(normalize_resolution)) + if (!error && PyObject_IsTrue(normalize_resolution)) { /* If we need to normalize the resolution parameter, * we will need to have node weights. */ if (node_weights == 0) { node_weights = (igraph_vector_t*)calloc(1, sizeof(igraph_vector_t)); - if (node_weights==0) { + if (node_weights == 0) { PyErr_NoMemory(); error = -1; - } else { - igraph_vector_init(node_weights, 0); - if (igraph_strength(&self->g, node_weights, igraph_vss_all(), IGRAPH_ALL, 0, edge_weights)) { - igraphmodule_handle_igraph_error(); - error = -1; - } + } else if (igraph_vector_init(node_weights, 0)) { + igraphmodule_handle_igraph_error(); + error = -1; + } else if (igraph_strength( + &self->g, node_weights, igraph_vss_all(), + igraph_is_directed(&self->g) ? IGRAPH_OUT : IGRAPH_ALL, + IGRAPH_NO_LOOPS, edge_weights + )) { + igraphmodule_handle_igraph_error(); + error = -1; } } resolution /= igraph_vector_sum(node_weights); @@ -13232,7 +13854,7 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, /* Run actual Leiden algorithm for several iterations. */ if (!error) { error = igraph_community_leiden(&self->g, - edge_weights, node_weights, + edge_weights, node_weights, node_in_weights, resolution, beta, start, n_iterations, membership, &nb_clusters, &quality); @@ -13246,6 +13868,10 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, igraph_vector_destroy(node_weights); free(node_weights); } + if (node_in_weights != 0) { + igraph_vector_destroy(node_in_weights); + free(node_in_weights); + } if (!error && membership != 0) { res = igraphmodule_vector_int_t_to_PyList(membership); @@ -13259,6 +13885,157 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, return error ? NULL : Py_BuildValue("Nd", res, (double) quality); } + /** + * Fluid communities + */ +PyObject *igraphmodule_Graph_community_fluid_communities(igraphmodule_GraphObject *self, + PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"no_of_communities", NULL}; + Py_ssize_t no_of_communities; + igraph_vector_int_t membership; + PyObject *result; + + // Parse the Python integer argument + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n", kwlist, &no_of_communities)) { + return NULL; + } + + if (igraph_vector_int_init(&membership, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_community_fluid_communities(&self->g, no_of_communities, &membership)) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&membership); + return NULL; + } + + result = igraphmodule_vector_int_t_to_PyList(&membership); + igraph_vector_int_destroy(&membership); + + return result; +} + +/** + * Voronoi clustering + */ +PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, + PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"lengths", "weights", "mode", "radius", NULL}; + PyObject *lengths_o = Py_None, *weights_o = Py_None; + PyObject *mode_o = Py_None; + PyObject *radius_o = Py_None; + igraph_vector_t *lengths_v = NULL; + igraph_vector_t *weights_v = NULL; + igraph_vector_int_t membership_v, generators_v; + igraph_neimode_t mode = IGRAPH_OUT; + igraph_real_t radius = -1.0; /* negative means auto-optimize */ + igraph_real_t modularity = IGRAPH_NAN; + PyObject *membership_o, *generators_o, *result_o; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, + &lengths_o, &weights_o, &mode_o, &radius_o)) { + return NULL; + } + + /* Handle mode parameter */ + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) { + return NULL; + } + + /* Handle radius parameter */ + if (radius_o != Py_None) { + if (igraphmodule_PyObject_to_real_t(radius_o, &radius)) { + return NULL; + } + } + + /* Handle lengths parameter */ + if (igraphmodule_attrib_to_vector_t(lengths_o, self, &lengths_v, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } + + /* Handle weights parameter */ + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights_v, ATTRIBUTE_TYPE_EDGE)) { + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + return NULL; + } + + /* Initialize result vectors */ + if (igraph_vector_int_init(&membership_v, 0)) { + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); + } + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_vector_int_init(&generators_v, 0)) { + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); + } + igraph_vector_int_destroy(&membership_v); + igraphmodule_handle_igraph_error(); + return NULL; + } + + /* Call the C function - pass NULL for None parameters */ + if (igraph_community_voronoi(&self->g, &membership_v, &generators_v, + &modularity, + lengths_v, + weights_v, + mode, radius)) { + + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); + } + igraph_vector_int_destroy(&membership_v); + igraph_vector_int_destroy(&generators_v); + igraphmodule_handle_igraph_error(); + return NULL; + } + + /* Clean up input vectors */ + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); + } + + /* Convert results to Python objects */ + membership_o = igraphmodule_vector_int_t_to_PyList(&membership_v); + igraph_vector_int_destroy(&membership_v); + if (!membership_o) { + igraph_vector_int_destroy(&generators_v); + return NULL; + } + + generators_o = igraphmodule_vector_int_t_to_PyList(&generators_v); + igraph_vector_int_destroy(&generators_v); + if (!generators_o) { + Py_DECREF(membership_o); + return NULL; + } + + /* Return tuple with membership, generators, and modularity */ + result_o = Py_BuildValue("(NNd)", membership_o, generators_o, modularity); + + return result_o; +} + /********************************************************************** * Random walks * **********************************************************************/ @@ -13271,7 +14048,7 @@ PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, static char *kwlist[] = { "start", "steps", "mode", "stuck", "weights", "return_type", NULL }; PyObject *start_o, *mode_o = Py_None, *stuck_o = Py_None, *weights_o = Py_None, *return_type_o = Py_None; - igraph_integer_t start; + igraph_int_t start; Py_ssize_t steps = 10; igraph_neimode_t mode = IGRAPH_OUT; igraph_random_walk_stuck_t stuck = IGRAPH_RANDOM_WALK_STUCK_RETURN; @@ -13406,6 +14183,46 @@ PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, } } +/********************************************************************** + * Spatial graphs * + **********************************************************************/ + +PyObject *igraphmodule_Graph_Nearest_Neighbor_Graph(PyTypeObject *type, + PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"points", "k", "r", "metric", "directed", NULL}; + PyObject *points_o = Py_None, *metric_o = Py_None, *directed_o = Py_False; + double r = -1; + Py_ssize_t k = 1; + igraph_matrix_t points; + igraphmodule_GraphObject *self; + igraph_t graph; + igraph_metric_t metric = IGRAPH_METRIC_EUCLIDEAN; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|ndOO", kwlist, + &points_o, &k, &r, &metric_o, &directed_o)) { + return NULL; + } + + if (igraphmodule_PyObject_to_metric_t(metric_o, &metric)) { + return NULL; + } + + if (igraphmodule_PyObject_to_matrix_t(points_o, &points, "points")) { + return NULL; + } + + if (igraph_nearest_neighbor_graph(&graph, &points, metric, k, r, PyObject_IsTrue(directed_o))) { + igraph_matrix_destroy(&points); + return igraphmodule_handle_igraph_error(); + } + + igraph_matrix_destroy(&points); + + CREATE_GRAPH_FROM_TYPE(self, graph, type); + + return (PyObject *) self; +} + /********************************************************************** * Special internal methods that you won't need to mess around with * **********************************************************************/ @@ -13510,6 +14327,32 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: C{True} if it is simple, C{False} otherwise.\n" "@rtype: boolean"}, + /* interface to igraph_is_complete */ + {"is_complete", (PyCFunction) igraphmodule_Graph_is_complete, + METH_NOARGS, + "is_complete()\n--\n\n" + "Checks whether the graph is complete, i.e. whether there is at least one\n" + "connection between all distinct pairs of vertices. In directed graphs,\n" + "ordered pairs are considered.\n\n" + "@return: C{True} if it is complete, C{False} otherwise.\n" + "@rtype: boolean"}, + + {"is_clique", (PyCFunction) igraphmodule_Graph_is_clique, + METH_VARARGS | METH_KEYWORDS, + "is_clique(vertices=None, directed=False)\n--\n\n" + "Decides whether a set of vertices is a clique, i.e. a fully connected subgraph.\n\n" + "@param vertices: a list of vertex IDs.\n" + "@param directed: whether to require mutual connections between vertex pairs\n" + " in directed graphs.\n" + "@return: C{True} is the given vertex set is a clique, C{False} if not.\n"}, + + {"is_independent_vertex_set", (PyCFunction) igraphmodule_Graph_is_independent_vertex_set, + METH_VARARGS | METH_KEYWORDS, + "is_independent_vertex_set(vertices=None)\n--\n\n" + "Decides whether no two vertices within a set are adjacent.\n\n" + "@param vertices: a list of vertex IDs.\n" + "@return: C{True} is the given vertices form an independent set, C{False} if not.\n"}, + /* interface to igraph_is_tree */ {"is_tree", (PyCFunction) igraphmodule_Graph_is_tree, METH_VARARGS | METH_KEYWORDS, @@ -13664,24 +14507,21 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_neighbors */ {"neighbors", (PyCFunction) igraphmodule_Graph_neighbors, METH_VARARGS | METH_KEYWORDS, - "neighbors(vertex, mode=\"all\")\n--\n\n" + "neighbors(vertex, mode=\"all\", loops=\"twice\", multiple=True)\n--\n\n" "Returns adjacent vertices to a given vertex.\n\n" "@param vertex: a vertex ID\n" "@param mode: whether to return only successors (C{\"out\"}),\n" " predecessors (C{\"in\"}) or both (C{\"all\"}). Ignored for undirected\n" - " graphs."}, - - {"successors", (PyCFunction) igraphmodule_Graph_successors, - METH_VARARGS | METH_KEYWORDS, - "successors(vertex)\n--\n\n" - "Returns the successors of a given vertex.\n\n" - "Equivalent to calling the L{neighbors()} method with type=C{\"out\"}."}, - - {"predecessors", (PyCFunction) igraphmodule_Graph_predecessors, - METH_VARARGS | METH_KEYWORDS, - "predecessors(vertex)\n--\n\n" - "Returns the predecessors of a given vertex.\n\n" - "Equivalent to calling the L{neighbors()} method with type=C{\"in\"}."}, + " graphs." + "@param loops: whether to return loops in I{undirected} graphs once\n" + " (C{\"once\"}), twice (C{\"twice\"}) or not at all (C{\"ignore\"}). C{False}\n" + " is accepted as an alias to C{\"ignore\"} and C{True} is accepted as an\n" + " alias to C{\"twice\"}. For directed graphs, C{\"twice\"} is equivalent\n" + " to C{\"once\"} (except when C{mode} is C{\"all\"} because the graph is\n" + " then treated as undirected).\n" + "@param multiple: whether to return endpoints of multiple edges as many\n" + " times as their multiplicities." + }, /* interface to igraph_get_eid */ {"get_eid", (PyCFunction) igraphmodule_Graph_get_eid, @@ -13725,7 +14565,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param vertex: a vertex ID\n" "@param mode: whether to return only successors (C{\"out\"}),\n" " predecessors (C{\"in\"}) or both (C{\"all\"}). Ignored for undirected\n" - " graphs."}, + " graphs." + "@param loops: whether to return loops in I{undirected} graphs once\n" + " (C{\"once\"}), twice (C{\"twice\"}) or not at all (C{\"ignore\"}). C{False}\n" + " is accepted as an alias to C{\"ignore\"} and C{True} is accepted as an\n" + " alias to C{\"twice\"}. For directed graphs, C{\"twice\"} is equivalent\n" + " to C{\"once\"} (except when C{mode} is C{\"all\"} because the graph is\n" + " then treated as undirected).\n" + }, ////////////////////// // GRAPH GENERATORS // @@ -13740,8 +14587,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param mode: the mode to be used. Possible values are:\n" "\n" " - C{\"directed\"} - the graph will be directed and a matrix\n" - " element gives the number of edges between two vertices.\n" - " - C{\"undirected\"} - alias to C{\"max\"} for convenience.\n" + " element specifies the number of edges between two vertices.\n" + " - C{\"undirected\"} - the graph will be undirected and a matrix\n" + " element specifies the number of edges between two vertices. The\n" + " input matrix must be symmetric.\n" " - C{\"max\"} - undirected graph will be created and the number of\n" " edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))}\n" " - C{\"min\"} - like C{\"max\"}, but with M{min(A(i,j), A(j,i))}\n" @@ -13803,8 +14652,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_CLASS | METH_KEYWORDS, "Barabasi(n, m, outpref=False, directed=False, power=1,\n" " zero_appeal=1, implementation=\"psumtree\", start_from=None)\n--\n\n" - "Generates a graph based on the Barabasi-Albert model.\n\n" - "B{Reference}: Barabasi, A-L and Albert, R. 1999. Emergence of scaling\n" + "Generates a graph based on the Barabási-Albert model.\n\n" + "B{Reference}: Barabási, A-L and Albert, R. 1999. Emergence of scaling\n" "in random networks. I{Science}, 286 509-512.\n\n" "@param n: the number of vertices\n" "@param m: either the number of outgoing edges generated for\n" @@ -13884,13 +14733,17 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_erdos_renyi_game {"Erdos_Renyi", (PyCFunction) igraphmodule_Graph_Erdos_Renyi, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Erdos_Renyi(n, p, m, directed=False, loops=False)\n--\n\n" - "Generates a graph based on the Erdos-Renyi model.\n\n" + "Erdos_Renyi(n, p, m, directed=False, loops=False, edge_labeled=False)\n--\n\n" + "Generates a graph based on the Erdős-Rényi model.\n\n" "@param n: the number of vertices.\n" "@param p: the probability of edges. If given, C{m} must be missing.\n" "@param m: the number of edges. If given, C{p} must be missing.\n" "@param directed: whether to generate a directed graph.\n" - "@param loops: whether self-loops are allowed.\n"}, + "@param loops: whether self-loops are allowed.\n" + "@param edge_labeled: whether to sample uniformly from the set of\n" + " I{ordered} edge lists. Use C{False} to recover the classic\n" + " Erdős-Rényi model.\n" + }, /* interface to igraph_famous */ {"Famous", (PyCFunction) igraphmodule_Graph_Famous, @@ -13980,6 +14833,18 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param mutual: whether to create all connections as mutual\n" " in case of a directed graph.\n"}, + /* interface to igraph_hypercube */ + {"Hypercube", (PyCFunction) igraphmodule_Graph_Hypercube, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Hypercube(n, directed=False)\n--\n\n" + "Generates an n-dimensional hypercube graph.\n\n" + "The hypercube graph M{Q_n} has M{2^n} vertices and M{2^{n-1} n} edges.\n" + "Two vertices are connected when the binary representations of their vertex\n" + "IDs differ in precisely one bit.\n" + "@param n: the dimension of the hypercube graph\n" + "@param directed: whether to create a directed graph; edges will point\n" + " from lower index vertices towards higher index ones."}, + /* interface to igraph_biadjacency */ {"_Biadjacency", (PyCFunction) igraphmodule_Graph_Biadjacency, METH_VARARGS | METH_CLASS | METH_KEYWORDS, @@ -14023,7 +14888,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_CLASS | METH_KEYWORDS, "Preference(n, type_dist, pref_matrix, attribute=None, directed=False, loops=False)\n--\n\n" "Generates a graph based on vertex types and connection probabilities.\n\n" - "This is practically the nongrowing variant of L{Establishment}.\n" + "This is practically the non-growing variant of L{Establishment}.\n" "A given number of vertices are generated. Every vertex is assigned to a\n" "vertex type according to the given type probabilities. Finally, every\n" "vertex pair is evaluated and an edge is created between them with a\n" @@ -14037,10 +14902,20 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param directed: whether to generate a directed graph.\n" "@param loops: whether loop edges are allowed.\n"}, + /* interface to igraph_from_prufer */ + {"Prufer", (PyCFunction) igraphmodule_Graph_Prufer, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Prufer(seq)\n--\n\n" + "Generates a tree from its Prüfer sequence.\n\n" + "A Prüfer sequence is a unique sequence of integers associated with a\n" + "labelled tree. A tree on M{n} vertices can be represented by a sequence\n" + "of M{n-2} integers, each between M{0} and M{n-1} (inclusive).\n\n" + "@param seq: the Prüfer sequence as an iterable of integers\n"}, + /* interface to igraph_bipartite_game */ {"_Random_Bipartite", (PyCFunction) igraphmodule_Graph_Random_Bipartite, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "_Random_Bipartite(n1, n2, p=None, m=None, directed=False, neimode=\"all\")\n--\n\n" + "_Random_Bipartite(n1, n2, p=None, m=None, directed=False, neimode=\"all\", allowed_edge_types=\"simple\", edge_labeled=False)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.Random_Bipartite()\n\n"}, @@ -14068,21 +14943,29 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_sbm_game */ {"SBM", (PyCFunction) igraphmodule_Graph_SBM, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "SBM(n, pref_matrix, block_sizes, directed=False, loops=False)\n--\n\n" - "Generates a graph based on a stochastic blockmodel.\n\n" - "A given number of vertices are generated. Every vertex is assigned to a\n" - "vertex type according to the given block sizes. Vertices of the same\n" + "SBM(pref_matrix, block_sizes, directed=False, allowed_edge_types=\"simple\")\n--\n\n" + "Generates a graph based on a stochastic block model.\n\n" + "Every vertex is assigned to a vertex type according to the given block\n" + "sizes, which also determine the total vertex count. Vertices of the same\n" "type will be assigned consecutive vertex IDs. Finally, every\n" "vertex pair is evaluated and an edge is created between them with a\n" "probability depending on the types of the vertices involved. The\n" "probabilities are taken from the preference matrix.\n\n" - "@param n: the number of vertices in the graph\n" - "@param pref_matrix: matrix giving the connection probabilities for\n" - " different vertex types.\n" + "@param pref_matrix: matrix giving the connection probabilities (or expected\n" + " edge multiplicities for multigraphs) between different vertex types.\n" "@param block_sizes: list giving the number of vertices in each block; must\n" " sum up to I{n}.\n" "@param directed: whether to generate a directed graph.\n" - "@param loops: whether loop edges are allowed.\n"}, + "@param allowed_edge_types: controls whether loops or multi-edges are allowed\n" + " during the generation process. Note that not all combinations are supported\n" + " for all types of graphs; an exception will be raised for unsupported\n" + " combinations. Possible values are:\n" + "\n" + " - C{\"simple\"}: simple graphs (no self-loops, no multi-edges)\n" + " - C{\"loops\"}: single self-loops allowed, but not multi-edges\n" + " - C{\"multi\"}: multi-edges allowed, but not self-loops\n" + " - C{\"all\"}: multi-edges and self-loops allowed\n" + "\n"}, // interface to igraph_star {"Star", (PyCFunction) igraphmodule_Graph_Star, @@ -14119,7 +15002,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "the number of vertices in the graph, a list of shifts giving\n" "additional edges to a cycle backbone and another integer giving how\n" "many times the shifts should be performed. See\n" - "U{http://mathworld.wolfram.com/LCFNotation.html} for details.\n\n" + "U{https://mathworld.wolfram.com/LCFNotation.html} for details.\n\n" "@param n: the number of vertices\n" "@param shifts: the shifts in a list or tuple\n" "@param repeats: the number of repeats\n" @@ -14176,6 +15059,37 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " See Horvát and Modes (2021) for details.\n" }, + {"Realize_Bipartite_Degree_Sequence", (PyCFunction) igraphmodule_Graph_Realize_Bipartite_Degree_Sequence, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Realize_Bipartite_Degree_Sequence(degrees1, degrees2, allowed_edge_types=\"simple\", method=\"smallest\")\n--\n\n" + "Generates a bipartite graph from the degree sequences of its partitions.\n" + "\n" + "This method implements a Havel-Hakimi style graph construction for biparite\n" + "graphs. In each step, the algorithm picks two vertices in a deterministic\n" + "manner and connects them. The way the vertices are picked is defined by the\n" + "C{method} parameter. The allowed edge types (i.e. whether multi-edges are allowed)\n" + "are specified in the C{allowed_edge_types} parameter. Self-loops are never created,\n" + "since a graph with self-loops is not bipartite.\n" + "\n" + "@param degrees1: the degrees of the first partition.\n" + "@param degrees2: the degrees of the second partition.\n" + "@param allowed_edge_types: controls whether multi-edges are allowed\n" + " during the generation process. Possible values are:\n" + "\n" + " - C{\"simple\"}: simple graphs (no multi-edges)\n" + " - C{\"multi\"}: multi-edges allowed\n" + "\n" + "@param method: controls how the vertices are selected during the generation\n" + " process. Possible values are:\n" + "\n" + " - C{smallest}: The vertex with smallest remaining degree first.\n" + " - C{largest}: The vertex with the largest remaining degree first.\n" + " - C{index}: The vertices are selected in order of their index.\n" + "\n" + " The smallest C{smallest} method is guaranteed to produce a connected graph,\n" + " if one exists." + }, + // interface to igraph_ring {"Ring", (PyCFunction) igraphmodule_Graph_Ring, METH_VARARGS | METH_CLASS | METH_KEYWORDS, @@ -14189,7 +15103,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_static_fitness_game */ {"Static_Fitness", (PyCFunction) igraphmodule_Graph_Static_Fitness, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Static_Fitness(m, fitness_out, fitness_in=None, loops=False, multiple=False)\n--\n\n" + "Static_Fitness(m, fitness_out, fitness_in=None, allowed_edge_types=\"simple\")\n--\n\n" "Generates a non-growing graph with edge probabilities proportional to node\n" "fitnesses.\n\n" "The algorithm randomly selects vertex pairs and connects them until the given\n" @@ -14204,8 +15118,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param fitness_in: a numeric vector with non-negative entries, one for each\n" " vertex. These values represent the in-fitness scores for directed graphs.\n" " For undirected graphs, this argument must be C{None}.\n" - "@param loops: whether loop edges are allowed.\n" - "@param multiple: whether multiple edges are allowed.\n" + "@param allowed_edge_types: controls whether loops or multi-edges are allowed\n" + " during the generation process. Note that not all combinations are supported\n" + " for all types of graphs; an exception will be raised for unsupported\n" + " combinations. Possible values are:\n" + "\n" + " - C{\"simple\"}: simple graphs (no self-loops, no multi-edges)\n" + " - C{\"loops\"}: single self-loops allowed, but not multi-edges\n" + " - C{\"multi\"}: multi-edges allowed, but not self-loops\n" + " - C{\"all\"}: multi-edges and self-loops allowed\n" + "\n" "@return: a directed or undirected graph with the prescribed power-law\n" " degree distributions.\n" }, @@ -14213,8 +15135,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_static_power_law_game */ {"Static_Power_Law", (PyCFunction) igraphmodule_Graph_Static_Power_Law, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Static_Power_Law(n, m, exponent_out, exponent_in=-1, loops=False, " - "multiple=False, finite_size_correction=True)\n--\n\n" + "Static_Power_Law(n, m, exponent_out, exponent_in=-1, allowed_edge_types=\"simple\", " + "finite_size_correction=True)\n--\n\n" "Generates a non-growing graph with prescribed power-law degree distributions.\n\n" "B{References}\n\n" " - Goh K-I, Kahng B, Kim D: Universal behaviour of load distribution\n" @@ -14232,8 +15154,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param exponent_in: the exponent of the in-degree distribution, which\n" " must be between 2 and infinity (inclusive) It can also be negative, in\n" " which case an undirected graph will be generated.\n" - "@param loops: whether loop edges are allowed.\n" - "@param multiple: whether multiple edges are allowed.\n" + "@param allowed_edge_types: controls whether loops or multi-edges are allowed\n" + " during the generation process. Note that not all combinations are supported\n" + " for all types of graphs; an exception will be raised for unsupported\n" + " combinations. Possible values are:\n" + "\n" + " - C{\"simple\"}: simple graphs (no self-loops, no multi-edges)\n" + " - C{\"loops\"}: single self-loops allowed, but not multi-edges\n" + " - C{\"multi\"}: multi-edges allowed, but not self-loops\n" + " - C{\"all\"}: multi-edges and self-loops allowed\n" + "\n" "@param finite_size_correction: whether to apply a finite-size correction\n" " to the generated fitness values for exponents less than 3. See the\n" " paper of Cho et al for more details.\n" @@ -14252,6 +15182,86 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " this is the case, also its orientation. Must be one of\n" " C{\"in\"}, C{\"out\"} and C{\"undirected\"}.\n"}, + /* interface to igraph_chung_lu_game */ + {"Chung_Lu", (PyCFunction) igraphmodule_Graph_Chung_Lu, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Chung_Lu(out, in_=None, loops=True, variant=\"original\")\n--\n\n" + "Generates a Chung-Lu random graph.\n\n" + "In the original Chung-Lu model, each pair of vertices M{i} and M{j} is connected\n" + "with independent probability M{p_{ij} = w_i w_j / S}, where M{w_i} is a weight\n" + "associated with vertex M{i} and M{S = \\sum_k w_k} is the sum of weights.\n" + "In the directed variant, vertices have both out-weights, M{w^\\text{out}},\n" + "and in-weights, M{w^\\text{in}}, with equal sums,\n" + "M{S = \\sum_k w^\\text{out}_k = \\sum_k w^\\text{in}_k}. The connection\n" + "probability between M{i} and M{j} is M{p_{ij} = w^\\text{out}_i w^\\text{in}_j / S}.\n\n" + "This model is commonly used to create random graphs with a fixed I{expected}\n" + "degree sequence. The expected degree of vertex M{i} is approximately equal\n" + "to the weight M{w_i}. Specifically, if the graph is directed and self-loops\n" + "are allowed, then the expected out- and in-degrees are precisely M{w^\\text{out}}\n" + "and M{w^\\text{in}}. If self-loops are disallowed, then the expected out-\n" + "and in-degrees are M{w^\\text{out} (S - w^\\text{in}) / S} and\n" + "M{w^\\text{in} (S - w^\\text{out}) / S}, respectively. If the graph is\n" + "undirected, then the expected degrees with and without self-loops are\n" + "M{w (S + w) / S} and M{w (S - w) / S}, respectively.\n\n" + "A limitation of the original Chung-Lu model is that when some of the\n" + "weights are large, the formula for M{p_{ij}} yields values larger than 1.\n" + "Chung and Lu's original paper excludes the use of such weights. When\n" + "M{p_{ij} > 1}, this function simply issues a warning and creates\n" + "a connection between M{i} and M{j}. However, in this case the expected degrees\n" + "will no longer relate to the weights in the manner stated above. Thus the\n" + "original Chung-Lu model cannot produce certain (large) expected degrees.\n\n" + "The overcome this limitation, this function implements additional variants of\n" + "the model, with modified expressions for the connection probability M{p_{ij}}\n" + "between vertices M{i} and M{j}. Let M{q_{ij} = w_i w_j / S}, or\n" + "M{q_{ij} = w^out_i w^in_j / S} in the directed case. All model\n" + "variants become equivalent in the limit of sparse graphs where M{q_{ij}}\n" + "approaches zero. In the original Chung-Lu model, selectable by setting\n" + "C{variant} to C{\"original\"}, M{p_{ij} = min(q_{ij}, 1)}.\n" + "The C{\"maxent\"} variant, sometimes referred to as the generalized\n" + "random graph, uses M{p_{ij} = q_{ij} / (1 + q_{ij})}, and is equivalent\n" + "to a maximum entropy model (i.e. exponential random graph model) with\n" + "a constraint on expected degrees, see Park and Newman (2004), Section B,\n" + "setting M{exp(-\\Theta_{ij}) = w_i w_j / S}. This model is also\n" + "discussed by Britton, Deijfen and Martin-Löf (2006). By virtue of being\n" + "a degree-constrained maximum entropy model, it generates graphs having\n" + "the same degree sequence with the same probability.\n" + "A third variant can be requested with C{\"nr\"}, and uses\n" + "M{p_{ij} = 1 - exp(-q_{ij})}. This is the underlying simple graph\n" + "of a multigraph model introduced by Norros and Reittu (2006).\n" + "For a discussion of these three model variants, see Section 16.4 of\n" + "Bollobás, Janson, Riordan (2007), as well as Van Der Hofstad (2013).\n\n" + "B{References:}\n\n" + " - Chung F and Lu L: Connected components in a random graph with given degree sequences.\n" + " I{Annals of Combinatorics} 6, 125-145 (2002) U{https://doi.org/10.1007/PL00012580}\n" + " - Miller JC and Hagberg A: Efficient Generation of Networks with Given Expected Degrees (2011)\n" + " U{https://doi.org/10.1007/978-3-642-21286-4_10}\n" + " - Park J and Newman MEJ: Statistical mechanics of networks.\n" + " I{Physical Review E} 70, 066117 (2004). U{https://doi.org/10.1103/PhysRevE.70.066117}\n" + " - Britton T, Deijfen M, Martin-Löf A: Generating Simple Random Graphs with Prescribed Degree Distribution.\n" + " I{J Stat Phys} 124, 1377–1397 (2006). U{https://doi.org/10.1007/s10955-006-9168-x}\n" + " - Norros I and Reittu H: On a conditionally Poissonian graph process.\n" + " I{Advances in Applied Probability} 38, 59–75 (2006).\n" + " U{https://doi.org/10.1239/aap/1143936140}\n" + " - Bollobás B, Janson S, Riordan O: The phase transition in inhomogeneous random graphs.\n" + " I{Random Struct Algorithms} 31, 3–122 (2007). U{https://doi.org/10.1002/rsa.20168}\n" + " - Van Der Hofstad R: Critical behavior in inhomogeneous random graphs.\n" + " I{Random Struct Algorithms} 42, 480–508 (2013). U{https://doi.org/10.1002/rsa.20450}\n\n" + "@param out: the vertex weights (or out-weights). In sparse graphs\n" + " these will be approximately equal to the expected (out-)degrees.\n" + "@param in_: the vertex in-weights, approximately equal to the expected\n" + " in-degrees of the graph. If omitted, the generated graph will be\n" + " undirected.\n" + "@param loops: whether to allow the generation of self-loops.\n" + "@param variant: the model variant to be used. Let M{q_{ij}=w_i w_j / S},\n" + " where M{S = \\sum_k w_k}. The following variants are available:\n" + " \n" + " - C{\"original\"} -- the original Chung-Lu model with\n" + " M{p_{ij} = min(1, q_{ij})}.\n" + " - C{\"maxent\"} -- maximum entropy model with fixed expected degrees\n" + " M{p_{ij} = q_{ij} / (1 + q_{ij})}\n" + " - C{\"nr\"} -- Norros and Reittu's model, M{p_{ij} = 1 - exp(-q_{ij})}\n" + }, + /* interface to igraph_degree_sequence_game */ {"Degree_Sequence", (PyCFunction) igraphmodule_Graph_Degree_Sequence, METH_VARARGS | METH_CLASS | METH_KEYWORDS, @@ -14318,7 +15328,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param directed: whether the graph should be directed\n" "@param method: the generation method to be used. One of the following:\n" " \n" - " - C{\"prufer\"} -- samples Prufer sequences uniformly, then converts\n" + " - C{\"prufer\"} -- samples Prüfer sequences uniformly, then converts\n" " them to trees\n" " - C{\"lerw\"} -- performs a loop-erased random walk on the complete\n" " graph to uniformly sample its spanning trees (Wilson's algorithm).\n" @@ -14339,7 +15349,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_watts_strogatz_game */ {"Watts_Strogatz", (PyCFunction) igraphmodule_Graph_Watts_Strogatz, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Watts_Strogatz(dim, size, nei, p, loops=False, multiple=False)\n--\n\n" + "Watts_Strogatz(dim, size, nei, p, allowed_edge_types=\"simple\")\n--\n\n" "This function generates networks with the small-world property based on a\n" "variant of the Watts-Strogatz model. The network is obtained by first creating\n" "a periodic undirected lattice, then rewiring both endpoints of each edge with\n" @@ -14359,8 +15369,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param nei: value giving the distance (number of steps) within which\n" " two vertices will be connected.\n" "@param p: rewiring probability\n\n" - "@param loops: specifies whether loop edges are allowed\n" - "@param multiple: specifies whether multiple edges are allowed\n" + "@param allowed_edge_types: controls whether loops or multi-edges are allowed\n" + " during the generation process. Note that not all combinations are supported\n" + " for all types of graphs; an exception will be raised for unsupported\n" + " combinations. Possible values are:\n" + "\n" + " - C{\"simple\"}: simple graphs (no self-loops, no multi-edges)\n" + " - C{\"loops\"}: single self-loops allowed, but not multi-edges\n" + " - C{\"multi\"}: multi-edges allowed, but not self-loops\n" + " - C{\"all\"}: multi-edges and self-loops allowed\n" + "\n" "@see: L{Lattice()}, L{rewire()}, L{rewire_edges()} if more flexibility is\n" " needed\n" }, @@ -14396,10 +15414,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // STRUCTURAL PROPERTIES OF GRAPHS // ///////////////////////////////////// - // interface to igraph_are_connected - {"are_connected", (PyCFunction) igraphmodule_Graph_are_connected, + // interface to igraph_are_adjacent + {"are_adjacent", (PyCFunction) igraphmodule_Graph_are_adjacent, METH_VARARGS | METH_KEYWORDS, - "are_connected(v1, v2)\n--\n\n" + "are_adjacent(v1, v2)\n--\n\n" "Decides whether two given vertices are directly connected.\n\n" "@param v1: the ID or name of the first vertex\n" "@param v2: the ID or name of the second vertex\n" @@ -14418,12 +15436,12 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_assortativity */ {"assortativity", (PyCFunction)igraphmodule_Graph_assortativity, METH_VARARGS | METH_KEYWORDS, - "assortativity(types1, types2=None, directed=True, normalized=True)\n--\n\n" + "assortativity(types1, types2=None, directed=True, normalized=True, weights=None)\n--\n\n" "Returns the assortativity of the graph based on numeric properties\n" "of the vertices.\n\n" "This coefficient is basically the correlation between the actual\n" "connectivity patterns of the vertices and the pattern expected from the\n" - "disribution of the vertex types.\n\n" + "distribution of the vertex types.\n\n" "See equation (21) in Newman MEJ: Mixing patterns in networks, Phys Rev E\n" "67:026126 (2003) for the proper definition. The actual calculation is\n" "performed using equation (26) in the same paper for directed graphs, and\n" @@ -14443,6 +15461,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param normalized: whether to compute the normalized covariance, i.e.\n" " Pearson correlation. Supply True here to compute the standard\n" " assortativity.\n" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" "@return: the assortativity coefficient\n\n" "@see: L{assortativity_degree()} when the types are the vertex degrees\n" }, @@ -14463,7 +15483,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_assortativity_nominal */ {"assortativity_nominal", (PyCFunction)igraphmodule_Graph_assortativity_nominal, METH_VARARGS | METH_KEYWORDS, - "assortativity_nominal(types, directed=True, normalized=True)\n--\n\n" + "assortativity_nominal(types, directed=True, normalized=True, weights=None)\n--\n\n" "Returns the assortativity of the graph based on vertex categories.\n\n" "Assuming that the vertices belong to different categories, this\n" "function calculates the assortativity coefficient, which specifies\n" @@ -14482,6 +15502,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param normalized: whether to compute the (usual) normalized assortativity.\n" " The unnormalized version is identical to modularity. Supply True here to\n" " compute the standard assortativity.\n" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" "@return: the assortativity coefficient\n\n" }, @@ -14508,8 +15530,6 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Calculates Kleinberg's authority score for the vertices of the graph\n\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" " even an edge attribute name.\n" - "@param scale: whether to normalize the scores so that the largest one\n" - " is 1.\n" "@param arpack_options: an L{ARPACKOptions} object used to fine-tune\n" " the ARPACK eigenvector calculation. If omitted, the module-level\n" " variable called C{arpack_options} is used.\n" @@ -14613,7 +15633,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "closeness(vertices=None, mode=\"all\", cutoff=None, weights=None, " "normalized=True)\n--\n\n" "Calculates the closeness centralities of given vertices in a graph.\n\n" - "The closeness centerality of a vertex measures how easily other\n" + "The closeness centrality of a vertex measures how easily other\n" "vertices can be reached from it (or the other way: how easily it\n" "can be reached from the other vertices). It is defined as the\n" "number of vertices minus one divided by the sum of\n" @@ -14646,7 +15666,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "harmonic_centrality(vertices=None, mode=\"all\", cutoff=None, weights=None, " "normalized=True)\n--\n\n" "Calculates the harmonic centralities of given vertices in a graph.\n\n" - "The harmonic centerality of a vertex measures how easily other\n" + "The harmonic centrality of a vertex measures how easily other\n" "vertices can be reached from it (or the other way: how easily it\n" "can be reached from the other vertices). It is defined as the\n" "mean inverse distance to all other vertices.\n\n" @@ -14673,7 +15693,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS, "connected_components(mode=\"strong\")\n--\n\n" "Calculates the (strong or weak) connected components for a given graph.\n\n" - "Atttention: this function has a more convenient interface in class\n" + "Attention: this function has a more convenient interface in class\n" "L{Graph}, which wraps the result in a L{VertexClustering} object.\n" "It is advised to use that.\n" "@param mode: must be either C{\"strong\"} or C{\"weak\"}, depending on\n" @@ -14749,13 +15769,23 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_density */ {"density", (PyCFunction) igraphmodule_Graph_density, METH_VARARGS | METH_KEYWORDS, - "density(loops=False)\n--\n\n" + "density(loops=False, weights=None)\n--\n\n" "Calculates the density of the graph.\n\n" "@param loops: whether to take loops into consideration. If C{True},\n" " the algorithm assumes that there might be some loops in the graph\n" " and calculates the density accordingly. If C{False}, the algorithm\n" " assumes that there can't be any loops.\n" - "@return: the density of the graph."}, + "@param weights: weights associated to the edges. Can be an attribute name\n" + " as well. If C{None}, every edge will have the same weight.\n" + "@return: the (weighted or unweighted) density of the graph."}, + + /* interface to igraph_mean_degree */ + {"mean_degree", (PyCFunction) igraphmodule_Graph_mean_degree, + METH_VARARGS | METH_KEYWORDS, + "mean_degree(loops=True)\n--\n\n" + "Calculates the mean degree of the graph.\n\n" + "@param loops: whether to consider self-loops during the calculation\n" + "@return: the mean degree of the graph."}, /* interfaces to igraph_diameter */ {"diameter", (PyCFunction) igraphmodule_Graph_diameter, @@ -14919,8 +15949,6 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "components, and the eigenvector centrality calculated for each separately.\n\n" "@param directed: whether to consider edge directions in a directed\n" " graph. Ignored for undirected graphs.\n" - "@param scale: whether to normalize the centralities so the largest\n" - " one will always be 1.\n" "@param weights: edge weights given as a list or an edge attribute. If\n" " C{None}, all edges have equal weight.\n" "@param return_eigenvalue: whether to return the actual largest\n" @@ -14954,16 +15982,39 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " breaking heuristic of Eades, Lin and Smyth, which is linear in the number\n" " of edges but not necessarily optimal; however, it guarantees that the\n" " number of edges to be removed is smaller than |E|/2 - |V|/6. C{\"ip\"} uses\n" - " an integer programming formulation which is guaranteed to yield an optimal\n" - " result, but is too slow for large graphs.\n" + " the most efficient available integer programming formulation which is guaranteed\n" + " to yield an optimal result. Specific integer programming formulations can be\n" + " selected using C{\"ip_ti\"} (using triangle inequalities) and C{\"ip_cg\"}\n" + " (a minimum set cover formulation using incremental constraint generation).\n" + " Note that the minimum feedback arc set problem is NP-hard, therefore all methods\n" + " that obtain exact optimal solutions are infeasibly slow on large graphs.\n" "@return: the IDs of the edges to be removed, in a list.\n\n" }, + /* interface to igraph_feedback_vertex_set */ + {"feedback_vertex_set", (PyCFunction) igraphmodule_Graph_feedback_vertex_set, + METH_VARARGS | METH_KEYWORDS, + "feedback_vertex_set(weights=None, method=\"ip\")\n--\n\n" + "Calculates a minimum feedback vertex set.\n\n" + "A feedback vertex set is a set of edges whose removal makes the graph acyclic.\n" + "Finding a minimum feedback vertex set is an NP-hard problem both in directed\n" + "and undirected graphs.\n\n" + "@param weights: vertex weights to be used. Can be a sequence or iterable or\n" + " even a vertex attribute name. When given, the algorithm will strive to\n" + " remove lightweight vertices in order to minimize the total weight of the\n" + " feedback vertex set.\n" + "@param method: the algorithm to use. C{\"ip\"} uses an exact integer programming\n" + " approach, and is currently the only available method.\n" + "@return: the IDs of the vertices to be removed, in a list.\n\n" + }, + /* interface to igraph_get_shortest_path */ {"get_shortest_path", (PyCFunction) igraphmodule_Graph_get_shortest_path, METH_VARARGS | METH_KEYWORDS, "get_shortest_path(v, to, weights=None, mode=\"out\", output=\"vpath\", algorithm=\"auto\")\n--\n\n" "Calculates the shortest path from a source vertex to a target vertex in a graph.\n\n" + "This function only returns a single shortest path. Consider using L{get_shortest_paths()}\n" + "to find all shortest paths between a source and one or more target vertices.\n\n" "@param v: the source vertex of the path\n" "@param to: the target vertex of the path\n" "@param weights: edge weights in a list or the name of an edge attribute\n" @@ -14981,7 +16032,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " algorithm automatically based on whether the graph has negative weights\n" " or not. C{\"dijkstra\"} uses Dijkstra's algorithm. C{\"bellman_ford\"}\n" " uses the Bellman-Ford algorithm. Ignored for unweighted graphs.\n" - "@return: see the documentation of the C{output} parameter.\n"}, + "@return: see the documentation of the C{output} parameter.\n" + "@see: L{get_shortest_paths()}\n"}, /* interface to igraph_get_shortest_paths */ {"get_shortest_paths", (PyCFunction) igraphmodule_Graph_get_shortest_paths, @@ -15089,7 +16141,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"_get_all_simple_paths", (PyCFunction) igraphmodule_Graph_get_all_simple_paths, METH_VARARGS | METH_KEYWORDS, - "_get_all_simple_paths(v, to=None, cutoff=-1, mode=\"out\")\n--\n\n" + "_get_all_simple_paths(v, to=None, minlen=0, maxlen=-1, mode=\"out\", max_results=None)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.get_all_simple_paths()\n\n" }, @@ -15127,8 +16179,6 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Calculates Kleinberg's hub score for the vertices of the graph\n\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" " even an edge attribute name.\n" - "@param scale: whether to normalize the scores so that the largest one\n" - " is 1.\n" "@param arpack_options: an L{ARPACKOptions} object used to fine-tune\n" " the ARPACK eigenvector calculation. If omitted, the module-level\n" " variable called C{arpack_options} is used.\n" @@ -15225,6 +16275,20 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param mode: whether we should calculate strong or weak connectivity.\n" "@return: C{True} if the graph is connected, C{False} otherwise.\n"}, + /* interface to igraph_is_biconnected */ + {"is_biconnected", (PyCFunction) igraphmodule_Graph_is_biconnected, + METH_NOARGS, + "is_biconnected()\n--\n\n" + "Decides whether the graph is biconnected.\n\n" + "A graph is biconnected if it stays connected after the removal of\n" + "any single vertex.\n\n" + "Note that there are different conventions in use about whether to\n" + "consider a graph consisting of two connected vertices to be biconnected.\n" + "igraph does consider it biconnected.\n\n" + "@return: C{True} if it is biconnected, C{False} otherwise.\n" + "@rtype: boolean" + }, + /* interface to igraph_linegraph */ {"linegraph", (PyCFunction) igraphmodule_Graph_linegraph, METH_VARARGS | METH_KEYWORDS, @@ -15286,7 +16350,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param vertices: a single vertex ID or a list of vertex IDs, or\n" " C{None} meaning all the vertices in the graph.\n" "@param order: the order of the neighborhood, i.e. the maximum number of\n" - " steps to take from the seed vertex.\n" + " steps to take from the seed vertex. Negative values are interpreted as\n" + " an infinite order, i.e. no limit on the number of steps.\n" "@param mode: specifies how to take into account the direction of\n" " the edges if a directed graph is analyzed. C{\"out\"} means that\n" " only the outgoing edges are followed, so all vertices reachable\n" @@ -15314,7 +16379,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param vertices: a single vertex ID or a list of vertex IDs, or\n" " C{None} meaning all the vertices in the graph.\n" "@param order: the order of the neighborhood, i.e. the maximum number of\n" - " steps to take from the seed vertex.\n" + " steps to take from the seed vertex. Negative values are interpreted as\n" + " an infinite order, i.e. no limit on the number of steps.\n" "@param mode: specifies how to take into account the direction of\n" " the edges if a directed graph is analyzed. C{\"out\"} means that\n" " only the outgoing edges are followed, so all vertices reachable\n" @@ -15392,8 +16458,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "permute_vertices(permutation)\n--\n\n" "Permutes the vertices of the graph according to the given permutation\n" "and returns the new graph.\n\n" - "Vertex M{k} of the original graph will become vertex M{permutation[k]}\n" - "in the new graph. No validity checks are performed on the permutation\n" + "Vertex M{k} of the new graph will belong to vertex M{permutation[k]}\n" + "in the original graph. No validity checks are performed on the permutation\n" "vector.\n\n" "@return: the new graph\n" }, @@ -15442,32 +16508,33 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, /* interface to igraph_rewire */ - {"rewire", (PyCFunction) igraphmodule_Graph_rewire, + {"_rewire", (PyCFunction) igraphmodule_Graph_rewire, METH_VARARGS | METH_KEYWORDS, - "rewire(n=1000, mode=\"simple\")\n--\n\n" - "Randomly rewires the graph while preserving the degree distribution.\n\n" - "Please note that the rewiring is done \"in-place\", so the original\n" - "graph will be modified. If you want to preserve the original graph,\n" - "use the L{copy} method before.\n\n" - "@param n: the number of rewiring trials.\n" - "@param mode: the rewiring algorithm to use. It can either be C{\"simple\"} or\n" - " C{\"loops\"}; the former does not create or destroy loop edges while the\n" - " latter does.\n"}, + "_rewire(n=None, allowed_edge_types=\"simple\")\n--\n\n" + "Internal function, undocumented.\n\n" + "@see: Graph.rewire()\n\n"}, /* interface to igraph_rewire_edges */ {"rewire_edges", (PyCFunction) igraphmodule_Graph_rewire_edges, METH_VARARGS | METH_KEYWORDS, - "rewire_edges(prob, loops=False, multiple=False)\n--\n\n" + "rewire_edges(prob, allowed_edge_types=\"simple\")\n--\n\n" "Rewires the edges of a graph with constant probability.\n\n" "Each endpoint of each edge of the graph will be rewired with a constant\n" "probability, given in the first argument.\n\n" "Please note that the rewiring is done \"in-place\", so the original\n" "graph will be modified. If you want to preserve the original graph,\n" - "use the L{copy} method before.\n\n" + "use the L{cop y} method before.\n\n" "@param prob: rewiring probability\n" - "@param loops: whether the algorithm is allowed to create loop edges\n" - "@param multiple: whether the algorithm is allowed to create multiple\n" - " edges.\n"}, + "@param allowed_edge_types: controls whether loops or multi-edges are allowed\n" + " during the rewiring process. Note that not all combinations are supported\n" + " for all types of graphs; an exception will be raised for unsupported\n" + " combinations. Possible values are:\n" + "\n" + " - C{\"simple\"}: simple graphs (no self-loops, no multi-edges)\n" + " - C{\"loops\"}: single self-loops allowed, but not multi-edges\n" + " - C{\"multi\"}: multi-edges allowed, but not self-loops\n" + " - C{\"all\"}: multi-edges and self-loops allowed\n" + "\n"}, /* interface to igraph_distances */ {"distances", (PyCFunction) igraphmodule_Graph_distances, @@ -15552,7 +16619,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_minimum_spanning_tree */ {"_spanning_tree", (PyCFunction) igraphmodule_Graph_spanning_tree, METH_VARARGS | METH_KEYWORDS, - "_spanning_tree(weights=None)\n--\n\n" + "_spanning_tree(weights=None, method=\"auto\")\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.spanning_tree()"}, @@ -15601,8 +16668,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { (PyCFunction) igraphmodule_Graph_to_prufer, METH_NOARGS, "to_prufer()\n--\n\n" - "Converts a tree graph into a Prufer sequence.\n\n" - "@return: the Prufer sequence as a list" + "Converts a tree graph into a Prüfer sequence.\n\n" + "@return: the Prüfer sequence as a list" }, // interface to igraph_transitivity_undirected @@ -15651,7 +16718,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " small-world networks. I{Nature} 393(6884):440-442, 1998.\n" " - Barrat A, Barthelemy M, Pastor-Satorras R and Vespignani A:\n" " The architecture of complex weighted networks. I{PNAS} 101, 3747 (2004).\n" - " U{http://arxiv.org/abs/cond-mat/0311416}.\n\n" + " U{https://arxiv.org/abs/cond-mat/0311416}.\n\n" "@param vertices: a list containing the vertex IDs which should be\n" " included in the result. C{None} means all of the vertices.\n" "@param mode: defines how to treat vertices with degree less than two.\n" @@ -15729,8 +16796,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " graph, therefore it is advised to set this to C{True}. The parameter\n" " is ignored if the connectivity between two given vertices is computed.\n" "@param neighbors: tells igraph what to do when the two vertices are\n" - " connected. C{\"error\"} raises an exception, C{\"infinity\"} returns\n" - " infinity, C{\"ignore\"} ignores the edge.\n" + " connected. C{\"error\"} raises an exception, C{\"negative\"} returns\n" + " a negative value, C{\"number_of_nodes\"} or C{\"nodes\"} returns the\n" + " number of nodes, or C{\"ignore\"} ignores the edge.\n" "@return: the vertex connectivity\n" }, @@ -15789,6 +16857,15 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Each vertex is assigned a weight which is 1 / log(degree). The\n" "log-weighted similarity of two vertices is the sum of the weights\n" "of their common neighbors.\n\n" + "Note that the presence of loop edges may yield counter-intuitive\n" + "results. A node with a loop edge is considered to be a neighbor of itself\n" + "I{twice} (because there are two edge stems incident on the node). Adding a\n" + "loop edge to a node may decrease its similarity to other nodes, but it may\n" + "also I{increase} it. For instance, if nodes A and B are connected but share\n" + "no common neighbors, their similarity is zero. However, if a loop edge is\n" + "added to B, then B itself becomes a common neighbor of A and B and thus the\n" + "similarity of A and B will be increased. Consider removing loop edges\n" + "explicitly before invoking this function using L{Graph.simplify()}.\n\n" "@param vertices: the vertices to be analysed. If C{None}, all vertices\n" " will be considered.\n" "@param mode: which neighbors should be considered for directed graphs.\n" @@ -15953,7 +17030,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"fundamental_cycles", (PyCFunction) igraphmodule_Graph_fundamental_cycles, METH_VARARGS | METH_KEYWORDS, - "fundamental_cycles(start_vid=None, cutoff=None)\n--\n\n" + "fundamental_cycles(start_vid=None, cutoff=None, weights=None)\n--\n\n" "Finds a single fundamental cycle basis of the graph\n\n" "@param start_vid: when C{None} or negative, a complete fundamental cycle basis is\n" " returned. When it is a vertex or a vertex ID, the fundamental cycles\n" @@ -15962,11 +17039,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param cutoff: when C{None} or negative, a complete cycle basis is returned. Otherwise\n" " the BFS is stopped after this many steps, so the result will effectively\n" " include cycles of length M{2 * cutoff + 1} or shorter only.\n" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" "@return: the cycle basis as a list of tuples containing edge IDs" }, {"minimum_cycle_basis", (PyCFunction) igraphmodule_Graph_minimum_cycle_basis, METH_VARARGS | METH_KEYWORDS, - "minimum_cycle_basis(cutoff=None, complete=True, use_cycle_order=True)\n--\n\n" + "minimum_cycle_basis(cutoff=None, complete=True, use_cycle_order=True, weights=None)\n--\n\n" "Computes a minimum cycle basis of the graph\n\n" "@param cutoff: when C{None} or negative, a complete minimum cycle basis is returned.\n" " Otherwise only those cycles in the result will be part of some minimum\n" @@ -15982,8 +17061,28 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param use_cycle_order: if C{True}, every cycle is returned in natural\n" " order: the edge IDs will appear ordered along the cycle. If C{False},\n" " no guarantees are given about the ordering of edge IDs within cycles.\n" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" "@return: the cycle basis as a list of tuples containing edge IDs" }, + {"simple_cycles", (PyCFunction) igraphmodule_Graph_simple_cycles, + METH_VARARGS | METH_KEYWORDS, + "simple_cycles(mode=None, min=-1, max=-1, output=\"vpath\")\n--\n\n" + "Finds simple cycles in a graph\n\n" + "@param mode: for directed graphs, specifies how the edge directions\n" + " should be taken into account. C{\"all\"} means that the edge directions\n" + " must be ignored, C{\"out\"} means that the edges must be oriented away\n" + " from the root, C{\"in\"} means that the edges must be oriented\n" + " towards the root. Ignored for undirected graphs.\n" + "@param min: the minimum number of vertices in a cycle\n" + " for it to be returned.\n" + "@param max: the maximum number of vertices in a cycle\n" + " for it to be considered.\n" + "@param output: determines what should be returned. If this is\n" + " C{\"vpath\"}, a list of tuples of vertex IDs will be returned. If this is\n" + " C{\"epath\"}, edge IDs are returned instead of vertex IDs.\n" + "@return: see the documentation of the C{output} parameter.\n" + }, /********************/ /* LAYOUT FUNCTIONS */ @@ -16239,7 +17338,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "forces among the vertices and then the physical system is simulated\n" "until it reaches an equilibrium or the maximal number of iterations is\n" "reached.\n\n" - "See U{http://www.schmuhl.org/graphopt/} for the original graphopt.\n\n" + "See U{https://web.archive.org/web/20220611030748/http://www.schmuhl.org/graphopt/}\n" + "and U{https://sourceforge.net/projects/graphopt/} for the original graphopt.\n\n" "@param niter: the number of iterations to perform. Should be a couple\n" " of hundred in general.\n\n" "@param node_charge: the charge of the vertices, used to calculate electric\n" @@ -16554,7 +17654,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Read_DIMACS(f, directed=False)\n--\n\n" "Reads a graph from a file conforming to the DIMACS minimum-cost flow file format.\n\n" "For the exact description of the format, see\n" - "U{http://lpsolve.sourceforge.net/5.5/DIMACS.htm}\n\n" + "U{https://lpsolve.sourceforge.net/5.5/DIMACS.htm}\n\n" "Restrictions compared to the official description of the format:\n\n" " - igraph's DIMACS reader requires only three fields in an arc definition,\n" " describing the edge's source and target node and its capacity.\n" @@ -16590,7 +17690,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Read_GraphDB(f, directed=False)\n--\n\n" "Reads a GraphDB format file and creates a graph based on it.\n\n" "GraphDB is a binary format, used in the graph database for\n" - "isomorphism testing (see U{http://amalfi.dis.unina.it/graph/}).\n\n" + "isomorphism testing (see U{https://mivia.unisa.it/datasets/graph-database/arg-database/}).\n\n" "@param f: the name of the file or a Python file handle\n" "@param directed: whether the generated graph should be directed.\n"}, /* interface to igraph_read_graph_graphml */ @@ -16749,9 +17849,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_write_graph_edgelist */ {"write_graphml", (PyCFunction) igraphmodule_Graph_write_graphml, METH_VARARGS | METH_KEYWORDS, - "write_graphml(f)\n--\n\n" + "write_graphml(f, prefixattr=True)\n--\n\n" "Writes the graph to a GraphML file.\n\n" "@param f: the name of the file to be written or a Python file handle\n" + "@param prefixattr: whether attribute names in the written file should be\n" + " prefixed with C{g_}, C{v_} and C{e_} for graph, vertex and edge\n" + " attributes, respectively. This might be needed to ensure the uniqueness\n" + " of attribute identifiers in the written GraphML file.\n" }, /* interface to igraph_write_graph_leda */ {"write_leda", (PyCFunction) igraphmodule_Graph_write_leda, @@ -16809,7 +17913,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "algorithm.\n\n" "Passing the permutation returned here to L{permute_vertices()} will\n" "transform the graph into its canonical form.\n\n" - "See U{http://www.tcs.hut.fi/Software/bliss/index.html} for more information\n" + "See U{https://users.aalto.fi/~tjunttil/bliss/} for more information\n" "about the BLISS algorithm and canonical permutations.\n\n" "@param sh: splitting heuristics for graph as a case-insensitive string,\n" " with the following possible values:\n\n" @@ -16835,7 +17939,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "count_automorphisms(sh=\"fl\", color=None)\n--\n\n" "Calculates the number of automorphisms of a graph using the BLISS isomorphism\n" "algorithm.\n\n" - "See U{http://www.tcs.hut.fi/Software/bliss/index.html} for more information\n" + "See U{https://users.aalto.fi/~tjunttil/bliss/} for more information\n" "about the BLISS algorithm and canonical permutations.\n\n" "@param sh: splitting heuristics for graph as a case-insensitive string,\n" " with the following possible values:\n\n" @@ -16886,7 +17990,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " sh1=\"fl\", sh2=None, color1=None, color2=None)\n--\n\n" "Checks whether the graph is isomorphic to another graph, using the\n" "BLISS isomorphism algorithm.\n\n" - "See U{http://www.tcs.hut.fi/Software/bliss/index.html} for more information\n" + "See U{https://users.aalto.fi/~tjunttil/bliss/} for more information\n" "about the BLISS algorithm.\n\n" "@param other: the other graph with which we want to compare the graph.\n" "@param color1: optional vector storing the coloring of the vertices of\n" @@ -17491,14 +18595,17 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /********************************/ {"cliques", (PyCFunction) igraphmodule_Graph_cliques, METH_VARARGS | METH_KEYWORDS, - "cliques(min=0, max=0)\n--\n\n" + "cliques(min=0, max=0, max_results=None)\n--\n\n" "Returns some or all cliques of the graph as a list of tuples.\n\n" "A clique is a complete subgraph -- a set of vertices where an edge\n" "is present between any two of them (excluding loops)\n\n" "@param min: the minimum size of cliques to be returned. If zero or\n" " negative, no lower bound will be used.\n" "@param max: the maximum size of cliques to be returned. If zero or\n" - " negative, no upper bound will be used."}, + " negative, no upper bound will be used.\n" + "@param max_results: the maximum number of results to return. C{None}\n" + " means no limit on the number of results.\n" + }, {"largest_cliques", (PyCFunction) igraphmodule_Graph_largest_cliques, METH_NOARGS, "largest_cliques()\n--\n\n" @@ -17510,7 +18617,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " L{maximal_cliques()} for the maximal cliques"}, {"maximal_cliques", (PyCFunction) igraphmodule_Graph_maximal_cliques, METH_VARARGS | METH_KEYWORDS, - "maximal_cliques(min=0, max=0, file=None)\n--\n\n" + "maximal_cliques(min=0, max=0, file=None, max_results=None)\n--\n\n" "Returns the maximal cliques of the graph as a list of tuples.\n\n" "A maximal clique is a clique which can't be extended by adding any other\n" "vertex to it. A maximal clique is not necessarily one of the largest\n" @@ -17524,6 +18631,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param file: a file object or the name of the file to write the results\n" " to. When this argument is C{None}, the maximal cliques will be returned\n" " as a list of lists.\n" + "@param max_results: the maximum number of results to return. C{None}\n" + " means no limit on the number of results.\n" "@return: the maximal cliques of the graph as a list of lists, or C{None}\n" " if the C{file} argument was given." "@see: L{largest_cliques()} for the largest cliques."}, @@ -17536,14 +18645,17 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"independent_vertex_sets", (PyCFunction) igraphmodule_Graph_independent_vertex_sets, METH_VARARGS | METH_KEYWORDS, - "independent_vertex_sets(min=0, max=0)\n--\n\n" + "independent_vertex_sets(min=0, max=0, max_results=None)\n--\n\n" "Returns some or all independent vertex sets of the graph as a list of tuples.\n\n" "Two vertices are independent if there is no edge between them. Members\n" "of an independent vertex set are mutually independent.\n\n" "@param min: the minimum size of sets to be returned. If zero or\n" " negative, no lower bound will be used.\n" "@param max: the maximum size of sets to be returned. If zero or\n" - " negative, no upper bound will be used."}, + " negative, no upper bound will be used.\n" + "@param max_results: the maximum number of results to return. C{None}\n" + " means no limit on the number of results.\n" + }, {"largest_independent_vertex_sets", (PyCFunction) igraphmodule_Graph_largest_independent_vertex_sets, METH_NOARGS, @@ -17558,8 +18670,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " (nonextendable) independent vertex sets"}, {"maximal_independent_vertex_sets", (PyCFunction) igraphmodule_Graph_maximal_independent_vertex_sets, - METH_NOARGS, - "maximal_independent_vertex_sets()\n--\n\n" + METH_VARARGS | METH_KEYWORDS, + "maximal_independent_vertex_sets(min=0, max=0, max_results=None)\n--\n\n" "Returns the maximal independent vertex sets of the graph as a list of tuples.\n\n" "A maximal independent vertex set is an independent vertex set\n" "which can't be extended by adding any other vertex to it. A maximal\n" @@ -17568,6 +18680,12 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "B{Reference}: S. Tsukiyama, M. Ide, H. Ariyoshi and I. Shirawaka: A new\n" "algorithm for generating all the maximal independent sets.\n" "I{SIAM J Computing}, 6:505-517, 1977.\n\n" + "@param min: the minimum size of sets to be returned. If zero or\n" + " negative, no lower bound will be used.\n" + "@param max: the maximum size of sets to be returned. If zero or\n" + " negative, no upper bound will be used.\n" + "@param max_results: the maximum number of results to return. C{None}\n" + " means no limit on the number of results.\n\n" "@see: L{largest_independent_vertex_sets()} for the largest independent\n" " vertex sets\n" }, @@ -17673,21 +18791,43 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "\n" "@see: modularity()\n" }, + {"community_fluid_communities", + (PyCFunction) igraphmodule_Graph_community_fluid_communities, + METH_VARARGS | METH_KEYWORDS, + "community_fluid_communities(no_of_communities)\n--\n\n" + "Community detection based on fluids interacting on the graph.\n\n" + "The algorithm is based on the simple idea of several fluids interacting\n" + "in a non-homogeneous environment (the graph topology), expanding and\n" + "contracting based on their interaction and density. Weighted graphs are\n" + "not supported.\n\n" + "B{Reference}\n\n" + " - Parés F, Gasulla DG, et. al. (2018) Fluid Communities: A Competitive,\n" + " Scalable and Diverse Community Detection Algorithm. In: Complex Networks\n" + " & Their Applications VI: Proceedings of Complex Networks 2017 (The Sixth\n" + " International Conference on Complex Networks and Their Applications),\n" + " Springer, vol 689, p 229. https://doi.org/10.1007/978-3-319-72150-7_19\n\n" + "@param no_of_communities: The number of communities to be found. Must be\n" + " greater than 0 and fewer than number of vertices in the graph.\n" + "@return: a list with the community membership of each vertex.\n" + "@note: The graph must be simple and connected. Edge directions will be\n" + " ignored if the graph is directed.\n" + "@note: Time complexity: O(|E|)\n", + }, {"community_infomap", (PyCFunction) igraphmodule_Graph_community_infomap, METH_VARARGS | METH_KEYWORDS, "community_infomap(edge_weights=None, vertex_weights=None, trials=10)\n--\n\n" "Finds the community structure of the network according to the Infomap\n" "method of Martin Rosvall and Carl T. Bergstrom.\n\n" - "See U{http://www.mapequation.org} for a visualization of the algorithm\n" - "or one of the references provided below.\n" - "B{References}\n" + "See U{https://www.mapequation.org} for a visualization of the algorithm\n" + "or one of the references provided below.\n\n" + "B{References}\n\n" " - M. Rosvall and C. T. Bergstrom: I{Maps of information flow reveal\n" " community structure in complex networks}. PNAS 105, 1118 (2008).\n" - " U{http://arxiv.org/abs/0707.0609}\n" + " U{https://arxiv.org/abs/0707.0609}\n" " - M. Rosvall, D. Axelsson and C. T. Bergstrom: I{The map equation}.\n" " I{Eur Phys J Special Topics} 178, 13 (2009).\n" - " U{http://arxiv.org/abs/0906.1405}\n" + " U{https://arxiv.org/abs/0906.1405}\n" "\n" "@param edge_weights: name of an edge attribute or a list containing\n" " edge weights.\n" @@ -17700,7 +18840,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"community_label_propagation", (PyCFunction) igraphmodule_Graph_community_label_propagation, METH_VARARGS | METH_KEYWORDS, - "community_label_propagation(weights=None, initial=None, fixed=None)\n--\n\n" + "community_label_propagation(weights=None, initial=None, fixed=None, variant=\"dominance\")\n--\n\n" "Finds the community structure of the graph according to the label\n" "propagation method of Raghavan et al.\n\n" "Initially, each vertex is assigned a different label. After that,\n" @@ -17716,7 +18856,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "B{Reference}: Raghavan, U.N. and Albert, R. and Kumara, S. Near linear\n" "time algorithm to detect community structures in large-scale\n" "networks. I{Phys Rev E} 76:036106, 2007.\n" - "U{http://arxiv.org/abs/0709.2938}.\n" + "U{https://arxiv.org/abs/0709.2938}.\n" "\n" "@param weights: name of an edge attribute or a list containing\n" " edge weights\n" @@ -17730,6 +18870,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " It only makes sense if initial labels are also given. Unlabeled\n" " vertices cannot be fixed. Note that vertex attribute names are not\n" " accepted here.\n" + "@param variant: the variant of the algorithm to use: C{\"dominance\"}, \n" + " C{\"retention\"} or C{\"fast\"}. See the documentation of the C core\n" + " of igraph for details.\n" "@return: the resulting membership vector\n" }, {"community_leading_eigenvector", (PyCFunction) igraphmodule_Graph_community_leading_eigenvector, @@ -17771,7 +18914,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "\n" "B{Reference}: VD Blondel, J-L Guillaume, R Lambiotte and E Lefebvre: Fast\n" "unfolding of community hierarchies in large networks. J Stat Mech\n" - "P10008 (2008), U{http://arxiv.org/abs/0803.0476}\n" + "P10008 (2008), U{https://arxiv.org/abs/0803.0476}\n" "\n" "Attention: this function is wrapped in a more convenient syntax in the\n" "derived class L{Graph}. It is advised to use that instead of this version.\n\n" @@ -17802,12 +18945,18 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "is typically high. So we gradually remove the edge with the highest\n" "betweenness from the network and recalculate edge betweenness after every\n" "removal, as long as all edges are removed.\n\n" + "When edge weights are given, the ratio of betweenness and weight values\n" + "is used to choose which edges to remove first, as described in\n" + "M. E. J. Newman: Analysis of Weighted Networks (2004), Section C.\n" + "Thus, edges with large weights are treated as strong connections,\n" + "and will be removed later than weak connections having similar betweenness.\n" + "Weights are also used for calculating modularity.\n\n" "Attention: this function is wrapped in a more convenient syntax in the\n" "derived class L{Graph}. It is advised to use that instead of this version.\n\n" "@param directed: whether to take into account the directedness of the edges\n" " when we calculate the betweenness values.\n" "@param weights: name of an edge attribute or a list containing\n" - " edge weights.\n\n" + " edge weights. Higher weights indicate stronger connections.\n\n" "@return: a tuple with the merge matrix that describes the dendrogram\n" " and the modularity scores before each merge. The modularity scores\n" " use the weights if the original graph was weighted.\n" @@ -17869,6 +19018,42 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " original implementation is used.\n" "@return: the community membership vector.\n" }, + {"community_voronoi", + (PyCFunction) igraphmodule_Graph_community_voronoi, + METH_VARARGS | METH_KEYWORDS, + "community_voronoi(lengths=None, weights=None, mode=\"out\", radius=None)\n--\n\n" + "Finds communities using Voronoi partitioning.\n\n" + "This function finds communities using a Voronoi partitioning of vertices based\n" + "on the given edge lengths divided by the edge clustering coefficient.\n" + "The generator vertices are chosen to be those with the largest local relative\n" + "density within a radius, with the local relative density of a vertex defined as\n" + "C{s * m / (m + k)}, where s is the strength of the vertex, m is the number of\n" + "edges within the vertex's first order neighborhood, while k is the number of\n" + "edges with only one endpoint within this neighborhood.\n\n" + "B{References}\n\n" + " - Deritei et al., Community detection by graph Voronoi diagrams,\n" + " New Journal of Physics 16, 063007 (2014)\n" + " U{https://doi.org/10.1088/1367-2630/16/6/063007}\n" + " - Molnár et al., Community Detection in Directed Weighted Networks\n" + " using Voronoi Partitioning, Scientific Reports 14, 8124 (2024)\n" + " U{https://doi.org/10.1038/s41598-024-58624-4}\n\n" + "@param lengths: edge lengths, or C{None} to consider all edges as having\n" + " unit length. Voronoi partitioning will use edge lengths equal to\n" + " lengths / ECC where ECC is the edge clustering coefficient.\n" + "@param weights: edge weights, or C{None} to consider all edges as having\n" + " unit weight. Weights are used when selecting generator points, as well\n" + " as for computing modularity.\n" + "@param mode: if C{\"out\"} (the default), distances from generator points to all other\n" + " nodes are considered. If C{\"in\"}, the reverse distances are used.\n" + " If C{\"all\"}, edge directions are ignored. This parameter is ignored\n" + " for undirected graphs.\n" + "@param radius: the radius/resolution to use when selecting generator points.\n" + " The larger this value, the fewer partitions there will be. Pass C{None}\n" + " to automatically select the radius that maximizes modularity.\n" + "@return: a tuple containing the membership vector, generator vertices, and\n" + " modularity score: (membership, generators, modularity).\n" + "@rtype: tuple\n" + }, {"community_leiden", (PyCFunction) igraphmodule_Graph_community_leiden, METH_VARARGS | METH_KEYWORDS, @@ -17910,7 +19095,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Attention: this function is wrapped in a more convenient syntax in the\n" "derived class L{Graph}. It is advised to use that instead of this version.\n\n" "B{Reference}: Pascal Pons, Matthieu Latapy: Computing communities in large\n" - "networks using random walks, U{http://arxiv.org/abs/physics/0512106}.\n\n" + "networks using random walks, U{https://arxiv.org/abs/physics/0512106}.\n\n" "@param weights: name of an edge attribute or a list containing\n" " edge weights\n" "@return: a tuple with the list of merges and the modularity scores corresponding\n" @@ -17965,6 +19150,22 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " the given length (shorter if the random walk got stuck).\n" }, + /**********************/ + /* SPATIAL GRAPHS */ + /**********************/ + {"Nearest_Neighbor_Graph", (PyCFunction)igraphmodule_Graph_Nearest_Neighbor_Graph, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Nearest_Neighbor_Graph(points, k=1, r=-1, metric=\"euclidean\", directed=False)\n--\n\n" + "Constructs a k nearest neighbor graph of a give point set. Each point is\n" + "connected to at most k spatial neighbors within a radius of 1.\n\n" + "@param points: coordinates of the points to use, in an arbitrary number of dimensions\n" + "@param k: at most how many neighbors to connect to. Pass a negative value to ignore\n" + "@param r: only neighbors within this radius are considered. Pass a negative value to ignore\n" + "@param metric: the metric to use. C{\"euclidean\"} and C{\"manhattan\"} are supported.\n" + "@param directed: whethe to create directed edges.\n" + "@return: the nearest neighbor graph.\n" + }, + /**********************/ /* INTERNAL FUNCTIONS */ /**********************/ diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index cff5df44e..cd57da92a 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -142,12 +142,8 @@ static int igraphmodule_clear(PyObject *m) { return 0; } -static igraph_error_t igraphmodule_igraph_interrupt_hook(void* data) { - if (PyErr_CheckSignals()) { - IGRAPH_FINALLY_FREE(); - return IGRAPH_INTERRUPTED; - } - return IGRAPH_SUCCESS; +static igraph_bool_t igraphmodule_igraph_interrupt_hook() { + return PyErr_CheckSignals(); } igraph_error_t igraphmodule_igraph_progress_hook(const char* message, igraph_real_t percent, @@ -231,6 +227,38 @@ PyObject* igraphmodule_set_status_handler(PyObject* self, PyObject* o) { Py_RETURN_NONE; } +PyObject* igraphmodule_align_layout(PyObject* self, PyObject* args, PyObject* kwds) { + static char* kwlist[] = {"graph", "layout", NULL}; + PyObject *graph_o, *layout_o; + PyObject *res; + igraph_t *graph; + igraph_matrix_t layout; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, &graph_o, &layout_o)) { + return NULL; + } + + if (igraphmodule_PyObject_to_igraph_t(graph_o, &graph)) { + return NULL; + } + + if (igraphmodule_PyObject_to_matrix_t(layout_o, &layout, "layout")) { + return NULL; + } + + if (igraph_layout_align(graph, &layout)) { + igraphmodule_handle_igraph_error(); + igraph_matrix_destroy(&layout); + return NULL; + } + + res = igraphmodule_matrix_t_to_PyList(&layout, IGRAPHMODULE_TYPE_FLOAT); + + igraph_matrix_destroy(&layout); + + return res; +} + PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwds) { static char* kwlist[] = {"vs", "coords", NULL}; PyObject *vs, *o, *o1 = 0, *o2 = 0, *o1_float, *o2_float, *coords = Py_False; @@ -317,7 +345,7 @@ PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwd igraph_matrix_destroy(&mtrx); return NULL; } - if (igraph_convex_hull(&mtrx, &result, 0)) { + if (igraph_convex_hull_2d(&mtrx, &result, 0)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&mtrx); igraph_vector_int_destroy(&result); @@ -331,7 +359,7 @@ PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwd igraph_matrix_destroy(&mtrx); return NULL; } - if (igraph_convex_hull(&mtrx, 0, &resmat)) { + if (igraph_convex_hull_2d(&mtrx, 0, &resmat)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&mtrx); igraph_matrix_destroy(&resmat); @@ -355,10 +383,10 @@ PyObject* igraphmodule_community_to_membership(PyObject *self, igraph_vector_int_t result, csize, *csize_p = 0; Py_ssize_t nodes, steps; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!nn|O", kwlist, - &PyList_Type, &merges_o, &nodes, &steps, &return_csize)) return NULL; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "Onn|O", kwlist, + &merges_o, &nodes, &steps, &return_csize)) return NULL; - if (igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(merges_o, &merges, 2, "merges")) { + if (igraphmodule_PyObject_to_matrix_int_t_with_minimum_column_count(merges_o, &merges, 2, "merges")) { return NULL; } @@ -641,7 +669,7 @@ PyObject* igraphmodule_split_join_distance(PyObject *self, static char* kwlist[] = { "comm1", "comm2", NULL }; PyObject *comm1_o, *comm2_o; igraph_vector_int_t comm1, comm2; - igraph_integer_t distance12, distance21; + igraph_int_t distance12, distance21; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, &comm1_o, &comm2_o)) @@ -666,7 +694,7 @@ PyObject* igraphmodule_split_join_distance(PyObject *self, igraph_vector_int_destroy(&comm1); igraph_vector_int_destroy(&comm2); - /* sizeof(Py_ssize_t) is most likely the same as sizeof(igraph_integer_t), + /* sizeof(Py_ssize_t) is most likely the same as sizeof(igraph_int_t), * but even if it isn't, we cast explicitly so we are safe */ return Py_BuildValue("nn", (Py_ssize_t)distance12, (Py_ssize_t)distance21); } @@ -687,7 +715,7 @@ PyObject *igraphmodule_umap_compute_weights( PyObject *result_o; igraphmodule_GraphObject * graph; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, &graph_o, &dist_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O", kwlist, igraphmodule_GraphType, &graph_o, &dist_o)) return NULL; /* Initialize distances */ @@ -794,6 +822,10 @@ static PyMethodDef igraphmodule_methods[] = METH_VARARGS | METH_KEYWORDS, "_power_law_fit(data, xmin=-1, force_continuous=False, p_precision=0.01)\n--\n\n" }, + {"_align_layout", (PyCFunction)igraphmodule_align_layout, + METH_VARARGS | METH_KEYWORDS, + "_align_layout(graph, layout)\n--\n\n" + }, {"convex_hull", (PyCFunction)igraphmodule_convex_hull, METH_VARARGS | METH_KEYWORDS, "convex_hull(vs, coords=False)\n--\n\n" @@ -1035,9 +1067,10 @@ PyObject* PyInit__igraph(void) PyObject* m; static void *PyIGraph_API[PyIGraph_API_pointers]; PyObject *c_api_object; + igraph_error_t retval; /* Prevent linking 64-bit igraph to 32-bit Python */ - PY_IGRAPH_ASSERT_AT_BUILD_TIME(sizeof(igraph_integer_t) >= sizeof(Py_ssize_t)); + PY_IGRAPH_ASSERT_AT_BUILD_TIME(sizeof(igraph_int_t) >= sizeof(Py_ssize_t)); /* Check if the module is already initialized (possibly in another Python * interpreter. If so, bail out as we don't support this. */ @@ -1047,6 +1080,14 @@ PyObject* PyInit__igraph(void) INITERROR; } + /* Initialize the igraph library */ + retval = igraph_setup(); + if (retval != IGRAPH_SUCCESS) { + PyErr_Format(PyExc_RuntimeError, "Failed to initialize the C core of " + "the igraph library, code: %d", retval); + INITERROR; + } + /* Run basic initialization of the pyhelpers.c module */ if (igraphmodule_helpers_init()) { INITERROR; @@ -1117,9 +1158,6 @@ PyObject* PyInit__igraph(void) PyModule_AddIntConstant(m, "GET_ADJACENCY_LOWER", IGRAPH_GET_ADJACENCY_LOWER); PyModule_AddIntConstant(m, "GET_ADJACENCY_BOTH", IGRAPH_GET_ADJACENCY_BOTH); - PyModule_AddIntConstant(m, "REWIRING_SIMPLE", IGRAPH_REWIRING_SIMPLE); - PyModule_AddIntConstant(m, "REWIRING_SIMPLE_LOOPS", IGRAPH_REWIRING_SIMPLE_LOOPS); - PyModule_AddIntConstant(m, "ADJ_DIRECTED", IGRAPH_ADJ_DIRECTED); PyModule_AddIntConstant(m, "ADJ_UNDIRECTED", IGRAPH_ADJ_UNDIRECTED); PyModule_AddIntConstant(m, "ADJ_MAX", IGRAPH_ADJ_MAX); diff --git a/src/_igraph/igraphmodule_api.h b/src/_igraph/igraphmodule_api.h index f7eeb932b..8ab364b50 100644 --- a/src/_igraph/igraphmodule_api.h +++ b/src/_igraph/igraphmodule_api.h @@ -56,25 +56,8 @@ extern "C" { /* Return -1 and set exception on error, 0 on success */ static int import_igraph(void) { - PyObject *c_api_object; - PyObject *module; - - module = PyImport_ImportModule("igraph._igraph"); - if (module == 0) - return -1; - - c_api_object = PyObject_GetAttrString(module, "_C_API"); - if (c_api_object == 0) { - Py_DECREF(module); - return -1; - } - - if (PyCObject_Check(c_api_object)) - PyIGraph_API = (void**)PyCObject_AsVoidPtr(c_api_object); - - Py_DECREF(c_api_object); - Py_DECREF(module); - return 0; + PyIGraph_API = (void **)PyCapsule_Import("igraph._igraph._C_API", 0); + return (PyIGraph_API != NULL) ? 0 : -1; } #endif diff --git a/src/_igraph/indexing.c b/src/_igraph/indexing.c index 5643cabba..8da3f600e 100644 --- a/src/_igraph/indexing.c +++ b/src/_igraph/indexing.c @@ -31,8 +31,8 @@ /***************************************************************************/ static PyObject* igraphmodule_i_Graph_adjmatrix_indexing_get_value_for_vertex_pair( - igraph_t* graph, igraph_integer_t from, igraph_integer_t to, PyObject* values) { - igraph_integer_t eid; + igraph_t* graph, igraph_int_t from, igraph_int_t to, PyObject* values) { + igraph_int_t eid; PyObject* result; /* Retrieving a single edge */ @@ -53,14 +53,14 @@ static PyObject* igraphmodule_i_Graph_adjmatrix_indexing_get_value_for_vertex_pa } static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, - igraph_integer_t from, igraph_vs_t* to, igraph_neimode_t neimode, + igraph_int_t from, igraph_vs_t* to, igraph_neimode_t neimode, PyObject* values); PyObject* igraphmodule_Graph_adjmatrix_get_index(igraph_t* graph, PyObject* row_index, PyObject* column_index, PyObject* attr_name) { PyObject *result = 0, *values; igraph_vs_t vs1, vs2; - igraph_integer_t vid1 = -1, vid2 = -1; + igraph_int_t vid1 = -1, vid2 = -1; char* attr; if (igraphmodule_PyObject_to_vs_t(row_index, &vs1, graph, 0, &vid1)) @@ -131,10 +131,10 @@ PyObject* igraphmodule_Graph_adjmatrix_get_index(igraph_t* graph, } static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, - igraph_integer_t from, igraph_vs_t* to, igraph_neimode_t neimode, + igraph_int_t from, igraph_vs_t* to, igraph_neimode_t neimode, PyObject* values) { igraph_vector_int_t eids; - igraph_integer_t eid, i, n, v; + igraph_int_t eid, i, n, v; igraph_vit_t vit; PyObject *result = 0, *item; @@ -142,7 +142,7 @@ static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, /* Simple case: all edges */ IGRAPH_PYCHECK(igraph_vector_int_init(&eids, 0)); IGRAPH_FINALLY(igraph_vector_int_destroy, &eids); - IGRAPH_PYCHECK(igraph_incident(graph, &eids, from, neimode)); + IGRAPH_PYCHECK(igraph_incident(graph, &eids, from, neimode, IGRAPH_LOOPS)); n = igraph_vector_int_size(&eids); result = igraphmodule_PyList_Zeroes(igraph_vcount(graph)); @@ -263,12 +263,12 @@ void igraphmodule_i_Graph_adjmatrix_set_index_data_destroy( } static int igraphmodule_i_Graph_adjmatrix_set_index_row(igraph_t* graph, - igraph_integer_t from, igraph_vs_t* to, igraph_neimode_t neimode, + igraph_int_t from, igraph_vs_t* to, igraph_neimode_t neimode, PyObject* values, PyObject* new_value, igraphmodule_i_Graph_adjmatrix_set_index_data_t* data) { PyObject *iter = 0, *item; igraph_vit_t vit; - igraph_integer_t v, v1, v2, eid; + igraph_int_t v, v1, v2, eid; igraph_bool_t deleting, ok = true; /* Check whether new_value is an iterable (and not a string). If not, @@ -423,7 +423,7 @@ int igraphmodule_Graph_adjmatrix_set_index(igraph_t* graph, PyObject *values; igraph_vs_t vs1, vs2; igraph_vit_t vit; - igraph_integer_t vid1 = -1, vid2 = -1, eid = -1; + igraph_int_t vid1 = -1, vid2 = -1, eid = -1; igraph_bool_t ok = true; igraphmodule_i_Graph_adjmatrix_set_index_data_t data; char* attr; diff --git a/src/_igraph/random.c b/src/_igraph/random.c index aef52c379..0552aba7d 100644 --- a/src/_igraph/random.c +++ b/src/_igraph/random.c @@ -112,7 +112,7 @@ PyObject* igraph_rng_Python_set_generator(PyObject* self, PyObject* object) { GET_FUNC("random"); new_state.random_func = func; GET_FUNC("gauss"); new_state.gauss_func = func; - /* construct the arguments of getrandbits(RNG_BITS) and randint(0, 2 ^ RNG_BITS-1) + /* construct the arguments of getrandbits(RNG_BITS) and randint(0, (2^RNG_BITS)-1) * in advance */ new_state.rng_bits_as_pyobject = PyLong_FromLong(RNG_BITS); if (new_state.rng_bits_as_pyobject == 0) { diff --git a/src/_igraph/utils.c b/src/_igraph/utils.c deleted file mode 100644 index 2fd58420c..000000000 --- a/src/_igraph/utils.c +++ /dev/null @@ -1,70 +0,0 @@ -/* - IGraph library. - Copyright (C) 2006-2023 Tamas Nepusz - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - 02110-1301 USA - -*/ - -#include "utils.h" - -/************************ Miscellaneous functions *************************/ - -/** - * Helper function that automatically selects a shortest path algorithm based on - * a graph, its weight vector and the source vertex set (if any). - */ -igraphmodule_shortest_path_algorithm_t igraphmodule_select_shortest_path_algorithm( - const igraph_t* graph, const igraph_vector_t* weights, const igraph_vs_t* from_vs, - igraph_neimode_t mode, igraph_bool_t allow_johnson -) { - igraph_error_t retval; - igraph_integer_t vs_size; - - /* Select the most suitable algorithm */ - if (weights && igraph_vector_size(weights) > 0) { - if (igraph_vector_min(weights) >= 0) { - /* Only positive weights, use Dijkstra's algorithm */ - return IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA; - } else if (allow_johnson) { - /* There are negative weights. For a small number of sources, use Bellman-Ford. - * Otherwise, use Johnson's algorithm */ - if (from_vs) { - retval = igraph_vs_size(graph, from_vs, &vs_size); - } else { - retval = IGRAPH_SUCCESS; - vs_size = IGRAPH_INTEGER_MAX; - } - if (retval == IGRAPH_SUCCESS) { - if (vs_size <= 100 || mode != IGRAPH_OUT) { - return IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_BELLMAN_FORD; - } else { - return IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_JOHNSON; - } - } else { - /* Error while calling igraph_vs_size(). Use Bellman-Ford. */ - return IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_BELLMAN_FORD; - } - } else { - /* Johnson's algorithm is disallowed, use Bellman-Ford */ - return IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_BELLMAN_FORD; - } - } else { - /* No weights or empty weight vector, use Dijstra, which should fall back to - * an unweighted algorithm */ - return IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA; - } -} diff --git a/src/_igraph/utils.h b/src/_igraph/utils.h deleted file mode 100644 index bc39b84b9..000000000 --- a/src/_igraph/utils.h +++ /dev/null @@ -1,40 +0,0 @@ -/* -*- mode: C -*- */ -/* - IGraph library. - Copyright (C) 2006-2023 Tamas Nepusz - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - 02110-1301 USA - -*/ - -#ifndef IGRAPHMODULE_UTILS_H -#define IGRAPHMODULE_UTILS_H - -#include "preamble.h" - -#include -#include -#include "convert.h" -#include "graphobject.h" - -/************************ Miscellaneous functions *************************/ - -igraphmodule_shortest_path_algorithm_t igraphmodule_select_shortest_path_algorithm( - const igraph_t* graph, const igraph_vector_t* weights, const igraph_vs_t* from_vs, - igraph_neimode_t mode, igraph_bool_t allow_johnson -); - -#endif diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index b15e67d90..0c1ad31e9 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -54,7 +54,7 @@ int igraphmodule_Vertex_Check(PyObject* obj) { * exception and returns zero if the vertex object is invalid. */ int igraphmodule_Vertex_Validate(PyObject* obj) { - igraph_integer_t n; + igraph_int_t n; igraphmodule_VertexObject *self; igraphmodule_GraphObject *graph; @@ -98,7 +98,7 @@ int igraphmodule_Vertex_Validate(PyObject* obj) { * changes, your existing vertex objects will point to elsewhere * (or they might even get invalidated). */ -PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer_t idx) { +PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_int_t idx) { return PyObject_CallFunction((PyObject*) igraphmodule_VertexType, "On", gref, (Py_ssize_t) idx); } @@ -110,7 +110,7 @@ PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer static int igraphmodule_Vertex_init(igraphmodule_EdgeObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "graph", "vid", NULL }; PyObject *g, *index_o = Py_None; - igraph_integer_t vid; + igraph_int_t vid; igraph_t *graph; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|O", kwlist, @@ -531,7 +531,7 @@ int igraphmodule_Vertex_set_attribute(igraphmodule_VertexObject* self, PyObject* /* result is NULL, check whether there was an error */ if (!PyErr_Occurred()) { /* no, there wasn't, so we must simply add the attribute */ - igraph_integer_t i, n = igraph_vcount(&o->g); + igraph_int_t i, n = igraph_vcount(&o->g); result = PyList_New(n); for (i = 0; i < n; i++) { if (i != self->idx) { @@ -572,9 +572,9 @@ PyObject* igraphmodule_Vertex_get_index(igraphmodule_VertexObject* self, void* c /** * \ingroup python_interface_vertex - * Returns the vertex index as an igraph_integer_t + * Returns the vertex index as an igraph_int_t */ -igraph_integer_t igraphmodule_Vertex_get_index_igraph_integer(igraphmodule_VertexObject* self) { +igraph_int_t igraphmodule_Vertex_get_index_igraph_integer(igraphmodule_VertexObject* self) { return self->idx; } @@ -618,7 +618,7 @@ static PyObject* _convert_to_edge_list(igraphmodule_VertexObject* vertex, PyObje for (i = 0; i < n; i++) { PyObject* idx = PyList_GetItem(obj, i); PyObject* edge; - igraph_integer_t idx_int; + igraph_int_t idx_int; if (!idx) { return NULL; @@ -663,7 +663,7 @@ static PyObject* _convert_to_vertex_list(igraphmodule_VertexObject* vertex, PyOb for (i = 0; i < n; i++) { PyObject* idx = PyList_GetItem(obj, i); PyObject* v; - igraph_integer_t idx_int; + igraph_int_t idx_int; if (!idx) { return NULL; @@ -840,10 +840,10 @@ PyMethodDef igraphmodule_Vertex_methods[] = { GRAPH_PROXY_METHOD_SPEC(neighbors, "neighbors"), GRAPH_PROXY_METHOD_SPEC_3(outdegree, "outdegree"), GRAPH_PROXY_METHOD_SPEC_3(pagerank, "pagerank"), - GRAPH_PROXY_METHOD_SPEC(predecessors, "predecessors"), + GRAPH_PROXY_METHOD_SPEC_3(predecessors, "predecessors"), GRAPH_PROXY_METHOD_SPEC(personalized_pagerank, "personalized_pagerank"), GRAPH_PROXY_METHOD_SPEC(strength, "strength"), - GRAPH_PROXY_METHOD_SPEC(successors, "successors"), + GRAPH_PROXY_METHOD_SPEC_3(successors, "successors"), {NULL} }; @@ -875,6 +875,9 @@ PyDoc_STRVAR( " >>> v[\"color\"] = \"red\" #doctest: +SKIP\n" " >>> print(v[\"color\"]) #doctest: +SKIP\n" " red\n" + "\n" + "@ivar index: Index of the vertex\n" + "@ivar graph: The graph the vertex belongs to\t" ); int igraphmodule_Vertex_register_type() { diff --git a/src/_igraph/vertexobject.h b/src/_igraph/vertexobject.h index ef8ce9e7b..21f1be9b7 100644 --- a/src/_igraph/vertexobject.h +++ b/src/_igraph/vertexobject.h @@ -35,7 +35,7 @@ typedef struct { PyObject_HEAD igraphmodule_GraphObject* gref; - igraph_integer_t idx; + igraph_int_t idx; long hash; } igraphmodule_VertexObject; @@ -44,8 +44,8 @@ extern PyTypeObject* igraphmodule_VertexType; int igraphmodule_Vertex_register_type(void); int igraphmodule_Vertex_Check(PyObject* obj); -PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer_t idx); -igraph_integer_t igraphmodule_Vertex_get_index_igraph_integer(igraphmodule_VertexObject* self); +PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_int_t idx); +igraph_int_t igraphmodule_Vertex_get_index_igraph_integer(igraphmodule_VertexObject* self); PyObject* igraphmodule_Vertex_update_attributes(PyObject* self, PyObject* args, PyObject* kwds); #endif diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index d511cd1d0..4e4f66e59 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -106,7 +106,7 @@ int igraphmodule_VertexSeq_init(igraphmodule_VertexSeqObject *self, igraph_vs_all(&vs); } else if (PyLong_Check(vsobj)) { /* We selected a single vertex */ - igraph_integer_t idx; + igraph_int_t idx; if (igraphmodule_PyObject_to_integer_t(vsobj, &idx)) { return -1; @@ -120,7 +120,7 @@ int igraphmodule_VertexSeq_init(igraphmodule_VertexSeqObject *self, igraph_vs_1(&vs, idx); } else { igraph_vector_int_t v; - igraph_integer_t n = igraph_vcount(&((igraphmodule_GraphObject*)g)->g); + igraph_int_t n = igraph_vcount(&((igraphmodule_GraphObject*)g)->g); if (igraphmodule_PyObject_to_vector_int_t(vsobj, &v)) return -1; if (!igraph_vector_int_isininterval(&v, 0, n-1)) { @@ -168,7 +168,7 @@ void igraphmodule_VertexSeq_dealloc(igraphmodule_VertexSeqObject* self) { */ Py_ssize_t igraphmodule_VertexSeq_sq_length(igraphmodule_VertexSeqObject* self) { igraph_t *g; - igraph_integer_t result; + igraph_int_t result; if (!self->gref) { return -1; @@ -190,7 +190,7 @@ Py_ssize_t igraphmodule_VertexSeq_sq_length(igraphmodule_VertexSeqObject* self) PyObject* igraphmodule_VertexSeq_sq_item(igraphmodule_VertexSeqObject* self, Py_ssize_t i) { igraph_t *g; - igraph_integer_t idx = -1; + igraph_int_t idx = -1; if (!self->gref) { return NULL; @@ -260,7 +260,7 @@ PyObject* igraphmodule_VertexSeq_attribute_names(igraphmodule_VertexSeqObject* s PyObject* igraphmodule_VertexSeq_get_attribute_values(igraphmodule_VertexSeqObject* self, PyObject* o) { igraphmodule_GraphObject *gr = self->gref; PyObject *result=0, *values, *item; - igraph_integer_t i, n; + igraph_int_t i, n; if (!igraphmodule_attribute_name_check(o)) return 0; @@ -409,7 +409,7 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb PyObject *dict, *list, *item; igraphmodule_GraphObject *gr; igraph_vector_int_t vs; - igraph_integer_t i, j, n, no_of_nodes; + igraph_int_t i, j, n, no_of_nodes; gr = self->gref; dict = ATTR_STRUCT_DICT(&gr->g)[ATTRHASH_IDX_VERTEX]; @@ -540,7 +540,7 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb /* We don't have attributes with the given name yet. Create an entry * in the dict, create a new list, fill with None for vertices not in the * sequence and copy the rest */ - igraph_integer_t n2 = igraph_vcount(&gr->g); + igraph_int_t n2 = igraph_vcount(&gr->g); list = PyList_New(n2); if (list == 0) { igraph_vector_int_destroy(&vs); @@ -605,7 +605,7 @@ PyObject* igraphmodule_VertexSeq_set_attribute_values(igraphmodule_VertexSeqObje */ PyObject* igraphmodule_VertexSeq_find(igraphmodule_VertexSeqObject *self, PyObject *args) { PyObject *item; - igraph_integer_t i; + igraph_int_t i; Py_ssize_t n; igraph_vit_t vit; @@ -683,7 +683,7 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, PyObject *args) { igraphmodule_VertexSeqObject *result; igraphmodule_GraphObject *gr; - igraph_integer_t igraph_idx, i, j, n, m; + igraph_int_t igraph_idx, i, j, n, m; igraph_bool_t working_on_whole_graph = igraph_vs_is_all(&self->vs); igraph_vector_int_t v, v2; @@ -787,7 +787,7 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, for (; i < n; i++) { PyObject *item2 = PyTuple_GetItem(args, i); - igraph_integer_t idx; + igraph_int_t idx; if (item2 == 0) { Py_DECREF(result); igraph_vector_int_destroy(&v); diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 4ce6ebd24..6a4e189b9 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2,7 +2,6 @@ igraph library. """ - __license__ = """ Copyright (C) 2006- The igraph development team @@ -48,8 +47,6 @@ IN, InternalError, OUT, - REWIRING_SIMPLE, - REWIRING_SIMPLE_LOOPS, STAR_IN, STAR_MUTUAL, STAR_OUT, @@ -109,7 +106,9 @@ _community_multilevel, _community_optimal_modularity, _community_edge_betweenness, + _community_fluid_communities, _community_spinglass, + _community_voronoi, _community_walktrap, _k_core, _community_leiden, @@ -225,6 +224,7 @@ from igraph.io.images import _write_graph_to_svg from igraph.layout import ( Layout, + align_layout, _layout, _layout_auto, _layout_sugiyama, @@ -239,6 +239,7 @@ intersection, operator_method_registry as _operator_method_registry, ) +from igraph.rewiring import _rewire from igraph.seq import EdgeSeq, VertexSeq, _add_proxy_methods from igraph.statistics import ( FittedPowerLaw, @@ -580,6 +581,23 @@ def is_weighted(self): """ return "weight" in self.edge_attributes() + ############################################# + # Neighbors + + def predecessors(self, vertex, loops=True, multiple=True): + """Returns the predecessors of a given vertex. + + Equivalent to calling the L{Graph.neighbors()} method with mode=C{\"in\"}. + """ + return self.neighbors(vertex, mode="in", loops=loops, multiple=multiple) + + def successors(self, vertex, loops=True, multiple=True): + """Returns the successors of a given vertex. + + Equivalent to calling the L{Graph.neighbors()} method with mode=C{\"out\"}. + """ + return self.neighbors(vertex, mode="out", loops=loops, multiple=multiple) + ############################################# # Vertex and edge sequence @property @@ -615,6 +633,7 @@ def es(self): disjoint_union = _operator_method_registry["disjoint_union"] union = _operator_method_registry["union"] intersection = _operator_method_registry["intersection"] + rewire = _rewire ############################################# # Adjacency/incidence @@ -658,7 +677,9 @@ def es(self): community_multilevel = _community_multilevel community_optimal_modularity = _community_optimal_modularity community_edge_betweenness = _community_edge_betweenness + community_fluid_communities = _community_fluid_communities community_spinglass = _community_spinglass + community_voronoi = _community_voronoi community_walktrap = _community_walktrap k_core = _k_core community_leiden = _community_leiden @@ -687,7 +708,7 @@ def es(self): ########################### # Paths/traversals - def get_all_simple_paths(self, v, to=None, cutoff=-1, mode="out"): + def get_all_simple_paths(self, v, to=None, minlen=0, maxlen=-1, mode="out", max_results=None): """Calculates all the simple paths from a given node to some other nodes (or all of them) in a graph. @@ -703,23 +724,19 @@ def get_all_simple_paths(self, v, to=None, cutoff=-1, mode="out"): paths. This can be a single vertex ID, a list of vertex IDs, a single vertex name, a list of vertex names or a L{VertexSeq} object. C{None} means all the vertices. - @param cutoff: maximum length of path that is considered. If negative, + @param minlen: minimum length of path that is considered. + @param maxlen: maximum length of path that is considered. If negative, paths of all lengths are considered. @param mode: the directionality of the paths. C{\"in\"} means to calculate incoming paths, C{\"out\"} means to calculate outgoing paths, C{\"all\"} means to calculate both ones. + @param max_results: the maximum number of results to return. C{None} means + no limit on the number of results. @return: all of the simple paths from the given node to every other reachable node in the graph in a list. Note that in case of mode=C{\"in\"}, the vertices in a path are returned in reversed order! """ - paths = self._get_all_simple_paths(v, to, cutoff, mode) - prev = 0 - result = [] - for index, item in enumerate(paths): - if item < 0: - result.append(paths[prev:index]) - prev = index + 1 - return result + return self._get_all_simple_paths(v, to, minlen, maxlen, mode, max_results) def path_length_hist(self, directed=True): """Returns the path length histogram of the graph @@ -782,7 +799,7 @@ def dfs(self, vid, mode=OUT): return (vids, parents) - def spanning_tree(self, weights=None, return_tree=True): + def spanning_tree(self, weights=None, return_tree=True, method="auto"): """Calculates a minimum spanning tree for a graph. B{Reference}: Prim, R.C. Shortest connection networks and some @@ -795,11 +812,16 @@ def spanning_tree(self, weights=None, return_tree=True): the minimum spanning tree instead (when C{return_tree} is C{False}). The default is C{True} for historical reasons as this argument was introduced in igraph 0.6. + @param method: the algorithm to use. C{"auto"} means that the algorithm + is selected automatically. C{"prim"} means that Prim's algorithm is + used. C{"kruskal"} means that Kruskal's algorithm is used. + C{"unweighted"} assumes that the graph is unweighted even if weights + are provided. @return: the spanning tree as a L{Graph} object if C{return_tree} is C{True} or the IDs of the edges that constitute the spanning tree if C{return_tree} is C{False}. """ - result = GraphBase._spanning_tree(self, weights) + result = GraphBase._spanning_tree(self, weights, method) if return_tree: return self.subgraph_edges(result, delete_vertices=False) return result @@ -860,7 +882,7 @@ def transitivity_avglocal_undirected(self, mode="nan", weights=None): networks. I{Nature} 393(6884):440-442, 1998. - Barrat A, Barthelemy M, Pastor-Satorras R and Vespignani A: The architecture of complex weighted networks. I{PNAS} 101, 3747 - (2004). U{http://arxiv.org/abs/cond-mat/0311416}. + (2004). U{https://arxiv.org/abs/cond-mat/0311416}. @param mode: defines how to treat vertices with degree less than two. If C{TRANSITIVITY_ZERO} or C{"zero"}, these vertices will have zero @@ -948,6 +970,13 @@ def Incidence(cls, *args, **kwds): deprecated("Graph.Incidence() is deprecated; use Graph.Biadjacency() instead") return cls.Biadjacency(*args, **kwds) + def are_connected(self, *args, **kwds): + """Deprecated alias to L{Graph.are_adjacent()}.""" + deprecated( + "Graph.are_connected() is deprecated; use Graph.are_adjacent() " "instead" + ) + return self.are_adjacent(*args, **kwds) + def get_incidence(self, *args, **kwds): """Deprecated alias to L{Graph.get_biadjacency()}.""" deprecated( @@ -1049,7 +1078,7 @@ def write(graph, filename, *args, **kwds): ############################################################## # Configuration singleton instance -config = init_configuration() +config: Configuration = init_configuration() """The main configuration object of igraph. Use this object to modify igraph's behaviour, typically when used in interactive mode. """ @@ -1093,7 +1122,9 @@ def write(graph, filename, *args, **kwds): _community_multilevel, _community_optimal_modularity, _community_edge_betweenness, + _community_fluid_communities, _community_spinglass, + _community_voronoi, _community_walktrap, _k_core, _community_leiden, @@ -1125,6 +1156,7 @@ def write(graph, filename, *args, **kwds): _cohesive_blocks, _connected_components, _add_proxy_methods, + _rewire, ) # Re-export from _igraph for API docs @@ -1240,8 +1272,6 @@ def write(graph, filename, *args, **kwds): "GET_ADJACENCY_UPPER", "IN", "OUT", - "REWIRING_SIMPLE", - "REWIRING_SIMPLE_LOOPS", "STAR_IN", "STAR_MUTUAL", "STAR_OUT", diff --git a/src/igraph/adjacency.py b/src/igraph/adjacency.py index 9a55c1bc4..3d42b0bb2 100644 --- a/src/igraph/adjacency.py +++ b/src/igraph/adjacency.py @@ -113,26 +113,35 @@ def _get_adjacency_sparse(self, attribute=None): weights = self.es[attribute] N = self.vcount() - mtx = sparse.csr_matrix((weights, list(zip(*edges))), shape=(N, N)) + r, c = zip(*edges) if edges else ([], []) + mtx = sparse.csr_matrix((weights, (r, c)), shape=(N, N)) if not self.is_directed(): mtx = mtx + sparse.triu(mtx, 1).T + sparse.tril(mtx, -1).T return mtx -def _get_adjlist(self, mode="out"): +def _get_adjlist(self, mode="out", loops="twice", multiple=True): """Returns the adjacency list representation of the graph. The adjacency list representation is a list of lists. Each item of the outer list belongs to a single vertex of the graph. The inner list contains the neighbors of the given vertex. - @param mode: if C{\"out\"}, returns the successors of the vertex. If - C{\"in\"}, returns the predecessors of the vertex. If C{\"all"\"}, both + @param mode: if C{"out"}, returns the successors of the vertex. If + C{"in"}, returns the predecessors of the vertex. If C{"all"}, both the predecessors and the successors will be returned. Ignored for undirected graphs. + @param loops: whether to return loops in I{undirected} graphs once + (C{"once"}), twice (C{"twice"}) or not at all (C{"ignore"}). C{False} + is accepted as an alias to C{"ignore"} and C{True} is accepted as an + alias to C{"twice"}. For directed graphs, C{"twice"} is equivalent + to C{"once"} (except when C{mode} is C{"all"} because the graph is + then treated as undirected). + @param multiple: whether to return endpoints of multiple edges as many + times as their multiplicities. """ - return [self.neighbors(idx, mode) for idx in range(self.vcount())] + return [self.neighbors(idx, mode, loops, multiple) for idx in range(self.vcount())] def _get_biadjacency(graph, types="type", *args, **kwds): @@ -140,7 +149,7 @@ def _get_biadjacency(graph, types="type", *args, **kwds): bipartite adjacency matrix is an M{n} times M{m} matrix, where M{n} and M{m} are the number of vertices in the two vertex classes. - @param types: an igraph vector containing the vertex types, or an + @param types: a vector containing the vertex types, or an attribute name. Anything that evalulates to C{False} corresponds to vertices of the first kind, everything else to the second kind. @return: the bipartite adjacency matrix and two lists in a triplet. The @@ -153,7 +162,7 @@ def _get_biadjacency(graph, types="type", *args, **kwds): return super(Graph, graph).get_biadjacency(types, *args, **kwds) -def _get_inclist(graph, mode="out"): +def _get_inclist(graph, mode="out", loops="twice"): """Returns the incidence list representation of the graph. The incidence list representation is a list of lists. Each @@ -161,9 +170,15 @@ def _get_inclist(graph, mode="out"): The inner list contains the IDs of the incident edges of the given vertex. - @param mode: if C{\"out\"}, returns the successors of the vertex. If - C{\"in\"}, returns the predecessors of the vertex. If C{\"all\"}, both + @param mode: if C{"out"}, returns the successors of each vertex. If + C{"in"}, returns the predecessors of each vertex. If C{"all"}, both the predecessors and the successors will be returned. Ignored for undirected graphs. + @param loops: whether to return loops in I{undirected} graphs once + (C{"once"}), twice (C{"twice"}) or not at all (C{"ignore"}). C{False} + is accepted as an alias to C{"ignore"} and C{True} is accepted as an + alias to C{"twice"}. For directed graphs, C{"twice"} is equivalent + to C{"once"} (except when C{mode} is C{"all"} because the graph is + then treated as undirected). """ - return [graph.incident(idx, mode) for idx in range(graph.vcount())] + return [graph.incident(idx, mode, loops) for idx in range(graph.vcount())] diff --git a/src/igraph/app/shell.py b/src/igraph/app/shell.py index 6a22eb097..d8318bcd0 100644 --- a/src/igraph/app/shell.py +++ b/src/igraph/app/shell.py @@ -19,7 +19,6 @@ Mac OS X users are likely to invoke igraph from the command line. """ - from abc import ABCMeta, abstractmethod import re import sys diff --git a/src/igraph/basic.py b/src/igraph/basic.py index 2944d3f2d..edc016d20 100644 --- a/src/igraph/basic.py +++ b/src/igraph/basic.py @@ -34,7 +34,7 @@ def _add_edges(graph, es, attributes=None): @param es: the list of edges to be added. Every edge is represented with a tuple containing the vertex IDs or names of the two endpoints. Vertices are enumerated from zero. - @param attributes: dict of sequences, all of length equal to the + @param attributes: dict of sequences, each of length equal to the number of edges to be added, containing the attributes of the new edges. """ @@ -92,7 +92,7 @@ def _add_vertices(graph, n, attributes=None): vertex to be added, or a sequence of strings, each corresponding to the name of a vertex to be added. Names will be assigned to the C{name} vertex attribute. - @param attributes: dict of sequences, all of length equal to the + @param attributes: dict of sequences, each of length equal to the number of vertices to be added, containing the attributes of the new vertices. If n is a string (so a single vertex is added), then the values of this dict are the attributes themselves, but if n=1 then @@ -136,7 +136,7 @@ def _delete_edges(graph, *args, **kwds): first positional argument is callable, an edge sequence is derived by calling L{EdgeSeq.select} with the same positional and keyword arguments. Edges in the derived edge sequence will be removed. - Otherwise the first positional argument is considered as follows: + Otherwise, the first positional argument is considered as follows: Deprecation notice: C{delete_edges(None)} has been replaced by C{delete_edges()} - with no arguments - since igraph 0.8.3. diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index b06e886e8..36df961ad 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -5,7 +5,7 @@ from copy import deepcopy from io import StringIO -from igraph._igraph import GraphBase, community_to_membership +from igraph._igraph import GraphBase, community_to_membership, _compare_communities from igraph.configuration import Configuration from igraph.datatypes import UniqueIdGenerator from igraph.drawing.colors import ClusterColoringPalette @@ -452,7 +452,7 @@ def __plot__(self, backend, context, *args, **kwds): - C{True}: all the groups will be highlighted, the colors matching the corresponding color indices from the current palette - (see the C{palette} keyword argument of L{Graph.__plot__}. + (see the C{palette} keyword argument of L{Graph.__plot__}). - A dict mapping cluster indices or tuples of vertex indices to color names. The given clusters or vertex groups will be @@ -1151,7 +1151,7 @@ def __plot__(self, backend, context, *args, **kwds): - C{True}: all the clusters will be highlighted, the colors matching the corresponding color indices from the current palette - (see the C{palette} keyword argument of L{Graph.__plot__}. + (see the C{palette} keyword argument of L{Graph.__plot__}). - A dict mapping cluster indices or tuples of vertex indices to color names. The given clusters or vertex groups will be @@ -1371,12 +1371,12 @@ def _handle_mark_groups_arg_for_clustering(mark_groups, clustering): to clusters automatically. """ # Handle the case of mark_groups = True, mark_groups containing a list or - # tuple of cluster IDs, and and mark_groups yielding (cluster ID, color) + # tuple of cluster IDs, and mark_groups yielding (cluster ID, color) # pairs if mark_groups is True: group_iter = ((group, color) for color, group in enumerate(clustering)) elif isinstance(mark_groups, dict): - group_iter = mark_groups.iteritems() + group_iter = mark_groups.items() elif hasattr(mark_groups, "__getitem__") and hasattr(mark_groups, "__len__"): # Lists, tuples try: @@ -1399,7 +1399,7 @@ def _handle_mark_groups_arg_for_clustering(mark_groups, clustering): # Iterators etc group_iter = mark_groups else: - group_iter = {}.iteritems() + group_iter = {}.items() def cluster_index_resolver(): for group, color in group_iter: @@ -1459,6 +1459,9 @@ def _ensure_list(obj): def compare_communities(comm1, comm2, method="vi", remove_none=False): """Compares two community structures using various distance measures. + For measures involving entropies (e.g., the variation of information metric), + igraph uses natural logarithms. + B{References} - Meila M: Comparing clusterings by the variation of information. In: @@ -1483,7 +1486,7 @@ def compare_communities(comm1, comm2, method="vi", remove_none=False): as a L{Clustering} object. @param method: the measure to use. C{"vi"} or C{"meila"} means the variation of information metric of Meila (2003), C{"nmi"} or C{"danon"} - means the normalized mutual information as defined by Danon et al (2005), + means the normalized mutual information as defined by Danon et al. (2005), C{"split-join"} means the split-join distance of van Dongen (2000), C{"rand"} means the Rand index of Rand (1971), C{"adjusted_rand"} means the adjusted Rand index of Hubert and Arabie (1985). @@ -1497,10 +1500,8 @@ def compare_communities(comm1, comm2, method="vi", remove_none=False): @return: the calculated measure. """ - import igraph._igraph - vec1, vec2 = _prepare_community_comparison(comm1, comm2, remove_none) - return igraph._igraph._compare_communities(vec1, vec2, method) + return _compare_communities(vec1, vec2, method) def split_join_distance(comm1, comm2, remove_none=False): diff --git a/src/igraph/community.py b/src/igraph/community.py index b890b97b2..a3c6ce50b 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -37,16 +37,16 @@ def _community_infomap(graph, edge_weights=None, vertex_weights=None, trials=10) - M. Rosvall and C. T. Bergstrom: Maps of information flow reveal community structure in complex networks, I{PNAS} 105, 1118 (2008). - U{http://dx.doi.org/10.1073/pnas.0706851105}, - U{http://arxiv.org/abs/0707.0609}. + U{https://doi.org/10.1073/pnas.0706851105}, + U{https://arxiv.org/abs/0707.0609}. - M. Rosvall, D. Axelsson, and C. T. Bergstrom: The map equation, I{Eur Phys. J Special Topics} 178, 13 (2009). - U{http://dx.doi.org/10.1140/epjst/e2010-01179-1}, - U{http://arxiv.org/abs/0906.1405}. + U{https://doi.org/10.1140/epjst/e2010-01179-1}, + U{https://arxiv.org/abs/0906.1405}. @param edge_weights: name of an edge attribute or a list containing edge weights. - @param vertex_weights: name of an vertex attribute or a list containing + @param vertex_weights: name of a vertex attribute or a list containing vertex weights. @param trials: the number of attempts to partition the network. @return: an appropriate L{VertexClustering} object with an extra attribute @@ -111,7 +111,7 @@ def _community_label_propagation(graph, weights=None, initial=None, fixed=None): Note that since ties are broken randomly, there is no guarantee that the algorithm returns the same community structure after each run. - In fact, they frequently differ. See the paper of Raghavan et al + In fact, they frequently differ. See the paper of Raghavan et al. on how to come up with an aggregated community structure. Also note that the community _labels_ (numbers) have no semantic meaning @@ -125,7 +125,7 @@ def _community_label_propagation(graph, weights=None, initial=None, fixed=None): B{Reference}: Raghavan, U.N. and Albert, R. and Kumara, S. Near linear time algorithm to detect community structures in large-scale networks. - I{Phys Rev} E 76:036106, 2007. U{http://arxiv.org/abs/0709.2938}. + I{Phys Rev} E 76:036106, 2007. U{https://arxiv.org/abs/0709.2938}. @param weights: name of an edge attribute or a list containing edge weights @@ -156,16 +156,16 @@ def _community_multilevel(graph, weights=None, return_levels=False, resolution=1 iteratively in a way that maximizes the vertices' local contribution to the overall modularity score. When a consensus is reached (i.e. no single move would increase the modularity score), every community in - the original graph is shrank to a single vertex (while keeping the + the original graph is shrunk to a single vertex (while keeping the total weight of the incident edges) and the process continues on the next level. The algorithm stops when it is not possible to increase - the modularity any more after shrinking the communities to vertices. + the modularity anymore after shrinking the communities to vertices. This algorithm is said to run almost in linear time on sparse graphs. B{Reference}: VD Blondel, J-L Guillaume, R Lambiotte and E Lefebvre: Fast unfolding of community hierarchies in large networks, I{J Stat Mech} - P10008 (2008). U{http://arxiv.org/abs/0803.0476} + P10008 (2008). U{https://arxiv.org/abs/0803.0476} @param weights: edge attribute name or a list containing edge weights @@ -235,6 +235,13 @@ def _community_edge_betweenness(graph, clusters=None, directed=True, weights=Non separate components. The result of the clustering will be represented by a dendrogram. + When edge weights are given, the ratio of betweenness and weight values + is used to choose which edges to remove first, as described in + M. E. J. Newman: Analysis of Weighted Networks (2004), Section C. + Thus, edges with large weights are treated as strong connections, + and will be removed later than weak connections having similar betweenness. + Weights are also used for calculating modularity. + @param clusters: the number of clusters we would like to see. This practically defines the "level" where we "cut" the dendrogram to get the membership vector of the vertices. If C{None}, the dendrogram @@ -245,7 +252,7 @@ def _community_edge_betweenness(graph, clusters=None, directed=True, weights=Non @param directed: whether the directionality of the edges should be taken into account or not. @param weights: name of an edge attribute or a list containing - edge weights. + edge weights. Higher weights indicate stronger connections. @return: a L{VertexDendrogram} object, initally cut at the maximum modularity or at the desired number of clusters. """ @@ -271,11 +278,11 @@ def _community_spinglass(graph, *args, **kwds): - Reichardt J and Bornholdt S: Statistical mechanics of community detection. I{Phys Rev E} 74:016110 (2006). - U{http://arxiv.org/abs/cond-mat/0603718}. + U{https://arxiv.org/abs/cond-mat/0603718}. - Traag VA and Bruggeman J: Community detection in networks with positive and negative links. I{Phys Rev E} 80:036115 (2009). - U{http://arxiv.org/abs/0811.2329}. + U{https://arxiv.org/abs/0811.2329}. @keyword weights: edge weights to be used. Can be a sequence or iterable or even an edge attribute name. @@ -320,6 +327,68 @@ def _community_spinglass(graph, *args, **kwds): return VertexClustering(graph, membership, modularity_params=modularity_params) +def _community_voronoi(graph, lengths=None, weights=None, mode="out", radius=None): + """Finds communities using Voronoi partitioning. + + This function finds communities using a Voronoi partitioning of vertices based + on the given edge lengths divided by the edge clustering coefficient. + The generator vertices are chosen to be those with the largest local relative + density within a radius, with the local relative density of a vertex defined + as C{s * m / (m + k)}, where C{s} is the strength of the vertex, C{m} is + the number of edges within the vertex's first order neighborhood, while C{k} + is the number of edges with only one endpoint within this neighborhood. + + B{References} + + - Deritei et al., Community detection by graph Voronoi diagrams, + I{New Journal of Physics} 16, 063007 (2014). + U{https://doi.org/10.1088/1367-2630/16/6/063007}. + - Molnár et al., Community Detection in Directed Weighted Networks using + Voronoi Partitioning, I{Scientific Reports} 14, 8124 (2024). + U{https://doi.org/10.1038/s41598-024-58624-4}. + + @param lengths: edge lengths, or C{None} to consider all edges as having + unit length. Voronoi partitioning will use edge lengths equal to + lengths / ECC where ECC is the edge clustering coefficient. + @param weights: edge weights, or C{None} to consider all edges as having + unit weight. Weights are used when selecting generator points, as well + as for computing modularity. + @param mode: specifies how to use the direction of edges when computing + distances from generator points. If C{"out"} (the default), distances + from generator points to all other nodes are considered following the + direction of edges. If C{"in"}, distances are computed in the reverse + direction (i.e., from all nodes to generator points). If C{"all"}, + edge directions are ignored and the graph is treated as undirected. + This parameter is ignored for undirected graphs. + @param radius: the radius/resolution to use when selecting generator points. + The larger this value, the fewer partitions there will be. Pass C{None} + to automatically select the radius that maximizes modularity. + @return: an appropriate L{VertexClustering} object with an extra attribute + called C{generators} (the generator vertices). + """ + # Convert mode string to proper enum value to avoid deprecation warning + if isinstance(mode, str): + mode_map = {"out": "out", "in": "in", "all": "all", "total": "all"} # alias + if mode.lower() in mode_map: + mode = mode_map[mode.lower()] + else: + raise ValueError(f"Invalid mode '{mode}'. Must be one of: out, in, all") + + membership, generators, modularity = GraphBase.community_voronoi(graph, lengths, weights, mode, radius) + + params = {"generators": generators} + modularity_params = {} + if weights is not None: + modularity_params["weights"] = weights + + clustering = VertexClustering( + graph, membership, modularity=modularity, params=params, modularity_params=modularity_params + ) + + clustering.generators = generators + return clustering + + def _community_walktrap(graph, weights=None, steps=4): """Community detection algorithm of Latapy & Pons, based on random walks. @@ -329,7 +398,7 @@ def _community_walktrap(graph, weights=None, steps=4): as a dendrogram. B{Reference}: Pascal Pons, Matthieu Latapy: Computing communities in large - networks using random walks, U{http://arxiv.org/abs/physics/0512106}. + networks using random walks, U{https://arxiv.org/abs/physics/0512106}. @param weights: name of an edge attribute or a list containing edge weights @@ -392,7 +461,8 @@ def _community_leiden( initial_membership=None, n_iterations=2, node_weights=None, - **kwds + node_in_weights=None, + **kwds, ): """Finds the community structure of the graph using the Leiden algorithm of Traag, van Eck & Waltman. @@ -422,6 +492,11 @@ def _community_leiden( If this is not provided, it will be automatically determined on the basis of whether you want to use CPM or modularity. If you do provide this, please make sure that you understand what you are doing. + @param node_in_weights: the inbound node weights used in the directed + variant of the Leiden algorithm. If this is not provided, it will be + automatically determined on the basis of whether you want to use CPM or + modularity. If you do provide this, please make sure that you understand + what you are doing. @return: an appropriate L{VertexClustering} object with an extra attribute called C{quality} that stores the value of the internal quality function optimized by the algorithm. @@ -443,6 +518,7 @@ def _community_leiden( graph, edge_weights=weights, node_weights=node_weights, + node_in_weights=node_in_weights, resolution=resolution, normalize_resolution=(objective_function == "modularity"), beta=beta, @@ -461,6 +537,47 @@ def _community_leiden( ) +def _community_fluid_communities(graph, no_of_communities): + """Community detection based on fluids interacting on the graph. + + The algorithm is based on the simple idea of several fluids interacting + in a non-homogeneous environment (the graph topology), expanding and + contracting based on their interaction and density. Weighted graphs are + not supported. + + This function implements the community detection method described in: + Parés F, Gasulla DG, et. al. (2018) Fluid Communities: A Competitive, + Scalable and Diverse Community Detection Algorithm. + + @param no_of_communities: The number of communities to be found. Must be + greater than 0 and fewer than or equal to the number of vertices in the graph. + @return: an appropriate L{VertexClustering} object. + """ + # Validate input parameters + if no_of_communities <= 0: + raise ValueError("no_of_communities must be greater than 0") + + if no_of_communities > graph.vcount(): + raise ValueError("no_of_communities must be fewer than or equal to the number of vertices") + + # Check if graph is weighted (not supported) + if graph.is_weighted(): + raise ValueError("Weighted graphs are not supported by the fluid communities algorithm") + + # Handle directed graphs - the algorithm works on undirected graphs + # but can accept directed graphs (they are treated as undirected) + if graph.is_directed(): + import warnings + warnings.warn( + "Directed graphs are treated as undirected in the fluid communities algorithm", + UserWarning, + stacklevel=2 + ) + + membership = GraphBase.community_fluid_communities(graph, no_of_communities) + return VertexClustering(graph, membership) + + def _modularity(self, membership, weights=None, resolution=1, directed=True): """Calculates the modularity score of the graph with respect to a given clustering. diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index 39788051c..9accb5325 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -11,6 +11,7 @@ import os.path from configparser import ConfigParser +from typing import IO, Optional, Sequence, Tuple, Union class Configuration: @@ -114,9 +115,6 @@ class Types: """Static class for the implementation of custom getter/setter functions for configuration keys""" - def __init__(self): - pass - @staticmethod def setboolean(obj, section, key, value): """Sets a boolean value in the given configuration object. @@ -127,7 +125,7 @@ def setboolean(obj, section, key, value): @param value: the value itself. C{0}, C{false}, C{no} and C{off} means false, C{1}, C{true}, C{yes} and C{on} means true, everything else results in a C{ValueError} being thrown. - Values are case insensitive + Values are case-insensitive """ value = str(value).lower() if value in ("0", "false", "no", "off"): @@ -169,7 +167,7 @@ def setfloat(obj, section, key, value): "float": {"getter": ConfigParser.getfloat, "setter": Types.setfloat}, } - _sections = ("general", "apps", "plotting", "remote", "shell") + _sections: Sequence[str] = ("general", "apps", "plotting", "remote", "shell") _definitions = { "general.shells": {"default": "IPythonShell,ClassicPythonShell"}, "general.verbose": {"default": True, "type": "boolean"}, @@ -184,10 +182,15 @@ def setfloat(obj, section, key, value): # The singleton instance we are using throughout other modules _instance = None + _filename: Optional[str] = None + """Name of the file that the configuration was loaded from, or ``None`` if + not known. + """ + def __init__(self, filename=None): """Creates a new configuration instance. - @param filename: file or file pointer to be read. Can be omitted. + @param filename: file or file-like object to be read. Can be omitted. """ self._config = ConfigParser() self._filename = None @@ -195,6 +198,7 @@ def __init__(self, filename=None): # Create default sections for sec in self._sections: self._config.add_section(sec) + # Create default values for name, definition in self._definitions.items(): if "default" in definition: @@ -204,7 +208,7 @@ def __init__(self, filename=None): self.load(filename) @property - def filename(self): + def filename(self) -> Optional[str]: """Returns the filename associated to the object. It is usually the name of the configuration file that was used when @@ -214,7 +218,7 @@ def filename(self): information.""" return self._filename - def _get(self, section, key): + def _get(self, section: str, key: str): """Internal function that returns the value of a given key in a given section.""" definition = self._definitions.get("%s.%s" % (section, key), {}) @@ -226,11 +230,11 @@ def _get(self, section, key): return getter(self._config, section, key) @staticmethod - def _item_to_section_key(item): + def _item_to_section_key(item: str) -> Tuple[str, str]: """Converts an item description to a section-key pair. @param item: the item to be converted - @return: if C{item} contains a period (C{.}), it is splitted into two parts + @return: if C{item} contains a period (C{.}), it is split into two parts at the first period, then the two parts are returned, so the part before the period is the section. If C{item} does not contain a period, the section is assumed to be C{general}, and the second part of the returned @@ -241,7 +245,7 @@ def _item_to_section_key(item): section, key = "general", item return section, key - def __contains__(self, item): + def __contains__(self, item: str) -> bool: """Checks whether the given configuration item is set. @param item: the configuration key to check. @@ -250,7 +254,7 @@ def __contains__(self, item): section, key = self._item_to_section_key(item) return self._config.has_option(section, key) - def __getitem__(self, item): + def __getitem__(self, item: str): """Returns the given configuration item. @param item: the configuration key to retrieve. @@ -264,7 +268,7 @@ def __getitem__(self, item): else: return self._get(section, key) - def __setitem__(self, item, value): + def __setitem__(self, item: str, value): """Sets the given configuration item. @param item: the configuration key to set @@ -279,7 +283,7 @@ def __setitem__(self, item, value): setter = self._config.__class__.set return setter(self._config, section, key, value) - def __delitem__(self, item): + def __delitem__(self, item: str): """Deletes the given item from the configuration. If the item has a default value, the default value is written back instead @@ -292,14 +296,11 @@ def __delitem__(self, item): else: self._config.remove_option(section, key) - def has_key(self, item): + def has_key(self, item: str) -> bool: """Checks if the configuration has a given key. @param item: the key being sought""" - if "." in item: - section, key = item.split(".", 1) - else: - section, key = "general", item + section, key = self._item_to_section_key(item) return self._config.has_option(section, key) def load(self, stream=None): @@ -310,28 +311,40 @@ def load(self, stream=None): loaded. """ stream = stream or get_user_config_file() + if isinstance(stream, str): stream = open(stream, "r") file_was_open = True + else: + file_was_open = False + self._config.read_file(stream) - self._filename = getattr(stream, "name", None) + + filename = getattr(stream, "name", None) + self._filename = str(filename) if filename is not None else None + if file_was_open: stream.close() - def save(self, stream=None): + def save(self, stream: Optional[Union[str, IO[str]]] = None): """Saves the configuration. - @param stream: name of a file or a file object. The configuration will be saved - there. Can be omitted, in this case, the user-level configuration file will - be overwritten. + @param stream: name of a file or a file-like object. The configuration + will be saved there. Can be omitted, in this case, the user-level + configuration file will be overwritten. """ stream = stream or get_user_config_file() + if not hasattr(stream, "write") or not hasattr(stream, "close"): - stream = open(stream, "w") + stream = open(stream, "w") # type: ignore file_was_open = True - self._config.write(stream) + else: + file_was_open = False + + self._config.write(stream) # type: ignore + if file_was_open: - stream.close() + stream.close() # type: ignore @classmethod def instance(cls): @@ -347,12 +360,12 @@ def instance(cls): return cls._instance -def get_user_config_file(): +def get_user_config_file() -> str: """Returns the path where the user-level configuration file is stored""" return os.path.expanduser("~/.igraphrc") -def init(): +def init() -> Configuration: """Default mechanism to initiate igraph configuration This method loads the user-specific configuration file from the diff --git a/src/igraph/datatypes.py b/src/igraph/datatypes.py index a8f26ed4c..b16e89cfe 100644 --- a/src/igraph/datatypes.py +++ b/src/igraph/datatypes.py @@ -185,6 +185,10 @@ def __isub__(self, other): row[i] -= other return self + def __len__(self): + """Returns the number of rows in the matrix.""" + return len(self._data) + def __ne__(self, other): """Checks whether a given matrix is not equal to another one""" return not self == other diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 7bdbee231..05743728a 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -1,13 +1,13 @@ """ Drawing and plotting routines for igraph. -IGraph has two stable plotting backends at the moment: Cairo and Matplotlib. +igraph has two stable plotting backends at the moment: Cairo and Matplotlib. It also has experimental support for plotly. The Cairo backend is dependent on the C{pycairo} or C{cairocffi} libraries that -provide Python bindings to the popular U{Cairo library}. -This means that if you don't have U{pycairo} -or U{cairocffi} installed, you won't be able +provide Python bindings to the popular U{Cairo library}. +This means that if you don't have U{pycairo} +or U{cairocffi} installed, you won't be able to use the Cairo plotting backend. Whenever the documentation refers to the C{pycairo} library, you can safely replace it with C{cairocffi} as the two are API-compatible. @@ -21,12 +21,11 @@ If you do not want to (or cannot) install any of the dependencies outlined above, you can still save the graph to an SVG file and view it from -U{Mozilla Firefox} (free) or edit it in -U{Inkscape} (free), U{Skencil} +U{Mozilla Firefox} (free) or edit it in +U{Inkscape} (free), U{Skencil} (formerly known as Sketch, also free) or Adobe Illustrator. """ - from pathlib import Path from warnings import warn diff --git a/src/igraph/drawing/cairo/edge.py b/src/igraph/drawing/cairo/edge.py index 1b0c46731..7535004c9 100644 --- a/src/igraph/drawing/cairo/edge.py +++ b/src/igraph/drawing/cairo/edge.py @@ -168,14 +168,16 @@ def draw_directed_edge(self, edge, src_vertex, dest_vertex): ] # Midpoint of the base of the arrow triangle - x_arrow_mid, y_arrow_mid = (aux_points[0][0] + aux_points[1][0]) / 2.0, ( - aux_points[0][1] + aux_points[1][1] - ) / 2.0 + x_arrow_mid, y_arrow_mid = ( + (aux_points[0][0] + aux_points[1][0]) / 2.0, + (aux_points[0][1] + aux_points[1][1]) / 2.0, + ) # Vector representing the base of the arrow triangle x_arrow_base_vec, y_arrow_base_vec = ( - aux_points[0][0] - aux_points[1][0] - ), (aux_points[0][1] - aux_points[1][1]) + (aux_points[0][0] - aux_points[1][0]), + (aux_points[0][1] - aux_points[1][1]), + ) # Recalculate the curve such that it lands on the base of the arrow triangle aux1, aux2 = get_bezier_control_points_for_curved_edge( @@ -224,9 +226,10 @@ def draw_directed_edge(self, edge, src_vertex, dest_vertex): ] # Midpoint of the base of the arrow triangle - x_arrow_mid, y_arrow_mid = (aux_points[0][0] + aux_points[1][0]) / 2.0, ( - aux_points[0][1] + aux_points[1][1] - ) / 2.0 + x_arrow_mid, y_arrow_mid = ( + (aux_points[0][0] + aux_points[1][0]) / 2.0, + (aux_points[0][1] + aux_points[1][1]) / 2.0, + ) # Draw the line ctx.line_to(x_arrow_mid, y_arrow_mid) diff --git a/src/igraph/drawing/cairo/plot.py b/src/igraph/drawing/cairo/plot.py index fb82c8542..5ac247b8f 100644 --- a/src/igraph/drawing/cairo/plot.py +++ b/src/igraph/drawing/cairo/plot.py @@ -1,12 +1,12 @@ """ Drawing and plotting routines for IGraph. -IGraph has two plotting backends at the moment: Cairo and Matplotlib. +igraph has two plotting backends at the moment: Cairo and Matplotlib. The Cairo backend is dependent on the C{pycairo} or C{cairocffi} libraries that provide Python bindings to the popular U{Cairo library}. This means that if you don't have U{pycairo} -or U{cairocffi} installed, you won't be able +or U{cairocffi} installed, you won't be able to use the Cairo plotting backend. Whenever the documentation refers to the C{pycairo} library, you can safely replace it with C{cairocffi} as the two are API-compatible. @@ -17,12 +17,11 @@ If you do not want to (or cannot) install any of the dependencies outlined above, you can still save the graph to an SVG file and view it from -U{Mozilla Firefox} (free) or edit it in -U{Inkscape} (free), U{Skencil} +U{Mozilla Firefox} (free) or edit it in +U{Inkscape} (free), U{Skencil} (formerly known as Sketch, also free) or Adobe Illustrator. """ - import os from io import BytesIO @@ -286,7 +285,7 @@ def redraw(self, context=None): bbox=bbox, palette=palette, *args, # noqa: B026 - **kwds + **kwds, ) if opacity < 1.0: ctx.pop_group_to_source() diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index ec326c48d..00f77f402 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -7,8 +7,8 @@ - Matplotlib axes (L{MatplotlibGraphDrawer}) It also contains routines to send an igraph graph directly to -(U{Cytoscape}) using the -(U{CytoscapeRPC plugin}), see +(U{Cytoscape}) using the +(U{CytoscapeRPC plugin}), see L{CytoscapeGraphDrawer}. L{CytoscapeGraphDrawer} can also fetch the current network from Cytoscape and convert it to igraph format. """ @@ -24,8 +24,8 @@ class CytoscapeGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer): """Graph drawer that sends/receives graphs to/from Cytoscape using CytoscapeRPC. - This graph drawer cooperates with U{Cytoscape} - using U{CytoscapeRPC}. + This graph drawer cooperates with U{Cytoscape} + using U{CytoscapeRPC}. You need to install the CytoscapeRPC plugin first and start the XML-RPC server on a given port (port 9000 by default) from the appropriate Plugins submenu in Cytoscape. diff --git a/src/igraph/drawing/plotly/edge.py b/src/igraph/drawing/plotly/edge.py index 15ea40a29..d9ee1e2cb 100644 --- a/src/igraph/drawing/plotly/edge.py +++ b/src/igraph/drawing/plotly/edge.py @@ -103,14 +103,16 @@ def draw_directed_edge(self, edge, src_vertex, dest_vertex): ] # Midpoint of the base of the arrow triangle - x_arrow_mid, y_arrow_mid = (aux_points[0][0] + aux_points[1][0]) / 2.0, ( - aux_points[0][1] + aux_points[1][1] - ) / 2.0 + x_arrow_mid, y_arrow_mid = ( + (aux_points[0][0] + aux_points[1][0]) / 2.0, + (aux_points[0][1] + aux_points[1][1]) / 2.0, + ) # Vector representing the base of the arrow triangle x_arrow_base_vec, y_arrow_base_vec = ( - aux_points[0][0] - aux_points[1][0] - ), (aux_points[0][1] - aux_points[1][1]) + (aux_points[0][0] - aux_points[1][0]), + (aux_points[0][1] - aux_points[1][1]), + ) # Recalculate the curve such that it lands on the base of the arrow triangle aux1, aux2 = get_bezier_control_points_for_curved_edge( @@ -161,9 +163,10 @@ def draw_directed_edge(self, edge, src_vertex, dest_vertex): ] # Midpoint of the base of the arrow triangle - x_arrow_mid, y_arrow_mid = (aux_points[0][0] + aux_points[1][0]) / 2.0, ( - aux_points[0][1] + aux_points[1][1] - ) / 2.0 + x_arrow_mid, y_arrow_mid = ( + (aux_points[0][0] + aux_points[1][0]) / 2.0, + (aux_points[0][1] + aux_points[1][1]) / 2.0, + ) # Draw the line path.append( format_path_step( diff --git a/src/igraph/drawing/shapes.py b/src/igraph/drawing/shapes.py index 4f0ea97b5..0c3ccd1c8 100644 --- a/src/igraph/drawing/shapes.py +++ b/src/igraph/drawing/shapes.py @@ -16,7 +16,6 @@ name in the C{shape} attribute of vertices. """ - __all__ = ("ShapeDrawerDirectory",) from abc import ABCMeta, abstractmethod diff --git a/src/igraph/formula.py b/src/igraph/formula.py index 8febe141a..c644b8d78 100644 --- a/src/igraph/formula.py +++ b/src/igraph/formula.py @@ -153,7 +153,7 @@ def construct_graph_from_formula(cls, formula=None, attr="name", simplify=True): + edges (vertex names): A->B - If you have may disconnected componnets, you can separate them + If you have many disconnected componnets, you can separate them with commas. You can also specify isolated vertices: >>> g = Graph.Formula("A--B, C--D, E--F, G--H, I, J, K") @@ -181,7 +181,7 @@ def construct_graph_from_formula(cls, formula=None, attr="name", simplify=True): @param formula: the formula itself @param attr: name of the vertex attribute where the vertex names will be stored - @param simplify: whether the simplify the constructed graph + @param simplify: whether to simplify the constructed graph @return: the constructed graph: """ diff --git a/src/igraph/io/adjacency.py b/src/igraph/io/adjacency.py index 5f4c3a5c4..7f09ed167 100644 --- a/src/igraph/io/adjacency.py +++ b/src/igraph/io/adjacency.py @@ -15,9 +15,11 @@ def _construct_graph_from_adjacency(cls, matrix, mode="directed", loops="once"): - a pandas.DataFrame (column/row names must match, and will be used as vertex names). @param mode: the mode to be used. Possible values are: - - C{"directed"} - the graph will be directed and a matrix - element gives the number of edges between two vertex. - - C{"undirected"} - alias to C{"max"} for convenience. + - C{"directed"} - the graph will be directed and a matrix element + specifies the number of edges between two vertices. + - C{"undirected"} - the graph will be undirected and a matrix element + specifies the number of edges between two vertices. The matrix must + be symmetric. - C{"max"} - undirected graph will be created and the number of edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} - C{"min"} - like C{"max"}, but with M{min(A(i,j), A(j,i))} @@ -76,17 +78,21 @@ def _construct_graph_from_weighted_adjacency( ): """Generates a graph from its weighted adjacency matrix. + Only edges with a non-zero weight are created. + @param matrix: the adjacency matrix. Possible types are: - a list of lists - a numpy 2D array or matrix (will be converted to list of lists) - a scipy.sparse matrix (will be converted to a COO matrix, but not to a dense matrix) @param mode: the mode to be used. Possible values are: - - C{"directed"} - the graph will be directed and a matrix - element gives the number of edges between two vertex. - - C{"undirected"} - alias to C{"max"} for convenience. - - C{"max"} - undirected graph will be created and the number of - edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} + - C{"directed"} - the graph will be directed and a matrix element + specifies the weight of the corresponding edge. + - C{"undirected"} - the graph will be undirected and a matrix element + specifies the weight of the corresponding edge. The matrix must + be symmetric. + - C{"max"} - undirected graph will be created and the weight of the + edge between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} - C{"min"} - like C{"max"}, but with M{min(A(i,j), A(j,i))} - C{"plus"} - like C{"max"}, but with M{A(i,j) + A(j,i)} - C{"upper"} - undirected graph with the upper right triangle of diff --git a/src/igraph/io/bipartite.py b/src/igraph/io/bipartite.py index 70fac3750..1268dc0bf 100644 --- a/src/igraph/io/bipartite.py +++ b/src/igraph/io/bipartite.py @@ -6,7 +6,7 @@ def _construct_bipartite_graph_from_adjacency( multiple=False, weighted=None, *args, - **kwds + **kwds, ): """Creates a bipartite graph from a bipartite adjacency matrix. @@ -117,7 +117,8 @@ def _construct_full_bipartite_graph( def _construct_random_bipartite_graph( - cls, n1, n2, p=None, m=None, directed=False, neimode="all", *args, **kwds + cls, n1, n2, p=None, m=None, directed=False, neimode="all", + allowed_edge_types="simple", edge_labeled=False, *args, **kwds ): """Generates a random bipartite graph with the given number of vertices and edges (if m is given), or with the given number of vertices and the given @@ -140,13 +141,23 @@ def _construct_random_bipartite_graph( edges will always point from type 1 to type 2. If it is C{"in"}, edges will always point from type 2 to type 1. This argument is ignored for undirected graphs. + @param allowed_edge_types: controls whether multi-edges are allowed + during the generation process. Possible values are: + + - C{"simple"}: simple graphs (no self-loops) + - C{"multi"}: multi-edges allowed + + @param edge_labeled: whether to sample uniformly from the set of + I{ordered} edge lists. Use C{False} to recover the classic random + bipartite model. """ if p is None: p = -1 if m is None: m = -1 result, types = cls._Random_Bipartite( - n1, n2, p, m, directed, neimode, *args, **kwds + n1, n2, p, m, directed, neimode, allowed_edge_types, edge_labeled, + *args, **kwds ) result.vs["type"] = types return result diff --git a/src/igraph/io/images.py b/src/igraph/io/images.py index 052be0500..da1c8b2c9 100644 --- a/src/igraph/io/images.py +++ b/src/igraph/io/images.py @@ -17,7 +17,7 @@ def _write_graph_to_svg( edge_stroke_widths="width", font_size=16, *args, - **kwds + **kwds, ): """Saves the graph as an SVG (Scalable Vector Graphics) file @@ -149,7 +149,7 @@ def _write_graph_to_svg( print('', file=f) print( - "", + "", file=f, ) print(file=f) @@ -289,16 +289,12 @@ def _write_graph_to_svg( vs = str(vertex_size) print( ' '.format( - vs, c[0] - ), + -{0},0" fill="{1}"/>'.format(vs, c[0]), file=f, ) print( ' '.format( - vs, c[1] - ), + -{0},0" fill="{1}"/>'.format(vs, c[1]), file=f, ) print( diff --git a/src/igraph/io/libraries.py b/src/igraph/io/libraries.py index d47490292..f35cc9545 100644 --- a/src/igraph/io/libraries.py +++ b/src/igraph/io/libraries.py @@ -55,7 +55,7 @@ def _export_graph_to_networkx( eattr["_igraph_index"] = i if multigraph and "_nx_multiedge_key" in eattr: - eattr["key"] = eattr.pop["_nx_multiedge_key"] + eattr["key"] = eattr.pop("_nx_multiedge_key") if vertex_attr_hashable in graph.vertex_attributes(): hashable_source = graph.vs[vertex_attr_hashable][edge.source] @@ -248,9 +248,12 @@ def _construct_graph_from_graph_tool(cls, g): # Node attributes for key, val in g.vertex_properties.items(): + # val.get_array() returns None for non-scalar types so use the slower + # way if this happens prop = val.get_array() + arr = prop if prop is not None else val for i in range(vcount): - graph.vs[i][key] = prop[i] + graph.vs[i][key] = arr[i] # Edges and edge attributes # NOTE: graph-tool is quite strongly typed, so each property is always diff --git a/src/igraph/layout.py b/src/igraph/layout.py index a846be7bd..ad5fffc09 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -6,7 +6,6 @@ This package contains the implementation of the L{Layout} object. """ - from math import sin, cos, pi from igraph._igraph import GraphBase @@ -16,6 +15,7 @@ __all__ = ( "Layout", + "align_layout", "_layout", "_layout_auto", "_layout_sugiyama", @@ -38,7 +38,7 @@ class Layout: the vertices. It was particularly convenient for me to use the same layout for all of them, especially when I made figures for a paper. However, C{igraph} will of course refuse to draw a graph with a layout that has - less coordinates than the node count of the graph. + fewer coordinates than the node count of the graph. Layouts behave exactly like lists when they are accessed using the item index operator (C{[...]}). They can even be iterated through. Items @@ -56,6 +56,14 @@ class Layout: >>> layout[1] = coords >>> print(layout[1]) [0, 3] + + Optionally, a layout may have I{edge routing} information attached to + each edge in the layout in the L{edge_routing} property. + + @ivar edge_routing: C{None} if no edge routing information is available, + or a list of lists of control point coordinates, one for each edge. When + an edge has control points, it should be drawn in a way that the edge + passes through all the control points in the order they appear in the list. """ def __init__(self, coords=None, dim=None): @@ -69,6 +77,8 @@ def __init__(self, coords=None, dim=None): length of the coordinate list is zero, otherwise it should be left as is. """ + self.edge_routing = None + if coords is not None: self._coords = [list(coord) for coord in coords] else: @@ -534,6 +544,28 @@ def _layout(graph, layout=None, *args, **kwds): return layout +def align_layout(graph, layout): + """Aligns a graph layout with the coordinate axes + + This function centers a vertex layout on the coordinate system origin and + rotates the layout to achieve a visually pleasing alignment with the coordinate + axes. Doing this is particularly useful with force-directed layouts such as + L{Graph.layout_fruchterman_reingold}. Layouts in arbitrary dimensional spaces + are supported. + + @param graph: the graph that the layout is associated with. + @param layout: the L{Layout} object containing the vertex coordinates + to align. + @return: a new aligned L{Layout} object. + """ + from igraph._igraph import _align_layout + + if not isinstance(layout, Layout): + layout = Layout(layout) + + return Layout(_align_layout(graph, layout.coords)) + + def _layout_auto(graph, *args, **kwds): """Chooses and runs a suitable layout function based on simple topological properties of the graph. @@ -609,7 +641,6 @@ def _layout_sugiyama( hgap=1, vgap=1, maxiter=100, - return_extended_graph=False, ): """Places the vertices using a layered Sugiyama layout. @@ -650,33 +681,16 @@ def _layout_sugiyama( @param maxiter: maximum number of iterations to take in the crossing reduction step. Increase this if you feel that you are getting too many edge crossings. - @param return_extended_graph: specifies that the extended graph with the - added dummy vertices should also be returned. When this is C{True}, the - result will be a tuple containing the layout and the extended graph. The - first |V| nodes of the extended graph will correspond to the nodes of the - original graph, the remaining ones are dummy nodes. Plotting the extended - graph with the returned layout and hidden dummy nodes will produce a layout - that is similar to the original graph, but with the added edge bends. - The extended graph also contains an edge attribute called C{_original_eid} - which specifies the ID of the edge in the original graph from which the - edge of the extended graph was created. - @return: the calculated layout, which may (and usually will) have more rows - than the number of vertices; the remaining rows correspond to the dummy - nodes introduced in the layering step. When C{return_extended_graph} is - C{True}, it will also contain the extended graph. + @return: the calculated layout and an additional list of matrices where the + i-th matrix contains the control points of edge I{i} in the original graph + (or an empty matrix if no control points are needed on the edge) """ - if not return_extended_graph: - return Layout( - GraphBase._layout_sugiyama( - graph, layers, weights, hgap, vgap, maxiter, return_extended_graph - ) - ) - - layout, extd_graph, extd_to_orig_eids = GraphBase._layout_sugiyama( - graph, layers, weights, hgap, vgap, maxiter, return_extended_graph + coords, routing = GraphBase._layout_sugiyama( + graph, layers, weights, hgap, vgap, maxiter ) - extd_graph.es["_original_eid"] = extd_to_orig_eids - return Layout(layout), extd_graph + layout = Layout(coords) + layout.edge_routing = routing + return layout def _layout_method_wrapper(func): diff --git a/src/igraph/operators/functions.py b/src/igraph/operators/functions.py index b305f5cb8..4ba3ede43 100644 --- a/src/igraph/operators/functions.py +++ b/src/igraph/operators/functions.py @@ -158,7 +158,7 @@ def union(graphs, byname="auto"): raise RuntimeError( f"Some graphs are not named (got {n_named} named, {ngr-n_named} unnamed)" ) - # Now we know that byname is only used is all graphs are named + # Now we know that byname is only used if all graphs are named # Trivial cases if ngr == 0: @@ -184,7 +184,12 @@ def union(graphs, byname="auto"): # Reorder vertices to match uninames # vertex k -> p[k] permutation = [permutation_map[x] for x in ng.vs["name"]] - ng = ng.permute_vertices(permutation) + + # permute_vertices() needs the inverse permutation + inv_permutation = [0] * len(permutation) + for i, x in enumerate(permutation): + inv_permutation[x] = i + ng = ng.permute_vertices(inv_permutation) newgraphs.append(ng) else: @@ -353,7 +358,7 @@ def intersection(graphs, byname="auto", keep_all_vertices=True): raise RuntimeError( f"Some graphs are not named (got {n_named} named, {ngr-n_named} unnamed)" ) - # Now we know that byname is only used is all graphs are named + # Now we know that byname is only used if all graphs are named # Trivial cases if ngr == 0: @@ -389,7 +394,12 @@ def intersection(graphs, byname="auto", keep_all_vertices=True): # Reorder vertices to match uninames # vertex k -> p[k] permutation = [permutation_map[x] for x in ng.vs["name"]] - ng = ng.permute_vertices(permutation) + + # permute_vertices() needs the inverse permutation + inv_permutation = [0] * len(permutation) + for i, x in enumerate(permutation): + inv_permutation[x] = i + ng = ng.permute_vertices(inv_permutation) newgraphs.append(ng) else: diff --git a/src/igraph/rewiring.py b/src/igraph/rewiring.py new file mode 100644 index 000000000..b4881a6c8 --- /dev/null +++ b/src/igraph/rewiring.py @@ -0,0 +1,25 @@ +from igraph._igraph import GraphBase + +from .utils import deprecated + +__all__ = ("_rewire", ) + + +def _rewire(graph, n=None, allowed_edge_types="simple", *, mode=None): + """Randomly rewires the graph while preserving the degree distribution. + + The rewiring is done \"in-place\", so the original graph will be modified. + If you want to preserve the original graph, use the L{copy} method before + rewiring. + + @param n: the number of rewiring trials. The default is 10 times the number + of edges. + @param allowed_edge_types: the rewiring algorithm to use. It can either be + C{"simple"} or C{"loops"}; the former does not create or destroy + loop edges while the latter does. + """ + if mode is not None: + deprecated("The 'mode' keyword argument is deprecated, use 'allowed_edge_types' instead") + allowed_edge_types = mode + + return GraphBase._rewire(graph, n, allowed_edge_types) diff --git a/src/igraph/seq.py b/src/igraph/seq.py index 6107cfc9c..3790cf510 100644 --- a/src/igraph/seq.py +++ b/src/igraph/seq.py @@ -737,9 +737,7 @@ def decorated(*args, **kwds): restricted to this sequence, and returns the result. @see: Graph.%(name)s() for details. -""" % { - "name": name - } +""" % {"name": name} return decorated diff --git a/src/igraph/sparse_matrix.py b/src/igraph/sparse_matrix.py index fe9565a79..93c6fa7ae 100644 --- a/src/igraph/sparse_matrix.py +++ b/src/igraph/sparse_matrix.py @@ -24,19 +24,15 @@ def _convert_mode_argument(mode): # resolve mode constants, convert to lowercase - mode = ( - { - ADJ_DIRECTED: "directed", - ADJ_UNDIRECTED: "undirected", - ADJ_MAX: "max", - ADJ_MIN: "min", - ADJ_PLUS: "plus", - ADJ_UPPER: "upper", - ADJ_LOWER: "lower", - } - .get(mode, mode) - .lower() - ) + mode = { + ADJ_DIRECTED: "directed", + ADJ_UNDIRECTED: "undirected", + ADJ_MAX: "max", + ADJ_MIN: "min", + ADJ_PLUS: "plus", + ADJ_UPPER: "upper", + ADJ_LOWER: "lower", + }.get(mode, mode).lower() if mode not in _SUPPORTED_MODES: raise ValueError("mode should be one of " + (" ".join(_SUPPORTED_MODES))) diff --git a/src/igraph/statistics.py b/src/igraph/statistics.py index 98a53cc7f..e134f7751 100644 --- a/src/igraph/statistics.py +++ b/src/igraph/statistics.py @@ -530,7 +530,7 @@ def power_law_fit(data, xmin=None, method="auto", p_precision=0.01): size if n is small. - C{discrete}: exact maximum likelihood estimation when the - input comes from a discrete scale (see Clauset et al among the + input comes from a discrete scale (see Clauset et al. among the references). - C{auto}: exact maximum likelihood estimation where the continuous diff --git a/src/igraph/utils.py b/src/igraph/utils.py index 2116332ae..867d68870 100644 --- a/src/igraph/utils.py +++ b/src/igraph/utils.py @@ -75,7 +75,7 @@ def numpy_to_contiguous_memoryview(obj): dtype = int32 else: raise TypeError( - f"size of igraph_integer_t in the C layer ({INTEGER_SIZE} bits) is not supported" + f"size of igraph_int_t in the C layer ({INTEGER_SIZE} bits) is not supported" ) return memoryview(require(obj, dtype=dtype, requirements="AC")) diff --git a/src/igraph/version.py b/src/igraph/version.py index e9509d902..b69224ddc 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 11, 2) +__version_info__ = (1, 0, 0) __version__ = ".".join("{0}".format(x) for x in __version_info__) diff --git a/tests/drawing/cairo/baseline_images/clustering_directed.png b/tests/drawing/cairo/baseline_images/clustering_directed.png index 712f1b95f..282f3fb21 100644 Binary files a/tests/drawing/cairo/baseline_images/clustering_directed.png and b/tests/drawing/cairo/baseline_images/clustering_directed.png differ diff --git a/tests/drawing/cairo/baseline_images/graph_basic.png b/tests/drawing/cairo/baseline_images/graph_basic.png index 2eee93d86..cfd394e49 100644 Binary files a/tests/drawing/cairo/baseline_images/graph_basic.png and b/tests/drawing/cairo/baseline_images/graph_basic.png differ diff --git a/tests/drawing/cairo/baseline_images/graph_directed.png b/tests/drawing/cairo/baseline_images/graph_directed.png index 83d6e3764..b94e82169 100644 Binary files a/tests/drawing/cairo/baseline_images/graph_directed.png and b/tests/drawing/cairo/baseline_images/graph_directed.png differ diff --git a/tests/drawing/cairo/baseline_images/graph_mark_groups_directed.png b/tests/drawing/cairo/baseline_images/graph_mark_groups_directed.png index 83d6e3764..b94e82169 100644 Binary files a/tests/drawing/cairo/baseline_images/graph_mark_groups_directed.png and b/tests/drawing/cairo/baseline_images/graph_mark_groups_directed.png differ diff --git a/tests/drawing/cairo/baseline_images/graph_mark_groups_squares_directed.png b/tests/drawing/cairo/baseline_images/graph_mark_groups_squares_directed.png index c5372745c..0e437f288 100644 Binary files a/tests/drawing/cairo/baseline_images/graph_mark_groups_squares_directed.png and b/tests/drawing/cairo/baseline_images/graph_mark_groups_squares_directed.png differ diff --git a/tests/drawing/matplotlib/test_graph.py b/tests/drawing/matplotlib/test_graph.py index 4a83cda5a..b77d42174 100644 --- a/tests/drawing/matplotlib/test_graph.py +++ b/tests/drawing/matplotlib/test_graph.py @@ -54,9 +54,11 @@ def test_labels(self): g = Graph.Ring(5) fig, ax = plt.subplots(figsize=(3, 3)) plot( - g, target=ax, layout=self.layout_small_ring, - vertex_label=['1', '2', '3', '4', '5'], - vertex_label_color='white', + g, + target=ax, + layout=self.layout_small_ring, + vertex_label=["1", "2", "3", "4", "5"], + vertex_label_color="white", vertex_label_size=16, ) @@ -156,7 +158,7 @@ def test_graph_with_curved_edges(self): ax.set_aspect(1.0) @image_comparison(baseline_images=["multigraph_with_curved_edges_undirected"]) - def test_graph_with_curved_edges(self): + def test_graph_with_curved_edges_undirected(self): plt.close("all") g = Graph.Ring(24, directed=False) g.add_edges([(0, 1), (1, 2)]) diff --git a/tests/drawing/matplotlib/utils.py b/tests/drawing/matplotlib/utils.py index 36bd50f1f..f32ad3f73 100644 --- a/tests/drawing/matplotlib/utils.py +++ b/tests/drawing/matplotlib/utils.py @@ -71,7 +71,7 @@ def wrapper(*args, **kwargs): def image_comparison( baseline_images, - tol=0.025, + tol=4.0, remove_text=False, savefig_kwarg=None, # Default of mpl_test_settings fixture and cleanup too. diff --git a/tests/test_atlas.py b/tests/test_atlas.py index 1f36f5f44..49f49531e 100644 --- a/tests/test_atlas.py +++ b/tests/test_atlas.py @@ -100,7 +100,14 @@ def testEigenvectorCentrality(self): def testHubScore(self): for idx, g in enumerate(self.__class__.graphs): try: - sc = g.hub_score() + if g.ecount() == 0: + with self.assertWarns(RuntimeWarning, msg="The graph has no edges"): + sc = g.hub_score() + elif not g.is_directed(): + with self.assertWarns(RuntimeWarning, msg="Hub and authority scores requested for undirected graph"): + sc = g.hub_score() + else: + sc = g.hub_score() except Exception as ex: self.assertTrue( False, @@ -126,7 +133,14 @@ def testHubScore(self): def testAuthorityScore(self): for idx, g in enumerate(self.__class__.graphs): try: - sc = g.authority_score() + if g.ecount() == 0: + with self.assertWarns(RuntimeWarning, msg="The graph has no edges"): + sc = g.authority_score() + elif not g.is_directed(): + with self.assertWarns(RuntimeWarning, msg="Hub and authority scores requested for undirected graph"): + sc = g.authority_score() + else: + sc = g.authority_score() except Exception as ex: self.assertTrue( False, diff --git a/tests/test_attributes.py b/tests/test_attributes.py index 4b6683d32..1299cada9 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -112,7 +112,7 @@ def testVertexNameIndexingBug196(self): g.add_vertices([a, b]) g.add_edges([(a, b)]) self.assertEqual(g.ecount(), 1) - self.assertTrue(g.are_connected(a, b)) + self.assertTrue(g.are_adjacent(a, b)) def testInvalidAttributeNames(self): g = Graph.Famous("bull") diff --git a/tests/test_basic.py b/tests/test_basic.py index ddda4f2d2..35d511b1b 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -350,6 +350,20 @@ def testAdjacency(self): self.assertTrue(g.get_adjlist(IN) == [[2], [0], [1], [2]]) self.assertTrue(g.get_adjlist(ALL) == [[1, 2], [0, 2], [0, 1, 3], [2]]) + def testAdjacencyWithLoopsAndMultiEdges(self): + g = Graph(4, [(0, 0), (0, 1), (1, 2), (2, 0), (2, 3), (2, 3), (3, 3), (3, 3)], directed=True) + + self.assertTrue(g.neighbors(2) == [0, 1, 3, 3]) + self.assertTrue(g.predecessors(2) == [1]) + self.assertTrue(g.successors(2) == [0, 3, 3]) + + self.assertTrue(g.get_adjlist() == [[0, 1], [2], [0, 3, 3], [3, 3]]) + self.assertTrue(g.get_adjlist(IN) == [[0, 2], [0], [1], [2, 2, 3, 3]]) + self.assertTrue(g.get_adjlist(ALL) == [[0, 0, 1, 2], [0, 2], [0, 1, 3, 3], [2, 2, 3, 3, 3, 3]]) + + self.assertTrue(g.get_adjlist(ALL, loops="once") == [[0, 1, 2], [0, 2], [0, 1, 3, 3], [2, 2, 3, 3]]) + self.assertTrue(g.get_adjlist(ALL, loops="once", multiple=False) == [[0, 1, 2], [0, 2], [0, 1, 3], [2, 3]]) + def testEdgeIncidence(self): g = Graph(4, [(0, 1), (1, 2), (2, 0), (2, 3)], directed=True) self.assertTrue(g.incident(2) == [2, 3]) @@ -359,6 +373,19 @@ def testEdgeIncidence(self): self.assertTrue(g.get_inclist(IN) == [[2], [0], [1], [3]]) self.assertTrue(g.get_inclist(ALL) == [[0, 2], [0, 1], [2, 1, 3], [3]]) + def testEdgeIncidenceWithLoopsAndMultiEdges(self): + g = Graph(4, [(0, 1), (1, 2), (2, 0), (2, 3), (2, 3), (0, 0), (3, 3), (3, 3)], directed=True) + + self.assertTrue(g.incident(2) == [2, 4, 3]) + self.assertTrue(g.incident(2, IN) == [1]) + self.assertTrue(g.incident(2, ALL) == [2, 1, 4, 3]) + + self.assertTrue(g.get_inclist() == [[5, 0], [1], [2, 4, 3], [7, 6]]) + self.assertTrue(g.get_inclist(IN) == [[5, 2], [0], [1], [4, 3, 7, 6]]) + self.assertTrue(g.get_inclist(ALL) == [[5, 5, 0, 2], [0, 1], [2, 1, 4, 3], [4, 3, 7, 7, 6, 6]]) + + self.assertTrue(g.get_inclist(ALL, loops="once") == [[5, 0, 2], [0, 1], [2, 1, 4, 3], [4, 3, 7, 6]]) + def testMultiplesLoops(self): g = Graph.Tree(7, 2) diff --git a/tests/test_bipartite.py b/tests/test_bipartite.py index 45cca2fc2..932838655 100644 --- a/tests/test_bipartite.py +++ b/tests/test_bipartite.py @@ -76,8 +76,8 @@ def testBiadjacency(self): ) ) self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) - self.assertListEqual(g.es["weight"], [1, 1, 1, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3)]) + self.assertListEqual(g.get_edgelist(), [(1, 2), (0, 3), (1, 3), (0, 4)]) + self.assertListEqual(g.es["weight"], [1, 1, 2, 1]) # Graph is not weighted when weighted=`str` g = Graph.Biadjacency([[0, 1, 1], [1, 2, 0]], weighted="some_attr_name") @@ -92,8 +92,8 @@ def testBiadjacency(self): ) ) self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) - self.assertListEqual(g.es["some_attr_name"], [1, 1, 1, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3)]) + self.assertListEqual(g.get_edgelist(), [(1, 2), (0, 3), (1, 3), (0, 4)]) + self.assertListEqual(g.es["some_attr_name"], [1, 1, 2, 1]) # Graph is not weighted when weighted="" g = Graph.Biadjacency([[0, 1, 1], [1, 2, 0]], weighted="") @@ -108,8 +108,8 @@ def testBiadjacency(self): ) ) self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) - self.assertListEqual(g.es[""], [1, 1, 1, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3)]) + self.assertListEqual(g.get_edgelist(), [(1, 2), (0, 3), (1, 3), (0, 4)]) + self.assertListEqual(g.es[""], [1, 1, 2, 1]) # Should work when directed=True and mode=out with weighted g = Graph.Biadjacency([[0, 1, 1], [1, 2, 0]], directed=True, weighted=True) @@ -117,8 +117,8 @@ def testBiadjacency(self): all((g.vcount() == 5, g.ecount() == 4, g.is_directed(), g.is_weighted())) ) self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) - self.assertListEqual(g.es["weight"], [1, 1, 1, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3)]) + self.assertListEqual(g.get_edgelist(), [(1, 2), (0, 3), (1, 3), (0, 4)]) + self.assertListEqual(g.es["weight"], [1, 1, 2, 1]) # Should work when directed=True and mode=in with weighted g = Graph.Biadjacency( @@ -128,8 +128,8 @@ def testBiadjacency(self): all((g.vcount() == 5, g.ecount() == 4, g.is_directed(), g.is_weighted())) ) self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) - self.assertListEqual(g.es["weight"], [1, 1, 1, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(2, 1), (3, 0), (3, 1), (4, 0)]) + self.assertListEqual(g.get_edgelist(), [(2, 1), (3, 0), (3, 1), (4, 0)]) + self.assertListEqual(g.es["weight"], [1, 1, 2, 1]) # Should work when directed=True and mode=all with weighted g = Graph.Biadjacency( @@ -139,11 +139,11 @@ def testBiadjacency(self): all((g.vcount() == 5, g.ecount() == 8, g.is_directed(), g.is_weighted())) ) self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) - self.assertListEqual(g.es["weight"], [1, 1, 1, 1, 1, 1, 2, 2]) self.assertListEqual( - sorted(g.get_edgelist()), - [(0, 3), (0, 4), (1, 2), (1, 3), (2, 1), (3, 0), (3, 1), (4, 0)], + g.get_edgelist(), + [(1, 2), (2, 1), (0, 3), (3, 0), (1, 3), (3, 1), (0, 4), (4, 0)] ) + self.assertListEqual(g.es["weight"], [1, 1, 1, 1, 2, 2, 1, 1]) def testBiadjacencyError(self): msg = "arguments weighted and multiple can not co-exist" @@ -172,7 +172,9 @@ def testBipartiteProjection(self): g = Graph.Full_Bipartite(10, 5) g1, g2 = g.bipartite_projection() + self.assertTrue(g1.is_complete()) self.assertTrue(g1.isomorphic(Graph.Full(10))) + self.assertTrue(g2.is_complete()) self.assertTrue(g2.isomorphic(Graph.Full(5))) self.assertTrue(g.bipartite_projection(which=0).isomorphic(g1)) self.assertTrue(g.bipartite_projection(which=1).isomorphic(g2)) @@ -183,7 +185,9 @@ def testBipartiteProjection(self): self.assertTrue(g.bipartite_projection_size() == (10, 45, 5, 10)) g1, g2 = g.bipartite_projection(probe1=10) + self.assertTrue(g1.is_complete()) self.assertTrue(g1.isomorphic(Graph.Full(5))) + self.assertTrue(g2.is_complete()) self.assertTrue(g2.isomorphic(Graph.Full(10))) self.assertTrue(g.bipartite_projection(which=0).isomorphic(g2)) self.assertTrue(g.bipartite_projection(which=1).isomorphic(g1)) @@ -191,7 +195,9 @@ def testBipartiteProjection(self): self.assertTrue(g.bipartite_projection(which=True).isomorphic(g1)) g1, g2 = g.bipartite_projection(multiplicity=False) + self.assertTrue(g1.is_complete()) self.assertTrue(g1.isomorphic(Graph.Full(10))) + self.assertTrue(g2.is_complete()) self.assertTrue(g2.isomorphic(Graph.Full(5))) self.assertTrue(g.bipartite_projection(which=0).isomorphic(g1)) self.assertTrue(g.bipartite_projection(which=1).isomorphic(g2)) diff --git a/tests/test_cliques.py b/tests/test_cliques.py index 3d63dd363..40c0f9ea3 100644 --- a/tests/test_cliques.py +++ b/tests/test_cliques.py @@ -1,5 +1,7 @@ import unittest +from math import inf + from igraph import Graph from .utils import temporary_file @@ -59,19 +61,37 @@ def testCliques(self): [1, 2, 4, 5], ], } + for (lo, hi), exp in tests.items(): self.assertEqual(sorted(exp), sorted(map(sorted, self.g.cliques(lo, hi)))) + for (lo, hi), exp in tests.items(): + self.assertEqual(sorted(exp), sorted(map(sorted, self.g.cliques(lo, hi, max_results=inf)))) + + for (lo, hi), exp in tests.items(): + observed = [sorted(cl) for cl in self.g.cliques(lo, hi, max_results=10)] + for cl in observed: + self.assertTrue(cl in exp) + + for (lo, hi), _ in tests.items(): + self.assertEqual([], self.g.cliques(lo, hi, max_results=0)) + + for (lo, hi), _ in tests.items(): + with self.assertRaises(ValueError): + self.g.cliques(lo, hi, max_results=-2) + def testLargestCliques(self): self.assertEqual( sorted(map(sorted, self.g.largest_cliques())), [[1, 2, 3, 4], [1, 2, 4, 5]] ) + self.assertTrue(all(map(self.g.is_clique, self.g.largest_cliques()))) def testMaximalCliques(self): self.assertEqual( sorted(map(sorted, self.g.maximal_cliques())), [[0, 3, 4], [0, 4, 5], [1, 2, 3, 4], [1, 2, 4, 5]], ) + self.assertTrue(all(map(self.g.is_clique, self.g.maximal_cliques()))) self.assertEqual( sorted(map(sorted, self.g.maximal_cliques(min=4))), [[1, 2, 3, 4], [1, 2, 4, 5]], @@ -136,6 +156,14 @@ def testLargestIndependentVertexSets(self): self.assertEqual( self.g1.largest_independent_vertex_sets(), [(0, 3, 4), (2, 3, 4)] ) + self.assertTrue( + all( + map( + self.g1.is_independent_vertex_set, + self.g1.largest_independent_vertex_sets(), + ) + ) + ) def testMaximalIndependentVertexSets(self): self.assertEqual( @@ -152,6 +180,14 @@ def testMaximalIndependentVertexSets(self): (2, 4, 7, 8), ], ) + self.assertTrue( + all( + map( + self.g2.is_independent_vertex_set, + self.g2.maximal_independent_vertex_sets(), + ) + ) + ) def testIndependenceNumber(self): self.assertEqual(self.g2.independence_number(), 6) diff --git a/tests/test_conversion.py b/tests/test_conversion.py index 91838df3c..6f3cdc22e 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -171,6 +171,28 @@ def testGetSparseAdjacency(self): np.all(g.get_adjacency_sparse() == np.array(g.get_adjacency().data)) ) + def testGetAdjacencyRoundtrip(self): + g = Graph.Tree(6, 3) + adj = g.get_adjacency() + g2 = Graph.Adjacency(adj, mode="undirected") + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(g.is_directed(), g2.is_directed()) + self.assertTrue(g.get_edgelist() == g2.get_edgelist()) + + +class PruferTests(unittest.TestCase): + def testFromPrufer(self): + g = Graph.Prufer([3, 3, 3, 4]) + self.assertEqual(6, g.vcount()) + self.assertEqual(5, g.ecount()) + self.assertEqual( + [(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)], sorted(g.get_edgelist()) + ) + + def testToPrufer(self): + g = Graph([(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)]) + self.assertEqual([3, 3, 3, 4], g.to_prufer()) + def suite(): direction_suite = unittest.defaultTestLoader.loadTestsFromTestCase( @@ -179,7 +201,8 @@ def suite(): representation_suite = unittest.defaultTestLoader.loadTestsFromTestCase( GraphRepresentationTests ) - return unittest.TestSuite([direction_suite, representation_suite]) + prufer_suite = unittest.defaultTestLoader.loadTestsFromTestCase(PruferTests) + return unittest.TestSuite([direction_suite, representation_suite, prufer_suite]) def test(): diff --git a/tests/test_cycles.py b/tests/test_cycles.py index 48a37f2f7..e3549121c 100644 --- a/tests/test_cycles.py +++ b/tests/test_cycles.py @@ -60,6 +60,25 @@ def test_fundamental_cycles(self): ] assert cycles == [[6, 7, 10], [8, 9, 10]] + def test_simple_cycles(self): + g = Graph( + [ + (0, 1), + (1, 2), + (2, 0), + (0, 0), + (0, 3), + (3, 4), + (4, 5), + (5, 0), + ] + ) + + vertices = g.simple_cycles(output="vpath") + edges = g.simple_cycles(output="epath") + assert len(vertices) == 3 + assert len(edges) == 3 + def test_minimum_cycle_basis(self): g = Graph( [ diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index 0bfdd1b6a..e9cb73ddc 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -263,11 +263,7 @@ def testEdgeBetweenness(self): g.es["weight"] = 1 g[0, 1] = g[1, 2] = g[2, 0] = g[3, 4] = 10 - # We need to specify the desired cluster count explicitly; this is - # because edge betweenness-based detection does not play well with - # modularity-based cluster count selection (the edge weights have - # different semantics) so we need to give igraph a hint - cl = g.community_edge_betweenness(weights="weight").as_clustering(n=2) + cl = g.community_edge_betweenness(weights="weight").as_clustering() self.assertMembershipsEqual(cl, [0, 0, 0, 1, 1]) self.assertAlmostEqual(cl.q, 0.2750, places=3) @@ -280,11 +276,66 @@ def testEigenvector(self): cl = g.community_leading_eigenvector(2) self.assertMembershipsEqual(cl, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) self.assertAlmostEqual(cl.q, 0.4523, places=3) + + def testFluidCommunities(self): + # Test with a simple graph: two cliques connected by a single edge + g = Graph.Full(5) + Graph.Full(5) + g.add_edges([(0, 5)]) + + # Test basic functionality - should find 2 communities + cl = g.community_fluid_communities(2) + self.assertEqual(len(set(cl.membership)), 2) + self.assertMembershipsEqual(cl, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) + + # Test with 3 cliques + g = Graph.Full(4) + Graph.Full(4) + Graph.Full(4) + g += [(0, 4), (4, 8)] # Connect the cliques + cl = g.community_fluid_communities(3) + self.assertEqual(len(set(cl.membership)), 3) + self.assertMembershipsEqual(cl, [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]) + + # Test error conditions + # Number of communities must be positive + with self.assertRaises(Exception): + g.community_fluid_communities(0) + + # Number of communities cannot exceed number of vertices + with self.assertRaises(Exception): + g.community_fluid_communities(g.vcount() + 1) + + # Test with disconnected graph (should raise error) + g_disconnected = Graph.Full(3) + Graph.Full(3) # No connecting edge + with self.assertRaises(Exception): + g_disconnected.community_fluid_communities(2) + + # Test with single vertex (edge case) + g_single = Graph(1) + cl = g_single.community_fluid_communities(1) + self.assertEqual(cl.membership, [0]) + + # Test with small connected graph + g_small = Graph([(0, 1), (1, 2), (2, 0)]) # Triangle + cl = g_small.community_fluid_communities(1) + self.assertEqual(len(set(cl.membership)), 1) + self.assertEqual(cl.membership, [0, 0, 0]) + + # Test deterministic behavior on simple structure + # Note: Fluid communities can be non-deterministic due to randomization, + # but on very simple structures it should be consistent + g_path = Graph([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]) + cl = g_path.community_fluid_communities(2) + self.assertEqual(len(set(cl.membership)), 2) + + # Test that it returns a VertexClustering object + g = Graph.Full(6) + cl = g.community_fluid_communities(2) + self.assertIsInstance(cl, VertexClustering) + self.assertEqual(len(cl.membership), g.vcount()) def testInfomap(self): g = Graph.Famous("zachary") cl = g.community_infomap() - self.assertAlmostEqual(cl.codelength, 4.60605, places=3) + self.assertAlmostEqual(cl.codelength, 4.31179, places=3) self.assertAlmostEqual(cl.q, 0.40203, places=3) self.assertMembershipsEqual( cl, @@ -483,6 +534,55 @@ def testSpinglass(self): ok = True break self.assertTrue(ok) + + def testVoronoi(self): + # Test 1: Two disconnected cliques - should find exactly 2 communities + g = Graph.Full(5) + Graph.Full(5) # Two separate complete graphs + cl = g.community_voronoi() + + # Should find exactly 2 communities + self.assertEqual(len(cl), 2) + + # Vertices 0-4 should be in one community, vertices 5-9 in another + communities = [set(), set()] + for vertex, community in enumerate(cl.membership): + communities[community].add(vertex) + + # One community should have vertices 0-4, the other should have 5-9 + expected_communities = [{0, 1, 2, 3, 4}, {5, 6, 7, 8, 9}] + self.assertEqual( + set(frozenset(c) for c in communities), + set(frozenset(c) for c in expected_communities) + ) + + # Test 2: Two cliques connected by a single bridge edge + g = Graph.Full(4) + Graph.Full(4) + g.add_edges([(0, 4)]) # Bridge connecting the two cliques + + cl = g.community_voronoi() + + # Should still find 2 communities (bridge is weak) + self.assertEqual(len(cl), 2) + + # Check that vertices within each clique are in the same community + # Vertices 0,1,2,3 should be together, and 4,5,6,7 should be together + comm_0123 = {cl.membership[i] for i in [0, 1, 2, 3]} + comm_4567 = {cl.membership[i] for i in [4, 5, 6, 7]} + + self.assertEqual(len(comm_0123), 1) # All in same community + self.assertEqual(len(comm_4567), 1) # All in same community + self.assertNotEqual(comm_0123, comm_4567) # Different communities + + # Test 3: Three disconnected triangles + g = Graph(9) + g.add_edges([(0, 1), (1, 2), (2, 0), # Triangle 1 + (3, 4), (4, 5), (5, 3), # Triangle 2 + (6, 7), (7, 8), (8, 6)]) # Triangle 3 + + cl = g.community_voronoi() + + # Should find exactly 3 communities + self.assertEqual(len(cl), 3) def testWalktrap(self): g = Graph.Full(5) + Graph.Full(5) + Graph.Full(5) diff --git a/tests/test_foreign.py b/tests/test_foreign.py index 69657062e..83664e5f5 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -215,14 +215,14 @@ def _testNCOLOrLGL(self, func, fname, can_be_reopened=True): self.assertTrue( "name" not in g.vertex_attributes() and "weight" in g.edge_attributes() ) - self.assertTrue(g.es["weight"] == [1, 2, 0, 3, 0]) + self.assertListEqual(g.es["weight"], [1.0, 2.0, 1.0, 3.0, 1.0]) g = func(fname, directed=False) self.assertTrue( "name" in g.vertex_attributes() and "weight" in g.edge_attributes() ) - self.assertTrue(g.vs["name"] == ["eggs", "spam", "ham", "bacon"]) - self.assertTrue(g.es["weight"] == [1, 2, 0, 3, 0]) + self.assertListEqual(g.vs["name"], ["eggs", "spam", "ham", "bacon"]) + self.assertListEqual(g.es["weight"], [1.0, 2.0, 1.0, 3.0, 1.0]) def testNCOL(self): with temporary_file( @@ -360,6 +360,7 @@ def testGraphML(self): self.assertTrue("name" in g.vertex_attributes()) g.write_graphml(tmpfname) + g.write_graphml(tmpfname, prefixattr=False) def testGraphMLz(self): with temporary_file( diff --git a/tests/test_games.py b/tests/test_games.py index 3e78637dc..cf36c75ce 100644 --- a/tests/test_games.py +++ b/tests/test_games.py @@ -163,7 +163,7 @@ def testRewire(self): self.assertTrue(g.is_simple()) # Rewiring with loops (1) - g.rewire(10000, mode="loops") + g.rewire(10000, allowed_edge_types="loops") self.assertEqual(degrees, g.degree()) self.assertFalse(any(g.is_multiple())) @@ -171,7 +171,7 @@ def testRewire(self): g = Graph.Full(4) g[1, 3] = 0 degrees = g.degree() - g.rewire(100, mode="loops") + g.rewire(100, allowed_edge_types="loops") self.assertEqual(degrees, g.degree()) self.assertFalse(any(g.is_multiple())) @@ -185,7 +185,7 @@ def testRewire(self): self.assertTrue(g.is_simple()) # Directed graph with loops - g.rewire(10000, mode="loops") + g.rewire(10000, allowed_edge_types="loops") self.assertEqual(indeg, g.indegree()) self.assertEqual(outdeg, g.outdegree()) self.assertFalse(any(g.is_multiple())) diff --git a/tests/test_generators.py b/tests/test_generators.py index 31b8692bc..0e1227658 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -110,6 +110,7 @@ def testFull(self): g = Graph.Full(20, directed=True) el = g.get_edgelist() el.sort() + self.assertTrue(g.is_complete()) self.assertTrue( g.get_edgelist() == [(x, y) for x in range(20) for y in range(20) if x != y] ) @@ -160,6 +161,24 @@ def testHexagonalLattice(self): g = Graph.Hexagonal_Lattice([2, 2], directed=True, mutual=True) self.assertEqual(sorted(g.get_edgelist()), sorted(el + [(y, x) for x, y in el])) + def testHypercube(self): + el = [ + (0, 1), + (0, 2), + (0, 4), + (1, 3), + (1, 5), + (2, 3), + (2, 6), + (3, 7), + (4, 5), + (4, 6), + (5, 7), + (6, 7), + ] + g = Graph.Hypercube(3) + self.assertEqual(g.get_edgelist(), el) + def testLCF(self): g1 = Graph.LCF(12, (5, -5), 6) g2 = Graph.Famous("Franklin") @@ -249,6 +268,67 @@ def testRealizeDegreeSequence(self): self.assertFalse(g.is_directed()) self.assertTrue(g.degree() == degrees) + def testRealizeBipartiteDegreeSequence(self): + deg1 = [2, 2] + deg2 = [1, 1, 2] + g = Graph.Realize_Bipartite_Degree_Sequence( + deg1, + deg2, + "simple", + "smallest", + ) + self.assertFalse(g.is_directed()) + self.assertTrue(g.is_connected()) + self.assertTrue(g.degree() == deg1 + deg2) + + g = Graph.Realize_Bipartite_Degree_Sequence( + deg1, + deg2, + "simple", + "largest", + ) + self.assertFalse(g.is_directed()) + self.assertTrue(g.degree() == deg1 + deg2) + + g = Graph.Realize_Bipartite_Degree_Sequence( + deg1, + deg2, + "simple", + "index", + ) + self.assertFalse(g.is_directed()) + self.assertTrue(g.degree() == deg1 + deg2) + + deg1 = [3, 1, 1] + deg2 = [2, 3] + self.assertRaises( + InternalError, + Graph.Realize_Bipartite_Degree_Sequence, + deg1, + deg2, + "simple", + "smallest", + ) + + self.assertRaises( + InternalError, + Graph.Realize_Bipartite_Degree_Sequence, + deg1, + deg2, + "simple", + "index", + ) + + g = Graph.Realize_Bipartite_Degree_Sequence( + deg1, + deg2, + "multi", + "smallest", + ) + self.assertFalse(g.is_directed()) + self.assertTrue(g.is_connected()) + self.assertTrue(g.degree() == deg1 + deg2) + def testKautz(self): g = Graph.Kautz(2, 2) deg_in = g.degree(mode="in") @@ -349,9 +429,8 @@ def testLattice(self): def testSBM(self): pref_matrix = [[0.5, 0, 0], [0, 0, 0.5], [0, 0.5, 0]] - n = 60 types = [20, 20, 20] - g = Graph.SBM(n, pref_matrix, types) + g = Graph.SBM(pref_matrix, types) # Simple smoke tests for the expected structure of the graph self.assertTrue(g.is_simple()) @@ -360,22 +439,20 @@ def testSBM(self): g2 = g.subgraph(list(range(20, 60))) self.assertTrue(not any(e.source // 20 == e.target // 20 for e in g2.es)) - # Check loops argument - g = Graph.SBM(n, pref_matrix, types, loops=True) + # Check allowed_edge_types argument + g = Graph.SBM(pref_matrix, types, allowed_edge_types="loops") self.assertFalse(g.is_simple()) self.assertTrue(sum(g.is_loop()) > 0) # Check directedness - g = Graph.SBM(n, pref_matrix, types, directed=True) + g = Graph.SBM(pref_matrix, types, directed=True) self.assertTrue(g.is_directed()) self.assertTrue(sum(g.is_mutual()) < g.ecount()) self.assertTrue(sum(g.is_loop()) == 0) # Check error conditions - self.assertRaises(ValueError, Graph.SBM, -1, pref_matrix, types) - self.assertRaises(InternalError, Graph.SBM, 61, pref_matrix, types) pref_matrix[0][1] = 0.7 - self.assertRaises(InternalError, Graph.SBM, 60, pref_matrix, types) + self.assertRaises(InternalError, Graph.SBM, pref_matrix, types) def testTriangularLattice(self): g = Graph.Triangular_Lattice([2, 2]) @@ -414,33 +491,35 @@ def testAdjacencyNumPy(self): # ADJ_DIRECTED (default) g = Graph.Adjacency(mat) el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)]) + self.assertListEqual( + sorted(el), [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)] + ) # ADJ MIN g = Graph.Adjacency(mat, mode="min") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (2, 2), (2, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (2, 2), (2, 2)]) # ADJ MAX g = Graph.Adjacency(mat, mode="max") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (1, 3), (2, 2), (2, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 3), (2, 2), (2, 2)]) # ADJ LOWER g = Graph.Adjacency(mat, mode="lower") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (2, 2), (2, 2), (1, 3)]) + self.assertListEqual(sorted(el), [(0, 1), (1, 3), (2, 2), (2, 2)]) # ADJ UPPER g = Graph.Adjacency(mat, mode="upper") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (2, 2), (2, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (2, 2), (2, 2)]) @unittest.skipIf(np is None, "test case depends on NumPy") def testAdjacencyNumPyLoopHandling(self): @@ -451,64 +530,66 @@ def testAdjacencyNumPyLoopHandling(self): # ADJ_DIRECTED (default) g = Graph.Adjacency(mat) el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)]) + self.assertListEqual( + sorted(el), [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)] + ) # ADJ MIN g = Graph.Adjacency(mat, mode="min", loops="twice") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (2, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (2, 2)]) # ADJ MAX g = Graph.Adjacency(mat, mode="max", loops="twice") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (1, 3), (2, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 3), (2, 2)]) # ADJ LOWER g = Graph.Adjacency(mat, mode="lower", loops="twice") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (2, 2), (1, 3)]) + self.assertListEqual(sorted(el), [(0, 1), (1, 3), (2, 2), (2, 2)]) # ADJ UPPER g = Graph.Adjacency(mat, mode="upper", loops="twice") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (2, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (2, 2), (2, 2)]) # ADJ_DIRECTED (default) g = Graph.Adjacency(mat, loops=False) el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (3, 1)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 0), (3, 1)]) # ADJ MIN g = Graph.Adjacency(mat, mode="min", loops=False) el = g.get_edgelist() - self.assertTrue(el == [(0, 1)]) + self.assertListEqual(sorted(el), [(0, 1)]) # ADJ MAX g = Graph.Adjacency(mat, mode="max", loops=False) el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (1, 3)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 3)]) # ADJ LOWER g = Graph.Adjacency(mat, mode="lower", loops=False) el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (1, 3)]) + self.assertListEqual(sorted(el), [(0, 1), (1, 3)]) # ADJ UPPER g = Graph.Adjacency(mat, mode="upper", loops=False) el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2)]) @unittest.skipIf( (sparse is None) or (np is None), "test case depends on NumPy/SciPy" @@ -622,23 +703,23 @@ def testWeightedAdjacency(self): g = Graph.Weighted_Adjacency(mat, attr="w0") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 2.5, 1]) + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 2.5]) g = Graph.Weighted_Adjacency(mat, mode="plus") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 3), (2, 2)]) - self.assertTrue(g.es["weight"] == [3, 2, 1, 2.5]) + self.assertListEqual(el, [(0, 1), (0, 2), (1, 3), (2, 2)]) + self.assertListEqual(g.es["weight"], [3, 2, 1, 2.5]) g = Graph.Weighted_Adjacency(mat, attr="w0", loops=False) el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 1]) + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2]) g = Graph.Weighted_Adjacency(mat, attr="w0", loops="twice") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 1.25, 1]) + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 2.5]) @unittest.skipIf(np is None, "test case depends on NumPy") def testWeightedAdjacencyNumPy(self): @@ -648,23 +729,23 @@ def testWeightedAdjacencyNumPy(self): g = Graph.Weighted_Adjacency(mat, attr="w0") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 2.5, 1]) + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 2.5]) g = Graph.Weighted_Adjacency(mat, mode="plus") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 3), (2, 2)]) - self.assertTrue(g.es["weight"] == [3, 2, 1, 2.5]) + self.assertListEqual(el, [(0, 1), (0, 2), (1, 3), (2, 2)]) + self.assertListEqual(g.es["weight"], [3, 2, 1, 2.5]) g = Graph.Weighted_Adjacency(mat, attr="w0", loops=False) el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 1]) + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2]) g = Graph.Weighted_Adjacency(mat, attr="w0", loops="twice") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 1.25, 1]) + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 2.5]) @unittest.skipIf( (sparse is None) or (np is None), "test case depends on NumPy/SciPy" @@ -678,36 +759,36 @@ def testSparseWeightedAdjacency(self): el = g.get_edgelist() self.assertTrue(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 2.5, 1]) + self.assertListEqual(el, [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) + self.assertListEqual(g.es["w0"], [1, 2, 2, 2.5, 1]) g = Graph.Weighted_Adjacency(mat, mode="plus") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (2, 2), (1, 3)]) - self.assertTrue(g.es["weight"] == [3, 2, 2.5, 1]) + self.assertListEqual(el, [(0, 1), (0, 2), (2, 2), (1, 3)]) + self.assertListEqual(g.es["weight"], [3, 2, 2.5, 1]) g = Graph.Weighted_Adjacency(mat, mode="min") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (2, 2)]) - self.assertTrue(g.es["weight"] == [1, 2.5]) + self.assertListEqual(el, [(0, 1), (2, 2)]) + self.assertListEqual(g.es["weight"], [1, 2.5]) g = Graph.Weighted_Adjacency(mat, attr="w0", loops=False) el = g.get_edgelist() self.assertTrue(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 1]) + self.assertListEqual(el, [(0, 1), (0, 2), (1, 0), (3, 1)]) + self.assertListEqual(g.es["w0"], [1, 2, 2, 1]) g = Graph.Weighted_Adjacency(mat, attr="w0", loops="twice") el = g.get_edgelist() self.assertTrue(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 1.25, 1]) + self.assertListEqual(el, [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) + self.assertListEqual(g.es["w0"], [1, 2, 2, 1.25, 1]) @unittest.skipIf((np is None) or (pd is None), "test case depends on NumPy/Pandas") def testDataFrame(self): @@ -721,9 +802,9 @@ def testDataFrame(self): [["A", "blue"], ["B", "yellow"], ["C", "blue"]], columns=[0, "color"] ) g = Graph.DataFrame(edges, directed=True, vertices=vertices, use_vids=False) - self.assertTrue(g.vs["name"] == ["A", "B", "C"]) - self.assertTrue(g.vs["color"] == ["blue", "yellow", "blue"]) - self.assertTrue(g.es["weight"] == [0.4, 0.1]) + self.assertListEqual(g.vs["name"], ["A", "B", "C"]) + self.assertListEqual(g.vs["color"], ["blue", "yellow", "blue"]) + self.assertListEqual(g.es["weight"], [0.4, 0.1]) # Issue #347 edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) @@ -731,8 +812,8 @@ def testDataFrame(self): {"node": [1, 2, 3, 4, 5, 6], "label": ["1", "2", "3", "4", "5", "6"]} )[["node", "label"]] g = Graph.DataFrame(edges, directed=True, vertices=vertices, use_vids=False) - self.assertTrue(g.vs["name"] == [1, 2, 3, 4, 5, 6]) - self.assertTrue(g.vs["label"] == ["1", "2", "3", "4", "5", "6"]) + self.assertListEqual(g.vs["name"], [1, 2, 3, 4, 5, 6]) + self.assertListEqual(g.vs["label"], ["1", "2", "3", "4", "5", "6"]) # Vertex names edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) @@ -813,6 +894,27 @@ def testDataFrame(self): edges = pd.DataFrame(np.array([[0, 1], [1, np.nan], [1, 2]]), dtype="Int64") Graph.DataFrame(edges) + def testNearestNeighborGraph(self): + points = [[0, 0], [1, 2], [-3, -3]] + + g = Graph.Nearest_Neighbor_Graph(points) + # expecting 1 - 2, 3 - 1 + self.assertFalse(g.is_directed()) + self.assertEqual(g.vcount(), 3) + self.assertEqual(g.ecount(), 2) + + g = Graph.Nearest_Neighbor_Graph(points, directed=True) + # expecting 1 <-> 2, 3 -> 1 + self.assertTrue(g.is_directed()) + self.assertEqual(g.vcount(), 3) + self.assertEqual(g.ecount(), 3) + + # expecting a complete graph + g = Graph.Nearest_Neighbor_Graph(points, k=2) + self.assertFalse(g.is_directed()) + self.assertEqual(g.vcount(), 3) + self.assertTrue(g.is_complete()) + def suite(): generator_suite = unittest.defaultTestLoader.loadTestsFromTestCase(GeneratorTests) diff --git a/tests/test_isomorphism.py b/tests/test_isomorphism.py index c1150da63..7984e9b34 100644 --- a/tests/test_isomorphism.py +++ b/tests/test_isomorphism.py @@ -359,6 +359,7 @@ def testCanonicalPermutation(self): perm = list(range(10)) shuffle(perm) g2 = g.permute_vertices(perm) + self.assertTrue(g.isomorphic(g2)) g3 = g.permute_vertices(g.canonical_permutation()) g4 = g2.permute_vertices(g2.canonical_permutation()) @@ -400,7 +401,7 @@ def testPermuteVertices(self): (7, 4), ], ) - _, _, mapping = g1.isomorphic_vf2(g2, return_mapping_21=True) + _, mapping, _ = g1.isomorphic_vf2(g2, return_mapping_12=True) g3 = g2.permute_vertices(mapping) self.assertTrue(g3.vcount() == g2.vcount() and g3.ecount() == g2.ecount()) self.assertTrue(set(g3.get_edgelist()) == set(g1.get_edgelist())) diff --git a/tests/test_layouts.py b/tests/test_layouts.py index ac32e0210..337f498f8 100644 --- a/tests/test_layouts.py +++ b/tests/test_layouts.py @@ -1,6 +1,6 @@ import unittest from math import hypot -from igraph import Graph, Layout, BoundingBox, InternalError +from igraph import Graph, Layout, BoundingBox, InternalError, align_layout from igraph import umap_compute_weights @@ -242,8 +242,12 @@ def testKamadaKawai(self): self.assertTrue(bbox.right <= 6) lo = g.layout( - "kk", miny=[2] * 100, maxy=[3] * 100, minx=[4] * 100, maxx=[6] * 100, - weights=range(10, g.ecount() + 10) + "kk", + miny=[2] * 100, + maxy=[3] * 100, + minx=[4] * 100, + maxx=[6] * 100, + weights=range(10, g.ecount() + 10), ) self.assertTrue(isinstance(lo, Layout)) @@ -452,6 +456,13 @@ def testDRL(self): lo = g.layout("drl") self.assertTrue(isinstance(lo, Layout)) + def testAlign(self): + g = Graph.Ring(3, circular=False) + lo = Layout([[1,1], [2,2], [3,3]]) + lo = align_layout(g, lo) + self.assertTrue(isinstance(lo, Layout)) + self.assertTrue(all(abs(lo[i][1]) < 1e-10 for i in range(3))) + def suite(): layout_suite = unittest.defaultTestLoader.loadTestsFromTestCase(LayoutTests) diff --git a/tests/test_structural.py b/tests/test_structural.py index 541a2c6c8..c0f1bdb59 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -24,6 +24,13 @@ def testDensity(self): self.assertAlmostEqual(7 / 16, self.gdir.density(True), places=5) self.assertAlmostEqual(1 / 7, self.tree.density(), places=5) + def testMeanDegree(self): + self.assertEqual(9.0, self.gfull.mean_degree()) + self.assertEqual(0.0, self.gempty.mean_degree()) + self.assertEqual(2.5, self.g.mean_degree()) + self.assertEqual(7 / 4, self.gdir.mean_degree()) + self.assertAlmostEqual(13 / 7, self.tree.mean_degree(), places=5) + def testDiameter(self): self.assertTrue(self.gfull.diameter() == 1) self.assertTrue(self.gempty.diameter(unconn=False) == inf) @@ -242,6 +249,8 @@ class BiconnectedComponentTests(unittest.TestCase): g1 = Graph.Full(10) g2 = Graph(5, [(0, 1), (1, 2), (2, 3), (3, 4)]) g3 = Graph(6, [(0, 1), (1, 2), (2, 3), (3, 0), (2, 4), (2, 5), (4, 5)]) + g4 = Graph.Full(2) + g5 = Graph.Full(1) def testBiconnectedComponents(self): s = self.g1.biconnected_components() @@ -260,6 +269,14 @@ def testArticulationPoints(self): self.assertTrue(self.g1.articulation_points() == []) self.assertTrue(self.g2.cut_vertices() == [1, 2, 3]) self.assertTrue(self.g3.articulation_points() == [2]) + self.assertTrue(self.g4.articulation_points() == []) + + def testIsBiconnected(self): + self.assertTrue(self.g1.is_biconnected()) + self.assertFalse(self.g2.is_biconnected()) + self.assertFalse(self.g3.is_biconnected()) + self.assertTrue(self.g4.is_biconnected()) + self.assertFalse(self.g5.is_biconnected()) class CentralityTests(unittest.TestCase): @@ -486,19 +503,28 @@ def testEigenvectorCentrality(self): def testAuthorityScore(self): g = Graph.Tree(15, 2, TREE_IN) - asc = g.authority_score() + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + asc = g.authority_score() self.assertAlmostEqual(max(asc), 1.0, places=3) # Smoke testing - g.authority_score(scale=False, return_eigenvalue=True) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + g.authority_score(scale=False, return_eigenvalue=True) def testHubScore(self): g = Graph.Tree(15, 2, TREE_IN) - hsc = g.hub_score() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + hsc = g.hub_score() self.assertAlmostEqual(max(hsc), 1.0, places=3) # Smoke testing - g.hub_score(scale=False, return_eigenvalue=True) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + g.hub_score(scale=False, return_eigenvalue=True) def testCoreness(self): g = Graph.Full(4) + Graph(4) + [(0, 4), (1, 5), (2, 6), (3, 7)] @@ -807,14 +833,6 @@ def testDistances(self): g.distances(weights="weight", target=[2, 3], algorithm="johnson") == [row[2:4] for row in expected] ) - self.assertRaises( - ValueError, - g.distances, - weights="weight", - target=[2, 3], - algorithm="johnson", - mode="in", - ) def testGetShortestPath(self): g = Graph(4, [(0, 1), (0, 2), (1, 3), (3, 2), (2, 1)], directed=True) @@ -984,7 +1002,7 @@ def testGetAllSimplePaths(self): self.assertEqual(15, path[-1]) curr = path[0] for next in path[1:]: - self.assertTrue(g.are_connected(curr, next)) + self.assertTrue(g.are_adjacent(curr, next)) curr = next def testPathLengthHist(self): diff --git a/tox.ini b/tox.ini index 221c56a55..04fb5ca92 100644 --- a/tox.ini +++ b/tox.ini @@ -1,18 +1,19 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests +# Tox (https://tox.wiki) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] -envlist = py38, py39, py310, py311, py312, pypy3 +envlist = py38, py39, py310, py311, py312, py313, pypy3 [gh-actions] python = 3.8: py38 3.9: py39 - 3.10: py310 - 3.11: py311 - 3.12: py312 + 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 pypy-3.7: pypy3 [testenv] diff --git a/vendor/source/igraph b/vendor/source/igraph index 4e04d3943..7b4ae766c 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 4e04d39438ac03bf177529e754a07b05beb0930b +Subproject commit 7b4ae766cbdee6b2017aa5b76752457db2a2972f