chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
[bumpversion]
|
||||
current_version = 0.2a47
|
||||
commit = True
|
||||
tag = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)a(?P<patch>\d+)
|
||||
serialize = {major}.{minor}a{patch}
|
||||
|
||||
[bumpversion:file:setup.py]
|
||||
search = version="{current_version}"
|
||||
replace = version="{new_version}"
|
||||
@@ -0,0 +1,69 @@
|
||||
[flake8]
|
||||
min_python_version = 3.7.0
|
||||
max-line-length = 88
|
||||
ban-relative-imports = true
|
||||
# flake8-use-fstring: https://github.com/MichaelKim0407/flake8-use-fstring#--percent-greedy-and---format-greedy
|
||||
format-greedy = 1
|
||||
inline-quotes = double
|
||||
enable-extensions = TC, TC1
|
||||
type-checking-exempt-modules = typing, typing-extensions
|
||||
eradicate-whitelist-extend = ^-.*;
|
||||
extend-ignore =
|
||||
# E203: Whitespace before ':' (pycqa/pycodestyle#373)
|
||||
E203,
|
||||
# SIM106: Handle error-cases first
|
||||
SIM106,
|
||||
# ANN101: Missing type annotation for self in method
|
||||
ANN101,
|
||||
# ANN102: Missing type annotation for cls in classmethod
|
||||
ANN102,
|
||||
F401,
|
||||
F403,
|
||||
I252,
|
||||
SIM115,
|
||||
N806,
|
||||
N803,
|
||||
B007,
|
||||
E501,
|
||||
C408,
|
||||
SIM113,
|
||||
N802,
|
||||
N400,
|
||||
E501,
|
||||
E802,
|
||||
E800,
|
||||
B001,
|
||||
B006,
|
||||
B020,
|
||||
C414,
|
||||
C416,
|
||||
E402,
|
||||
E711,
|
||||
E712,
|
||||
E722,
|
||||
E741,
|
||||
F405,
|
||||
F541,
|
||||
F811,
|
||||
F821,
|
||||
F841,
|
||||
FS001,
|
||||
FS002,
|
||||
I250,
|
||||
N801,
|
||||
N804,
|
||||
N812,
|
||||
N816,
|
||||
Q002,
|
||||
SIM102,
|
||||
SIM105,
|
||||
SIM111,
|
||||
SIM114,
|
||||
SIM118,
|
||||
SIM201,
|
||||
SIM203,
|
||||
SIM300,
|
||||
SIM401,
|
||||
TC002
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: "\U0001F41E Bug Report"
|
||||
about: Did you find a bug?
|
||||
title: ''
|
||||
labels: Bug, Triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Hi there! Thank you for discovering and submitting an issue.
|
||||
|
||||
Before you submit this; let's make sure of a few things.
|
||||
Please make sure the following boxes are ticked if they are correct.
|
||||
If not, please try and fulfill these first.
|
||||
-->
|
||||
|
||||
<!-- Checked checkbox should look like this: [x] -->
|
||||
- [ ] I am on the [latest](https://github.com/easy-graph/Easy-Graph/releases/latest) EasyGraph version.
|
||||
- [ ] I have searched the [issues](https://github.com/easy-graph/Easy-Graph/issues) of this repo and believe that this is not a duplicate.
|
||||
|
||||
<!--
|
||||
Once those are done, if you're able to fill in the following list with your information,
|
||||
it'd be very helpful to whoever handles the issue.
|
||||
-->
|
||||
|
||||
- **OS version and name**: <!-- Replace with version + name -->
|
||||
- **EasyGraph version**: <!-- Replace with version -->
|
||||
<!-- - **Link of a [Gist](https://gist.github.com/) with the contents of your pyproject.toml file**: Gist Link Here -->
|
||||
|
||||
## Issue
|
||||
<!-- Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: "\U0001F4DA Documentation"
|
||||
about: Did you find errors, problems, or anything unintelligible in the docs (https://easy-graph.github.io/)?
|
||||
title: ''
|
||||
labels: Documentation, Triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Hi there! Thank you for discovering and submitting an issue with our documentation.
|
||||
|
||||
Before you submit this; let's make sure of a few things.
|
||||
Please make sure the following boxes are ticked if they are correct.
|
||||
If not, please try and fulfill these first.
|
||||
-->
|
||||
|
||||
<!-- Checked checkbox should look like this: [x] -->
|
||||
- [ ] I have searched the [issues](https://github.com/easy-graph/Easy-Graph/issues) of this repo and believe that this is not a duplicate.
|
||||
|
||||
## Issue
|
||||
<!-- Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "\U0001F5C3 Everything Else"
|
||||
about: For questions and issues that do not fall in any of the other categories. This
|
||||
can include questions about EasyGraph's roadmap. For support questions, please post
|
||||
on StackOverflow.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Describe your question and issue here. This space is meant to be used for general questions that are neither bugs, feature requests, nor documentation issues. A good example would be a question regarding EasyGraph's roadmap, for example. If you're looking for help when it comes to using EasyGraph, consider posting a question on StackOverflow instead: http://stackoverflow.com/questions/tagged/python-easygraph -->
|
||||
|
||||
|
||||
<!-- Checked checkbox should look like this: [x] -->
|
||||
- [ ] I have searched the [issues](https://github.com/easy-graph/Easy-Graph/issues) of this repo and believe that this is not a duplicate.
|
||||
- [ ] I have searched the [documentation](https://easy-graph.github.io/) and believe that my question is not covered.
|
||||
|
||||
## Issue
|
||||
<!-- Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: "\U0001F381 Feature Request"
|
||||
about: Do you have ideas for new features and improvements?
|
||||
title: ''
|
||||
labels: Feature, Triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Hi there! Thank you for wanting to make EasyGraph better.
|
||||
|
||||
Before you submit this; let's make sure of a few things.
|
||||
Please make sure the following boxes are ticked if they are correct.
|
||||
If not, please try and fulfill these first.
|
||||
-->
|
||||
|
||||
<!-- Checked checkbox should look like this: [x] -->
|
||||
- [ ] I have searched the [issues](https://github.com/easy-graph/Easy-Graph/issues) of this repo and believe that this is not a duplicate.
|
||||
- [ ] I have searched the [documentation](https://easy-graph.github.io/) and believe that my question is not covered.
|
||||
|
||||
## Feature Request
|
||||
<!-- Now feel free to write your idea for improvement. Thanks again 🙌 ❤️ -->
|
||||
@@ -0,0 +1,7 @@
|
||||
# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: '💬 Discord Server'
|
||||
url: https://discord.gg/JV6N8Whe
|
||||
about: |
|
||||
Chat with the community, ask questions and learn about best practices.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Pull Request Check List
|
||||
|
||||
Resolves: #issue-number-here
|
||||
|
||||
<!-- This is just a reminder about the most common mistakes. Please make sure that you tick all *appropriate* boxes. But please read our [contribution guide](https://github.com/easy-graph/Easy-Graph/blob/master/CONTRIBUTING.md) at least once, it will save you unnecessary review cycles! -->
|
||||
|
||||
- [ ] Added **tests** for changed code.
|
||||
- [ ] Updated **documentation** for changed code.
|
||||
|
||||
<!-- If you have *any* questions to *any* of the points above, just **submit and ask**! This checklist is here to *help* you, not to deter you from contributing! -->
|
||||
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -0,0 +1,44 @@
|
||||
# https://github.com/marketplace/actions/manylinux-wheel-builder
|
||||
name: pypi deployer
|
||||
on:
|
||||
workflow_dispatch
|
||||
# push
|
||||
# push:
|
||||
# tags:
|
||||
# - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
jobs:
|
||||
Linux-build:
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: build and upload manylinux wheels
|
||||
# cSpell:disable
|
||||
uses: Niraj-Kamdar/manylinux-wheel-builder@master
|
||||
# cSpell:enable
|
||||
Matrix-build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
os: [macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: build wheel
|
||||
run: |
|
||||
pip install wheel
|
||||
python setup.py bdist_wheel
|
||||
- name: upload wheel
|
||||
run: |
|
||||
pip install twine
|
||||
twine upload dist/*
|
||||
continue-on-error: true
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Build and Publish to PyPI (macOS)
|
||||
|
||||
# on: push
|
||||
# disabled on push cuz Error: Container action is only supported on Linux
|
||||
# no solution found yet
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
# name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
||||
name: Build and publish Python 🐍 distributions 📦 to PyPI
|
||||
# runs-on: ubuntu-22.04
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.6", "3.7", "3.8", "3.9"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install pypa/build
|
||||
run: >-
|
||||
python -m
|
||||
pip install
|
||||
build
|
||||
--user
|
||||
- name: Build a binary wheel and a source tarball
|
||||
run: >-
|
||||
python -m
|
||||
build
|
||||
--sdist
|
||||
--wheel
|
||||
--outdir dist/
|
||||
.
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
# if: startsWith(github.ref, 'refs/tags')
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Build and Publish to PyPI (manylinux)
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
# name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
||||
name: Build and publish Python 🐍 distributions 📦 to PyPI
|
||||
# runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-22.04
|
||||
# strategy:
|
||||
# matrix:
|
||||
# python-version: ["3.6", "3.7", "3.8", "3.9"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
# - name: Set up Python ${{ matrix.python-version }}
|
||||
# uses: actions/setup-python@v2
|
||||
# with:
|
||||
# python-version: ${{ matrix.python-version }}
|
||||
# - name: Install pypa/build
|
||||
# run: >-
|
||||
# python -m
|
||||
# pip install
|
||||
# build
|
||||
# --user
|
||||
# - name: Build a binary wheel and a source tarball
|
||||
# run: >-
|
||||
# python -m
|
||||
# build
|
||||
# --sdist
|
||||
# --wheel
|
||||
# --outdir dist/
|
||||
# .
|
||||
# cSpell:disable
|
||||
- name: Build on manylinux
|
||||
uses: RalfG/python-wheels-manylinux-build@v0.4.2-manylinux2014_x86_64
|
||||
# cSpell:enable
|
||||
with:
|
||||
python-versions: "cp36-cp36m cp37-cp37m cp38-cp38m cp39-cp39m"
|
||||
# build-requirements: "cython numpy"
|
||||
# system-packages: "lrzip-devel zlib-devel"
|
||||
# pre-build-command: "sh pre-build-script.sh"
|
||||
# package-path: "my_project"
|
||||
# pip-wheel-args: "-w ./dist --no-deps"
|
||||
# pip-wheel-args: "--sdist --wheel --outdir dist/"
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
# if: startsWith(github.ref, 'refs/tags')
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
@@ -0,0 +1,15 @@
|
||||
name: "Run Teddy's benchmarking workflow"
|
||||
on:
|
||||
# push:
|
||||
# pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
trigger:
|
||||
permissions: write-all
|
||||
name: "Trigger Teddy's benchmarking workflow"
|
||||
uses: tddschn/easygraph-bench/.github/workflows/bench-all.yaml@master
|
||||
secrets:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
releaseTag: "CI-easygraph-bench"
|
||||
@@ -0,0 +1,16 @@
|
||||
name: Create diagram
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
# push:
|
||||
# branches:
|
||||
# - master
|
||||
jobs:
|
||||
get_data:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@master
|
||||
- name: Update diagram
|
||||
uses: githubocto/repo-visualizer@main
|
||||
with:
|
||||
excluded_paths: "ignore,.github"
|
||||
@@ -0,0 +1,50 @@
|
||||
name: PR code review
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- pybind11
|
||||
push:
|
||||
branches:
|
||||
- pybind11
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [windows-latest, macos-latest, ubuntu-latest]
|
||||
python-version: ["3.9", "3.12"]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Upgrade pip
|
||||
run: python -m pip install --upgrade pip
|
||||
|
||||
- name: Installing dependencies
|
||||
uses: py-actions/py-dependency-install@v4
|
||||
with:
|
||||
path: "requirements.txt"
|
||||
|
||||
- name: Add requirements
|
||||
run: python -m pip install --upgrade setuptools wheel build packaging
|
||||
|
||||
- name: Build and install
|
||||
run: pip install --verbose .
|
||||
|
||||
- name: Smoke test
|
||||
run: python -c "import os, tempfile; os.chdir(tempfile.gettempdir()); import easygraph as eg; G = eg.Graph(); G.add_edge(1, 2); assert G.number_of_nodes() == 2; assert G.number_of_edges() == 1"
|
||||
@@ -0,0 +1,38 @@
|
||||
name: pre-commit
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- pybind11
|
||||
push:
|
||||
branches:
|
||||
- pybind11
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install pre-commit
|
||||
run: python -m pip install pre-commit
|
||||
|
||||
- name: Run pre-commit on changed files
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
base_ref="${{ github.event.pull_request.base.sha }}"
|
||||
elif [[ -n "${{ github.event.before }}" && ! "${{ github.event.before }}" =~ ^0+$ ]]; then
|
||||
base_ref="${{ github.event.before }}"
|
||||
else
|
||||
base_ref="$(git rev-parse HEAD~1)"
|
||||
fi
|
||||
|
||||
pre-commit run --show-diff-on-failure --color=always --from-ref "$base_ref" --to-ref "${{ github.sha }}"
|
||||
@@ -0,0 +1,19 @@
|
||||
name: "Close stale issues and PRs"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
with:
|
||||
stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days."
|
||||
stale-pr-message: "This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days."
|
||||
close-issue-message: "This issue was closed because it has been stalled for 5 days with no activity."
|
||||
close-pr-message: "This PR was closed because it has been stalled for 10 days with no activity."
|
||||
days-before-issue-stale: 30
|
||||
days-before-pr-stale: 45
|
||||
days-before-issue-close: 5
|
||||
days-before-pr-close: 10
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Test the package with pytest
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- pybind11
|
||||
push:
|
||||
branches:
|
||||
- pybind11
|
||||
|
||||
jobs:
|
||||
pytest:
|
||||
name: Test with pytest
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# ubuntu 22.04 has deprecated python 3.6
|
||||
python-version: [ "3.8", "3.9", "3.10","3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Installing dependencies
|
||||
uses: py-actions/py-dependency-install@v4
|
||||
with:
|
||||
path: "requirements.txt"
|
||||
|
||||
- name: Installing test dependencies
|
||||
run: |
|
||||
pip install pytest
|
||||
|
||||
- name: Build and install package
|
||||
run: |
|
||||
pip install .
|
||||
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest easygraph --disable-warnings
|
||||
@@ -0,0 +1,34 @@
|
||||
#name: 'issue-translator'
|
||||
#on:
|
||||
# issue_comment:
|
||||
# types: [created]
|
||||
# issues:
|
||||
# types: [opened]
|
||||
#
|
||||
#jobs:
|
||||
# build:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: usthe/issues-translate-action@v2.7
|
||||
# with:
|
||||
# IS_MODIFY_TITLE: false
|
||||
# # not require, default false, . Decide whether to modify the issue title
|
||||
# # if true, the robot account @Issues-translate-bot must have modification permissions, invite @Issues-translate-bot to your project or use your custom bot.
|
||||
# CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑🤝🧑👫🧑🏿🤝🧑🏻👩🏾🤝👨🏿👬🏿
|
||||
# # not require. Customize the translation robot prefix message.
|
||||
name: 'issue-translator'
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: usthe/issues-translate-action@v2.7
|
||||
with:
|
||||
CUSTOM_BOT_NOTE: hello
|
||||
BOT_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Build,download and publish wheels
|
||||
on: workflow_dispatch
|
||||
jobs:
|
||||
build_sdist:
|
||||
name: Build source distribution
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# sdist used to compile in user local environment
|
||||
- name: Build sdist
|
||||
run: pipx run build --sdist
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cibw-sdist
|
||||
path: dist/*.tar.gz
|
||||
|
||||
build_wheels:
|
||||
name: Build wheels on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
# macos-13 is closing down, macos-15 is apple silicon
|
||||
# macos-15-intel is for the x86_64
|
||||
# note: Apple has discontinued support for the x86_64 (Intel) architecture
|
||||
os: [ ubuntu-latest, windows-latest, macos-15, macos-15-intel]
|
||||
# modify python version in project.toml
|
||||
steps:
|
||||
# 检出代码
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
|
||||
# You can test your matrix by printing the current Python version
|
||||
- name: Display Python version
|
||||
run: python -c "import sys; print(sys.version)"
|
||||
- name: Install dependencies
|
||||
run: python -m pip install --upgrade pip setuptools wheel build packaging
|
||||
|
||||
|
||||
- name: Build wheels
|
||||
uses: pypa/cibuildwheel@v2.23.3
|
||||
with:
|
||||
output-dir: dist/
|
||||
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
|
||||
path: dist/*.whl
|
||||
|
||||
|
||||
upload_pypi:
|
||||
needs: [build_wheels, build_sdist]
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
# unpacks all CIBW artifacts into dist/
|
||||
pattern: cibw-*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.12.4
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
skip-existing: true
|
||||
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
*.tmp
|
||||
.vscode
|
||||
__pycache__
|
||||
test/
|
||||
related-repos/
|
||||
tmp_test/test.py
|
||||
tmp_test/dynamic_hypergraph.pdf
|
||||
tmp_test/hypergraph_learning_tutorial.py
|
||||
tmp_test/visulation_test.py
|
||||
hypergraph-bench/
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/python
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/python
|
||||
.idea/
|
||||
|
||||
# Visual Studio project files
|
||||
x64/
|
||||
.vs/
|
||||
*.sln
|
||||
*.vcxproj
|
||||
*.vcxproj.filters
|
||||
*.vcxproj.user
|
||||
|
||||
workspace/
|
||||
.DS_Store# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
tmp_test/allset_test.py
|
||||
tmp_test/
|
||||
RandomNetwork.txt
|
||||
TEST/
|
||||
CON_TEST/
|
||||
Comm_TEST/
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "cpp_easygraph/pybind11"]
|
||||
path = cpp_easygraph/pybind11
|
||||
url = https://github.com/pybind/pybind11.git
|
||||
@@ -0,0 +1,94 @@
|
||||
ci:
|
||||
autofix_prs: false
|
||||
|
||||
repos:
|
||||
# - repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
# rev: v4.3.0
|
||||
# hooks:
|
||||
# - id: trailing-whitespace
|
||||
# - id: end-of-file-fixer
|
||||
# exclude: ^.*\.egg-info/
|
||||
# - id: check-merge-conflict
|
||||
# - id: check-case-conflict
|
||||
# - id: check-json
|
||||
# - id: check-toml
|
||||
# - id: check-yaml
|
||||
# - id: pretty-format-json
|
||||
# args: [--autofix, --no-ensure-ascii, --no-sort-keys]
|
||||
# - id: check-ast
|
||||
# - id: debug-statements
|
||||
# - id: check-docstring-first
|
||||
|
||||
# - repo: https://github.com/pre-commit/pygrep-hooks
|
||||
# rev: v1.9.0
|
||||
# hooks:
|
||||
# - id: python-check-mock-methods
|
||||
# - id: python-use-type-annotations
|
||||
# - id: python-check-blanket-noqa
|
||||
|
||||
# - repo: https://github.com/asottile/yesqa
|
||||
# rev: v1.3.0
|
||||
# hooks:
|
||||
# - id: yesqa
|
||||
# additional_dependencies: &flake8_deps
|
||||
# - flake8-annotations==2.9.0
|
||||
# - flake8-broken-line==0.4.0
|
||||
# - flake8-bugbear==22.4.25
|
||||
# - flake8-comprehensions==3.10.0
|
||||
# - flake8-eradicate==1.2.1
|
||||
# - flake8-quotes==3.3.1
|
||||
# - flake8-simplify==0.19.2
|
||||
# - flake8-tidy-imports==4.8.0
|
||||
# - flake8-type-checking==1.5.0
|
||||
# - flake8-typing-imports==1.12.0
|
||||
# - flake8-use-fstring==1.3
|
||||
# - pep8-naming==0.12.1
|
||||
|
||||
# - repo: https://github.com/asottile/pyupgrade
|
||||
# rev: v2.37.1
|
||||
# hooks:
|
||||
# - id: pyupgrade
|
||||
# args: [--py36-plus]
|
||||
# exclude: ^(install|get)-poetry.py$
|
||||
|
||||
# - repo: https://github.com/hadialqattan/pycln
|
||||
# rev: v2.0.4
|
||||
# hooks:
|
||||
# - id: pycln
|
||||
# args: [--all]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
# rev: 5.10.1
|
||||
# https://stackoverflow.com/questions/75269700/pre-commit-fails-to-install-isort-5-11-4-with-error-runtimeerror-the-poetry-co
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
name: "isort (python)"
|
||||
types: [python]
|
||||
args: []
|
||||
# args: [--add-import, from __future__ import annotations]
|
||||
# exclude: |
|
||||
# (?x)(
|
||||
# ^(install|get)-poetry.py$
|
||||
# | ^src/poetry/__init__.py$
|
||||
# )
|
||||
# - id: isort
|
||||
# name: "isort (pyi)"
|
||||
# types: [pyi]
|
||||
# args: [--lines-after-imports, "-1"]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.6.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
# - repo: https://github.com/pycqa/flake8
|
||||
# rev: 4.0.1
|
||||
# hooks:
|
||||
# - id: flake8
|
||||
# additional_dependencies: *flake8_deps
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit
|
||||
rev: v2.20.0
|
||||
hooks:
|
||||
- id: validate_manifest
|
||||
@@ -0,0 +1,19 @@
|
||||
cmake_minimum_required(VERSION 3.23)
|
||||
|
||||
project(easygraph)
|
||||
|
||||
option(EASYGRAPH_ENABLE_OPENMP "Enable OpenMP acceleration (auto-detect)" ON)
|
||||
|
||||
option(EASYGRAPH_ENABLE_GPU "EASYGRAPH_ENABLE_GPU" OFF)
|
||||
|
||||
add_subdirectory(cpp_easygraph)
|
||||
|
||||
if (EASYGRAPH_ENABLE_GPU)
|
||||
message("easygraph gpu module is enabled")
|
||||
add_subdirectory(gpu_easygraph)
|
||||
target_include_directories(cpp_easygraph
|
||||
PRIVATE gpu_easygraph
|
||||
)
|
||||
else()
|
||||
message("easygraph gpu module is disabled")
|
||||
endif()
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
# Contributing to easygraph
|
||||
|
||||
First off, thanks for taking the time to contribute!
|
||||
|
||||
The following is a set of guidelines for contributing to easygraph on GitHub. These are mostly guidelines, not rules. Use your best judgement, and feel free to propose changes to this document in a pull request.
|
||||
|
||||
#### Table of contents
|
||||
|
||||
[How to contribute](#how-to-contribute)
|
||||
|
||||
* [Reporting bugs](#reporting-bugs)
|
||||
* [Suggesting enhancements](#suggesting-enhancements)
|
||||
* [Contributing to documentation](#contributing-to-documentation)
|
||||
* [Contributing to code](#contributing-to-code)
|
||||
* [Issue triage](#issue-triage)
|
||||
* [Git workflow](#git-workflow)
|
||||
|
||||
|
||||
## How to contribute
|
||||
|
||||
### Reporting bugs
|
||||
|
||||
This section guides you through submitting a bug report for easygraph.
|
||||
Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports.
|
||||
|
||||
Before creating bug reports, please check [this list](#before-submitting-a-bug-report) to be sure that you need to create one. When you are creating a bug report, please include as many details as possible. Fill out the [required template](https://github.com/easy-graph/Easy-Graph/blob/master/.github/ISSUE_TEMPLATE/---bug-report.md), the information it asks helps the maintainers resolve the issue faster.
|
||||
|
||||
> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
|
||||
|
||||
#### Before submitting a bug report
|
||||
|
||||
<!-- * **Check the [FAQs on the official website](https://python-easygraph.org/docs/faq)** for a list of common questions and problems. -->
|
||||
* **Check that your issue does not already exist in the [issue tracker](https://github.com/easy-graph/Easy-Graph/issues)**.
|
||||
|
||||
#### How do I submit a bug report?
|
||||
|
||||
Bugs are tracked on the [official issue tracker](https://github.com/easy-graph/Easy-Graph/issues) where you can create a new one and provide the following information by filling in [the template](https://github.com/easy-graph/Easy-Graph/blob/master/.github/ISSUE_TEMPLATE/---bug-report.md).
|
||||
|
||||
Explain the problem and include additional details to help maintainers reproduce the problem:
|
||||
|
||||
* **Use a clear and descriptive title** for the issue to identify the problem.
|
||||
* **Describe the exact steps which reproduce the problem** in as many details as possible.
|
||||
<!-- * **Provide your pyproject.toml file** in a [Gist](https://gist.github.com) after removing potential private information (like private package repositories). -->
|
||||
* **Provide specific examples to demonstrate the steps to reproduce the issue**. Include links to files or GitHub projects, or copy-paste-able snippets, which you use in those examples.
|
||||
* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
|
||||
* **Explain which behavior you expected to see instead and why.**
|
||||
<!-- * **If the problem is an unexpected error being raised**, execute the corresponding command in **debug** mode (the `-vvv` option). -->
|
||||
|
||||
Provide more context by answering these questions:
|
||||
|
||||
* **Did the problem start happening recently** (e.g. after updating to a new version of easygraph) or was this always a problem?
|
||||
* If the problem started happening recently, **can you reproduce the problem in an older version of easygraph?** What's the most recent version in which the problem doesn't happen?
|
||||
* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
|
||||
|
||||
Include details about your configuration and environment:
|
||||
|
||||
* **Which version of easygraph are you using?**
|
||||
* **Which Python version easygraph has been installed for?**
|
||||
* **What's the name and version of the OS you're using**?
|
||||
|
||||
|
||||
### Suggesting enhancements
|
||||
|
||||
This section guides you through submitting an enhancement suggestion for easygraph, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions.
|
||||
|
||||
Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-an-enhancement-suggestion). Fill in [the template](https://github.com/easy-graph/Easy-Graph/blob/master/.github/ISSUE_TEMPLATE/---feature-request.md), including the steps that you imagine you would take if the feature you're requesting existed.
|
||||
|
||||
#### Before submitting an enhancement suggestion
|
||||
|
||||
<!-- * **Check the [FAQs on the official website](https://python-easygraph.org/docs/faq)** for a list of common questions and problems. -->
|
||||
* **Check that your issue does not already exist in the [issue tracker](https://github.com/easy-graph/Easy-Graph/issues)**.
|
||||
|
||||
#### How do I submit an Enhancement suggestion?
|
||||
|
||||
Enhancement suggestions are tracked on the [official issue tracker](https://github.com/easy-graph/Easy-Graph/issues) where you can create a new one and provide the following information:
|
||||
|
||||
* **Use a clear and descriptive title** for the issue to identify the suggestion.
|
||||
* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
|
||||
* **Provide specific examples to demonstrate the steps**..
|
||||
* **Describe the current behavior** and **explain which behavior you expected to see instead** and why.
|
||||
|
||||
### Contributing to documentation
|
||||
|
||||
One of the simplest ways to get started contributing to a project is through improving documentation. easygraph is constantly evolving, this means that sometimes our documentation has gaps. You can help by
|
||||
adding missing sections, editing the existing content so it is more accessible or creating new content (tutorials, FAQs, etc).
|
||||
|
||||
> **Note:** A great way to understand easygraph's design and how it all fits together, is to add FAQ entries for commonly
|
||||
> asked questions. easygraph members usually mark issues with [candidate/faq](https://github.com/easy-graph/Easy-Graph/issues?q=is%3Aissue+label%3Acandidate%2Ffaq+) to indicate that the issue either contains a response
|
||||
> that explains how something works or might benefit from an entry in the FAQ.
|
||||
|
||||
Issues pertaining to the documentation are usually marked with the [Documentation](https://github.com/easy-graph/Easy-Graph/labels/Documentation) label.
|
||||
|
||||
### Contributing to code
|
||||
|
||||
#### Picking an issue
|
||||
|
||||
> **Note:** If you are a first time contributor, and are looking for an issue to take on, you might want to look for [Good First Issue](https://github.com/easy-graph/Easy-Graph/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22)
|
||||
> labelled issues. We do our best to label such issues, however we might fall behind at times. So, ask us.
|
||||
|
||||
<!-- If you would like to take on an issue, feel free to comment on the issue tagging `@python-easygraph/triage`. We are more than happy to discuss solutions on the issue. If you would like help with navigating -->
|
||||
the code base, join us on our [Discord Server](https://discord.gg/ppgcw2wUPg).
|
||||
|
||||
#### Local development
|
||||
|
||||
You will need easygraph to start contributing on the easygraph codebase. Refer to the [documentation](https://easy-graph.github.io/) to start using easygraph.
|
||||
|
||||
> **Note:** Local development of easygraph requires Python 3.8 or newer.
|
||||
|
||||
You will first need to clone the repository using `git` and place yourself in its directory:
|
||||
|
||||
```bash
|
||||
git clone git@github.com:python-easygraph/easygraph.git
|
||||
cd easygraph
|
||||
```
|
||||
|
||||
> **Note:** We recommend that you use a personal [fork](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo) for this step. If you are new to GitHub collaboration,
|
||||
> you can refer to the [Forking Projects Guide](https://guides.github.com/activities/forking/).
|
||||
|
||||
Now, you will need to install the required dependency for easygraph and be sure that the current
|
||||
tests are passing on your machine:
|
||||
|
||||
```bash
|
||||
pytest
|
||||
```
|
||||
|
||||
<!-- easygraph uses [mypy](https://github.com/python/mypy) for typechecking, and the CI
|
||||
will fail if it finds any errors. To run mypy locally:
|
||||
|
||||
```bash
|
||||
easygraph run mypy
|
||||
``` -->
|
||||
|
||||
easygraph uses the [black](https://github.com/psf/black) coding style and you must ensure that your
|
||||
code follows it. If not, the CI will fail and your Pull Request will not be merged.
|
||||
|
||||
Similarly, the import statements are sorted with [isort](https://github.com/timothycrosley/isort)
|
||||
and special care must be taken to respect it. If you don't, the CI will fail as well.
|
||||
|
||||
To make sure that you don't accidentally commit code that does not follow the coding style, you can
|
||||
install a [pre-commit](https://pre-commit.com/) hook that will check that everything is in order:
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
You can also run it anytime using:
|
||||
|
||||
```bash
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
Your code must always be accompanied by corresponding tests, if tests are not present your code
|
||||
will not be merged.
|
||||
|
||||
#### Run the Tests Locally
|
||||
|
||||
- Install [tox](https://tox.readthedocs.io/) and [pyenv](https://github.com/pyenv/pyenv)
|
||||
- Use pyenv to install python 3.6 to 3.10, and make sure that `python3.6`, `python3.7`, ..., `python3.10` is in your `$PATH`.
|
||||
- Run `tox` to run the tests across python 3.6 to 3.10.
|
||||
- To only run tox on python3.9 and 3.10, run `tox -e py39,py310`.
|
||||
|
||||
#### Pull requests
|
||||
|
||||
* Fill in [the required template](https://github.com/easy-graph/Easy-Graph/blob/master/.github/PULL_REQUEST_TEMPLATE.md)
|
||||
* Be sure that your pull request contains tests that cover the changed or added code.
|
||||
* If your changes warrant a documentation change, the pull request must also update the documentation.
|
||||
|
||||
> **Note:** Make sure your branch is [rebased](https://docs.github.com/en/free-pro-team@latest/github/using-git/about-git-rebase) against the latest main branch. A maintainer might ask you to ensure the branch is
|
||||
> up-to-date prior to merging your Pull Request if changes have conflicts.
|
||||
|
||||
All pull requests, unless otherwise instructed, need to be first accepted into the main branch (`master`).
|
||||
|
||||
### Issue triage
|
||||
|
||||
<!-- > **Note:** If you have an issue that hasn't had any attention, you can ping us `@python-easygraph/triage` on the issue. Please, give us reasonable time to get to your issue first, spamming us with messages
|
||||
> does not help anyone. -->
|
||||
|
||||
If you are helping with the triage of reported issues, this section provides some useful information to assist you in your contribution.
|
||||
|
||||
#### Triage steps
|
||||
|
||||
<!-- 1. If `pyproject.toml` is missing or `-vvv` debug logs (with stack trace) is not provided and required, request that the issue author provides it. -->
|
||||
1. Attempt to reproduce the issue with the reported easygraph version or request further clarification from the issue author.
|
||||
1. Ensure the issue is not already resolved. You can attempt to reproduce using the latest preview release and/or easygraph from the main branch.
|
||||
1. If the issue cannot be reproduced,
|
||||
1. clarify with the issue's author,
|
||||
<!-- 1. close the issue or notify `@python-easygraph/triage`. -->
|
||||
1. If the issue can be reproduced,
|
||||
1. comment on the issue confirming so
|
||||
<!-- 1. notify `@python-easygraph/triage`. -->
|
||||
1. if possible, identify the root cause of the issue.
|
||||
1. if interested, attempt to fix it via a pull request.
|
||||
|
||||
<!-- #### Multiple versions
|
||||
|
||||
Often times you would want to attempt to reproduce issues with multiple versions of `easygraph` at the same time. For these use cases, the [pipx project](https://pypa.github.io/pipx/) is useful.
|
||||
|
||||
You can set your environment up like so.
|
||||
|
||||
```sh
|
||||
pipx install --suffix @1.0.10 'easygraph==1.0.10'
|
||||
pipx install --suffix @1.1.0rc1 'easygraph==1.1.0rc1'
|
||||
pipx install --suffix @master 'easygraph @ git+https://github.com/easy-graph/Easy-Graph'
|
||||
```
|
||||
|
||||
> **Hint:** Do not forget to update your `easygraph@master` installation in sync with upstream.
|
||||
|
||||
For `@local` it is recommended that you do something similar to the following as editable installs are not supported for PEP 517 projects.
|
||||
|
||||
```sh
|
||||
# note this will not work for Windows, and we assume you have already run `easygraph install`
|
||||
cd /path/to/python-easygraph/easygraph
|
||||
ln -sf $(easygraph run which easygraph) ~/.local/bin/easygraph@local
|
||||
```
|
||||
|
||||
> **Hint:** This mechanism can also be used to test pull requests. -->
|
||||
|
||||
### Git Workflow
|
||||
|
||||
All development work is performed against easygraph's main branch (`master`). All changes are expected to be submitted and accepted to this
|
||||
branch.
|
||||
|
||||
#### Release branch
|
||||
|
||||
When a release is ready, the following are required before a release is tagged.
|
||||
|
||||
1. A release branch with the prefix `release-`, eg: `release-1.1.0rc1`.
|
||||
1. A pull request from the release branch to the main branch (`master`) if it's a minor or major release. Otherwise, to the bug fix branch (eg: `1.0`).
|
||||
1. The pull request description MUST include the change log corresponding to the release (eg: [#2971](https://github.com/easy-graph/Easy-Graph/pull/2971)).
|
||||
1. The pull request must contain a commit that updates [CHANGELOG.md](CHANGELOG.md) and bumps the project version (eg: [#2971](https://github.com/easy-graph/Easy-Graph/pull/2971/commits/824e7b79defca435cf1d765bb633030b71b9a780)).
|
||||
1. The pull request must have the `Release` label specified.
|
||||
|
||||
<!-- Once the branch pull-request is ready and approved, a member of `@python-easygraph/core` will, -->
|
||||
|
||||
1. Tag the branch with the version identifier (eg: `1.1.0rc1`).
|
||||
2. Merge the pull request once the release is created and assets are uploaded by the CI.
|
||||
|
||||
> **Note:** In this case, we prefer a merge commit instead of squash or rebase merge.
|
||||
|
||||
#### Bug fix branch
|
||||
|
||||
Once a minor version (eg: `1.1.0`) is released, a new branch for the minor version (eg: `1.1`) is created for the bug fix releases. Changes identified
|
||||
or acknowledged by the easygraph team as requiring a bug fix can be submitted as a pull requests against this branch.
|
||||
|
||||
At the time of writing only issues meeting the following criteria may be accepted into a bug fix branch. Trivial fixes may be accepted on a
|
||||
case-by-case basis.
|
||||
|
||||
1. The issue breaks a core functionality and/or is a critical regression.
|
||||
1. The change set does not introduce a new feature or changes an existing functionality.
|
||||
1. A new minor release is not expected within a reasonable time frame.
|
||||
1. If the issue affects the next minor/major release, a corresponding fix has been accepted into the main branch.
|
||||
|
||||
> **Note:** This is subject to the interpretation of a maintainer within the context of the issue.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 182 KiB |
@@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020, Mobile Systems and Networking Group, Fudan University
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,3 @@
|
||||
include README.md
|
||||
include CMakeLists.txt
|
||||
recursive-include cpp_easygraph *
|
||||
@@ -0,0 +1,13 @@
|
||||
pre-commit:
|
||||
pre-commit run --all-files
|
||||
|
||||
test:
|
||||
tox
|
||||
|
||||
publish:
|
||||
gh workflow run release-cibuildwheel.yaml
|
||||
|
||||
bump:
|
||||
bump2version patch
|
||||
|
||||
.PHONY: *
|
||||
@@ -0,0 +1,187 @@
|
||||
EasyGraph
|
||||
==================
|
||||
|
||||
Copyright (C) <2020-2026> by [DataNET Group, Fudan University](https://fudan-datanet.mysxl.cn/)
|
||||
|
||||
___________________________________________________________________________
|
||||
|
||||
[![PyPI Version][pypi-image]][pypi-url]
|
||||
[![Python][python-image]][python-url]
|
||||
[![License][license-image]][license-url]
|
||||
[![Downloads][downloads-image]][downloads-url]
|
||||
|
||||
[pypi-image]: https://img.shields.io/pypi/v/Python-EasyGraph.svg?label=PyPI
|
||||
[pypi-url]: https://pypi.org/project/Python-EasyGraph/
|
||||
[python-image]: https://img.shields.io/pypi/pyversions/Python-EasyGraph.svg?label=Python
|
||||
[python-url]: https://pypi.org/project/Python-EasyGraph/
|
||||
[license-image]: https://img.shields.io/pypi/l/Python-EasyGraph?label=License
|
||||
[license-url]: https://github.com/easy-graph/Easy-Graph/blob/master/LICENSE
|
||||
[downloads-image]: https://img.shields.io/pepy/dt/python-easygraph?label=Downloads&labelColor=brightgreen&color=yellowgreen
|
||||
[downloads-url]: https://pypi.org/project/Python-EasyGraph/
|
||||
|
||||
- **Documentation:** https://easy-graph.github.io/
|
||||
- **Source Code:** https://github.com/easy-graph/Easy-Graph
|
||||
- **Issue Tracker:** https://github.com/easy-graph/Easy-Graph/issues
|
||||
- **PyPI Homepage:** https://pypi.org/project/Python-EasyGraph/
|
||||
- **Youtube channel:** https://www.youtube.com/@python-easygraph
|
||||
|
||||
# Introduction
|
||||
The framework of EasyGraph is composed of four components: **EasyGraph (Core)**, **EasyHypergraph**, **EGGPU**, and **EasyGNN**.
|
||||

|
||||
|
||||
**EasyGraph** is an open-source network analysis library primarily written in Python. It supports both undirected and directed networks and accommodates various network data formats. EasyGraph includes a comprehensive suite of network analysis algorithms such as community detection, structural hole spanner detection, network embedding, and motif detection. Additionally, it optimizes performance by implementing key components in C++ and utilizing multiprocessing.
|
||||
|
||||
<!-- # New Features in Version 1.3
|
||||
- **Support for more hypergraph metrics and algorithms.** Such as [hypercoreness](https://www.nature.com/articles/s41467-023-41887-2), [vector-centrality](https://www.sciencedirect.com/science/article/pii/S0960077922006075), [s-centrality](https://epjds.epj.org/articles/epjdata/abs/2020/01/13688_2020_Article_231/13688_2020_Article_231.html), and so on.
|
||||
- **Support for more hypergraph datasets.** Static hypergraph datasets and dynamic datasets can be both loaded by calling the corresponding dataset name.
|
||||
- **Support for more flexible dynamic hypergraph visualization.** Users can define dynamic hypergraphs and visualize the structure of the hypergraph at each timestamp.
|
||||
- **Support for more efficient hypergraph computation and hypergraph learning.** Adoption of suitable storage structure and caching strategy for different metrics/hypergraph neural networks.
|
||||
-->
|
||||
👉 For more details, please refer to our [documentation](https://easy-graph.github.io/) page.
|
||||
|
||||
|
||||
---
|
||||
**EasyHypergraph** is a comprehensive, computation-effective, and storage-saving hypergraph computation tool designed not only for in-depth hypergraph analysis but also for the growing field of hypergraph learning.
|
||||
It bridges the gap between EasyGraph and higher-order relationships. EasyHypergraph is developed as an integrated module within the EasyGraph framework, maintaining full compatibility with its core architecture.
|
||||
|
||||
👉 For more details, please refer to its [documentation](https://easy-graph.github.io/docs/hypergraph.html) page.
|
||||
|
||||
---
|
||||
**EGGPU** is a high-performance GPU-accelerated network analysis library that supports essential functions such as betweenness centrality, k-core centrality, and single-source shortest path,as well as structural hole metrics like constraint. Built on top of the EasyGraph library, EGGPU features an efficient system architecture and native CUDA implementation, while providing a user-friendly Python API and significant speedups for large-scale network analysis.
|
||||
|
||||
👉 For more details, please refer to its [documentation](https://easy-graph.github.io/docs/eggpu.html) page.
|
||||
|
||||
# 📢 EasyGraph News
|
||||
|
||||
## 📣 Media & Press
|
||||
- [08-09-2025] [EasyHypergraph: Fast, Efficient Higher-Order Network Analysis](https://scienmag.com/easyhypergraph-fast-efficient-higher-order-network-analysis/)
|
||||
- [01-15-2025] [开放原子大赛OpenRank开源数字生态分析与应用创新大赛全国一等奖 (in Chinese)](https://mp.weixin.qq.com/s/e54JHaP2AAEUN3S8RZ-Y2g)
|
||||
- [01-07-2025] [计算机科学技术学院教授陈阳入选“2024中国开源先锋33人” (in Chinese)](https://news.fudan.edu.cn/2025/0107/c2463a143932/page.htm)
|
||||
- [12-04-2024] [国际开源基准委员会的"顶级开源证书" (in Chinese)](https://chenyang03.wordpress.com/wp-content/uploads/2025/07/image.png?w=1024)
|
||||
- [10-16-2024] [2023年度上海开源创新卓越成果奖 (in Chinese)](https://mp.weixin.qq.com/s/kO6Dpyolf74dlDvKuoLlJA)
|
||||
- [11-06-2023] [复旦大学陈阳Patterns:EasyGraph——面向多学科的高性能网络结构分析工具箱|Cell Press论文速递 (in Chinese)](https://mp.weixin.qq.com/s/f2LCyQv1dYuquM_EfGX6Ow?poc_token=HBV092ijI2L534IrD0Jl_fnf3VjhX8UudcPFLH6b)
|
||||
- [11-04-2023] [EasyGraph:多功能、跨平台、高效率的跨学科网络分析库 (in Chinese)](https://swarma.org/?p=46252)
|
||||
|
||||
## 🚀 Releases & Milestones
|
||||
- [06-07-2026] EasyGraph **v1.6.2** released (Community functions upgraded)
|
||||
- [05-07-2026] EasyGraph **v1.6.1** released (Add OpenMP-powered path-based functions)
|
||||
- [02-01-2026] EasyGraph **v1.6** released (OpenMP-powered functions for large network analysis)
|
||||
- **[01-16-2026] 🎉 1M Downloads! Thanks to our amazing community!**
|
||||
- [01-01-2026] EasyGraph **v1.5.3** released ([The Hypergraph Interchange Format (HIF) standard](https://github.com/HIF-org/HIF-standard))
|
||||
- [11-23-2025] EasyGraph **v1.5.2** released (LS algorithm for effective community detection)
|
||||
- [10-11-2025] EasyGraph **v1.5.1** released (Python 3.14 supported)
|
||||
- [07-27-2025] EasyGraph **v1.5** released (This version integrates the HWNN model and supports 11 representative network datasets)
|
||||
- **[06-29-2025] 🎉 800K+ Downloads!**
|
||||
- [11-22-2024] EasyGraph **v1.4.1** released (Python 3.13 supported)
|
||||
- [09-20-2024] EasyGraph **v1.4** released (GPU-powered functions for large network analysis)
|
||||
- [05-27-2024] EasyGraph **v1.3** released (issues related to hypergraph analysis and visualization resolved)
|
||||
- [04-09-2024] EasyGraph **v1.2** released (Python 3.12 supported)
|
||||
- [02-05-2024] EasyGraph **v1.1** released (hypergraph analysis and learning for higher-order networks)
|
||||
- [08-17-2023] EasyGraph **v1.0** released
|
||||
- [07-22-2020] EasyGraph **first public release**
|
||||
|
||||
## 📈 Publications
|
||||
- [05-30-2025] 🎉 Our paper "EasyHypergraph: an open-source software for fast and memory-saving analysis and learning of higher-order networks" was accepted by Humanities and Social Sciences Communications (Nature Portfolio)! [[PDF](https://www.nature.com/articles/s41599-025-05180-5)]
|
||||
- [08-08-2023] 🎉 Our paper "EasyGraph: A Multifunctional, Cross-Platform, and Effective Library for Interdisciplinary Network Analysis" was accepted by Patterns (Cell Press)! [[PDF](https://www.sciencedirect.com/science/article/pii/S2666389923002180)]
|
||||
|
||||
# Stargazers
|
||||
|
||||
[![Stars][star-image]][star-url]
|
||||
|
||||
[star-image]:https://reporoster.com/stars/easy-graph/Easy-Graph
|
||||
[star-url]: https://github.com/easy-graph/Easy-Graph/stargazers
|
||||
|
||||
# Install
|
||||
|
||||
## Supported Versions
|
||||
|
||||
``3.8 <= Python <= 3.14`` is required.
|
||||
|
||||
## Installation With pip
|
||||
```
|
||||
$ pip install --upgrade Python-EasyGraph
|
||||
```
|
||||
The conda package is no longer updated or maintained.
|
||||
|
||||
If you've previously installed EasyGraph with conda, please uninstall it with ``conda`` and reinstall with ``pip``.
|
||||
|
||||
## Build From Source
|
||||
If prebuilt EasyGraph wheels are not supported for your platform (OS / CPU arch, check [here](https://pypi.org/simple/python-easygraph/)), or you want to have GPU-based functions enabled, you can build it locally.
|
||||
|
||||
### Prerequisites
|
||||
- CMake >= 3.23
|
||||
- A compiler that fully supports C++11
|
||||
- CUDA Toolkit 11.8 or later would be preferred (If need GPUs enabled)
|
||||
|
||||
### Installation
|
||||
#### On Linux
|
||||
```
|
||||
git clone --recursive https://github.com/easy-graph/Easy-Graph
|
||||
export EASYGRAPH_ENABLE_GPU="TRUE" # for users who want to enable GPUs
|
||||
pip install ./Easy-Graph
|
||||
```
|
||||
|
||||
#### On Windows
|
||||
```
|
||||
% For Windows users who want to enable GPU-based functions, %
|
||||
% you must execute the commands below in cmd but not PowerShell. %
|
||||
git clone --recursive https://github.com/easy-graph/Easy-Graph
|
||||
set EASYGRAPH_ENABLE_GPU=TRUE % for users who want to enable GPUs %
|
||||
pip install ./Easy-Graph
|
||||
```
|
||||
|
||||
#### On macOS
|
||||
```
|
||||
# Since macOS doesn't support CUDA, we can't have GPUs enabled on macOS
|
||||
git clone --recursive https://github.com/easy-graph/Easy-Graph
|
||||
pip install ./Easy-Graph
|
||||
```
|
||||
|
||||
## Hint
|
||||
|
||||
EasyGraph uses 1.12.1 <= [PyTorch](https://pytorch.org/get-started/locally/) < 2.0 for machine learning functions.
|
||||
Note that this does not prevent your from running non-machine learning functions normally, if there is no PyTorch in your environment.
|
||||
But you will receive some warnings which remind you some unavailable modules when they depend on it.
|
||||
|
||||
# Simple Example
|
||||
|
||||
This example demonstrates the general usage of methods in EasyGraph.
|
||||
```python
|
||||
>>> import easygraph as eg
|
||||
>>> G = eg.Graph()
|
||||
>>> G.add_edges([(1,2), (2,3), (1,3), (3,4), (4,5), (3,5), (5,6)])
|
||||
>>> eg.pagerank(G)
|
||||
{1: 0.14272233049003707, 2: 0.14272233049003694, 3: 0.2685427766200994, 4: 0.14336430577918527, 5: 0.21634929087322705, 6: 0.0862989657474143}
|
||||
```
|
||||
This is a simple example for the detection of [structural hole spanners](https://en.wikipedia.org/wiki/Structural_holes)
|
||||
using the [HIS](https://keg.cs.tsinghua.edu.cn/jietang/publications/WWW13-Lou&Tang-Structural-Hole-Information-Diffusion.pdf) algorithm.
|
||||
|
||||
```python
|
||||
>>> import easygraph as eg
|
||||
>>> G = eg.Graph()
|
||||
>>> G.add_edges([(1,2), (2,3), (1,3), (3,4), (4,5), (3,5), (5,6)])
|
||||
>>> _, _, H = eg.get_structural_holes_HIS(G, C=[frozenset([1,2,3]), frozenset([4,5,6])])
|
||||
>>> H # The structural hole score of each node. Note that node `4` is regarded as the most possible structural hole spanner.
|
||||
{1: {0: 0.703948974609375},
|
||||
2: {0: 0.703948974609375},
|
||||
3: {0: 1.2799804687499998},
|
||||
4: {0: 1.519976806640625},
|
||||
5: {0: 1.519976806640625},
|
||||
6: {0: 0.83595703125}
|
||||
}
|
||||
```
|
||||
# Citation
|
||||
|
||||
If you use EasyGraph in a scientific publication, we kindly request that you cite the following paper:
|
||||
```
|
||||
@article{gao2023easygraph,
|
||||
title={{EasyGraph: A Multifunctional, Cross-Platform, and Effective Library for Interdisciplinary Network Analysis}},
|
||||
author={Min Gao and Zheng Li and Ruichen Li and Chenhao Cui and Xinyuan Chen and Bodian Ye and Yupeng Li and Weiwei Gu and Qingyuan Gong and Xin Wang and Yang Chen},
|
||||
year={2023},
|
||||
journal={Patterns},
|
||||
volume={4},
|
||||
number={10},
|
||||
pages={100839},
|
||||
}
|
||||
```
|
||||
📢 If you notice anything unexpected, please open an issue and let us know. If you have any questions or require a specific feature, feel free to discuss them with us. We are motivated to constantly make EasyGraph even better and let more developers benefit!
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
EasyGraph
|
||||
==================
|
||||
|
||||
Copyright (C) <2020-2024> by DataNET Group, Fudan University
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/Python-EasyGraph.svg?label=PyPI
|
||||
:target: https://pypi.org/project/Python-EasyGraph/
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/Python-EasyGraph.svg?label=Python
|
||||
:target: https://pypi.org/project/Python-EasyGraph/
|
||||
|
||||
.. image:: https://img.shields.io/pypi/l/Python-EasyGraph?label=License
|
||||
:target: https://github.com/easy-graph/Easy-Graph/blob/master/LICENSE
|
||||
|
||||
.. image:: https://static.pepy.tech/personalized-badge/python-easygraph?period=total&units=international_system&left_color=brightgreen&right_color=yellowgreen&left_text=Downloads
|
||||
:target: https://pypi.org/project/Python-EasyGraph/
|
||||
|
||||
- **Documentation:** https://easy-graph.github.io/
|
||||
- **Source Code:** https://github.com/easy-graph/Easy-Graph
|
||||
- **Issue Tracker:** https://github.com/easy-graph/Easy-Graph/issues
|
||||
- **PyPI Homepage:** https://pypi.org/project/Python-EasyGraph/
|
||||
- **Youtube channel:** https://www.youtube.com/@python-easygraph
|
||||
|
||||
Introduction
|
||||
------------
|
||||
**EasyGraph** is an open-source network analysis library. It is mainly written in Python and supports analysis for undirected networks and directed networks. EasyGraph supports various formats of network data and covers a series of important network analysis algorithms for community detection, structural hole spanner detection, network embedding, and motif detection. Moreover, EasyGraph implements some key elements using C++ and introduces multiprocessing optimization to achieve better efficiency.
|
||||
|
||||
New Features in Version 1.1
|
||||
------------
|
||||
- **Support for more hypergraph metrics and algorithms.** Such as `hypercoreness <https://www.nature.com/articles/s41467-023-41887-2>`_, `vector-centrality <https://www.sciencedirect.com/science/article/pii/S0960077922006075>`_, `s-centrality <https://epjds.epj.org/articles/epjdata/abs/2020/01/13688_2020_Article_231/13688_2020_Article_231.html>`_, and so on.
|
||||
- **Support for more hypergraph datasets.** `Static hypergraph datasets and dynamic datasets <https://easy-graph.github.io/docs/reference/easygraph.datasets.html>`_ can be both loaded by calling corresponding dataset name.
|
||||
- **Support for more flexible dynamic hypergraph visualization.** Users can define dynamic hypergraphs and visualize the structure of the hypergraph at each timestamp.
|
||||
- **Support for more efficient hypergraph computation and hypergraph learning** Adoption of suitable storage structure and caching strategy for different metrics/hypergraph neural networks.
|
||||
|
||||
If you need more details, please see our `documentation <https://easy-graph.github.io/>`_ of latest version.
|
||||
|
||||
News
|
||||
----
|
||||
- [02-05-2024] We release EasyGraph 1.1! This version features hypergraph analysis and learning for higher-order network modeling and representation.
|
||||
- [08-17-2023] We release EasyGraph 1.0!
|
||||
- [08-08-2023] Our paper "EasyGraph: A Multifunctional, Cross-Platform, and Effective Library for Interdisciplinary Network Analysis" has been accepted by Patterns!
|
||||
|
||||
Stargazers
|
||||
----------
|
||||
.. image:: https://reporoster.com/stars/easy-graph/Easy-Graph
|
||||
:target: https://github.com/easy-graph/Easy-Graph/stargazers
|
||||
:alt: Stargazers repo roster for @easy-graph/Easy-Graph
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
.. The current version on PyPI is outdated, we'll push the latest version as soon as we figure out how to integrate the C++ binding framework we use with our CI pipeline.
|
||||
|
||||
.. In the meantime, here's a work around you can try to install the latest version of easygraph on your machine:
|
||||
|
||||
- **Prerequisites**
|
||||
|
||||
``3.8 <= Python <= 3.11`` is required.
|
||||
|
||||
.. Installation with ``pip`` (outdated)
|
||||
|
||||
- **Installation with** ``pip``
|
||||
|
||||
.. code::
|
||||
|
||||
$ pip install --upgrade Python-EasyGraph
|
||||
|
||||
The conda package is no longer updated or maintained.
|
||||
|
||||
If you've installed EasyGraph this way before, please uninstall it with ``conda`` and install it with ``pip``.
|
||||
|
||||
If prebuilt EasyGraph wheels are not supported for your platform (OS / CPU arch, check `here <https://pypi.org/simple/python-easygraph/>`_), you can build it locally this way:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
git clone https://github.com/easy-graph/Easy-Graph && cd Easy-Graph && git checkout pybind11
|
||||
pip install pybind11
|
||||
python3 setup.py build_ext
|
||||
python3 setup.py install
|
||||
|
||||
- **Hint**
|
||||
|
||||
EasyGraph uses 1.12.1 <= `PyTorch <https://pytorch.org/get-started/locally/>`_ < 2.0 for machine
|
||||
learning functions.
|
||||
Note that this does not prevent your from running non-machine learning functions normally,
|
||||
if there is no PyTorch in your environment.
|
||||
But you will receive some warnings which remind you some unavailable modules when they depend on it.
|
||||
|
||||
Simple Example
|
||||
--------------
|
||||
|
||||
|
||||
This example shows the general usage of methods in EasyGraph.
|
||||
|
||||
.. code:: python
|
||||
|
||||
>>> import easygraph as eg
|
||||
>>> G = eg.Graph()
|
||||
>>> G.add_edges([(1,2), (2,3), (1,3), (3,4), (4,5), (3,5), (5,6)])
|
||||
>>> eg.pagerank(G)
|
||||
{1: 0.14272233049003707, 2: 0.14272233049003694, 3: 0.2685427766200994, 4: 0.14336430577918527, 5: 0.21634929087322705, 6: 0.0862989657474143}
|
||||
|
||||
This is a simple example for the detection of `structural hole spanners <https://en.wikipedia.org/wiki/Structural_holes>`_
|
||||
using the `HIS <https://keg.cs.tsinghua.edu.cn/jietang/publications/WWW13-Lou&Tang-Structural-Hole-Information-Diffusion.pdf>`_ algorithm.
|
||||
|
||||
.. code:: python
|
||||
|
||||
>>> import easygraph as eg
|
||||
>>> G = eg.Graph()
|
||||
>>> G.add_edges([(1,2), (2,3), (1,3), (3,4), (4,5), (3,5), (5,6)])
|
||||
>>> _, _, H = eg.get_structural_holes_HIS(G, C=[frozenset([1,2,3]), frozenset([4,5,6])])
|
||||
>>> H # The structural hole score of each node. Note that node `4` is regarded as the most possible structural hole spanner.
|
||||
{1: {0: 0.703948974609375},
|
||||
2: {0: 0.703948974609375},
|
||||
3: {0: 1.2799804687499998},
|
||||
4: {0: 1.519976806640625},
|
||||
5: {0: 1.519976806640625},
|
||||
6: {0: 0.83595703125}
|
||||
}
|
||||
|
||||
Citation
|
||||
--------
|
||||
|
||||
If you use EasyGraph in a scientific publication, we would appreciate citations to the following paper:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
@article{gao2023easygraph,
|
||||
title={{EasyGraph: A Multifunctional, Cross-Platform, and Effective Library for Interdisciplinary Network Analysis}},
|
||||
author={Min Gao and Zheng Li and Ruichen Li and Chenhao Cui and Xinyuan Chen and Bodian Ye and Yupeng Li and Weiwei Gu and Qingyuan Gong and Xin Wang and Yang Chen},
|
||||
year={2023},
|
||||
journal={Patterns},
|
||||
volume={4},
|
||||
number={10}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`easy-graph/Easy-Graph`
|
||||
- 原始仓库:https://github.com/easy-graph/Easy-Graph
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
[default]
|
||||
# extend-ignore-identifiers-re = [
|
||||
# # *sigh* this just isn't worth the cost of fixing
|
||||
# "AttributeID.*Supress.*",
|
||||
# ]
|
||||
|
||||
[default.extend-identifiers]
|
||||
# *sigh* this just isn't worth the cost of fixing
|
||||
# AttributeIDSupressMenu = "AttributeIDSupressMenu"
|
||||
|
||||
[default.extend-words]
|
||||
# Don't correct the surname "Teh"
|
||||
# teh = "teh"
|
||||
2nd = "2nd"
|
||||
l_2nd = 'l_2nd'
|
||||
loss_2nd = 'loss_2nd'
|
||||
nd = 'nd'
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
"_typos.toml",
|
||||
"easygraph/functions/graph_embedding/embedding-citeseer.ipynb",
|
||||
]
|
||||
@@ -0,0 +1,83 @@
|
||||
cmake_minimum_required(VERSION 3.23)
|
||||
project(cpp_easygraph)
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
|
||||
file(GLOB SOURCES
|
||||
classes/*.cpp
|
||||
common/*.cpp
|
||||
functions/*/*.cpp
|
||||
cpp_easygraph.cpp
|
||||
)
|
||||
|
||||
add_subdirectory(pybind11)
|
||||
|
||||
option(EASYGRAPH_ENABLE_OPENMP "Enable OpenMP acceleration (auto-detect)" ON)
|
||||
option(EASYGRAPH_ENABLE_GPU "EASYGRAPH_ENABLE_GPU" OFF)
|
||||
|
||||
if (EASYGRAPH_ENABLE_GPU)
|
||||
|
||||
pybind11_add_module(cpp_easygraph
|
||||
${SOURCES}
|
||||
$<TARGET_OBJECTS:gpu_easygraph>
|
||||
)
|
||||
|
||||
set_property(TARGET cpp_easygraph PROPERTY CUDA_ARCHITECTURES all)
|
||||
|
||||
target_compile_definitions(cpp_easygraph
|
||||
PRIVATE EASYGRAPH_ENABLE_GPU
|
||||
)
|
||||
|
||||
target_link_libraries(cpp_easygraph
|
||||
PRIVATE cudart_static
|
||||
)
|
||||
|
||||
else()
|
||||
pybind11_add_module(cpp_easygraph
|
||||
${SOURCES}
|
||||
)
|
||||
|
||||
endif()
|
||||
|
||||
if (EASYGRAPH_ENABLE_OPENMP)
|
||||
if (APPLE AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
find_path(EG_LIBOMP_INCLUDE_DIR
|
||||
NAMES omp.h
|
||||
PATHS
|
||||
/opt/homebrew/opt/libomp/include
|
||||
/usr/local/opt/libomp/include
|
||||
)
|
||||
|
||||
find_library(EG_LIBOMP_LIBRARY
|
||||
NAMES omp libomp
|
||||
PATHS
|
||||
/opt/homebrew/opt/libomp/lib
|
||||
/usr/local/opt/libomp/lib
|
||||
)
|
||||
|
||||
if (EG_LIBOMP_INCLUDE_DIR AND EG_LIBOMP_LIBRARY)
|
||||
message(STATUS "libomp found: ${EG_LIBOMP_LIBRARY}")
|
||||
|
||||
set(OpenMP_C_FLAGS "-Xpreprocessor -fopenmp" CACHE STRING "" FORCE)
|
||||
set(OpenMP_CXX_FLAGS "-Xpreprocessor -fopenmp" CACHE STRING "" FORCE)
|
||||
|
||||
set(OpenMP_CXX_INCLUDE_DIR "${EG_LIBOMP_INCLUDE_DIR}" CACHE PATH "" FORCE)
|
||||
set(OpenMP_omp_LIBRARY "${EG_LIBOMP_LIBRARY}" CACHE FILEPATH "" FORCE)
|
||||
else()
|
||||
message(STATUS
|
||||
"libomp not found on macOS. OpenMP will be disabled.\n"
|
||||
"To enable OpenMP, run: brew install libomp"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
find_package(OpenMP QUIET)
|
||||
endif()
|
||||
|
||||
if (OpenMP_CXX_FOUND)
|
||||
message(STATUS "OpenMP found, enabling parallel acceleration.")
|
||||
target_link_libraries(cpp_easygraph PRIVATE OpenMP::OpenMP_CXX)
|
||||
target_compile_definitions(cpp_easygraph PRIVATE EASYGRAPH_USE_OPENMP=1)
|
||||
else()
|
||||
message(STATUS "OpenMP not found, building in single-thread mode.")
|
||||
target_compile_definitions(cpp_easygraph PRIVATE EASYGRAPH_USE_OPENMP=0)
|
||||
endif()
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "graph.h"
|
||||
#include "directed_graph.h"
|
||||
#include "operation.h"
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "../common/common.h"
|
||||
|
||||
struct COOGraph {
|
||||
std::vector<int> row;
|
||||
std::vector<int> col;
|
||||
std::vector<double> unweighted_W;
|
||||
std::unordered_map<std::string, std::shared_ptr<std::vector<double>>> W_map;
|
||||
|
||||
std::vector<node_t> nodes;
|
||||
std::unordered_map<node_t, int> node2idx;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "../common/common.h"
|
||||
|
||||
struct CSRGraph {
|
||||
std::vector<int> V;
|
||||
std::vector<int> E;
|
||||
std::vector<double> unweighted_W;
|
||||
std::unordered_map<std::string, std::shared_ptr<std::vector<double>>> W_map;
|
||||
|
||||
std::vector<node_t> nodes;
|
||||
std::unordered_map<node_t, int> node2idx;
|
||||
};
|
||||
@@ -0,0 +1,613 @@
|
||||
#include "directed_graph.h"
|
||||
|
||||
#include "../common/utils.h"
|
||||
|
||||
DiGraph::DiGraph() : Graph() {
|
||||
|
||||
}
|
||||
|
||||
py::object DiGraph__init__(py::args args, py::kwargs kwargs) {
|
||||
py::object self = args[0];
|
||||
self.attr("__init__")();
|
||||
DiGraph& self_ = self.cast<DiGraph&>();
|
||||
py::dict graph_attr = kwargs;
|
||||
self_.graph.attr("update")(graph_attr);
|
||||
self_.nodes_cache = py::dict();
|
||||
self_.adj_cache = py::dict();
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object DiGraph_out_degree(py::object self, py::object weight) {
|
||||
py::dict degree = py::dict();
|
||||
py::list edges = self.attr("edges").cast<py::list>();
|
||||
py::object u, v;
|
||||
py::dict d;
|
||||
for (int i = 0; i < py::len(edges); i++) {
|
||||
py::tuple edge = edges[i].cast<py::tuple>();
|
||||
u = edge[0];
|
||||
v = edge[1];
|
||||
d = edge[2].cast<py::dict>();
|
||||
if (degree.contains(u)) {
|
||||
degree[u] = py::object(degree[u]) + d.attr("get")(weight, 1);
|
||||
} else {
|
||||
degree[u] = d.attr("get")(weight, 1);
|
||||
}
|
||||
}
|
||||
py::list nodes = py::list(self.attr("nodes"));
|
||||
for (int i = 0; i < py::len(nodes); i++) {
|
||||
py::object node = nodes[i];
|
||||
if (!degree.contains(node)) {
|
||||
degree[node] = 0;
|
||||
}
|
||||
}
|
||||
return degree;
|
||||
}
|
||||
|
||||
py::object DiGraph_in_degree(py::object self, py::object weight) {
|
||||
py::dict degree = py::dict();
|
||||
py::list edges = self.attr("edges").cast<py::list>();
|
||||
py::object u, v;
|
||||
py::dict d;
|
||||
for (int i = 0; i < py::len(edges); i++) {
|
||||
py::tuple edge = edges[i].cast<py::tuple>();
|
||||
u = edge[0];
|
||||
v = edge[1];
|
||||
d = edge[2].cast<py::dict>();
|
||||
if (degree.contains(v)) {
|
||||
degree[v] = py::object(degree[v]) + d.attr("get")(weight, 1);
|
||||
} else {
|
||||
degree[v] = d.attr("get")(weight, 1);
|
||||
}
|
||||
}
|
||||
py::list nodes = py::list(self.attr("nodes"));
|
||||
for (int i = 0; i < py::len(nodes); i++) {
|
||||
py::object node = nodes[i];
|
||||
if (!degree.contains(node)) {
|
||||
degree[node] = 0;
|
||||
}
|
||||
}
|
||||
return degree;
|
||||
}
|
||||
|
||||
py::object DiGraph_degree(py::object self, py::object weight) {
|
||||
py::dict degree = py::dict();
|
||||
py::dict out_degree = self.attr("out_degree")(weight).cast<py::dict>();
|
||||
py::dict in_degree = self.attr("in_degree")(weight).cast<py::dict>();
|
||||
py::list nodes = py::list(self.attr("nodes"));
|
||||
for (int i = 0; i < py::len(nodes); i++) {
|
||||
py::object u = nodes[i];
|
||||
degree[u] = out_degree[u] + in_degree[u];
|
||||
}
|
||||
return degree;
|
||||
}
|
||||
|
||||
py::object DiGraph_size(py::object self, py::object weight) {
|
||||
py::dict out_degree = self.attr("out_degree")(weight).cast<py::dict>();
|
||||
py::object s = py_sum(out_degree.attr("values")());
|
||||
return (weight.is_none()) ? py::int_(s) : s;
|
||||
}
|
||||
|
||||
py::object DiGraph_number_of_edges(py::object self, py::object u, py::object v) {
|
||||
if (u.is_none()) {
|
||||
return self.attr("size")();
|
||||
}
|
||||
Graph& G = self.cast<Graph&>();
|
||||
node_t u_id = G.node_to_id[u].cast<node_t>();
|
||||
node_t v_id = G.node_to_id.attr("get")(v, -1).cast<node_t>();
|
||||
return py::cast(int(v_id != -1 && G.adj[u_id].count(v_id)));
|
||||
}
|
||||
|
||||
py::object DiGraph_neighbors(py::object self, py::object node) {
|
||||
Graph& self_ = self.cast<Graph&>();
|
||||
if (self_.node_to_id.contains(node)) {
|
||||
return self.attr("adj")[node].attr("__iter__")();
|
||||
} else {
|
||||
PyErr_Format(PyExc_KeyError, "No node %R", node.ptr());
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
|
||||
py::object DiGraph_predecessors(py::object self, py::object node) {
|
||||
DiGraph& self_ = self.cast<DiGraph&>();
|
||||
adj_dict_factory& pred = self_.pred;
|
||||
node_t node_id = self_.node_to_id[node].cast<node_t>();
|
||||
if (pred.find(node_id) != pred.end()) {
|
||||
adj_attr_dict_factory node_pred_dict = pred[node_id];
|
||||
py::dict node_pred = py::dict();
|
||||
for (adj_attr_dict_factory::iterator i = node_pred_dict.begin();
|
||||
i != node_pred_dict.end(); i++) {
|
||||
edge_attr_dict_factory edge_attr_dict = i->second;
|
||||
node_pred[self_.id_to_node[py::cast(i->first)]] = attr_to_dict(edge_attr_dict);
|
||||
}
|
||||
return node_pred.attr("__iter__")();
|
||||
} else {
|
||||
PyErr_Format(PyExc_KeyError, "No node %R", node.ptr());
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
|
||||
node_t DiGraph_add_one_node(DiGraph& self, py::object one_node_for_adding,
|
||||
py::object node_attr = py::dict()) {
|
||||
node_t id;
|
||||
if (self.node_to_id.contains(one_node_for_adding)) {
|
||||
id = self.node_to_id[one_node_for_adding].cast<node_t>();
|
||||
} else {
|
||||
id = ++(self.id);
|
||||
self.id_to_node[py::cast(id)] = one_node_for_adding;
|
||||
self.node_to_id[one_node_for_adding] = id;
|
||||
}
|
||||
py::list items = py::list(node_attr.attr("items")());
|
||||
self.node[id] = node_attr_dict_factory();
|
||||
self.adj[id] = adj_attr_dict_factory();
|
||||
self.pred[id] = adj_attr_dict_factory();
|
||||
|
||||
for (int i = 0; i < len(items); i++) {
|
||||
py::tuple kv = items[i].cast<py::tuple>();
|
||||
py::object pkey = kv[0];
|
||||
std::string weight_key = weight_to_string(pkey);
|
||||
weight_t value = kv[1].cast<weight_t>();
|
||||
self.node[id].insert(std::make_pair(weight_key, value));
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
py::object DiGraph_add_node(py::args args, py::kwargs kwargs) {
|
||||
DiGraph& self = args[0].cast<DiGraph&>();
|
||||
self.dirty_nodes = true;
|
||||
self.dirty_adj = true;
|
||||
py::object one_node_for_adding = args[1];
|
||||
py::dict node_attr = kwargs;
|
||||
DiGraph_add_one_node(self, one_node_for_adding, node_attr);
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object DiGraph_add_nodes(DiGraph& self, py::list nodes_for_adding, py::list nodes_attr) {
|
||||
self.dirty_nodes = true;
|
||||
self.dirty_adj = true;
|
||||
if (py::len(nodes_attr) != 0) {
|
||||
if (py::len(nodes_for_adding) != py::len(nodes_attr)) {
|
||||
PyErr_Format(PyExc_AssertionError, "Nodes and Attributes lists must have same length.");
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < py::len(nodes_for_adding); i++) {
|
||||
py::object one_node_for_adding = nodes_for_adding[i];
|
||||
py::dict node_attr;
|
||||
if (py::len(nodes_attr)) {
|
||||
node_attr = nodes_attr[i].cast<py::dict>();
|
||||
} else {
|
||||
node_attr = py::dict();
|
||||
}
|
||||
DiGraph_add_one_node(self, one_node_for_adding, node_attr);
|
||||
}
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object DiGraph_add_nodes_from(py::args args, py::kwargs kwargs) {
|
||||
DiGraph& self = args[0].cast<DiGraph&>();
|
||||
self.dirty_nodes = true;
|
||||
self.dirty_adj = true;
|
||||
py::list nodes_for_adding = py::list(args[1]);
|
||||
for (int i = 0; i < py::len(nodes_for_adding); i++) {
|
||||
bool newnode;
|
||||
py::dict attr = kwargs;
|
||||
py::dict newdict, ndict;
|
||||
py::object n = nodes_for_adding[i];
|
||||
try {
|
||||
newnode = !self.node_to_id.contains(n);
|
||||
newdict = attr;
|
||||
} catch (const py::error_already_set&) {
|
||||
PyObject *type, *value, *traceback;
|
||||
PyErr_Fetch(&type, &value, &traceback);
|
||||
if (PyErr_GivenExceptionMatches(PyExc_TypeError, type)) {
|
||||
py::tuple n_pair = n.cast<py::tuple>();
|
||||
n = n_pair[0];
|
||||
ndict = n_pair[1].cast<py::dict>();
|
||||
newnode = !self.node_to_id.contains(n);
|
||||
newdict = attr.attr("copy")();
|
||||
newdict.attr("update")(ndict);
|
||||
} else {
|
||||
PyErr_Restore(type, value, traceback);
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
if (newnode) {
|
||||
if (n.is_none()) {
|
||||
PyErr_Format(PyExc_ValueError, "None cannot be a node");
|
||||
return py::none();
|
||||
}
|
||||
DiGraph_add_one_node(self, n);
|
||||
}
|
||||
node_t id = self.node_to_id[n].cast<node_t>();
|
||||
py::list items = py::list(newdict.attr("items")());
|
||||
for (int i = 0; i < len(items); i++) {
|
||||
py::tuple kv = items[i].cast<py::tuple>();
|
||||
py::object pkey = kv[0];
|
||||
std::string weight_key = weight_to_string(pkey);
|
||||
weight_t value = kv[1].cast<weight_t>();
|
||||
self.node[id].insert(std::make_pair(weight_key, value));
|
||||
}
|
||||
}
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object DiGraph_remove_node(DiGraph& self, py::object node_to_remove) {
|
||||
self.dirty_nodes = true;
|
||||
self.dirty_adj = true;
|
||||
if (!self.node_to_id.contains(node_to_remove)) {
|
||||
PyErr_Format(PyExc_KeyError, "No node %R in graph.", node_to_remove.ptr());
|
||||
return py::none();
|
||||
}
|
||||
node_t node_to_remove_id = self.node_to_id[node_to_remove].cast<node_t>();
|
||||
adj_attr_dict_factory succs = self.adj[node_to_remove_id];
|
||||
adj_attr_dict_factory preds = self.pred[node_to_remove_id];
|
||||
self.node.erase(node_to_remove_id);
|
||||
for (adj_attr_dict_factory::iterator i = succs.begin(); i != succs.end(); i++) {
|
||||
self.pred[i->first].erase(node_to_remove_id);
|
||||
}
|
||||
for (adj_attr_dict_factory::iterator i = preds.begin(); i != preds.end(); i++) {
|
||||
self.adj[i->first].erase(node_to_remove_id);
|
||||
}
|
||||
self.adj.erase(node_to_remove_id);
|
||||
self.pred.erase(node_to_remove_id);
|
||||
self.node_to_id.attr("pop")(node_to_remove);
|
||||
self.id_to_node.attr("pop")(node_to_remove_id);
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object DiGraph_remove_nodes(py::object self, py::list nodes_to_remove) {
|
||||
DiGraph& self_ = self.cast<DiGraph&>();
|
||||
self_.dirty_nodes = true;
|
||||
self_.dirty_adj = true;
|
||||
for (int i = 0; i < py::len(nodes_to_remove); i++) {
|
||||
py::object node_to_remove = nodes_to_remove[i];
|
||||
if (!self_.node_to_id.contains(node_to_remove)) {
|
||||
PyErr_Format(PyExc_KeyError, "No node %R in graph.", node_to_remove.ptr());
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < py::len(nodes_to_remove); i++) {
|
||||
py::object node_to_remove = nodes_to_remove[i];
|
||||
self.attr("remove_node")(node_to_remove);
|
||||
}
|
||||
return py::none();
|
||||
}
|
||||
|
||||
void DiGraph_add_one_edge(DiGraph& self, py::object u_of_edge,
|
||||
py::object v_of_edge, py::object edge_attr) {
|
||||
node_t u, v;
|
||||
if (!self.node_to_id.contains(u_of_edge)) {
|
||||
u = DiGraph_add_one_node(self, u_of_edge);
|
||||
} else {
|
||||
u = self.node_to_id[u_of_edge].cast<node_t>();
|
||||
}
|
||||
if (!self.node_to_id.contains(v_of_edge)) {
|
||||
v = DiGraph_add_one_node(self, v_of_edge);
|
||||
} else {
|
||||
v = self.node_to_id[v_of_edge].cast<node_t>();
|
||||
}
|
||||
py::list items = py::list(edge_attr.attr("items")());
|
||||
self.adj[u][v] = node_attr_dict_factory();
|
||||
self.pred[v][u] = edge_attr_dict_factory();
|
||||
for (int i = 0; i < len(items); i++) {
|
||||
py::tuple kv = items[i].cast<py::tuple>();
|
||||
py::object pkey = kv[0];
|
||||
std::string weight_key = weight_to_string(pkey);
|
||||
weight_t value = kv[1].cast<weight_t>();
|
||||
self.adj[u][v].insert(std::make_pair(weight_key, value));
|
||||
self.pred[v][u].insert(std::make_pair(weight_key, value));
|
||||
}
|
||||
}
|
||||
|
||||
py::object DiGraph_add_edge(py::args args, py::kwargs kwargs) {
|
||||
DiGraph& self = args[0].cast<DiGraph&>();
|
||||
self.dirty_nodes = true;
|
||||
self.dirty_adj = true;
|
||||
py::object u_of_edge = args[1], v_of_edge = args[2];
|
||||
py::dict edge_attr = kwargs;
|
||||
DiGraph_add_one_edge(self, u_of_edge, v_of_edge, edge_attr);
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object DiGraph_add_edges(DiGraph& self, py::list edges_for_adding, py::list edges_attr) {
|
||||
self.dirty_nodes = true;
|
||||
self.dirty_adj = true;
|
||||
if (py::len(edges_attr) != 0) {
|
||||
if (py::len(edges_for_adding) != py::len(edges_attr)) {
|
||||
PyErr_Format(PyExc_AssertionError, "Edges and Attributes lists must have same length.");
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < py::len(edges_for_adding); i++) {
|
||||
py::tuple one_edge_for_adding = edges_for_adding[i].cast<py::tuple>();
|
||||
py::dict edge_attr;
|
||||
if (py::len(edges_attr)) {
|
||||
edge_attr = edges_attr[i].cast<py::dict>();
|
||||
} else {
|
||||
edge_attr = py::dict();
|
||||
}
|
||||
DiGraph_add_one_edge(self, one_edge_for_adding[0], one_edge_for_adding[1], edge_attr);
|
||||
}
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object DiGraph_add_edges_from(py::args args, py::kwargs attr) {
|
||||
DiGraph& self = args[0].cast<DiGraph&>();
|
||||
self.dirty_nodes = true;
|
||||
self.dirty_adj = true;
|
||||
py::list ebunch_to_add = py::list(args[1]);
|
||||
for (int i = 0; i < len(ebunch_to_add); i++) {
|
||||
py::list e = py::list(ebunch_to_add[i]);
|
||||
py::object u, v;
|
||||
py::dict dd;
|
||||
switch (len(e)) {
|
||||
case 2: {
|
||||
u = e[0];
|
||||
v = e[1];
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
u = e[0];
|
||||
v = e[1];
|
||||
dd = e[2].cast<py::dict>();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
PyErr_Format(PyExc_ValueError, "Edge tuple %R must be a 2 - tuple or 3 - tuple.", e.ptr());
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
node_t u_id, v_id;
|
||||
if (!self.node_to_id.contains(u)) {
|
||||
if (u.is_none()) {
|
||||
PyErr_Format(PyExc_ValueError, "None cannot be a node");
|
||||
return py::none();
|
||||
}
|
||||
u_id = DiGraph_add_one_node(self, u);
|
||||
|
||||
} else {
|
||||
u_id = self.node_to_id[u].cast<node_t>();
|
||||
}
|
||||
if (!self.node_to_id.contains(v)) {
|
||||
if (v.is_none()) {
|
||||
PyErr_Format(PyExc_ValueError, "None cannot be a node");
|
||||
return py::none();
|
||||
}
|
||||
v_id = DiGraph_add_one_node(self, v);
|
||||
|
||||
} else {
|
||||
v_id = self.node_to_id[v].cast<node_t>();
|
||||
}
|
||||
auto datadict = self.adj[u_id].count(v_id) ? self.adj[u_id][v_id] : node_attr_dict_factory();
|
||||
py::list items = py::list(attr.attr("items")());
|
||||
items.attr("extend")(py::list(dd.attr("items")()));
|
||||
for (int j = 0; j < py::len(items); j++) {
|
||||
py::tuple kv = items[j].cast<py::tuple>();
|
||||
py::object pkey = kv[0];
|
||||
std::string weight_key = weight_to_string(pkey);
|
||||
weight_t value = kv[1].cast<weight_t>();
|
||||
datadict.insert(std::make_pair(weight_key, value));
|
||||
}
|
||||
// Warning: in Graph.py the edge attr is directed assigned by the dict extended from the original attr
|
||||
self.adj[u_id][v_id].insert(datadict.begin(), datadict.end());
|
||||
}
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object DiGraph_add_edges_from_file(DiGraph& self, py::str file, py::object weighted, py::object is_transform) {
|
||||
self.dirty_nodes = true;
|
||||
self.dirty_adj = true;
|
||||
bool _is_transform = is_transform.cast<bool>();
|
||||
struct commactype : std::ctype<char> {
|
||||
commactype() : std::ctype<char>(get_table()) {}
|
||||
std::ctype_base::mask const* get_table() {
|
||||
std::ctype_base::mask* rc = 0;
|
||||
if (rc == 0) {
|
||||
rc = new std::ctype_base::mask[std::ctype<char>::table_size];
|
||||
std::fill_n(rc, std::ctype<char>::table_size, std::ctype_base::mask());
|
||||
rc[','] = std::ctype_base::space;
|
||||
rc[' '] = std::ctype_base::space;
|
||||
rc['\t'] = std::ctype_base::space;
|
||||
rc['\n'] = std::ctype_base::space;
|
||||
rc['\r'] = std::ctype_base::space;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
};
|
||||
|
||||
std::ios::sync_with_stdio(0);
|
||||
std::string file_path = file.cast<std::string>();
|
||||
std::ifstream in;
|
||||
in.open(file_path);
|
||||
if (!in.is_open()) {
|
||||
PyErr_Format(PyExc_FileNotFoundError, "Please check the file and make sure the path only contains English");
|
||||
return py::none();
|
||||
}
|
||||
in.imbue(std::locale(std::locale(), new commactype));
|
||||
std::string data, key("weight");
|
||||
std::string su, sv;
|
||||
weight_t weight;
|
||||
while (in >> su >> sv) {
|
||||
py::str pu(su), pv(sv);
|
||||
node_t u, v;
|
||||
if (!self.node_to_id.contains(pu)) {
|
||||
u = DiGraph_add_one_node(self, pu);
|
||||
} else {
|
||||
u = self.node_to_id[pu].cast<node_t>();
|
||||
}
|
||||
if (!self.node_to_id.contains(pv)) {
|
||||
v = DiGraph_add_one_node(self, pv);
|
||||
} else {
|
||||
v = self.node_to_id[pv].cast<node_t>();
|
||||
}
|
||||
if (weighted.cast<bool>()) {
|
||||
in >> weight;
|
||||
self.adj[u][v][key] = weight;
|
||||
self.pred[v][u][key] = weight;
|
||||
} else {
|
||||
if (!self.adj[u].count(v)) {
|
||||
self.adj[u][v] = node_attr_dict_factory();
|
||||
self.pred[v][u] = node_attr_dict_factory();
|
||||
}
|
||||
}
|
||||
}
|
||||
if(_is_transform){
|
||||
Graph_L g_l = graph_to_linkgraph(self,true, key, true, false);
|
||||
self.linkgraph_structure = g_l;
|
||||
self.linkgraph_dirty = false;
|
||||
}
|
||||
in.close();
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object DiGraph_add_weighted_edge(DiGraph& self, py::object u_of_edge, py::object v_of_edge, weight_t weight) {
|
||||
self.dirty_nodes = true;
|
||||
self.dirty_adj = true;
|
||||
py::dict edge_attr;
|
||||
edge_attr["weight"] = weight;
|
||||
DiGraph_add_one_edge(self, u_of_edge, v_of_edge, edge_attr);
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object DiGraph_remove_edge(DiGraph& self, py::object u, py::object v) {
|
||||
self.dirty_nodes = true;
|
||||
self.dirty_adj = true;
|
||||
if (self.node_to_id.contains(u) && self.node_to_id.contains(v)) {
|
||||
node_t u_id = self.node_to_id[u].cast<node_t>();
|
||||
node_t v_id = self.node_to_id[v].cast<node_t>();
|
||||
auto& u_neighbors_info = self.adj[u_id];
|
||||
if (u_neighbors_info.find(v_id) != u_neighbors_info.end()) {
|
||||
u_neighbors_info.erase(v_id);
|
||||
auto& v_predecessors_info = self.pred[v_id];
|
||||
v_predecessors_info.erase(u_id);
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
PyErr_Format(PyExc_KeyError, "No edge %R-%R in graph.", u.ptr(), v.ptr());
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object DiGraph_remove_edges(py::object self, py::list edges_to_remove) {
|
||||
DiGraph& self_ = self.cast<DiGraph&>();
|
||||
for (int i = 0; i < py::len(edges_to_remove); i++) {
|
||||
py::tuple edge = edges_to_remove[i].cast<py::tuple>();
|
||||
py::object u = edge[0], v = edge[1];
|
||||
self.attr("remove_edge")(u, v);
|
||||
}
|
||||
self_.dirty_nodes = true;
|
||||
self_.dirty_adj = true;
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object DiGraph_remove_edges_from(py::object self, py::list ebunch) {
|
||||
DiGraph& self_ = self.cast<DiGraph&>();
|
||||
for (int i = 0; i < py::len(ebunch); i++) {
|
||||
py::tuple edge = ebunch[i].cast<py::tuple>();
|
||||
node_t u_id = edge[0].cast<node_t>();
|
||||
node_t v_id = edge[1].cast<node_t>();
|
||||
if (self_.adj[u_id].find(v_id) != self_.adj[u_id].end() && self_.adj[v_id].find(u_id) != self_.adj[v_id].end()) {
|
||||
self_.adj[u_id].erase(v_id);
|
||||
self_.pred[v_id].erase(u_id);
|
||||
}
|
||||
}
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object DiGraph_nodes_subgraph(py::object self, py::list from_nodes) {
|
||||
py::object G = self.attr("__class__")();
|
||||
Graph& self_ = self.cast<Graph&>();
|
||||
DiGraph& G_ = G.cast<DiGraph&>();
|
||||
G_.graph.attr("update")(self_.graph);
|
||||
py::object nodes = self.attr("nodes");
|
||||
py::object adj = self.attr("adj");
|
||||
for (int i = 0; i < py::len(from_nodes); i++) {
|
||||
py::object node = from_nodes[i];
|
||||
if (self_.node_to_id.contains(node)) {
|
||||
py::object node_attr = nodes[node];
|
||||
DiGraph_add_one_node(G_, node, node_attr);
|
||||
}
|
||||
py::object out_edges = adj[node];
|
||||
py::list edge_items = py::list(out_edges.attr("items")());
|
||||
for (int j = 0; j < py::len(edge_items); j++) {
|
||||
py::tuple item = edge_items[j].cast<py::tuple>();
|
||||
py::object v = item[0];
|
||||
py::object edge_attr = item[1];
|
||||
if (from_nodes.contains(v)) {
|
||||
DiGraph_add_one_edge(G_, node, v, edge_attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return G;
|
||||
}
|
||||
|
||||
py::object DiGraph_generate_linkgraph(py::object self, py::object weight){
|
||||
DiGraph& G_ = self.cast<DiGraph&>();
|
||||
std::string w = weight_to_string(weight);
|
||||
Graph_L g_l = graph_to_linkgraph(G_, true, w, true, false);
|
||||
G_.linkgraph_dirty = false;
|
||||
G_.linkgraph_structure = g_l;
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object DiGraph_copy(py::object self) {
|
||||
DiGraph& self_ = self.cast<DiGraph&>();
|
||||
py::object G = self.attr("__class__")();
|
||||
DiGraph& G_ = G.cast<DiGraph&>();
|
||||
G_.graph.attr("update")(self_.graph);
|
||||
G_.id_to_node.attr("update")(self_.id_to_node);
|
||||
G_.node_to_id.attr("update")(self_.node_to_id);
|
||||
G_.node = self_.node;
|
||||
G_.adj = self_.adj;
|
||||
G_.pred = self_.pred;
|
||||
return py::object(G);
|
||||
}
|
||||
|
||||
py::object DiGraph_is_directed(py::object self) {
|
||||
return py::cast(true);
|
||||
}
|
||||
|
||||
py::object DiGraph_py(py::object self) {
|
||||
py::object G = py::module_::import("easygraph").attr("DiGraph")();
|
||||
G.attr("graph").attr("update")(self.attr("graph"));
|
||||
G.attr("adj").attr("update")(self.attr("adj"));
|
||||
G.attr("nodes").attr("update")(self.attr("nodes"));
|
||||
G.attr("pred").attr("update")(self.attr("pred"));
|
||||
// G.attr("succ").attr("update")(self.attr("succ"));
|
||||
return G;
|
||||
}
|
||||
|
||||
py::object DiGraph::get_pred() {
|
||||
adj_dict_factory pred = this->pred;
|
||||
py::dict predecessors = py::dict();
|
||||
for (const auto& ego_edges : this->pred) {
|
||||
node_t start_point = ego_edges.first;
|
||||
py::dict ego_edges_dict = py::dict();
|
||||
for (const auto& edge_info : ego_edges.second) {
|
||||
node_t end_point = edge_info.first;
|
||||
const auto& edge_attr = edge_info.second;
|
||||
ego_edges_dict[this->id_to_node[py::cast(end_point)]] = attr_to_dict(edge_attr);
|
||||
}
|
||||
predecessors[this->id_to_node[py::cast(start_point)]] = ego_edges_dict;
|
||||
}
|
||||
|
||||
return predecessors;
|
||||
}
|
||||
|
||||
py::object DiGraph::get_edges() {
|
||||
py::list edges = py::list();
|
||||
std::set<std::pair<node_t, node_t> > seen;
|
||||
for (const auto& ego_edges : this->adj) {
|
||||
node_t u = ego_edges.first;
|
||||
for (const auto& edge_info : ego_edges.second) {
|
||||
node_t v = edge_info.first;
|
||||
const auto& edge_attr = edge_info.second;
|
||||
if (seen.find(std::make_pair(u, v)) == seen.end()) {
|
||||
seen.insert(std::make_pair(u, v));
|
||||
edges.append(py::make_tuple(this->id_to_node[py::cast(u)], this->id_to_node[py::cast(v)],
|
||||
attr_to_dict(edge_attr)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "../common/common.h"
|
||||
#include "graph.h"
|
||||
|
||||
struct DiGraph : public Graph {
|
||||
DiGraph();
|
||||
py::object get_edges();
|
||||
py::object get_pred();
|
||||
|
||||
adj_dict_factory pred;
|
||||
};
|
||||
|
||||
py::object DiGraph__init__(py::args args, py::kwargs kwargs);
|
||||
py::object DiGraph_out_degree(py::object self, py::object weight);
|
||||
py::object DiGraph_in_degree(py::object self, py::object weight);
|
||||
py::object DiGraph_degree(py::object self, py::object weight);
|
||||
py::object DiGraph_size(py::object self, py::object weight);
|
||||
py::object DiGraph_number_of_edges(py::object self, py::object u, py::object v);
|
||||
py::object DiGraph_predecessors(py::object self, py::object node);
|
||||
py::object DiGraph_add_node(py::args args, py::kwargs kwargs);
|
||||
py::object DiGraph_add_nodes(DiGraph& self, py::list nodes_for_adding, py::list nodes_attr);
|
||||
py::object DiGraph_add_nodes_from(py::args args, py::kwargs kwargs);
|
||||
py::object DiGraph_remove_node(DiGraph& self, py::object node_to_remove);
|
||||
py::object DiGraph_remove_nodes(py::object self, py::list nodes_to_remove);
|
||||
py::object DiGraph_add_edge(py::args args, py::kwargs kwargs);
|
||||
py::object DiGraph_add_edges(DiGraph& self, py::list edges_for_adding, py::list edges_attr);
|
||||
py::object DiGraph_add_edges_from_file(DiGraph& self, py::str file, py::object weighted, py::object is_transform);
|
||||
py::object DiGraph_add_edges_from(py::args args, py::kwargs attr);
|
||||
py::object DiGraph_remove_edge(DiGraph& self, py::object u, py::object v);
|
||||
py::object DiGraph_remove_edges(py::object self, py::list edges_to_remove);
|
||||
py::object DiGraph_remove_edges_from(py::object self, py::list ebunch);
|
||||
py::object DiGraph_add_weighted_edge(DiGraph& self, py::object u_of_edge, py::object v_of_edge, weight_t weight);
|
||||
py::object DiGraph_nodes_subgraph(py::object self, py::list from_nodes);
|
||||
py::object DiGraph_is_directed(py::object self);
|
||||
py::object DiGraph_py(py::object self);
|
||||
py::object DiGraph_generate_linkgraph(py::object self, py::object weight);
|
||||
@@ -0,0 +1,990 @@
|
||||
#include "graph.h"
|
||||
#include "linkgraph.h"
|
||||
#include "../common/utils.h"
|
||||
|
||||
|
||||
Graph::Graph() {
|
||||
this->id = 0;
|
||||
this->dirty_nodes = true;
|
||||
this->dirty_adj = true;
|
||||
this->linkgraph_dirty = true;
|
||||
this->csr_graph = nullptr;
|
||||
this->node_to_id = py::dict();
|
||||
this->id_to_node = py::dict();
|
||||
this->graph = py::dict();
|
||||
this->nodes_cache = py::dict();
|
||||
this->adj_cache = py::dict();
|
||||
this->coo_graph = nullptr;
|
||||
}
|
||||
|
||||
py::object Graph__init__(py::args args, py::kwargs kwargs) {
|
||||
py::object self = args[0];
|
||||
self.attr("__init__")();
|
||||
Graph& self_ = self.cast<Graph&>();
|
||||
py::dict graph_attr = kwargs;
|
||||
self_.graph.attr("update")(graph_attr);
|
||||
self_.nodes_cache = py::dict();
|
||||
self_.adj_cache = py::dict();
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph__iter__(py::object self) {
|
||||
return self.attr("nodes").attr("__iter__")();
|
||||
}
|
||||
|
||||
py::object Graph__len__(py::object self) {
|
||||
Graph& self_ = self.cast<Graph&>();
|
||||
return py::cast(py::len(self_.node_to_id));
|
||||
}
|
||||
|
||||
py::object Graph__contains__(py::object self, py::object node) {
|
||||
Graph& self_ = self.cast<Graph&>();
|
||||
try {
|
||||
return py::cast(self_.node_to_id.contains(node));
|
||||
} catch (const py::error_already_set&) {
|
||||
PyObject *type, *value, *traceback;
|
||||
PyErr_Fetch(&type, &value, &traceback);
|
||||
if (PyErr_GivenExceptionMatches(PyExc_TypeError, type)) {
|
||||
return py::cast(false);
|
||||
} else {
|
||||
PyErr_Restore(type, value, traceback);
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
py::object Graph__getitem__(py::object self, py::object node) {
|
||||
return self.attr("adj")[node];
|
||||
}
|
||||
|
||||
node_t _add_one_node(Graph& self, py::object one_node_for_adding, py::object node_attr = py::dict()) {
|
||||
node_t id;
|
||||
if (self.node_to_id.contains(one_node_for_adding)) {
|
||||
id = self.node_to_id[one_node_for_adding].cast<node_t>();
|
||||
} else {
|
||||
id = ++(self.id);
|
||||
self.id_to_node[py::cast(id)] = one_node_for_adding;
|
||||
self.node_to_id[one_node_for_adding] = id;
|
||||
}
|
||||
py::list items = py::list(node_attr.attr("items")());
|
||||
self.node[id] = node_attr_dict_factory();
|
||||
self.adj[id] = adj_attr_dict_factory();
|
||||
for (int i = 0; i < len(items); i++) {
|
||||
py::tuple kv = items[i].cast<py::tuple>();
|
||||
py::object pkey = kv[0];
|
||||
std::string weight_key = weight_to_string(pkey);
|
||||
weight_t value = kv[1].cast<weight_t>();
|
||||
self.node[id].insert(std::make_pair(weight_key, value));
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
py::object Graph_add_node(py::args args, py::kwargs kwargs) {
|
||||
Graph& self = args[0].cast<Graph&>();
|
||||
self.drop_cache();
|
||||
py::object one_node_for_adding = args[1];
|
||||
py::dict node_attr = kwargs;
|
||||
_add_one_node(self, one_node_for_adding, node_attr);
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph_add_nodes(Graph& self, py::list nodes_for_adding, py::list nodes_attr) {
|
||||
self.drop_cache();
|
||||
if (py::len(nodes_attr) != 0) {
|
||||
if (py::len(nodes_for_adding) != py::len(nodes_attr)) {
|
||||
PyErr_Format(PyExc_AssertionError, "Nodes and Attributes lists must have same length.");
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < py::len(nodes_for_adding); i++) {
|
||||
py::object one_node_for_adding = nodes_for_adding[i];
|
||||
py::dict node_attr;
|
||||
if (py::len(nodes_attr)) {
|
||||
node_attr = nodes_attr[i].cast<py::dict>();
|
||||
} else {
|
||||
node_attr = py::dict();
|
||||
}
|
||||
_add_one_node(self, one_node_for_adding, node_attr);
|
||||
}
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph_add_nodes_from(py::args args, py::kwargs kwargs) {
|
||||
Graph& self = args[0].cast<Graph&>();
|
||||
self.drop_cache();
|
||||
py::list nodes_for_adding = py::list(args[1]);
|
||||
for (int i = 0; i < py::len(nodes_for_adding); i++) {
|
||||
bool newnode;
|
||||
py::dict attr = kwargs;
|
||||
py::dict newdict, ndict;
|
||||
py::object n = nodes_for_adding[i];
|
||||
try {
|
||||
newnode = !self.node_to_id.contains(n);
|
||||
newdict = attr;
|
||||
}
|
||||
catch (const py::error_already_set&) {
|
||||
PyObject *type, *value, *traceback;
|
||||
PyErr_Fetch(&type, &value, &traceback);
|
||||
if (PyErr_GivenExceptionMatches(PyExc_TypeError, type)) {
|
||||
py::tuple n_pair = n.cast<py::tuple>();
|
||||
n = n_pair[0];
|
||||
ndict = n_pair[1].cast<py::dict>();
|
||||
newnode = !self.node_to_id.contains(n);
|
||||
newdict = attr.attr("copy")();
|
||||
newdict.attr("update")(ndict);
|
||||
} else {
|
||||
PyErr_Restore(type, value, traceback);
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
if (newnode) {
|
||||
if (n.is_none()) {
|
||||
PyErr_Format(PyExc_ValueError, "None cannot be a node");
|
||||
return py::none();
|
||||
}
|
||||
_add_one_node(self, n);
|
||||
}
|
||||
node_t id = self.node_to_id[n].cast<node_t>();
|
||||
for (auto item : newdict) {
|
||||
std::string weight_key = weight_to_string(item.first.cast<py::object>());
|
||||
weight_t value = item.second.cast<weight_t>();
|
||||
self.node[id].insert(std::make_pair(weight_key, value));
|
||||
}
|
||||
}
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph_remove_node(Graph& self, py::object node_to_remove) {
|
||||
self.drop_cache();
|
||||
if (!self.node_to_id.contains(node_to_remove)) {
|
||||
PyErr_Format(PyExc_KeyError, "No node %R in graph.", node_to_remove.ptr());
|
||||
return py::none();
|
||||
}
|
||||
node_t node_id = self.node_to_id[node_to_remove].cast<node_t>();
|
||||
for (const auto& neighbor_info : self.adj[node_id]) {
|
||||
node_t neighbor_id = neighbor_info.first;
|
||||
self.adj[neighbor_id].erase(node_id);
|
||||
}
|
||||
self.adj.erase(node_id);
|
||||
self.node.erase(node_id);
|
||||
self.node_to_id.attr("pop")(node_to_remove);
|
||||
self.id_to_node.attr("pop")(node_id);
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph_remove_nodes(py::object self, py::list nodes_to_remove) {
|
||||
Graph& self_ = self.cast<Graph&>();
|
||||
self_.drop_cache();
|
||||
for (int i = 0; i < py::len(nodes_to_remove); i++) {
|
||||
py::object node_to_remove = nodes_to_remove[i];
|
||||
if (!self_.node_to_id.contains(node_to_remove)) {
|
||||
PyErr_Format(PyExc_KeyError, "No node %R in graph.", node_to_remove.ptr());
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < py::len(nodes_to_remove); i++) {
|
||||
py::object node_to_remove = nodes_to_remove[i];
|
||||
self.attr("remove_node")(node_to_remove);
|
||||
}
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph_number_of_nodes(Graph& self) {
|
||||
return py::cast(int(self.node.size()));
|
||||
}
|
||||
|
||||
py::object Graph_has_node(Graph& self, py::object node) {
|
||||
return py::cast(self.node_to_id.contains(node));
|
||||
}
|
||||
|
||||
py::object Graph_nbunch_iter(py::object self, py::object nbunch) {
|
||||
py::object bunch = py::none();
|
||||
if (nbunch.is_none()) {
|
||||
bunch = self.attr("adj").attr("__iter__")();
|
||||
} else if (self.contains(nbunch)) {
|
||||
py::list nbunch_wrapper = py::list();
|
||||
nbunch_wrapper.append(nbunch);
|
||||
bunch = nbunch_wrapper.attr("__iter__")();
|
||||
} else {
|
||||
py::list nbunch_list = py::list(nbunch), nodes_list = py::list();
|
||||
for (int i = 0; i < py::len(nbunch_list); i++) {
|
||||
py::object n = nbunch_list[i];
|
||||
if (self.contains(n)) {
|
||||
nodes_list.append(n);
|
||||
}
|
||||
}
|
||||
bunch = nbunch_list.attr("__iter__")();
|
||||
}
|
||||
return bunch;
|
||||
}
|
||||
|
||||
void _add_one_edge(Graph& self, py::object u_of_edge, py::object v_of_edge, py::object edge_attr) {
|
||||
node_t u, v;
|
||||
if (!self.node_to_id.contains(u_of_edge)) {
|
||||
u = _add_one_node(self, u_of_edge);
|
||||
} else {
|
||||
u = self.node_to_id[u_of_edge].cast<node_t>();
|
||||
}
|
||||
if (!self.node_to_id.contains(v_of_edge)) {
|
||||
v = _add_one_node(self, v_of_edge);
|
||||
} else {
|
||||
v = self.node_to_id[v_of_edge].cast<node_t>();
|
||||
}
|
||||
py::list items = py::list(edge_attr.attr("items")());
|
||||
self.adj[u][v] = node_attr_dict_factory();
|
||||
self.adj[v][u] = node_attr_dict_factory();
|
||||
for (int i = 0; i < len(items); i++) {
|
||||
py::tuple kv = items[i].cast<py::tuple>();
|
||||
py::object pkey = kv[0];
|
||||
std::string weight_key = weight_to_string(pkey);
|
||||
weight_t value = kv[1].cast<weight_t>();
|
||||
self.adj[u][v].insert(std::make_pair(weight_key, value));
|
||||
self.adj[v][u].insert(std::make_pair(weight_key, value));
|
||||
}
|
||||
}
|
||||
|
||||
py::object Graph_add_edge(py::args args, py::kwargs kwargs) {
|
||||
Graph& self = args[0].cast<Graph&>();
|
||||
self.drop_cache();
|
||||
py::object u_of_edge = args[1], v_of_edge = args[2];
|
||||
py::dict edge_attr = kwargs;
|
||||
_add_one_edge(self, u_of_edge, v_of_edge, edge_attr);
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph_add_edges(Graph& self, py::list edges_for_adding, py::list edges_attr) {
|
||||
self.drop_cache();
|
||||
if (py::len(edges_attr) != 0) {
|
||||
if (py::len(edges_for_adding) != py::len(edges_attr)) {
|
||||
PyErr_Format(PyExc_AssertionError, "Edges and Attributes lists must have same length.");
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < py::len(edges_for_adding); i++) {
|
||||
py::tuple one_edge_for_adding = edges_for_adding[i].cast<py::tuple>();
|
||||
py::dict edge_attr;
|
||||
if (py::len(edges_attr)) {
|
||||
edge_attr = edges_attr[i].cast<py::dict>();
|
||||
} else {
|
||||
edge_attr = py::dict();
|
||||
}
|
||||
_add_one_edge(self, one_edge_for_adding[0], one_edge_for_adding[1], edge_attr);
|
||||
}
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph_add_edges_from(py::args args, py::kwargs attr) {
|
||||
Graph& self = args[0].cast<Graph&>();
|
||||
self.drop_cache();
|
||||
py::list ebunch_to_add = py::list(args[1]);
|
||||
for (int i = 0; i < len(ebunch_to_add); i++) {
|
||||
py::list e = py::list(ebunch_to_add[i]);
|
||||
py::object u, v;
|
||||
py::dict dd;
|
||||
switch (len(e)) {
|
||||
case 2: {
|
||||
u = e[0];
|
||||
v = e[1];
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
u = e[0];
|
||||
v = e[1];
|
||||
dd = e[2].cast<py::dict>();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
PyErr_Format(PyExc_ValueError, "Edge tuple %R must be a 2 - tuple or 3 - tuple.", e.ptr());
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
node_t u_id, v_id;
|
||||
if (!self.node_to_id.contains(u)) {
|
||||
if (u.is_none()) {
|
||||
PyErr_Format(PyExc_ValueError, "None cannot be a node");
|
||||
return py::none();
|
||||
}
|
||||
u_id = _add_one_node(self, u);
|
||||
} else {
|
||||
u_id = self.node_to_id[u].cast<node_t>();
|
||||
}
|
||||
if (!self.node_to_id.contains(v)) {
|
||||
if (v.is_none()) {
|
||||
PyErr_Format(PyExc_ValueError, "None cannot be a node");
|
||||
return py::none();
|
||||
}
|
||||
v_id = _add_one_node(self, v);
|
||||
} else {
|
||||
v_id = (self.node_to_id[v]).cast<node_t>();
|
||||
}
|
||||
auto datadict = self.adj[u_id].count(v_id) ? self.adj[u_id][v_id] : node_attr_dict_factory();
|
||||
py::list items = py::list(attr.attr("items")());
|
||||
items.attr("extend")(py::list(dd.attr("items")()));
|
||||
for (int i = 0; i < py::len(items); i++) {
|
||||
py::tuple kv = items[i].cast<py::tuple>();
|
||||
py::object pkey = kv[0];
|
||||
std::string weight_key = weight_to_string(pkey);
|
||||
weight_t value = kv[1].cast<weight_t>();
|
||||
datadict.insert(std::make_pair(weight_key, value));
|
||||
}
|
||||
// Warning: in Graph.py the edge attr is directed assigned by the dict extended from the original attr
|
||||
self.adj[u_id][v_id].insert(datadict.begin(), datadict.end());
|
||||
self.adj[v_id][u_id].insert(datadict.begin(), datadict.end());
|
||||
}
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph_add_edges_from_file(Graph& self, py::str file, py::object weighted, py::object is_transform) {
|
||||
self.drop_cache();
|
||||
bool _is_transform = is_transform.cast<bool>();
|
||||
struct commactype : std::ctype<char> {
|
||||
commactype() : std::ctype<char>(get_table()) {}
|
||||
std::ctype_base::mask const* get_table() {
|
||||
std::ctype_base::mask* rc = 0;
|
||||
if (rc == 0) {
|
||||
rc = new std::ctype_base::mask[std::ctype<char>::table_size];
|
||||
std::fill_n(rc, std::ctype<char>::table_size, std::ctype_base::mask());
|
||||
rc[','] = std::ctype_base::space;
|
||||
rc[' '] = std::ctype_base::space;
|
||||
rc[' '] = std::ctype_base::space;
|
||||
rc['\t'] = std::ctype_base::space;
|
||||
rc['\n'] = std::ctype_base::space;
|
||||
rc['\r'] = std::ctype_base::space;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
};
|
||||
|
||||
std::ios::sync_with_stdio(0);
|
||||
std::string file_path = file.cast<std::string>();
|
||||
std::ifstream in;
|
||||
in.open(file_path);
|
||||
if (!in.is_open()) {
|
||||
PyErr_Format(PyExc_FileNotFoundError, "Please check the file and make sure the path only contains English");
|
||||
return py::none();
|
||||
}
|
||||
in.imbue(std::locale(std::locale(), new commactype));
|
||||
std::string data, key("weight");
|
||||
std::string su, sv;
|
||||
weight_t weight;
|
||||
while (in >> su >> sv) {
|
||||
py::str pu(su), pv(sv);
|
||||
node_t u, v;
|
||||
if (!self.node_to_id.contains(pu)) {
|
||||
u = _add_one_node(self, pu);
|
||||
} else {
|
||||
u = self.node_to_id[pu].cast<node_t>();
|
||||
}
|
||||
if (!self.node_to_id.contains(pv)) {
|
||||
v = _add_one_node(self, pv);
|
||||
} else {
|
||||
v = self.node_to_id[pv].cast<node_t>();
|
||||
}
|
||||
if (weighted.cast<bool>()) {
|
||||
in >> weight;
|
||||
self.adj[u][v][key] = self.adj[v][u][key] = weight;
|
||||
} else {
|
||||
if (!self.adj[u].count(v)) {
|
||||
self.adj[u][v] = node_attr_dict_factory();
|
||||
}
|
||||
if (!self.adj[v].count(u)) {
|
||||
self.adj[v][u] = node_attr_dict_factory();
|
||||
}
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
if(_is_transform){
|
||||
Graph_L g_l = graph_to_linkgraph(self, false, key, true, false);
|
||||
self.linkgraph_structure = g_l;
|
||||
self.linkgraph_dirty = false;
|
||||
}
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph_add_weighted_edge(Graph& self, py::object u_of_edge, py::object v_of_edge, weight_t weight) {
|
||||
self.drop_cache();
|
||||
py::dict edge_attr;
|
||||
edge_attr["weight"] = weight;
|
||||
_add_one_edge(self, u_of_edge, v_of_edge, edge_attr);
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph_remove_edge(Graph& self, py::object u, py::object v) {
|
||||
self.drop_cache();
|
||||
if (self.node_to_id.contains(u) && self.node_to_id.contains(v)) {
|
||||
node_t u_id = self.node_to_id[u].cast<node_t>();
|
||||
node_t v_id = self.node_to_id[v].cast<node_t>();
|
||||
auto& v_neighbors_info = self.adj[u_id];
|
||||
if (v_neighbors_info.find(v_id) != v_neighbors_info.end()) {
|
||||
v_neighbors_info.erase(v_id);
|
||||
if (u_id != v_id) {
|
||||
self.adj[v_id].erase(u_id);
|
||||
}
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
PyErr_Format(PyExc_KeyError, "No edge %R-%R in graph.", u.ptr(), v.ptr());
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph_remove_edges(py::object self, py::list edges_to_remove) {
|
||||
Graph& self_ = self.cast<Graph&>();
|
||||
for (int i = 0; i < py::len(edges_to_remove); i++) {
|
||||
py::tuple edge = edges_to_remove[i].cast<py::tuple>();
|
||||
py::object u = edge[0], v = edge[1];
|
||||
self.attr("remove_edge")(u, v);
|
||||
}
|
||||
self_.drop_cache();
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph_number_of_edges(py::object self, py::object u, py::object v) {
|
||||
if (u.is_none()) {
|
||||
return self.attr("size")();
|
||||
}
|
||||
Graph& self_ = self.cast<Graph&>();
|
||||
node_t u_id = self_.node_to_id.attr("get")(u, -1).cast<node_t>();
|
||||
node_t v_id = self_.node_to_id.attr("get")(v, -1).cast<node_t>();
|
||||
return py::cast(int(self_.adj.count(u_id) && self_.adj[u_id].count(v_id)));
|
||||
}
|
||||
|
||||
py::object Graph_has_edge(Graph& self, py::object u, py::object v) {
|
||||
if (self.node_to_id.contains(u) && self.node_to_id.contains(v)) {
|
||||
node_t u_id = self.node_to_id[u].cast<node_t>();
|
||||
node_t v_id = self.node_to_id[v].cast<node_t>();
|
||||
auto& v_neighbors_info = self.adj[u_id];
|
||||
if (v_neighbors_info.find(v_id) != v_neighbors_info.end()) {
|
||||
return py::cast(true);
|
||||
}
|
||||
}
|
||||
return py::cast(false);
|
||||
}
|
||||
|
||||
py::object Graph_copy(py::object self) {
|
||||
Graph& self_ = self.cast<Graph&>();
|
||||
py::object G = self.attr("__class__")();
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
G_.graph.attr("update")(self_.graph);
|
||||
G_.id_to_node.attr("update")(self_.id_to_node);
|
||||
G_.node_to_id.attr("update")(self_.node_to_id);
|
||||
G_.id = self_.id;
|
||||
G_.node = self_.node;
|
||||
G_.adj = self_.adj;
|
||||
return G;
|
||||
}
|
||||
|
||||
py::object Graph_degree(py::object self, py::object weight) {
|
||||
py::dict degree;
|
||||
py::list edges = self.attr("edges").cast<py::list>();
|
||||
py::object u, v;
|
||||
py::dict d;
|
||||
for (int i = 0; i < py::len(edges); i++) {
|
||||
py::tuple edge = edges[i].cast<py::tuple>();
|
||||
u = edge[0];
|
||||
v = edge[1];
|
||||
d = edge[2].cast<py::dict>();
|
||||
if (degree.contains(u)) {
|
||||
degree[u] = py::object(degree[u]) + d.attr("get")(weight, 1);
|
||||
} else {
|
||||
degree[u] = d.attr("get")(weight, 1);
|
||||
}
|
||||
if (degree.contains(v)) {
|
||||
degree[v] = py::object(degree[v]) + d.attr("get")(weight, 1);
|
||||
} else {
|
||||
degree[v] = d.attr("get")(weight, 1);
|
||||
}
|
||||
}
|
||||
py::list nodes = py::list(self.attr("nodes"));
|
||||
for (int i = 0; i < py::len(nodes); i++) {
|
||||
py::object node = nodes[i];
|
||||
if (!degree.contains(node)) {
|
||||
degree[node] = 0;
|
||||
}
|
||||
}
|
||||
return degree;
|
||||
}
|
||||
|
||||
py::object Graph_neighbors(py::object self, py::object node) {
|
||||
Graph& self_ = self.cast<Graph&>();
|
||||
if (self_.node_to_id.contains(node)) {
|
||||
return self.attr("adj")[node].attr("__iter__")();
|
||||
} else {
|
||||
PyErr_Format(PyExc_KeyError, "No node %R", node.ptr());
|
||||
return py::none();
|
||||
}
|
||||
}
|
||||
|
||||
py::object Graph_generate_linkgraph(py::object self, py::object weight){
|
||||
Graph& G_ = self.cast<Graph&>();
|
||||
std::string w = weight_to_string(weight);
|
||||
Graph_L g_l = graph_to_linkgraph(G_, false, w, true, false);
|
||||
G_.linkgraph_dirty = false;
|
||||
G_.linkgraph_structure = g_l;
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph_nodes_subgraph(py::object self, py::list from_nodes) {
|
||||
py::object G = self.attr("__class__")();
|
||||
Graph& self_ = self.cast<Graph&>();
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
G_.graph.attr("update")(self_.graph);
|
||||
py::object nodes = self.attr("nodes");
|
||||
py::object adj = self.attr("adj");
|
||||
for (int i = 0; i < py::len(from_nodes); i++) {
|
||||
py::object node = from_nodes[i];
|
||||
if (self_.node_to_id.contains(node)) {
|
||||
py::object node_attr = nodes[node];
|
||||
_add_one_node(G_, node, node_attr);
|
||||
}
|
||||
py::object out_edges = adj[node];
|
||||
py::list edge_items = py::list(out_edges.attr("items")());
|
||||
for (int j = 0; j < py::len(edge_items); j++) {
|
||||
py::tuple item = edge_items[j].cast<py::tuple>();
|
||||
py::object v = item[0];
|
||||
py::object edge_attr = item[1];
|
||||
if (from_nodes.contains(v)) {
|
||||
_add_one_edge(G_, node, v, edge_attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return G;
|
||||
}
|
||||
|
||||
py::object Graph_ego_subgraph(py::object self, py::object center) {
|
||||
py::list neighbors_of_center = py::list(self.attr("all_neighbors")(center));
|
||||
neighbors_of_center.append(center);
|
||||
return self.attr("nodes_subgraph")(neighbors_of_center);
|
||||
}
|
||||
|
||||
py::object Graph_size(py::object self, py::object weight) {
|
||||
py::dict degree = self.attr("degree")(weight).cast<py::dict>();
|
||||
weight_t s = 0;
|
||||
for (auto item : degree) {
|
||||
s += item.second.cast<weight_t>();
|
||||
}
|
||||
return (weight.is_none()) ? py::cast(int(s) / 2) : py::cast(s / 2);
|
||||
}
|
||||
|
||||
py::object Graph_is_directed(py::object self) {
|
||||
return py::cast(false);
|
||||
}
|
||||
|
||||
py::object Graph_is_multigraph(py::object self) {
|
||||
return py::cast(false);
|
||||
}
|
||||
|
||||
py::object Graph_to_index_node_graph(py::object self, py::object begin_index) {
|
||||
py::object G = self.attr("__class__")();
|
||||
G.attr("graph").attr("update")(self.attr("graph"));
|
||||
py::dict index_of_node = py::dict(), node_of_index = py::dict();
|
||||
int begin = begin_index.cast<int>();
|
||||
int index = 0;
|
||||
for (auto item : self.attr("nodes").cast<py::dict>()) {
|
||||
py::object node = item.first.cast<py::object>();
|
||||
py::dict node_attr = item.second.cast<py::dict>();
|
||||
G.attr("add_node")(py::cast(index + begin), **node_attr);
|
||||
index_of_node[node] = index + begin;
|
||||
node_of_index[py::cast(index + begin)] = node;
|
||||
index++;
|
||||
}
|
||||
for (auto item : self.attr("adj").cast<py::dict>()) {
|
||||
py::object u = item.first.cast<py::object>();
|
||||
py::dict nbrs = item.second.cast<py::dict>();
|
||||
for (auto item_ : nbrs) {
|
||||
py::object v = item_.first.cast<py::object>();
|
||||
py::dict edge_data = item_.second.cast<py::dict>();
|
||||
G.attr("add_edge")(index_of_node[u], index_of_node[v], **edge_data);
|
||||
}
|
||||
}
|
||||
return py::make_tuple(G, index_of_node, node_of_index);
|
||||
}
|
||||
|
||||
py::object Graph_py(py::object self) {
|
||||
py::object G = py::module_::import("easygraph").attr("Graph")();
|
||||
G.attr("graph").attr("update")(self.attr("graph"));
|
||||
G.attr("adj").attr("update")(self.attr("adj"));
|
||||
G.attr("nodes").attr("update")(self.attr("nodes"));
|
||||
return G;
|
||||
}
|
||||
|
||||
py::object Graph::get_nodes() {
|
||||
if (this->dirty_nodes) {
|
||||
py::dict nodes = py::dict();
|
||||
for (const auto& node_info : node) {
|
||||
node_t id = node_info.first;
|
||||
const auto& node_attr = node_info.second;
|
||||
nodes[this->id_to_node[py::cast(id)]] = attr_to_dict(node_attr);
|
||||
}
|
||||
this->nodes_cache = nodes;
|
||||
this->dirty_nodes = false;
|
||||
}
|
||||
return this->nodes_cache;
|
||||
}
|
||||
|
||||
py::object Graph::get_name() {
|
||||
return this->graph.attr("get")("name", "");
|
||||
}
|
||||
|
||||
py::object Graph::set_name(py::object name) {
|
||||
this->graph[py::cast("name")] = name;
|
||||
return py::none();
|
||||
}
|
||||
|
||||
py::object Graph::get_node_index() {
|
||||
py::dict node_index = py::dict();
|
||||
int len = py::len(this->node_to_id);
|
||||
for(int i = 1; i <= len; i++){
|
||||
node_index[this->id_to_node[py::cast(i)]] = py::cast(i - 1);
|
||||
}
|
||||
return node_index;
|
||||
}
|
||||
py::object Graph::get_graph() {
|
||||
return this->graph;
|
||||
}
|
||||
|
||||
py::object Graph::get_adj() {
|
||||
if (this->dirty_adj) {
|
||||
py::dict adj = py::dict();
|
||||
for (const auto& ego_edges : this->adj) {
|
||||
node_t start_point = ego_edges.first;
|
||||
py::dict ego_edges_dict = py::dict();
|
||||
for (const auto& edge_info : ego_edges.second) {
|
||||
node_t end_point = edge_info.first;
|
||||
const auto& edge_attr = edge_info.second;
|
||||
ego_edges_dict[this->id_to_node[py::cast(end_point)]] = attr_to_dict(edge_attr);
|
||||
}
|
||||
adj[this->id_to_node[py::cast(start_point)]] = ego_edges_dict;
|
||||
}
|
||||
this->adj_cache = adj;
|
||||
this->dirty_adj = false;
|
||||
}
|
||||
return this->adj_cache;
|
||||
}
|
||||
|
||||
py::object Graph::get_edges() {
|
||||
py::list edges = py::list();
|
||||
std::set<std::pair<node_t, node_t> > seen;
|
||||
for (const auto& ego_edges : this->adj) {
|
||||
node_t u = ego_edges.first;
|
||||
for (const auto& edge_info : ego_edges.second) {
|
||||
node_t v = edge_info.first;
|
||||
const auto& edge_attr = edge_info.second;
|
||||
if (seen.find(std::make_pair(u, v)) == seen.end()) {
|
||||
seen.insert(std::make_pair(u, v));
|
||||
seen.insert(std::make_pair(v, u));
|
||||
edges.append(py::make_tuple(this->id_to_node[py::cast(u)], this->id_to_node[py::cast(v)], attr_to_dict(edge_attr)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
|
||||
Graph_L Graph::_get_linkgraph_structure() {
|
||||
return this->linkgraph_structure;
|
||||
}
|
||||
bool Graph::is_linkgraph_dirty(){
|
||||
return this->linkgraph_dirty;
|
||||
}
|
||||
std::vector<graph_edge> Graph::_get_edges(bool if_directed) {
|
||||
std::vector<graph_edge> edges;
|
||||
std::set<std::pair<node_t, node_t> > seen;
|
||||
for (const auto& ego_edges : this->adj) {
|
||||
node_t u = ego_edges.first;
|
||||
for (const auto& edge_info : ego_edges.second) {
|
||||
node_t v = edge_info.first;
|
||||
const auto& edge_attr = edge_info.second;
|
||||
if (seen.find(std::make_pair(u, v)) == seen.end()) {
|
||||
|
||||
seen.insert(std::make_pair(u, v));
|
||||
if(!if_directed){
|
||||
seen.insert(std::make_pair(v, u));
|
||||
}
|
||||
|
||||
edges.emplace_back(u, v, edge_attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
void Graph::drop_cache() {
|
||||
dirty_nodes = true;
|
||||
dirty_adj = true;
|
||||
linkgraph_dirty = true;
|
||||
csr_graph = nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<CSRGraph> Graph::gen_CSR(const std::string& weight) {
|
||||
if (csr_graph != nullptr) {
|
||||
if (csr_graph->W_map.find(weight) == csr_graph->W_map.end()) {
|
||||
auto W = std::make_shared<std::vector<double>>();
|
||||
|
||||
// According to C++ Standard, the iteration order of a unordered contrainer will
|
||||
// not change without rehashing which only happens during a insert.
|
||||
for (node_t n : csr_graph->nodes) {
|
||||
// if n is not in adj, this way can raise an exception
|
||||
const auto& n_adjs = adj.find(n)->second;
|
||||
|
||||
for (auto adj_it = n_adjs.begin(); adj_it != n_adjs.end(); ++adj_it) {
|
||||
const edge_attr_dict_factory& edge_attr = adj_it->second;
|
||||
auto edge_it = edge_attr.find(weight);
|
||||
weight_t w = edge_it != edge_attr.end() ? edge_it->second : 1.0;
|
||||
|
||||
W->push_back(w);
|
||||
}
|
||||
}
|
||||
|
||||
csr_graph->W_map[weight] = W;
|
||||
}
|
||||
} else {
|
||||
// the graph has been modified
|
||||
|
||||
csr_graph = std::make_shared<CSRGraph>();
|
||||
|
||||
std::vector<node_t>& nodes = csr_graph->nodes;
|
||||
for (auto it = node.begin(); it != node.end(); ++it) {
|
||||
nodes.push_back(it->first);
|
||||
}
|
||||
|
||||
std::sort(nodes.begin(), nodes.end());
|
||||
|
||||
std::unordered_map<node_t, int>& node2idx = csr_graph->node2idx;
|
||||
|
||||
for (int i = 0; i < nodes.size(); ++i) {
|
||||
node2idx[nodes[i]] = i;
|
||||
}
|
||||
|
||||
std::vector<int>& V = csr_graph->V;
|
||||
std::vector<int>& E = csr_graph->E;
|
||||
auto W = std::make_shared<std::vector<double>>();
|
||||
|
||||
for (int idx = 0; idx < nodes.size(); ++idx) {
|
||||
V.push_back(E.size());
|
||||
|
||||
node_t n = nodes[idx];
|
||||
|
||||
// if n is not in adj, this way can raise an exception
|
||||
const auto& n_adjs = adj.find(n)->second;
|
||||
|
||||
for (auto adj_it = n_adjs.begin(); adj_it != n_adjs.end(); ++adj_it) {
|
||||
const edge_attr_dict_factory& edge_attr = adj_it->second;
|
||||
auto edge_it = edge_attr.find(weight);
|
||||
weight_t w = edge_it != edge_attr.end() ? edge_it->second : 1.0;
|
||||
|
||||
W->push_back(w);
|
||||
E.push_back(node2idx[adj_it->first]);
|
||||
}
|
||||
}
|
||||
|
||||
V.push_back(E.size());
|
||||
|
||||
csr_graph->W_map[weight] = W;
|
||||
}
|
||||
|
||||
return csr_graph;
|
||||
}
|
||||
|
||||
std::shared_ptr<CSRGraph> Graph::gen_CSR() {
|
||||
if (csr_graph != nullptr) {
|
||||
if (csr_graph->unweighted_W.size() != csr_graph->E.size()) {
|
||||
csr_graph->unweighted_W = std::vector<double>(csr_graph->E.size(), 1.0);
|
||||
}
|
||||
} else {
|
||||
// the graph has been modified
|
||||
|
||||
csr_graph = std::make_shared<CSRGraph>();
|
||||
|
||||
std::vector<node_t>& nodes = csr_graph->nodes;
|
||||
for (auto it = node.begin(); it != node.end(); ++it) {
|
||||
nodes.push_back(it->first);
|
||||
}
|
||||
|
||||
std::sort(nodes.begin(), nodes.end());
|
||||
|
||||
std::unordered_map<node_t, int>& node2idx = csr_graph->node2idx;
|
||||
|
||||
for (int i = 0; i < nodes.size(); ++i) {
|
||||
node2idx[nodes[i]] = i;
|
||||
}
|
||||
|
||||
std::vector<int>& V = csr_graph->V;
|
||||
std::vector<int>& E = csr_graph->E;
|
||||
|
||||
for (int idx = 0; idx < nodes.size(); ++idx) {
|
||||
V.push_back(E.size());
|
||||
|
||||
node_t n = nodes[idx];
|
||||
|
||||
// if n is not in adj, this way can raise an exception
|
||||
const auto& n_adjs = adj.find(n)->second;
|
||||
|
||||
for (auto adj_it = n_adjs.begin(); adj_it != n_adjs.end(); ++adj_it) {
|
||||
const edge_attr_dict_factory& edge_attr = adj_it->second;
|
||||
|
||||
E.push_back(node2idx[adj_it->first]);
|
||||
}
|
||||
}
|
||||
|
||||
V.push_back(E.size());
|
||||
csr_graph->unweighted_W = std::vector<double>(E.size(), 1.0);
|
||||
}
|
||||
|
||||
return csr_graph;
|
||||
}
|
||||
|
||||
std::shared_ptr<std::vector<int>> Graph::gen_CSR_sources(const py::object& py_sources) {
|
||||
auto sources = std::make_shared<std::vector<int>>();
|
||||
|
||||
if (py_sources.is_none()) {
|
||||
for (int i = 0; i < csr_graph->V.size() - 1; ++i) {
|
||||
sources->push_back(i);
|
||||
}
|
||||
} else {
|
||||
for (auto it = py_sources.begin(); it != py_sources.end(); ++it) {
|
||||
sources->push_back(csr_graph->node2idx[node_to_id[*it].cast<node_t>()]);
|
||||
}
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
std::shared_ptr<COOGraph> Graph::gen_COO() {
|
||||
if (coo_graph != nullptr) {
|
||||
if (coo_graph->unweighted_W.size() != coo_graph->row.size()) {
|
||||
coo_graph->unweighted_W = std::vector<double>(coo_graph->row.size(), 1.0);
|
||||
}
|
||||
} else {
|
||||
|
||||
coo_graph = std::make_shared<COOGraph>();
|
||||
|
||||
std::vector<node_t>& nodes = coo_graph->nodes;
|
||||
for (auto it = node.begin(); it != node.end(); ++it) {
|
||||
nodes.push_back(it->first);
|
||||
}
|
||||
|
||||
std::sort(nodes.begin(), nodes.end());
|
||||
|
||||
std::unordered_map<node_t, int>& node2idx = coo_graph->node2idx;
|
||||
for (int i = 0; i < nodes.size(); ++i) {
|
||||
node2idx[nodes[i]] = i;
|
||||
}
|
||||
|
||||
std::vector<int>& row = coo_graph->row;
|
||||
std::vector<int>& col = coo_graph->col;
|
||||
std::vector<double>& unweighted_W = coo_graph->unweighted_W;
|
||||
|
||||
for (int idx = 0; idx < nodes.size(); ++idx) {
|
||||
node_t n = nodes[idx];
|
||||
|
||||
const auto& n_adjs = adj.find(n)->second;
|
||||
|
||||
for (auto adj_it = n_adjs.begin(); adj_it != n_adjs.end(); ++adj_it) {
|
||||
node_t neighbor = adj_it->first;
|
||||
|
||||
row.push_back(idx);
|
||||
col.push_back(node2idx[neighbor]);
|
||||
|
||||
unweighted_W.push_back(1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return coo_graph;
|
||||
}
|
||||
|
||||
std::shared_ptr<COOGraph> Graph::gen_COO(const std::string& weight) {
|
||||
if (coo_graph != nullptr) {
|
||||
if (coo_graph->W_map.find(weight) == coo_graph->W_map.end()) {
|
||||
auto W = std::make_shared<std::vector<double>>();
|
||||
|
||||
for (node_t n : coo_graph->nodes) {
|
||||
const auto& n_adjs = adj.find(n)->second;
|
||||
|
||||
for (auto adj_it = n_adjs.begin(); adj_it != n_adjs.end(); ++adj_it) {
|
||||
const edge_attr_dict_factory& edge_attr = adj_it->second;
|
||||
auto edge_it = edge_attr.find(weight);
|
||||
weight_t w = edge_it != edge_attr.end() ? edge_it->second : 1.0;
|
||||
|
||||
W->push_back(w);
|
||||
}
|
||||
}
|
||||
|
||||
coo_graph->W_map[weight] = W;
|
||||
}
|
||||
} else {
|
||||
coo_graph = std::make_shared<COOGraph>();
|
||||
|
||||
std::vector<node_t>& nodes = coo_graph->nodes;
|
||||
for (auto it = node.begin(); it != node.end(); ++it) {
|
||||
nodes.push_back(it->first);
|
||||
}
|
||||
|
||||
std::sort(nodes.begin(), nodes.end());
|
||||
|
||||
std::unordered_map<node_t, int>& node2idx = coo_graph->node2idx;
|
||||
for (int i = 0; i < nodes.size(); ++i) {
|
||||
node2idx[nodes[i]] = i;
|
||||
}
|
||||
|
||||
std::vector<int>& row = coo_graph->row;
|
||||
std::vector<int>& col = coo_graph->col;
|
||||
auto W = std::make_shared<std::vector<double>>();
|
||||
|
||||
for (int idx = 0; idx < nodes.size(); ++idx) {
|
||||
node_t n = nodes[idx];
|
||||
|
||||
const auto& n_adjs = adj.find(n)->second;
|
||||
|
||||
for (auto adj_it = n_adjs.begin(); adj_it != n_adjs.end(); ++adj_it) {
|
||||
const edge_attr_dict_factory& edge_attr = adj_it->second;
|
||||
auto edge_it = edge_attr.find(weight);
|
||||
weight_t w = edge_it != edge_attr.end() ? edge_it->second : 1.0;
|
||||
|
||||
row.push_back(idx);
|
||||
col.push_back(node2idx[adj_it->first]);
|
||||
|
||||
W->push_back(w);
|
||||
}
|
||||
}
|
||||
|
||||
coo_graph->W_map[weight] = W;
|
||||
}
|
||||
|
||||
return coo_graph;
|
||||
}
|
||||
|
||||
std::shared_ptr<COOGraph> Graph::transfer_csr_to_coo(const std::shared_ptr<CSRGraph>& csr_graph) {
|
||||
auto coo_graph = std::make_shared<COOGraph>();
|
||||
|
||||
coo_graph->nodes = csr_graph->nodes;
|
||||
coo_graph->node2idx = csr_graph->node2idx;
|
||||
|
||||
const std::vector<int>& V = csr_graph->V;
|
||||
const std::vector<int>& E = csr_graph->E;
|
||||
int num_edges = E.size();
|
||||
|
||||
coo_graph->row.reserve(num_edges);
|
||||
coo_graph->col.reserve(num_edges);
|
||||
|
||||
std::vector<int>& row = coo_graph->row;
|
||||
std::vector<int>& col = coo_graph->col;
|
||||
|
||||
for (int i = 0; i < V.size() - 1; ++i) {
|
||||
int start_idx = V[i];
|
||||
int end_idx = V[i + 1];
|
||||
|
||||
for (int j = start_idx; j < end_idx; ++j) {
|
||||
row.push_back(i);
|
||||
col.push_back(E[j]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!csr_graph->unweighted_W.empty()) {
|
||||
coo_graph->unweighted_W = csr_graph->unweighted_W;
|
||||
} else {
|
||||
coo_graph->W_map = csr_graph->W_map;
|
||||
}
|
||||
|
||||
return coo_graph;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include "../common/common.h"
|
||||
#include "csr_graph.h"
|
||||
#include "linkgraph.h"
|
||||
#include "coo_graph.h"
|
||||
|
||||
// old version
|
||||
struct Graph {
|
||||
node_dict_factory node;
|
||||
adj_dict_factory adj;
|
||||
Graph_L linkgraph_structure;
|
||||
std::shared_ptr<CSRGraph> csr_graph;
|
||||
py::kwargs node_to_id, id_to_node, graph;
|
||||
node_t id;
|
||||
bool dirty_nodes, dirty_adj, linkgraph_dirty;
|
||||
py::object nodes_cache, adj_cache;
|
||||
std::shared_ptr<COOGraph> coo_graph;
|
||||
|
||||
Graph();
|
||||
py::object get_nodes();
|
||||
py::object get_name();
|
||||
py::object set_name(py::object name);
|
||||
py::object get_graph();
|
||||
py::object get_adj();
|
||||
py::object get_edges();
|
||||
py::object get_node_index();
|
||||
std::vector<graph_edge> _get_edges(bool if_directed=true);
|
||||
bool is_linkgraph_dirty();
|
||||
Graph_L _get_linkgraph_structure();
|
||||
void drop_cache();
|
||||
|
||||
std::shared_ptr<CSRGraph> gen_CSR(const std::string& weight);
|
||||
std::shared_ptr<CSRGraph> gen_CSR();
|
||||
std::shared_ptr<std::vector<int>> gen_CSR_sources(const py::object& py_sources);
|
||||
std::shared_ptr<COOGraph> gen_COO();
|
||||
std::shared_ptr<COOGraph> gen_COO(const std::string& weight);
|
||||
std::shared_ptr<COOGraph> transfer_csr_to_coo(const std::shared_ptr<CSRGraph>& csr_graph);
|
||||
};
|
||||
|
||||
py::object Graph__init__(py::args args, py::kwargs kwargs);
|
||||
py::object Graph__iter__(py::object self);
|
||||
py::object Graph__len__(py::object self);
|
||||
py::object Graph__contains__(py::object self, py::object node);
|
||||
py::object Graph__getitem__(py::object self, py::object node);
|
||||
py::object Graph_add_node(py::args args, py::kwargs kwargs);
|
||||
py::object Graph_add_nodes(Graph& self, py::list nodes_for_adding, py::list nodes_attr);
|
||||
py::object Graph_add_nodes_from(py::args args, py::kwargs kwargs);
|
||||
py::object Graph_remove_node(Graph& self, py::object node_to_remove);
|
||||
py::object Graph_remove_nodes(py::object self, py::list nodes_to_remove);
|
||||
py::object Graph_number_of_nodes(Graph& self);
|
||||
py::object Graph_has_node(Graph& self, py::object node);
|
||||
py::object Graph_nbunch_iter(py::object self, py::object nbunch);
|
||||
py::object Graph_add_edge(py::args args, py::kwargs kwargs);
|
||||
py::object Graph_add_edges(Graph& self, py::list edges_for_adding, py::list edges_attr);
|
||||
py::object Graph_add_edges_from(py::args args, py::kwargs attr);
|
||||
py::object Graph_add_edges_from_file(Graph& self, py::str file, py::object weighted, py::object is_transform);
|
||||
py::object Graph_add_weighted_edge(Graph& self, py::object u_of_edge, py::object v_of_edge, weight_t weight);
|
||||
py::object Graph_remove_edge(Graph& self, py::object u, py::object v);
|
||||
py::object Graph_remove_edges(py::object self, py::list edges_to_remove);
|
||||
py::object Graph_number_of_edges(py::object self, py::object u, py::object v);
|
||||
py::object Graph_has_edge(Graph& self, py::object u, py::object v);
|
||||
py::object Graph_copy(py::object self);
|
||||
py::object Graph_degree(py::object self, py::object weight);
|
||||
py::object Graph_neighbors(py::object self, py::object node);
|
||||
py::object Graph_nodes_subgraph(py::object self, py::list from_nodes);
|
||||
py::object Graph_ego_subgraph(py::object self, py::object center);
|
||||
py::object Graph_size(py::object self, py::object weight);
|
||||
py::object Graph_is_directed(py::object self);
|
||||
py::object Graph_is_multigraph(py::object self);
|
||||
py::object Graph_to_index_node_graph(py::object self, py::object begin_index);
|
||||
py::object Graph_generate_linkgraph(py::object self, py::object weight);
|
||||
py::object Graph_py(py::object self);
|
||||
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
#include "../common/common.h"
|
||||
|
||||
struct LinkEdge{
|
||||
// 终点
|
||||
node_t to;
|
||||
// 边权重
|
||||
weight_t w;
|
||||
// 同起点的上一条边的编号
|
||||
node_t next;
|
||||
|
||||
};
|
||||
struct Graph_L{
|
||||
int n;
|
||||
int e;
|
||||
bool is_directed;
|
||||
bool is_deg;
|
||||
std::vector<int> head;
|
||||
std::vector<LinkEdge> edges;
|
||||
std::vector<int> degree;
|
||||
int max_deg = -1;
|
||||
|
||||
|
||||
Graph_L(int vertex_num = 0, bool directed = true, bool deg = false){
|
||||
this->n = vertex_num;
|
||||
this->e = 0;
|
||||
this->is_deg = deg;
|
||||
this->is_directed = directed;
|
||||
LinkEdge le;
|
||||
le.to = -1;
|
||||
le.next = -1;
|
||||
edges.emplace_back(le);
|
||||
if(n > 0){
|
||||
head.resize(vertex_num + 1);
|
||||
if(deg){
|
||||
degree.resize(vertex_num + 1);
|
||||
for(int i = 0; i < vertex_num+1; i++){
|
||||
head[i] = -1;
|
||||
degree[i] = 0;
|
||||
}
|
||||
}
|
||||
else{
|
||||
for(int i = 0; i < vertex_num+1; i++){
|
||||
head[i] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void add_unweighted_edge(const int &u, const int &v) {
|
||||
LinkEdge le;
|
||||
le.to = v;
|
||||
le.next = head[u];
|
||||
this->edges.emplace_back(le);
|
||||
this->head[u] = this->e;
|
||||
this->e += 1;
|
||||
if(this->is_deg){
|
||||
this->degree[u]++;
|
||||
this->max_deg = std::max(this->max_deg, this->degree[u]);
|
||||
}
|
||||
}
|
||||
|
||||
void add_weighted_edge(const int &u, const int &v, const double &w) {
|
||||
// printf("u:%d,v:%d w:%.2f\n",u,v,w);
|
||||
LinkEdge le;
|
||||
this->e += 1;
|
||||
le.to = v;
|
||||
le.w = w;
|
||||
le.next = head[u];
|
||||
// printf("next:%d\n",this->head[u]);
|
||||
this->edges.emplace_back(le);
|
||||
this->head[u] = this->e;
|
||||
// printf("head:%d\n",this->head[u]);
|
||||
|
||||
if(this->is_deg){
|
||||
this->degree[u]++;
|
||||
this->max_deg = std::max(this->max_deg, this->degree[u]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct compare_node {
|
||||
compare_node(){}
|
||||
compare_node(int _x, int _d) {x = _x; d = _d;}
|
||||
int x, d;
|
||||
bool operator < (const compare_node &rhs) const {
|
||||
return d > rhs.d;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<double> _dijkstra(const Graph_L& G_l, int source, int target);
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "operation.h"
|
||||
#include "graph.h"
|
||||
|
||||
py::object density(py::object G) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
int n = G_.node.size();
|
||||
int m = G.attr("number_of_edges")().cast<int>();
|
||||
if (m == 0 || n <= 1) {
|
||||
return py::cast(0);
|
||||
}
|
||||
weight_t d = m * 1.0 / (n * (n - 1));
|
||||
if(G.attr("is_directed")().equal(py::cast(false))){
|
||||
d*=2;
|
||||
}
|
||||
return py::cast(d);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "../common/common.h"
|
||||
|
||||
py::object density(py::object G);
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "../common/common.h"
|
||||
|
||||
class Segment_tree_zkw {
|
||||
private:
|
||||
int tn;
|
||||
int size;
|
||||
public:
|
||||
std::vector<int> t;
|
||||
std::vector<int> num;
|
||||
Segment_tree_zkw(int N){
|
||||
size = (N+1)<<2;
|
||||
this->t = std::vector<int>(size+1 , INT_MAX);
|
||||
this->num = std::vector<int>(size+1 , 0);
|
||||
}
|
||||
void init(int N) {
|
||||
for(int i = 0; i < size; i++){
|
||||
this->t[i] = INT_MAX;
|
||||
this->num[i] = 0;
|
||||
}
|
||||
|
||||
tn = 1;
|
||||
while(tn < N) tn <<= 1;
|
||||
--tn;
|
||||
for (int i = 1; i <= N; ++i)
|
||||
this->num[i + tn] = i;
|
||||
}
|
||||
|
||||
void change(int p, const int &k) {
|
||||
p += tn; this->t[p] = k; p >>= 1;
|
||||
while (p) {
|
||||
if (this->t[p<<1] < this->t[p<<1|1]) {
|
||||
this->t[p] = this->t[p<<1];
|
||||
this->num[p] = this->num[p<<1];
|
||||
}
|
||||
else {
|
||||
this->t[p] = this->t[p<<1|1];
|
||||
this->num[p] = this->num[p<<1|1];
|
||||
}
|
||||
p >>= 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
#include "common.h"
|
||||
|
||||
graph_edge::graph_edge(node_t u_, node_t v_, edge_attr_dict_factory attr_) : u(u_), v(v_), attr(attr_) {}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/numpy.h>
|
||||
#include <pybind11/stl.h>
|
||||
#include <iostream>
|
||||
#include <stdio.h>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <exception>
|
||||
#include <set>
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
#include <algorithm>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <inttypes.h>
|
||||
#include <limits>
|
||||
namespace py = pybind11;
|
||||
|
||||
typedef int node_t;
|
||||
typedef float weight_t;
|
||||
typedef std::map<std::string, weight_t> node_attr_dict_factory; //(weight_key, value)
|
||||
typedef std::map<std::string, weight_t> edge_attr_dict_factory; //(weight_key, value)
|
||||
typedef std::unordered_map<node_t, node_attr_dict_factory> node_dict_factory; //(node, node_attr)
|
||||
typedef std::unordered_map<node_t, edge_attr_dict_factory> adj_attr_dict_factory; //(out_node, (weight_key, value))
|
||||
typedef std::unordered_map<node_t, adj_attr_dict_factory> adj_dict_factory; //(node, edge_attr)
|
||||
struct graph_edge {
|
||||
node_t u, v;
|
||||
edge_attr_dict_factory attr;
|
||||
graph_edge(node_t, node_t, edge_attr_dict_factory);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "utils.h"
|
||||
#include "../classes/graph.h"
|
||||
#include "../classes/linkgraph.h"
|
||||
|
||||
py::object attr_to_dict(const node_attr_dict_factory& attr) {
|
||||
py::dict attr_dict = py::dict();
|
||||
for (const auto& kv : attr) {
|
||||
attr_dict[py::cast(kv.first)] = kv.second;
|
||||
}
|
||||
return attr_dict;
|
||||
}
|
||||
|
||||
std::string weight_to_string(py::object weight) {
|
||||
py::object warn = py::module_::import("warnings").attr("warn");
|
||||
if (!py::isinstance<py::str>(weight)) {
|
||||
if (!weight.is_none()) {
|
||||
warn(py::str(weight) + py::str(" would be transformed into an instance of str."));
|
||||
}
|
||||
weight = py::str(weight);
|
||||
}
|
||||
std::string weight_key = weight.cast<std::string>();
|
||||
return weight_key;
|
||||
}
|
||||
|
||||
py::object py_sum(py::object o) {
|
||||
py::object sum = py::module_::import("builtins").attr("sum");
|
||||
return sum(o);
|
||||
}
|
||||
|
||||
Graph_L graph_to_linkgraph(Graph &G, bool if_directed, std::string weight_key, bool is_deg, bool is_reverse){
|
||||
int node_num = G.node.size();
|
||||
const std::vector<graph_edge>& edges = G._get_edges(if_directed);
|
||||
int edges_num = edges.size();
|
||||
Graph_L G_l(node_num, if_directed, is_deg);
|
||||
for(int i = 0; i < edges_num; i++){
|
||||
graph_edge e = edges[i];
|
||||
edge_attr_dict_factory& edge_attr = e.attr;
|
||||
weight_t edge_weight = edge_attr.find(weight_key) != edge_attr.end() ? edge_attr[weight_key] : 1;
|
||||
if(is_reverse){
|
||||
std::swap(e.u, e.v);
|
||||
}
|
||||
G_l.add_weighted_edge(e.u, e.v, edge_weight);
|
||||
if (!if_directed){
|
||||
G_l.add_weighted_edge(e.v, e.u, edge_weight);
|
||||
}
|
||||
}
|
||||
return G_l;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
#include "common.h"
|
||||
#include "../classes/linkgraph.h"
|
||||
#include "../classes/graph.h"
|
||||
|
||||
py::object attr_to_dict(const node_attr_dict_factory& attr);
|
||||
std::string weight_to_string(py::object weight);
|
||||
py::object py_sum(py::object o);
|
||||
Graph_L graph_to_linkgraph(Graph &G, bool if_directed=false, std::string weight_key = "weight", bool is_deg = false, bool is_reverse = false);
|
||||
@@ -0,0 +1,120 @@
|
||||
#include "classes/__init__.h"
|
||||
#include "functions/__init__.h"
|
||||
PYBIND11_MODULE(cpp_easygraph, m) {
|
||||
|
||||
py::class_<Graph>(m, "Graph")
|
||||
.def(py::init<>())
|
||||
.def("__init__", &Graph__init__)
|
||||
.def("__iter__", &Graph__iter__)
|
||||
.def("__len__", &Graph__len__)
|
||||
.def("__contains__", &Graph__contains__, py::arg("node"))
|
||||
.def("__getitem__", &Graph__getitem__, py::arg("node"))
|
||||
.def("add_node", &Graph_add_node)
|
||||
.def("add_nodes", &Graph_add_nodes, py::arg("nodes_for_adding"), py::arg("nodes_attr") = py::list())
|
||||
.def("add_nodes_from", &Graph_add_nodes_from)
|
||||
.def("remove_node", &Graph_remove_node, py::arg("node_to_remove"))
|
||||
.def("remove_nodes", &Graph_remove_nodes, py::arg("nodes_to_remove"))
|
||||
.def("number_of_nodes", &Graph_number_of_nodes)
|
||||
.def("has_node", &Graph_has_node, py::arg("node"))
|
||||
.def("nbunch_iter", &Graph_nbunch_iter, py::arg("nbunch") = py::none())
|
||||
.def("add_edge", &Graph_add_edge)
|
||||
.def("add_edges", &Graph_add_edges, py::arg("edges_for_adding"), py::arg("edges_attr") = py::list())
|
||||
.def("add_edges_from", &Graph_add_edges_from)
|
||||
.def("add_edges_from_file", &Graph_add_edges_from_file, py::arg("file"), py::arg("weighted") = false, py::arg("is_transform") = false)
|
||||
.def("add_weighted_edge", &Graph_add_weighted_edge, py::arg("u_of_edge"), py::arg("v_of_edge"), py::arg("weight"))
|
||||
.def("remove_edge", &Graph_remove_edge, py::arg("u"), py::arg("v"))
|
||||
.def("remove_edges", &Graph_remove_edges, py::arg("edges_to_remove"))
|
||||
.def("number_of_edges", &Graph_number_of_edges, py::arg("u") = py::none(), py::arg("v") = py::none())
|
||||
.def("has_edge", &Graph_has_edge, py::arg("u"), py::arg("y"))
|
||||
.def("copy", &Graph_copy)
|
||||
.def("degree", &Graph_degree, py::arg("weight") = "weight")
|
||||
.def("neighbors", &Graph_neighbors, py::arg("node"))
|
||||
.def("all_neighbors", &Graph_neighbors, py::arg("node"))
|
||||
.def("nodes_subgraph", &Graph_nodes_subgraph, py::arg("from_nodes"))
|
||||
.def("ego_subgraph", &Graph_ego_subgraph, py::arg("center"))
|
||||
.def("size", &Graph_size, py::arg("weight") = py::none())
|
||||
.def("is_directed", &Graph_is_directed)
|
||||
.def("is_multigraph", &Graph_is_multigraph)
|
||||
.def("to_index_node_graph", &Graph_to_index_node_graph, py::arg("begin_index") = 0)
|
||||
.def("py", &Graph_py)
|
||||
.def_property("graph", &Graph::get_graph, nullptr)
|
||||
.def_property("nodes", &Graph::get_nodes, nullptr)
|
||||
.def_property("name", &Graph::get_name, &Graph::set_name)
|
||||
.def_property("adj", &Graph::get_adj, nullptr)
|
||||
.def_property("edges", &Graph::get_edges, nullptr)
|
||||
.def_property("node_index", &Graph::get_node_index, nullptr)
|
||||
.def("generate_linkgraph", &Graph_generate_linkgraph,py::arg("weight") = "weight");
|
||||
|
||||
py::class_<DiGraph, Graph>(m, "DiGraph")
|
||||
.def(py::init<>())
|
||||
.def("__init__", &DiGraph__init__)
|
||||
.def("out_degree", &DiGraph_out_degree, py::arg("weight") = "weight")
|
||||
.def("in_degree", &DiGraph_in_degree, py::arg("weight") = "weight")
|
||||
.def("degree", &DiGraph_degree, py::arg("weight") = "weight")
|
||||
.def("size", &DiGraph_size, py::arg("weight") = py::none())
|
||||
.def("number_of_edges", &DiGraph_number_of_edges, py::arg("u") = py::none(), py::arg("v") = py::none())
|
||||
.def("successors", &Graph_neighbors, py::arg("node"))
|
||||
.def("predecessors", &DiGraph_predecessors, py::arg("node"))
|
||||
.def("add_node", &DiGraph_add_node)
|
||||
.def("add_nodes", &DiGraph_add_nodes, py::arg("nodes_for_adding"), py::arg("nodes_attr") = py::list())
|
||||
.def("add_nodes_from", &DiGraph_add_nodes_from)
|
||||
.def("remove_node", &DiGraph_remove_node, py::arg("node_to_remove"))
|
||||
.def("remove_nodes", &DiGraph_remove_nodes, (py::arg("nodes_to_remove")))
|
||||
.def("add_edge", &DiGraph_add_edge)
|
||||
.def("add_edges", &DiGraph_add_edges, py::arg("edges_for_adding"), py::arg("edges_attr") = py::list())
|
||||
.def("add_edges_from", &DiGraph_add_edges_from)
|
||||
.def("add_edges_from_file", &DiGraph_add_edges_from_file, py::arg("file"), py::arg("weighted") = false, py::arg("is_transform") = false)
|
||||
.def("add_weighted_edge", &DiGraph_add_weighted_edge, py::arg("u_of_edge"), py::arg("v_of_edge"), py::arg("weight"))
|
||||
.def("remove_edge", &DiGraph_remove_edge, py::arg("u"), py::arg("v"))
|
||||
.def("remove_edges", &DiGraph_remove_edges, py::arg("edges_to_remove"))
|
||||
.def("remove_edges_from", &DiGraph_remove_edges_from, py::arg("ebunch"))
|
||||
.def("nodes_subgraph", &DiGraph_nodes_subgraph, py::arg("from_nodes"))
|
||||
.def("is_directed", &DiGraph_is_directed)
|
||||
.def("py", &DiGraph_py)
|
||||
.def_property("edges", &DiGraph::get_edges, nullptr)
|
||||
.def_property("pred", &DiGraph::get_pred,nullptr)
|
||||
.def("generate_linkgraph", &DiGraph_generate_linkgraph,py::arg("weight") = "weight");
|
||||
|
||||
m.def("cpp_degree_centrality", °ree_centrality, py::arg("G"));
|
||||
m.def("cpp_in_degree_centrality", &in_degree_centrality, py::arg("G"));
|
||||
m.def("cpp_out_degree_centrality", &out_degree_centrality, py::arg("G"));
|
||||
m.def("cpp_closeness_centrality", &closeness_centrality, py::arg("G"), py::arg("weight") = "weight", py::arg("cutoff") = py::none(), py::arg("sources") = py::none());
|
||||
m.def("cpp_betweenness_centrality", &betweenness_centrality, py::arg("G"), py::arg("weight") = "weight", py::arg("cutoff") = py::none(),py::arg("sources") = py::none(), py::arg("normalized") = py::bool_(true), py::arg("endpoints") = py::bool_(false));
|
||||
m.def("cpp_katz_centrality", &cpp_katz_centrality, py::arg("G"), py::arg("alpha") = 0.1, py::arg("beta") = 1.0, py::arg("max_iter") = 1000, py::arg("tol") = 1e-6, py::arg("normalized") = true);
|
||||
m.def("cpp_eigenvector_centrality", &cpp_eigenvector_centrality, py::arg("G"), py::arg("max_iter") = 100, py::arg("tol") = 1.0e-6, py::arg("nstart") = py::none(), py::arg("weight") = "weight");
|
||||
m.def("cpp_k_core", &core_decomposition, py::arg("G"));
|
||||
m.def("cpp_density", &density, py::arg("G"));
|
||||
m.def("cpp_constraint", &constraint, py::arg("G"), py::arg("nodes") = py::none(), py::arg("weight") = py::none(), py::arg("n_workers") = py::none());
|
||||
m.def("cpp_effective_size", &effective_size, py::arg("G"), py::arg("nodes") = py::none(), py::arg("weight") = py::none(), py::arg("n_workers") = py::none());
|
||||
m.def("cpp_efficiency", &efficiency, py::arg("G"), py::arg("nodes") = py::none(), py::arg("weight") = py::none(), py::arg("n_workers") = py::none());
|
||||
m.def("cpp_hierarchy", &hierarchy, py::arg("G"), py::arg("nodes") = py::none(), py::arg("weight") = py::none(), py::arg("n_workers") = py::none());
|
||||
m.def("cpp_pagerank", &_pagerank, py::arg("G"), py::arg("alpha") = 0.85, py::arg("max_iterator") = 500, py::arg("threshold") = 1e-6, py::arg("weight") = "weight");
|
||||
m.def("cpp_dijkstra_multisource", &_dijkstra_multisource, py::arg("G"), py::arg("sources"), py::arg("weight") = "weight", py::arg("target") = py::none());
|
||||
m.def("cpp_spfa", &_spfa, py::arg("G"), py::arg("source"), py::arg("weight") = "weight");
|
||||
m.def("cpp_clustering", &clustering, py::arg("G"), py::arg("nodes") = py::none(), py::arg("weight") = py::none());
|
||||
m.def("cpp_biconnected_dfs_record_edges", &_biconnected_dfs_record_edges, py::arg("G"), py::arg("need_components") = true);
|
||||
m.def("cpp_strongly_connected_components",&strongly_connected_components,py::arg("G"));
|
||||
m.def("cpp_Floyd", &Floyd, py::arg("G"), py::arg("weight") = "weight");
|
||||
m.def("cpp_Prim", &Prim, py::arg("G"), py::arg("weight") = "weight");
|
||||
m.def("cpp_Kruskal", &Kruskal, py::arg("G"), py::arg("weight") = "weight");
|
||||
m.def("cpp_plain_bfs", &plain_bfs, py::arg("G"), py::arg("source"));
|
||||
m.def("cpp_kruskal_mst_edges", &kruskal_mst_edges, py::arg("G"), py::arg("minimum") = true, py::arg("weight") = "weight", py::arg("data") = true, py::arg("ignore_nan") = false);
|
||||
m.def("cpp_prim_mst_edges", &prim_mst_edges, py::arg("G"), py::arg("minimum") = true, py::arg("weight") = "weight", py::arg("data") = true, py::arg("ignore_nan") = false);
|
||||
m.def("cpp_boruvka_mst_edges", &boruvka_mst_edges, py::arg("G"), py::arg("minimum") = true, py::arg("weight") = "weight", py::arg("data") = true, py::arg("ignore_nan") = false);
|
||||
m.def("cpp_average_shortest_path_length", &average_shortest_path_length, py::arg("G"), py::arg("weight") = py::none(), py::arg("method") = py::none());
|
||||
m.def("cpp_eccentricity", &eccentricity, py::arg("G"), py::arg("v") = py::none(), py::arg("sp") = py::none());
|
||||
m.def("cpp_connected_components_undirected", &connected_component_undirected, py::arg("G"));
|
||||
m.def("cpp_connected_components_directed", &connected_component_directed, py::arg("G"));
|
||||
|
||||
// community methods
|
||||
m.def("cpp_modularity", &cpp_modularity, py::arg("G"), py::arg("communities"), py::arg("weight") = py::str("weight"));
|
||||
m.def("cpp_greedy_modularity_communities", &cpp_greedy_modularity_communities, py::arg("G"), py::arg("weight") = py::str("weight"));
|
||||
m.def("cpp_enumerate_subgraph", &cpp_enumerate_subgraph, py::arg("G"), py::arg("k"));
|
||||
m.def("cpp_random_enumerate_subgraph", &cpp_random_enumerate_subgraph, py::arg("G"), py::arg("k"), py::arg("cut_prob"));
|
||||
m.def("cpp_louvain_communities", &cpp_louvain_communities, py::arg("G"), py::arg("weight") = py::str("weight"), py::arg("threshold") = py::float_(0.00002), py::arg("resolution") = py::float_(1.0));
|
||||
m.def("cpp_louvain_communities_serial", &cpp_louvain_communities_serial, py::arg("G"), py::arg("weight") = py::str("weight"), py::arg("threshold") = py::float_(0.00002), py::arg("resolution") = py::float_(1.0));
|
||||
m.def("cpp_LPA", &cpp_LPA, py::arg("G"));
|
||||
m.def("cpp_ego_graph", &cpp_ego_graph, py::arg("G"), py::arg("n"), py::arg("radius") = py::int_(1), py::arg("center") = py::bool_(true), py::arg("undirected") = py::bool_(false), py::arg("distance") = py::none());
|
||||
m.def("cpp_ego_graph_csr", &cpp_ego_graph_csr, py::arg("G"), py::arg("n"), py::arg("radius") = py::int_(1), py::arg("center") = py::bool_(true), py::arg("undirected") = py::bool_(false), py::arg("distance") = py::none());
|
||||
m.def("cpp_localsearch", &cpp_localsearch, py::arg("G"), py::arg("center_num") = py::none(), py::arg("auto_choose_centers") = py::bool_(false), py::arg("maximum_tree") = py::bool_(true), py::arg("seed") = py::none(), py::arg("self_loop") = py::bool_(false));
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "components/__init__.h"
|
||||
#include "basic/__init__.h"
|
||||
#include "path/__init__.h"
|
||||
#include "structural_holes/__init__.h"
|
||||
#include "cores/__init__.h"
|
||||
#include "centrality/__init__.h"
|
||||
#include "pagerank/__init__.h"
|
||||
#include "community/__init__.h"
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#include "avg_degree.h"
|
||||
#include "cluster.h"
|
||||
@@ -0,0 +1,9 @@
|
||||
#include "avg_degree.h"
|
||||
|
||||
#include "../../classes/graph.h"
|
||||
py::object average_degree(py::object G) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
int n = G_.node.size();
|
||||
int m = G.attr("number_of_edges")().cast<int>();
|
||||
return py::cast(2.0 * m / n);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../common/common.h"
|
||||
|
||||
py::object average_degree(py::object G);
|
||||
@@ -0,0 +1,298 @@
|
||||
#include "cluster.h"
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../classes/directed_graph.h"
|
||||
#include "../../common/utils.h"
|
||||
|
||||
inline weight_t wt(adj_dict_factory& adj, node_t u, node_t v, std::string weight, weight_t max_weight = 1) {
|
||||
auto& attr = adj[u][v];
|
||||
return (attr.count(weight) ? attr[weight] : 1) / max_weight;
|
||||
}
|
||||
|
||||
py::list _weighted_triangles_and_degree(py::object G, py::object nodes, py::object weight) {
|
||||
std::string weight_key = weight_to_string(weight);
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
auto& adj = G_.adj;
|
||||
weight_t max_weight = 1;
|
||||
if (weight.is_none() || G.attr("number_of_edges")().equal(py::cast(0))) {
|
||||
max_weight = 1;
|
||||
}
|
||||
else {
|
||||
int assigned = 0;
|
||||
for (auto& u_info : G_.adj) {
|
||||
for (auto& v_info : u_info.second) {
|
||||
auto& d = v_info.second;
|
||||
if (assigned) {
|
||||
max_weight = std::max(max_weight, d.count(weight_key) ? d[weight_key] : 1);
|
||||
}
|
||||
else {
|
||||
assigned = 1;
|
||||
max_weight = d.count(weight_key) ? d[weight_key] : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
py::list nodes_list = py::list(G.attr("nbunch_iter")(nodes));
|
||||
py::list ret = py::list();
|
||||
for (int i = 0;i < py::len(nodes_list);i++) {
|
||||
node_t i_id = (G_.node_to_id[nodes_list[i]]).cast<node_t>();
|
||||
std::unordered_set<node_t> inbrs, seen;
|
||||
for (const auto& pair : adj[i_id]) {
|
||||
inbrs.insert(pair.first);
|
||||
}
|
||||
inbrs.erase(i_id);
|
||||
weight_t weighted_triangles = 0;
|
||||
for (const auto& j_id : inbrs) {
|
||||
seen.insert(j_id);
|
||||
weight_t wij = wt(adj, i_id, j_id, weight_key, max_weight);
|
||||
for (const auto& k_id : inbrs) {
|
||||
if (adj[j_id].count(k_id) && !seen.count(k_id)) {
|
||||
weight_t wjk = wt(adj, j_id, k_id, weight_key, max_weight);
|
||||
weight_t wki = wt(adj, k_id, i_id, weight_key, max_weight);
|
||||
weighted_triangles += std::cbrt(wij * wjk * wki);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret.append(py::make_tuple(G_.id_to_node[py::cast(i_id)], inbrs.size(), 2 * weighted_triangles));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
py::list _directed_weighted_triangles_and_degree(py::object G, py::object nodes, py::object weight) {
|
||||
std::string weight_key = weight_to_string(weight);
|
||||
DiGraph& G_ = G.cast<DiGraph&>();
|
||||
auto& adj = G_.adj;
|
||||
weight_t max_weight = 1;
|
||||
if (weight.is_none() || G.attr("number_of_edges")().equal(py::cast(0))) {
|
||||
max_weight = 1;
|
||||
}
|
||||
else {
|
||||
int assigned = 0;
|
||||
for (auto& u_info : G_.adj) {
|
||||
for (auto& v_info : u_info.second) {
|
||||
auto& d = v_info.second;
|
||||
if (assigned) {
|
||||
max_weight = std::max(max_weight, d.count(weight_key) ? d[weight_key] : 1);
|
||||
}
|
||||
else {
|
||||
assigned = 1;
|
||||
max_weight = d.count(weight_key) ? d[weight_key] : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
py::list nodes_list = py::list(G.attr("nbunch_iter")(nodes));
|
||||
py::list ret = py::list();
|
||||
for (int i = 0;i < py::len(nodes_list);i++) {
|
||||
node_t i_id = (G_.node_to_id[nodes_list[i]]).cast<node_t>();
|
||||
std::unordered_set<node_t> ipreds, isuccs;
|
||||
for (const auto& pair : G_.pred[i_id]) {
|
||||
ipreds.insert(pair.first);
|
||||
}
|
||||
ipreds.erase(i_id);
|
||||
for (const auto& pair : G_.adj[i_id]) {
|
||||
isuccs.insert(pair.first);
|
||||
}
|
||||
isuccs.erase(i_id);
|
||||
|
||||
weight_t directed_triangles = 0;
|
||||
for (const auto& j_id : ipreds) {
|
||||
for (const auto& k_pair : G_.pred[j_id]) {
|
||||
node_t k_id = k_pair.first;
|
||||
if (k_id == j_id) {
|
||||
continue;
|
||||
}// jpreds
|
||||
if (ipreds.count(k_id)) { // ipreds & jpreds
|
||||
directed_triangles += std::cbrt(wt(adj, j_id, i_id, weight_key, max_weight) * wt(adj, k_id, i_id, weight_key, max_weight) * wt(adj, k_id, j_id, weight_key, max_weight));
|
||||
}
|
||||
if (isuccs.count(k_id)) { // isuccs & jpreds
|
||||
directed_triangles += std::cbrt(wt(adj, j_id, i_id, weight_key, max_weight) * wt(adj, i_id, k_id, weight_key, max_weight) * wt(adj, k_id, j_id, weight_key, max_weight));
|
||||
}
|
||||
}
|
||||
for (const auto& k_pair : G_.adj[j_id]) {
|
||||
node_t k_id = k_pair.first;
|
||||
if (k_id == j_id) {
|
||||
continue;
|
||||
}// jsuccs
|
||||
if (ipreds.count(k_id)) { // ipreds & jsuccs
|
||||
directed_triangles += std::cbrt(wt(adj, j_id, i_id, weight_key, max_weight) * wt(adj, k_id, i_id, weight_key, max_weight) * wt(adj, j_id, k_id, weight_key, max_weight));
|
||||
}
|
||||
if (isuccs.count(k_id)) { // isuccs & jsuccs
|
||||
directed_triangles += std::cbrt(wt(adj, j_id, i_id, weight_key, max_weight) * wt(adj, i_id, k_id, weight_key, max_weight) * wt(adj, j_id, k_id, weight_key, max_weight));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& j_id : isuccs) {
|
||||
for (const auto& k_pair : G_.pred[j_id]) {
|
||||
node_t k_id = k_pair.first;
|
||||
if (k_id == j_id) {
|
||||
continue;
|
||||
}// jpreds
|
||||
if (ipreds.count(k_id)) { // ipreds & jpreds
|
||||
directed_triangles += std::cbrt(wt(adj, i_id, j_id, weight_key, max_weight) * wt(adj, k_id, i_id, weight_key, max_weight) * wt(adj, k_id, j_id, weight_key, max_weight));
|
||||
}
|
||||
if (isuccs.count(k_id)) { // isuccs & jpreds
|
||||
directed_triangles += std::cbrt(wt(adj, i_id, j_id, weight_key, max_weight) * wt(adj, i_id, k_id, weight_key, max_weight) * wt(adj, k_id, j_id, weight_key, max_weight));
|
||||
}
|
||||
}
|
||||
for (const auto& k_pair : G_.adj[j_id]) {
|
||||
node_t k_id = k_pair.first;
|
||||
if (k_id == j_id) {
|
||||
continue;
|
||||
}// jsuccs
|
||||
if (ipreds.count(k_id)) { // ipreds & jsuccs
|
||||
directed_triangles += std::cbrt(wt(adj, i_id, j_id, weight_key, max_weight) * wt(adj, k_id, i_id, weight_key, max_weight) * wt(adj, j_id, k_id, weight_key, max_weight));
|
||||
}
|
||||
if (isuccs.count(k_id)) { // isuccs & jsuccs
|
||||
directed_triangles += std::cbrt(wt(adj, i_id, j_id, weight_key, max_weight) * wt(adj, i_id, k_id, weight_key, max_weight) * wt(adj, j_id, k_id, weight_key, max_weight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int dtotal = ipreds.size() + isuccs.size();
|
||||
int dbidirectional = 0;
|
||||
for (const auto& node : ipreds) {
|
||||
dbidirectional += isuccs.count(node);
|
||||
}
|
||||
ret.append(py::make_tuple(nodes_list[i], dtotal, dbidirectional, directed_triangles));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
py::list _triangles_and_degree(py::object G, py::object nodes = py::none()) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
auto& adj = G_.adj;
|
||||
py::list nodes_list = py::list(G.attr("nbunch_iter")(nodes));
|
||||
py::list ret = py::list();
|
||||
for (int i = 0;i < py::len(nodes_list);i++) {
|
||||
node_t v = (G_.node_to_id[nodes_list[i]]).cast<node_t>();
|
||||
std::unordered_set<node_t> vs;
|
||||
for (const auto& pair : adj[v]) {
|
||||
vs.insert(pair.first);
|
||||
}
|
||||
vs.erase(v);
|
||||
weight_t ntriangles = 0;
|
||||
for (const auto& w : vs) {
|
||||
for (const auto& node : vs) {
|
||||
ntriangles += node != w && adj[w].count(node);
|
||||
}
|
||||
}
|
||||
ret.append(py::make_tuple(G_.id_to_node[py::cast(v)], vs.size(), ntriangles));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
py::list _directed_triangles_and_degree(py::object G, py::object nodes = py::none()) {
|
||||
DiGraph& G_ = G.cast<DiGraph&>();
|
||||
auto& adj = G_.adj;
|
||||
py::list nodes_list = py::list(G.attr("nbunch_iter")(nodes));
|
||||
py::list ret = py::list();
|
||||
for (int i = 0;i < py::len(nodes_list);i++) {
|
||||
node_t i_id = (G_.node_to_id[nodes_list[i]]).cast<node_t>();
|
||||
std::unordered_set<node_t> ipreds, isuccs;
|
||||
for (const auto& pair : G_.pred[i_id]) {
|
||||
ipreds.insert(pair.first);
|
||||
}
|
||||
ipreds.erase(i_id);
|
||||
for (const auto& pair : G_.adj[i_id]) {
|
||||
isuccs.insert(pair.first);
|
||||
}
|
||||
isuccs.erase(i_id);
|
||||
|
||||
weight_t directed_triangles = 0;
|
||||
for (const auto& j_id : ipreds) {
|
||||
for (const auto& k_pair : G_.pred[j_id]) {
|
||||
node_t k_id = k_pair.first;
|
||||
if (k_id == j_id) {
|
||||
continue;
|
||||
}// jpreds
|
||||
directed_triangles += ipreds.count(k_id); // ipreds & jpreds
|
||||
directed_triangles += isuccs.count(k_id); // isuccs & jpreds
|
||||
}
|
||||
for (const auto& k_pair : G_.adj[j_id]) {
|
||||
node_t k_id = k_pair.first;
|
||||
if (k_id == j_id) {
|
||||
continue;
|
||||
}// jsuccs
|
||||
directed_triangles += ipreds.count(k_id); // ipreds & jsuccs
|
||||
directed_triangles += isuccs.count(k_id); // isuccs & jsuccs
|
||||
}
|
||||
}
|
||||
for (const auto& j_id : isuccs) {
|
||||
for (const auto& k_pair : G_.pred[j_id]) {
|
||||
node_t k_id = k_pair.first;
|
||||
if (k_id == j_id) {
|
||||
continue;
|
||||
}// jpreds
|
||||
directed_triangles += ipreds.count(k_id); // ipreds & jpreds
|
||||
directed_triangles += isuccs.count(k_id); // isuccs & jpreds
|
||||
}
|
||||
for (const auto& k_pair : G_.adj[j_id]) {
|
||||
node_t k_id = k_pair.first;
|
||||
if (k_id == j_id) {
|
||||
continue;
|
||||
}// jsuccs
|
||||
directed_triangles += ipreds.count(k_id); // ipreds & jsuccs
|
||||
directed_triangles += isuccs.count(k_id); // isuccs & jsuccs
|
||||
}
|
||||
}
|
||||
|
||||
int dtotal = ipreds.size() + isuccs.size();
|
||||
int dbidirectional = 0;
|
||||
for (const auto& node : ipreds) {
|
||||
dbidirectional += isuccs.count(node);
|
||||
}
|
||||
ret.append(py::make_tuple(nodes_list[i], dtotal, dbidirectional, directed_triangles));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
py::object clustering(py::object G, py::object nodes, py::object weight) {
|
||||
py::dict clusterc = py::dict();
|
||||
if (G.attr("is_directed")().cast<bool>()) {
|
||||
py::list td_list;
|
||||
if (!weight.is_none()) {
|
||||
td_list = _directed_weighted_triangles_and_degree(G, nodes, weight);
|
||||
}
|
||||
else {
|
||||
td_list = _directed_triangles_and_degree(G, nodes);
|
||||
}
|
||||
for (int i = 0;i < py::len(td_list);i++) {
|
||||
py::tuple tuple = td_list[i].cast<py::tuple>();
|
||||
py::object v = tuple[0];
|
||||
int dt = tuple[1].cast<int>(), db = tuple[2].cast<int>();
|
||||
weight_t t = tuple[3].cast<weight_t>();
|
||||
if (t == 0) {
|
||||
clusterc[v] = 0;
|
||||
}
|
||||
else {
|
||||
clusterc[v] = t / ((dt * (dt - 1) - 2 * db) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
py::list td_list;
|
||||
if (!weight.is_none()) {
|
||||
td_list = _weighted_triangles_and_degree(G, nodes, weight);
|
||||
}
|
||||
else {
|
||||
td_list = _triangles_and_degree(G, nodes);
|
||||
}
|
||||
for (int i = 0;i < py::len(td_list);i++) {
|
||||
py::tuple tuple = td_list[i].cast<py::tuple>();
|
||||
py::object v = tuple[0];
|
||||
int d = tuple[1].cast<int>();
|
||||
weight_t t = tuple[2].cast<weight_t>();
|
||||
if (t == 0) {
|
||||
clusterc[v] = 0;
|
||||
}
|
||||
else {
|
||||
clusterc[v] = t / (d * (d - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (G.contains(nodes)) {
|
||||
return clusterc[nodes];
|
||||
}
|
||||
return clusterc;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../common/common.h"
|
||||
|
||||
py::object clustering(py::object G, py::object nodes, py::object weight);
|
||||
@@ -0,0 +1 @@
|
||||
#include "centrality.h"
|
||||
@@ -0,0 +1,467 @@
|
||||
#ifdef _OPENMP
|
||||
#include <omp.h>
|
||||
#endif
|
||||
#include <vector>
|
||||
#include <queue>
|
||||
#include <limits.h>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <cstdio>
|
||||
|
||||
#include "centrality.h"
|
||||
#ifdef EASYGRAPH_ENABLE_GPU
|
||||
#include <gpu_easygraph.h>
|
||||
#endif
|
||||
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../common/utils.h"
|
||||
#include "../../classes/linkgraph.h"
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
void betweenness_bfs_worker(
|
||||
const Graph_L& G_l, const int& S, std::vector<double>& bc, int cutoff, int endpoints_,
|
||||
std::vector<int>& q, std::vector<int>& dis, std::vector<int>& head_path, std::vector<int>& St,
|
||||
std::vector<long long>& count_path, std::vector<double>& delta, std::vector<LinkEdge>& E_path,
|
||||
std::vector<int>& stamp, int& cur_stamp
|
||||
) {
|
||||
int N = G_l.n;
|
||||
int edge_number_path = 0;
|
||||
int cnt_St = 0;
|
||||
++cur_stamp;
|
||||
if ((int)q.size() < N + 1)
|
||||
q.resize(N + 1);
|
||||
int front = 0, back = 0;
|
||||
int cutoff_int = (cutoff < 0) ? -1 : cutoff;
|
||||
|
||||
stamp[S] = cur_stamp;
|
||||
dis[S] = 0;
|
||||
count_path[S] = 1;
|
||||
delta[S] = 0.0;
|
||||
head_path[S] = 0;
|
||||
q[back++] = S;
|
||||
|
||||
const std::vector<int>& head = G_l.head;
|
||||
const std::vector<LinkEdge>& E = G_l.edges;
|
||||
|
||||
while (front < back) {
|
||||
int u = q[front++];
|
||||
int du = dis[u];
|
||||
if (cutoff_int >= 0 && du > cutoff_int)
|
||||
break;
|
||||
St[cnt_St++] = u;
|
||||
|
||||
for (int p = head[u]; p != -1; p = E[p].next) {
|
||||
int v = E[p].to;
|
||||
int new_dis = du + 1;
|
||||
if (cutoff_int >= 0 && new_dis > cutoff_int)
|
||||
continue;
|
||||
|
||||
if (stamp[v] != cur_stamp) {
|
||||
stamp[v] = cur_stamp;
|
||||
dis[v] = new_dis;
|
||||
count_path[v] = count_path[u];
|
||||
delta[v] = 0.0;
|
||||
head_path[v] = 0;
|
||||
q[back++] = v;
|
||||
E_path[++edge_number_path].next = head_path[v];
|
||||
E_path[edge_number_path].to = u;
|
||||
head_path[v] = edge_number_path;
|
||||
} else if (dis[v] == new_dis) {
|
||||
count_path[v] += count_path[u];
|
||||
E_path[++edge_number_path].next = head_path[v];
|
||||
E_path[edge_number_path].to = u;
|
||||
head_path[v] = edge_number_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (endpoints_)
|
||||
bc[S] += cnt_St - 1;
|
||||
|
||||
while (cnt_St > 0) {
|
||||
int u = St[--cnt_St];
|
||||
double cu = count_path[u];
|
||||
if (cu != 0) {
|
||||
double coeff = (1.0 + delta[u]) / cu;
|
||||
for (int p = head_path[u]; p; p = E_path[p].next) {
|
||||
int w = E_path[p].to;
|
||||
delta[w] += count_path[w] * coeff;
|
||||
}
|
||||
}
|
||||
if (u != S)
|
||||
bc[u] += delta[u] + endpoints_;
|
||||
}
|
||||
}
|
||||
|
||||
void betweenness_dijkstra_worker(
|
||||
const Graph_L& G_l, const int& S, std::vector<double>& bc, double cutoff,
|
||||
std::vector<int>& dis, std::vector<int>& head_path,
|
||||
std::vector<int>& St, std::vector<long long>& count_path, std::vector<double>& delta,
|
||||
std::vector<LinkEdge>& E_path, int endpoints_,
|
||||
std::vector<int>& stamp, int& cur_stamp
|
||||
) {
|
||||
const int dis_inf = 0x3f3f3f3f;
|
||||
|
||||
int N = G_l.n;
|
||||
int edge_number_path = 0;
|
||||
int cnt_St = 0;
|
||||
++cur_stamp;
|
||||
|
||||
stamp[S] = cur_stamp;
|
||||
dis[S] = 0;
|
||||
count_path[S] = 1;
|
||||
delta[S] = 0.0;
|
||||
head_path[S] = 0;
|
||||
|
||||
std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, std::greater<std::pair<int, int>>> pq;
|
||||
pq.push({0, S});
|
||||
|
||||
const std::vector<int>& head = G_l.head;
|
||||
const std::vector<LinkEdge>& E = G_l.edges;
|
||||
|
||||
while (!pq.empty()) {
|
||||
std::pair<int, int> top = pq.top();
|
||||
pq.pop();
|
||||
int d = top.first;
|
||||
int u = top.second;
|
||||
|
||||
if (d > dis[u]) continue;
|
||||
|
||||
if (cutoff >= 0 && d > cutoff) continue;
|
||||
|
||||
St[cnt_St++] = u;
|
||||
|
||||
for (int p = head[u]; p != -1; p = E[p].next) {
|
||||
int v = E[p].to;
|
||||
int w = E[p].w;
|
||||
int nd = dis[u] + w;
|
||||
|
||||
if (cutoff >= 0 && nd > cutoff) continue;
|
||||
|
||||
bool first_visit = (stamp[v] != cur_stamp);
|
||||
|
||||
if (first_visit || dis[v] > nd) {
|
||||
if (first_visit) {
|
||||
stamp[v] = cur_stamp;
|
||||
delta[v] = 0.0;
|
||||
}
|
||||
dis[v] = nd;
|
||||
count_path[v] = count_path[u];
|
||||
head_path[v] = 0;
|
||||
E_path[++edge_number_path].next = head_path[v];
|
||||
E_path[edge_number_path].to = u;
|
||||
head_path[v] = edge_number_path;
|
||||
|
||||
pq.push({nd, v});
|
||||
} else if (dis[v] == nd) {
|
||||
count_path[v] += count_path[u];
|
||||
E_path[++edge_number_path].next = head_path[v];
|
||||
E_path[edge_number_path].to = u;
|
||||
head_path[v] = edge_number_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (endpoints_)
|
||||
bc[S] += cnt_St - 1;
|
||||
|
||||
while (cnt_St > 0) {
|
||||
int u = St[--cnt_St];
|
||||
double cu = count_path[u];
|
||||
if (cu != 0) {
|
||||
double coeff = (1.0 + delta[u]) / cu;
|
||||
for (int p = head_path[u]; p; p = E_path[p].next) {
|
||||
int w = E_path[p].to;
|
||||
delta[w] += count_path[w] * coeff;
|
||||
}
|
||||
}
|
||||
if (u != S)
|
||||
bc[u] += delta[u] + endpoints_;
|
||||
}
|
||||
}
|
||||
|
||||
static double calc_scale(int len_V, int is_directed, int normalized, int endpoints) {
|
||||
double scale = 1.0;
|
||||
if (normalized) {
|
||||
if (endpoints) {
|
||||
if (len_V < 2) {
|
||||
scale = 1.0;
|
||||
} else {
|
||||
scale = 1.0 / (double(len_V) * (len_V - 1));
|
||||
}
|
||||
} else {
|
||||
if (len_V <= 2) {
|
||||
scale = 1.0;
|
||||
} else {
|
||||
scale = 1.0 / ((double(len_V) - 1) * (len_V - 2));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!is_directed) {
|
||||
scale = 0.5;
|
||||
} else {
|
||||
scale = 1.0;
|
||||
}
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
static py::object invoke_cpp_betweenness_centrality(
|
||||
py::object G, py::object weight, py::object cutoff, py::object sources,
|
||||
py::object normalized, py::object endpoints
|
||||
) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
int cutoff_ = -1;
|
||||
if (!cutoff.is_none()) {
|
||||
cutoff_ = cutoff.cast<int>();
|
||||
}
|
||||
int N = G_.node.size();
|
||||
bool is_directed = G.attr("is_directed")().cast<bool>();
|
||||
int normalized_ = normalized.cast<bool>();
|
||||
int endpoints_ = endpoints.cast<bool>();
|
||||
double scale = calc_scale(N, is_directed, normalized_, endpoints_);
|
||||
bool use_weights = !weight.is_none();
|
||||
std::string weight_key = "";
|
||||
if (use_weights) {
|
||||
weight_key = weight_to_string(weight);
|
||||
}
|
||||
|
||||
Graph_L G_l;
|
||||
if (G_.linkgraph_dirty) {
|
||||
G_l = graph_to_linkgraph(G_, is_directed, weight_key, false, false);
|
||||
G_.linkgraph_structure = G_l;
|
||||
} else {
|
||||
G_l = G_.linkgraph_structure;
|
||||
}
|
||||
|
||||
int edges_num = G_l.edges.size();
|
||||
std::vector<double> bc(N + 1, 0.0);
|
||||
std::vector<double> BC;
|
||||
int num_threads = 1;
|
||||
#ifdef _OPENMP
|
||||
num_threads = omp_get_max_threads();
|
||||
#endif
|
||||
|
||||
std::vector<std::vector<int>> dis_all(num_threads, std::vector<int>(N + 1));
|
||||
std::vector<std::vector<int>> head_path_all(num_threads, std::vector<int>(N + 1));
|
||||
std::vector<std::vector<int>> St_all(num_threads, std::vector<int>(N + 1));
|
||||
std::vector<std::vector<long long>> count_path_all(num_threads, std::vector<long long>(N + 1));
|
||||
std::vector<std::vector<double>> delta_all(num_threads, std::vector<double>(N + 1));
|
||||
std::vector<std::vector<LinkEdge>> E_path_all(num_threads, std::vector<LinkEdge>(edges_num + 1));
|
||||
|
||||
std::vector<std::vector<int>> queue_all(num_threads, std::vector<int>(N + 1));
|
||||
std::vector<std::vector<int>> stamp_all(num_threads, std::vector<int>(N + 1, 0));
|
||||
std::vector<int> cur_stamp_all(num_threads, 0);
|
||||
|
||||
std::vector<std::vector<double>> bc_local_all(num_threads, std::vector<double>(N + 1, 0.0));
|
||||
|
||||
if (!sources.is_none()) {
|
||||
py::list sources_list = py::list(sources);
|
||||
int sources_list_len = py::len(sources_list);
|
||||
std::vector<node_t> sources_vec;
|
||||
sources_vec.reserve(sources_list_len);
|
||||
for (int i = 0; i < sources_list_len; i++) {
|
||||
if (G_.node_to_id.attr("get")(sources_list[i], py::none()).is_none()) {
|
||||
printf("The node should exist in the graph!");
|
||||
return py::none();
|
||||
}
|
||||
sources_vec.push_back(G_.node_to_id.attr("get")(sources_list[i]).cast<node_t>());
|
||||
}
|
||||
|
||||
#ifdef _OPENMP
|
||||
#pragma omp parallel for schedule(dynamic)
|
||||
#endif
|
||||
for (int i = 0; i < sources_list_len; i++) {
|
||||
node_t source_id = sources_vec[i];
|
||||
#ifdef _OPENMP
|
||||
int tid = omp_get_thread_num();
|
||||
#else
|
||||
int tid = 0;
|
||||
#endif
|
||||
auto& bc_local = bc_local_all[tid];
|
||||
auto& dis = dis_all[tid];
|
||||
auto& head_path = head_path_all[tid];
|
||||
auto& St = St_all[tid];
|
||||
auto& count_path = count_path_all[tid];
|
||||
auto& delta = delta_all[tid];
|
||||
auto& E_path = E_path_all[tid];
|
||||
auto& q = queue_all[tid];
|
||||
auto& stamp = stamp_all[tid];
|
||||
int& cur_stamp = cur_stamp_all[tid];
|
||||
|
||||
if (use_weights) {
|
||||
betweenness_dijkstra_worker(
|
||||
G_l, source_id, bc_local, cutoff_, dis, head_path,
|
||||
St, count_path, delta, E_path, endpoints_, stamp, cur_stamp
|
||||
);
|
||||
} else {
|
||||
betweenness_bfs_worker(
|
||||
G_l, source_id, bc_local, cutoff_, endpoints_, q, dis, head_path,
|
||||
St, count_path, delta, E_path, stamp, cur_stamp
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
#ifdef _OPENMP
|
||||
#pragma omp parallel for schedule(dynamic)
|
||||
#endif
|
||||
for (int i = 1; i <= N; ++i) {
|
||||
#ifdef _OPENMP
|
||||
int tid = omp_get_thread_num();
|
||||
#else
|
||||
int tid = 0;
|
||||
#endif
|
||||
auto& bc_local = bc_local_all[tid];
|
||||
auto& dis = dis_all[tid];
|
||||
auto& head_path = head_path_all[tid];
|
||||
auto& St = St_all[tid];
|
||||
auto& count_path = count_path_all[tid];
|
||||
auto& delta = delta_all[tid];
|
||||
auto& E_path = E_path_all[tid];
|
||||
auto& q = queue_all[tid];
|
||||
auto& stamp = stamp_all[tid];
|
||||
int& cur_stamp = cur_stamp_all[tid];
|
||||
|
||||
if (use_weights) {
|
||||
betweenness_dijkstra_worker(
|
||||
G_l, i, bc_local, cutoff_, dis, head_path,
|
||||
St, count_path, delta, E_path, endpoints_, stamp, cur_stamp
|
||||
);
|
||||
} else {
|
||||
betweenness_bfs_worker(
|
||||
G_l, i, bc_local, cutoff_, endpoints_, q, dis, head_path,
|
||||
St, count_path, delta, E_path, stamp, cur_stamp
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _OPENMP
|
||||
#pragma omp parallel for schedule(static)
|
||||
for (int j = 1; j <= N; ++j) {
|
||||
double s = 0.0;
|
||||
for (int tid = 0; tid < num_threads; ++tid)
|
||||
s += bc_local_all[tid][j];
|
||||
bc[j] += s;
|
||||
}
|
||||
#else
|
||||
for (int j = 1; j <= N; ++j) {
|
||||
bc[j] += bc_local_all[0][j];
|
||||
}
|
||||
#endif
|
||||
|
||||
BC.reserve(N);
|
||||
for (int i = 1; i <= N; i++) {
|
||||
BC.push_back(scale * bc[i]);
|
||||
}
|
||||
|
||||
py::array::ShapeContainer ret_shape{(int)BC.size()};
|
||||
py::array_t<double> ret(ret_shape, BC.data());
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef EASYGRAPH_ENABLE_GPU
|
||||
static py::object invoke_gpu_betweenness_centrality(py::object G, py::object weight,
|
||||
py::object py_sources, py::object normalized, py::object endpoints) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
if (weight.is_none()) {
|
||||
G_.gen_CSR();
|
||||
} else {
|
||||
G_.gen_CSR(weight_to_string(weight));
|
||||
}
|
||||
auto csr_graph = G_.csr_graph;
|
||||
std::vector<int>& E = csr_graph->E;
|
||||
std::vector<int>& V = csr_graph->V;
|
||||
std::vector<double> *W_p = weight.is_none() ? &(csr_graph->unweighted_W)
|
||||
: csr_graph->W_map.find(weight_to_string(weight))->second.get();
|
||||
auto sources = G_.gen_CSR_sources(py_sources);
|
||||
std::vector<double> BC;
|
||||
bool is_directed = G.attr("is_directed")().cast<bool>();
|
||||
int gpu_r = gpu_easygraph::betweenness_centrality(V, E, *W_p, *sources,
|
||||
is_directed, normalized.cast<py::bool_>(),
|
||||
endpoints.cast<py::bool_>(), BC);
|
||||
if (gpu_r != gpu_easygraph::EG_GPU_SUCC) {
|
||||
// the code below will throw an exception
|
||||
py::pybind11_fail(gpu_easygraph::err_code_detail(gpu_r));
|
||||
}
|
||||
py::array::ShapeContainer ret_shape{(int)BC.size()};
|
||||
py::array_t<double> ret(ret_shape, BC.data());
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
py::object betweenness_centrality(py::object G, py::object weight, py::object cutoff, py::object sources,
|
||||
py::object normalized, py::object endpoints) {
|
||||
#ifdef EASYGRAPH_ENABLE_GPU
|
||||
return invoke_gpu_betweenness_centrality(G, weight, sources, normalized, endpoints);
|
||||
#else
|
||||
return invoke_cpp_betweenness_centrality(G, weight, cutoff, sources, normalized, endpoints);
|
||||
#endif
|
||||
}
|
||||
|
||||
// void betweenness_dijkstra(const Graph_L& G_l, const int &S, std::vector<double>& bc, double cutoff) {
|
||||
// int N = G_l.n;
|
||||
// int edge_number_path = 0;
|
||||
// __gnu_pbds::priority_queue<compare_node> q;
|
||||
// std::vector<double> dis(N+1, INFINITY);
|
||||
// std::vector<bool> vis(N+1, false);
|
||||
// std::vector<int> head_path(N+1, 0);
|
||||
// const std::vector<int>& head = G_l.head;
|
||||
// const std::vector<LinkEdge>& E = G_l.edges;
|
||||
// int edges_num = E.size();
|
||||
// std::vector<int> St(N+1, 0);
|
||||
// std::vector<long long> count_path(N+1, 0);
|
||||
// std::vector<double> delta(N+1, 0);
|
||||
// std::vector<LinkEdge> E_path(edges_num+1);
|
||||
// head_path[S] = 0;
|
||||
// dis[S] = 0;
|
||||
// count_path[S] = 1;
|
||||
// dis[S] = 0;
|
||||
// count_path[S] = 1;
|
||||
// q.push(compare_node(S, 0));
|
||||
// int cnt_St = 0;
|
||||
// while(!q.empty()) {
|
||||
// int u = q.top().x;
|
||||
// q.pop();
|
||||
// if (vis[u]){
|
||||
// continue;
|
||||
// }
|
||||
// if (cutoff >= 0 && dis[u] > cutoff){
|
||||
// continue;
|
||||
// }
|
||||
// St[cnt_St++] = u;
|
||||
// vis[u] = true;
|
||||
// for(int p = head[u]; p != -1; p = E[p].next) {
|
||||
// int v = E[p].to;
|
||||
// if(cutoff >= 0 && (dis[u] + E[p].w) > cutoff){
|
||||
// continue;
|
||||
// }
|
||||
// if (dis[v] > dis[u] + E[p].w) {
|
||||
// dis[v] = dis[u] + E[p].w;
|
||||
// q.push(compare_node(v, dis[v]));
|
||||
// count_path[v] = count_path[u];
|
||||
// head_path[v] = 0;
|
||||
// E_path[++edge_number_path].next = head_path[v];
|
||||
// E_path[edge_number_path].to = u;
|
||||
// head_path[v] = edge_number_path;
|
||||
// }
|
||||
// else if (dis[v] == dis[u] + E[p].w) {
|
||||
// count_path[v] += count_path[u];
|
||||
// E_path[++edge_number_path].next = head_path[v];
|
||||
// E_path[edge_number_path].to = u;
|
||||
// head_path[v] = edge_number_path;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// while (cnt_St > 0) {
|
||||
// int u = St[--cnt_St];
|
||||
// float coeff = (1.0 + delta[u]) / count_path[u];
|
||||
// for(int p = head_path[u]; p; p = E_path[p].next){
|
||||
// delta[E_path[p].to] += count_path[E_path[p].to] * coeff;
|
||||
// }
|
||||
// if (u != S)
|
||||
// bc[u] += delta[u];
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../common/common.h"
|
||||
|
||||
py::object closeness_centrality(py::object G, py::object weight, py::object cutoff, py::object sources);
|
||||
py::object betweenness_centrality(py::object G, py::object weight, py::object cutoff, py::object sources,
|
||||
py::object normalized, py::object endpoints);
|
||||
py::object cpp_katz_centrality(
|
||||
py::object G,
|
||||
py::object py_alpha,
|
||||
py::object py_beta,
|
||||
py::object py_max_iter,
|
||||
py::object py_tol,
|
||||
py::object py_normalized
|
||||
);
|
||||
|
||||
py::object degree_centrality(py::object G);
|
||||
py::object in_degree_centrality(py::object G);
|
||||
py::object out_degree_centrality(py::object G);
|
||||
|
||||
py::object cpp_eigenvector_centrality(
|
||||
py::object G,
|
||||
py::object py_max_iter,
|
||||
py::object py_tol,
|
||||
py::object py_nstart,
|
||||
py::object py_weight
|
||||
);
|
||||
@@ -0,0 +1,353 @@
|
||||
#include "centrality.h"
|
||||
|
||||
#ifdef EASYGRAPH_ENABLE_GPU
|
||||
#include <gpu_easygraph.h>
|
||||
#endif
|
||||
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../common/utils.h"
|
||||
#include "../../classes/linkgraph.h"
|
||||
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
#include <limits>
|
||||
#include <cmath>
|
||||
|
||||
#ifdef _OPENMP
|
||||
#include <omp.h>
|
||||
#endif
|
||||
|
||||
// Heap node: use negative value + max heap to implement min heap
|
||||
typedef std::pair<float, int> HeapNode;
|
||||
|
||||
// Optimized adjacency list cache
|
||||
struct FastAdjCache {
|
||||
std::vector<int*> neighbor_ptrs;
|
||||
std::vector<int> neighbor_counts;
|
||||
std::vector<float*> weight_ptrs;
|
||||
std::vector<std::vector<int>> neighbor_storage;
|
||||
std::vector<std::vector<float>> weight_storage;
|
||||
|
||||
void init(int N) {
|
||||
neighbor_ptrs.resize(N + 1, nullptr);
|
||||
neighbor_counts.resize(N + 1, 0);
|
||||
neighbor_storage.resize(N + 1);
|
||||
}
|
||||
|
||||
void init_with_weights(int N) {
|
||||
init(N);
|
||||
weight_ptrs.resize(N + 1, nullptr);
|
||||
weight_storage.resize(N + 1);
|
||||
}
|
||||
|
||||
inline void build_if_needed(int u, const std::vector<int>& head,
|
||||
const std::vector<LinkEdge>& edges, bool store_weights) {
|
||||
if (neighbor_ptrs[u] != nullptr) return;
|
||||
|
||||
std::vector<int>& neis = neighbor_storage[u];
|
||||
for (int p = head[u]; p != -1; p = edges[p].next) {
|
||||
neis.push_back(edges[p].to);
|
||||
if (store_weights) {
|
||||
weight_storage[u].push_back(edges[p].w);
|
||||
}
|
||||
}
|
||||
|
||||
neighbor_counts[u] = neis.size();
|
||||
neighbor_ptrs[u] = neis.data();
|
||||
if (store_weights) {
|
||||
weight_ptrs[u] = weight_storage[u].data();
|
||||
}
|
||||
}
|
||||
|
||||
inline int* get_neighbors_ptr(int u) const { return neighbor_ptrs[u]; }
|
||||
inline int get_neighbor_count(int u) const { return neighbor_counts[u]; }
|
||||
inline float* get_weights_ptr(int u) const { return weight_ptrs[u]; }
|
||||
};
|
||||
|
||||
// BFS implementation - directly use raw adjacency list
|
||||
double closeness_bfs_direct(const Graph_L& G_l, const int &S, int cutoff,
|
||||
std::vector<int>& already_counted,
|
||||
std::vector<int>& queue_storage,
|
||||
int timestamp) {
|
||||
int N = G_l.n;
|
||||
const std::vector<LinkEdge>& E = G_l.edges;
|
||||
const std::vector<int>& head = G_l.head;
|
||||
|
||||
int nodes_reached = 0;
|
||||
long long sum_dis = 0;
|
||||
|
||||
queue_storage.clear();
|
||||
|
||||
int queue_front = 0;
|
||||
already_counted[S] = timestamp;
|
||||
queue_storage.push_back(S);
|
||||
queue_storage.push_back(0);
|
||||
|
||||
while (queue_front < static_cast<int>(queue_storage.size())) {
|
||||
int u = queue_storage[queue_front++];
|
||||
int actdist = queue_storage[queue_front++];
|
||||
|
||||
if (cutoff >= 0 && actdist > cutoff) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sum_dis += actdist;
|
||||
nodes_reached++;
|
||||
|
||||
for (int p = head[u]; p != -1; p = E[p].next) {
|
||||
int v = E[p].to;
|
||||
|
||||
if (already_counted[v] == timestamp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
already_counted[v] = timestamp;
|
||||
queue_storage.push_back(v);
|
||||
queue_storage.push_back(actdist + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes_reached == 1)
|
||||
return 0.0;
|
||||
else
|
||||
return 1.0 * (nodes_reached - 1) * (nodes_reached - 1) / ((N - 1) * sum_dis);
|
||||
}
|
||||
|
||||
// Check if the graph is unweighted
|
||||
inline bool is_unweighted_graph(const Graph_L& G_l) {
|
||||
const std::vector<LinkEdge>& E = G_l.edges;
|
||||
for (const auto& edge : E) {
|
||||
if (std::abs(edge.w - 1.0f) > 1e-9) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dijkstra implementation - use on-demand adjacency cache
|
||||
double closeness_dijkstra_cached(const Graph_L& G_l, const int &S, int cutoff,
|
||||
std::vector<float>& dist,
|
||||
std::vector<int>& which,
|
||||
FastAdjCache& cache,
|
||||
int timestamp) {
|
||||
int N = G_l.n;
|
||||
const std::vector<LinkEdge>& E = G_l.edges;
|
||||
const std::vector<int>& head = G_l.head;
|
||||
|
||||
int nodes_reached = 0;
|
||||
double sum_dis = 0.0;
|
||||
|
||||
std::priority_queue<HeapNode> heap;
|
||||
|
||||
dist[S] = 1.0f;
|
||||
which[S] = timestamp;
|
||||
heap.push({-1.0f, S});
|
||||
|
||||
while (!heap.empty()) {
|
||||
HeapNode top = heap.top();
|
||||
heap.pop();
|
||||
float mindist = -top.first;
|
||||
int minnei = top.second;
|
||||
|
||||
if (mindist > dist[minnei]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float actual_dist = mindist - 1.0f;
|
||||
if (cutoff >= 0 && actual_dist > cutoff) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sum_dis += actual_dist;
|
||||
nodes_reached++;
|
||||
|
||||
cache.build_if_needed(minnei, head, E, true);
|
||||
|
||||
int* neis = cache.get_neighbors_ptr(minnei);
|
||||
float* ws = cache.get_weights_ptr(minnei);
|
||||
int nlen = cache.get_neighbor_count(minnei);
|
||||
|
||||
for (int j = 0; j < nlen; j++) {
|
||||
int to = neis[j];
|
||||
float altdist = mindist + ws[j];
|
||||
float curdist = dist[to];
|
||||
|
||||
if (which[to] != timestamp) {
|
||||
which[to] = timestamp;
|
||||
dist[to] = altdist;
|
||||
heap.push({-altdist, to});
|
||||
} else if (curdist == 0.0f || altdist < curdist) {
|
||||
dist[to] = altdist;
|
||||
heap.push({-altdist, to});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes_reached == 1)
|
||||
return 0.0;
|
||||
else
|
||||
return 1.0 * (nodes_reached - 1) * (nodes_reached - 1) / ((N - 1) * sum_dis);
|
||||
}
|
||||
|
||||
static py::object invoke_cpp_closeness_centrality(py::object G, py::object weight,
|
||||
py::object cutoff, py::object sources) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
int N = G_.node.size();
|
||||
bool is_directed = G.attr("is_directed")().cast<bool>();
|
||||
std::string weight_key = weight_to_string(weight);
|
||||
const Graph_L& G_l = graph_to_linkgraph(G_, is_directed, weight_key, false, false);
|
||||
int cutoff_ = -1;
|
||||
if (!cutoff.is_none()){
|
||||
cutoff_ = cutoff.cast<int>();
|
||||
}
|
||||
|
||||
// Auto algorithm selection
|
||||
bool use_bfs = (weight.is_none() || is_unweighted_graph(G_l));
|
||||
|
||||
std::vector<double> CC;
|
||||
|
||||
if(!sources.is_none()){
|
||||
py::list sources_list = py::list(sources);
|
||||
int sources_list_len = py::len(sources_list);
|
||||
CC.resize(sources_list_len);
|
||||
|
||||
// Collect all source node IDs
|
||||
std::vector<node_t> source_ids(sources_list_len);
|
||||
for(int i = 0; i < sources_list_len; i++){
|
||||
if(G_.node_to_id.attr("get")(sources_list[i],py::none()).is_none()){
|
||||
printf("The node should exist in the graph!");
|
||||
return py::none();
|
||||
}
|
||||
source_ids[i] = G_.node_to_id.attr("get")(sources_list[i]).cast<node_t>();
|
||||
}
|
||||
|
||||
// OpenMP parallel computation
|
||||
// Only enable parallelism when sources are many to avoid overhead
|
||||
#pragma omp parallel if(sources_list_len > 100)
|
||||
{
|
||||
// Per-thread data structures (avoid race conditions)
|
||||
std::vector<int> already_counted(N + 1, 0);
|
||||
std::vector<int> queue_storage;
|
||||
queue_storage.reserve(N * 2);
|
||||
|
||||
std::vector<float> dist(N + 1, 0.0f);
|
||||
std::vector<int> which(N + 1, 0);
|
||||
|
||||
FastAdjCache cache;
|
||||
if (!use_bfs) {
|
||||
cache.init_with_weights(N);
|
||||
}
|
||||
|
||||
// Assign unique timestamp start for each thread
|
||||
#ifdef _OPENMP
|
||||
int thread_id = omp_get_thread_num();
|
||||
int num_threads = omp_get_num_threads();
|
||||
int timestamp = thread_id * sources_list_len;
|
||||
#else
|
||||
int timestamp = 0;
|
||||
#endif
|
||||
|
||||
// Parallel loop: each thread handles different source node
|
||||
#pragma omp for schedule(dynamic, 1)
|
||||
for(int i = 0; i < sources_list_len; i++){
|
||||
timestamp++;
|
||||
double res;
|
||||
if (use_bfs) {
|
||||
res = closeness_bfs_direct(G_l, source_ids[i], cutoff_,
|
||||
already_counted, queue_storage, timestamp);
|
||||
} else {
|
||||
res = closeness_dijkstra_cached(G_l, source_ids[i], cutoff_,
|
||||
dist, which, cache, timestamp);
|
||||
}
|
||||
CC[i] = res;
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
CC.resize(N);
|
||||
|
||||
// OpenMP parallel computation for all nodes
|
||||
// Only enable parallelism when node count is large
|
||||
#pragma omp parallel if(N > 100)
|
||||
{
|
||||
// Per-thread data structures
|
||||
std::vector<int> already_counted(N + 1, 0);
|
||||
std::vector<int> queue_storage;
|
||||
queue_storage.reserve(N * 2);
|
||||
|
||||
std::vector<float> dist(N + 1, 0.0f);
|
||||
std::vector<int> which(N + 1, 0);
|
||||
|
||||
FastAdjCache cache;
|
||||
if (!use_bfs) {
|
||||
cache.init_with_weights(N);
|
||||
}
|
||||
|
||||
// Assign unique timestamp start for each thread
|
||||
#ifdef _OPENMP
|
||||
int thread_id = omp_get_thread_num();
|
||||
int num_threads = omp_get_num_threads();
|
||||
int timestamp = thread_id * N;
|
||||
#else
|
||||
int timestamp = 0;
|
||||
#endif
|
||||
|
||||
// Parallel loop: dynamic scheduling for load balancing
|
||||
#pragma omp for schedule(dynamic, 10)
|
||||
for(int i = 1; i <= N; i++){
|
||||
timestamp++;
|
||||
double res;
|
||||
if (use_bfs) {
|
||||
res = closeness_bfs_direct(G_l, i, cutoff_,
|
||||
already_counted, queue_storage, timestamp);
|
||||
} else {
|
||||
res = closeness_dijkstra_cached(G_l, i, cutoff_,
|
||||
dist, which, cache, timestamp);
|
||||
}
|
||||
CC[i - 1] = res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
py::array::ShapeContainer ret_shape{(int)CC.size()};
|
||||
py::array_t<double> ret(ret_shape, CC.data());
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef EASYGRAPH_ENABLE_GPU
|
||||
static py::object invoke_gpu_closeness_centrality(py::object G, py::object weight, py::object py_sources) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
if (weight.is_none()) {
|
||||
G_.gen_CSR();
|
||||
} else {
|
||||
G_.gen_CSR(weight_to_string(weight));
|
||||
}
|
||||
auto csr_graph = G_.csr_graph;
|
||||
std::vector<int>& E = csr_graph->E;
|
||||
std::vector<int>& V = csr_graph->V;
|
||||
std::vector<double> *W_p = weight.is_none() ? &(csr_graph->unweighted_W)
|
||||
: csr_graph->W_map.find(weight_to_string(weight))->second.get();
|
||||
auto sources = G_.gen_CSR_sources(py_sources);
|
||||
std::vector<double> CC;
|
||||
int gpu_r = gpu_easygraph::closeness_centrality(V, E, *W_p, *sources, CC);
|
||||
|
||||
if (gpu_r != gpu_easygraph::EG_GPU_SUCC) {
|
||||
// the code below will throw an exception
|
||||
py::pybind11_fail(gpu_easygraph::err_code_detail(gpu_r));
|
||||
}
|
||||
|
||||
py::array::ShapeContainer ret_shape{(int)CC.size()};
|
||||
py::array_t<double> ret(ret_shape, CC.data());
|
||||
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
py::object closeness_centrality(py::object G, py::object weight, py::object cutoff, py::object sources) {
|
||||
#ifdef EASYGRAPH_ENABLE_GPU
|
||||
return invoke_gpu_closeness_centrality(G, weight, sources);
|
||||
#else
|
||||
return invoke_cpp_closeness_centrality(G, weight, cutoff, sources);
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
#include "centrality.h"
|
||||
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../classes/directed_graph.h"
|
||||
#include "../../common/utils.h"
|
||||
#include "../../classes/linkgraph.h"
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
py::object degree_centrality(
|
||||
py::object G
|
||||
) {
|
||||
Graph* graph = G.cast<Graph*>();
|
||||
py::dict centrality_map = py::dict();
|
||||
py::object nodes = graph->get_nodes();
|
||||
int n = py::len(nodes);
|
||||
if (n <= 1) {
|
||||
for (const auto& node_handle : nodes) {
|
||||
centrality_map[node_handle] = 0.0;
|
||||
}
|
||||
return centrality_map;
|
||||
}
|
||||
|
||||
double scale = 1.0 / (n - 1);
|
||||
|
||||
std::string class_name = G.attr("__class__").attr("__name__").cast<std::string>();
|
||||
|
||||
if (class_name == "DiGraphC") {
|
||||
// 有向图 (DiGraph)
|
||||
DiGraph* digraph = G.cast<DiGraph*>();
|
||||
py::object adj = digraph->get_adj();
|
||||
py::object pred = digraph->get_pred();
|
||||
|
||||
for (const auto& node_handle : nodes) {
|
||||
int out_deg = py::len(adj[node_handle]);
|
||||
int in_deg = py::len(pred[node_handle]);
|
||||
centrality_map[node_handle] = (double)(out_deg + in_deg) * scale;
|
||||
}
|
||||
} else {
|
||||
py::object adj = graph->get_adj();
|
||||
for (const auto& node_handle : nodes) {
|
||||
int degree = py::len(adj[node_handle]);
|
||||
centrality_map[node_handle] = (double)degree * scale;
|
||||
}
|
||||
}
|
||||
return centrality_map;
|
||||
}
|
||||
|
||||
|
||||
py::object in_degree_centrality(
|
||||
py::object G
|
||||
) {
|
||||
DiGraph* graph = G.cast<DiGraph*>();
|
||||
py::dict centrality_map = py::dict();
|
||||
|
||||
py::object nodes = graph->get_nodes();
|
||||
int n = py::len(nodes);
|
||||
|
||||
if (n <= 1) {
|
||||
return centrality_map;
|
||||
}
|
||||
|
||||
double scale = 1.0 / (n - 1);
|
||||
|
||||
py::object pred = graph->get_pred();
|
||||
|
||||
for (const auto& node_handle : nodes) {
|
||||
int in_degree = py::len(pred[node_handle]);
|
||||
centrality_map[node_handle] = in_degree * scale;
|
||||
}
|
||||
return centrality_map;
|
||||
}
|
||||
|
||||
|
||||
py::object out_degree_centrality(
|
||||
py::object G
|
||||
) {
|
||||
Graph* graph = G.cast<Graph*>();
|
||||
py::dict centrality_map = py::dict();
|
||||
|
||||
py::object nodes = graph->get_nodes();
|
||||
int n = py::len(nodes);
|
||||
|
||||
if (n <= 1) {
|
||||
return centrality_map;
|
||||
}
|
||||
double scale = 1.0 / (n - 1);
|
||||
|
||||
py::object adj = graph->get_adj();
|
||||
|
||||
for (const auto& node_handle : nodes) {
|
||||
int out_degree = py::len(adj[node_handle]);
|
||||
centrality_map[node_handle] = out_degree * scale;
|
||||
}
|
||||
return centrality_map;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
#include <pybind11/numpy.h>
|
||||
|
||||
#ifdef _OPENMP
|
||||
#include <omp.h>
|
||||
#endif
|
||||
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../common/utils.h"
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
class CSRMatrix {
|
||||
public:
|
||||
std::vector<int> indptr;
|
||||
std::vector<int> indices;
|
||||
std::vector<double> data; // Empty if unweighted (all 1.0)
|
||||
int rows, cols;
|
||||
bool is_weighted;
|
||||
|
||||
CSRMatrix(int r, int c) : rows(r), cols(c), is_weighted(false) {
|
||||
indptr.assign(r + 1, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// Power iteration with branch optimization for weighted/unweighted paths
|
||||
std::vector<double> power_iteration_optimized(
|
||||
const CSRMatrix& A,
|
||||
int max_iter,
|
||||
double tol,
|
||||
std::vector<double>& x
|
||||
) {
|
||||
const int n = A.rows;
|
||||
std::vector<double> x_next(n);
|
||||
bool use_weight = A.is_weighted && !A.data.empty();
|
||||
|
||||
// Initial normalization
|
||||
double norm = 0.0;
|
||||
#pragma omp parallel for reduction(+:norm)
|
||||
for (int i = 0; i < n; ++i) norm += x[i] * x[i];
|
||||
norm = std::sqrt(norm);
|
||||
|
||||
if (norm < 1e-12) {
|
||||
std::fill(x.begin(), x.end(), 1.0 / std::sqrt(n));
|
||||
} else {
|
||||
double inv_norm = 1.0 / norm;
|
||||
#pragma omp parallel for
|
||||
for (int i = 0; i < n; ++i) x[i] *= inv_norm;
|
||||
}
|
||||
|
||||
double delta = tol + 1.0;
|
||||
for (int iter = 0; iter < max_iter && delta >= tol; ++iter) {
|
||||
double next_norm_sq = 0.0;
|
||||
|
||||
#pragma omp parallel for reduction(+:next_norm_sq) schedule(dynamic, 64)
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double sum = 0.0;
|
||||
const int start = A.indptr[i];
|
||||
const int end = A.indptr[i+1];
|
||||
|
||||
if (use_weight) {
|
||||
for (int j = start; j < end; ++j) {
|
||||
sum += A.data[j] * x[A.indices[j]];
|
||||
}
|
||||
} else {
|
||||
for (int j = start; j < end; ++j) {
|
||||
sum += x[A.indices[j]];
|
||||
}
|
||||
}
|
||||
|
||||
x_next[i] = sum;
|
||||
next_norm_sq += sum * sum;
|
||||
}
|
||||
|
||||
double next_norm = std::sqrt(next_norm_sq);
|
||||
if (next_norm < 1e-12) break;
|
||||
|
||||
double inv_next_norm = 1.0 / next_norm;
|
||||
delta = 0.0;
|
||||
|
||||
#pragma omp parallel for reduction(+:delta) schedule(static)
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double val = x_next[i] * inv_next_norm;
|
||||
delta += std::abs(val - x[i]);
|
||||
x_next[i] = val;
|
||||
}
|
||||
x.swap(x_next);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
// Build transpose CSR with fallback logic for missing weight keys
|
||||
CSRMatrix build_transpose_matrix_smart(Graph& graph, const std::vector<node_t>& nodes, const std::string& weight_key) {
|
||||
std::shared_ptr<CSRGraph> csr_ptr = weight_key.empty() ? graph.gen_CSR() : graph.gen_CSR(weight_key);
|
||||
|
||||
int n = static_cast<int>(nodes.size());
|
||||
CSRMatrix At(n, n);
|
||||
if (!csr_ptr) return At;
|
||||
|
||||
const auto& src_indptr = csr_ptr->V;
|
||||
const auto& src_indices = csr_ptr->E;
|
||||
std::vector<double> src_data;
|
||||
bool actually_weighted = false;
|
||||
|
||||
// Detect if weighted calculation is required
|
||||
if (!weight_key.empty()) {
|
||||
auto it = csr_ptr->W_map.find(weight_key);
|
||||
if (it != csr_ptr->W_map.end() && it->second) {
|
||||
src_data = *(it->second);
|
||||
for (double w : src_data) {
|
||||
if (std::abs(w - 1.0) > 1e-9) {
|
||||
actually_weighted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
At.is_weighted = actually_weighted;
|
||||
|
||||
// Calculate row counts for transpose
|
||||
for (int x_idx : src_indices) {
|
||||
if (x_idx >= 0 && x_idx < n) At.indptr[x_idx + 1]++;
|
||||
}
|
||||
for (int i = 0; i < n; ++i) At.indptr[i + 1] += At.indptr[i];
|
||||
|
||||
At.indices.resize(src_indices.size());
|
||||
if (actually_weighted) At.data.resize(src_indices.size());
|
||||
|
||||
std::vector<int> cur_pos(At.indptr.begin(), At.indptr.end());
|
||||
|
||||
// Populate transpose CSR data
|
||||
for (int r = 0; r < n; ++r) {
|
||||
for (int p = src_indptr[r]; p < src_indptr[r+1]; ++p) {
|
||||
int c = src_indices[p];
|
||||
if (c < 0 || c >= n) continue;
|
||||
int dest = cur_pos[c]++;
|
||||
At.indices[dest] = r;
|
||||
if (actually_weighted) At.data[dest] = src_data[p];
|
||||
}
|
||||
}
|
||||
return At;
|
||||
}
|
||||
|
||||
py::object cpp_eigenvector_centrality(
|
||||
py::object G,
|
||||
py::object py_max_iter,
|
||||
py::object py_tol,
|
||||
py::object py_nstart,
|
||||
py::object py_weight
|
||||
) {
|
||||
try {
|
||||
Graph& graph = G.cast<Graph&>();
|
||||
int max_iter = py_max_iter.cast<int>();
|
||||
double tol = py_tol.cast<double>();
|
||||
std::string weight_key = py_weight.is_none() ? "" : py_weight.cast<std::string>();
|
||||
|
||||
if (graph.node.empty()) return py::dict();
|
||||
|
||||
std::vector<node_t> nodes;
|
||||
for (auto& pair : graph.node) nodes.push_back(pair.first);
|
||||
int n = nodes.size();
|
||||
|
||||
CSRMatrix A_transpose = build_transpose_matrix_smart(graph, nodes, weight_key);
|
||||
|
||||
// Initialize x vector (prefer degree-based or uniform)
|
||||
std::vector<double> x(n, 1.0 / n);
|
||||
if (!py_nstart.is_none()) {
|
||||
py::dict nstart = py_nstart.cast<py::dict>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
py::object node_obj = graph.id_to_node[py::cast(nodes[i])];
|
||||
if (nstart.contains(node_obj)) x[i] = nstart[node_obj].cast<double>();
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < n; i++) {
|
||||
int degree = A_transpose.indptr[i+1] - A_transpose.indptr[i];
|
||||
x[i] = (degree > 0) ? (double)degree : 1.0/n;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<double> res = power_iteration_optimized(A_transpose, max_iter, tol, x);
|
||||
|
||||
py::dict result;
|
||||
for (int i = 0; i < n; i++) {
|
||||
py::object node_obj = graph.id_to_node[py::cast(nodes[i])];
|
||||
result[node_obj] = res[i];
|
||||
}
|
||||
return result;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
throw std::runtime_error(std::string("C++ Eigenvector Error: ") + e.what());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
#ifdef _OPENMP
|
||||
#include <omp.h>
|
||||
#endif
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
#include "centrality.h"
|
||||
#include "../../classes/graph.h"
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
class CSRMatrix {
|
||||
public:
|
||||
std::vector<int> indptr; // size rows+1
|
||||
std::vector<int> indices; // size nnz
|
||||
std::vector<double> data; // size nnz
|
||||
int rows = 0;
|
||||
int cols = 0;
|
||||
|
||||
CSRMatrix() = default;
|
||||
CSRMatrix(int r, int c) : rows(r), cols(c) {
|
||||
indptr.assign(r + 1, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// Build transpose CSR from EasyGraph CSR so that row i contains in-neighbors of i.
|
||||
static CSRMatrix build_transpose_matrix_from_csr(const std::shared_ptr<CSRGraph>& csr_ptr) {
|
||||
if (!csr_ptr) return CSRMatrix();
|
||||
|
||||
const int n = static_cast<int>(csr_ptr->nodes.size());
|
||||
if (n == 0) return CSRMatrix(0, 0);
|
||||
|
||||
const auto& src_indptr = csr_ptr->V;
|
||||
const auto& src_indices = csr_ptr->E;
|
||||
|
||||
// Unweighted: all ones.
|
||||
std::vector<double> src_data(src_indices.size(), 1.0);
|
||||
|
||||
CSRMatrix At(n, n);
|
||||
|
||||
// Count nnz per column in the source (becomes nnz per row in transpose).
|
||||
for (int c : src_indices) {
|
||||
if (c >= 0 && c < n) At.indptr[c + 1]++;
|
||||
}
|
||||
|
||||
// Prefix sum.
|
||||
for (int i = 0; i < n; ++i) {
|
||||
At.indptr[i + 1] += At.indptr[i];
|
||||
}
|
||||
|
||||
const int nnz = static_cast<int>(src_indices.size());
|
||||
At.indices.resize(nnz);
|
||||
At.data.resize(nnz);
|
||||
|
||||
std::vector<int> cur_pos(At.indptr.begin(), At.indptr.end());
|
||||
|
||||
// Fill transpose.
|
||||
for (int r = 0; r < n; ++r) {
|
||||
const int start = src_indptr[r];
|
||||
const int end = src_indptr[r + 1];
|
||||
for (int p = start; p < end; ++p) {
|
||||
const int c = src_indices[p];
|
||||
if (c < 0 || c >= n) continue;
|
||||
const int dest = cur_pos[c]++;
|
||||
At.indices[dest] = r;
|
||||
At.data[dest] = src_data[p];
|
||||
}
|
||||
}
|
||||
|
||||
return At;
|
||||
}
|
||||
|
||||
static std::vector<double> katz_centrality_omp(const CSRMatrix& A,
|
||||
double alpha,
|
||||
const std::vector<double>& beta,
|
||||
int max_iters,
|
||||
double tol,
|
||||
bool normalize) {
|
||||
const int n = A.rows;
|
||||
std::vector<double> x(n, 1.0); // initial guess
|
||||
std::vector<double> x_next(n, 0.0); // next iterate
|
||||
if (n == 0) return x;
|
||||
|
||||
for (int iter = 0; iter < max_iters; ++iter) {
|
||||
double err_sq = 0.0;
|
||||
double norm_sq = 0.0;
|
||||
|
||||
// SpMV + Katz update + error and norm in ONE pass
|
||||
#pragma omp parallel for reduction(+ : err_sq, norm_sq) schedule(static)
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double sum = 0.0;
|
||||
const int row_start = A.indptr[i];
|
||||
const int row_end = A.indptr[i + 1];
|
||||
|
||||
for (int e = row_start; e < row_end; ++e) {
|
||||
sum += A.data[e] * x[A.indices[e]];
|
||||
}
|
||||
|
||||
const double new_val = alpha * sum + beta[i];
|
||||
const double diff = new_val - x[i];
|
||||
|
||||
x_next[i] = new_val;
|
||||
err_sq += diff * diff;
|
||||
norm_sq += new_val * new_val;
|
||||
}
|
||||
|
||||
const double err = std::sqrt(err_sq);
|
||||
const double norm = std::sqrt(norm_sq);
|
||||
|
||||
x.swap(x_next);
|
||||
|
||||
if (norm > 0.0 && (err / norm) < tol) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (normalize) {
|
||||
double norm_sq2 = 0.0;
|
||||
#pragma omp parallel for reduction(+ : norm_sq2) schedule(static)
|
||||
for (int i = 0; i < n; ++i) {
|
||||
norm_sq2 += x[i] * x[i];
|
||||
}
|
||||
const double norm = std::sqrt(norm_sq2);
|
||||
if (norm > 0.0) {
|
||||
#pragma omp parallel for schedule(static)
|
||||
for (int i = 0; i < n; ++i) {
|
||||
x[i] /= norm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
py::object cpp_katz_centrality(py::object G,
|
||||
py::object py_alpha,
|
||||
py::object py_beta,
|
||||
py::object py_max_iter,
|
||||
py::object py_tol,
|
||||
py::object py_normalized) {
|
||||
Graph& graph = G.cast<Graph&>();
|
||||
|
||||
const double alpha = py_alpha.cast<double>();
|
||||
const int max_iter = py_max_iter.cast<int>();
|
||||
const double tol = py_tol.cast<double>();
|
||||
const bool normalized = py_normalized.cast<bool>();
|
||||
|
||||
std::shared_ptr<CSRGraph> csr_ptr = graph.gen_CSR();
|
||||
if (!csr_ptr || csr_ptr->nodes.empty()) {
|
||||
return py::dict();
|
||||
}
|
||||
|
||||
const int n = static_cast<int>(csr_ptr->nodes.size());
|
||||
|
||||
// Build transpose CSR so that we accumulate from in-neighbors.
|
||||
CSRMatrix A = build_transpose_matrix_from_csr(csr_ptr);
|
||||
|
||||
// Process beta parameter: scalar or dict(node->beta).
|
||||
std::vector<double> beta(n, 1.0);
|
||||
if (py::isinstance<py::float_>(py_beta) || py::isinstance<py::int_>(py_beta)) {
|
||||
const double beta_val = py_beta.cast<double>();
|
||||
#pragma omp parallel for schedule(static)
|
||||
for (int i = 0; i < n; ++i) {
|
||||
beta[i] = beta_val;
|
||||
}
|
||||
} else if (py::isinstance<py::dict>(py_beta)) {
|
||||
py::dict beta_dict = py_beta.cast<py::dict>();
|
||||
for (int i = 0; i < n; ++i) {
|
||||
node_t internal_id = csr_ptr->nodes[i];
|
||||
py::object node_obj = graph.id_to_node[py::cast(internal_id)];
|
||||
if (beta_dict.contains(node_obj)) {
|
||||
beta[i] = beta_dict[node_obj].cast<double>();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw py::type_error("beta must be a float/int or a dict");
|
||||
}
|
||||
|
||||
std::vector<double> scores = katz_centrality_omp(A, alpha, beta, max_iter, tol, normalized);
|
||||
|
||||
// Prepare results
|
||||
py::dict result;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
node_t internal_id = csr_ptr->nodes[i];
|
||||
py::object node_obj = graph.id_to_node[py::cast(internal_id)];
|
||||
result[node_obj] = scores[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <queue>
|
||||
#include <cstdint>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../classes/linkgraph.h"
|
||||
#include "../../common/utils.h"
|
||||
|
||||
namespace py = pybind11;
|
||||
using namespace std;
|
||||
|
||||
py::object cpp_LPA(py::object G) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
Graph_L linkgraph = G_._get_linkgraph_structure();
|
||||
int n = linkgraph.n;
|
||||
|
||||
py::dict id_to_node = G_.id_to_node;
|
||||
|
||||
if (n <= 1) {
|
||||
py::dict result;
|
||||
if (n == 1) {
|
||||
py::list node_list;
|
||||
node_list.append(id_to_node[py::cast(1)]);
|
||||
result[py::cast(1)] = node_list;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
vector<vector<int>> adj(n + 1);
|
||||
for (int u = 1; u <= n; ++u) {
|
||||
for (int e = linkgraph.head[u]; e != -1; e = linkgraph.edges[e].next) {
|
||||
int v = linkgraph.edges[e].to;
|
||||
if (u != v) {
|
||||
adj[u].push_back(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int u = 1; u <= n; ++u) {
|
||||
if (adj[u].size() > 1) {
|
||||
sort(adj[u].begin(), adj[u].end());
|
||||
adj[u].erase(unique(adj[u].begin(), adj[u].end()), adj[u].end());
|
||||
}
|
||||
}
|
||||
|
||||
vector<int> labels(n);
|
||||
vector<int> nodes(n);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
labels[i] = i;
|
||||
nodes[i] = i + 1;
|
||||
}
|
||||
|
||||
random_device rd;
|
||||
mt19937 gen(rd());
|
||||
|
||||
const int MAX_ITERATIONS = 1000;
|
||||
int iteration_count = 0;
|
||||
|
||||
vector<int> label_count(n, 0);
|
||||
vector<int> active_labels;
|
||||
active_labels.reserve(128);
|
||||
|
||||
while (iteration_count < MAX_ITERATIONS) {
|
||||
iteration_count++;
|
||||
|
||||
shuffle(nodes.begin(), nodes.end(), gen);
|
||||
|
||||
bool changed = false;
|
||||
for (int node : nodes) {
|
||||
int max_count = 0;
|
||||
|
||||
for (int neighbor : adj[node]) {
|
||||
int neighbor_label = labels[neighbor - 1];
|
||||
if (label_count[neighbor_label] == 0) {
|
||||
active_labels.push_back(neighbor_label);
|
||||
}
|
||||
label_count[neighbor_label]++;
|
||||
if (label_count[neighbor_label] > max_count) {
|
||||
max_count = label_count[neighbor_label];
|
||||
}
|
||||
}
|
||||
|
||||
if (max_count > 0) {
|
||||
int current_label = labels[node - 1];
|
||||
|
||||
vector<int> best_labels;
|
||||
for (int label : active_labels) {
|
||||
if (label_count[label] == max_count) {
|
||||
best_labels.push_back(label);
|
||||
}
|
||||
}
|
||||
|
||||
bool current_is_best = false;
|
||||
for (int label : best_labels) {
|
||||
if (label == current_label) {
|
||||
current_is_best = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!current_is_best) {
|
||||
changed = true;
|
||||
|
||||
if (best_labels.size() == 1) {
|
||||
labels[node - 1] = best_labels[0];
|
||||
} else {
|
||||
uniform_int_distribution<> dis(0, best_labels.size() - 1);
|
||||
labels[node - 1] = best_labels[dis(gen)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int label : active_labels) {
|
||||
label_count[label] = 0;
|
||||
}
|
||||
active_labels.clear();
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
unordered_map<int, vector<int>> label_to_nodes;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
int label = labels[i];
|
||||
label_to_nodes[label].push_back(i + 1);
|
||||
}
|
||||
|
||||
py::dict result;
|
||||
int community_id = 1;
|
||||
for (const auto& pair : label_to_nodes) {
|
||||
py::list node_list;
|
||||
for (int internal_id : pair.second) {
|
||||
node_list.append(id_to_node[py::cast(internal_id)]);
|
||||
}
|
||||
result[py::cast(community_id)] = node_list;
|
||||
community_id++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
py::object cpp_LPA(py::object G);
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "modularity.h"
|
||||
#include "greedy_modularity.h"
|
||||
#include "motif.h"
|
||||
#include "louvain.h"
|
||||
#include "LPA.h"
|
||||
#include "ego_graph.h"
|
||||
#include "graph_coloring.h"
|
||||
#include "localsearch.h"
|
||||
@@ -0,0 +1,155 @@
|
||||
#include "ego_graph.h"
|
||||
#include "subgraph.h"
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../classes/directed_graph.h"
|
||||
#include "../../classes/csr_graph.h"
|
||||
#include "../../common/utils.h"
|
||||
#include "../../classes/linkgraph.h"
|
||||
#include "../path/path.h"
|
||||
#include <algorithm>
|
||||
|
||||
|
||||
struct _EgoGraphCore {
|
||||
Graph_L G_l;
|
||||
std::vector<node_t> node_ids;
|
||||
std::unordered_map<node_t, int> node_to_idx;
|
||||
py::dict id_to_node_py;
|
||||
bool has_error = false;
|
||||
};
|
||||
|
||||
|
||||
static _EgoGraphCore _cpp_ego_graph_compute_impl(
|
||||
Graph& G_,
|
||||
py::object n,
|
||||
double radius_val,
|
||||
bool center_val,
|
||||
bool undirected_val,
|
||||
bool is_directed,
|
||||
const std::string& weight_key) {
|
||||
|
||||
_EgoGraphCore out;
|
||||
|
||||
if (G_.node_to_id.contains(n) == 0) {
|
||||
PyErr_Format(PyExc_KeyError, "Node %R is not in the graph.", n.ptr());
|
||||
out.has_error = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
node_t center_id = G_.node_to_id[n].cast<node_t>();
|
||||
out.id_to_node_py = G_.id_to_node;
|
||||
|
||||
if (G_.linkgraph_dirty || G_.linkgraph_structure.max_deg == -1) {
|
||||
if (undirected_val) {
|
||||
out.G_l = graph_to_linkgraph(G_, false, weight_key, true, false);
|
||||
} else {
|
||||
out.G_l = graph_to_linkgraph(G_, is_directed, weight_key, true, false);
|
||||
}
|
||||
G_.linkgraph_dirty = false;
|
||||
G_.linkgraph_structure = out.G_l; // also cache the freshly built linkgraph
|
||||
} else {
|
||||
out.G_l = G_.linkgraph_structure;
|
||||
}
|
||||
|
||||
std::vector<double> dist = _dijkstra(out.G_l, center_id, -1);
|
||||
int N = out.G_l.n;
|
||||
for (int i = 1; i <= N; i++) {
|
||||
if (dist[i] > radius_val) continue;
|
||||
if (!center_val && i == (int)center_id) continue;
|
||||
out.node_to_idx[i] = (int)out.node_ids.size();
|
||||
out.node_ids.push_back(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
static _EgoGraphCore _cpp_ego_graph_compute(
|
||||
py::object G,
|
||||
py::object n,
|
||||
py::object radius,
|
||||
py::object center,
|
||||
py::object undirected,
|
||||
py::object distance) {
|
||||
|
||||
bool is_directed = G.attr("is_directed")().cast<bool>();
|
||||
bool center_val = center.cast<bool>();
|
||||
bool undirected_val = undirected.cast<bool>();
|
||||
double radius_val = radius.cast<double>();
|
||||
std::string weight_key = weight_to_string(distance);
|
||||
|
||||
if (is_directed) {
|
||||
DiGraph& G_ = G.cast<DiGraph&>();
|
||||
return _cpp_ego_graph_compute_impl(
|
||||
G_, n, radius_val, center_val, undirected_val, is_directed, weight_key);
|
||||
} else {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
return _cpp_ego_graph_compute_impl(
|
||||
G_, n, radius_val, center_val, undirected_val, is_directed, weight_key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
py::object cpp_ego_graph(
|
||||
py::object G,
|
||||
py::object n,
|
||||
py::object radius,
|
||||
py::object center,
|
||||
py::object undirected,
|
||||
py::object distance) {
|
||||
|
||||
_EgoGraphCore core = _cpp_ego_graph_compute(G, n, radius, center, undirected, distance);
|
||||
if (core.has_error) return py::none();
|
||||
|
||||
return nodes_subgraph_cpp(G, core.node_ids);
|
||||
}
|
||||
|
||||
|
||||
py::object cpp_ego_graph_csr(
|
||||
py::object G,
|
||||
py::object n,
|
||||
py::object radius,
|
||||
py::object center,
|
||||
py::object undirected,
|
||||
py::object distance) {
|
||||
|
||||
_EgoGraphCore core = _cpp_ego_graph_compute(G, n, radius, center, undirected, distance);
|
||||
if (core.has_error) return py::none();
|
||||
|
||||
int n_nodes = (int)core.node_ids.size();
|
||||
if (n_nodes == 0) {
|
||||
return py::dict();
|
||||
}
|
||||
|
||||
std::vector<int> V(n_nodes + 1, 0);
|
||||
std::vector<int> E;
|
||||
std::vector<double> W;
|
||||
|
||||
for (int new_u = 0; new_u < n_nodes; new_u++) {
|
||||
node_t u = core.node_ids[new_u];
|
||||
V[new_u + 1] = V[new_u];
|
||||
|
||||
int edge_idx = core.G_l.head[u];
|
||||
while (edge_idx != -1 && edge_idx < core.G_l.e) {
|
||||
node_t v = core.G_l.edges[edge_idx].to;
|
||||
auto it = core.node_to_idx.find(v);
|
||||
if (it != core.node_to_idx.end()) {
|
||||
E.push_back(it->second);
|
||||
W.push_back(core.G_l.edges[edge_idx].w);
|
||||
V[new_u + 1]++;
|
||||
}
|
||||
edge_idx = core.G_l.edges[edge_idx].next;
|
||||
}
|
||||
}
|
||||
|
||||
py::list original_nodes;
|
||||
for (node_t internal_id : core.node_ids) {
|
||||
original_nodes.append(core.id_to_node_py[py::cast(internal_id)]);
|
||||
}
|
||||
|
||||
py::dict result;
|
||||
result["nodes"] = original_nodes;
|
||||
result["V"] = py::cast(V);
|
||||
result["E"] = py::cast(E);
|
||||
result["W"] = py::cast(W);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
py::object cpp_ego_graph(
|
||||
py::object G,
|
||||
py::object n,
|
||||
py::object radius = py::int_(1),
|
||||
py::object center = py::bool_(true),
|
||||
py::object undirected = py::bool_(false),
|
||||
py::object distance = py::none()
|
||||
);
|
||||
|
||||
py::object cpp_ego_graph_csr(
|
||||
py::object G,
|
||||
py::object n,
|
||||
py::object radius = py::int_(1),
|
||||
py::object center = py::bool_(true),
|
||||
py::object undirected = py::bool_(false),
|
||||
py::object distance = py::none()
|
||||
);
|
||||
@@ -0,0 +1,135 @@
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <numeric>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <queue>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef _OPENMP
|
||||
#include <omp.h>
|
||||
#else
|
||||
#warning "OpenMP is not available: omp_graph_coloring will fall back to single-threaded execution."
|
||||
#endif
|
||||
|
||||
#include "../../classes/linkgraph.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
vector<int> greedy_graph_coloring(const Graph_L& G) {
|
||||
int n = G.n;
|
||||
if (n == 0) return vector<int>();
|
||||
|
||||
vector<int> degree(n + 1, 0);
|
||||
for (int v = 1; v <= n; ++v) {
|
||||
for (int e = G.head[v]; e != -1; e = G.edges[e].next) {
|
||||
degree[v]++;
|
||||
}
|
||||
}
|
||||
|
||||
vector<int> nodes(n);
|
||||
iota(nodes.begin(), nodes.end(), 1);
|
||||
sort(nodes.begin(), nodes.end(), [°ree](int a, int b) {
|
||||
return degree[a] > degree[b];
|
||||
});
|
||||
|
||||
vector<int> colors(n, -1);
|
||||
vector<int> used(n + 1, -1);
|
||||
|
||||
for (int u : nodes) {
|
||||
for (int e = G.head[u]; e != -1; e = G.edges[e].next) {
|
||||
int v = G.edges[e].to;
|
||||
if (v == u) continue;
|
||||
int neighbor_color = colors[v - 1];
|
||||
if (neighbor_color != -1) {
|
||||
used[neighbor_color] = u;
|
||||
}
|
||||
}
|
||||
|
||||
int cr = 0;
|
||||
while (cr <= n && used[cr] == u) {
|
||||
cr++;
|
||||
}
|
||||
colors[u - 1] = cr;
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
vector<int> omp_graph_coloring(const Graph_L& G) {
|
||||
int n = G.n;
|
||||
if (n == 0) return vector<int>();
|
||||
|
||||
vector<int> degree(n + 1, 0);
|
||||
for (int v = 1; v <= n; ++v) {
|
||||
for (int e = G.head[v]; e != -1; e = G.edges[e].next) {
|
||||
degree[v]++;
|
||||
}
|
||||
}
|
||||
|
||||
vector<int> nodes(n);
|
||||
iota(nodes.begin(), nodes.end(), 1);
|
||||
sort(nodes.begin(), nodes.end(), [°ree](int a, int b) {
|
||||
return degree[a] > degree[b];
|
||||
});
|
||||
|
||||
vector<int> colors(n, -1);
|
||||
|
||||
#pragma omp parallel
|
||||
{
|
||||
vector<int> local_used(n + 1, -1);
|
||||
|
||||
#pragma omp for schedule(guided)
|
||||
for (int i = 0; i < n; ++i) {
|
||||
int u = nodes[i];
|
||||
|
||||
fill(local_used.begin(), local_used.end(), -1);
|
||||
|
||||
for (int e = G.head[u]; e != -1; e = G.edges[e].next) {
|
||||
int v = G.edges[e].to;
|
||||
if (v == u) continue;
|
||||
int neighbor_color = colors[v - 1];
|
||||
if (neighbor_color != -1 && neighbor_color <= n) {
|
||||
local_used[neighbor_color] = u;
|
||||
}
|
||||
}
|
||||
|
||||
int cr = 0;
|
||||
while (cr <= n && local_used[cr] == u) {
|
||||
cr++;
|
||||
}
|
||||
colors[u - 1] = cr;
|
||||
}
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
bool verify_coloring(const Graph_L& g, const vector<int>& colors) {
|
||||
int n = g.n;
|
||||
for (int v = 1; v <= n; v++) {
|
||||
int v_color = colors[v - 1];
|
||||
for (int e = g.head[v]; e != -1; e = g.edges[e].next) {
|
||||
int u = g.edges[e].to;
|
||||
if (u == v) continue;
|
||||
int u_color = colors[u - 1];
|
||||
if (v_color == u_color) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int count_colors(const vector<int>& colors) {
|
||||
int max_color = -1;
|
||||
for (int c : colors) {
|
||||
if (c > max_color) {
|
||||
max_color = c;
|
||||
}
|
||||
}
|
||||
return max_color + 1;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include "../../classes/linkgraph.h"
|
||||
|
||||
std::vector<int> greedy_graph_coloring(const Graph_L& G);
|
||||
|
||||
std::vector<int> omp_graph_coloring(const Graph_L& G);
|
||||
|
||||
bool verify_coloring(const Graph_L& g, const std::vector<int>& colors);
|
||||
|
||||
int count_colors(const std::vector<int>& colors);
|
||||
@@ -0,0 +1,289 @@
|
||||
#include "indexed_heap.h"
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../common/utils.h"
|
||||
#include "../../classes/linkgraph.h"
|
||||
#include "greedy_modularity.h"
|
||||
|
||||
namespace py = pybind11;
|
||||
using namespace std;
|
||||
|
||||
py::object cpp_greedy_modularity_communities(py::object G, py::object weight) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
|
||||
bool is_directed = G.attr("is_directed")().cast<bool>();
|
||||
if (is_directed) {
|
||||
throw py::value_error("greedy_modularity_communities currently only supports undirected graphs (Graph class). "
|
||||
"For directed graphs, please use other community detection algorithms.");
|
||||
}
|
||||
|
||||
string weight_key = weight.is_none() ? "weight" : weight.cast<string>();
|
||||
|
||||
Graph_L GL;
|
||||
bool used_cached_linkgraph = false;
|
||||
|
||||
if (G_.linkgraph_dirty || G_.linkgraph_structure.max_deg == -1) {
|
||||
GL = graph_to_linkgraph(G_, false, weight_key, true, false);
|
||||
G_.linkgraph_dirty = false;
|
||||
} else {
|
||||
GL = G_.linkgraph_structure;
|
||||
used_cached_linkgraph = true;
|
||||
}
|
||||
|
||||
int N = GL.n;
|
||||
|
||||
if (N == 0) {
|
||||
return py::list();
|
||||
}
|
||||
|
||||
double m = 0.0;
|
||||
vector<double> k(N + 1, 0.0);
|
||||
|
||||
for (int u = 1; u <= N; ++u) {
|
||||
for (int e = GL.head[u]; e != -1; e = GL.edges[e].next) {
|
||||
int v = GL.edges[e].to;
|
||||
double w = GL.edges[e].w;
|
||||
if (v > u) {
|
||||
m += w;
|
||||
}
|
||||
k[u] += w;
|
||||
}
|
||||
}
|
||||
|
||||
if (m == 0) {
|
||||
py::list result;
|
||||
py::dict id_to_node = G_.id_to_node;
|
||||
for (int i = 1; i <= N; ++i) {
|
||||
py::set comm;
|
||||
comm.add(id_to_node[py::cast(i)]);
|
||||
result.append(comm);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
double q0 = 1.0 / (2.0 * m);
|
||||
|
||||
vector<double> a(N);
|
||||
for (int i = 0; i < N; ++i) {
|
||||
a[i] = k[i + 1] * q0;
|
||||
}
|
||||
|
||||
vector<int> parent(N);
|
||||
vector<vector<int>> nodes(N);
|
||||
for (int i = 0; i < N; ++i) {
|
||||
parent[i] = i;
|
||||
nodes[i].push_back(i + 1);
|
||||
}
|
||||
|
||||
vector<vector<int>> neigh(N);
|
||||
int max_neigh_size = 0;
|
||||
for (int i = 0; i < N; ++i) {
|
||||
int u = i + 1;
|
||||
for (int e = GL.head[u]; e != -1; e = GL.edges[e].next) {
|
||||
int v = GL.edges[e].to;
|
||||
if (v > 0 && v <= N && v != u) {
|
||||
neigh[i].push_back(v - 1);
|
||||
}
|
||||
}
|
||||
sort(neigh[i].begin(), neigh[i].end());
|
||||
neigh[i].erase(unique(neigh[i].begin(), neigh[i].end()), neigh[i].end());
|
||||
if ((int)neigh[i].size() > max_neigh_size) {
|
||||
max_neigh_size = neigh[i].size();
|
||||
}
|
||||
}
|
||||
|
||||
unordered_map<long long, double> edge_weights;
|
||||
for (int u = 1; u <= N; ++u) {
|
||||
for (int e = GL.head[u]; e != -1; e = GL.edges[e].next) {
|
||||
int v = GL.edges[e].to;
|
||||
double w = GL.edges[e].w;
|
||||
if (v > u) {
|
||||
long long key = ((long long)(u - 1) << 32) | (unsigned int)(v - 1);
|
||||
edge_weights[key] = w;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unordered_map<long long, double> dq;
|
||||
dq.reserve(N * 4);
|
||||
|
||||
for (int i = 0; i < N; ++i) {
|
||||
int u = i + 1;
|
||||
for (int j : neigh[i]) {
|
||||
if (j > i) {
|
||||
int v = j + 1;
|
||||
long long edge_key = ((long long)i << 32) | (unsigned int)j;
|
||||
double w = edge_weights.count(edge_key) ? edge_weights[edge_key] : 0.0;
|
||||
double dq_val = 2.0 * w * q0 - 2.0 * k[u] * k[v] * q0 * q0;
|
||||
long long key = ((long long)i << 32) | (unsigned int)j;
|
||||
dq[key] = dq_val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IndexedMaxHeap H(N);
|
||||
for (const auto& kv : dq) {
|
||||
int i = (int)(kv.first >> 32);
|
||||
int j = (int)(kv.first & 0xFFFFFFFF);
|
||||
H.push(kv.second, i, j);
|
||||
}
|
||||
|
||||
vector<char> merged(N, 0);
|
||||
int merge_count = 0;
|
||||
vector<int> combined;
|
||||
combined.reserve(max_neigh_size * 2);
|
||||
|
||||
while (!H.empty()) {
|
||||
auto best = H.pop();
|
||||
|
||||
int i = best.i;
|
||||
int j = best.j;
|
||||
|
||||
if (merged[i] || merged[j]) {
|
||||
continue;
|
||||
}
|
||||
if (parent[i] != i || parent[j] != j) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long long key = ((long long)i << 32) | (unsigned int)j;
|
||||
auto it = dq.find(key);
|
||||
if (it == dq.end() || it->second != best.dq) {
|
||||
continue;
|
||||
}
|
||||
if (best.dq <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
nodes[i].insert(nodes[i].end(), nodes[j].begin(), nodes[j].end());
|
||||
nodes[j].clear();
|
||||
parent[j] = i;
|
||||
merged[j] = 1;
|
||||
merge_count++;
|
||||
|
||||
combined.clear();
|
||||
|
||||
const vector<int>& list_i = neigh[i];
|
||||
const vector<int>& list_j = neigh[j];
|
||||
size_t p1 = 0, p2 = 0;
|
||||
|
||||
while (p1 < list_i.size() && p2 < list_j.size()) {
|
||||
int val1 = list_i[p1];
|
||||
int val2 = list_j[p2];
|
||||
if (val1 < val2) {
|
||||
if (val1 != i && val1 != j && !merged[val1]) {
|
||||
combined.push_back(val1);
|
||||
}
|
||||
p1++;
|
||||
} else if (val1 > val2) {
|
||||
if (val2 != i && val2 != j && !merged[val2]) {
|
||||
combined.push_back(val2);
|
||||
}
|
||||
p2++;
|
||||
} else {
|
||||
if (val1 != i && val1 != j && !merged[val1]) {
|
||||
combined.push_back(val1);
|
||||
}
|
||||
p1++;
|
||||
p2++;
|
||||
}
|
||||
}
|
||||
while (p1 < list_i.size()) {
|
||||
int val = list_i[p1];
|
||||
if (val != i && val != j && !merged[val]) {
|
||||
combined.push_back(val);
|
||||
}
|
||||
p1++;
|
||||
}
|
||||
while (p2 < list_j.size()) {
|
||||
int val = list_j[p2];
|
||||
if (val != i && val != j && !merged[val]) {
|
||||
combined.push_back(val);
|
||||
}
|
||||
p2++;
|
||||
}
|
||||
|
||||
neigh[i].swap(combined);
|
||||
|
||||
for (int k_node : neigh[i]) {
|
||||
if (k_node == i) continue;
|
||||
if (parent[k_node] != k_node) continue;
|
||||
|
||||
long long key_ik = ((long long)min(i, k_node) << 32) | (unsigned int)max(i, k_node);
|
||||
long long key_jk = ((long long)min(j, k_node) << 32) | (unsigned int)max(j, k_node);
|
||||
|
||||
bool has_ik = dq.find(key_ik) != dq.end();
|
||||
bool has_jk = dq.find(key_jk) != dq.end();
|
||||
|
||||
double new_dq;
|
||||
if (has_ik && has_jk) {
|
||||
new_dq = dq[key_ik] + dq[key_jk];
|
||||
} else if (has_jk) {
|
||||
new_dq = dq[key_jk] - 2.0 * a[i] * a[k_node];
|
||||
} else {
|
||||
new_dq = dq[key_ik] - 2.0 * a[j] * a[k_node];
|
||||
}
|
||||
|
||||
dq[key_ik] = new_dq;
|
||||
if (has_jk) {
|
||||
dq.erase(key_jk);
|
||||
}
|
||||
|
||||
if (H.get_index(i, k_node) >= 0) {
|
||||
H.update(new_dq, i, k_node);
|
||||
} else {
|
||||
H.push(new_dq, i, k_node);
|
||||
}
|
||||
}
|
||||
|
||||
const vector<int>& old_j_neigh = neigh[j];
|
||||
for (int k_node : old_j_neigh) {
|
||||
if (k_node == i || k_node == j) continue;
|
||||
if (parent[k_node] != k_node) continue;
|
||||
H.remove(j, k_node);
|
||||
}
|
||||
|
||||
neigh[j].clear();
|
||||
|
||||
a[i] += a[j];
|
||||
a[j] = 0;
|
||||
}
|
||||
|
||||
py::dict id_to_node = G_.id_to_node;
|
||||
py::list result;
|
||||
|
||||
for (int i = 0; i < N; ++i) {
|
||||
if (parent[i] == i && !nodes[i].empty()) {
|
||||
py::set comm;
|
||||
for (int node_id : nodes[i]) {
|
||||
comm.add(id_to_node[py::cast(node_id)]);
|
||||
}
|
||||
result.append(comm);
|
||||
}
|
||||
}
|
||||
|
||||
py::list sorted_result;
|
||||
vector<int> sizes;
|
||||
for (size_t i = 0; i < result.size(); ++i) {
|
||||
sizes.push_back(py::len(result[i]));
|
||||
}
|
||||
|
||||
vector<int> indices(result.size());
|
||||
iota(indices.begin(), indices.end(), 0);
|
||||
sort(indices.begin(), indices.end(), [&](int a_idx, int b_idx) {
|
||||
return sizes[a_idx] > sizes[b_idx];
|
||||
});
|
||||
|
||||
for (int idx : indices) {
|
||||
sorted_result.append(result[idx]);
|
||||
}
|
||||
|
||||
return sorted_result;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
|
||||
py::object cpp_greedy_modularity_communities(
|
||||
py::object G,
|
||||
py::object weight = py::str("weight")
|
||||
);
|
||||
@@ -0,0 +1,191 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
class IndexedMaxHeap {
|
||||
public:
|
||||
struct HeapEntry {
|
||||
double dq;
|
||||
int i;
|
||||
int j;
|
||||
};
|
||||
|
||||
private:
|
||||
std::vector<HeapEntry> heap;
|
||||
std::vector<std::vector<int>> pair_to_idx;
|
||||
size_t heap_size;
|
||||
int capacity_n;
|
||||
|
||||
static inline long long make_key(int i, int j) {
|
||||
int a = std::min(i, j);
|
||||
int b = std::max(i, j);
|
||||
return ((long long)a << 32) | (unsigned int)b;
|
||||
}
|
||||
|
||||
public:
|
||||
IndexedMaxHeap(size_t capacity = 0) : heap_size(0), capacity_n((int)capacity) {
|
||||
heap.reserve(capacity);
|
||||
if (capacity > 0) {
|
||||
pair_to_idx.assign(capacity, std::vector<int>(capacity, -1));
|
||||
}
|
||||
}
|
||||
|
||||
bool empty() const { return heap_size == 0; }
|
||||
size_t size() const { return heap_size; }
|
||||
|
||||
int get_index(int i, int j) const {
|
||||
int a = std::min(i, j);
|
||||
int b = std::max(i, j);
|
||||
if (a < 0 || b < 0 || a >= capacity_n || b >= capacity_n) return -1;
|
||||
size_t idx = (size_t)pair_to_idx[a][b];
|
||||
if (idx >= heap.size()) return -1;
|
||||
if (heap[idx].i == a && heap[idx].j == b) {
|
||||
return (int)idx;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void push(double dq, int i, int j) {
|
||||
int a = std::min(i, j);
|
||||
int b = std::max(i, j);
|
||||
if (a < 0 || b < 0 || a >= capacity_n || b >= capacity_n) return;
|
||||
|
||||
size_t existing = (size_t)pair_to_idx[a][b];
|
||||
if (existing < heap.size() && heap[existing].i == a && heap[existing].j == b) {
|
||||
update(dq, i, j);
|
||||
return;
|
||||
}
|
||||
|
||||
HeapEntry entry = {dq, a, b};
|
||||
heap.push_back(entry);
|
||||
size_t idx = heap.size() - 1;
|
||||
pair_to_idx[a][b] = (int)idx;
|
||||
sift_up(idx);
|
||||
heap_size++;
|
||||
}
|
||||
|
||||
void update(double dq, int i, int j) {
|
||||
int a = std::min(i, j);
|
||||
int b = std::max(i, j);
|
||||
if (a < 0 || b < 0 || a >= capacity_n || b >= capacity_n) return;
|
||||
|
||||
size_t idx = (size_t)pair_to_idx[a][b];
|
||||
if (idx >= heap.size()) return;
|
||||
if (heap[idx].i != a || heap[idx].j != b) return;
|
||||
if (heap[idx].dq == dq) return;
|
||||
|
||||
bool is_increase = dq > heap[idx].dq;
|
||||
heap[idx].dq = dq;
|
||||
|
||||
if (is_increase) {
|
||||
sift_up(idx);
|
||||
} else {
|
||||
sift_down(idx);
|
||||
}
|
||||
}
|
||||
|
||||
void remove(int i, int j) {
|
||||
int a = std::min(i, j);
|
||||
int b = std::max(i, j);
|
||||
if (a < 0 || b < 0 || a >= capacity_n || b >= capacity_n) return;
|
||||
|
||||
size_t idx = (size_t)pair_to_idx[a][b];
|
||||
if (idx >= heap.size()) return;
|
||||
if (heap[idx].i != a || heap[idx].j != b) return;
|
||||
|
||||
pair_to_idx[a][b] = -1;
|
||||
|
||||
if (idx == heap.size() - 1) {
|
||||
heap.pop_back();
|
||||
} else {
|
||||
heap[idx] = heap.back();
|
||||
int last_a = heap[idx].i;
|
||||
int last_b = heap[idx].j;
|
||||
if (last_a >= 0 && last_a < capacity_n && last_b >= 0 && last_b < capacity_n) {
|
||||
pair_to_idx[last_a][last_b] = (int)idx;
|
||||
}
|
||||
heap.pop_back();
|
||||
sift_up(idx);
|
||||
sift_down(idx);
|
||||
}
|
||||
if (heap_size > 0) heap_size--;
|
||||
}
|
||||
|
||||
HeapEntry pop() {
|
||||
HeapEntry result = heap[0];
|
||||
pair_to_idx[result.i][result.j] = -1;
|
||||
|
||||
if (heap.size() > 1) {
|
||||
heap[0] = heap.back();
|
||||
int new_a = heap[0].i;
|
||||
int new_b = heap[0].j;
|
||||
if (new_a >= 0 && new_a < capacity_n && new_b >= 0 && new_b < capacity_n) {
|
||||
pair_to_idx[new_a][new_b] = 0;
|
||||
}
|
||||
heap.pop_back();
|
||||
sift_down(0);
|
||||
} else {
|
||||
heap.pop_back();
|
||||
}
|
||||
if (heap_size > 0) heap_size--;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const HeapEntry& top() const { return heap[0]; }
|
||||
|
||||
private:
|
||||
void sift_up(size_t idx) {
|
||||
while (idx > 0) {
|
||||
size_t parent = (idx - 1) >> 1;
|
||||
if (heap[parent].dq >= heap[idx].dq) break;
|
||||
|
||||
std::swap(heap[parent], heap[idx]);
|
||||
|
||||
int p_i = heap[parent].i, p_j = heap[parent].j;
|
||||
int c_i = heap[idx].i, c_j = heap[idx].j;
|
||||
|
||||
if (p_i >= 0 && p_i < capacity_n && p_j >= 0 && p_j < capacity_n) {
|
||||
pair_to_idx[p_i][p_j] = (int)parent;
|
||||
}
|
||||
if (c_i >= 0 && c_i < capacity_n && c_j >= 0 && c_j < capacity_n) {
|
||||
pair_to_idx[c_i][c_j] = (int)idx;
|
||||
}
|
||||
|
||||
idx = parent;
|
||||
}
|
||||
}
|
||||
|
||||
void sift_down(size_t idx) {
|
||||
size_t n = heap.size();
|
||||
while (true) {
|
||||
size_t largest = idx;
|
||||
size_t left = idx * 2 + 1;
|
||||
size_t right = idx * 2 + 2;
|
||||
|
||||
if (left < n && heap[left].dq > heap[largest].dq) {
|
||||
largest = left;
|
||||
}
|
||||
if (right < n && heap[right].dq > heap[largest].dq) {
|
||||
largest = right;
|
||||
}
|
||||
|
||||
if (largest == idx) break;
|
||||
|
||||
std::swap(heap[largest], heap[idx]);
|
||||
|
||||
int l_i = heap[largest].i, l_j = heap[largest].j;
|
||||
int c_i = heap[idx].i, c_j = heap[idx].j;
|
||||
|
||||
if (l_i >= 0 && l_i < capacity_n && l_j >= 0 && l_j < capacity_n) {
|
||||
pair_to_idx[l_i][l_j] = (int)largest;
|
||||
}
|
||||
if (c_i >= 0 && c_i < capacity_n && c_j >= 0 && c_j < capacity_n) {
|
||||
pair_to_idx[c_i][c_j] = (int)idx;
|
||||
}
|
||||
|
||||
idx = largest;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,613 @@
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <algorithm>
|
||||
#include <queue>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <cmath>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../common/utils.h"
|
||||
#include "../../classes/linkgraph.h"
|
||||
#include "localsearch.h"
|
||||
|
||||
namespace py = pybind11;
|
||||
using namespace std;
|
||||
|
||||
struct RootDecision {
|
||||
int superior;
|
||||
int path_length;
|
||||
int degree;
|
||||
};
|
||||
|
||||
int choose_center(const vector<pair<int, double>>& sorted_multi) {
|
||||
if (sorted_multi.size() < 2) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
vector<double> y;
|
||||
for (const auto& p : sorted_multi) {
|
||||
y.push_back(p.second);
|
||||
}
|
||||
|
||||
vector<double> delta;
|
||||
for (size_t i = 1; i < y.size(); ++i) {
|
||||
delta.push_back(fabs(y[i] - y[i-1]));
|
||||
}
|
||||
|
||||
if (delta.empty()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
vector<double> delta_nozero;
|
||||
for (double d : delta) {
|
||||
if (d != 0) {
|
||||
delta_nozero.push_back(d);
|
||||
}
|
||||
}
|
||||
|
||||
if (delta_nozero.empty()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
double mean = 0;
|
||||
for (double d : delta_nozero) {
|
||||
mean += d;
|
||||
}
|
||||
mean /= delta_nozero.size();
|
||||
|
||||
double variance = 0;
|
||||
for (double d : delta_nozero) {
|
||||
variance += (d - mean) * (d - mean);
|
||||
}
|
||||
variance /= delta_nozero.size();
|
||||
double std_dev = sqrt(variance);
|
||||
|
||||
double threshold = std_dev + mean;
|
||||
|
||||
for (size_t i = 0; i < delta.size(); ++i) {
|
||||
if (delta[i] > threshold) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
py::object cpp_localsearch(
|
||||
py::object G,
|
||||
py::object center_num,
|
||||
py::object auto_choose_centers,
|
||||
py::object maximum_tree,
|
||||
py::object seed,
|
||||
py::object self_loop
|
||||
) {
|
||||
Graph_generate_linkgraph(G, py::str("weight"));
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
Graph_L GL = G_._get_linkgraph_structure();
|
||||
py::dict id_to_node = G_.id_to_node;
|
||||
|
||||
int n = GL.n;
|
||||
if (n == 0) {
|
||||
return py::make_tuple(py::none(), py::list(), py::list(), py::dict(), py::dict(), py::none());
|
||||
}
|
||||
|
||||
bool has_edges = false;
|
||||
for (int u = 1; u <= n; ++u) {
|
||||
if (GL.head[u] != -1) {
|
||||
has_edges = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!has_edges) {
|
||||
py::dict result_grouped;
|
||||
py::list result_center_dcd;
|
||||
py::list result_y_dcd;
|
||||
py::dict result_y_partition;
|
||||
for (int u = 1; u <= n; ++u) {
|
||||
py::object node = id_to_node[py::cast(u)];
|
||||
py::list members;
|
||||
members.append(node);
|
||||
result_grouped[node] = members;
|
||||
result_center_dcd.append(node);
|
||||
result_y_dcd.append(node);
|
||||
result_y_partition[node] = node;
|
||||
}
|
||||
return py::make_tuple(py::none(), result_center_dcd, result_y_dcd, result_y_partition, result_grouped, py::none());
|
||||
}
|
||||
|
||||
std::mt19937 rng(42);
|
||||
if (!seed.is_none()) {
|
||||
try {
|
||||
int seed_value = seed.cast<int>();
|
||||
rng.seed(seed_value);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
std::uniform_real_distribution<double> random_dist(0.0, 1.0);
|
||||
|
||||
unordered_set<int> selfloop_nodes;
|
||||
bool has_selfloop = self_loop.cast<bool>();
|
||||
if (!has_selfloop) {
|
||||
selfloop_nodes.clear();
|
||||
}
|
||||
|
||||
vector<int> degree(n + 1, 0);
|
||||
for (int u = 1; u <= n; ++u) {
|
||||
int deg = 0;
|
||||
for (int e = GL.head[u]; e != -1; e = GL.edges[e].next) {
|
||||
deg++;
|
||||
}
|
||||
if (selfloop_nodes.count(u)) {
|
||||
degree[u] = deg + 1;
|
||||
} else {
|
||||
degree[u] = deg;
|
||||
}
|
||||
}
|
||||
|
||||
unordered_map<int, vector<int>> dag_adj;
|
||||
unordered_map<int, vector<int>> dag_pred;
|
||||
|
||||
for (int v = 1; v <= n; ++v) {
|
||||
if (degree[v] == 0) continue;
|
||||
int kv = degree[v];
|
||||
vector<pair<int, int>> neighbors;
|
||||
for (int e = GL.head[v]; e != -1; e = GL.edges[e].next) {
|
||||
int nn = GL.edges[e].to;
|
||||
if (nn == v && selfloop_nodes.count(v)) continue;
|
||||
int deg_nn = degree[nn];
|
||||
neighbors.push_back({nn, deg_nn});
|
||||
}
|
||||
|
||||
if (!neighbors.empty()) {
|
||||
int knnmax = -1;
|
||||
for (auto& p : neighbors) {
|
||||
if (p.second > knnmax) knnmax = p.second;
|
||||
}
|
||||
|
||||
if (knnmax >= kv) {
|
||||
for (auto& p : neighbors) {
|
||||
if (p.second == knnmax) {
|
||||
int nn = p.first;
|
||||
|
||||
bool already_has = false;
|
||||
bool has_reverse = false;
|
||||
for (int existing : dag_adj[v]) {
|
||||
if (existing == nn) {
|
||||
already_has = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int existing : dag_adj[nn]) {
|
||||
if (existing == v) {
|
||||
has_reverse = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!already_has && !has_reverse) {
|
||||
dag_adj[v].push_back(nn);
|
||||
dag_pred[nn].push_back(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vector<int> out_degree_dag(n + 1, 0);
|
||||
for (int u = 1; u <= n; ++u) {
|
||||
out_degree_dag[u] = (int)dag_adj[u].size();
|
||||
}
|
||||
|
||||
vector<int> roots;
|
||||
for (int u = 1; u <= n; ++u) {
|
||||
if (out_degree_dag[u] == 0 && degree[u] > 0) {
|
||||
roots.push_back(u);
|
||||
}
|
||||
}
|
||||
|
||||
if (roots.empty()) {
|
||||
for (int u = 1; u <= n; ++u) {
|
||||
if (degree[u] > 0) {
|
||||
roots.push_back(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (roots.size() > 1) {
|
||||
bool all_same_degree = true;
|
||||
int first_degree = -1;
|
||||
for (int root : roots) {
|
||||
if (first_degree == -1) {
|
||||
first_degree = degree[root];
|
||||
} else if (degree[root] != first_degree) {
|
||||
all_same_degree = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (all_same_degree) {
|
||||
int max_root = -1;
|
||||
for (int root : roots) {
|
||||
if (root > max_root) {
|
||||
max_root = root;
|
||||
}
|
||||
}
|
||||
roots.clear();
|
||||
roots.push_back(max_root);
|
||||
}
|
||||
}
|
||||
|
||||
unordered_map<int, int> tree_rootnode;
|
||||
unordered_map<int, int> tree_parentnode;
|
||||
unordered_map<int, int> tree_distancetoroot;
|
||||
|
||||
for (int i = 1; i <= n; ++i) {
|
||||
tree_rootnode[i] = -1;
|
||||
tree_parentnode[i] = -1;
|
||||
tree_distancetoroot[i] = -1;
|
||||
}
|
||||
|
||||
queue<pair<int, int>> bfs_queue;
|
||||
for (int root : roots) {
|
||||
bfs_queue.push({root, 0});
|
||||
tree_rootnode[root] = root;
|
||||
tree_parentnode[root] = -1;
|
||||
tree_distancetoroot[root] = 0;
|
||||
}
|
||||
|
||||
vector<int> visited(n + 1, 0);
|
||||
for (int root : roots) {
|
||||
visited[root] = 1;
|
||||
}
|
||||
|
||||
while (!bfs_queue.empty()) {
|
||||
int parent, dist;
|
||||
std::tie(parent, dist) = bfs_queue.front();
|
||||
bfs_queue.pop();
|
||||
|
||||
for (int pred : dag_pred[parent]) {
|
||||
|
||||
if (tree_distancetoroot[pred] != -1 && tree_distancetoroot[pred] < dist + 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tree_distancetoroot[pred] == -1) {
|
||||
|
||||
tree_rootnode[pred] = tree_rootnode[parent];
|
||||
tree_parentnode[pred] = parent;
|
||||
tree_distancetoroot[pred] = dist + 1;
|
||||
bfs_queue.push({pred, dist + 1});
|
||||
} else if (tree_distancetoroot[pred] == dist + 1) {
|
||||
|
||||
if (random_dist(rng) < 0.5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tree_rootnode[pred] = tree_rootnode[parent];
|
||||
tree_parentnode[pred] = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unordered_map<int, vector<int>> root_to_node;
|
||||
for (int node = 1; node <= n; ++node) {
|
||||
int root = tree_rootnode[node];
|
||||
if (root != -1) {
|
||||
root_to_node[root].push_back(node);
|
||||
}
|
||||
}
|
||||
|
||||
vector<int> valid_roots;
|
||||
for (auto& kv : root_to_node) {
|
||||
if ((int)kv.second.size() > 1) {
|
||||
valid_roots.push_back(kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
unordered_set<int> root_set(valid_roots.begin(), valid_roots.end());
|
||||
unordered_map<int, RootDecision> root_decision;
|
||||
|
||||
auto BFS_from_s = [&](int s) -> pair<int, int> {
|
||||
queue<int> search_queue;
|
||||
unordered_map<int, int> path_dict;
|
||||
unordered_set<int> seen;
|
||||
|
||||
search_queue.push(s);
|
||||
seen.insert(s);
|
||||
path_dict[s] = 0;
|
||||
|
||||
while (!search_queue.empty()) {
|
||||
int vertex = search_queue.front();
|
||||
search_queue.pop();
|
||||
int current_dist = path_dict[vertex];
|
||||
|
||||
vector<pair<int, int>> neighbors;
|
||||
for (int e = GL.head[vertex]; e != -1; e = GL.edges[e].next) {
|
||||
int nn = GL.edges[e].to;
|
||||
int deg_nn = degree[nn];
|
||||
neighbors.push_back({nn, deg_nn});
|
||||
}
|
||||
|
||||
sort(neighbors.begin(), neighbors.end(),
|
||||
[](const pair<int, int>& a, const pair<int, int>& b) {
|
||||
return a.second > b.second;
|
||||
});
|
||||
|
||||
for (auto& p : neighbors) {
|
||||
int w = p.first;
|
||||
if (!seen.count(w)) {
|
||||
path_dict[w] = current_dist + 1;
|
||||
seen.insert(w);
|
||||
search_queue.push(w);
|
||||
}
|
||||
|
||||
if (root_set.count(w) && degree[w] > degree[s]) {
|
||||
return {w, path_dict[w]};
|
||||
}
|
||||
}
|
||||
}
|
||||
return {s, -1};
|
||||
};
|
||||
|
||||
for (int root : valid_roots) {
|
||||
auto result = BFS_from_s(root);
|
||||
root_decision[root] = {result.first, result.second, degree[root]};
|
||||
}
|
||||
|
||||
int max_path = -1;
|
||||
for (auto& kv : root_decision) {
|
||||
if (kv.second.path_length > max_path) {
|
||||
max_path = kv.second.path_length;
|
||||
}
|
||||
}
|
||||
if (max_path < 0) max_path = 2;
|
||||
|
||||
for (auto& kv : root_decision) {
|
||||
if (kv.second.path_length == -1) {
|
||||
kv.second.path_length = max_path;
|
||||
}
|
||||
}
|
||||
|
||||
unordered_map<int, RootDecision> node_plot = root_decision;
|
||||
for (int node = 1; node <= n; ++node) {
|
||||
if (node_plot.find(node) == node_plot.end() && degree[node] > 0) {
|
||||
int parent = tree_parentnode[node];
|
||||
node_plot[node] = {parent, 1, degree[node]};
|
||||
}
|
||||
}
|
||||
|
||||
vector<int> node_ids;
|
||||
vector<int> degrees;
|
||||
vector<int> path_lens;
|
||||
for (auto& kv : node_plot) {
|
||||
node_ids.push_back(kv.first);
|
||||
degrees.push_back(kv.second.degree);
|
||||
path_lens.push_back(kv.second.path_length);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < path_lens.size(); ++i) {
|
||||
if (degrees[i] <= 1) {
|
||||
path_lens[i] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
unordered_map<int, int> degree_rank;
|
||||
vector<pair<int, int>> sorted_by_deg;
|
||||
for (size_t i = 0; i < node_ids.size(); ++i) {
|
||||
sorted_by_deg.push_back({degrees[i], node_ids[i]});
|
||||
}
|
||||
sort(sorted_by_deg.begin(), sorted_by_deg.end(),
|
||||
[](const pair<int, int>& a, const pair<int, int>& b) {
|
||||
return a.first < b.first;
|
||||
});
|
||||
|
||||
int rank = 1;
|
||||
int last_deg = -1;
|
||||
for (auto& p : sorted_by_deg) {
|
||||
if (p.first != last_deg) {
|
||||
degree_rank[p.second] = rank;
|
||||
rank++;
|
||||
last_deg = p.first;
|
||||
} else {
|
||||
degree_rank[p.second] = rank - 1;
|
||||
}
|
||||
}
|
||||
|
||||
int min_rank = 1;
|
||||
int max_rank = rank - 1;
|
||||
double rank_range = (max_rank - min_rank);
|
||||
if (rank_range == 0) rank_range = 1;
|
||||
|
||||
vector<double> square_path(node_ids.size());
|
||||
double max_sq_path = 0;
|
||||
double min_sq_path = 1e9;
|
||||
for (size_t i = 0; i < node_ids.size(); ++i) {
|
||||
square_path[i] = (double)path_lens[i] * (double)path_lens[i];
|
||||
if (square_path[i] > max_sq_path) max_sq_path = square_path[i];
|
||||
if (square_path[i] < min_sq_path) min_sq_path = square_path[i];
|
||||
}
|
||||
double sq_range = max_sq_path - min_sq_path;
|
||||
if (sq_range == 0) sq_range = 1;
|
||||
|
||||
unordered_map<int, double> multi_dict;
|
||||
for (size_t i = 0; i < node_ids.size(); ++i) {
|
||||
int node = node_ids[i];
|
||||
double norm_deg, norm_sq_path;
|
||||
|
||||
if (max_rank == min_rank) {
|
||||
norm_deg = 1.0 / (double)node_ids.size();
|
||||
} else {
|
||||
norm_deg = (double)(degree_rank[node] - min_rank) / rank_range;
|
||||
}
|
||||
|
||||
if (max_sq_path == min_sq_path) {
|
||||
norm_sq_path = 1.0 / (double)node_ids.size();
|
||||
} else {
|
||||
norm_sq_path = (square_path[i] - min_sq_path) / sq_range;
|
||||
}
|
||||
|
||||
multi_dict[node] = norm_deg * norm_sq_path;
|
||||
}
|
||||
|
||||
vector<pair<int, double>> sorted_multi;
|
||||
for (auto& kv : multi_dict) {
|
||||
sorted_multi.push_back(kv);
|
||||
}
|
||||
sort(sorted_multi.begin(), sorted_multi.end(),
|
||||
[](const pair<int, double>& a, const pair<int, double>& b) {
|
||||
if (fabs(a.second - b.second) > 1e-9) return a.second > b.second;
|
||||
return a.first > b.first;
|
||||
});
|
||||
|
||||
int num_centers = (int)valid_roots.size();
|
||||
|
||||
bool auto_choose = auto_choose_centers.cast<bool>();
|
||||
if (auto_choose && sorted_multi.size() > 0) {
|
||||
int auto_centernum = choose_center(sorted_multi);
|
||||
if (!center_num.is_none()) {
|
||||
int user_center_num = center_num.cast<int>();
|
||||
num_centers = (auto_centernum < user_center_num) ? auto_centernum : user_center_num;
|
||||
} else {
|
||||
num_centers = auto_centernum;
|
||||
}
|
||||
} else if (!center_num.is_none()) {
|
||||
num_centers = center_num.cast<int>();
|
||||
}
|
||||
|
||||
if (num_centers <= 0) {
|
||||
num_centers = (int)valid_roots.size();
|
||||
}
|
||||
|
||||
vector<int> center_dcd;
|
||||
int local_cnt = 0;
|
||||
for (size_t i = 0; i < sorted_multi.size() && local_cnt < num_centers; ++i) {
|
||||
if (sorted_multi[i].second > 0) {
|
||||
local_cnt++;
|
||||
center_dcd.push_back(sorted_multi[i].first);
|
||||
}
|
||||
}
|
||||
|
||||
if (center_dcd.empty() && !sorted_multi.empty()) {
|
||||
center_dcd.push_back(sorted_multi[0].first);
|
||||
}
|
||||
|
||||
bool all_same_degree = true;
|
||||
int first_deg = -1;
|
||||
for (int i = 1; i <= n && all_same_degree; ++i) {
|
||||
if (degree[i] > 0) {
|
||||
if (first_deg == -1) {
|
||||
first_deg = degree[i];
|
||||
} else if (degree[i] != first_deg) {
|
||||
all_same_degree = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (all_same_degree && n > 0) {
|
||||
center_dcd.clear();
|
||||
center_dcd.push_back(n);
|
||||
}
|
||||
|
||||
unordered_set<int> center_set(center_dcd.begin(), center_dcd.end());
|
||||
|
||||
for (int node : valid_roots) {
|
||||
int superior = root_decision[node].superior;
|
||||
tree_parentnode[node] = superior;
|
||||
tree_rootnode[node] = superior;
|
||||
}
|
||||
|
||||
for (int node = 0; node < n; ++node) {
|
||||
if (degree[node] > 0 && center_set.count(node)) {
|
||||
tree_rootnode[node] = node;
|
||||
}
|
||||
}
|
||||
|
||||
for (int node = 0; node < n; ++node) {
|
||||
if (degree[node] == 0) continue;
|
||||
vector<int> recent;
|
||||
recent.push_back(node);
|
||||
bool flag = false;
|
||||
|
||||
while (center_set.find(tree_rootnode[node]) == center_set.end() && !flag) {
|
||||
int j = tree_rootnode[node];
|
||||
if (j == -1 || find(recent.begin(), recent.end(), j) != recent.end()) {
|
||||
tree_rootnode[node] = -1;
|
||||
flag = true;
|
||||
break;
|
||||
}
|
||||
recent.push_back(j);
|
||||
tree_rootnode[node] = tree_rootnode[j];
|
||||
}
|
||||
}
|
||||
|
||||
unordered_map<int, int> y_partition;
|
||||
vector<int> y_dcd;
|
||||
|
||||
for (int node = 1; node <= n; ++node) {
|
||||
if (degree[node] == 0) continue;
|
||||
int root = tree_rootnode[node];
|
||||
if (root == -1 && !center_dcd.empty()) {
|
||||
root = center_dcd[0];
|
||||
tree_rootnode[node] = root;
|
||||
}
|
||||
y_partition[node] = root;
|
||||
if (root == -1) {
|
||||
y_dcd.push_back(-1);
|
||||
} else {
|
||||
y_dcd.push_back(root);
|
||||
}
|
||||
}
|
||||
|
||||
unordered_map<int, vector<int>> grouped;
|
||||
for (auto& kv : y_partition) {
|
||||
int center = kv.second;
|
||||
if (center != -1) {
|
||||
grouped[center].push_back(kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
py::dict result_grouped;
|
||||
for (auto& kv : grouped) {
|
||||
py::list members;
|
||||
for (int node_id : kv.second) {
|
||||
members.append(id_to_node[py::cast(node_id)]);
|
||||
}
|
||||
result_grouped[id_to_node[py::cast(kv.first)]] = members;
|
||||
}
|
||||
|
||||
py::list result_center_dcd;
|
||||
for (int center : center_dcd) {
|
||||
result_center_dcd.append(id_to_node[py::cast(center)]);
|
||||
}
|
||||
|
||||
py::list result_y_dcd;
|
||||
for (int label : y_dcd) {
|
||||
if (label == -1) {
|
||||
result_y_dcd.append(py::cast(-1));
|
||||
} else {
|
||||
result_y_dcd.append(id_to_node[py::cast(label)]);
|
||||
}
|
||||
}
|
||||
|
||||
py::dict result_y_partition;
|
||||
for (auto& kv : y_partition) {
|
||||
if (kv.second == -1) {
|
||||
result_y_partition[id_to_node[py::cast(kv.first)]] = py::cast(-1);
|
||||
} else {
|
||||
result_y_partition[id_to_node[py::cast(kv.first)]] = id_to_node[py::cast(kv.second)];
|
||||
}
|
||||
}
|
||||
|
||||
return py::make_tuple(
|
||||
py::none(),
|
||||
result_center_dcd,
|
||||
result_y_dcd,
|
||||
result_y_partition,
|
||||
result_grouped,
|
||||
py::none()
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
py::object cpp_localsearch(
|
||||
py::object G,
|
||||
py::object center_num = py::none(),
|
||||
py::object auto_choose_centers = py::bool_(false),
|
||||
py::object maximum_tree = py::bool_(true),
|
||||
py::object seed = py::none(),
|
||||
py::object self_loop = py::bool_(false)
|
||||
);
|
||||
@@ -0,0 +1,544 @@
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <numeric>
|
||||
#include <cstdint>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
#ifdef _OPENMP
|
||||
#include <omp.h>
|
||||
#else
|
||||
#warning "OpenMP is not available: cpp_louvain_communities will fall back to single-threaded execution."
|
||||
#endif
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../common/utils.h"
|
||||
#include "../../classes/linkgraph.h"
|
||||
#include "../../functions/community/graph_coloring.h"
|
||||
|
||||
namespace py = pybind11;
|
||||
using namespace std;
|
||||
|
||||
const double epsilon = 1e-10;
|
||||
const int MAX_ITERATIONS = 50;
|
||||
const double CONVERGENCE_THRESHOLD = 1e-7;
|
||||
|
||||
struct CommunityState {
|
||||
int size = 0;
|
||||
double weight_inside = 0;
|
||||
double weight_all = 0;
|
||||
};
|
||||
|
||||
double calculate_norm(const Graph_L& G) {
|
||||
double total_weight = 0.0;
|
||||
for (int u = 1; u < G.n + 1; ++u) {
|
||||
for (int i = G.head[u]; i != -1; i = G.edges[i].next) {
|
||||
total_weight += G.edges[i].w;
|
||||
}
|
||||
}
|
||||
return total_weight;
|
||||
}
|
||||
|
||||
double calculate_actual_modularity(const Graph_L& G, const vector<int>& membership,
|
||||
double norm, double resolution) {
|
||||
unordered_map<int, double> comm_weight_inside;
|
||||
unordered_map<int, double> comm_weight_total;
|
||||
|
||||
for (int u = 1; u <= G.n; ++u) {
|
||||
int comm_u = membership[u - 1];
|
||||
for (int e_idx = G.head[u]; e_idx != -1; e_idx = G.edges[e_idx].next) {
|
||||
int v = G.edges[e_idx].to;
|
||||
double w = G.edges[e_idx].w;
|
||||
int comm_v = membership[v - 1];
|
||||
comm_weight_total[comm_u] += w;
|
||||
if (comm_u == comm_v) {
|
||||
comm_weight_inside[comm_u] += w;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double modularity = 0.0;
|
||||
for (const auto& kv : comm_weight_inside) {
|
||||
int comm = kv.first;
|
||||
double inside = kv.second;
|
||||
double total = comm_weight_total[comm];
|
||||
modularity += (inside / 2.0) - resolution * (total * total) / (4.0 * norm * norm);
|
||||
}
|
||||
|
||||
return modularity;
|
||||
}
|
||||
|
||||
inline double calculate_modularity_gain(double comm_weight_without_u, double norm,
|
||||
double node_weight_all, double weight_to_comm, double resolution) {
|
||||
double gain = weight_to_comm - resolution * (comm_weight_without_u * node_weight_all) / norm;
|
||||
return 2 * gain / norm;
|
||||
}
|
||||
|
||||
double modularity_optimization_serial(const Graph_L& G, vector<int>& membership, double resolution_val, double norm) {
|
||||
int n = G.n;
|
||||
double total_modularity_gain = 0.0;
|
||||
vector<CommunityState> comms(n);
|
||||
|
||||
for (int i = 0; i < n; ++i) {
|
||||
membership[i] = i;
|
||||
comms[i].size = 1;
|
||||
comms[i].weight_all = 0.0;
|
||||
comms[i].weight_inside = 0.0;
|
||||
for (int j = G.head[i + 1]; j != -1; j = G.edges[j].next) {
|
||||
int v = G.edges[j].to;
|
||||
double w = G.edges[j].w;
|
||||
comms[i].weight_all += w;
|
||||
if (v == i + 1) comms[i].weight_inside += w;
|
||||
}
|
||||
}
|
||||
|
||||
vector<double> node_weight(n + 1, 0.0);
|
||||
vector<double> node_loop(n + 1, 0.0);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
node_weight[i + 1] = comms[i].weight_all;
|
||||
node_loop[i + 1] = comms[i].weight_inside;
|
||||
}
|
||||
|
||||
vector<double> neigh_weight(n, 0.0);
|
||||
vector<int> timestamp(n, 0);
|
||||
vector<int> touched;
|
||||
touched.reserve(512);
|
||||
int version = 0;
|
||||
|
||||
std::random_device rd;
|
||||
std::mt19937 rng(rd());
|
||||
vector<int> node_order(n);
|
||||
for (int i = 0; i < n; ++i) node_order[i] = i + 1;
|
||||
|
||||
int num_moved_nodes = 1;
|
||||
int iteration = 0;
|
||||
double previous_modularity = -1e100;
|
||||
|
||||
do {
|
||||
num_moved_nodes = 0;
|
||||
iteration++;
|
||||
|
||||
std::shuffle(node_order.begin(), node_order.end(), rng);
|
||||
|
||||
for (int idx = 0; idx < n; ++idx) {
|
||||
int u = node_order[idx];
|
||||
|
||||
touched.clear();
|
||||
version++;
|
||||
if (version == 0) version = 1;
|
||||
|
||||
double weight_all = node_weight[u];
|
||||
double weight_loop = node_loop[u];
|
||||
double weight_inside = 0.0;
|
||||
int my_comm = membership[u - 1];
|
||||
|
||||
for (int e = G.head[u]; e != -1; e = G.edges[e].next) {
|
||||
int v = G.edges[e].to;
|
||||
double w = G.edges[e].w;
|
||||
|
||||
if (u == v) continue;
|
||||
|
||||
int v_comm = membership[v - 1];
|
||||
|
||||
if (timestamp[v_comm] != version) {
|
||||
timestamp[v_comm] = version;
|
||||
neigh_weight[v_comm] = 0.0;
|
||||
touched.push_back(v_comm);
|
||||
}
|
||||
neigh_weight[v_comm] += w;
|
||||
|
||||
if (v_comm == my_comm) {
|
||||
weight_inside += w;
|
||||
}
|
||||
}
|
||||
|
||||
if (weight_all < epsilon) continue;
|
||||
|
||||
int old_comm = my_comm;
|
||||
double comm_old_w = comms[old_comm].weight_all - weight_all;
|
||||
double old_gain = calculate_modularity_gain(comm_old_w, norm, weight_all, weight_inside, resolution_val);
|
||||
|
||||
double max_gain = std::max(old_gain, 0.0);
|
||||
int best_comm = old_comm;
|
||||
double best_weight_to_comm = weight_inside;
|
||||
|
||||
for (int target_comm : touched) {
|
||||
if (target_comm == old_comm) continue;
|
||||
|
||||
double w_to_target = neigh_weight[target_comm];
|
||||
double comm_target_w = comms[target_comm].weight_all;
|
||||
|
||||
double gain = calculate_modularity_gain(comm_target_w, norm, weight_all, w_to_target, resolution_val);
|
||||
|
||||
if (gain > max_gain + epsilon) {
|
||||
max_gain = gain;
|
||||
best_comm = target_comm;
|
||||
best_weight_to_comm = w_to_target;
|
||||
}
|
||||
}
|
||||
|
||||
if (best_comm != old_comm) {
|
||||
comms[old_comm].size--;
|
||||
comms[old_comm].weight_all -= weight_all;
|
||||
comms[old_comm].weight_inside -= (2.0 * weight_inside + weight_loop);
|
||||
|
||||
comms[best_comm].size++;
|
||||
comms[best_comm].weight_all += weight_all;
|
||||
comms[best_comm].weight_inside += (2.0 * best_weight_to_comm + weight_loop);
|
||||
|
||||
membership[u - 1] = best_comm;
|
||||
num_moved_nodes++;
|
||||
}
|
||||
}
|
||||
|
||||
if (num_moved_nodes == 0) {
|
||||
return calculate_actual_modularity(G, membership, norm, resolution_val);
|
||||
}
|
||||
|
||||
if (iteration >= MAX_ITERATIONS) {
|
||||
return calculate_actual_modularity(G, membership, norm, resolution_val);
|
||||
}
|
||||
|
||||
} while (true);
|
||||
|
||||
for (int i = 0; i < n; ++i) {
|
||||
comms[i].weight_all = node_weight[i + 1];
|
||||
}
|
||||
|
||||
return calculate_actual_modularity(G, membership, norm, resolution_val);
|
||||
}
|
||||
|
||||
double modularity_optimization_parallel_simplified(const Graph_L& G, vector<int>& membership, double resolution_val, double norm) {
|
||||
int n = G.n;
|
||||
vector<CommunityState> comms(n);
|
||||
|
||||
for (int i = 0; i < n; ++i) {
|
||||
membership[i] = i;
|
||||
comms[i].size = 1;
|
||||
comms[i].weight_all = 0.0;
|
||||
comms[i].weight_inside = 0.0;
|
||||
for (int j = G.head[i + 1]; j != -1; j = G.edges[j].next) {
|
||||
int v = G.edges[j].to;
|
||||
double w = G.edges[j].w;
|
||||
comms[i].weight_all += w;
|
||||
if (v == i + 1) comms[i].weight_inside += w;
|
||||
}
|
||||
}
|
||||
|
||||
vector<double> node_weight(n + 1, 0.0);
|
||||
vector<double> node_loop(n + 1, 0.0);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
node_weight[i + 1] = comms[i].weight_all;
|
||||
node_loop[i + 1] = comms[i].weight_inside;
|
||||
}
|
||||
|
||||
std::random_device rd;
|
||||
std::mt19937 rng(rd());
|
||||
vector<int> node_order(n);
|
||||
iota(node_order.begin(), node_order.end(), 0);
|
||||
|
||||
int num_moved_nodes = 1;
|
||||
int iteration = 0;
|
||||
|
||||
vector<double> comm_weights(n, 0.0);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
comm_weights[i] = comms[i].weight_all;
|
||||
}
|
||||
|
||||
do {
|
||||
num_moved_nodes = 0;
|
||||
iteration++;
|
||||
|
||||
std::shuffle(node_order.begin(), node_order.end(), rng);
|
||||
|
||||
#pragma omp parallel
|
||||
{
|
||||
#pragma omp for reduction(+:num_moved_nodes) schedule(guided)
|
||||
for (int idx = 0; idx < n; ++idx) {
|
||||
int u = node_order[idx];
|
||||
|
||||
double weight_all = node_weight[u];
|
||||
int my_comm = membership[u - 1];
|
||||
|
||||
if (weight_all < epsilon) continue;
|
||||
|
||||
unordered_map<int, double> neigh_weight;
|
||||
neigh_weight.reserve(32);
|
||||
vector<int> touched;
|
||||
touched.reserve(32);
|
||||
|
||||
double weight_inside = 0.0;
|
||||
|
||||
for (int e = G.head[u]; e != -1; e = G.edges[e].next) {
|
||||
int v = G.edges[e].to;
|
||||
double w = G.edges[e].w;
|
||||
|
||||
if (u == v) continue;
|
||||
|
||||
int v_comm = membership[v - 1];
|
||||
|
||||
auto it = neigh_weight.find(v_comm);
|
||||
if (it == neigh_weight.end()) {
|
||||
touched.push_back(v_comm);
|
||||
neigh_weight[v_comm] = w;
|
||||
} else {
|
||||
it->second += w;
|
||||
}
|
||||
|
||||
if (v_comm == my_comm) {
|
||||
weight_inside += w;
|
||||
}
|
||||
}
|
||||
|
||||
int old_comm = my_comm;
|
||||
double comm_old_w = comm_weights[old_comm] - weight_all;
|
||||
double old_gain = calculate_modularity_gain(comm_old_w, norm, weight_all, weight_inside, resolution_val);
|
||||
|
||||
double max_gain = std::max(old_gain, 0.0);
|
||||
int best_comm = old_comm;
|
||||
|
||||
for (int target_comm : touched) {
|
||||
if (target_comm == old_comm) continue;
|
||||
|
||||
double w_to_target = neigh_weight.at(target_comm);
|
||||
double comm_target_w = comm_weights[target_comm];
|
||||
|
||||
double gain = calculate_modularity_gain(comm_target_w, norm, weight_all, w_to_target, resolution_val);
|
||||
|
||||
if (gain > max_gain + epsilon) {
|
||||
max_gain = gain;
|
||||
best_comm = target_comm;
|
||||
}
|
||||
}
|
||||
|
||||
if (best_comm != old_comm) {
|
||||
#pragma omp atomic
|
||||
comm_weights[old_comm] -= weight_all;
|
||||
|
||||
#pragma omp atomic
|
||||
comm_weights[best_comm] += weight_all;
|
||||
|
||||
membership[u - 1] = best_comm;
|
||||
num_moved_nodes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (num_moved_nodes == 0) break;
|
||||
if (iteration >= MAX_ITERATIONS) break;
|
||||
|
||||
} while (true);
|
||||
|
||||
for (int i = 0; i < n; ++i) {
|
||||
comms[i].weight_all = comm_weights[i];
|
||||
}
|
||||
|
||||
return calculate_actual_modularity(G, membership, norm, resolution_val);
|
||||
}
|
||||
|
||||
struct SuperEdge {
|
||||
int u, v;
|
||||
double w;
|
||||
};
|
||||
|
||||
Graph_L* graph_compress(const Graph_L* G, vector<int>& membership) {
|
||||
unordered_map<int, int> new_comm_id;
|
||||
int new_vcount = 0;
|
||||
for (int i = 0; i < G->n; i++) {
|
||||
int c = membership[i];
|
||||
if (new_comm_id.find(c) == new_comm_id.end()) {
|
||||
new_comm_id[c] = new_vcount++;
|
||||
}
|
||||
membership[i] = new_comm_id[c];
|
||||
}
|
||||
|
||||
vector<SuperEdge> edges;
|
||||
|
||||
for (int u = 1; u < G->n + 1; ++u) {
|
||||
int su = membership[u-1] + 1;
|
||||
for (int e = G->head[u]; e != -1; e = G->edges[e].next) {
|
||||
int v = G->edges[e].to;
|
||||
double w = G->edges[e].w;
|
||||
int sv = membership[v-1] + 1;
|
||||
edges.push_back({su, sv, w});
|
||||
}
|
||||
}
|
||||
|
||||
sort(edges.begin(), edges.end(), [](const SuperEdge& a, const SuperEdge& b) {
|
||||
if (a.u != b.u) return a.u < b.u;
|
||||
return a.v < b.v;
|
||||
});
|
||||
|
||||
Graph_L* super_g = new Graph_L(new_vcount, G->is_directed, G->is_deg);
|
||||
if (edges.empty()) return super_g;
|
||||
|
||||
int curr_u = edges[0].u;
|
||||
int curr_v = edges[0].v;
|
||||
double curr_w = edges[0].w;
|
||||
|
||||
for (size_t i = 1; i < edges.size(); ++i) {
|
||||
if (edges[i].u == curr_u && edges[i].v == curr_v) {
|
||||
curr_w += edges[i].w;
|
||||
} else {
|
||||
super_g->add_weighted_edge(curr_u, curr_v, curr_w);
|
||||
curr_u = edges[i].u;
|
||||
curr_v = edges[i].v;
|
||||
curr_w = edges[i].w;
|
||||
}
|
||||
}
|
||||
super_g->add_weighted_edge(curr_u, curr_v, curr_w);
|
||||
return super_g;
|
||||
}
|
||||
|
||||
py::object cpp_louvain_communities_serial(py::object G, py::object weight, py::object threshold, py::object resolution) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
|
||||
{
|
||||
if (G_.is_linkgraph_dirty()) {
|
||||
G_._get_linkgraph_structure();
|
||||
}
|
||||
}
|
||||
|
||||
Graph_L original_GL = G_._get_linkgraph_structure();
|
||||
|
||||
Graph_L* current_GL = &original_GL;
|
||||
double threshold_val = threshold.cast<double>();
|
||||
double resolution_val = resolution.cast<double>();
|
||||
int vcount = current_GL->n;
|
||||
if (vcount == 0) return py::list();
|
||||
|
||||
double norm = calculate_norm(*current_GL);
|
||||
vector<int> membership(vcount);
|
||||
iota(membership.begin(), membership.end(), 0);
|
||||
|
||||
vector<Graph_L*> allocated_graphs;
|
||||
int iteration = 0;
|
||||
double previous_modularity = -1e100;
|
||||
|
||||
if (norm > 0.0) {
|
||||
while (true) {
|
||||
iteration++;
|
||||
int curr_vcount = current_GL->n;
|
||||
vector<int> curr_membership(curr_vcount);
|
||||
|
||||
double current_modularity;
|
||||
current_modularity = modularity_optimization_serial(*current_GL, curr_membership, resolution_val, norm);
|
||||
|
||||
double modularity_gain = current_modularity - previous_modularity;
|
||||
|
||||
if (modularity_gain <= threshold_val || iteration > 100) {
|
||||
break;
|
||||
}
|
||||
|
||||
Graph_L* next_GL = graph_compress(current_GL, curr_membership);
|
||||
allocated_graphs.push_back(next_GL);
|
||||
current_GL = next_GL;
|
||||
|
||||
for (int i = 0; i < vcount; i++) {
|
||||
membership[i] = curr_membership[membership[i]];
|
||||
}
|
||||
|
||||
previous_modularity = current_modularity;
|
||||
}
|
||||
}
|
||||
|
||||
unordered_map<int, vector<node_t>> cpp_groups;
|
||||
for (int i = 0; i < vcount; ++i) {
|
||||
int comm_id = membership[i];
|
||||
node_t node_id = i + 1;
|
||||
cpp_groups[comm_id].push_back(node_id);
|
||||
}
|
||||
|
||||
py::dict id_to_node = G_.id_to_node;
|
||||
|
||||
py::list final_result;
|
||||
for (auto& kv : cpp_groups) {
|
||||
py::set py_comm;
|
||||
for (node_t node_id : kv.second) {
|
||||
py::object node_obj = id_to_node[py::cast(node_id)];
|
||||
py_comm.add(node_obj);
|
||||
}
|
||||
final_result.append(py_comm);
|
||||
}
|
||||
|
||||
for (auto g : allocated_graphs) {
|
||||
delete g;
|
||||
}
|
||||
|
||||
return final_result;
|
||||
}
|
||||
|
||||
py::object cpp_louvain_communities(py::object G, py::object weight, py::object threshold, py::object resolution) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
|
||||
#ifdef _OPENMP
|
||||
omp_set_num_threads(8);
|
||||
#endif
|
||||
|
||||
Graph_L original_GL = G_._get_linkgraph_structure();
|
||||
|
||||
Graph_L* current_GL = &original_GL;
|
||||
double threshold_val = threshold.cast<double>();
|
||||
double resolution_val = resolution.cast<double>();
|
||||
int vcount = current_GL->n;
|
||||
if (vcount == 0) return py::list();
|
||||
|
||||
double norm = calculate_norm(*current_GL);
|
||||
vector<int> membership(vcount);
|
||||
iota(membership.begin(), membership.end(), 0);
|
||||
|
||||
vector<Graph_L*> allocated_graphs;
|
||||
int iteration = 0;
|
||||
double previous_modularity = 0.0;
|
||||
|
||||
if (norm > 0.0) {
|
||||
while (true) {
|
||||
iteration++;
|
||||
int curr_vcount = current_GL->n;
|
||||
vector<int> curr_membership(curr_vcount);
|
||||
double current_modularity;
|
||||
|
||||
current_modularity = modularity_optimization_parallel_simplified(*current_GL, curr_membership, resolution_val, norm);
|
||||
|
||||
double modularity_gain = current_modularity - previous_modularity;
|
||||
previous_modularity = current_modularity;
|
||||
|
||||
if (modularity_gain <= threshold_val || iteration > 100) {
|
||||
break;
|
||||
}
|
||||
|
||||
Graph_L* next_GL = graph_compress(current_GL, curr_membership);
|
||||
allocated_graphs.push_back(next_GL);
|
||||
current_GL = next_GL;
|
||||
|
||||
for (int i = 0; i < vcount; i++) {
|
||||
membership[i] = curr_membership[membership[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unordered_map<int, vector<node_t>> cpp_groups;
|
||||
for (int i = 0; i < vcount; ++i) {
|
||||
int comm_id = membership[i];
|
||||
node_t node_id = i + 1;
|
||||
cpp_groups[comm_id].push_back(node_id);
|
||||
}
|
||||
|
||||
py::dict id_to_node = G_.id_to_node;
|
||||
|
||||
py::list final_result;
|
||||
for (auto& kv : cpp_groups) {
|
||||
py::set py_comm;
|
||||
for (node_t node_id : kv.second) {
|
||||
py::object node_obj = id_to_node[py::cast(node_id)];
|
||||
py_comm.add(node_obj);
|
||||
}
|
||||
final_result.append(py_comm);
|
||||
}
|
||||
|
||||
for (auto g : allocated_graphs) {
|
||||
delete g;
|
||||
}
|
||||
|
||||
return final_result;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
|
||||
py::object cpp_louvain_communities(
|
||||
py::object G,
|
||||
py::object weight = py::str("weight"),
|
||||
py::object threshold = py::float_(0.0),
|
||||
py::object resolution = py::float_(1.0)
|
||||
);
|
||||
|
||||
py::object cpp_louvain_communities_serial(
|
||||
py::object G,
|
||||
py::object weight = py::str("weight"),
|
||||
py::object threshold = py::float_(0.0),
|
||||
py::object resolution = py::float_(1.0)
|
||||
);
|
||||
@@ -0,0 +1,213 @@
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <string>
|
||||
#include <cstddef>
|
||||
#ifdef _OPENMP
|
||||
#include <omp.h>
|
||||
#else
|
||||
#warning "OpenMP is not available: modularity utility functions will fall back to single-threaded execution."
|
||||
#endif
|
||||
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../common/utils.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
void addVectorsInPlace(std::vector<double>& v1, std::vector<double>& v2) {
|
||||
if (v1.size() != v2.size()) {
|
||||
throw std::invalid_argument("Vectors must have the same size for element-wise addition.");
|
||||
}
|
||||
|
||||
const std::ptrdiff_t n = static_cast<std::ptrdiff_t>(v1.size());
|
||||
|
||||
#pragma omp parallel for
|
||||
for (std::ptrdiff_t i = 0; i < n; ++i) {
|
||||
double sum = v1[i] + v2[i];
|
||||
v1[i] = sum;
|
||||
v2[i] = sum;
|
||||
}
|
||||
}
|
||||
|
||||
double dotProduct(const std::vector<double>& v1, const std::vector<double>& v2) {
|
||||
if (v1.size() != v2.size()) {
|
||||
throw std::invalid_argument("Vectors must have the same size for dot product.");
|
||||
}
|
||||
|
||||
double result = 0.0;
|
||||
const std::ptrdiff_t n = static_cast<std::ptrdiff_t>(v1.size());
|
||||
|
||||
#pragma omp parallel for reduction(+:result)
|
||||
for (std::ptrdiff_t i = 0; i < n; ++i) {
|
||||
result += v1[i] * v2[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void calculate_degrees_and_edges_adj_parallel(
|
||||
const adj_dict_factory& adj,
|
||||
const std::vector<int>& membership,
|
||||
bool directed,
|
||||
int num_communities,
|
||||
double& e,
|
||||
double& m,
|
||||
std::vector<double>& k_out,
|
||||
std::vector<double>& k_in
|
||||
)
|
||||
{
|
||||
const int N = membership.size();
|
||||
|
||||
#pragma omp parallel
|
||||
{
|
||||
double local_e = 0.0;
|
||||
double local_m = 0.0;
|
||||
std::vector<double> local_k_out(num_communities, 0.0);
|
||||
std::vector<double> local_k_in(num_communities, 0.0);
|
||||
double directed_factor = directed ? 1.0 : 2.0;
|
||||
|
||||
#pragma omp for
|
||||
for (int i = 0; i < N; i++) {
|
||||
node_t u = i + 1;
|
||||
int c1 = membership[i];
|
||||
|
||||
auto adj_it = adj.find(u);
|
||||
if (adj_it == adj.end()) continue;
|
||||
auto& u_neighbors = adj_it->second;
|
||||
|
||||
for (auto& v_pair : u_neighbors) {
|
||||
node_t v = v_pair.first;
|
||||
|
||||
if (!directed && u > v) continue;
|
||||
|
||||
int c2 = membership[v - 1];
|
||||
|
||||
double w = 1.0;
|
||||
if (!v_pair.second.empty()) {
|
||||
auto it = v_pair.second.begin();
|
||||
w = it->second;
|
||||
}
|
||||
|
||||
if (c1 == c2) {
|
||||
local_e += directed_factor * w;
|
||||
}
|
||||
|
||||
local_k_out[c1] += w;
|
||||
local_k_in[c2] += w;
|
||||
local_m += w;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma omp critical
|
||||
{
|
||||
e += local_e;
|
||||
m += local_m;
|
||||
for (int i = 0; i < num_communities; i++) {
|
||||
k_out[i] += local_k_out[i];
|
||||
k_in[i] += local_k_in[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The input `communities` may be either:
|
||||
// (a) a membership list: a flat sequence of ints, membership[i] = community id of node (i+1); or
|
||||
// (b) a community list: a sequence of iterables of node ids
|
||||
|
||||
py::object cpp_modularity(py::object G, py::object communities, py::object weight=py::str("weight")) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
bool directed = G.attr("is_directed")().cast<bool>();
|
||||
adj_dict_factory& adj = G_.adj;
|
||||
const int N = G_.node.size();
|
||||
|
||||
|
||||
{
|
||||
bool is_empty_seq = false;
|
||||
try {
|
||||
py::sequence seq = communities.cast<py::sequence>();
|
||||
is_empty_seq = (seq.size() == 0);
|
||||
} catch (const py::cast_error&) {
|
||||
is_empty_seq = false;
|
||||
}
|
||||
if (is_empty_seq) {
|
||||
py::module warnings = py::module::import("warnings");
|
||||
warnings.attr("warn")(
|
||||
"cpp_modularity: received an empty community list; returning Q = 0.0."
|
||||
);
|
||||
return py::float_(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::vector<int> membership_vec;
|
||||
|
||||
bool is_membership = false;
|
||||
py::sequence outer_seq;
|
||||
try {
|
||||
outer_seq = communities.cast<py::sequence>();
|
||||
if (outer_seq.size() > 0) {
|
||||
try {
|
||||
outer_seq[0].cast<int>();
|
||||
is_membership = true;
|
||||
} catch (const py::cast_error&) {
|
||||
is_membership = false;
|
||||
}
|
||||
}
|
||||
} catch (const py::cast_error&) {
|
||||
is_membership = true;
|
||||
}
|
||||
|
||||
if (is_membership) {
|
||||
// Already a membership vector: membership[i] is the community id of the (i+1)-th node.
|
||||
membership_vec = communities.cast<std::vector<int>>();
|
||||
} else {
|
||||
membership_vec = std::vector<int>(N, -1);
|
||||
|
||||
int comm_id = 0;
|
||||
for (auto community_handle : py::iter(communities)) {
|
||||
for (auto node_handle : py::iter(community_handle)) {
|
||||
py::object node = py::reinterpret_borrow<py::object>(node_handle.ptr());
|
||||
if (G_.node_to_id.contains(node)) {
|
||||
int node_id = G_.node_to_id[node].cast<node_t>() - 1;
|
||||
if (node_id >= 0 && node_id < N) {
|
||||
membership_vec[node_id] = comm_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
comm_id++;
|
||||
}
|
||||
}
|
||||
|
||||
int num_communities = membership_vec.size();
|
||||
|
||||
double e = 0.0;
|
||||
double m = 0.0;
|
||||
std::vector<double> k_out(num_communities, 0.0);
|
||||
std::vector<double> k_in(num_communities, 0.0);
|
||||
|
||||
calculate_degrees_and_edges_adj_parallel(adj, membership_vec, directed, num_communities, e, m, k_out, k_in);
|
||||
|
||||
if (!directed) addVectorsInPlace(k_out, k_in);
|
||||
|
||||
// Handle empty graph / zero total edge weight: m == 0 makes
|
||||
// `norm = 1.0 / (directed_factor * m)` divide by zero and produces
|
||||
// inf / nan. Define Q = 0.0 in this degenerate case (no edges -> no
|
||||
// community structure to measure), with a Python warning.
|
||||
if (m == 0.0) {
|
||||
py::module warnings = py::module::import("warnings");
|
||||
warnings.attr("warn")(
|
||||
"cpp_modularity: graph has no edges (m == 0); returning Q = 0.0."
|
||||
);
|
||||
return py::float_(0.0);
|
||||
}
|
||||
|
||||
double directed_factor = directed ? 1.0 : 2.0;
|
||||
double norm = 1.0 / (directed_factor * m);
|
||||
e *= norm;
|
||||
|
||||
double sum_products = dotProduct(k_out, k_in);
|
||||
sum_products *= norm * norm;
|
||||
|
||||
double Q = e - sum_products;
|
||||
|
||||
return py::float_(Q);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
#include <string>
|
||||
|
||||
#include "../../classes/graph.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
py::object cpp_modularity(py::object G, py::object communities, py::object weight = py::str("weight"));
|
||||
@@ -0,0 +1,553 @@
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <random>
|
||||
#include <algorithm>
|
||||
#include <queue>
|
||||
#include <bitset>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
#include <pybind11/numpy.h>
|
||||
#ifdef _OPENMP
|
||||
#include <omp.h>
|
||||
#else
|
||||
#warning "OpenMP is not available: motif counting functions will fall back to single-threaded execution."
|
||||
#endif
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../classes/linkgraph.h"
|
||||
#include "motif.h"
|
||||
|
||||
namespace py = pybind11;
|
||||
using namespace std;
|
||||
|
||||
const int MAX_NODES = 65536;
|
||||
|
||||
struct MotifResult {
|
||||
vector<int> nodes;
|
||||
};
|
||||
|
||||
class MotifEnumerator {
|
||||
private:
|
||||
Graph_L GL;
|
||||
int k;
|
||||
int v_start;
|
||||
vector<int>& id_to_node;
|
||||
mt19937 rng;
|
||||
uniform_real_distribution<double> dist;
|
||||
vector<double> cut_prob;
|
||||
|
||||
vector<int> temp_nexcl;
|
||||
vector<MotifResult> results;
|
||||
bitset<MAX_NODES> nvp_bitset;
|
||||
|
||||
public:
|
||||
vector<int> node_list;
|
||||
vector<int> neighbor_offsets;
|
||||
vector<int> neighbor_data;
|
||||
int n_edges;
|
||||
|
||||
void build_neighbor_structure() {
|
||||
neighbor_offsets.resize(GL.n + 2, 0);
|
||||
for (int i = 1; i <= GL.n; ++i) {
|
||||
int count = 0;
|
||||
for (int e = GL.head[i]; e != -1; e = GL.edges[e].next) {
|
||||
count++;
|
||||
}
|
||||
neighbor_offsets[i + 1] = neighbor_offsets[i] + count;
|
||||
}
|
||||
|
||||
n_edges = neighbor_offsets[GL.n + 1];
|
||||
neighbor_data.resize(n_edges);
|
||||
|
||||
for (int i = 1; i <= GL.n; ++i) {
|
||||
int idx = neighbor_offsets[i];
|
||||
for (int e = GL.head[i]; e != -1; e = GL.edges[e].next) {
|
||||
neighbor_data[idx++] = GL.edges[e].to;
|
||||
}
|
||||
}
|
||||
|
||||
node_list.reserve(GL.n);
|
||||
for (int i = 1; i <= GL.n; ++i) {
|
||||
if (GL.head[i] != -1) {
|
||||
node_list.push_back(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int get_neighbor_count(int v) const {
|
||||
return neighbor_offsets[v + 1] - neighbor_offsets[v];
|
||||
}
|
||||
|
||||
const int* get_neighbors_ptr(int v) const {
|
||||
return neighbor_data.data() + neighbor_offsets[v];
|
||||
}
|
||||
|
||||
void exclusive_neighborhood(int v, const vector<int>& vp, vector<int>& result) {
|
||||
result.clear();
|
||||
|
||||
const int* nbr_ptr = get_neighbors_ptr(v);
|
||||
int nbr_count = get_neighbor_count(v);
|
||||
|
||||
nvp_bitset.reset();
|
||||
for (int node : vp) {
|
||||
nvp_bitset.set(node);
|
||||
const int* node_nbr_ptr = get_neighbors_ptr(node);
|
||||
int node_nbr_count = get_neighbor_count(node);
|
||||
for (int i = 0; i < node_nbr_count; ++i) {
|
||||
nvp_bitset.set(node_nbr_ptr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < nbr_count; ++i) {
|
||||
int neighbor = nbr_ptr[i];
|
||||
if (!nvp_bitset.test(neighbor)) {
|
||||
result.push_back(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void extend_subgraph_local(const vector<int>& Vsubgraph, const vector<int>& Vextension, int local_v_start,
|
||||
vector<int>& local_temp_nexcl, bitset<MAX_NODES>& local_nvp_bitset,
|
||||
vector<MotifResult>& local_results) {
|
||||
if ((int)Vsubgraph.size() == k) {
|
||||
local_results.push_back({Vsubgraph});
|
||||
return;
|
||||
}
|
||||
|
||||
int original_size = Vextension.size();
|
||||
for (int i = original_size - 1; i >= 0; --i) {
|
||||
int w = Vextension[i];
|
||||
|
||||
if (!cut_prob.empty() && (int)cut_prob.size() > (int)Vsubgraph.size() && dist(rng) > cut_prob[Vsubgraph.size()]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
vector<int> new_subgraph;
|
||||
new_subgraph.reserve(Vsubgraph.size() + 1);
|
||||
new_subgraph.assign(Vsubgraph.begin(), Vsubgraph.end());
|
||||
new_subgraph.push_back(w);
|
||||
|
||||
exclusive_neighborhood_local(w, Vsubgraph, local_temp_nexcl, local_nvp_bitset);
|
||||
|
||||
vector<int> next_ext;
|
||||
next_ext.reserve(original_size + local_temp_nexcl.size());
|
||||
for (int j = 0; j < original_size; ++j) {
|
||||
if (j != i) {
|
||||
int u = Vextension[j];
|
||||
if (u > local_v_start) {
|
||||
next_ext.push_back(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int u : local_temp_nexcl) {
|
||||
if (u > local_v_start) {
|
||||
next_ext.push_back(u);
|
||||
}
|
||||
}
|
||||
|
||||
extend_subgraph_local(new_subgraph, next_ext, local_v_start, local_temp_nexcl, local_nvp_bitset, local_results);
|
||||
}
|
||||
}
|
||||
|
||||
int extend_subgraph_count_local(const vector<int>& Vsubgraph, const vector<int>& Vextension, int local_v_start, vector<int>& local_temp_nexcl, bitset<MAX_NODES>& local_nvp_bitset) {
|
||||
if ((int)Vsubgraph.size() == k) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
int original_size = Vextension.size();
|
||||
for (int i = original_size - 1; i >= 0; --i) {
|
||||
int w = Vextension[i];
|
||||
|
||||
vector<int> new_subgraph;
|
||||
new_subgraph.reserve(Vsubgraph.size() + 1);
|
||||
new_subgraph.assign(Vsubgraph.begin(), Vsubgraph.end());
|
||||
new_subgraph.push_back(w);
|
||||
|
||||
exclusive_neighborhood_local(w, Vsubgraph, local_temp_nexcl, local_nvp_bitset);
|
||||
|
||||
vector<int> next_ext;
|
||||
next_ext.reserve(original_size + local_temp_nexcl.size());
|
||||
for (int j = 0; j < original_size; ++j) {
|
||||
if (j != i) {
|
||||
int u = Vextension[j];
|
||||
if (u > local_v_start) {
|
||||
next_ext.push_back(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int u : local_temp_nexcl) {
|
||||
if (u > local_v_start) {
|
||||
next_ext.push_back(u);
|
||||
}
|
||||
}
|
||||
|
||||
count += extend_subgraph_count_local(new_subgraph, next_ext, local_v_start, local_temp_nexcl, local_nvp_bitset);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int extend_subgraph_count(const vector<int>& Vsubgraph, const vector<int>& Vextension) {
|
||||
return extend_subgraph_count_local(Vsubgraph, Vextension, v_start, temp_nexcl, nvp_bitset);
|
||||
}
|
||||
|
||||
void exclusive_neighborhood_local(int v, const vector<int>& vp, vector<int>& result, bitset<MAX_NODES>& local_nvp_bitset) {
|
||||
result.clear();
|
||||
|
||||
const int* nbr_ptr = get_neighbors_ptr(v);
|
||||
int nbr_count = get_neighbor_count(v);
|
||||
|
||||
local_nvp_bitset.reset();
|
||||
for (int node : vp) {
|
||||
local_nvp_bitset.set(node);
|
||||
const int* node_nbr_ptr = get_neighbors_ptr(node);
|
||||
int node_nbr_count = get_neighbor_count(node);
|
||||
for (int i = 0; i < node_nbr_count; ++i) {
|
||||
local_nvp_bitset.set(node_nbr_ptr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < nbr_count; ++i) {
|
||||
int neighbor = nbr_ptr[i];
|
||||
if (!local_nvp_bitset.test(neighbor)) {
|
||||
result.push_back(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
MotifEnumerator(Graph_L gl, int k, vector<int>& id_to_node)
|
||||
: GL(gl), k(k), v_start(0), id_to_node(id_to_node), rng(42), dist(0.0, 1.0) {
|
||||
build_neighbor_structure();
|
||||
temp_nexcl.reserve(gl.n);
|
||||
}
|
||||
|
||||
MotifEnumerator(Graph_L gl, int k, vector<int>& id_to_node, const vector<double>& cut_prob_vec, int seed)
|
||||
: GL(gl), k(k), v_start(0), id_to_node(id_to_node), cut_prob(cut_prob_vec), rng(seed), dist(0.0, 1.0) {
|
||||
build_neighbor_structure();
|
||||
temp_nexcl.reserve(gl.n);
|
||||
}
|
||||
|
||||
py::array_t<int> enumerate() {
|
||||
results.clear();
|
||||
|
||||
if (k == 2) {
|
||||
for (int v : node_list) {
|
||||
if (!cut_prob.empty() && dist(rng) > cut_prob[0]) {
|
||||
continue;
|
||||
}
|
||||
const int* nbr_ptr = get_neighbors_ptr(v);
|
||||
int nbr_count = get_neighbor_count(v);
|
||||
for (int j = 0; j < nbr_count; ++j) {
|
||||
int u = nbr_ptr[j];
|
||||
if (u > v) {
|
||||
results.push_back({vector<int>{v, u}});
|
||||
}
|
||||
}
|
||||
}
|
||||
return convert_results_to_numpy();
|
||||
}
|
||||
|
||||
vector<vector<MotifResult>> thread_results(8);
|
||||
|
||||
#ifdef _OPENMP
|
||||
omp_set_num_threads(8);
|
||||
#endif
|
||||
#pragma omp parallel
|
||||
{
|
||||
int thread_id = 0;
|
||||
#ifdef _OPENMP
|
||||
thread_id = omp_get_thread_num();
|
||||
#endif
|
||||
vector<MotifResult>& local_results = thread_results[thread_id];
|
||||
local_results.reserve(10000);
|
||||
|
||||
#pragma omp for schedule(dynamic)
|
||||
for (int i = 0; i < (int)node_list.size(); ++i) {
|
||||
int v = node_list[i];
|
||||
|
||||
if (!cut_prob.empty() && dist(rng) > cut_prob[0]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int local_v_start = v;
|
||||
vector<int> Vsubgraph;
|
||||
Vsubgraph.reserve(k);
|
||||
Vsubgraph.push_back(v);
|
||||
|
||||
vector<int> Vextension;
|
||||
Vextension.reserve(GL.n);
|
||||
const int* nbr_ptr = get_neighbors_ptr(v);
|
||||
int nbr_count = get_neighbor_count(v);
|
||||
for (int j = 0; j < nbr_count; ++j) {
|
||||
int u = nbr_ptr[j];
|
||||
if (u > v) {
|
||||
Vextension.push_back(u);
|
||||
}
|
||||
}
|
||||
|
||||
vector<int> local_temp_nexcl;
|
||||
local_temp_nexcl.reserve(GL.n);
|
||||
bitset<MAX_NODES> local_nvp_bitset;
|
||||
|
||||
extend_subgraph_local(Vsubgraph, Vextension, local_v_start, local_temp_nexcl, local_nvp_bitset, local_results);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& tr : thread_results) {
|
||||
results.insert(results.end(), tr.begin(), tr.end());
|
||||
}
|
||||
|
||||
return convert_results_to_numpy();
|
||||
}
|
||||
|
||||
void extend_subgraph(const vector<int>& Vsubgraph, const vector<int>& Vextension) {
|
||||
if ((int)Vsubgraph.size() == k) {
|
||||
results.push_back({Vsubgraph});
|
||||
return;
|
||||
}
|
||||
|
||||
int original_size = Vextension.size();
|
||||
for (int i = original_size - 1; i >= 0; --i) {
|
||||
int w = Vextension[i];
|
||||
|
||||
if (w <= v_start) {
|
||||
continue;
|
||||
}
|
||||
|
||||
vector<int> new_subgraph;
|
||||
new_subgraph.reserve(Vsubgraph.size() + 1);
|
||||
new_subgraph.assign(Vsubgraph.begin(), Vsubgraph.end());
|
||||
new_subgraph.push_back(w);
|
||||
|
||||
exclusive_neighborhood(w, Vsubgraph, temp_nexcl);
|
||||
|
||||
vector<int> next_ext;
|
||||
next_ext.reserve(original_size + temp_nexcl.size());
|
||||
for (int j = 0; j < original_size; ++j) {
|
||||
if (j != i) {
|
||||
int u = Vextension[j];
|
||||
if (u > v_start) {
|
||||
next_ext.push_back(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int u : temp_nexcl) {
|
||||
if (u > v_start) {
|
||||
next_ext.push_back(u);
|
||||
}
|
||||
}
|
||||
|
||||
extend_subgraph(new_subgraph, next_ext);
|
||||
}
|
||||
}
|
||||
|
||||
int count_enumerate() {
|
||||
int count = 0;
|
||||
|
||||
if (k == 2) {
|
||||
for (int v : node_list) {
|
||||
const int* nbr_ptr = get_neighbors_ptr(v);
|
||||
int nbr_count = get_neighbor_count(v);
|
||||
for (int j = 0; j < nbr_count; ++j) {
|
||||
int u = nbr_ptr[j];
|
||||
if (u > v) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
#ifdef _OPENMP
|
||||
omp_set_num_threads(8);
|
||||
#endif
|
||||
#pragma omp parallel reduction(+:count)
|
||||
{
|
||||
#pragma omp for schedule(dynamic)
|
||||
for (int i = 0; i < (int)node_list.size(); ++i) {
|
||||
int v = node_list[i];
|
||||
int local_v_start = v;
|
||||
vector<int> Vsubgraph;
|
||||
Vsubgraph.reserve(k);
|
||||
Vsubgraph.push_back(v);
|
||||
|
||||
vector<int> Vextension;
|
||||
Vextension.reserve(GL.n);
|
||||
const int* nbr_ptr = get_neighbors_ptr(v);
|
||||
int nbr_count = get_neighbor_count(v);
|
||||
for (int j = 0; j < nbr_count; ++j) {
|
||||
int u = nbr_ptr[j];
|
||||
if (u > v) {
|
||||
Vextension.push_back(u);
|
||||
}
|
||||
}
|
||||
|
||||
vector<int> local_temp_nexcl;
|
||||
local_temp_nexcl.reserve(GL.n);
|
||||
bitset<MAX_NODES> local_nvp_bitset;
|
||||
|
||||
count += extend_subgraph_count_local(Vsubgraph, Vextension, local_v_start, local_temp_nexcl, local_nvp_bitset);
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
py::list convert_results_to_python() {
|
||||
py::list py_results;
|
||||
for (const auto& result : results) {
|
||||
py::set py_set;
|
||||
for (int node_id : result.nodes) {
|
||||
if (node_id > 0 && node_id < (int)id_to_node.size()) {
|
||||
py_set.add(py::cast(id_to_node[node_id]));
|
||||
}
|
||||
}
|
||||
py_results.append(py_set);
|
||||
}
|
||||
return py_results;
|
||||
}
|
||||
|
||||
py::array_t<int> convert_results_to_numpy() {
|
||||
size_t num_results = results.size();
|
||||
size_t k_size = k;
|
||||
|
||||
int* data = new int[num_results * k_size];
|
||||
|
||||
size_t idx = 0;
|
||||
for (const auto& result : results) {
|
||||
for (size_t i = 0; i < k_size; ++i) {
|
||||
if (i < result.nodes.size() && result.nodes[i] > 0 && result.nodes[i] < (int)id_to_node.size()) {
|
||||
data[idx++] = id_to_node[result.nodes[i]];
|
||||
} else {
|
||||
data[idx++] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a capsule to manage the memory
|
||||
py::capsule capsule(data, [](void* ptr) {
|
||||
delete[] static_cast<int*>(ptr);
|
||||
});
|
||||
|
||||
// Create numpy array with the capsule
|
||||
py::array_t<int> result_array = py::array_t<int>(
|
||||
{num_results, k_size}, // shape
|
||||
{k_size * sizeof(int), sizeof(int)}, // strides
|
||||
data,
|
||||
capsule
|
||||
);
|
||||
|
||||
return result_array;
|
||||
}
|
||||
|
||||
const vector<MotifResult>& get_results() const {
|
||||
return results;
|
||||
}
|
||||
|
||||
size_t get_k() const {
|
||||
return k;
|
||||
}
|
||||
};
|
||||
|
||||
py::list cpp_enumerate_subgraph(py::object G, int k) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
Graph_L GL = G_.linkgraph_structure;
|
||||
|
||||
py::dict id_to_node_py = G_.id_to_node;
|
||||
vector<int> id_to_node_vec;
|
||||
id_to_node_vec.reserve(id_to_node_py.size() + 1);
|
||||
id_to_node_vec.push_back(0);
|
||||
for (int i = 1; i <= (int)id_to_node_py.size(); ++i) {
|
||||
py::object node_obj = id_to_node_py[py::cast(i)];
|
||||
id_to_node_vec.push_back(node_obj.cast<int>());
|
||||
}
|
||||
|
||||
MotifEnumerator enumerator(GL, k, id_to_node_vec);
|
||||
enumerator.enumerate();
|
||||
return enumerator.convert_results_to_python();
|
||||
}
|
||||
|
||||
py::int_ cpp_count_enumerate_subgraph(py::object G, int k) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
Graph_L GL = G_.linkgraph_structure;
|
||||
|
||||
py::dict id_to_node_py = G_.id_to_node;
|
||||
vector<int> id_to_node_vec;
|
||||
id_to_node_vec.reserve(id_to_node_py.size() + 1);
|
||||
id_to_node_vec.push_back(0);
|
||||
for (int i = 1; i <= (int)id_to_node_py.size(); ++i) {
|
||||
py::object node_obj = id_to_node_py[py::cast(i)];
|
||||
id_to_node_vec.push_back(node_obj.cast<int>());
|
||||
}
|
||||
|
||||
MotifEnumerator enumerator(GL, k, id_to_node_vec);
|
||||
int count = enumerator.count_enumerate();
|
||||
return count;
|
||||
}
|
||||
|
||||
py::list cpp_random_enumerate_subgraph(py::object G, int k, py::object cut_prob) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
Graph_L GL = G_.linkgraph_structure;
|
||||
|
||||
py::dict id_to_node_py = G_.id_to_node;
|
||||
vector<int> id_to_node_vec;
|
||||
id_to_node_vec.reserve(id_to_node_py.size() + 1);
|
||||
id_to_node_vec.push_back(0);
|
||||
for (int i = 1; i <= (int)id_to_node_py.size(); ++i) {
|
||||
py::object node_obj = id_to_node_py[py::cast(i)];
|
||||
id_to_node_vec.push_back(node_obj.cast<int>());
|
||||
}
|
||||
|
||||
vector<double> cut_prob_vec;
|
||||
if (py::isinstance<py::list>(cut_prob)) {
|
||||
py::list cut_prob_list = cut_prob.cast<py::list>();
|
||||
cut_prob_vec.reserve(cut_prob_list.size());
|
||||
for (size_t i = 0; i < cut_prob_list.size(); ++i) {
|
||||
cut_prob_vec.push_back(cut_prob_list[i].cast<double>());
|
||||
}
|
||||
} else {
|
||||
throw std::runtime_error("cut_prob must be a list");
|
||||
}
|
||||
|
||||
if (cut_prob_vec.size() != k) {
|
||||
throw py::value_error("length of cut_prob invalid, should equal to k");
|
||||
}
|
||||
|
||||
MotifEnumerator enumerator(GL, k, id_to_node_vec, cut_prob_vec, 42);
|
||||
enumerator.enumerate();
|
||||
return enumerator.convert_results_to_python();
|
||||
}
|
||||
|
||||
py::list cpp_random_enumerate_subgraph_with_seed(py::object G, int k, py::object cut_prob, int seed) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
Graph_L GL = G_.linkgraph_structure;
|
||||
|
||||
py::dict id_to_node_py = G_.id_to_node;
|
||||
vector<int> id_to_node_vec;
|
||||
id_to_node_vec.reserve(id_to_node_py.size() + 1);
|
||||
id_to_node_vec.push_back(0);
|
||||
for (int i = 1; i <= (int)id_to_node_py.size(); ++i) {
|
||||
py::object node_obj = id_to_node_py[py::cast(i)];
|
||||
id_to_node_vec.push_back(node_obj.cast<int>());
|
||||
}
|
||||
|
||||
vector<double> cut_prob_vec;
|
||||
if (py::isinstance<py::list>(cut_prob)) {
|
||||
py::list cut_prob_list = cut_prob.cast<py::list>();
|
||||
cut_prob_vec.reserve(cut_prob_list.size());
|
||||
for (size_t i = 0; i < cut_prob_list.size(); ++i) {
|
||||
cut_prob_vec.push_back(cut_prob_list[i].cast<double>());
|
||||
}
|
||||
} else {
|
||||
throw std::runtime_error("cut_prob must be a list");
|
||||
}
|
||||
|
||||
if (cut_prob_vec.size() != k) {
|
||||
throw py::value_error("length of cut_prob invalid, should equal to k");
|
||||
}
|
||||
|
||||
MotifEnumerator enumerator(GL, k, id_to_node_vec, cut_prob_vec, seed);
|
||||
enumerator.enumerate();
|
||||
return enumerator.convert_results_to_python();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/numpy.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
py::list cpp_enumerate_subgraph(py::object G, int k);
|
||||
|
||||
py::int_ cpp_count_enumerate_subgraph(py::object G, int k);
|
||||
|
||||
py::list cpp_random_enumerate_subgraph(py::object G, int k, py::object cut_prob);
|
||||
@@ -0,0 +1,179 @@
|
||||
#include "subgraph.h"
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../classes/directed_graph.h"
|
||||
#include "../../common/utils.h"
|
||||
|
||||
static node_t _add_one_node_for_subgraph(Graph& self, py::object one_node_for_adding, py::object node_attr = py::dict()) {
|
||||
node_t id;
|
||||
if (self.node_to_id.contains(one_node_for_adding)) {
|
||||
id = self.node_to_id[one_node_for_adding].cast<node_t>();
|
||||
} else {
|
||||
id = ++(self.id);
|
||||
self.id_to_node[py::cast(id)] = one_node_for_adding;
|
||||
self.node_to_id[one_node_for_adding] = id;
|
||||
}
|
||||
py::list items = py::list(node_attr.attr("items")());
|
||||
self.node[id] = node_attr_dict_factory();
|
||||
for (int i = 0; i < len(items); i++) {
|
||||
py::tuple kv = items[i].cast<py::tuple>();
|
||||
py::object pkey = kv[0];
|
||||
std::string weight_key = weight_to_string(pkey);
|
||||
weight_t value = kv[1].cast<weight_t>();
|
||||
self.node[id].insert(std::make_pair(weight_key, value));
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
static void _add_one_edge_for_subgraph(Graph& self, node_t u, node_t v, py::object edge_attr) {
|
||||
py::list items = py::list(edge_attr.attr("items")());
|
||||
self.adj[u][v] = node_attr_dict_factory();
|
||||
self.adj[v][u] = node_attr_dict_factory();
|
||||
for (int i = 0; i < len(items); i++) {
|
||||
py::tuple kv = items[i].cast<py::tuple>();
|
||||
py::object pkey = kv[0];
|
||||
std::string weight_key = weight_to_string(pkey);
|
||||
weight_t value = kv[1].cast<weight_t>();
|
||||
self.adj[u][v].insert(std::make_pair(weight_key, value));
|
||||
self.adj[v][u].insert(std::make_pair(weight_key, value));
|
||||
}
|
||||
}
|
||||
|
||||
static py::object _nodes_subgraph_cpp_graph(py::object self, std::vector<node_t>& node_ids) {
|
||||
Graph& self_ = self.cast<Graph&>();
|
||||
py::object G = self.attr("__class__")();
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
G_.graph.attr("update")(self_.graph);
|
||||
|
||||
py::object nodes = self.attr("nodes");
|
||||
py::object adj = self.attr("adj");
|
||||
|
||||
// 修复:维护原图内部索引到新图内部索引的映射
|
||||
std::unordered_map<node_t, node_t> old_id_to_new_id;
|
||||
|
||||
// 第一遍:添加所有节点并建立映射
|
||||
for (node_t node_id : node_ids) {
|
||||
py::object node = self_.id_to_node[py::cast(node_id)];
|
||||
py::object node_attr = nodes[node];
|
||||
node_t new_id = _add_one_node_for_subgraph(G_, node, node_attr);
|
||||
old_id_to_new_id[node_id] = new_id;
|
||||
}
|
||||
|
||||
// 第二遍:添加边(使用映射后的索引)
|
||||
for (node_t node_id : node_ids) {
|
||||
py::object node = self_.id_to_node[py::cast(node_id)];
|
||||
py::object out_edges = adj[node];
|
||||
py::list edge_items = py::list(out_edges.attr("items")());
|
||||
|
||||
// 获取当前节点的新索引
|
||||
node_t new_u = old_id_to_new_id[node_id];
|
||||
|
||||
for (int j = 0; j < py::len(edge_items); j++) {
|
||||
py::tuple item = edge_items[j].cast<py::tuple>();
|
||||
py::object v = item[0];
|
||||
py::object edge_attr = item[1];
|
||||
|
||||
if (self_.node_to_id.contains(v)) {
|
||||
node_t v_id = self_.node_to_id[v].cast<node_t>();
|
||||
bool v_in_subgraph = std::find(node_ids.begin(), node_ids.end(), v_id) != node_ids.end();
|
||||
if (v_in_subgraph) {
|
||||
// 使用映射后的索引
|
||||
node_t new_v = old_id_to_new_id[v_id];
|
||||
_add_one_edge_for_subgraph(G_, new_u, new_v, edge_attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return G;
|
||||
}
|
||||
|
||||
static node_t DiGraph_add_one_node_for_subgraph(DiGraph& self, py::object one_node_for_adding, py::object node_attr = py::dict()) {
|
||||
node_t id;
|
||||
if (self.node_to_id.contains(one_node_for_adding)) {
|
||||
id = self.node_to_id[one_node_for_adding].cast<node_t>();
|
||||
} else {
|
||||
id = ++(self.id);
|
||||
self.id_to_node[py::cast(id)] = one_node_for_adding;
|
||||
self.node_to_id[one_node_for_adding] = id;
|
||||
}
|
||||
py::list items = py::list(node_attr.attr("items")());
|
||||
self.node[id] = node_attr_dict_factory();
|
||||
for (int i = 0; i < len(items); i++) {
|
||||
py::tuple kv = items[i].cast<py::tuple>();
|
||||
py::object pkey = kv[0];
|
||||
std::string weight_key = weight_to_string(pkey);
|
||||
weight_t value = kv[1].cast<weight_t>();
|
||||
self.node[id].insert(std::make_pair(weight_key, value));
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
static void DiGraph_add_one_edge_for_subgraph(DiGraph& self, node_t u, node_t v, py::object edge_attr) {
|
||||
py::list items = py::list(edge_attr.attr("items")());
|
||||
self.adj[u][v] = node_attr_dict_factory();
|
||||
for (int i = 0; i < len(items); i++) {
|
||||
py::tuple kv = items[i].cast<py::tuple>();
|
||||
py::object pkey = kv[0];
|
||||
std::string weight_key = weight_to_string(pkey);
|
||||
weight_t value = kv[1].cast<weight_t>();
|
||||
self.adj[u][v].insert(std::make_pair(weight_key, value));
|
||||
}
|
||||
}
|
||||
|
||||
static py::object _nodes_subgraph_cpp_digraph(py::object self, std::vector<node_t>& node_ids) {
|
||||
DiGraph& self_ = self.cast<DiGraph&>();
|
||||
py::object G = self.attr("__class__")();
|
||||
DiGraph& G_ = G.cast<DiGraph&>();
|
||||
G_.graph.attr("update")(self_.graph);
|
||||
|
||||
py::object nodes = self.attr("nodes");
|
||||
py::object adj = self.attr("adj");
|
||||
|
||||
// 修复:维护原图内部索引到新图内部索引的映射
|
||||
std::unordered_map<node_t, node_t> old_id_to_new_id;
|
||||
|
||||
// 第一遍:添加所有节点并建立映射
|
||||
for (node_t node_id : node_ids) {
|
||||
py::object node = self_.id_to_node[py::cast(node_id)];
|
||||
py::object node_attr = nodes[node];
|
||||
node_t new_id = DiGraph_add_one_node_for_subgraph(G_, node, node_attr);
|
||||
old_id_to_new_id[node_id] = new_id;
|
||||
}
|
||||
|
||||
// 第二遍:添加边(使用映射后的索引)
|
||||
for (node_t node_id : node_ids) {
|
||||
py::object node = self_.id_to_node[py::cast(node_id)];
|
||||
py::object out_edges = adj[node];
|
||||
py::list edge_items = py::list(out_edges.attr("items")());
|
||||
|
||||
// 获取当前节点的新索引
|
||||
node_t new_u = old_id_to_new_id[node_id];
|
||||
|
||||
for (int j = 0; j < py::len(edge_items); j++) {
|
||||
py::tuple item = edge_items[j].cast<py::tuple>();
|
||||
py::object v = item[0];
|
||||
py::object edge_attr = item[1];
|
||||
|
||||
if (self_.node_to_id.contains(v)) {
|
||||
node_t v_id = self_.node_to_id[v].cast<node_t>();
|
||||
bool v_in_subgraph = std::find(node_ids.begin(), node_ids.end(), v_id) != node_ids.end();
|
||||
if (v_in_subgraph) {
|
||||
// 使用映射后的索引
|
||||
node_t new_v = old_id_to_new_id[v_id];
|
||||
DiGraph_add_one_edge_for_subgraph(G_, new_u, new_v, edge_attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return G;
|
||||
}
|
||||
|
||||
py::object nodes_subgraph_cpp(py::object self, std::vector<node_t>& node_ids) {
|
||||
bool is_directed = self.attr("is_directed")().cast<bool>();
|
||||
if (is_directed) {
|
||||
return _nodes_subgraph_cpp_digraph(self, node_ids);
|
||||
} else {
|
||||
return _nodes_subgraph_cpp_graph(self, node_ids);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include "../../common/common.h"
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../classes/directed_graph.h"
|
||||
#include "../../classes/csr_graph.h"
|
||||
|
||||
py::object nodes_subgraph_cpp(py::object self, std::vector<node_t>& node_ids);
|
||||
@@ -0,0 +1 @@
|
||||
# TEST package init
|
||||
@@ -0,0 +1,139 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
def _get_cpp_module():
|
||||
try:
|
||||
import cpp_easygraph
|
||||
return cpp_easygraph
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def _to_undirected_cpp(G_cpp_digraph):
|
||||
import cpp_easygraph
|
||||
|
||||
G_cpp = cpp_easygraph.Graph()
|
||||
G_cpp.graph.update(G_cpp_digraph.graph)
|
||||
for node, node_attr in G_cpp_digraph.nodes.items():
|
||||
G_cpp.add_node(node, **node_attr)
|
||||
|
||||
seen_edges = set()
|
||||
for u, v, edge_data in G_cpp_digraph.edges:
|
||||
edge = (min(u, v), max(u, v))
|
||||
if edge not in seen_edges:
|
||||
seen_edges.add(edge)
|
||||
G_cpp.add_edge(u, v, **edge_data)
|
||||
|
||||
return G_cpp
|
||||
|
||||
|
||||
class TestLPA(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G_simple = eg.Graph()
|
||||
self.G_simple.add_edges_from([(0, 1), (1, 2), (3, 4)])
|
||||
|
||||
self.G_weighted = eg.Graph()
|
||||
self.G_weighted.add_edges_from([
|
||||
(0, 1, {"weight": 3}),
|
||||
(1, 2, {"weight": 2}),
|
||||
(2, 0, {"weight": 4}),
|
||||
(3, 4, {"weight": 1}),
|
||||
])
|
||||
|
||||
self.G_disconnected = eg.Graph()
|
||||
self.G_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)])
|
||||
|
||||
self.G_single = eg.Graph()
|
||||
self.G_single.add_node(42)
|
||||
|
||||
self.G_empty = eg.Graph()
|
||||
|
||||
def _get_cpp_module(self):
|
||||
return _get_cpp_module()
|
||||
|
||||
def test_lpa_simple(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
result = cpeg.cpp_LPA(self.G_simple.cpp())
|
||||
self.assertIsInstance(result, dict)
|
||||
|
||||
all_nodes = set()
|
||||
for community in result.values():
|
||||
all_nodes.update(community)
|
||||
self.assertEqual(all_nodes, set(self.G_simple.nodes))
|
||||
|
||||
def test_lpa_weighted(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
result = cpeg.cpp_LPA(self.G_weighted.cpp())
|
||||
self.assertIsInstance(result, dict)
|
||||
|
||||
all_nodes = set(self.G_weighted.nodes)
|
||||
result_nodes = set()
|
||||
for community in result.values():
|
||||
result_nodes.update(community)
|
||||
self.assertEqual(all_nodes, result_nodes)
|
||||
|
||||
def test_lpa_disconnected(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
result = cpeg.cpp_LPA(self.G_disconnected.cpp())
|
||||
self.assertIsInstance(result, dict)
|
||||
|
||||
all_nodes = set(self.G_disconnected.nodes)
|
||||
result_nodes = set()
|
||||
for community in result.values():
|
||||
result_nodes.update(community)
|
||||
self.assertEqual(all_nodes, result_nodes)
|
||||
|
||||
def test_lpa_single_node(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
result = cpeg.cpp_LPA(self.G_single.cpp())
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertEqual(len(result), 1)
|
||||
for community in result.values():
|
||||
self.assertIn(42, community)
|
||||
|
||||
def test_lpa_empty_graph(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
result = cpeg.cpp_LPA(self.G_empty.cpp())
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_python_cpp_consistency(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
result_cpp = cpeg.cpp_LPA(self.G_simple.cpp())
|
||||
result_py = eg.functions.community.LPA(self.G_simple)
|
||||
|
||||
self.assertEqual(len(result_cpp), len(result_py))
|
||||
|
||||
cpp_nodes = set()
|
||||
for community in result_cpp.values():
|
||||
cpp_nodes.update(community)
|
||||
|
||||
py_nodes = set()
|
||||
for community in result_py.values():
|
||||
py_nodes.update(community)
|
||||
|
||||
self.assertEqual(cpp_nodes, py_nodes)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,254 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
def _get_cpp_module():
|
||||
"""获取cpp_easygraph模块"""
|
||||
try:
|
||||
import cpp_easygraph
|
||||
return cpp_easygraph
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def _to_undirected_cpp(G_cpp_digraph):
|
||||
"""将C++ DiGraph转换为C++ Graph无向图"""
|
||||
import cpp_easygraph
|
||||
|
||||
G_cpp = cpp_easygraph.Graph()
|
||||
G_cpp.graph.update(G_cpp_digraph.graph)
|
||||
for node, node_attr in G_cpp_digraph.nodes.items():
|
||||
G_cpp.add_node(node, **node_attr)
|
||||
|
||||
seen_edges = set()
|
||||
for u, v, edge_data in G_cpp_digraph.edges:
|
||||
edge = (min(u, v), max(u, v))
|
||||
if edge not in seen_edges:
|
||||
seen_edges.add(edge)
|
||||
G_cpp.add_edge(u, v, **edge_data)
|
||||
|
||||
return G_cpp
|
||||
|
||||
|
||||
def _csr_dict_to_graph(csr_dict):
|
||||
import cpp_easygraph
|
||||
|
||||
if not csr_dict or 'nodes' not in csr_dict:
|
||||
return cpp_easygraph.Graph()
|
||||
|
||||
G = cpp_easygraph.Graph()
|
||||
|
||||
nodes = csr_dict['nodes']
|
||||
for node in nodes:
|
||||
G.add_node(node)
|
||||
|
||||
V = csr_dict['V']
|
||||
E = csr_dict['E']
|
||||
W = csr_dict.get('W', [1.0] * len(E))
|
||||
|
||||
for u in range(len(V) - 1):
|
||||
start = V[u]
|
||||
end = V[u + 1]
|
||||
for i in range(start, end):
|
||||
v_idx = E[i]
|
||||
weight = W[i] if i < len(W) else 1.0
|
||||
G.add_edge(nodes[u], nodes[v_idx], weight=weight)
|
||||
|
||||
return G
|
||||
|
||||
|
||||
class TestEgoGraph(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.simple_graph = eg.Graph()
|
||||
self.simple_graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)])
|
||||
|
||||
self.directed_graph = eg.DiGraph()
|
||||
self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
|
||||
self.weighted_graph = eg.Graph()
|
||||
self.weighted_graph.add_edges_from(
|
||||
[(0, 1, {"weight": 1}), (1, 2, {"weight": 2}), (2, 3, {"weight": 3})]
|
||||
)
|
||||
|
||||
self.disconnected_graph = eg.Graph()
|
||||
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)])
|
||||
|
||||
self.single_node_graph = eg.Graph()
|
||||
self.single_node_graph.add_node(42)
|
||||
|
||||
def _get_cpp_module(self):
|
||||
return _get_cpp_module()
|
||||
|
||||
def test_ego_graph_simple_radius_1(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.simple_graph.cpp()
|
||||
ego = cpeg.cpp_ego_graph(G_cpp, 2, radius=1)
|
||||
self.assertIsInstance(ego, type(G_cpp))
|
||||
self.assertIn(2, ego.nodes)
|
||||
self.assertIn(1, ego.nodes)
|
||||
self.assertIn(3, ego.nodes)
|
||||
|
||||
def test_ego_graph_simple_radius_2(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.simple_graph.cpp()
|
||||
ego = cpeg.cpp_ego_graph(G_cpp, 2, radius=2)
|
||||
self.assertIsInstance(ego, type(G_cpp))
|
||||
self.assertIn(0, ego.nodes)
|
||||
self.assertIn(1, ego.nodes)
|
||||
self.assertIn(2, ego.nodes)
|
||||
self.assertIn(3, ego.nodes)
|
||||
self.assertIn(4, ego.nodes)
|
||||
|
||||
def test_ego_graph_directed(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.directed_graph.cpp()
|
||||
ego = cpeg.cpp_ego_graph(G_cpp, 1, radius=1)
|
||||
self.assertIsInstance(ego, type(G_cpp))
|
||||
self.assertIn(1, ego.nodes)
|
||||
self.assertIn(2, ego.nodes)
|
||||
|
||||
def test_ego_graph_weighted_with_distance(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.weighted_graph.cpp()
|
||||
G_cpp.generate_linkgraph('weight')
|
||||
ego = cpeg.cpp_ego_graph(G_cpp, 0, radius=2, distance="weight")
|
||||
self.assertIsInstance(ego, type(G_cpp))
|
||||
self.assertIn(0, ego.nodes)
|
||||
self.assertIn(1, ego.nodes)
|
||||
|
||||
def test_ego_graph_disconnected(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.disconnected_graph.cpp()
|
||||
ego = cpeg.cpp_ego_graph(G_cpp, 0, radius=1)
|
||||
self.assertIsInstance(ego, type(G_cpp))
|
||||
self.assertIn(0, ego.nodes)
|
||||
self.assertIn(1, ego.nodes)
|
||||
|
||||
def test_ego_graph_single_node(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.single_node_graph.cpp()
|
||||
ego = cpeg.cpp_ego_graph(G_cpp, 42, radius=1)
|
||||
self.assertIsInstance(ego, type(G_cpp))
|
||||
self.assertIn(42, ego.nodes)
|
||||
|
||||
def test_ego_graph_center_false(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.simple_graph.cpp()
|
||||
ego = cpeg.cpp_ego_graph(G_cpp, 2, radius=1, center=False)
|
||||
self.assertIsInstance(ego, type(G_cpp))
|
||||
self.assertNotIn(2, ego.nodes)
|
||||
self.assertIn(1, ego.nodes)
|
||||
self.assertIn(3, ego.nodes)
|
||||
|
||||
def test_python_cpp_consistency(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.simple_graph.cpp()
|
||||
ego_py = eg.functions.community.ego_graph(self.simple_graph, 2, radius=1)
|
||||
ego_cpp = cpeg.cpp_ego_graph(G_cpp, 2, radius=1)
|
||||
|
||||
self.assertEqual(set(ego_py.nodes), set(ego_cpp.nodes))
|
||||
|
||||
|
||||
class TestEgoGraphCSR(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.simple_graph = eg.Graph()
|
||||
self.simple_graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)])
|
||||
|
||||
self.disconnected_graph = eg.Graph()
|
||||
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)])
|
||||
|
||||
self.single_node_graph = eg.Graph()
|
||||
self.single_node_graph.add_node(42)
|
||||
|
||||
def _get_cpp_module(self):
|
||||
return _get_cpp_module()
|
||||
|
||||
def test_ego_graph_csr_simple(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.simple_graph.cpp()
|
||||
G_cpp.generate_linkgraph('weight')
|
||||
ego_csr_dict = cpeg.cpp_ego_graph_csr(G_cpp, 2, radius=1)
|
||||
|
||||
ego = _csr_dict_to_graph(ego_csr_dict)
|
||||
|
||||
self.assertTrue(hasattr(ego, 'nodes'))
|
||||
self.assertIn(2, ego.nodes)
|
||||
|
||||
def test_ego_graph_csr_disconnected(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.disconnected_graph.cpp()
|
||||
G_cpp.generate_linkgraph('weight')
|
||||
ego_csr_dict = cpeg.cpp_ego_graph_csr(G_cpp, 0, radius=1)
|
||||
|
||||
ego = _csr_dict_to_graph(ego_csr_dict)
|
||||
|
||||
self.assertTrue(hasattr(ego, 'nodes'))
|
||||
self.assertIn(0, ego.nodes)
|
||||
self.assertIn(1, ego.nodes)
|
||||
|
||||
def test_ego_graph_csr_single_node(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.single_node_graph.cpp()
|
||||
G_cpp.generate_linkgraph('weight')
|
||||
ego_csr_dict = cpeg.cpp_ego_graph_csr(G_cpp, 42, radius=1)
|
||||
|
||||
ego = _csr_dict_to_graph(ego_csr_dict)
|
||||
|
||||
self.assertTrue(hasattr(ego, 'nodes'))
|
||||
self.assertIn(42, ego.nodes)
|
||||
|
||||
def test_csr_vs_original_consistency(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.simple_graph.cpp()
|
||||
G_cpp.generate_linkgraph('weight')
|
||||
|
||||
ego_original = cpeg.cpp_ego_graph(G_cpp, 2, radius=1, undirected=False)
|
||||
ego_csr_dict = cpeg.cpp_ego_graph_csr(G_cpp, 2, radius=1)
|
||||
|
||||
ego_csr = _csr_dict_to_graph(ego_csr_dict)
|
||||
|
||||
self.assertIn(2, ego_original.nodes)
|
||||
self.assertIn(2, ego_csr.nodes)
|
||||
|
||||
self.assertTrue(set(ego_csr.nodes).issubset(set(ego_original.nodes) | {4}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,143 @@
|
||||
import random
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
def _get_cpp_module():
|
||||
try:
|
||||
import cpp_easygraph
|
||||
return cpp_easygraph
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def _to_undirected_cpp(G_cpp_digraph):
|
||||
import cpp_easygraph
|
||||
|
||||
G_cpp = cpp_easygraph.Graph()
|
||||
G_cpp.graph.update(G_cpp_digraph.graph)
|
||||
for node, node_attr in G_cpp_digraph.nodes.items():
|
||||
G_cpp.add_node(node, **node_attr)
|
||||
|
||||
seen_edges = set()
|
||||
for u, v, edge_data in G_cpp_digraph.edges:
|
||||
edge = (min(u, v), max(u, v))
|
||||
if edge not in seen_edges:
|
||||
seen_edges.add(edge)
|
||||
G_cpp.add_edge(u, v, **edge_data)
|
||||
|
||||
return G_cpp
|
||||
|
||||
|
||||
class TestEnumerateSubgraph(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G_triangle = eg.Graph()
|
||||
self.G_triangle.add_edges_from([(1, 2), (2, 3), (3, 1)])
|
||||
|
||||
self.G_complex = eg.Graph()
|
||||
self.G_complex.add_edges_from([
|
||||
(1, 3), (2, 3), (3, 4), (4, 5), (3, 5)
|
||||
])
|
||||
|
||||
self.G_empty = eg.Graph()
|
||||
|
||||
self.G_small = eg.Graph()
|
||||
self.G_small.add_edges_from([(1, 2)])
|
||||
|
||||
def _get_cpp_module(self):
|
||||
return _get_cpp_module()
|
||||
|
||||
def test_enumerate_subgraph_k3(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
motifs = cpeg.cpp_enumerate_subgraph(self.G_complex.cpp(), 3)
|
||||
self.assertIsInstance(motifs, list)
|
||||
for m in motifs:
|
||||
self.assertEqual(len(m), 3)
|
||||
|
||||
def test_enumerate_subgraph_k4(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
motifs = cpeg.cpp_enumerate_subgraph(self.G_complex.cpp(), 4)
|
||||
for m in motifs:
|
||||
self.assertEqual(len(m), 4)
|
||||
|
||||
def test_enumerate_subgraph_empty_graph(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
motifs = cpeg.cpp_enumerate_subgraph(self.G_empty.cpp(), 3)
|
||||
self.assertEqual(motifs, [])
|
||||
|
||||
def test_enumerate_subgraph_k_larger_than_graph(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
motifs = cpeg.cpp_enumerate_subgraph(self.G_small.cpp(), 3)
|
||||
self.assertEqual(motifs, [])
|
||||
|
||||
def test_python_cpp_consistency(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
motifs_py = eg.enumerate_subgraph(self.G_complex, 3)
|
||||
motifs_py = [frozenset(m) for m in motifs_py]
|
||||
motifs_cpp = cpeg.cpp_enumerate_subgraph(self.G_complex.cpp(), 3)
|
||||
motifs_cpp = [frozenset(m) for m in motifs_cpp]
|
||||
|
||||
self.assertEqual(set(motifs_py), set(motifs_cpp))
|
||||
|
||||
|
||||
class TestRandomEnumerateSubgraph(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G_complex = eg.Graph()
|
||||
self.G_complex.add_edges_from([
|
||||
(1, 3), (2, 3), (3, 4), (4, 5), (3, 5)
|
||||
])
|
||||
|
||||
self.G_empty = eg.Graph()
|
||||
|
||||
def _get_cpp_module(self):
|
||||
return _get_cpp_module()
|
||||
|
||||
def test_random_enumerate_valid_cut_prob(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
cut_prob = [1.0, 1.0, 1.0]
|
||||
motifs = cpeg.cpp_random_enumerate_subgraph(self.G_complex.cpp(), 3, cut_prob)
|
||||
self.assertIsInstance(motifs, list)
|
||||
|
||||
def test_random_enumerate_zero_cut_prob(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
cut_prob = [0.0, 0.0, 0.0]
|
||||
motifs = cpeg.cpp_random_enumerate_subgraph(self.G_complex.cpp(), 3, cut_prob)
|
||||
self.assertEqual(motifs, [])
|
||||
|
||||
def test_random_enumerate_different_results(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
cut_prob = [1.0, 1.0, 1.0]
|
||||
motifs1 = cpeg.cpp_random_enumerate_subgraph(self.G_complex.cpp(), 3, cut_prob)
|
||||
motifs2 = cpeg.cpp_random_enumerate_subgraph(self.G_complex.cpp(), 3, cut_prob)
|
||||
|
||||
self.assertIsInstance(motifs1, list)
|
||||
self.assertIsInstance(motifs2, list)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,117 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
def _get_cpp_module():
|
||||
try:
|
||||
import cpp_easygraph
|
||||
return cpp_easygraph
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def _to_undirected_cpp(G_cpp_digraph):
|
||||
import cpp_easygraph
|
||||
|
||||
G_cpp = cpp_easygraph.Graph()
|
||||
G_cpp.graph.update(G_cpp_digraph.graph)
|
||||
for node, node_attr in G_cpp_digraph.nodes.items():
|
||||
G_cpp.add_node(node, **node_attr)
|
||||
|
||||
seen_edges = set()
|
||||
for u, v, edge_data in G_cpp_digraph.edges:
|
||||
edge = (min(u, v), max(u, v))
|
||||
if edge not in seen_edges:
|
||||
seen_edges.add(edge)
|
||||
G_cpp.add_edge(u, v, **edge_data)
|
||||
|
||||
return G_cpp
|
||||
|
||||
|
||||
class TestGreedyModularity(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G_simple = eg.Graph()
|
||||
self.G_simple.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)])
|
||||
|
||||
self.G_disconnected = eg.Graph()
|
||||
self.G_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)])
|
||||
|
||||
self.G_weighted = eg.Graph()
|
||||
self.G_weighted.add_edge(0, 1, weight=5)
|
||||
self.G_weighted.add_edge(1, 2, weight=3)
|
||||
self.G_weighted.add_edge(2, 0, weight=2)
|
||||
self.G_weighted.add_edge(3, 4, weight=1)
|
||||
|
||||
self.G_single = eg.Graph()
|
||||
self.G_single.add_node(42)
|
||||
|
||||
self.G_empty = eg.Graph()
|
||||
|
||||
def _get_cpp_module(self):
|
||||
return _get_cpp_module()
|
||||
|
||||
def test_greedy_simple(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = cpeg.cpp_greedy_modularity_communities(self.G_simple.cpp())
|
||||
self.assertIsInstance(communities, list)
|
||||
flat = {node for comm in communities for node in comm}
|
||||
self.assertSetEqual(flat, set(self.G_simple.nodes))
|
||||
|
||||
def test_greedy_weighted(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = cpeg.cpp_greedy_modularity_communities(self.G_weighted.cpp(), weight="weight")
|
||||
self.assertIsInstance(communities, list)
|
||||
flat = {node for comm in communities for node in comm}
|
||||
self.assertSetEqual(flat, set(self.G_weighted.nodes))
|
||||
|
||||
def test_greedy_disconnected(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = cpeg.cpp_greedy_modularity_communities(self.G_disconnected.cpp())
|
||||
self.assertIsInstance(communities, list)
|
||||
flat = {node for comm in communities for node in comm}
|
||||
self.assertSetEqual(flat, set(self.G_disconnected.nodes))
|
||||
|
||||
def test_greedy_single_node(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = cpeg.cpp_greedy_modularity_communities(self.G_single.cpp())
|
||||
self.assertEqual(len(communities), 1)
|
||||
self.assertIn(42, communities[0])
|
||||
|
||||
def test_greedy_empty_graph(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = cpeg.cpp_greedy_modularity_communities(self.G_empty.cpp())
|
||||
self.assertEqual(communities, [])
|
||||
|
||||
def test_python_cpp_consistency(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities_cpp = cpeg.cpp_greedy_modularity_communities(self.G_simple.cpp())
|
||||
communities_py = eg.functions.community.greedy_modularity_communities(self.G_simple)
|
||||
|
||||
self.assertEqual(len(communities_cpp), len(communities_py))
|
||||
|
||||
flat_cpp = {node for comm in communities_cpp for node in comm}
|
||||
flat_py = {node for comm in communities_py for node in comm}
|
||||
self.assertEqual(flat_cpp, flat_py)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,188 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
def _get_cpp_module():
|
||||
try:
|
||||
import cpp_easygraph
|
||||
return cpp_easygraph
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def _to_undirected_cpp(G_cpp_digraph):
|
||||
import cpp_easygraph
|
||||
|
||||
G_cpp = cpp_easygraph.Graph()
|
||||
G_cpp.graph.update(G_cpp_digraph.graph)
|
||||
for node, node_attr in G_cpp_digraph.nodes.items():
|
||||
G_cpp.add_node(node, **node_attr)
|
||||
|
||||
seen_edges = set()
|
||||
for u, v, edge_data in G_cpp_digraph.edges:
|
||||
edge = (min(u, v), max(u, v))
|
||||
if edge not in seen_edges:
|
||||
seen_edges.add(edge)
|
||||
G_cpp.add_edge(u, v, **edge_data)
|
||||
|
||||
return G_cpp
|
||||
|
||||
|
||||
class TestLocalsearch(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G_simple = eg.Graph()
|
||||
self.G_simple.add_edges_from([(0, 2), (0, 3), (0, 4), (0, 5), (1, 2), (1, 3), (1, 4), (1, 5)])
|
||||
|
||||
self.G_disconnected = eg.Graph()
|
||||
self.G_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)])
|
||||
|
||||
self.G_single = eg.Graph()
|
||||
self.G_single.add_node(42)
|
||||
|
||||
self.G_empty = eg.Graph()
|
||||
|
||||
def _get_cpp_module(self):
|
||||
return _get_cpp_module()
|
||||
|
||||
def test_localsearch_simple(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.G_simple.cpp()
|
||||
G_cpp.generate_linkgraph('weight')
|
||||
result = cpeg.cpp_localsearch(G_cpp, seed=163)
|
||||
|
||||
self.assertIsInstance(result, tuple)
|
||||
self.assertEqual(len(result), 6)
|
||||
y_partition = result[3]
|
||||
self.assertIsInstance(y_partition, dict)
|
||||
|
||||
all_nodes = set(self.G_simple.nodes)
|
||||
result_nodes = set(y_partition.keys())
|
||||
self.assertEqual(all_nodes, result_nodes)
|
||||
|
||||
def test_localsearch_disconnected(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.G_disconnected.cpp()
|
||||
G_cpp.generate_linkgraph('weight')
|
||||
result = cpeg.cpp_localsearch(G_cpp, seed=163)
|
||||
|
||||
self.assertIsInstance(result, tuple)
|
||||
self.assertEqual(len(result), 6)
|
||||
y_partition = result[3]
|
||||
self.assertIsInstance(y_partition, dict)
|
||||
|
||||
all_nodes = set(self.G_disconnected.nodes)
|
||||
result_nodes = set(y_partition.keys())
|
||||
self.assertEqual(all_nodes, result_nodes)
|
||||
|
||||
def test_localsearch_single_node(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.G_single.cpp()
|
||||
G_cpp.generate_linkgraph('weight')
|
||||
result = cpeg.cpp_localsearch(G_cpp, seed=163)
|
||||
|
||||
self.assertIsInstance(result, tuple)
|
||||
self.assertEqual(len(result), 6)
|
||||
y_partition = result[3]
|
||||
self.assertIsInstance(y_partition, dict)
|
||||
self.assertIn(42, y_partition)
|
||||
|
||||
def test_localsearch_empty_graph(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.G_empty.cpp()
|
||||
G_cpp.generate_linkgraph('weight')
|
||||
result = cpeg.cpp_localsearch(G_cpp, seed=163)
|
||||
|
||||
self.assertIsInstance(result, tuple)
|
||||
self.assertEqual(len(result), 6)
|
||||
y_partition = result[3]
|
||||
self.assertIsInstance(y_partition, dict)
|
||||
|
||||
def test_localsearch_maximum_tree_true(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.G_simple.cpp()
|
||||
G_cpp.generate_linkgraph('weight')
|
||||
result = cpeg.cpp_localsearch(G_cpp, maximum_tree=True, seed=163)
|
||||
|
||||
self.assertIsInstance(result, tuple)
|
||||
self.assertEqual(len(result), 6)
|
||||
y_partition = result[3]
|
||||
self.assertIsInstance(y_partition, dict)
|
||||
|
||||
def test_localsearch_maximum_tree_false(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.G_simple.cpp()
|
||||
G_cpp.generate_linkgraph('weight')
|
||||
result = cpeg.cpp_localsearch(G_cpp, maximum_tree=False, seed=163)
|
||||
|
||||
self.assertIsInstance(result, tuple)
|
||||
self.assertEqual(len(result), 6)
|
||||
y_partition = result[3]
|
||||
self.assertIsInstance(y_partition, dict)
|
||||
|
||||
def test_localsearch_auto_choose_centers(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.G_simple.cpp()
|
||||
G_cpp.generate_linkgraph('weight')
|
||||
result = cpeg.cpp_localsearch(G_cpp, auto_choose_centers=True, seed=163)
|
||||
|
||||
self.assertIsInstance(result, tuple)
|
||||
self.assertEqual(len(result), 6)
|
||||
y_partition = result[3]
|
||||
self.assertIsInstance(y_partition, dict)
|
||||
|
||||
def test_localsearch_center_num(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.G_simple.cpp()
|
||||
G_cpp.generate_linkgraph('weight')
|
||||
result = cpeg.cpp_localsearch(G_cpp, center_num=2, seed=163)
|
||||
|
||||
self.assertIsInstance(result, tuple)
|
||||
self.assertEqual(len(result), 6)
|
||||
y_partition = result[3]
|
||||
self.assertIsInstance(y_partition, dict)
|
||||
|
||||
def test_localsearch_with_seed_reproducibility(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
G_cpp = self.G_simple.cpp()
|
||||
G_cpp.generate_linkgraph('weight')
|
||||
|
||||
result1 = cpeg.cpp_localsearch(G_cpp, seed=42)
|
||||
result2 = cpeg.cpp_localsearch(G_cpp, seed=42)
|
||||
|
||||
self.assertIsInstance(result1, tuple)
|
||||
self.assertIsInstance(result2, tuple)
|
||||
y_partition1 = result1[3]
|
||||
y_partition2 = result2[3]
|
||||
self.assertEqual(y_partition1.keys(), y_partition2.keys())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,178 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
def _get_cpp_module():
|
||||
try:
|
||||
import cpp_easygraph
|
||||
return cpp_easygraph
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def _to_undirected_cpp(G_cpp_digraph):
|
||||
import cpp_easygraph
|
||||
|
||||
G_cpp = cpp_easygraph.Graph()
|
||||
G_cpp.graph.update(G_cpp_digraph.graph)
|
||||
for node, node_attr in G_cpp_digraph.nodes.items():
|
||||
G_cpp.add_node(node, **node_attr)
|
||||
|
||||
seen_edges = set()
|
||||
for u, v, edge_data in G_cpp_digraph.edges:
|
||||
edge = (min(u, v), max(u, v))
|
||||
if edge not in seen_edges:
|
||||
seen_edges.add(edge)
|
||||
G_cpp.add_edge(u, v, **edge_data)
|
||||
|
||||
return G_cpp
|
||||
|
||||
|
||||
class TestLouvainCommunities(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G_simple = eg.Graph()
|
||||
self.G_simple.add_edges_from([(0, 1), (1, 2), (3, 4)])
|
||||
|
||||
self.G_weighted = eg.Graph()
|
||||
self.G_weighted.add_edge(0, 1, weight=5)
|
||||
self.G_weighted.add_edge(1, 2, weight=3)
|
||||
self.G_weighted.add_edge(3, 4, weight=2)
|
||||
|
||||
self.G_disconnected = eg.Graph()
|
||||
self.G_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)])
|
||||
|
||||
self.G_single = eg.Graph()
|
||||
self.G_single.add_node(42)
|
||||
|
||||
self.G_empty = eg.Graph()
|
||||
|
||||
def _get_cpp_module(self):
|
||||
return _get_cpp_module()
|
||||
|
||||
def test_louvain_simple(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = cpeg.cpp_louvain_communities(self.G_simple.cpp())
|
||||
self.assertIsInstance(communities, list)
|
||||
flat = {node for comm in communities for node in comm}
|
||||
self.assertSetEqual(flat, set(self.G_simple.nodes))
|
||||
|
||||
def test_louvain_weighted(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = cpeg.cpp_louvain_communities(self.G_weighted.cpp(), weight="weight")
|
||||
self.assertIsInstance(communities, list)
|
||||
flat = {node for comm in communities for node in comm}
|
||||
self.assertSetEqual(flat, set(self.G_weighted.nodes))
|
||||
|
||||
def test_louvain_disconnected(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = cpeg.cpp_louvain_communities(self.G_disconnected.cpp())
|
||||
self.assertIsInstance(communities, list)
|
||||
flat = {node for comm in communities for node in comm}
|
||||
self.assertSetEqual(flat, set(self.G_disconnected.nodes))
|
||||
|
||||
def test_louvain_single_node(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = cpeg.cpp_louvain_communities(self.G_single.cpp())
|
||||
self.assertEqual(len(communities), 1)
|
||||
self.assertIn(42, communities[0])
|
||||
|
||||
def test_louvain_empty_graph(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = cpeg.cpp_louvain_communities(self.G_empty.cpp())
|
||||
self.assertEqual(communities, [])
|
||||
|
||||
def test_louvain_resolution_parameter(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities1 = cpeg.cpp_louvain_communities(self.G_simple.cpp(), resolution=1.0)
|
||||
communities2 = cpeg.cpp_louvain_communities(self.G_simple.cpp(), resolution=0.5)
|
||||
|
||||
self.assertIsInstance(communities1, list)
|
||||
self.assertIsInstance(communities2, list)
|
||||
|
||||
flat1 = {node for comm in communities1 for node in comm}
|
||||
flat2 = {node for comm in communities2 for node in comm}
|
||||
self.assertEqual(flat1, flat2)
|
||||
|
||||
def test_python_cpp_consistency(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities_cpp = cpeg.cpp_louvain_communities(self.G_simple.cpp())
|
||||
communities_py = eg.functions.community.louvain_communities(self.G_simple)
|
||||
|
||||
self.assertEqual(len(communities_cpp), len(communities_py))
|
||||
|
||||
flat_cpp = {node for comm in communities_cpp for node in comm}
|
||||
flat_py = {node for comm in communities_py for node in comm}
|
||||
self.assertEqual(flat_cpp, flat_py)
|
||||
|
||||
|
||||
class TestLouvainCommunitiesSerial(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G_simple = eg.Graph()
|
||||
self.G_simple.add_edges_from([(0, 1), (1, 2), (3, 4)])
|
||||
|
||||
self.G_weighted = eg.Graph()
|
||||
self.G_weighted.add_edge(0, 1, weight=5)
|
||||
self.G_weighted.add_edge(1, 2, weight=3)
|
||||
self.G_weighted.add_edge(3, 4, weight=2)
|
||||
|
||||
def _get_cpp_module(self):
|
||||
return _get_cpp_module()
|
||||
|
||||
def test_louvain_serial_simple(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = cpeg.cpp_louvain_communities_serial(self.G_simple.cpp())
|
||||
self.assertIsInstance(communities, list)
|
||||
flat = {node for comm in communities for node in comm}
|
||||
self.assertSetEqual(flat, set(self.G_simple.nodes))
|
||||
|
||||
def test_louvain_serial_weighted(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = cpeg.cpp_louvain_communities_serial(self.G_weighted.cpp(), weight="weight")
|
||||
self.assertIsInstance(communities, list)
|
||||
flat = {node for comm in communities for node in comm}
|
||||
self.assertSetEqual(flat, set(self.G_weighted.nodes))
|
||||
|
||||
def test_parallel_vs_serial_consistency(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities_parallel = cpeg.cpp_louvain_communities(self.G_simple.cpp())
|
||||
communities_serial = cpeg.cpp_louvain_communities_serial(self.G_simple.cpp())
|
||||
|
||||
flat_parallel = {frozenset(comm) for comm in communities_parallel}
|
||||
flat_serial = {frozenset(comm) for comm in communities_serial}
|
||||
|
||||
self.assertEqual(flat_parallel, flat_serial)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,161 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
def _get_cpp_module():
|
||||
"""获取cpp_easygraph模块"""
|
||||
try:
|
||||
import cpp_easygraph
|
||||
return cpp_easygraph
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def _to_undirected_cpp(G_cpp_digraph):
|
||||
"""将C++ DiGraph转换为C++ Graph无向图"""
|
||||
import cpp_easygraph
|
||||
|
||||
G_cpp = cpp_easygraph.Graph()
|
||||
G_cpp.graph.update(G_cpp_digraph.graph)
|
||||
for node, node_attr in G_cpp_digraph.nodes.items():
|
||||
G_cpp.add_node(node, **node_attr)
|
||||
|
||||
seen_edges = set()
|
||||
for u, v, edge_data in G_cpp_digraph.edges:
|
||||
edge = (min(u, v), max(u, v))
|
||||
if edge not in seen_edges:
|
||||
seen_edges.add(edge)
|
||||
G_cpp.add_edge(u, v, **edge_data)
|
||||
|
||||
return G_cpp
|
||||
|
||||
|
||||
def _communities_to_membership(G, communities):
|
||||
|
||||
if not isinstance(communities, list):
|
||||
communities = list(communities)
|
||||
|
||||
N = G.number_of_nodes()
|
||||
membership = [-1] * N
|
||||
node_index = G.node_index
|
||||
|
||||
for comm_id, community in enumerate(communities):
|
||||
for node in community:
|
||||
if node in node_index:
|
||||
node_id = node_index[node]
|
||||
membership[node_id] = comm_id
|
||||
|
||||
return membership
|
||||
|
||||
|
||||
class TestModularity(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G = eg.Graph()
|
||||
self.G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)])
|
||||
|
||||
self.DG = eg.DiGraph()
|
||||
self.DG.add_edges_from([(0, 1), (1, 2), (2, 0)])
|
||||
|
||||
self.G_weighted = eg.Graph()
|
||||
self.G_weighted.add_edge(0, 1, weight=2)
|
||||
self.G_weighted.add_edge(1, 2, weight=3)
|
||||
self.G_weighted.add_edge(2, 0, weight=1)
|
||||
|
||||
self.G_selfloop = eg.Graph()
|
||||
self.G_selfloop.add_edges_from([(0, 0), (1, 1), (0, 1)])
|
||||
|
||||
self.G_empty = eg.Graph()
|
||||
|
||||
self.config = type('Config', (), {
|
||||
'is_directed': False
|
||||
})()
|
||||
|
||||
def _get_cpp_module(self):
|
||||
return _get_cpp_module()
|
||||
|
||||
def test_undirected_modularity(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = [{0, 1}, {2, 3}]
|
||||
membership = _communities_to_membership(self.G, communities)
|
||||
q = cpeg.cpp_modularity(self.G.cpp(), membership)
|
||||
self.assertIsInstance(q, float)
|
||||
self.assertGreaterEqual(q, -1.0)
|
||||
self.assertLessEqual(q, 1.0)
|
||||
|
||||
def test_directed_modularity(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = [{0, 1, 2}]
|
||||
membership = _communities_to_membership(self.DG, communities)
|
||||
q = cpeg.cpp_modularity(self.DG.cpp(), membership)
|
||||
self.assertIsInstance(q, float)
|
||||
|
||||
def test_weighted_graph(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = [{0, 1}, {2}]
|
||||
membership = _communities_to_membership(self.G_weighted, communities)
|
||||
q = cpeg.cpp_modularity(self.G_weighted.cpp(), membership, weight="weight")
|
||||
self.assertIsInstance(q, float)
|
||||
|
||||
def test_self_loops(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = [{0, 1}]
|
||||
membership = _communities_to_membership(self.G_selfloop, communities)
|
||||
q = cpeg.cpp_modularity(self.G_selfloop.cpp(), membership)
|
||||
self.assertIsInstance(q, float)
|
||||
|
||||
def test_single_community(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = [{0, 1, 2, 3}]
|
||||
membership = _communities_to_membership(self.G, communities)
|
||||
q = cpeg.cpp_modularity(self.G.cpp(), membership)
|
||||
self.assertIsInstance(q, float)
|
||||
|
||||
def test_each_node_its_own_community(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities = [{0}, {1}, {2}, {3}]
|
||||
membership = _communities_to_membership(self.G, communities)
|
||||
q = cpeg.cpp_modularity(self.G.cpp(), membership)
|
||||
self.assertIsInstance(q, float)
|
||||
|
||||
def test_empty_community_list(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
q = cpeg.cpp_modularity(self.G.cpp(), [])
|
||||
self.assertEqual(q, 0.0)
|
||||
|
||||
def test_python_cpp_consistency(self):
|
||||
cpeg = self._get_cpp_module()
|
||||
if cpeg is None:
|
||||
self.skipTest("cpp_easygraph module not available")
|
||||
|
||||
communities_py = [{0, 1}, {2, 3}]
|
||||
|
||||
q_py = eg.functions.community.modularity(self.G, communities_py)
|
||||
q_cpp = cpeg.cpp_modularity(self.G.cpp(), communities_py)
|
||||
|
||||
self.assertAlmostEqual(q_py, q_cpp, places=5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,55 @@
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from cpp_modularity_test import TestModularity
|
||||
from cpp_greedy_modularity_test import TestGreedyModularity
|
||||
from cpp_enumerate_subgraph_test import (
|
||||
TestEnumerateSubgraph,
|
||||
TestRandomEnumerateSubgraph
|
||||
)
|
||||
from cpp_louvain_test import TestLouvainCommunities, TestLouvainCommunitiesSerial
|
||||
from cpp_LPA_test import TestLPA
|
||||
from cpp_ego_graph_test import TestEgoGraph, TestEgoGraphCSR
|
||||
from cpp_localsearch_test import TestLocalsearch
|
||||
|
||||
|
||||
def create_test_suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestModularity))
|
||||
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestGreedyModularity))
|
||||
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestEnumerateSubgraph))
|
||||
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestRandomEnumerateSubgraph))
|
||||
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestLouvainCommunities))
|
||||
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestLouvainCommunitiesSerial))
|
||||
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestLPA))
|
||||
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestEgoGraph))
|
||||
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestEgoGraphCSR))
|
||||
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestLocalsearch))
|
||||
return suite
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 70)
|
||||
print("Easy-Graph C++ Module Test Suite")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
suite = create_test_suite()
|
||||
runner = unittest.TextTestRunner(verbosity=2)
|
||||
result = runner.run(suite)
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
print("Test Summary")
|
||||
print("=" * 70)
|
||||
print(f"Tests run: {result.testsRun}")
|
||||
print(f"Successes: {result.testsRun - len(result.failures) - len(result.errors)}")
|
||||
print(f"Failures: {len(result.failures)}")
|
||||
print(f"Errors: {len(result.errors)}")
|
||||
print(f"Skipped: {len(result.skipped)}")
|
||||
print("=" * 70)
|
||||
|
||||
sys.exit(0 if result.wasSuccessful() else 1)
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "biconnected.h"
|
||||
#include "connected.h"
|
||||
#include "strongly_connected.h"
|
||||
@@ -0,0 +1,109 @@
|
||||
#include "biconnected.h"
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../common/utils.h"
|
||||
|
||||
|
||||
node_t index_edge(std::vector<std::pair<node_t, node_t>>& edges, const std::pair<node_t, node_t>& target) {
|
||||
for (int i = edges.size() - 1;i >= 0;i--) {
|
||||
if ((edges[i].first == target.first) && (edges[i].second == target.second)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
py::object _biconnected_dfs_record_edges(py::object G, py::object need_components) {
|
||||
py::list ret = py::list();
|
||||
std::unordered_set<node_t> visited;
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
node_dict_factory &nodes_list = G_.node;
|
||||
for (node_dict_factory::iterator iter = nodes_list.begin();iter != nodes_list.end();iter++) {
|
||||
node_t start_id = iter->first;
|
||||
if (visited.find(start_id) != visited.end()) {
|
||||
continue;
|
||||
}
|
||||
std::unordered_map<node_t, int> discovery;
|
||||
std::unordered_map<node_t, int> low;
|
||||
node_t root_children = 0;
|
||||
discovery.emplace(start_id, 0);
|
||||
low.emplace(start_id, 0);
|
||||
visited.emplace(start_id);
|
||||
std::vector<std::pair<node_t, node_t>> edge_stack;
|
||||
std::vector<stack_node> stack;
|
||||
adj_attr_dict_factory& start_adj = G_.adj[start_id];
|
||||
NeighborIterator neighbors_iter = NeighborIterator(start_adj);
|
||||
stack_node initial_stack_node(start_id, start_id, neighbors_iter);
|
||||
stack.emplace_back(initial_stack_node);
|
||||
while (!stack.empty()) {
|
||||
stack_node& node_info = stack.back();
|
||||
node_t node_grandparent_id = node_info.grandparent;
|
||||
node_t node_parent_id = node_info.parent;
|
||||
try {
|
||||
node_t node_child_id = node_info.neighbors_iter.next();
|
||||
if (node_grandparent_id == node_child_id) {
|
||||
continue;
|
||||
}
|
||||
if (visited.find(node_child_id) != visited.end()) {
|
||||
if (discovery[node_child_id] <= discovery[node_parent_id]) {
|
||||
low[node_parent_id] = std::min(low[node_parent_id], discovery[node_child_id]);
|
||||
if (need_components.cast<bool>()) {
|
||||
edge_stack.emplace_back(std::make_pair(node_parent_id, node_child_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
low[node_child_id] = discovery[node_child_id] = discovery.size();
|
||||
visited.emplace(node_child_id);
|
||||
adj_attr_dict_factory& node_child_adj = G_.adj[node_child_id];
|
||||
NeighborIterator child_neighbors_iter = NeighborIterator(G_.adj[node_child_id]);
|
||||
stack.emplace_back(node_parent_id, node_child_id, child_neighbors_iter);
|
||||
if (need_components.cast<bool>()) {
|
||||
edge_stack.emplace_back(std::make_pair(node_parent_id, node_child_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (int) {
|
||||
stack.pop_back();
|
||||
if (stack.size() > 1) {
|
||||
if (low[node_parent_id] >= discovery[node_grandparent_id]) {
|
||||
if (need_components.cast<bool>()) {
|
||||
py::list tmp_ret = py::list();
|
||||
std::pair<node_t, node_t> iter_edge = std::make_pair(-1, -1);
|
||||
while ((iter_edge.first != node_grandparent_id || iter_edge.second != node_parent_id)) {
|
||||
iter_edge = edge_stack.back();
|
||||
edge_stack.pop_back();
|
||||
tmp_ret.append(py::make_tuple(G_.id_to_node[py::cast(iter_edge.first)], G_.id_to_node[py::cast(iter_edge.second)]));
|
||||
}
|
||||
ret.append(tmp_ret);
|
||||
}
|
||||
else {
|
||||
ret.append(G_.id_to_node[py::cast(node_grandparent_id)]);
|
||||
}
|
||||
}
|
||||
low[node_grandparent_id] = std::min(low[node_grandparent_id], low[node_parent_id]);
|
||||
}
|
||||
else if (stack.size() > 0) {
|
||||
root_children += 1;
|
||||
if (need_components.cast<bool>()) {
|
||||
std::pair<node_t, node_t> target = std::make_pair(node_grandparent_id, node_parent_id);
|
||||
node_t ind = index_edge(edge_stack, target);
|
||||
if (ind != -1) {
|
||||
py::list tmp_ret = py::list();
|
||||
for (node_t z = ind;z < edge_stack.size();z++) {
|
||||
tmp_ret.append(py::make_tuple(G_.id_to_node[py::cast(edge_stack[z].first)], G_.id_to_node[py::cast(edge_stack[z].second)]));
|
||||
}
|
||||
ret.append(tmp_ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!need_components.cast<bool>()) {
|
||||
if (root_children > 1) {
|
||||
ret.append(G_.id_to_node(start_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../common/common.h"
|
||||
|
||||
class NeighborIterator {
|
||||
public:
|
||||
NeighborIterator() {
|
||||
}
|
||||
NeighborIterator(adj_attr_dict_factory& neighbor_map) {
|
||||
now = neighbor_map.begin();;
|
||||
end = neighbor_map.end();
|
||||
}
|
||||
node_t next() {
|
||||
if (now == end) {
|
||||
throw -1;
|
||||
}
|
||||
else {
|
||||
return (now++)->first;
|
||||
}
|
||||
}
|
||||
private:
|
||||
adj_attr_dict_factory::iterator now, end;
|
||||
};
|
||||
typedef struct stackNode {
|
||||
node_t grandparent, parent;
|
||||
NeighborIterator neighbors_iter;
|
||||
stackNode(node_t grandparent, node_t parent, NeighborIterator neighbors_iter) {
|
||||
this->grandparent = grandparent;
|
||||
this->parent = parent;
|
||||
this->neighbors_iter = neighbors_iter;
|
||||
}
|
||||
}stack_node;
|
||||
|
||||
py::object _biconnected_dfs_record_edges(py::object G, py::object need_components);
|
||||
@@ -0,0 +1,205 @@
|
||||
#include "connected.h"
|
||||
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
#include "../../classes/graph.h"
|
||||
#include "../../classes/directed_graph.h"
|
||||
#include "../../common/utils.h"
|
||||
#include "time.h"
|
||||
|
||||
#define gmin(x, y) x = x < y? x: y
|
||||
|
||||
py::object plain_bfs(py::object G, py::object source) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
node_t source_id = G_.node_to_id.attr("get")(source).cast<node_t>();
|
||||
adj_dict_factory& G_adj = G_.adj;
|
||||
std::unordered_set<node_t> seen;
|
||||
std::unordered_set<node_t> nextlevel;
|
||||
nextlevel.emplace(source_id);
|
||||
py::list res = py::list();
|
||||
while (nextlevel.size()) {
|
||||
std::unordered_set<node_t> thislevel = nextlevel;
|
||||
nextlevel = std::unordered_set<node_t>();
|
||||
for (std::unordered_set<node_t>::iterator i = thislevel.begin(); i != thislevel.end(); i++) {
|
||||
node_t v_id = *i;
|
||||
if (seen.find(v_id) == seen.end()) {
|
||||
seen.emplace(v_id);
|
||||
adj_attr_dict_factory& v_adj = G_adj[v_id];
|
||||
for (adj_attr_dict_factory::iterator j = v_adj.begin(); j != v_adj.end(); j++) {
|
||||
node_t neighbor_id = j->first;
|
||||
nextlevel.emplace(neighbor_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (std::unordered_set<node_t>::iterator i = seen.begin(); i != seen.end(); i++) {
|
||||
node_t res_id = *(i);
|
||||
res.append(G_.id_to_node.attr("get")(res_id));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
py::object connected_component_undirected(py::object G) {
|
||||
Graph& G_ = G.cast<Graph&>();
|
||||
bool is_directed = G.attr("is_directed")().cast<bool>();
|
||||
if (is_directed == true) {
|
||||
printf("connected_component_undirected is designed for undirected graphs.\n");
|
||||
return py::dict();
|
||||
}
|
||||
int N = G_.node.size();
|
||||
int M = G.attr("number_of_edges")().cast<int>();
|
||||
std::vector<Edge_weighted> E_res(N+5);
|
||||
for(int i=0; i < N+5; i++) {
|
||||
E_res[i].toward = 0;
|
||||
E_res[i].next = 0;
|
||||
}
|
||||
int edge_number_res = 0;
|
||||
std::vector<int> parent(N+5, 0);
|
||||
std::vector<int> rank_node(N+5, 0);
|
||||
std::vector<int> color(N+5, 0);
|
||||
std::vector<int> head_res(N+5, 0);
|
||||
std::vector<bool> has_edge(N+5, false);
|
||||
|
||||
|
||||
py::list nodes_list = py::list(G.attr("nodes"));
|
||||
for (int i = 0;i < py::len(nodes_list);i++) {
|
||||
node_t i_id = (G_.node_to_id[nodes_list[i]]).cast<node_t>();
|
||||
parent[i_id] = i_id;
|
||||
}
|
||||
|
||||
for (graph_edge& edge : G_._get_edges()) {
|
||||
node_t u = edge.u, v = edge.v;
|
||||
has_edge[u] = true; has_edge[v] = true;
|
||||
_union_node(u, v, parent, rank_node);
|
||||
}
|
||||
|
||||
int Tot = 0;
|
||||
for(int i = 1; i < N + 1; ++i) {
|
||||
if (!has_edge[i])
|
||||
continue;
|
||||
int fx = _getfa(i, parent);
|
||||
if(fx == i){
|
||||
color[++Tot] = fx;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 1; i < N + 1; ++i) {
|
||||
int fx = _getfa(i, parent);
|
||||
_add_edge_res(fx, i, E_res, head_res, &edge_number_res);
|
||||
}
|
||||
|
||||
py::dict ret = py::dict();
|
||||
for (int i = 1; i <= Tot; ++i) {
|
||||
py::list tmp = py::list();
|
||||
for(int p = head_res[color[i]]; p; p = E_res[p].next){
|
||||
tmp.append(py::cast(E_res[p].toward));
|
||||
}
|
||||
ret[py::cast(i)] = tmp;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline void _union_node(const int &u, const int &v, std::vector<int> &parent, std::vector<int> &rank_node) {
|
||||
int x = _getfa(u, parent), y = _getfa(v, parent); //先找到两个根节点
|
||||
if (rank_node[x] <= rank_node[y])
|
||||
parent[x] = y;
|
||||
else
|
||||
parent[y] = x;
|
||||
if (rank_node[x] == rank_node[y] && x != y)
|
||||
rank_node[y]++; //如果深度相同且根节点不同,则新的根节点的深度+1
|
||||
}
|
||||
|
||||
int _getfa(const int & x, std::vector<int> &parent) {
|
||||
int r,k,t;
|
||||
r=x;
|
||||
while(parent[r]!=r)
|
||||
r=parent[r];
|
||||
k=r;
|
||||
r=x;
|
||||
while(parent[r]!=k) {
|
||||
t=parent[r];
|
||||
parent[r]=k;
|
||||
r=t;
|
||||
}
|
||||
return k;
|
||||
}
|
||||
|
||||
py::object connected_component_directed(py::object G) {
|
||||
bool is_directed = G.attr("is_directed")().cast<bool>();
|
||||
if (is_directed == false) {
|
||||
printf("connected_component_directed is designed for directed graphs.\n");
|
||||
return py::list();
|
||||
}
|
||||
DiGraph& G_ = G.cast<DiGraph&>();
|
||||
int N = G_.node.size();
|
||||
Graph_L G_l;
|
||||
if(G_.linkgraph_dirty || G_.linkgraph_structure.max_deg == -1){
|
||||
G_l = graph_to_linkgraph(G_, is_directed, "", true, false);
|
||||
G_.linkgraph_dirty = false;
|
||||
}
|
||||
else{
|
||||
G_l = G_.linkgraph_structure;
|
||||
}
|
||||
std::vector<LinkEdge>& E = G_l.edges;
|
||||
std::vector<int> outDegree = G_l.degree;
|
||||
std::vector<int> head = G_l.head;
|
||||
|
||||
int Time = 0, cnt = 0, Tot = 0, edge_number_res = 0;
|
||||
|
||||
std::vector<int> dfn(N+5, 0);
|
||||
std::vector<int> low(N+5, 0);
|
||||
std::vector<int> st(N+5, 0);
|
||||
std::vector<int> color(N+5, 0);
|
||||
std::vector<int> head_res(N+5, 0);
|
||||
std::vector<bool> in_stack(N+5, false);
|
||||
std::vector<bool> has_edge(N+5, false);
|
||||
|
||||
std::vector<Edge_weighted> E_res(N+5);
|
||||
for(int i=0; i < N+5; i++) {
|
||||
E_res[i].toward = 0;
|
||||
E_res[i].next = 0;
|
||||
}
|
||||
|
||||
for (graph_edge& edge : G_._get_edges()) {
|
||||
node_t u = edge.u, v = edge.v;
|
||||
has_edge[u] = true; has_edge[v] = true;
|
||||
}
|
||||
|
||||
for (int i = 1; i < N + 1; ++i)
|
||||
if (!dfn[i] && has_edge[i])
|
||||
_tarjan(i, &Time, &cnt, &Tot, E, head, dfn, low, st, color, in_stack, E_res, head_res, &edge_number_res);
|
||||
|
||||
py::list ret = py::list();
|
||||
for (int i = 1; i <= Tot; ++i) {
|
||||
py::set tmp;
|
||||
for(int p = head_res[i]; p; p = E_res[p].next)
|
||||
tmp.add(G_.id_to_node.attr("get")(E_res[p].toward));
|
||||
ret.append(tmp);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void _add_edge_res(const int &u, const int &v, std::vector<Edge_weighted> &E_res, std::vector<int> &head_res, int *edge_number_res) {
|
||||
E_res[++(*edge_number_res)].next = head_res[u];
|
||||
E_res[*edge_number_res].toward = v;
|
||||
head_res[u] = *edge_number_res;
|
||||
}
|
||||
|
||||
void _tarjan(const int &u, int *Time, int *cnt, int *Tot, std::vector<LinkEdge>& E, std::vector<int>& head, std::vector<int> &dfn, std::vector<int> &low, std::vector<int> &st, std::vector<int> &color, std::vector<bool> &in_stack, std::vector<Edge_weighted> &E_res, std::vector<int> &head_res, int *edge_number_res) {
|
||||
dfn[u] = low[u] = ++(*Time); st[++(*cnt)] = u; in_stack[u] = true;
|
||||
for(int p = head[u]; p != -1; p = E[p].next){
|
||||
int v = E[p].to;
|
||||
if (!dfn[v]) _tarjan(v, Time, cnt, Tot, E, head, dfn, low, st, color, in_stack, E_res, head_res, edge_number_res), gmin(low[u], low[v]);
|
||||
else if (in_stack[v]) gmin(low[u], dfn[v]);
|
||||
}
|
||||
|
||||
if (dfn[u] == low[u]) {
|
||||
for (++(*Tot); st[*cnt] != u; --(*cnt)) {
|
||||
_add_edge_res(*Tot, st[*cnt], E_res, head_res, edge_number_res);
|
||||
in_stack[st[*cnt]] = false, color[st[*cnt]] = *Tot;
|
||||
}
|
||||
_add_edge_res(*Tot, st[*cnt], E_res, head_res, edge_number_res);
|
||||
in_stack[u] = false; color[u] = *Tot; --(*cnt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../common/common.h"
|
||||
#include "../../classes/linkgraph.h"
|
||||
|
||||
struct Edge_weighted{
|
||||
int toward, next;
|
||||
};
|
||||
|
||||
py::object plain_bfs(py::object G, py::object source);
|
||||
py::object connected_component_undirected(py::object G);
|
||||
inline void _union_node(const int &u, const int &v, std::vector<int>& parent, std::vector<int> &rank_node);
|
||||
int _getfa(const int & x, std::vector<int> &parent);
|
||||
py::object connected_component_directed(py::object G);
|
||||
void _tarjan(const int &u, int *Time, int *cnt, int *Tot, std::vector<LinkEdge>& E, std::vector<int>& head, std::vector<int> &dfn, std::vector<int> &low, std::vector<int> &st, std::vector<int> &color, std::vector<bool> &in_stack, std::vector<Edge_weighted> &E_res, std::vector<int> &head_res, int *edge_number_res);
|
||||
void _add_edge_res(const int &u, const int &v, std::vector<Edge_weighted> &E_res, std::vector<int> &head_res, int *edge_number_res);
|
||||
@@ -0,0 +1,90 @@
|
||||
#include "strongly_connected.h"
|
||||
#include "connected.h"
|
||||
|
||||
#include "../../classes/directed_graph.h"
|
||||
|
||||
#define MAX_NODES_NUM4RECURSION_METHOD 100000
|
||||
|
||||
py::object strongly_connected_components(py::object G) {
|
||||
bool is_directed = G.attr("is_directed")().cast<bool>();
|
||||
if (is_directed == false) {
|
||||
printf("connected_component_directed is designed for directed graphs.\n");
|
||||
return py::list();
|
||||
}
|
||||
|
||||
int N = G.attr("number_of_nodes")().cast<int>();
|
||||
if(N < MAX_NODES_NUM4RECURSION_METHOD){
|
||||
return connected_component_directed(G);
|
||||
}
|
||||
|
||||
return strongly_connected_components_iteration_impl(G);
|
||||
}
|
||||
|
||||
py::object strongly_connected_components_iteration_impl(py::object G) {
|
||||
py::list res = py::list();
|
||||
DiGraph& G_ = py::cast<DiGraph&>(G);
|
||||
adj_dict_factory& adj = G_.adj;
|
||||
std::unordered_map<node_t, node_t> preorder;
|
||||
std::unordered_map<node_t, node_t> lowlink;
|
||||
std::set<node_t> scc_found;
|
||||
std::vector<node_t> scc_queue;
|
||||
int i = 0;
|
||||
node_dict_factory& nodes_list = G_.node;
|
||||
for (node_dict_factory::iterator source = nodes_list.begin(); source != nodes_list.end(); source++) {
|
||||
node_t source_id = source->first;
|
||||
if (scc_found.find(source_id) == scc_found.end()) {
|
||||
std::vector<node_t> que;
|
||||
que.emplace_back(source_id);
|
||||
while (!que.empty()) {
|
||||
node_t v_id = que.back();
|
||||
if (preorder.find(v_id) == preorder.end()) {
|
||||
i += 1;
|
||||
preorder[v_id] = i;
|
||||
}
|
||||
bool done = true;
|
||||
adj_attr_dict_factory& v_neighbors = adj[v_id];
|
||||
for (adj_attr_dict_factory::iterator w = v_neighbors.begin(); w != v_neighbors.end(); w++) {
|
||||
node_t w_id = w->first;
|
||||
if (preorder.find(w_id) == preorder.end()) {
|
||||
que.emplace_back(w_id);
|
||||
done = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (done) {
|
||||
lowlink[v_id] = preorder[v_id];
|
||||
for (adj_attr_dict_factory::iterator w = v_neighbors.begin(); w != v_neighbors.end(); w++) {
|
||||
node_t w_id = w->first;
|
||||
if (scc_found.find(w_id) == scc_found.end()) {
|
||||
if (preorder[w_id] > preorder[v_id]) {
|
||||
lowlink[v_id] = std::min(lowlink[v_id], lowlink[w_id]);
|
||||
} else {
|
||||
lowlink[v_id] = std::min(lowlink[v_id], preorder[w_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
que.pop_back();
|
||||
if (lowlink[v_id] == preorder[v_id]) {
|
||||
std::unordered_set<node_t> scc;
|
||||
scc.emplace(v_id);
|
||||
while (!scc_queue.empty() && (preorder[scc_queue.back()] > preorder[v_id])) {
|
||||
node_t k = scc_queue.back();
|
||||
scc_queue.pop_back();
|
||||
scc.emplace(k);
|
||||
}
|
||||
|
||||
py::set tmp_res;
|
||||
for (std::unordered_set<node_t>::iterator z = scc.begin(); z != scc.end(); z++) {
|
||||
scc_found.emplace(*z);
|
||||
tmp_res.add(G_.id_to_node.attr("get")(*z));
|
||||
}
|
||||
res.append(tmp_res);
|
||||
} else {
|
||||
scc_queue.emplace_back(v_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../common/common.h"
|
||||
|
||||
py::object strongly_connected_components(py::object G);
|
||||
py::object strongly_connected_components_iteration_impl(py::object G);
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "k_cores.h"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user