├── .gitattributes ├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── .readthedocs.yaml ├── CITATION.bib ├── CITATION.cff ├── CONTRIBUTING.md ├── LICENSE.md ├── MANIFEST ├── README.md ├── docs ├── Makefile ├── about.rst ├── api │ ├── hypergraphx.communities.core_periphery.rst │ ├── hypergraphx.communities.hy_mmsbm.rst │ ├── hypergraphx.communities.hy_sc.rst │ ├── hypergraphx.communities.hypergraph_mt.rst │ ├── hypergraphx.communities.hyperlink_comm.rst │ ├── hypergraphx.communities.rst │ ├── hypergraphx.core.rst │ ├── hypergraphx.dynamics.rst │ ├── hypergraphx.filters.rst │ ├── hypergraphx.generation.rst │ ├── hypergraphx.linalg.rst │ ├── hypergraphx.measures.rst │ ├── hypergraphx.motifs.rst │ ├── hypergraphx.readwrite.rst │ ├── hypergraphx.representations.rst │ ├── hypergraphx.rst │ ├── hypergraphx.utils.rst │ ├── hypergraphx.viz.rst │ └── modules.rst ├── conf.py ├── index.rst ├── installation.rst └── make.bat ├── hypergraphx ├── __init__.py ├── communities │ ├── __init__.py │ ├── core_periphery │ │ ├── __init__.py │ │ └── model.py │ ├── hy_mmsbm │ │ ├── __init__.py │ │ ├── _linear_ops.py │ │ └── model.py │ ├── hy_sc │ │ ├── __init__.py │ │ └── model.py │ ├── hypergraph_mt │ │ ├── __init__.py │ │ └── model.py │ └── hyperlink_comm │ │ ├── __init__.py │ │ └── hyperlink_communities.py ├── core │ ├── __init__.py │ ├── directed_hypergraph.py │ ├── hypergraph.py │ ├── multiplex_hypergraph.py │ └── temporal_hypergraph.py ├── dynamics │ ├── __init__.py │ ├── contagion.py │ ├── randwalk.py │ ├── synch.py │ └── utils.py ├── filters │ ├── __init__.py │ ├── metadata_filters.py │ └── statistical_filters.py ├── generation │ ├── GAM.py │ ├── __init__.py │ ├── activity_driven.py │ ├── configuration_model.py │ ├── directed_configuration_model.py │ ├── hy_mmsbm_sampling.py │ ├── random.py │ └── scale_free.py ├── linalg │ ├── __init__.py │ └── linalg.py ├── measures │ ├── __init__.py │ ├── degree.py │ ├── directed │ │ ├── __init__.py │ │ ├── degree.py │ │ ├── hyperedge_signature.py │ │ └── reciprocity.py │ ├── edge_similarity.py │ ├── eigen_centralities.py │ ├── multiplex │ │ ├── __init__.py │ │ ├── degree.py │ │ └── overlap.py │ ├── s_centralities.py │ ├── sub_hypergraph_centrality.py │ └── temporal │ │ ├── __init__.py │ │ ├── temporal_correlations.py │ │ └── temporal_topological_correlation.py ├── motifs │ ├── __init__.py │ ├── directed_motifs.py │ ├── motifs.py │ └── utils.py ├── readwrite │ ├── __init__.py │ ├── hashing.py │ ├── hif.py │ ├── load.py │ └── save.py ├── representations │ ├── __init__.py │ ├── projections.py │ └── simplicial_complex.py ├── utils │ ├── __init__.py │ ├── cc.py │ ├── community.py │ ├── labeling.py │ └── visits.py └── viz │ ├── __init__.py │ ├── draw_communities.py │ ├── draw_hypergraph.py │ ├── draw_multilayer_projection.py │ ├── draw_projections.py │ ├── draw_simplicial.py │ └── plot_motifs.py ├── images └── hypergraph.png ├── logo ├── logo.png ├── logo.svg ├── logo_cropped.png └── logo_cropped.svg ├── requirements.txt ├── requirements_docs.txt ├── setup.cfg ├── setup.py ├── test_data ├── hs │ ├── High-School_data_2013.csv │ ├── hs.json │ └── hs_one_class.json ├── justice_data │ ├── hyperedges.txt │ └── weights.txt ├── small_hypergraph1 │ └── hyperedges.txt ├── small_hypergraph2 │ └── hyperedges.txt ├── small_hypergraph3 │ └── hyperedges.txt ├── with_isolated_nodes │ └── hyperedges.txt ├── with_literal_nodes │ └── hyperedges.txt └── workplace │ ├── workplace.dat │ ├── workplace.json │ └── workplace_meta.csv ├── tests ├── conftest.py ├── core │ ├── directed_hypergraphs │ │ ├── test_directed_hypergraph.py │ │ ├── test_reciprocity.py │ │ └── test_signature.py │ ├── hypergraphs │ │ └── test_hypergraphs.py │ ├── multiplex_hypergraphs │ │ └── test_multiplex_hypergraphs.py │ └── temporal_hypergraphs │ │ └── test_temporal_hypergraphs.py ├── filters │ └── test_metadata_filters.py ├── generation │ └── test_random.py ├── linalg │ ├── test_adjacency.py │ ├── test_hye_list_to_binary_incidence.py │ └── test_incidence.py ├── measures │ └── test_sub_hypergraph_centrality.py ├── readwrite │ └── test_hashing.py └── utils │ └── test_visits.py └── tutorials ├── _example_data ├── justice_data │ ├── hyperedges.txt │ └── weights.txt ├── social_contagion.npy ├── temporal_correlations.npy └── walmart_data │ └── walmart-trips-reduced.txt ├── activity_driven.ipynb ├── analysis_paper.ipynb ├── basics.ipynb ├── communities.ipynb ├── directed_measures.ipynb ├── filters.ipynb ├── generation.ipynb ├── motifs.ipynb ├── random_walk.ipynb ├── simplicial_contagion.ipynb └── synchronization.ipynb /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ipynb -linguist-detectable 2 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [ published ] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /venv/ 2 | /.idea/ 3 | /.vscode/ 4 | *.ipynb_checkpoints/ 5 | *__pycache__/ 6 | .DS_Store 7 | *.egg-info/ 8 | docs/_build/ -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.9" 7 | apt_packages: 8 | - r-base 9 | 10 | sphinx: 11 | configuration: docs/conf.py 12 | 13 | python: 14 | install: 15 | - requirements: requirements_docs.txt 16 | - requirements: requirements.txt 17 | -------------------------------------------------------------------------------- /CITATION.bib: -------------------------------------------------------------------------------- 1 | @article{lotito2023hypergraphx, 2 | author = {Lotito, Quintino Francesco and Contisciani, Martina and De Bacco, Caterina and Di Gaetano, Leonardo and Gallo, Luca and Montresor, Alberto and Musciotto, Federico and Ruggeri, Nicolò and Battiston, Federico}, 3 | title = {Hypergraphx: a library for higher-order network analysis}, 4 | journal = {Journal of Complex Networks}, 5 | volume = {11}, 6 | number = {3}, 7 | pages = {cnad019}, 8 | year = {2023}, 9 | publisher = {Oxford University Press}, 10 | doi = {10.1093/comnet/cnad019}, 11 | url = {https://doi.org/10.1093/comnet/cnad019} 12 | } 13 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite the following article:" 3 | preferred-citation: 4 | type: article 5 | title: "Hypergraphx: a library for higher-order network analysis" 6 | authors: 7 | - family-names: "Lotito" 8 | given-names: "Quintino Francesco" 9 | - family-names: "Contisciani" 10 | given-names: "Martina" 11 | - family-names: "De Bacco" 12 | given-names: "Caterina" 13 | - family-names: "Di Gaetano" 14 | given-names: "Leonardo" 15 | - family-names: "Gallo" 16 | given-names: "Luca" 17 | - family-names: "Montresor" 18 | given-names: "Alberto" 19 | - family-names: "Musciotto" 20 | given-names: "Federico" 21 | - family-names: "Ruggeri" 22 | given-names: "Nicolò" 23 | - family-names: "Battiston" 24 | given-names: "Federico" 25 | journal: "Journal of Complex Networks" 26 | volume: "11" 27 | issue: "3" 28 | pages: "cnad019" 29 | year: 2023 30 | publisher: "Oxford University Press" 31 | doi: "10.1093/comnet/cnad019" 32 | url: "https://doi.org/10.1093/comnet/cnad019" 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Hypergraphx on GitHub 2 | 3 | Follow this step-by-step guide to contribute to Hypergraphx. 4 | 5 | ## 1. Set Up Your GitHub Account 6 | 7 | If you haven't already, create a GitHub account at [GitHub.com](https://github.com/). 8 | 9 | ## 2. Fork the Repository 10 | 11 | - Go to the main page of the Hypergraphx repository. 12 | - In the top-right corner of the page, click on the "Fork" button. This will create a copy of the repository in your 13 | GitHub account. 14 | 15 | ## 3. Clone Your Forked Repository 16 | 17 | - Navigate to your forked repository in your GitHub account. 18 | - Click the "Code" button and copy the URL. 19 | - Open your terminal and navigate to the directory where you want to clone the repository. 20 | - Run the following command: 21 | 22 | ```bash 23 | git clone [URL] 24 | ``` 25 | 26 | Replace `[URL]` with the URL you copied. 27 | 28 | ## 4. Set Upstream Remote 29 | 30 | To keep your forked repository updated with the changes from the original repository, you need to set an upstream 31 | remote: 32 | 33 | - Navigate to the directory of your cloned repository in the terminal. 34 | - Run the following command: 35 | 36 | ```bash 37 | git remote add upstream https://github.com/HGX-Team/hypergraphx.git 38 | ``` 39 | 40 | ## 5. Create a New Branch 41 | 42 | Before making any changes, it's a good practice to create a new branch: 43 | 44 | - Navigate to the directory of your cloned repository in the terminal. 45 | - Run the following command to create and switch to a new branch: 46 | 47 | ```bash 48 | git checkout -b your-branch-name 49 | ``` 50 | 51 | ## 6. Make Your Changes 52 | 53 | - Edit the files or add new files as required. 54 | - Once you've made your changes, save them. 55 | 56 | ## 7. Commit Your Changes 57 | 58 | - In the terminal, navigate to the directory of your cloned repository. 59 | - Run the following commands to add and commit your changes: 60 | 61 | ```bash 62 | git add . 63 | git commit -m "Your commit message here" 64 | ``` 65 | 66 | ## 8. Push Your Changes to GitHub 67 | 68 | - Push your changes to your forked repository on GitHub: 69 | 70 | ```bash 71 | git push origin your-branch-name 72 | ``` 73 | 74 | ## 9. Create a Pull Request (PR) 75 | 76 | - Go to your forked repository on GitHub. 77 | - Click on the "Pull requests" tab and then click on the "New pull request" button. 78 | - Ensure the base repository is the original Hypergraphx repository and the base branch is the branch you want to merge 79 | your changes into (usually `main`). 80 | - Ensure the head repository is your forked repository and the compare branch is the branch you made your changes in. 81 | - Click on the "Create pull request" button. 82 | - Fill in the PR title and description, explaining your changes. 83 | - Click on the "Create pull request" button to submit your PR. 84 | 85 | ## 10. Wait for Review 86 | 87 | - The maintainers of the Hypergraphx repository will review your PR. 88 | - They might request some changes or improvements. If so, make the required changes in your branch, commit them, and 89 | push them to GitHub. Your PR will be automatically updated. 90 | 91 | ## 11. PR Gets Merged 92 | 93 | Once your PR is approved, the maintainers will merge it into the main branch of the Hypergraphx repository. 94 | 95 | **Note:** Always follow the contribution guidelines provided by the repository maintainers, and always be respectful and 96 | constructive in your interactions. 97 | 98 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2023 HGX-TEAM 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 4 | following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 7 | disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, 16 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 20 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 21 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | --- 24 | (For some centrality measures) 25 | 26 | XGI is distributed with the 3-clause BSD license. 27 | 28 | Copyright (C) 2023, XGI Developers 29 | 30 | Nicholas Landry nicholas.landry.91@gmail.com 31 | 32 | Leo Torres leo@leotrs.com 33 | 34 | Iacopo Iacopini iacopiniiacopo@gmail.com 35 | 36 | Maxime Lucas maxime.lucas@isi.it 37 | 38 | Giovanni Petri giovanni.petri@isi.it 39 | 40 | Alice Patania apatania@uvm.edu 41 | 42 | Alice Schwarze alice.c.schwarze@dartmouth.edu 43 | 44 | All rights reserved. 45 | 46 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 47 | following conditions are met: 48 | 49 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following 50 | disclaimer. 51 | 52 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 53 | disclaimer in the documentation and/or other materials provided with the distribution. 54 | 55 | Neither the names of the XGI Developers nor the names of its contributors may be used to endorse or promote products 56 | derived from this software without specific prior written permission. 57 | 58 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 59 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 60 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 61 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 62 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 63 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 64 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 65 | 66 | --- 67 | (For the configuration model) 68 | 69 | MIT License 70 | 71 | Copyright (c) 2019 Phil Chodrow 72 | 73 | Permission is hereby granted, free of charge, to any person obtaining a copy 74 | of this software and associated documentation files (the "Software"), to deal 75 | in the Software without restriction, including without limitation the rights 76 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 77 | copies of the Software, and to permit persons to whom the Software is 78 | furnished to do so, subject to the following conditions: 79 | 80 | The above copyright notice and this permission notice shall be included in all 81 | copies or substantial portions of the Software. 82 | 83 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 84 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 85 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 86 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 87 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 88 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 89 | SOFTWARE. 90 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.cfg 3 | setup.py 4 | hypergraphx/__init__.py 5 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/about.rst: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | .. image:: ../logo/logo.png 5 | :width: 200 6 | 7 | Hypergraphx (HGX) is a Python library for higher-order network analysis. 8 | 9 | - Arxiv: https://arxiv.org/pdf/2303.15356.pdf 10 | - GitHub: https://github.com/HGX-Team/hypergraphx 11 | - PyPI: https://pypi.org/project/hypergraphx/ 12 | - Documentation: https://hypergraphx.readthedocs.io/ 13 | - Data: To appear soon -------------------------------------------------------------------------------- /docs/api/hypergraphx.communities.core_periphery.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.communities.core\_periphery package 2 | =============================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.communities.core\_periphery.core\_periphery module 8 | -------------------------------------------------------------- 9 | 10 | .. automodule:: hypergraphx.communities.core_periphery.core_periphery 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | Module contents 16 | --------------- 17 | 18 | .. automodule:: hypergraphx.communities.core_periphery 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.communities.hy_mmsbm.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.communities.hy\_mmsbm package 2 | ========================================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.communities.hy\_mmsbm.model module 8 | ---------------------------------------------- 9 | 10 | .. automodule:: hypergraphx.communities.hy_mmsbm.model 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | Module contents 16 | --------------- 17 | 18 | .. automodule:: hypergraphx.communities.hy_mmsbm 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.communities.hy_sc.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.communities.hy\_sc package 2 | ====================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.communities.hy\_sc.model module 8 | ------------------------------------------- 9 | 10 | .. automodule:: hypergraphx.communities.hy_sc.model 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | Module contents 16 | --------------- 17 | 18 | .. automodule:: hypergraphx.communities.hy_sc 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.communities.hypergraph_mt.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.communities.hypergraph\_mt package 2 | ============================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.communities.hypergraph\_mt.model module 8 | --------------------------------------------------- 9 | 10 | .. automodule:: hypergraphx.communities.hypergraph_mt.model 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | Module contents 16 | --------------- 17 | 18 | .. automodule:: hypergraphx.communities.hypergraph_mt 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.communities.hyperlink_comm.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.communities.hyperlink\_comm package 2 | =============================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.communities.hyperlink\_comm.hyperlink\_communities module 8 | --------------------------------------------------------------------- 9 | 10 | .. automodule:: hypergraphx.communities.hyperlink_comm.hyperlink_communities 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | Module contents 16 | --------------- 17 | 18 | .. automodule:: hypergraphx.communities.hyperlink_comm 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.communities.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.communities package 2 | =============================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | hypergraphx.communities.core_periphery 11 | hypergraphx.communities.hy_mmsbm 12 | hypergraphx.communities.hy_sc 13 | hypergraphx.communities.hypergraph_mt 14 | hypergraphx.communities.hyperlink_comm 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: hypergraphx.communities 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.core.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.core package 2 | ======================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.core.hypergraph module 8 | ---------------------------------- 9 | 10 | .. automodule:: hypergraphx.core.hypergraph 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | hypergraphx.core.meta\_handler module 16 | ------------------------------------- 17 | 18 | .. automodule:: hypergraphx.core.meta_handler 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | hypergraphx.core.multiplex\_hypergraph module 24 | --------------------------------------------- 25 | 26 | .. automodule:: hypergraphx.core.multiplex_hypergraph 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | hypergraphx.core.temporal\_hypergraph module 32 | -------------------------------------------- 33 | 34 | .. automodule:: hypergraphx.core.temporal_hypergraph 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | Module contents 40 | --------------- 41 | 42 | .. automodule:: hypergraphx.core 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.dynamics.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.dynamics package 2 | ============================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.dynamics.contagion module 8 | ------------------------------------- 9 | 10 | .. automodule:: hypergraphx.dynamics.contagion 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | hypergraphx.dynamics.randwalk module 16 | ------------------------------------ 17 | 18 | .. automodule:: hypergraphx.dynamics.randwalk 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | hypergraphx.dynamics.synch module 24 | --------------------------------- 25 | 26 | .. automodule:: hypergraphx.dynamics.synch 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | hypergraphx.dynamics.utils module 32 | --------------------------------- 33 | 34 | .. automodule:: hypergraphx.dynamics.utils 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | Module contents 40 | --------------- 41 | 42 | .. automodule:: hypergraphx.dynamics 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.filters.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.filters package 2 | =========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.filters.filters module 8 | ---------------------------------- 9 | 10 | .. automodule:: hypergraphx.filters.filters 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | Module contents 16 | --------------- 17 | 18 | .. automodule:: hypergraphx.filters 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.generation.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.generation package 2 | ============================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.generation.activity\_driven module 8 | ---------------------------------------------- 9 | 10 | .. automodule:: hypergraphx.generation.activity_driven 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | hypergraphx.generation.configuration\_model module 16 | -------------------------------------------------- 17 | 18 | .. automodule:: hypergraphx.generation.configuration_model 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | hypergraphx.generation.hy\_mmsbm\_sampling module 24 | ------------------------------------------------- 25 | 26 | .. automodule:: hypergraphx.generation.hy_mmsbm_sampling 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | hypergraphx.generation.random module 32 | ------------------------------------ 33 | 34 | .. automodule:: hypergraphx.generation.random 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | hypergraphx.generation.scale\_free module 40 | ----------------------------------------- 41 | 42 | .. automodule:: hypergraphx.generation.scale_free 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | Module contents 48 | --------------- 49 | 50 | .. automodule:: hypergraphx.generation 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.linalg.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.linalg package 2 | ========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.linalg.linalg module 8 | -------------------------------- 9 | 10 | .. automodule:: hypergraphx.linalg.linalg 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | Module contents 16 | --------------- 17 | 18 | .. automodule:: hypergraphx.linalg 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.measures.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.measures package 2 | ============================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.measures.degree module 8 | ---------------------------------- 9 | 10 | .. automodule:: hypergraphx.measures.degree 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | hypergraphx.measures.edge\_similarity module 16 | -------------------------------------------- 17 | 18 | .. automodule:: hypergraphx.measures.edge_similarity 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | hypergraphx.measures.eigen\_centralities module 24 | ----------------------------------------------- 25 | 26 | .. automodule:: hypergraphx.measures.eigen_centralities 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | hypergraphx.measures.s\_centralities module 32 | ------------------------------------------- 33 | 34 | .. automodule:: hypergraphx.measures.s_centralities 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | hypergraphx.measures.sub\_hypergraph\_centrality module 40 | ------------------------------------------------------- 41 | 42 | .. automodule:: hypergraphx.measures.sub_hypergraph_centrality 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | Module contents 48 | --------------- 49 | 50 | .. automodule:: hypergraphx.measures 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.motifs.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.motifs package 2 | ========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.motifs.approx\_motifs module 8 | ---------------------------------------- 9 | 10 | .. automodule:: hypergraphx.motifs.approx_motifs 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | hypergraphx.motifs.motifs module 16 | -------------------------------- 17 | 18 | .. automodule:: hypergraphx.motifs.motifs 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | hypergraphx.motifs.utils module 24 | ------------------------------- 25 | 26 | .. automodule:: hypergraphx.motifs.utils 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | Module contents 32 | --------------- 33 | 34 | .. automodule:: hypergraphx.motifs 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.readwrite.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.readwrite package 2 | ============================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.readwrite.data\_to\_json module 8 | ------------------------------------------- 9 | 10 | .. automodule:: hypergraphx.readwrite.data_to_json 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | hypergraphx.readwrite.load module 16 | --------------------------------- 17 | 18 | .. automodule:: hypergraphx.readwrite.load 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | hypergraphx.readwrite.loaders module 24 | ------------------------------------ 25 | 26 | .. automodule:: hypergraphx.readwrite.loaders 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | hypergraphx.readwrite.save module 32 | --------------------------------- 33 | 34 | .. automodule:: hypergraphx.readwrite.save 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | Module contents 40 | --------------- 41 | 42 | .. automodule:: hypergraphx.readwrite 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.representations.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.representations package 2 | =================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.representations.projections module 8 | ---------------------------------------------- 9 | 10 | .. automodule:: hypergraphx.representations.projections 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | hypergraphx.representations.simplicial module 16 | --------------------------------------------- 17 | 18 | .. automodule:: hypergraphx.representations.simplicial 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: hypergraphx.representations 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.rst: -------------------------------------------------------------------------------- 1 | hypergraphx package 2 | =================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | hypergraphx.communities 11 | hypergraphx.core 12 | hypergraphx.dynamics 13 | hypergraphx.filters 14 | hypergraphx.generation 15 | hypergraphx.linalg 16 | hypergraphx.measures 17 | hypergraphx.motifs 18 | hypergraphx.readwrite 19 | hypergraphx.representations 20 | hypergraphx.utils 21 | hypergraphx.viz 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: hypergraphx 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.utils.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.utils package 2 | ========================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.utils.cc module 8 | --------------------------- 9 | 10 | .. automodule:: hypergraphx.utils.cc 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | hypergraphx.utils.community module 16 | ---------------------------------- 17 | 18 | .. automodule:: hypergraphx.utils.community 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | hypergraphx.utils.labeling module 24 | --------------------------------- 25 | 26 | .. automodule:: hypergraphx.utils.labeling 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | hypergraphx.utils.visits module 32 | ------------------------------- 33 | 34 | .. automodule:: hypergraphx.utils.visits 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | Module contents 40 | --------------- 41 | 42 | .. automodule:: hypergraphx.utils 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | -------------------------------------------------------------------------------- /docs/api/hypergraphx.viz.rst: -------------------------------------------------------------------------------- 1 | hypergraphx.viz package 2 | ======================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | hypergraphx.viz.draw\_communities module 8 | ---------------------------------------- 9 | 10 | .. automodule:: hypergraphx.viz.draw_communities 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | hypergraphx.viz.draw\_hypergraph module 16 | --------------------------------------- 17 | 18 | .. automodule:: hypergraphx.viz.draw_hypergraph 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | hypergraphx.viz.draw\_multilayer\_projection module 24 | --------------------------------------------------- 25 | 26 | .. automodule:: hypergraphx.viz.draw_multilayer_projection 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | hypergraphx.viz.draw\_projections module 32 | ---------------------------------------- 33 | 34 | .. automodule:: hypergraphx.viz.draw_projections 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | hypergraphx.viz.draw\_simplicial module 40 | --------------------------------------- 41 | 42 | .. automodule:: hypergraphx.viz.draw_simplicial 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | Module contents 48 | --------------- 49 | 50 | .. automodule:: hypergraphx.viz 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | -------------------------------------------------------------------------------- /docs/api/modules.rst: -------------------------------------------------------------------------------- 1 | hypergraphx 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | hypergraphx 8 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | import os 10 | import sys 11 | 12 | sys.path.insert(0, os.path.abspath("..")) 13 | 14 | project = "Hypergraphx" 15 | 16 | import datetime 17 | 18 | year = datetime.datetime.now().year 19 | 20 | copyright = f"{year}, HGX-Team" 21 | author = "HGX-Team" 22 | 23 | # -- General configuration --------------------------------------------------- 24 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 25 | 26 | extensions = [ 27 | "sphinx.ext.autodoc", 28 | "sphinx.ext.autosummary", 29 | "sphinx.ext.napoleon", 30 | "sphinx.ext.viewcode", 31 | "sphinx.ext.mathjax", 32 | ] 33 | 34 | templates_path = ["_templates"] 35 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 36 | 37 | 38 | # -- Options for HTML output ------------------------------------------------- 39 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 40 | 41 | html_theme = "furo" 42 | html_static_path = ["_static"] 43 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Hypergraphx's documentation! 2 | ======================================= 3 | .. image:: ../logo/logo.png 4 | :width: 200 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: Home 9 | :hidden: 10 | 11 | about 12 | installation 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | :caption: Tutorials 17 | :hidden: 18 | 19 | Link to GitHub 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | :caption: API Reference 24 | :hidden: 25 | 26 | Core 27 | Communities 28 | Dynamics 29 | Filters 30 | Generation 31 | Measures 32 | Motifs 33 | Linear algebra 34 | Visualization 35 | Data loading 36 | Representations 37 | 38 | About 39 | ===== 40 | 41 | Hypergraphx (HGX) is a Python library for higher-order network analysis. 42 | 43 | - Arxiv: https://arxiv.org/pdf/2303.15356.pdf 44 | - GitHub: https://github.com/HGX-Team/hypergraphx 45 | - PyPI: https://pypi.org/project/hypergraphx/ 46 | - Documentation: https://hypergraphx.readthedocs.io/ 47 | - Data: To appear soon 48 | 49 | Higher-order data repository 50 | ================== 51 | 52 | To appear soon 53 | 54 | HGX Team 55 | ============ 56 | 57 | Project coordinators: 58 | 59 | * Quintino Francesco Lotito 60 | * Federico Battiston 61 | 62 | Core members: 63 | 64 | * Martina Contisciani 65 | * Caterina De Bacco 66 | * Leonardo Di Gaetano 67 | * Luca Gallo 68 | * Alberto Montresor 69 | * Federico Musciotto 70 | * Nicolò Ruggeri 71 | 72 | Reference 73 | =================== 74 | * Lotito, Quintino Francesco, et al. "Hypergraphx: a library for higher-order network analysis." Journal of Complex Networks 11.3 (2023): cnad019. 75 | 76 | Contributing 77 | ============ 78 | 79 | HGX is a collaborative project and we welcome suggestions and contributions. 80 | If you are interested in contributing to HGX or have any questions about our project, please do not hesitate to reach out to us. 81 | We look forward to hearing from you! 82 | 83 | 84 | License 85 | ======= 86 | 87 | This project is licensed under the `BSD 3-Clause License 88 | `_. 89 | 90 | Copyright (C) 2023 HGX-Team 91 | 92 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | You can easily install HGX using pip 5 | 6 | .. code:: bash 7 | 8 | pip install hypergraphx 9 | 10 | Otherwise, you can clone the repository and manually install the package 11 | 12 | .. code:: bash 13 | 14 | pip install -e .['all'] -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /hypergraphx/__init__.py: -------------------------------------------------------------------------------- 1 | from hypergraphx.core.directed_hypergraph import DirectedHypergraph 2 | from hypergraphx.core.hypergraph import Hypergraph 3 | from hypergraphx.core.multiplex_hypergraph import MultiplexHypergraph 4 | from hypergraphx.core.temporal_hypergraph import TemporalHypergraph 5 | from . import readwrite 6 | 7 | import sys 8 | 9 | MIN_PYTHON_VERSION = (3, 10) 10 | assert ( 11 | sys.version_info >= MIN_PYTHON_VERSION 12 | ), f"requires Python {'.'.join([str(n) for n in MIN_PYTHON_VERSION])} or newer" 13 | 14 | __version__ = "1.7.7" 15 | -------------------------------------------------------------------------------- /hypergraphx/communities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/hypergraphx/communities/__init__.py -------------------------------------------------------------------------------- /hypergraphx/communities/core_periphery/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/hypergraphx/communities/core_periphery/__init__.py -------------------------------------------------------------------------------- /hypergraphx/communities/core_periphery/model.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | 4 | import numpy as np 5 | 6 | from hypergraphx import Hypergraph 7 | 8 | 9 | def relabel_nodes(d, w): 10 | N = set() 11 | 12 | node2id = {} 13 | idx = 0 14 | 15 | new_d = set() 16 | new_w = {} 17 | 18 | for e in d: 19 | for n in e: 20 | if n not in node2id: 21 | node2id[n] = idx 22 | idx += 1 23 | N.add(n) 24 | 25 | tmp = [] 26 | for n in e: 27 | tmp.append(node2id[n]) 28 | tmp = tuple(sorted(tmp)) 29 | new_d.add(tmp) 30 | new_w[tmp] = w[e] 31 | 32 | return len(N), node2id, new_d, new_w 33 | 34 | 35 | def sample_params(): 36 | return np.random.uniform(0, 1), np.random.uniform(0, 1) 37 | 38 | 39 | def transition_function(i, N_nodes, a, b): 40 | if i <= math.floor(b * N_nodes): 41 | return (i * (1 - a)) / (2 * math.floor(b * N_nodes)) 42 | else: 43 | return ((i - math.floor(b * N_nodes)) * (1 - a)) / ( 44 | 2 * (N_nodes - math.floor(b * N_nodes)) 45 | ) + (1 + a) / 2 46 | 47 | 48 | def aggregation_function(values): 49 | return sum(values) 50 | 51 | 52 | def aggregate_local_core_values(nodes, order, local_core_values): 53 | values = [] 54 | for n in nodes: 55 | values.append(local_core_values[order[n]]) 56 | return aggregation_function(values) 57 | 58 | 59 | def get_core_quality(edges, order, local_core_values): 60 | R = 0 61 | for e in edges: 62 | R += aggregate_local_core_values(list(e), order, local_core_values) 63 | return R 64 | 65 | 66 | def get_adj(d): 67 | adj = {} 68 | for e in d: 69 | for n in e: 70 | if n in adj: 71 | adj[n].append(tuple(sorted(list(e)))) 72 | else: 73 | adj[n] = [tuple(sorted(list(e)))] 74 | 75 | return adj 76 | 77 | 78 | def sort_by_degree(d): 79 | deg = {} 80 | for e in d: 81 | for n in e: 82 | if n not in deg: 83 | deg[n] = 0 84 | deg[n] += 1 85 | deg_list = [] 86 | for k in deg: 87 | deg_list.append((deg[k], k)) 88 | 89 | deg_list = list(sorted(deg_list)) 90 | return deg_list 91 | 92 | 93 | def core_periphery(hypergraph: Hypergraph, greedy_start=False, N_ITER=1000): 94 | """ 95 | Implementation of the core-periphery model described in: https://arxiv.org/pdf/2202.12769.pdf 96 | 97 | Parameters 98 | ---------- 99 | hypergraph: Hypergraph 100 | Hypergraph object 101 | greedy_start: bool 102 | If True, use a greedy approach to find the initial permutation of nodes (default: False) 103 | N_ITER: int 104 | Number of iterations (default: 1000) 105 | 106 | Returns 107 | ------- 108 | dict 109 | Dictionary with coreness scores for each node 110 | """ 111 | 112 | if hypergraph.is_weighted(): 113 | w = {} 114 | for edge in hypergraph.get_edges(): 115 | w[tuple(sorted(edge))] = hypergraph.get_weight(tuple(sorted(edge))) 116 | else: 117 | w = {} 118 | for edge in hypergraph.get_edges(): 119 | w[tuple(sorted(edge))] = 1 120 | 121 | d = list(w.keys()) 122 | 123 | N_nodes, node2id, d, w = relabel_nodes(d, w) 124 | deg_list = sort_by_degree(d) 125 | 126 | adj = get_adj(d) 127 | 128 | cs = {i: 0 for i in range(N_nodes)} 129 | 130 | NUM_SWITCH = N_nodes * 10 131 | 132 | for n_iter in range(N_ITER): 133 | a, b = sample_params() 134 | local_core_values = {} 135 | 136 | for i in range(1, N_nodes + 1): 137 | local_core_values[i - 1] = transition_function(i, N_nodes, a, b) 138 | 139 | order = {} 140 | 141 | # initial permutation 142 | if greedy_start: 143 | for i, k in enumerate(deg_list): 144 | order[k[1]] = i 145 | else: 146 | tmp = [i for i in range(N_nodes)] 147 | random.shuffle(tmp) 148 | for i in range(N_nodes): 149 | order[i] = tmp[i] 150 | 151 | R = get_core_quality(d, order, local_core_values) 152 | 153 | # label switching 154 | for _ in range(NUM_SWITCH): 155 | i, j = random.sample(range(N_nodes), 2) 156 | 157 | new_R = R 158 | 159 | for e in adj[i]: 160 | new_R -= ( 161 | w[e] 162 | / len(e) 163 | * aggregate_local_core_values(list(e), order, local_core_values) 164 | ) 165 | 166 | for e in adj[j]: 167 | new_R -= ( 168 | w[e] 169 | / len(e) 170 | * aggregate_local_core_values(list(e), order, local_core_values) 171 | ) 172 | 173 | s_tmp = order[i] 174 | order[i] = order[j] 175 | order[j] = s_tmp 176 | 177 | for e in adj[i]: 178 | new_R += ( 179 | w[e] 180 | / len(e) 181 | * aggregate_local_core_values(list(e), order, local_core_values) 182 | ) 183 | 184 | for e in adj[j]: 185 | new_R += ( 186 | w[e] 187 | / len(e) 188 | * aggregate_local_core_values(list(e), order, local_core_values) 189 | ) 190 | 191 | if new_R < R: 192 | s_tmp = order[i] 193 | order[i] = order[j] 194 | order[j] = s_tmp 195 | else: 196 | R = new_R 197 | 198 | for node in range(N_nodes): 199 | cs[node] = cs[node] + local_core_values[order[node]] * R 200 | 201 | print("{} of {} iter".format(n_iter, N_ITER)) 202 | 203 | max_node = max(cs, key=cs.get) 204 | Z = 1 / cs[max_node] 205 | 206 | for node in range(N_nodes): 207 | cs[node] = Z * cs[node] 208 | 209 | id2node = {} 210 | for node in node2id: 211 | id2node[node2id[node]] = node 212 | 213 | return {id2node[i]: cs[i] for i in cs} 214 | -------------------------------------------------------------------------------- /hypergraphx/communities/hy_mmsbm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/hypergraphx/communities/hy_mmsbm/__init__.py -------------------------------------------------------------------------------- /hypergraphx/communities/hy_mmsbm/_linear_ops.py: -------------------------------------------------------------------------------- 1 | """Various linear operations. 2 | In the function names, the following abbreviations are utilized: 3 | - bf = bilinear form 4 | - qf = quadratic form 5 | """ 6 | 7 | import numpy as np 8 | 9 | 10 | def qf(u: np.ndarray, w: np.ndarray) -> np.ndarray: 11 | """ 12 | Quadratic form between a vector u and a matrix w. 13 | .. math:: 14 | u^T w u 15 | 16 | Parameters 17 | ---------- 18 | u: array of shape (..., K). 19 | The quadratic form operation is batched along all the dimensions but the last. 20 | w: square matrix of shape (K, K) 21 | 22 | Returns 23 | ------- 24 | The array of quadratic form values. If u has shape (..., K), the returned array has 25 | shape (...). 26 | """ 27 | K = u.shape[-1] 28 | assert w.shape == (K, K), "Shapes of u and w are incorrect." 29 | return ((u @ w) * u).sum(axis=-1) 30 | 31 | 32 | def bf(u: np.ndarray, v: np.ndarray, w: np.ndarray) -> np.ndarray: 33 | """ 34 | Bilinear form between two vectors u, v and a matrix w. 35 | .. math:: 36 | u^T w v 37 | 38 | Parameters 39 | ---------- 40 | u: array of shape (N1, K) or (K,). 41 | The bilinear form operation is batched for the N1 vectors is u is 2-dimensional. 42 | v: array of shape (N2, K) or (K,). 43 | The bilinear form operation is batched for the N2 vectors is v is 2-dimensional. 44 | w: square matrix of shape (K, K) 45 | 46 | Returns 47 | ------- 48 | The array of bilinear form values. If u and v are batches of vectors, it has shape 49 | (N1, N2). 50 | """ 51 | assert ( 52 | u.shape[-1] == v.shape[-1] == w.shape[0] == w.shape[1] 53 | ), "Shapes are not compatible." 54 | return (u @ w) @ v.T 55 | 56 | 57 | def qf_and_sum(u: np.ndarray, w: np.ndarray) -> float: 58 | """ 59 | Quadratic form and sum. 60 | For a set of vectors u_i of length K and a matrix K, compute implements 61 | .. math:: 62 | \sum_i u_i^T w u_i 63 | 64 | Parameters 65 | ---------- 66 | u: batch of vectors 67 | A number N of vectors of length K are collected along the first dimension u, 68 | which has shape (N, K). 69 | w: square matrix 70 | Matrix of shape (K, K). 71 | 72 | Returns 73 | ------- 74 | The scalar result of the operation. 75 | """ 76 | assert u.shape[1] == w.shape[0] == w.shape[1], "Shapes are incorrect." 77 | return ((u @ w) * u).sum() 78 | 79 | 80 | def bf_and_sum(u: np.ndarray, w: np.ndarray) -> float: 81 | """ 82 | Bilinear form and sum. 83 | For a set of vectors u_i of length K and a symmetric matrix w of shape (K, K), this 84 | function implements 85 | .. math:: 86 | \sum_{i None: 25 | """Initialize the model. 26 | 27 | Parameters 28 | ---------- 29 | seed: random seed. 30 | inf: initial value for the log-likelihood. 31 | n_realizations: number of realizations with different random initialization. 32 | out_inference: flag to store the inferred parameters. 33 | out_folder: path to store the output. 34 | end_file: output file suffix. 35 | """ 36 | # Training related attributes. 37 | self.seed = seed 38 | self.inf = inf 39 | self.n_realizations = n_realizations 40 | # Output related attributes. 41 | self.out_inference = out_inference 42 | self.out_folder = out_folder 43 | self.end_file = end_file 44 | # Membership matrix 45 | self.u: Optional[np.array] = None 46 | 47 | def fit(self, hypergraph: Hypergraph, K: int, weighted_L: bool = False) -> np.array: 48 | """Perform community detection on hypergraphs with spectral clustering. 49 | 50 | Parameters 51 | ---------- 52 | hypergraph: the hypergraph to perform inference on. 53 | K: number of communities. 54 | weighted_L: flag to use the weighted Laplacian. 55 | 56 | Returns 57 | ------- 58 | u: hard-membership matrix of dimension (N, K). 59 | """ 60 | # Initialize all the parameters needed for training. 61 | self._init_data(hypergraph=hypergraph, K=K) 62 | 63 | # Get the Laplacian matrix. 64 | self._extract_laplacian(weighted_L=weighted_L) 65 | # Get eigenvalues and eigenvectors of the Laplacian matrix. 66 | e_vals, e_vecs = self.extract_eigenvectors() 67 | # Extract hard-memberships by applying the K-Means algorithm to the eigenvectors. 68 | self.u = self.apply_kmeans(e_vecs.real, seed=self.seed) 69 | 70 | # Save inferred parameter. 71 | if self.out_inference: 72 | self.output_results() 73 | 74 | return self.u 75 | 76 | def _init_data( 77 | self, 78 | hypergraph: Hypergraph, 79 | K: int, 80 | ): 81 | """Initialize parameters for the inference. 82 | 83 | Parameters 84 | ---------- 85 | hypergraph: the hypergraph to perform inference on. 86 | K: number of communities. 87 | """ 88 | # Weighted incidence matrix. 89 | self.incidence = incidence_matrix(hypergraph) 90 | # Binary incidence matrix. 91 | self.binary_incidence = binary_incidence_matrix(hypergraph) 92 | 93 | # Number of nodes, and number of hyperedges. 94 | self.N, self.E = self.incidence.shape 95 | # Number of communities. 96 | self.K = K 97 | # Maximum observed hyperedge size. 98 | self.D = max([len(e) for e in np.array(hypergraph.get_edges(), dtype=object)]) 99 | 100 | # Nodes' degree. 101 | # TODO: is there a better way to get the array with the node degrees? 102 | self.node_degree = self.binary_incidence.sum(axis=1) 103 | # TODO: is there a better way to get the weighted degrees? 104 | self.node_degree_weighted = self.incidence.sum(axis=1) 105 | # Hyperedges' size. 106 | self.hye_size = np.array(hypergraph.get_sizes()) 107 | # TODO: is there a better way to get the weighted sizes? 108 | self.hye_size_weighted = self.incidence.sum(axis=1) 109 | 110 | # Isolated nodes. 111 | self.isolates = np.where(self.incidence.getnnz(1) == 0)[ 112 | 0 113 | ] # TODO: implement it as a core method 114 | # Non-isolated nodes. 115 | self.non_isolates = np.where(self.incidence.getnnz(1) != 0)[ 116 | 0 117 | ] # TODO: implement it as a core method 118 | 119 | def _extract_laplacian(self, weighted_L: bool = False) -> None: 120 | # Check for division by zero and handle isolated nodes 121 | invDE = np.diag(1.0 / np.where(self.hye_size == 0, 1, self.hye_size)) 122 | invDV2 = np.diag( 123 | np.sqrt(1.0 / np.where(self.node_degree == 0, 1, self.node_degree)) 124 | ) 125 | 126 | # set to zero for isolated nodes - we check directly on self.node_degree 127 | invDV2[self.node_degree == 0, self.node_degree == 0] = 0 128 | 129 | if weighted_L: 130 | dense_incidence = self.incidence.toarray() 131 | HT = dense_incidence.T 132 | self.L = np.eye(self.N) - invDV2 @ dense_incidence @ invDE @ HT @ invDV2 133 | else: 134 | dense_binary_incidence = self.binary_incidence.toarray() 135 | HT = dense_binary_incidence.T 136 | self.L = ( 137 | np.eye(self.N) - invDV2 @ dense_binary_incidence @ invDE @ HT @ invDV2 138 | ) 139 | 140 | def extract_eigenvectors(self) -> Tuple[np.array, np.array]: 141 | """Extract eigenvalues and eigenvectors of the Laplacian matrix.""" 142 | e_vals, e_vecs = np.linalg.eig(self.L[self.non_isolates][:, self.non_isolates]) 143 | sorted_indices = np.argsort(e_vals) 144 | return e_vals[sorted_indices[: self.K]], e_vecs[:, sorted_indices[1 : self.K]] 145 | 146 | def apply_kmeans(self, X: np.array, seed: int = 10) -> np.array: 147 | """Apply K-means algorithm to the eigenvectors of the Laplacian matrix. 148 | 149 | Parameters 150 | ---------- 151 | X: matrix with eigenvectors. 152 | seed: random seed. 153 | 154 | Returns 155 | ------- 156 | X_pred: membership matrix. 157 | """ 158 | y_pred = KMeans( 159 | n_clusters=self.K, random_state=seed, n_init=self.n_realizations 160 | ).fit_predict(X) 161 | X_pred = np.zeros((self.N, self.K)) 162 | for idx, i in enumerate(self.non_isolates): 163 | X_pred[i, y_pred[idx]] = 1 164 | return X_pred 165 | 166 | def output_results(self) -> None: 167 | """Save the results in a .npz file.""" 168 | outfile = self.out_folder + "theta" + self.end_file 169 | np.savez_compressed(outfile + ".npz", u=self.u) 170 | print(f'\nInferred parameters saved in: {outfile + ".npz"}') 171 | print('To load: theta=np.load(filename), then e.g. theta["u"]') 172 | -------------------------------------------------------------------------------- /hypergraphx/communities/hypergraph_mt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/hypergraphx/communities/hypergraph_mt/__init__.py -------------------------------------------------------------------------------- /hypergraphx/communities/hyperlink_comm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/hypergraphx/communities/hyperlink_comm/__init__.py -------------------------------------------------------------------------------- /hypergraphx/communities/hyperlink_comm/hyperlink_communities.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import numpy as np 3 | import scipy.spatial.distance as ssd 4 | from scipy.cluster.hierarchy import fcluster, linkage 5 | 6 | from hypergraphx import Hypergraph 7 | from hypergraphx.measures.edge_similarity import jaccard_distance as jaccard 8 | from hypergraphx.readwrite.load import _load_pickle 9 | from hypergraphx.readwrite.save import _save_pickle 10 | 11 | 12 | def hyperlink_communities( 13 | H: Hypergraph, load_distances=None, save_distances=None 14 | ) -> np.ndarray: 15 | """ 16 | Computes the dendrogram of the given hypergraph 17 | 18 | Parameters 19 | ---------- 20 | H : Hypergraph 21 | The hypergraph of interest 22 | load_distances : str 23 | The path to load the distances from 24 | save_distances : str 25 | The path to save the distances to 26 | 27 | Returns 28 | ------- 29 | np.ndarray 30 | The dendrogram of the given hypergraph 31 | 32 | """ 33 | print("Hypergraph info - nodes: {} edges: {}".format(H.num_nodes(), H.num_edges())) 34 | lcc = H.largest_component() 35 | H = H.subhypergraph(lcc) 36 | h = H.get_edges() 37 | print( 38 | "Subhypergraph info - nodes: {} edges: {}".format(H.num_nodes(), H.num_edges()) 39 | ) 40 | 41 | adj = {} 42 | edge_to_id = {} 43 | 44 | cont = 0 45 | for e in h: 46 | e = tuple(sorted(e)) 47 | edge_to_id[e] = cont 48 | for n in e: 49 | if n not in adj: 50 | adj[n] = [] 51 | adj[n].append(e) 52 | cont += 1 53 | 54 | print("Computing distances") 55 | 56 | G = nx.Graph() 57 | G.add_nodes_from([i for i in range(len(h))]) 58 | 59 | try: 60 | X = _load_pickle("{}.hlcd".format(load_distances)) 61 | except FileNotFoundError: 62 | vis = {} 63 | c = 0 64 | for n in adj: 65 | print("Done {} of {}".format(c, len(adj))) 66 | for i in range(len(adj[n]) - 1): 67 | for j in range(i + 1, len(adj[n])): 68 | k = tuple(sorted((edge_to_id[adj[n][i]], edge_to_id[adj[n][j]]))) 69 | e_i = set(adj[n][i]) 70 | e_j = set(adj[n][j]) 71 | if k not in vis: 72 | w = jaccard(e_i, e_j) 73 | if w > 0: 74 | G.add_edge( 75 | edge_to_id[adj[n][i]], edge_to_id[adj[n][j]], weight=w 76 | ) 77 | vis[k] = True 78 | c += 1 79 | 80 | X = nx.to_numpy_array(G, weight="weight", nonedge=1.0) 81 | _save_pickle(X, "{}.hlcd".format(save_distances)) 82 | 83 | print("dist computed") 84 | 85 | np.fill_diagonal(X, 0.0) 86 | dist_matrix = ssd.squareform(X) 87 | dendrogram = linkage(dist_matrix, method="average") 88 | return dendrogram 89 | 90 | 91 | def _cut_dendrogram(dendrogram, cut_height): 92 | cut = fcluster(dendrogram, t=cut_height, criterion="distance") 93 | return cut 94 | 95 | 96 | def _edge_label2node_labels(h, labels): 97 | nodes = {} 98 | for i in range(len(labels)): 99 | label_arco = labels[i] 100 | for nodo in h[i]: 101 | if nodo not in nodes: 102 | nodes[nodo] = [] 103 | nodes[nodo].append(label_arco) 104 | else: 105 | nodes[nodo].append(label_arco) 106 | return nodes 107 | 108 | 109 | def get_num_hyperlink_communties(dendrogram: np.ndarray, cut_height: float) -> int: 110 | """ 111 | Returns the number of communities in the dendrogram at the cut height 112 | 113 | Parameters 114 | ---------- 115 | dendrogram : np.ndarray 116 | 117 | cut_height : float 118 | The cut height 119 | Returns 120 | ------- 121 | int 122 | The number of communities 123 | """ 124 | cut = _cut_dendrogram(dendrogram, cut_height) 125 | return len(set(cut)) 126 | 127 | 128 | def overlapping_communities( 129 | H: Hypergraph, dendrogram: np.ndarray, cut_height: float 130 | ) -> dict: 131 | """ 132 | Returns the overlapping communities in the dendrogram at the given cut height 133 | 134 | Parameters 135 | ---------- 136 | H : Hypergraph 137 | The hypergraph of interest 138 | dendrogram : np.ndarray 139 | The dendrogram of the given hypergraph 140 | cut_height 141 | The cut height 142 | Returns 143 | ------- 144 | dict 145 | The overlapping communities 146 | """ 147 | cut = _cut_dendrogram(dendrogram, cut_height) 148 | h = H.get_edges() 149 | labels = _edge_label2node_labels(h, cut) 150 | return labels 151 | -------------------------------------------------------------------------------- /hypergraphx/core/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /hypergraphx/dynamics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/hypergraphx/dynamics/__init__.py -------------------------------------------------------------------------------- /hypergraphx/dynamics/contagion.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def simplicial_contagion(hypergraph, I_0, T, beta, beta_D, mu): 5 | """ 6 | Simulates the contagion process on a simplicial hypergraph. 7 | The process is run for T time steps. 8 | The initial condition is given by I_0, which is a dictionary where the keys are the nodes and the values are 1 if the node is infected and 0 otherwise. 9 | The infection rate is beta, the three-body infection rate is beta_D, and the recovery rate is mu. 10 | The output is a vector of length T, where the i-th entry is the fraction of infected nodes at time i. 11 | 12 | Parameters 13 | ---------- 14 | hypergraph : hypergraphx.Hypergraph 15 | The hypergraph on which the contagion process is run. 16 | 17 | I_0 : dictionary 18 | The initial condition of the contagion process. 19 | 20 | T : int 21 | The number of time steps. 22 | 23 | beta : float 24 | The infection rate. 25 | 26 | beta_D : float 27 | The three-body infection rate. 28 | 29 | mu : float 30 | The recovery rate. 31 | 32 | Returns 33 | ------- 34 | numpy.ndarray 35 | The fraction of infected nodes at each time step. 36 | """ 37 | 38 | numberInf = np.linspace(0, 0, T) 39 | Infected = sum(I_0.values()) 40 | numberInf[0] = Infected 41 | N = len(I_0) 42 | nodes = hypergraph.get_nodes() 43 | mapping = hypergraph.get_mapping() 44 | I_old = I_0.copy() 45 | t = 1 46 | 47 | while Infected > 0 and t < T: 48 | # I_new = np.copy(I_old) 49 | I_new = I_old.copy() 50 | 51 | # We run over the nodes 52 | for node in nodes: 53 | # if the node is susceptible, we run the infection process 54 | if I_old[node] == 0: 55 | # we first run the two-body infections 56 | neighbors = hypergraph.get_neighbors(node, order=1) 57 | for neigh in neighbors: 58 | if I_old[neigh] == 1 and np.random.random() < beta: 59 | I_new[node] = 1 60 | break # if the susceptile node gets infected, we stop iterating over its neighbors 61 | if I_new[node] == 1: 62 | continue # if the susceptile node is already infected, we don't run the three-body processes 63 | # we run the three-body infections 64 | triplets = hypergraph.get_incident_edges(node, order=2) 65 | for triplet in triplets: 66 | neighbors = list(triplet) 67 | neighbors.remove(node) 68 | neigh1, neigh2 = tuple(neighbors) 69 | if ( 70 | I_old[neigh1] == 1 71 | and I_old[neigh2] == 1 72 | and np.random.random() < beta_D 73 | ): 74 | I_new[node] = 1 75 | break # if the susceptile node gets infected, we stop iterating over the triplets 76 | # if the node is infected, we run the recovery process 77 | elif np.random.random() < mu: 78 | I_new[node] = 0 79 | 80 | I_old = I_new.copy() 81 | Infected = sum(I_new.values()) 82 | numberInf[t] = Infected 83 | t = t + 1 84 | 85 | return numberInf / N 86 | -------------------------------------------------------------------------------- /hypergraphx/dynamics/randwalk.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import sparse 3 | 4 | from hypergraphx import Hypergraph 5 | 6 | 7 | def transition_matrix(HG: Hypergraph) -> sparse.spmatrix: 8 | """Compute the transition matrix of the random walk on the hypergraph. 9 | 10 | Parameters 11 | ---------- 12 | HG : Hypergraph 13 | The hypergraph on which the random walk is defined. 14 | 15 | Returns 16 | ------- 17 | K : np.ndarray 18 | 19 | The transition matrix of the random walk on the hypergraph. 20 | 21 | References 22 | ---------- 23 | [1] Timoteo Carletti, Federico Battiston, Giulia Cencetti, and Duccio Fanelli, Random walks on hypergraphs, Phys. Rev. E 96, 012308 (2017) 24 | """ 25 | N = HG.num_nodes() 26 | # assert if HG i connected 27 | assert HG.is_connected(), "The hypergraph is not connected" 28 | hedge_list = HG.get_edges() 29 | T = np.zeros((N, N)) 30 | for l in hedge_list: 31 | for i in range(len(l)): 32 | for j in range(i + 1, len(l)): 33 | T[l[i], l[j]] += len(l) - 1 34 | T[l[j], l[i]] += len(l) - 1 35 | # cast t to numpy.matrix 36 | # make it sparse 37 | 38 | T = np.matrix(T) 39 | T = T / T.sum(axis=1) 40 | 41 | T = sparse.csr_matrix(T) 42 | return T 43 | 44 | 45 | def random_walk(HG: Hypergraph, s: int, time: int) -> list: 46 | """Compute the random walk on the hypergraph. 47 | 48 | Parameters 49 | ---------- 50 | HG : Hypergraph 51 | The hypergraph on which the random walk is defined. 52 | s : int 53 | The starting node of the random walk. 54 | time : int 55 | The number of steps of the random walk. 56 | 57 | Returns 58 | ------- 59 | nodes : list 60 | The list of nodes visited by the random walk. 61 | """ 62 | K = np.array(transition_matrix(HG).todense()) 63 | nodes = [s] 64 | for t in range(time): 65 | next_node = np.random.choice(K.shape[0], p=K[nodes[-1], :]) 66 | nodes.append(next_node) 67 | return nodes 68 | 69 | 70 | def RW_stationary_state(HG: Hypergraph) -> np.ndarray: 71 | """Compute the stationary state of the random walk on the hypergraph. 72 | 73 | Parameters 74 | ---------- 75 | HG : Hypergraph 76 | The hypergraph on which the random walk is defined. 77 | 78 | Returns 79 | ------- 80 | stationary_state : np.ndarray 81 | The stationary state of the random walk on the hypergraph. 82 | """ 83 | K = np.array(transition_matrix(HG).todense()) 84 | stationary_state = np.linalg.solve(np.eye(K.shape[0]) - K.T, np.ones(K.shape[0])) 85 | stationary_state = stationary_state / np.sum(stationary_state) 86 | return stationary_state 87 | 88 | 89 | def random_walk_density(HG: Hypergraph, s: np.ndarray, time: int) -> list: 90 | """Compute the random walk on the hypergraph with starting density vector. 91 | 92 | Parameters 93 | ---------- 94 | HG : Hypergraph 95 | The hypergraph on which the random walk is defined. 96 | s : np.ndarray 97 | The starting density vector of the random walk. 98 | 99 | Returns 100 | ------- 101 | nodes : list 102 | The list of density vectors over time. 103 | """ 104 | assert np.isclose(np.sum(s), 1), "The vector is not a probability density" 105 | 106 | K = np.array(transition_matrix(HG).todense()) 107 | starting_density = s 108 | density_list = [starting_density] 109 | for t in range(time): 110 | s = s @ K 111 | density_list.append(s) 112 | return density_list 113 | -------------------------------------------------------------------------------- /hypergraphx/filters/__init__.py: -------------------------------------------------------------------------------- 1 | from hypergraphx.filters.statistical_filters import get_svc, get_svh 2 | from hypergraphx.filters.metadata_filters import filter_hypergraph 3 | -------------------------------------------------------------------------------- /hypergraphx/filters/metadata_filters.py: -------------------------------------------------------------------------------- 1 | def filter_hypergraph( 2 | hypergraph, node_criteria=None, edge_criteria=None, mode="keep", keep_edges=False 3 | ): 4 | """ 5 | Filters nodes and edges of a hypergraph based on metadata attributes and allowed values. 6 | 7 | Parameters 8 | ---------- 9 | hypergraph : object 10 | The hypergraph instance to filter. Must have `_node_metadata` and `_edge_metadata` attributes, 11 | along with `remove_node` and `remove_edge` methods. 12 | node_criteria : dict, optional 13 | A dictionary specifying metadata attribute keys and allowed values for nodes. 14 | Example: {"type": ["person", "animal"]}. 15 | edge_criteria : dict, optional 16 | A dictionary specifying metadata attribute keys and allowed values for edges. 17 | Example: {"relationship": ["friendship", "collaboration"]}. 18 | mode : str, optional 19 | Either "keep" to retain only matching nodes and edges, or "remove" to discard them. Default is "keep". 20 | keep_edges : bool, optional 21 | If False (default), edges involving removed nodes are fully removed. 22 | If True, edges are kept but the removed nodes are excluded from the edges. 23 | 24 | Returns 25 | ------- 26 | None 27 | The hypergraph is modified in place. 28 | 29 | Raises 30 | ------ 31 | ValueError 32 | If the mode is not "keep" or "remove". 33 | """ 34 | 35 | if mode not in {"keep", "remove"}: 36 | raise ValueError('Mode must be either "keep" or "remove".') 37 | 38 | def matches_criteria(metadata, criteria): 39 | return all(metadata.get(attr) in values for attr, values in criteria.items()) 40 | 41 | if node_criteria is not None: 42 | nodes_to_process = [] 43 | for node, node_metadata in hypergraph.get_nodes(metadata=True).items(): 44 | matches = matches_criteria(node_metadata, node_criteria) 45 | if (mode == "keep" and not matches) or (mode == "remove" and matches): 46 | nodes_to_process.append(node) 47 | 48 | for node in nodes_to_process: 49 | hypergraph.remove_node(node, keep_edges=keep_edges) 50 | 51 | if edge_criteria is not None: 52 | edges_to_process = [] 53 | for edge, edge_metadata in hypergraph.get_edges(metadata=True).items(): 54 | matches = matches_criteria(edge_metadata, edge_criteria) 55 | if (mode == "keep" and not matches) or (mode == "remove" and matches): 56 | edges_to_process.append(edge) 57 | 58 | for edge in edges_to_process: 59 | hypergraph.remove_edge(edge) 60 | -------------------------------------------------------------------------------- /hypergraphx/generation/__init__.py: -------------------------------------------------------------------------------- 1 | from hypergraphx.generation.random import random_hypergraph 2 | -------------------------------------------------------------------------------- /hypergraphx/generation/activity_driven.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from hypergraphx.core.temporal_hypergraph import TemporalHypergraph 4 | from hypergraphx.dynamics.randwalk import * 5 | 6 | 7 | def rnd_pwl(xmin, xmax, g, size=1): 8 | r = np.random.random(size=size) 9 | return (r * (xmax ** (1.0 - g) - xmin ** (1.0 - g)) + xmin ** (1.0 - g)) ** ( 10 | 1.0 / (1.0 - g) 11 | ) 12 | 13 | 14 | def HOADmodel(N: int, activities_per_order: dict, time=100): 15 | """ 16 | Generate a temporal hypergraph according to the HOAD model. 17 | 18 | Parameters 19 | ---------- 20 | N : int 21 | The number of nodes in the hypergraph. 22 | activities_per_order : dict 23 | The dictionary of activities per order. The keys are the orders and the values are the activities. 24 | time : int 25 | The number of time steps. 26 | 27 | Returns 28 | ------- 29 | HG : TemporalHypergraph 30 | The temporal hypergraph generated according to the HOAD model. 31 | 32 | """ 33 | 34 | hyperlinks = [] 35 | for order in activities_per_order.keys(): 36 | act_vect = activities_per_order[order] 37 | for t in range(time): 38 | for node_i in range(N): 39 | if act_vect[node_i] > random.random(): 40 | neigh_list = random.sample(range(N), order) 41 | neigh_list.append(node_i) 42 | if len(neigh_list) == len(set(neigh_list)): 43 | hyperlinks.append((t, tuple(neigh_list))) 44 | HG = TemporalHypergraph(hyperlinks) 45 | return HG 46 | -------------------------------------------------------------------------------- /hypergraphx/generation/configuration_model.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | 3 | import numpy as np 4 | 5 | from hypergraphx import Hypergraph 6 | 7 | 8 | def _cm_MCMC(hypergraph, n_steps=1000, label="edge", n_clash=1, detailed=True): 9 | """ 10 | Conduct Markov Chain Monte Carlo in order to approximately 11 | sample from the space of appropriately-labeled graphs. 12 | n_steps: number of steps to perform 13 | label: the label space to use. Can take values in ['vertex' , 'stub', 'edge']. 14 | n_clash: the number of clashes permitted when updating the edge counts in vertex-labeled MH. 15 | n_clash = 0 will be exact but very slow. 16 | n_clash >= 2 may lead to performance gains at the cost of decreased accuracy. 17 | detailed: if True, preserve the number of edges of given dimension incident to each node 18 | """ 19 | 20 | def proposal_generator(m): 21 | # Propose a transition in stub- and edge-labeled MH. 22 | 23 | def __proposal(edge_list): 24 | i, j = np.random.randint(0, m, 2) 25 | f1, f2 = edge_list[i], edge_list[j] 26 | if detailed: 27 | while len(f1) != len(f2): 28 | i, j = np.random.randint(0, m, 2) 29 | f1, f2 = edge_list[i], edge_list[j] 30 | g1, g2 = __pairwise_reshuffle(f1, f2) 31 | return i, j, f1, f2, g1, g2 32 | 33 | return __proposal 34 | 35 | def __pairwise_reshuffle(f1, f2): 36 | # Randomly reshuffle the nodes of two edges while preserving their sizes. 37 | 38 | f = list(f1) + list(f2) 39 | intersection = set(f1).intersection(set(f2)) 40 | ix = list(intersection) 41 | g1 = ix.copy() 42 | g2 = ix.copy() 43 | 44 | for v in ix: 45 | f.remove(v) 46 | f.remove(v) 47 | 48 | for v in f: 49 | if (len(g1) < len(f1)) & (len(g2) < len(f2)): 50 | if np.random.rand() < 0.5: 51 | g1.append(v) 52 | else: 53 | g2.append(v) 54 | elif len(g1) < len(f1): 55 | g1.append(v) 56 | elif len(g2) < len(f2): 57 | g2.append(v) 58 | if len(g1) != len(f1): 59 | print("oops") 60 | print(f1, f2, g1, g2) 61 | return tuple(sorted(g1)), tuple(sorted(g2)) 62 | 63 | def stub_edge_mh(message=True): 64 | mh_rounds = 0 65 | mh_steps = 0 66 | c_new = [list(c) for c in hypergraph.get_edges()] 67 | m = len(c_new) 68 | 69 | proposal = proposal_generator(m) 70 | 71 | def mh_step(): 72 | i, j, f1, f2, g1, g2 = proposal(c_new) 73 | c_new[i] = sorted(g1) 74 | c_new[j] = sorted(g2) 75 | 76 | n = 0 77 | 78 | while n < n_steps: 79 | mh_step() 80 | n += 1 81 | 82 | new_h = Hypergraph() 83 | # check this behavior 84 | generated_edges = list(set([tuple(sorted(f)) for f in c_new])) 85 | new_h.add_edges(generated_edges) 86 | mh_steps += n 87 | mh_rounds += 1 88 | 89 | if message: 90 | print(str(n_steps) + " steps completed.") 91 | 92 | return new_h 93 | 94 | def vertex_labeled_mh(message=True): 95 | rand = np.random.rand 96 | randint = np.random.randint 97 | 98 | k = 0 99 | done = False 100 | c = Counter(hypergraph._edge_list) 101 | 102 | epoch_num = 0 103 | n_rejected = 0 104 | 105 | m = sum(c.values()) 106 | 107 | mh_rounds = 0 108 | mh_steps = 0 109 | 110 | while not done: 111 | # initialize epoch 112 | l = list(c.elements()) 113 | 114 | add = [] 115 | remove = [] 116 | num_clash = 0 117 | epoch_num += 1 118 | 119 | # within each epoch 120 | 121 | k_rand = 20000 # generate many random numbers at a time 122 | 123 | k_ = 0 124 | ij = randint(0, m, k_rand) 125 | a = rand(k_rand) 126 | while True: 127 | if k_ >= k_rand / 2.0: 128 | ij = randint(0, m, k_rand) 129 | a = rand(k_rand) 130 | k_ = 0 131 | i, j = (ij[k_], ij[k_ + 1]) 132 | k_ += 2 133 | 134 | f1, f2 = l[i], l[j] 135 | while f1 == f2: 136 | i, j = (ij[k_], ij[k_ + 1]) 137 | k_ += 2 138 | f1, f2 = l[i], l[j] 139 | if detailed: 140 | while len(f1) != len(f2): 141 | i, j = (ij[k_], ij[k_ + 1]) 142 | k_ += 2 143 | f1, f2 = l[i], l[j] 144 | while f1 == f2: 145 | i, j = (ij[k_], ij[k_ + 1]) 146 | k_ += 2 147 | f1, f2 = l[i], l[j] 148 | 149 | inter = 2 ** (-len((set(f1).intersection(set(f2))))) 150 | if a[k_] > inter / (c[f1] * c[f2]): 151 | n_rejected += 1 152 | k += 1 153 | else: # if proposal was accepted 154 | g1, g2 = __pairwise_reshuffle(f1, f2) 155 | num_clash += remove.count(f1) + remove.count(f2) 156 | if (num_clash >= n_clash) & (n_clash >= 1): 157 | break 158 | else: 159 | remove.append(f1) 160 | remove.append(f2) 161 | add.append(g1) 162 | add.append(g2) 163 | k += 1 164 | if n_clash == 0: 165 | break 166 | 167 | add = Counter(add) 168 | add.subtract(Counter(remove)) 169 | 170 | c.update(add) 171 | done = k - n_rejected >= n_steps 172 | if message: 173 | print( 174 | str(epoch_num) 175 | + " epochs completed, " 176 | + str(k - n_rejected) 177 | + " steps taken, " 178 | + str(n_rejected) 179 | + " steps rejected." 180 | ) 181 | 182 | new_h = Hypergraph() 183 | new_h.add_edges([tuple(sorted(f)) for f in list(c.elements())]) 184 | mh_steps += k - n_rejected 185 | mh_rounds += 1 186 | return new_h 187 | 188 | if (label == "edge") or (label == "stub"): 189 | return stub_edge_mh() 190 | elif label == "vertex": 191 | return vertex_labeled_mh() 192 | else: 193 | print("not implemented") 194 | 195 | 196 | def configuration_model( 197 | hypergraph, 198 | n_steps=1000, 199 | label="edge", 200 | order=None, 201 | size=None, 202 | n_clash=1, 203 | detailed=True, 204 | ): 205 | if order is not None and size is not None: 206 | raise ValueError("Only one of order and size can be specified.") 207 | if order is None and size is None: 208 | return _cm_MCMC( 209 | hypergraph, n_steps=n_steps, label=label, n_clash=n_clash, detailed=detailed 210 | ) 211 | 212 | if size is None: 213 | size = order + 1 214 | 215 | tmp_h = hypergraph.get_edges( 216 | size=size, up_to=False, subhypergraph=True, keep_isolated_nodes=True 217 | ) 218 | shuffled = _cm_MCMC( 219 | tmp_h, n_steps=n_steps, label=label, n_clash=n_clash, detailed=detailed 220 | ) 221 | for e in hypergraph.get_edges(): 222 | if len(e) != size: 223 | shuffled.add_edge(e) 224 | return shuffled 225 | -------------------------------------------------------------------------------- /hypergraphx/generation/directed_configuration_model.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from hypergraphx import DirectedHypergraph 4 | 5 | 6 | def directed_configuration_model(hypergraph: DirectedHypergraph) -> DirectedHypergraph: 7 | """ 8 | This function implements the directed configuration model to generate 9 | random hyperedges based on the given hypergraph. It ensures that each node's 10 | in-degree and out-degree are preserved in the generated hypergraph. 11 | The number of hyperedges is preserved. 12 | Head and tail size is preserved for each hyperedge. 13 | 14 | Parameters 15 | ---------- 16 | hypergraph : DirectedHypergraph 17 | The input hypergraph. 18 | 19 | Returns 20 | ------- 21 | DirectedHypergraph 22 | The generated hypergraph. 23 | """ 24 | 25 | hypergraph = list(hypergraph.get_edges()) 26 | num_steps_sources = len(hypergraph) * 10 27 | num_steps_targets = len(hypergraph) * 10 28 | 29 | new_hyperedges = [] 30 | 31 | for hyperedge in hypergraph: 32 | new_hyperedges.append((list(hyperedge[0]), list(hyperedge[1]))) 33 | for _ in range(num_steps_sources): 34 | id1 = random.randint(0, len(new_hyperedges) - 1) 35 | id2 = random.randint(0, len(new_hyperedges) - 1) 36 | 37 | if id1 == id2: 38 | continue 39 | 40 | source1 = new_hyperedges[id1][0] 41 | source2 = new_hyperedges[id2][0] 42 | 43 | # select random node from source1 44 | node1 = random.choice(source1) 45 | # select random node from source2 46 | node2 = random.choice(source2) 47 | 48 | if node2 in source1 or node1 in source2: 49 | continue 50 | 51 | # swap node1 and node2 52 | source1[source1.index(node1)] = node2 53 | source2[source2.index(node2)] = node1 54 | 55 | for _ in range(num_steps_targets): 56 | id1 = random.randint(0, len(new_hyperedges) - 1) 57 | id2 = random.randint(0, len(new_hyperedges) - 1) 58 | 59 | if id1 == id2: 60 | continue 61 | 62 | target1 = new_hyperedges[id1][1] 63 | target2 = new_hyperedges[id2][1] 64 | 65 | # select random node from target1 66 | node1 = random.choice(target1) 67 | # select random node from target2 68 | node2 = random.choice(target2) 69 | 70 | # swap node1 and node2 71 | if node2 in target1 or node1 in target2: 72 | continue 73 | target1[target1.index(node1)] = node2 74 | target2[target2.index(node2)] = node1 75 | 76 | final_hyperedges = [] 77 | for edge in new_hyperedges: 78 | final_hyperedges.append(tuple((tuple(sorted(edge[0])), tuple(sorted(edge[1]))))) 79 | 80 | new_hypergraph = DirectedHypergraph(edge_list=final_hyperedges) 81 | return new_hypergraph 82 | -------------------------------------------------------------------------------- /hypergraphx/generation/scale_free.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.stats import spearmanr 3 | 4 | from hypergraphx import Hypergraph 5 | 6 | 7 | def scale_free_hypergraph( 8 | num_nodes: int, 9 | edges_by_size: dict, 10 | scale_by_size: dict, 11 | correlated: bool = True, 12 | corr_target: float = None, 13 | num_shuffles: int = 0, 14 | ): 15 | """ 16 | Generate a scale-free hypergraph. 17 | 18 | Parameters 19 | ---------- 20 | num_nodes : int 21 | The number of nodes in the hypergraph. 22 | edges_by_size : dict 23 | A dictionary mapping the size of the hyperedges to the number of hyperedges of that size. 24 | scale_by_size : dict 25 | A dictionary mapping the size of the hyperedges to the scale parameter of the exponential distribution. 26 | correlated : bool 27 | Whether the exponential distributions of the different sizes should be correlated. 28 | corr_target : float 29 | The target correlation between the exponential distributions of the different sizes. 30 | num_shuffles : int 31 | 32 | Returns 33 | ------- 34 | Hypergraph 35 | A scale-free hypergraph with the given number of nodes and hyperedges for each size. 36 | """ 37 | if num_shuffles != 0 and not correlated: 38 | raise ValueError("Cannot shuffle if correlated == False") 39 | if num_shuffles < 0: 40 | raise ValueError("Cannot shuffle negative number of times") 41 | if corr_target < 0 or corr_target > 1: 42 | raise ValueError("Correlation must be between 0 and 1") 43 | if corr_target is not None and not correlated: 44 | raise ValueError("Cannot provide correlation value if correlated == False") 45 | if corr_target is not None and num_shuffles != 0: 46 | raise ValueError("Cannot provide both correlation value and number of shuffles") 47 | for k in edges_by_size: 48 | if k not in scale_by_size: 49 | raise ValueError("Must provide scale for each edge size") 50 | for k in scale_by_size: 51 | if k not in edges_by_size: 52 | raise ValueError("Must provide number of edges for each edge size") 53 | for k in edges_by_size: 54 | try: 55 | edges_by_size[k] = int(edges_by_size[k]) 56 | except ValueError: 57 | raise ValueError("Number of edges must be an integer") 58 | if edges_by_size[k] < 0: 59 | raise ValueError("Number of edges must be non-negative") 60 | h = Hypergraph() 61 | nodes = list(range(num_nodes)) 62 | h.add_nodes(nodes) 63 | old_dist = None 64 | for size in edges_by_size: 65 | num_edges = edges_by_size[size] 66 | scale = scale_by_size[size] 67 | exp_dist = np.random.exponential(scale, num_nodes) 68 | if correlated: 69 | exp_dist = list(sorted(exp_dist, reverse=True)) 70 | for _ in range(num_shuffles): 71 | a, b = np.random.choice(num_nodes, size=2, replace=False) 72 | exp_dist[a], exp_dist[b] = exp_dist[b], exp_dist[a] 73 | if corr_target != 1 and old_dist is not None: 74 | corr, _ = spearmanr(exp_dist, old_dist) 75 | while corr > corr_target: 76 | a, b = np.random.choice(num_nodes, size=2, replace=False) 77 | exp_dist[a], exp_dist[b] = exp_dist[b], exp_dist[a] 78 | corr, _ = spearmanr(exp_dist, old_dist) 79 | edges = set() 80 | while len(edges) < num_edges: 81 | edge = np.random.choice( 82 | nodes, size=size, replace=False, p=exp_dist / np.sum(exp_dist) 83 | ) 84 | edge = tuple(sorted(edge)) 85 | edges.add(edge) 86 | 87 | h.add_edges(edges) 88 | if old_dist is None: 89 | old_dist = exp_dist 90 | return h 91 | -------------------------------------------------------------------------------- /hypergraphx/linalg/__init__.py: -------------------------------------------------------------------------------- 1 | from hypergraphx.linalg.linalg import * 2 | -------------------------------------------------------------------------------- /hypergraphx/measures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/hypergraphx/measures/__init__.py -------------------------------------------------------------------------------- /hypergraphx/measures/degree.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from hypergraphx import ( 4 | Hypergraph, 5 | DirectedHypergraph, 6 | TemporalHypergraph, 7 | MultiplexHypergraph, 8 | ) 9 | 10 | 11 | def degree( 12 | hg: Hypergraph | DirectedHypergraph | TemporalHypergraph, 13 | node, 14 | order=None, 15 | size=None, 16 | ): 17 | """ 18 | Computes the degree of a node in the hypergraph. 19 | 20 | Parameters 21 | ---------- 22 | hg : Hypergraph|DirectedHypergraph|TemporalHypergraph 23 | The hypergraph of interest. 24 | node : Node 25 | The node to check. 26 | order : int, optional 27 | The order of the hyperedges to consider. If None, all hyperedges are considered. 28 | size : int, optional 29 | The size of the hyperedges to consider. If None, all hyperedges are considered. 30 | 31 | Returns 32 | ------- 33 | int 34 | The degree of the node. 35 | """ 36 | if order is not None and size is not None: 37 | raise ValueError("Order and size cannot be both specified.") 38 | if order is None and size is None: 39 | return len(hg.get_incident_edges(node)) 40 | elif size is not None: 41 | return len(hg.get_incident_edges(node, size=size)) 42 | elif order is not None: 43 | return len(hg.get_incident_edges(node, order=order)) 44 | 45 | 46 | def degree_sequence( 47 | hg: Hypergraph | DirectedHypergraph | TemporalHypergraph, order=None, size=None 48 | ): 49 | """ 50 | Computes the degree sequence of the hypergraph. 51 | 52 | Parameters 53 | ---------- 54 | hg : Hypergraph|DirectedHypergraph|TemporalHypergraph 55 | The hypergraph of interest. 56 | order : int, optional 57 | The order of the hyperedges to consider. If None, all hyperedges are considered. 58 | size : int, optional 59 | The size of the hyperedges to consider. If None, all hyperedges are considered. 60 | 61 | Returns 62 | ------- 63 | dict 64 | The degree sequence of the hypergraph. The keys are the nodes and the values are the degrees. 65 | """ 66 | if order is not None and size is not None: 67 | raise ValueError("Order and size cannot be both specified.") 68 | if size is not None: 69 | order = size - 1 70 | if order is None: 71 | return {node: hg.degree(node) for node in hg.get_nodes()} 72 | else: 73 | return {node: hg.degree(node, order=order) for node in hg.get_nodes()} 74 | 75 | 76 | def degree_correlation(hg: "Hypergraph") -> np.ndarray: 77 | """ 78 | Computes the degree sequence correlation matrix of the hypergraph. 79 | 80 | Parameters 81 | ---------- 82 | hg: Hypergraph 83 | The hypergraph of interest. 84 | 85 | Returns 86 | ------- 87 | np.ndarray 88 | The degree sequence correlation matrix of the hypergraph. 89 | The (i, j) entry is the Pearson correlation coefficient between the degree sequence at size i + 2 90 | and the degree sequence at size j + 2. 91 | """ 92 | from scipy.stats import pearsonr 93 | 94 | seqs = [hg.degree_sequence(size=size) for size in range(2, hg.max_size() + 1)] 95 | matrix_degree_corr = np.zeros((len(seqs), len(seqs))) 96 | for i in range(len(seqs)): 97 | for j in range(len(seqs)): 98 | matrix_degree_corr[i, j] = pearsonr( 99 | list(seqs[i].values()), list(seqs[j].values()) 100 | )[0] 101 | 102 | return matrix_degree_corr 103 | 104 | 105 | def degree_distribution( 106 | hg: Hypergraph | DirectedHypergraph | TemporalHypergraph, order=None, size=None 107 | ): 108 | """ 109 | Computes the degree distribution of the hypergraph. 110 | 111 | Parameters 112 | ---------- 113 | hg : Hypergraph|DirectedHypergraph|TemporalHypergraph 114 | The hypergraph of interest. 115 | order : int, optional 116 | The order of the hyperedges to consider. If None, all hyperedges are considered. 117 | size : int, optional 118 | The size of the hyperedges to consider. If None, all hyperedges are considered. 119 | 120 | Returns 121 | ------- 122 | dict 123 | The degree distribution of the hypergraph. The keys are the degrees and the values are the number of nodes with that degree. 124 | """ 125 | if order is not None and size is not None: 126 | raise ValueError("Order and size cannot be both specified.") 127 | if size is not None: 128 | order = size - 1 129 | if order is None: 130 | degree_seq = hg.degree_sequence() 131 | else: 132 | degree_seq = hg.degree_sequence(order=order) 133 | 134 | degree_dist = {} 135 | for node, deg in degree_seq.items(): 136 | if deg not in degree_dist: 137 | degree_dist[deg] = 0 138 | degree_dist[deg] += 1 139 | 140 | return degree_dist 141 | -------------------------------------------------------------------------------- /hypergraphx/measures/directed/__init__.py: -------------------------------------------------------------------------------- 1 | from hypergraphx.measures.directed.degree import * 2 | from hypergraphx.measures.directed.hyperedge_signature import hyperedge_signature_vector 3 | from hypergraphx.measures.directed.reciprocity import ( 4 | exact_reciprocity, 5 | strong_reciprocity, 6 | weak_reciprocity, 7 | ) 8 | -------------------------------------------------------------------------------- /hypergraphx/measures/directed/degree.py: -------------------------------------------------------------------------------- 1 | from hypergraphx import DirectedHypergraph 2 | 3 | 4 | def in_degree(hypergraph: DirectedHypergraph, node: int, order=None, size=None) -> int: 5 | return len(list(hypergraph.get_source_edges(node, order=order, size=size))) 6 | 7 | 8 | def out_degree(hypergraph: DirectedHypergraph, node: int, order=None, size=None) -> int: 9 | return len(list(hypergraph.get_target_edges(node, order=order, size=size))) 10 | 11 | 12 | def in_degree_sequence(hg: DirectedHypergraph, order=None, size=None): 13 | """ 14 | Computes the in-degree sequence of the hypergraph. 15 | 16 | Parameters 17 | ---------- 18 | hg : DirectedHypergraph 19 | The hypergraph of interest. 20 | order : int 21 | The order of the hyperedges to consider. If None, all hyperedges are considered. 22 | size : int 23 | The size of the hyperedges to consider. If None, all hyperedges are considered. 24 | 25 | Returns 26 | ------- 27 | dict 28 | The in-degree sequence of the hypergraph. The keys are the nodes and the values are the degrees. 29 | """ 30 | return { 31 | node: in_degree(hg, node, order=order, size=size) for node in hg.get_nodes() 32 | } 33 | 34 | 35 | def out_degree_sequence(hg: DirectedHypergraph, order=None, size=None): 36 | """ 37 | Computes the out-degree sequence of the hypergraph. 38 | 39 | Parameters 40 | ---------- 41 | hg : DirectedHypergraph 42 | The hypergraph of interest. 43 | order : int 44 | The order of the hyperedges to consider. If None, all hyperedges are considered. 45 | size : int 46 | The size of the hyperedges to consider. If None, all hyperedges are considered. 47 | 48 | Returns 49 | ------- 50 | dict 51 | The out-degree sequence of the hypergraph. The keys are the nodes and the values are the degrees. 52 | """ 53 | return { 54 | node: out_degree(hg, node, order=order, size=size) for node in hg.get_nodes() 55 | } 56 | -------------------------------------------------------------------------------- /hypergraphx/measures/directed/hyperedge_signature.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from hypergraphx import DirectedHypergraph 4 | 5 | 6 | def hyperedge_signature_vector(hypergraph: DirectedHypergraph, max_hyperedge_size=None): 7 | """ 8 | Compute the hyperedge signature vector of a directed hypergraph. The hyperedge signature is a vector that counts the 9 | number of hyperedges for each combination of source and target size. The signature is computed up to a maximum hyperedge size. If the maximum hyperedge 10 | size is not provided, the maximum size of the hyperedges in the hypergraph is used. 11 | 12 | Parameters 13 | ---------- 14 | hypergraph: DirectedHypergraph 15 | The directed hypergraph for which to compute the hyperedge signature vector. 16 | max_hyperedge_size: int, optional 17 | The maximum hyperedge size to consider in the signature. If not provided, the maximum size of the hyperedges in the hypergraph is used. 18 | 19 | Returns 20 | ------- 21 | numpy.ndarray 22 | The hyperedge signature vector. 23 | 24 | Raises 25 | ------ 26 | ValueError 27 | If the hypergraph is not a DirectedHypergraph. 28 | 29 | Examples 30 | -------- 31 | >>> from hypergraphx import DirectedHypergraph 32 | >>> from hypergraphx.measures.directed import hyperedge_signature_vector 33 | >>> edges = [ 34 | ... ((1, 2), (3, 4)), # Hyperedge with source size 2, target size 2 35 | ... ((5, ), (6, 7, 8)), # Hyperedge with source size 1, target size 3 36 | ... ] 37 | >>> hypergraph = DirectedHypergraph(edges) 38 | >>> result = hyperedge_signature_vector(hypergraph) 39 | # output 40 | array([0, 0, 1, 0, 1, 0, 0, 0, 0]) 41 | """ 42 | 43 | if not isinstance(hypergraph, DirectedHypergraph): 44 | raise ValueError("The hypergraph must be a DirectedHypergraph") 45 | 46 | if max_hyperedge_size is None: 47 | try: 48 | max_hyperedge_size = max(hypergraph.get_sizes()) 49 | except ValueError: 50 | return np.array([]) 51 | 52 | signature = np.zeros((max_hyperedge_size - 1, max_hyperedge_size - 1)) 53 | 54 | for hyperedge in hypergraph.get_edges(size=max_hyperedge_size, up_to=True): 55 | source_size = len(hyperedge[0]) 56 | target_size = len(hyperedge[1]) 57 | signature[source_size - 1, target_size - 1] += 1 58 | 59 | signature = np.array(signature.flatten()) 60 | return signature 61 | -------------------------------------------------------------------------------- /hypergraphx/measures/directed/reciprocity.py: -------------------------------------------------------------------------------- 1 | from hypergraphx import DirectedHypergraph 2 | 3 | 4 | def exact_reciprocity(hypergraph: DirectedHypergraph, max_hyperedge_size: int) -> dict: 5 | """ 6 | Calculate the exact reciprocity ratio for each hyperedge size in the hypergraph. 7 | 8 | Parameters 9 | ---------- 10 | hypergraph: DirectedHypergraph 11 | The hypergraph to calculate the exact reciprocity ratio for. 12 | max_hyperedge_size: int 13 | The maximum hyperedge size to consider 14 | 15 | Returns 16 | ------- 17 | dict 18 | A dictionary containing the exact reciprocity ratio for each hyperedge size. 19 | """ 20 | 21 | edges = hypergraph.get_edges() 22 | rec = {i: 0 for i in range(2, max_hyperedge_size + 1)} 23 | tot = {i: 0 for i in range(2, max_hyperedge_size + 1)} 24 | edge_set = {} 25 | 26 | # Count total edges of each size and populate edge_set 27 | for edge in edges: 28 | size = len(edge[0]) + len(edge[1]) 29 | if 2 <= size <= max_hyperedge_size: 30 | tot[size] += 1 31 | edge_tuple = (tuple(edge[0]), tuple(edge[1])) 32 | edge_set[edge_tuple] = 1 33 | 34 | # Count reciprocated edges 35 | for edge in edge_set: 36 | reciprocated_edge = (edge[1], edge[0]) 37 | if reciprocated_edge in edge_set: 38 | size = len(edge[0]) + len(edge[1]) 39 | rec[size] += 1 40 | 41 | # Calculate reciprocity ratios 42 | for size in range(2, max_hyperedge_size + 1): 43 | if tot[size] != 0: 44 | rec[size] = rec[size] / tot[size] 45 | else: 46 | rec[size] = 0 47 | 48 | return rec 49 | 50 | 51 | def strong_reciprocity(hypergraph: DirectedHypergraph, max_hyperedge_size: int) -> dict: 52 | """ 53 | Calculate the strong reciprocity ratio for each hyperedge size in the hypergraph. 54 | 55 | Parameters 56 | ---------- 57 | hypergraph: DirectedHypergraph 58 | The hypergraph to calculate the strong reciprocity ratio for. 59 | max_hyperedge_size: int 60 | The maximum hyperedge size to consider. 61 | 62 | Returns 63 | ------- 64 | dict 65 | A dictionary containing the strong reciprocity ratio for each hyperedge size. 66 | """ 67 | 68 | edges = hypergraph.get_edges() 69 | rec = {i: 0 for i in range(2, max_hyperedge_size + 1)} 70 | tot = {i: 0 for i in range(2, max_hyperedge_size + 1)} 71 | edge_set = {} 72 | node_reach = {} 73 | 74 | # Count total edges of each size, populate edge_set, and track reachability 75 | for edge in edges: 76 | size = len(edge[0]) + len(edge[1]) 77 | if 2 <= size <= max_hyperedge_size: 78 | tot[size] += 1 79 | edge_tuple = (tuple(edge[0]), tuple(edge[1])) 80 | edge_set[edge_tuple] = 1 81 | 82 | # Track reachable nodes for each head node 83 | for node in edge[0]: 84 | if node not in node_reach: 85 | node_reach[node] = set(edge[1]) 86 | else: 87 | node_reach[node] = node_reach[node].union(set(edge[1])) 88 | 89 | # Count reciprocated edges based on reachability 90 | for edge in edge_set: 91 | source, target = edge 92 | covered = set() 93 | 94 | # Accumulate nodes reachable from each node in the tail 95 | for node in target: 96 | if node in node_reach: 97 | covered = covered.union(node_reach[node]) 98 | 99 | # Check if all head nodes are reachable from the tail 100 | if set(source).issubset(covered): 101 | size = len(source) + len(target) 102 | rec[size] += 1 103 | 104 | # Calculate reciprocity ratios 105 | for size in range(2, max_hyperedge_size + 1): 106 | if tot[size] != 0: 107 | rec[size] = rec[size] / tot[size] 108 | else: 109 | rec[size] = 0 110 | 111 | return rec 112 | 113 | 114 | def weak_reciprocity(hypergraph: DirectedHypergraph, max_hyperedge_size: int) -> dict: 115 | """ 116 | Calculate the weak reciprocity ratio for each hyperedge size in the hypergraph. 117 | 118 | Parameters 119 | ---------- 120 | hypergraph: DirectedHypergraph 121 | The hypergraph to calculate the weak reciprocity ratio for. 122 | max_hyperedge_size: int 123 | The maximum hyperedge size to consider. 124 | 125 | Returns 126 | ------- 127 | dict 128 | A dictionary containing the weak reciprocity ratio for each hyperedge size. 129 | """ 130 | 131 | edges = hypergraph.get_edges() 132 | rec = {i: 0 for i in range(2, max_hyperedge_size + 1)} 133 | tot = {i: 0 for i in range(2, max_hyperedge_size + 1)} 134 | edge_set = {} 135 | bin_edges = {} 136 | 137 | # Count total edges of each size, populate edge_set, and track individual node connections 138 | for edge in edges: 139 | size = len(edge[0]) + len(edge[1]) 140 | if 2 <= size <= max_hyperedge_size: 141 | tot[size] += 1 142 | edge_tuple = (tuple(edge[0]), tuple(edge[1])) 143 | edge_set[edge_tuple] = 1 144 | source, target = edge_tuple 145 | 146 | # Record direct connections between source and target nodes 147 | for i in source: 148 | for j in target: 149 | bin_edges[(i, j)] = 1 150 | 151 | # Count reciprocated edges based on any pairwise reciprocity 152 | for edge in edge_set: 153 | source, target = edge 154 | is_reciprocated = False 155 | 156 | # Check if there exists a reverse connection for any source-target node pair 157 | for i in source: 158 | for j in target: 159 | if (j, i) in bin_edges: 160 | is_reciprocated = True 161 | break 162 | if is_reciprocated: 163 | break 164 | 165 | if is_reciprocated: 166 | size = len(source) + len(target) 167 | rec[size] += 1 168 | 169 | # Calculate reciprocity ratios 170 | for size in range(2, max_hyperedge_size + 1): 171 | if tot[size] != 0: 172 | rec[size] = rec[size] / tot[size] 173 | else: 174 | rec[size] = 0 175 | 176 | return rec 177 | -------------------------------------------------------------------------------- /hypergraphx/measures/edge_similarity.py: -------------------------------------------------------------------------------- 1 | def intersection(a: set, b: set): 2 | """ 3 | Computes the intersection between two sets. 4 | 5 | Parameters 6 | ---------- 7 | a: set 8 | The first set. 9 | b: set 10 | The second set. 11 | 12 | Returns 13 | ------- 14 | int 15 | The size of the intersection between the two sets. 16 | 17 | Example 18 | ------- 19 | >>> intersection({1, 2, 3}, {2, 3, 4}) 20 | 2 21 | """ 22 | return len(a.intersection(b)) 23 | 24 | 25 | def jaccard_similarity(a: set, b: set): 26 | """ 27 | Computes the Jaccard similarity between two sets. 28 | 29 | Parameters 30 | ---------- 31 | a : set 32 | The first set. 33 | b : set 34 | The second set. 35 | 36 | Returns 37 | ------- 38 | float 39 | The Jaccard similarity between the two sets. 40 | 41 | See Also 42 | -------- 43 | jaccard_distance 44 | 45 | Example 46 | ------- 47 | >>> jaccard_similarity({1, 2, 3}, {2, 3, 4}) 48 | 0.5 49 | """ 50 | a = set(a) 51 | b = set(b) 52 | return len(a.intersection(b)) / len(a.union(b)) 53 | 54 | 55 | def jaccard_distance(a: set, b: set): 56 | """ 57 | Compute the Jaccard distance between two sets. 58 | 59 | Parameters 60 | ---------- 61 | a : set 62 | The first set. 63 | b : set 64 | The second set. 65 | 66 | Returns 67 | ------- 68 | float 69 | The Jaccard distance between the two sets. The distance is 1 - the similarity. 70 | 71 | See Also 72 | -------- 73 | jaccard_similarity 74 | 75 | Example 76 | ------- 77 | >>> jaccard_distance({1, 2, 3}, {2, 3, 4}) 78 | 0.5 79 | """ 80 | return 1 - jaccard_similarity(a, b) 81 | -------------------------------------------------------------------------------- /hypergraphx/measures/eigen_centralities.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def power_method(W, max_iter=1000, tol=1e-7): 5 | # initialize x 6 | x = np.random.rand(len(W)) 7 | x = x / np.linalg.norm(x) 8 | # initialize the residual 9 | res = np.inf 10 | # initialize the number of iterations 11 | k = 0 12 | while res > tol and k < max_iter: 13 | # compute y 14 | y = np.dot(W, x) 15 | # compute the norm of y 16 | y_norm = np.linalg.norm(y) 17 | # compute the residual 18 | res = np.linalg.norm(x - y / y_norm) 19 | # update x 20 | x = y / y_norm 21 | # update the number of iterations 22 | k += 1 23 | return x 24 | 25 | 26 | def CEC_centrality(HG, tol=1e-7, max_iter=1000): 27 | """ 28 | Compute the CEC centrality for uniform hypergraphs. 29 | 30 | Parameters 31 | ---------- 32 | 33 | HG : Hypergraph 34 | The uniform hypergraph on which the CEC centrality is computed. 35 | tol : float 36 | The tolerance for calculating the dominant eigenvalue by power method. 37 | max_iter : int 38 | The maximum number of iterations for calculating the dominant eigenvalue by power method. 39 | 40 | Returns 41 | ------- 42 | cec : dict 43 | The dictionary of keys nodes of HG and values the CEC centrality of the node. 44 | 45 | 46 | References 47 | ---------- 48 | Three Hypergraph Eigenvector Centralities, 49 | Austin R. Benson, 50 | https://doi.org/10.1137/18M1203031 51 | 52 | """ 53 | 54 | # check if the hypergraph is uniform, use raise exception 55 | if not HG.is_uniform(): 56 | raise Exception("The hypergraph is not uniform.") 57 | # check if HG is connected, use raise exception 58 | if not HG.is_connected(): 59 | raise Exception("The hypergraph is not connected.") 60 | # define W, matrix N x N where i,j is the number of common edges between i and j 61 | W = np.zeros((HG.num_nodes(), HG.num_nodes())) 62 | order = len(HG.get_edges()[0]) 63 | for edge in HG.get_edges(): 64 | for i in range(order): 65 | for j in range(i + 1, order): 66 | W[edge[i], edge[j]] += 1 67 | W[edge[j], edge[i]] += 1 68 | dominant_eig = power_method(W, tol=tol, max_iter=max_iter) 69 | return {node: dominant_eig[node] for node in range(HG.num_nodes())} 70 | 71 | 72 | def ZEC_centrality(HG, max_iter=1000, tol=1e-7): 73 | """ 74 | Compute the ZEC centrality for uniform hypergraphs. 75 | 76 | Parameters 77 | ---------- 78 | 79 | HG : Hypergraph 80 | The uniform hypergraph on which the ZEC centrality is computed. 81 | max_iter : int 82 | The maximum number of iterations. 83 | tol : float 84 | The tolerance for the stopping criterion. 85 | 86 | Returns 87 | ------- 88 | ZEC : dict 89 | The dictionary of keys nodes of HG and values the ZEC centrality of the node. 90 | 91 | References 92 | ---------- 93 | Three Hypergraph Eigenvector Centralities, 94 | Austin R. Benson, 95 | https://doi.org/10.1137/18M1203031 96 | 97 | """ 98 | if not HG.is_uniform(): 99 | raise Exception("The hypergraph is not uniform.") 100 | 101 | if not HG.is_connected(): 102 | raise Exception("The hypergraph is not connected.") 103 | 104 | g = lambda v, e: np.prod(v[list(e)]) 105 | 106 | x = np.random.uniform(size=(HG.num_nodes())) 107 | x = x / np.linalg.norm(x, 1) 108 | 109 | for iter in range(max_iter): 110 | new_x = apply(HG, x, g) 111 | # multiply by the sign to try and enforce positivity 112 | new_x = np.sign(new_x[0]) * new_x / np.linalg.norm(new_x, 1) 113 | if np.linalg.norm(x - new_x) <= tol: 114 | break 115 | x = new_x.copy() 116 | else: 117 | "Iteration did not converge!" 118 | return {node: x[node] for node in range(HG.num_nodes())} 119 | 120 | 121 | def HEC_centrality(HG, max_iter=100, tol=1e-6): 122 | """ 123 | 124 | Compute the HEC centrality for uniform hypergraphs. 125 | 126 | Parameters 127 | ---------- 128 | 129 | HG : Hypergraph 130 | The uniform hypergraph on which the HEC centrality is computed. 131 | max_iter : int 132 | The maximum number of iterations. 133 | tol : float 134 | The tolerance for the stopping criterion. 135 | 136 | Returns 137 | ------- 138 | HEC : dict 139 | The dictionary of keys nodes of HG and values the HEC centrality of the node. 140 | 141 | References 142 | ---------- 143 | Three Hypergraph Eigenvector Centralities, 144 | Austin R. Benson, 145 | https://doi.org/10.1137/18M1203031 146 | 147 | """ 148 | # check if the hypergraph is uniform, use raise exception 149 | if not HG.is_uniform(): 150 | raise Exception("The hypergraph is not uniform.") 151 | 152 | if not HG.is_connected(): 153 | raise Exception("The hypergraph is not connected.") 154 | 155 | order = len(HG.get_edges()[0]) - 1 156 | f = lambda v, m: np.power(v, 1.0 / m) 157 | g = lambda v, x: np.prod(v[list(x)]) 158 | 159 | x = np.random.uniform(size=(HG.num_nodes())) 160 | x = x / np.linalg.norm(x, 1) 161 | 162 | for iter in range(max_iter): 163 | new_x = apply(HG, x, g) 164 | new_x = f(new_x, order) 165 | # Multiply by the sign to try and enforce positivity. 166 | new_x = np.sign(new_x[0]) * new_x / np.linalg.norm(new_x, 1) 167 | if np.linalg.norm(x - new_x) <= tol: 168 | break 169 | x = new_x.copy() 170 | else: 171 | print("Iteration did not converge!") 172 | return {node: x[node] for node in range(HG.num_nodes())} 173 | 174 | 175 | def apply(HG, x, g=lambda v, e: np.sum(v[list(e)])): 176 | new_x = np.zeros(HG.num_nodes()) 177 | for edge in HG.get_edges(): 178 | edge = list(edge) 179 | # Ordered permutations. 180 | for shift in range(len(edge)): 181 | new_x[edge[shift]] += g(x, edge[shift + 1 :] + edge[:shift]) 182 | return new_x 183 | -------------------------------------------------------------------------------- /hypergraphx/measures/multiplex/__init__.py: -------------------------------------------------------------------------------- 1 | from hypergraphx.measures.multiplex.overlap import edge_overlap 2 | -------------------------------------------------------------------------------- /hypergraphx/measures/multiplex/degree.py: -------------------------------------------------------------------------------- 1 | """ 2 | def degree(h: MultiplexHypergraph, node, size=None): 3 | degree = {} 4 | for layer_name in h.existing_layers: 5 | degree[layer_name] = layer.degree(node, size=size) 6 | return degree 7 | 8 | def degree_sequence(h: MultiplexHypergraph, size=None): 9 | degree = {} 10 | for node in h.get_nodes(): 11 | degree[node] = h.degree(node, size=size) 12 | return degree 13 | """ 14 | -------------------------------------------------------------------------------- /hypergraphx/measures/multiplex/overlap.py: -------------------------------------------------------------------------------- 1 | from hypergraphx import MultiplexHypergraph 2 | 3 | 4 | def edge_overlap(h: MultiplexHypergraph, edge): 5 | edge = tuple(sorted(edge)) 6 | overlap = 0 7 | 8 | for layer in h.get_existing_layers(): 9 | try: 10 | w = h.get_weight(edge, layer) 11 | overlap += w 12 | except ValueError: 13 | pass 14 | return overlap 15 | -------------------------------------------------------------------------------- /hypergraphx/measures/s_centralities.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | from hypergraphx import Hypergraph, TemporalHypergraph, DirectedHypergraph 3 | from hypergraphx.representations.projections import line_graph, bipartite_projection 4 | 5 | 6 | def s_betweenness(H: Hypergraph, s=1): 7 | """ 8 | Computes the betweenness centrality for each edge in the hypergraph. 9 | 10 | Parameters 11 | ---------- 12 | H : Hypergraph to compute the betweenness centrality for. 13 | s 14 | 15 | Returns 16 | ------- 17 | dict. The betweenness centrality for each edge in the hypergraph. The keys are the edges and the values are the betweenness centrality. 18 | """ 19 | 20 | lg, id_to_edge = line_graph(H, s=s) 21 | b = nx.betweenness_centrality(lg) 22 | return {id_to_edge[k]: v for k, v in b.items()} 23 | 24 | 25 | def s_closeness(H: Hypergraph, s=1): 26 | """ 27 | Compute the closeness centrality for each edge in the hypergraph. 28 | Parameters 29 | ---------- 30 | H : Hypergraph to compute the closeness centrality for. 31 | s 32 | 33 | Returns 34 | ------- 35 | dict. The closeness centrality for each edge in the hypergraph. The keys are the edges and the values are the closeness centrality. 36 | """ 37 | lg, id_to_edge = line_graph(H, s=s) 38 | c = nx.closeness_centrality(lg) 39 | return {id_to_edge[k]: v for k, v in c.items()} 40 | 41 | 42 | def s_betweenness_averaged(H: TemporalHypergraph, s=1): 43 | """ 44 | Computes the betweenness centrality for each edge in the temporal hypergraph. 45 | The function calculates the betweenness centrality during each time of the temporal hypergraph and then 46 | the result is the average betweenness centrality in each time. 47 | Parameters 48 | ---------- 49 | H : TemporalHypergraph 50 | The temporal hypergraph to compute the betweenness centrality for. 51 | s : int, optional 52 | Returns 53 | ------- 54 | dict. 55 | The betweenness centrality for each edge in the temporal hypergraph. 56 | The keys are the edges and the values are the betweenness centrality. 57 | """ 58 | subhypergraphs = H.subhypergraph() 59 | T = len(subhypergraphs) 60 | res = dict() 61 | for hypergraph in subhypergraphs.values(): 62 | lg, id_to_edge = line_graph(hypergraph, s=s) 63 | b = nx.betweenness_centrality(lg) 64 | for k, v in b.items(): 65 | k = id_to_edge[k] 66 | if k not in res.keys(): 67 | res[k] = 0 68 | res[k] += v 69 | return {k: v / T for k, v in res.items()} 70 | 71 | 72 | def s_closeness_averaged(H: TemporalHypergraph, s=1): 73 | """ 74 | Computes the closeness centrality for each edge in the temporal hypergraph. 75 | The function calculates the closeness centrality during each time of the temporal hypergraph and then 76 | the result is the average closeness centrality in each time. 77 | Parameters 78 | ---------- 79 | H : TemporalHypergraph 80 | The temporal hypergraph to compute the closeness centrality for. 81 | s : int, optional 82 | Returns 83 | ------- 84 | dict. 85 | The closeness centrality for each edge in the hypergraph. 86 | The keys are the edges and the values are the closeness centrality. 87 | """ 88 | subhypergraphs = H.subhypergraph() 89 | T = len(subhypergraphs) 90 | res = dict() 91 | for hypergraph in subhypergraphs.values(): 92 | lg, id_to_edge = line_graph(hypergraph, s=s) 93 | b = nx.closeness_centrality(lg) 94 | for k, v in b.items(): 95 | k = id_to_edge[k] 96 | if k not in res.keys(): 97 | res[k] = 0 98 | res[k] += v 99 | return {k: v / T for k, v in res.items()} 100 | 101 | 102 | def s_betweenness_nodes(H: Hypergraph | DirectedHypergraph): 103 | """ 104 | Computes the betweenness centrality for each node in the hypergraph. 105 | Parameters 106 | ---------- 107 | H : Hypergraph 108 | The hypergraph to compute the betweenness centrality for. 109 | Returns 110 | ------- 111 | dict. 112 | The betweenness centrality for each node in the hypergraph. 113 | The keys are the nodes and the values are the betweenness centrality. 114 | """ 115 | 116 | lg, id_to_edge = bipartite_projection(H) 117 | b = nx.betweenness_centrality(lg) 118 | return {id_to_edge[k]: v for k, v in b.items() if "E" not in k} 119 | 120 | 121 | def s_closeness_nodes(H: Hypergraph | DirectedHypergraph): 122 | """ 123 | Computes the closeness centrality for each node in the hypergraph. 124 | Parameters 125 | ---------- 126 | H : Hypergraph to compute the closeness centrality for. 127 | Returns 128 | ------- 129 | dict. 130 | The closeness centrality for each node in the hypergraph. 131 | The keys are the nodes and the values are the betweenness centrality. 132 | """ 133 | 134 | lg, id_to_edge = bipartite_projection(H) 135 | b = nx.closeness_centrality(lg) 136 | return {id_to_edge[k]: v for k, v in b.items() if "E" not in k} 137 | 138 | 139 | def s_betweenness_nodes_averaged(H: TemporalHypergraph): 140 | """ 141 | Computes the betweenness centrality for each node in the temporal hypergraph. 142 | The function calculates the betweenness centrality during each time of the temporal hypergraph and then 143 | the result is the average betweenness centrality in each time. 144 | Parameters 145 | ---------- 146 | H : TemporalHypergraph 147 | The temporal hypergraph to compute the betweenness centrality for. 148 | Returns 149 | ------- 150 | dict. 151 | The betweenness centrality for each node in the temporal hypergraph. 152 | The keys are the nodes and the values are the betweenness centrality. 153 | """ 154 | 155 | subhypergraphs = H.subhypergraph() 156 | T = len(subhypergraphs) 157 | res = dict() 158 | for hypergraph in subhypergraphs.values(): 159 | lg, id_to_edge = bipartite_projection(hypergraph) 160 | b = nx.betweenness_centrality(lg) 161 | for k, v in b.items(): 162 | if "E" not in k: 163 | k = id_to_edge[k] 164 | if k not in res.keys(): 165 | res[k] = 0 166 | res[k] += v 167 | return {k: v / T for k, v in res.items() if "E" not in k} 168 | 169 | 170 | def s_closenness_nodes_averaged(H: TemporalHypergraph): 171 | """ 172 | Computes the closeness centrality for each node in the temporal hypergraph. 173 | The function calculates the closeness centrality during each time of the temporal hypergraph and then 174 | the result is the average closeness centrality in each time. 175 | Parameters 176 | ---------- 177 | H : TemporalHypergraph 178 | The temporal hypergraph to compute the closeness centrality for. 179 | Returns 180 | ------- 181 | dict. 182 | The closeness centrality for each node in the hypergraph. 183 | The keys are the nodes and the values are the closeness centrality. 184 | """ 185 | subhypergraphs = H.subhypergraph() 186 | T = len(subhypergraphs) 187 | res = dict() 188 | for hypergraph in subhypergraphs.values(): 189 | lg, id_to_edge = bipartite_projection(hypergraph) 190 | b = nx.closeness_centrality(lg) 191 | for k, v in b.items(): 192 | if "E" not in k: 193 | k = id_to_edge[k] 194 | if k not in res.keys(): 195 | res[k] = 0 196 | res[k] += v 197 | return {k: v / T for k, v in res.items() if "E" not in k} 198 | -------------------------------------------------------------------------------- /hypergraphx/measures/sub_hypergraph_centrality.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import special 3 | 4 | from hypergraphx import Hypergraph 5 | 6 | 7 | def subhypergraph_centrality(hypergraph: Hypergraph) -> np.ndarray: 8 | """Compute the logarithm of the sub-hypergraph centrality, defined in 9 | "Complex Networks as Hypergraphs", 10 | Estrada & Rodríguez-Velázquez, 2005 11 | 12 | For every node v in the hypergraph, the sub-hypergraph centrality is given by the 13 | number of closed random walks starting and ending at v, each one discounted by the 14 | factorial of its length. This function computes the logarithm of the centrality 15 | defined in the reference paper. 16 | 17 | 18 | Parameters 19 | ---------- 20 | hypergraph: the hypergraph. 21 | 22 | Returns 23 | ------- 24 | The array of the log-sub-hypergraph centrality values for all the nodes in the 25 | hypergraph. 26 | """ 27 | adj = hypergraph.adjacency_matrix().todense() 28 | eigenvals, eigenvecs = np.linalg.eigh(adj) 29 | return special.logsumexp(eigenvals.reshape(1, -1), b=eigenvecs**2, axis=1) 30 | -------------------------------------------------------------------------------- /hypergraphx/measures/temporal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/hypergraphx/measures/temporal/__init__.py -------------------------------------------------------------------------------- /hypergraphx/motifs/__init__.py: -------------------------------------------------------------------------------- 1 | from hypergraphx.motifs.motifs import compute_motifs 2 | -------------------------------------------------------------------------------- /hypergraphx/motifs/directed_motifs.py: -------------------------------------------------------------------------------- 1 | from hypergraphx import DirectedHypergraph 2 | from hypergraphx.generation.directed_configuration_model import ( 3 | directed_configuration_model, 4 | ) 5 | from hypergraphx.motifs.utils import ( 6 | _directed_motifs_ho_full, 7 | _directed_motifs_ho_not_full, 8 | directed_diff_sum, 9 | norm_vector, 10 | ) 11 | 12 | 13 | def compute_directed_motifs( 14 | hypergraph: DirectedHypergraph, order=3, runs_config_model=10 15 | ): 16 | """ 17 | Compute the number of motifs of a given order in a directed hypergraph. 18 | 19 | Parameters 20 | ---------- 21 | hypergraph : DirectedHypergraph 22 | The directed hypergraph of interest 23 | order : int 24 | The order of the motifs to compute 25 | runs_config_model : int 26 | The number of runs of the configuration model 27 | 28 | Returns 29 | ------- 30 | dict 31 | keys: 'observed', 'config_model', 'norm_delta' 32 | 'observed' reports the number of occurrences of each motif in the observed hypergraph 33 | 'config_model' reports the number of occurrences of each motif in each sample of the configuration model 34 | 'norm_delta' reports the norm of the difference between the observed and the configuration model 35 | """ 36 | 37 | def _motifs_order_3(edges): 38 | full, visited = _directed_motifs_ho_full(edges, 3) 39 | 40 | res = [] 41 | for i in range(len(full)): 42 | res.append((full[i][0], full[i][1])) 43 | 44 | return res 45 | 46 | def _motifs_order_4(edges): 47 | full, visited = _directed_motifs_ho_full(edges, 4) 48 | not_full, visited = _directed_motifs_ho_not_full(edges, 4, visited) 49 | 50 | mappa = {} 51 | for i in range(len(full)): 52 | mappa[full[i][0]] = full[i][1] 53 | for i in range(len(not_full)): 54 | mappa[not_full[i][0]] = not_full[i][1] 55 | 56 | res = [] 57 | for key in mappa.keys(): 58 | res.append((key, mappa[key])) 59 | return res 60 | 61 | edges = hypergraph.get_edges(size=order, up_to=True) 62 | output = {} 63 | 64 | print("Computing observed motifs of order {}...".format(order)) 65 | 66 | if order == 3: 67 | output["observed"] = _motifs_order_3(edges) 68 | 69 | elif order == 4: 70 | output["observed"] = _motifs_order_4(edges) 71 | else: 72 | raise ValueError("Exact computation of motifs of order > 5 is not available.") 73 | 74 | if runs_config_model == 0: 75 | return output 76 | 77 | ROUNDS = runs_config_model 78 | 79 | results = [] 80 | 81 | for i in range(ROUNDS): 82 | print( 83 | "Computing config model motifs of order {}. Step: {}".format(order, i + 1) 84 | ) 85 | e1 = directed_configuration_model(hypergraph).get_edges() 86 | 87 | if order == 3: 88 | m1 = _motifs_order_3(e1) 89 | elif order == 4: 90 | m1 = _motifs_order_4(e1) 91 | else: 92 | raise ValueError( 93 | "Exact computation of motifs of order > 5 is not available." 94 | ) 95 | 96 | results.append(m1) 97 | 98 | output["config_model"] = results 99 | 100 | delta = list(directed_diff_sum(output["observed"], output["config_model"])) 101 | norm_delta = list(norm_vector(delta)) 102 | output["norm_delta"] = [] 103 | 104 | for i in range(len(delta)): 105 | output["norm_delta"].append((output["observed"][i][0], norm_delta[i])) 106 | 107 | return output 108 | -------------------------------------------------------------------------------- /hypergraphx/motifs/motifs.py: -------------------------------------------------------------------------------- 1 | from hypergraphx import Hypergraph 2 | from hypergraphx.generation.configuration_model import configuration_model 3 | from hypergraphx.motifs.utils import ( 4 | _motifs_ho_full, 5 | _motifs_ho_not_full, 6 | _motifs_standard, 7 | diff_sum, 8 | norm_vector, 9 | ) 10 | 11 | 12 | def compute_motifs(hypergraph: Hypergraph, order=3, runs_config_model=10): 13 | """ 14 | Compute the number of motifs of a given order in a hypergraph. 15 | 16 | Parameters 17 | ---------- 18 | hypergraph : Hypergraph 19 | The hypergraph of interest 20 | order : int 21 | The order of the motifs to compute 22 | runs_config_model : int 23 | The number of runs of the configuration model 24 | 25 | Returns 26 | ------- 27 | dict 28 | keys: 'observed', 'config_model', 'norm_delta' 29 | 'observed' reports the number of occurrences of each motif in the observed hypergraph 30 | 'config_model' reports the number of occurrences of each motif in each sample of the configuration model 31 | 'norm_delta' reports the norm of the difference between the observed and the configuration model 32 | 33 | """ 34 | 35 | def _motifs_order_3(edges): 36 | full, visited = _motifs_ho_full(edges, 3) 37 | standard = _motifs_standard(edges, 3, visited) 38 | 39 | res = [] 40 | for i in range(len(full)): 41 | res.append((full[i][0], max(full[i][1], standard[i][1]))) 42 | 43 | return res 44 | 45 | def _motifs_order_4(edges): 46 | full, visited = _motifs_ho_full(edges, 4) 47 | not_full, visited = _motifs_ho_not_full(edges, 4, visited) 48 | standard = _motifs_standard(edges, 4, visited) 49 | 50 | res = [] 51 | for i in range(len(full)): 52 | res.append((full[i][0], max([full[i][1], not_full[i][1], standard[i][1]]))) 53 | 54 | return res 55 | 56 | edges = hypergraph.get_edges(size=order, up_to=True) 57 | output = {} 58 | 59 | print("Computing observed motifs of order {}...".format(order)) 60 | 61 | if order == 3: 62 | output["observed"] = _motifs_order_3(edges) 63 | elif order == 4: 64 | output["observed"] = _motifs_order_4(edges) 65 | else: 66 | raise ValueError("Exact computation of motifs of order > 4 is not available.") 67 | 68 | if runs_config_model == 0: 69 | return output 70 | 71 | STEPS = hypergraph.num_edges(size=order, up_to=True) * 10 72 | ROUNDS = runs_config_model 73 | 74 | results = [] 75 | 76 | for i in range(ROUNDS): 77 | print( 78 | "Computing config model motifs of order {}. Step: {}".format(order, i + 1) 79 | ) 80 | e1 = configuration_model(hypergraph, label="stub", n_steps=STEPS) 81 | if order == 3: 82 | m1 = _motifs_order_3(e1.get_edges()) 83 | elif order == 4: 84 | m1 = _motifs_order_4(e1.get_edges()) 85 | else: 86 | raise ValueError( 87 | "Exact computation of motifs of order > 4 is not available." 88 | ) 89 | results.append(m1) 90 | 91 | output["config_model"] = results 92 | 93 | delta = list(diff_sum(output["observed"], output["config_model"])) 94 | norm_delta = list(norm_vector(delta)) 95 | output["norm_delta"] = [] 96 | 97 | for i in range(len(delta)): 98 | output["norm_delta"].append((output["observed"][i][0], norm_delta[i])) 99 | 100 | return output 101 | -------------------------------------------------------------------------------- /hypergraphx/readwrite/__init__.py: -------------------------------------------------------------------------------- 1 | from .load import load_hypergraph 2 | from .save import save_hypergraph 3 | from .hif import read_hif 4 | from .hif import write_hif 5 | -------------------------------------------------------------------------------- /hypergraphx/readwrite/hashing.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | 4 | 5 | def hash_hypergraph(hypergraph): 6 | """ 7 | Generates a SHA-256 hash of a hypergraph based on its exposed attributes. 8 | 9 | Parameters 10 | ---------- 11 | hypergraph : object 12 | The hypergraph instance to hash. Should implement `expose_attributes_for_hashing`. 13 | 14 | Returns 15 | ------- 16 | str 17 | The SHA-256 hash hex digest of the hypergraph. 18 | """ 19 | 20 | def serialize(obj): 21 | """ 22 | Recursively serialize the hypergraph attributes into a JSON-compatible structure, 23 | ensuring that dictionaries are sorted by key and lists are sorted if applicable. 24 | """ 25 | if isinstance(obj, dict): 26 | return {k: serialize(obj[k]) for k in sorted(obj)} 27 | elif isinstance(obj, list): 28 | return [serialize(item) for item in obj] 29 | else: 30 | return obj 31 | 32 | exposed_attrs = hypergraph.expose_attributes_for_hashing() 33 | 34 | serialized_hg = serialize(exposed_attrs) 35 | 36 | json_str = json.dumps(serialized_hg, sort_keys=True) 37 | 38 | hash_digest = hashlib.sha256(json_str.encode("utf-8")).hexdigest() 39 | 40 | return hash_digest 41 | -------------------------------------------------------------------------------- /hypergraphx/readwrite/hif.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from hypergraphx import Hypergraph 4 | 5 | 6 | def read_hif(path: str) -> Hypergraph: 7 | """ 8 | Load a hypergraph from a HIF file. 9 | 10 | Parameters 11 | ---------- 12 | path : str 13 | The path to the HIF file 14 | 15 | Returns 16 | ------- 17 | Hypergraph 18 | The loaded hypergraph 19 | """ 20 | edge_name_to_uid = {} 21 | node_name_to_uid = {} 22 | eid = 0 23 | nid = 0 24 | 25 | with open(path) as file: 26 | data = json.loads(file.read()) 27 | 28 | if "type" not in data: 29 | print("No hypergraph type - assume undirected") 30 | data["type"] = "undirected" 31 | 32 | if data["type"] == "undirected" or data["type"] == "asc": 33 | H = Hypergraph() 34 | elif data["type"] == "directed": 35 | H = Hypergraph(directed=True) 36 | else: 37 | raise ValueError(f"Unknown hypergraph type: {data['type']}") 38 | 39 | if "metadata" in data: 40 | H.set_hypergraph_metadata(data["metadata"]) 41 | 42 | tmp_edges = {} 43 | for incidence in data["incidences"]: 44 | if incidence["edge"] not in edge_name_to_uid: 45 | edge_name_to_uid[incidence["edge"]] = eid 46 | eid += 1 47 | edge = edge_name_to_uid[incidence["edge"]] 48 | 49 | if incidence["node"] not in node_name_to_uid: 50 | node_name_to_uid[incidence["node"]] = nid 51 | nid += 1 52 | node = node_name_to_uid[incidence["node"]] 53 | 54 | if edge not in tmp_edges: 55 | tmp_edges[edge] = [] 56 | tmp_edges[edge].append(node) 57 | 58 | for record in data["nodes"]: 59 | node_name = record["node"] 60 | if node_name not in node_name_to_uid: 61 | node_name_to_uid[node_name] = nid 62 | nid += 1 63 | node = node_name_to_uid[node_name] 64 | H.add_node(node) 65 | H.set_node_metadata(node, record) 66 | 67 | added = {} 68 | 69 | for record in data["edges"]: 70 | edge_name = record["edge"] 71 | if edge_name not in edge_name_to_uid: 72 | edge_name_to_uid[edge_name] = eid 73 | eid += 1 74 | edge = edge_name_to_uid[edge_name] 75 | if edge in tmp_edges: 76 | H.add_edge(tuple(sorted(tmp_edges[edge]))) 77 | added[tuple(sorted(tmp_edges[edge]))] = True 78 | H.set_edge_metadata(tuple(sorted(tmp_edges[edge])), record) 79 | else: 80 | H.add_empty_edge(edge_name, record) 81 | 82 | for incidence in data["incidences"]: 83 | edge = edge_name_to_uid[incidence["edge"]] 84 | node = node_name_to_uid[incidence["node"]] 85 | if tuple(sorted(tmp_edges[edge])) not in added: 86 | H.add_edge(tuple(sorted(tmp_edges[edge]))) 87 | added[tuple(sorted(tmp_edges[edge]))] = True 88 | H.set_incidence_metadata(tuple(sorted(tmp_edges[edge])), node, incidence) 89 | 90 | return H 91 | 92 | 93 | def write_hif(H: Hypergraph, path: str): 94 | """ 95 | Save a hypergraph to a HIF file. 96 | 97 | Parameters 98 | ---------- 99 | H: Hypergraph 100 | The hypergraph to save. 101 | path: str 102 | The path to save the hypergraph to. 103 | """ 104 | 105 | data = { 106 | "type": "undirected", 107 | "metadata": H.get_hypergraph_metadata(), 108 | "edges": H.get_all_edges_metadata(), 109 | "nodes": H.get_all_nodes_metadata(), 110 | "incidences": H.get_all_incidences_metadata(), 111 | } 112 | 113 | with open(path, "w") as file: 114 | file.write(json.dumps(data)) 115 | -------------------------------------------------------------------------------- /hypergraphx/readwrite/save.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pickle 3 | 4 | from hypergraphx import ( 5 | Hypergraph, 6 | TemporalHypergraph, 7 | DirectedHypergraph, 8 | MultiplexHypergraph, 9 | ) 10 | 11 | import pickle 12 | 13 | 14 | def _save_pickle(obj, file_name: str): 15 | """ 16 | Save an object as a pickle file. 17 | 18 | Parameters 19 | ---------- 20 | obj : object 21 | The object to save. Must implement `expose_data_structures`. 22 | file_name : str 23 | The name of the file to save the object to. 24 | 25 | Returns 26 | ------- 27 | None 28 | The object is saved to a file. 29 | """ 30 | try: 31 | if not hasattr(obj, "expose_data_structures"): 32 | raise AttributeError( 33 | "Object must implement 'expose_data_structures' method." 34 | ) 35 | 36 | data = obj.expose_data_structures() 37 | 38 | with open(file_name, "wb") as f: 39 | pickle.dump(data, f) 40 | except Exception as e: 41 | raise RuntimeError(f"Failed to save object to {file_name}: {e}") 42 | 43 | 44 | import json 45 | 46 | 47 | def save_hypergraph(hypergraph, file_name: str, binary=False): 48 | """ 49 | Save a hypergraph to a file in JSON format efficiently by writing in chunks. 50 | 51 | Parameters 52 | ---------- 53 | hypergraph: Hypergraph 54 | The hypergraph to save. 55 | file_name: str 56 | The name of the file. 57 | binary: bool 58 | Whether to save the hypergraph as a binary file (hgx) or a text file (json). 59 | 60 | Returns 61 | ------- 62 | None 63 | The hypergraph is saved to a file. 64 | 65 | Raises 66 | ------ 67 | ValueError 68 | If the hypergraph type is not valid. 69 | """ 70 | if binary: 71 | _save_pickle(hypergraph, file_name) 72 | return 73 | 74 | with open(file_name, "w") as outfile: 75 | outfile.write("[\n") # Start JSON array 76 | first = True 77 | 78 | def write_item(item): 79 | """Helper function to write a JSON object, ensuring proper comma placement.""" 80 | nonlocal first 81 | if not first: 82 | outfile.write(",\n") 83 | json.dump( 84 | item, outfile, separators=(",", ":") 85 | ) # No pretty-print for efficiency 86 | first = False 87 | 88 | # Get hypergraph metadata 89 | hypergraph_type = str(type(hypergraph)).split(".")[-1][:-2] 90 | weighted = hypergraph.is_weighted() 91 | 92 | write_item( 93 | { 94 | "hypergraph_type": hypergraph_type, 95 | "hypergraph_metadata": hypergraph.get_hypergraph_metadata(), 96 | } 97 | ) 98 | 99 | # Write nodes 100 | for node, metadata in hypergraph.get_nodes(metadata=True).items(): 101 | write_item({"type": "node", "idx": node, "metadata": metadata}) 102 | 103 | # Write edges 104 | if hypergraph_type in ["Hypergraph", "DirectedHypergraph"]: 105 | for edge, metadata in hypergraph.get_edges(metadata=True).items(): 106 | if weighted: 107 | metadata["weight"] = hypergraph.get_weight(edge) 108 | write_item({"type": "edge", "interaction": edge, "metadata": metadata}) 109 | 110 | elif hypergraph_type == "MultiplexHypergraph": 111 | for edge, metadata in hypergraph.get_edges(metadata=True).items(): 112 | edge, layer = edge 113 | metadata["layer"] = layer 114 | if weighted: 115 | metadata["weight"] = hypergraph.get_weight(edge, layer) 116 | write_item({"type": "edge", "interaction": edge, "metadata": metadata}) 117 | 118 | elif hypergraph_type == "TemporalHypergraph": 119 | for edge, metadata in hypergraph.get_edges(metadata=True).items(): 120 | time, edge = edge 121 | if weighted: 122 | metadata["weight"] = hypergraph.get_weight(edge, time) 123 | metadata["time"] = time 124 | write_item({"type": "edge", "interaction": edge, "metadata": metadata}) 125 | 126 | else: 127 | raise ValueError("Invalid hypergraph type.") 128 | 129 | outfile.write("\n]") # Close JSON array 130 | -------------------------------------------------------------------------------- /hypergraphx/representations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/hypergraphx/representations/__init__.py -------------------------------------------------------------------------------- /hypergraphx/representations/simplicial_complex.py: -------------------------------------------------------------------------------- 1 | from itertools import chain, combinations 2 | 3 | from hypergraphx import Hypergraph 4 | 5 | 6 | def get_all_subsets(s): 7 | """ 8 | Returns all subsets of a set. 9 | Parameters 10 | ---------- 11 | s : set. The set to get all subsets of. 12 | 13 | Returns 14 | ------- 15 | subsets : list. All subsets of the set. 16 | """ 17 | return chain(*map(lambda x: combinations(s, x), range(0, len(s) + 1))) 18 | 19 | 20 | def simplicial_complex(h: Hypergraph): 21 | """ 22 | Returns a simplicial complex representation of the hypergraph. 23 | 24 | Parameters 25 | ---------- 26 | h : Hypergraph. The hypergraph to be projected. 27 | 28 | Returns 29 | ------- 30 | S : Hypergraph. The simplicial complex representation of the hypergraph. 31 | """ 32 | s_edges = set() 33 | 34 | for edge in h.get_edges(): 35 | subsets = get_all_subsets(edge) 36 | for subset in subsets: 37 | subset = tuple(sorted(subset)) 38 | s_edges.add(subset) 39 | S = Hypergraph(s_edges) 40 | return S 41 | -------------------------------------------------------------------------------- /hypergraphx/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from hypergraphx.utils.community import calculate_permutation_matrix, normalize_array 2 | -------------------------------------------------------------------------------- /hypergraphx/utils/cc.py: -------------------------------------------------------------------------------- 1 | from hypergraphx import Hypergraph, DirectedHypergraph, TemporalHypergraph 2 | from hypergraphx.utils.visits import _bfs 3 | 4 | 5 | def connected_components(hg: Hypergraph, order=None, size=None): 6 | """ 7 | Return the connected components of the hypergraph. 8 | Parameters 9 | ---------- 10 | hg : Hypergraph. The hypergraph to check. 11 | order : int. The order of the hyperedges to consider. If None, all hyperedges are considered. 12 | size : int. The size of the hyperedges to consider. If None, all hyperedges are considered. 13 | 14 | Returns 15 | ------- 16 | list. The connected components of the hypergraph. 17 | """ 18 | if order is not None and size is not None: 19 | raise ValueError("Order and size cannot be both specified.") 20 | visited = [] 21 | components = [] 22 | for node in hg.get_nodes(): 23 | if node not in visited: 24 | component = _bfs(hg, node, size=order, order=size) 25 | visited += component 26 | components.append(component) 27 | return components 28 | 29 | 30 | def node_connected_component(hg: Hypergraph, node, order=None, size=None): 31 | """ 32 | Return the connected component of the hypergraph containing the given node. 33 | Parameters 34 | ---------- 35 | hg : Hypergraph. The hypergraph to check. 36 | node : Node. The node to check. 37 | order : int. The order of the hyperedges to consider. If None, all hyperedges are considered. 38 | size : int. The size of the hyperedges to consider. If None, all hyperedges are considered. 39 | 40 | Returns 41 | ------- 42 | list. The nodes in the connected component of the input node. 43 | """ 44 | if order is not None and size is not None: 45 | raise ValueError("Order and size cannot be both specified.") 46 | return _bfs(hg, node, size=None, order=None) 47 | 48 | 49 | def num_connected_components(hg: Hypergraph, order=None, size=None): 50 | """ 51 | Return the number of connected components of the hypergraph. 52 | Parameters 53 | ---------- 54 | hg : Hypergraph. The hypergraph to check. 55 | order : int. The order of the hyperedges to consider. If None, all hyperedges are considered. 56 | size : int. The size of the hyperedges to consider. If None, all hyperedges are considered. 57 | 58 | Returns 59 | ------- 60 | int. The number of connected components. 61 | """ 62 | if order is not None and size is not None: 63 | raise ValueError("Order and size cannot be both specified.") 64 | return len(hg.connected_components(size=None, order=None)) 65 | 66 | 67 | def largest_component(hg: Hypergraph, order=None, size=None): 68 | """ 69 | Return the largest connected component of the hypergraph. 70 | Parameters 71 | ---------- 72 | hg : Hypergraph. The hypergraph to check. 73 | order : int. The order of the hyperedges to consider. If None, all hyperedges are considered. 74 | size : int. The size of the hyperedges to consider. If None, all hyperedges are considered. 75 | 76 | Returns 77 | ------- 78 | list. The nodes in the largest connected component. 79 | """ 80 | if order is not None and size is not None: 81 | raise ValueError("Order and size cannot be both specified.") 82 | components = hg.connected_components(size=None, order=None) 83 | return max(components, key=len) 84 | 85 | 86 | def largest_component_size(hg: Hypergraph, order=None, size=None): 87 | """ 88 | Return the size of the largest connected component of the hypergraph. 89 | Parameters 90 | ---------- 91 | hg : Hypergraph. The hypergraph to check. 92 | order : int. The order of the hyperedges to consider. If None, all hyperedges are considered. 93 | size : int. The size of the hyperedges to consider. If None, all hyperedges are considered. 94 | 95 | Returns 96 | ------- 97 | int. The size of the largest connected component. 98 | """ 99 | if order is not None and size is not None: 100 | raise ValueError("Order and size cannot be both specified.") 101 | return len(hg.largest_component(size=None, order=None)) 102 | 103 | 104 | def isolated_nodes( 105 | hg: Hypergraph | DirectedHypergraph | TemporalHypergraph, order=None, size=None 106 | ): 107 | """ 108 | Return the isolated nodes of the hypergraph. 109 | Parameters 110 | ---------- 111 | hg: Hypergraph. The hypergraph to check. 112 | order: int. The order of the hyperedges to consider. If None, all hyperedges are considered. 113 | size: int. The size of the hyperedges to consider. If None, all hyperedges are considered. 114 | 115 | Returns 116 | ------- 117 | list. The isolated nodes. 118 | """ 119 | if order is not None and size is not None: 120 | raise ValueError("Order and size cannot be both specified.") 121 | return [ 122 | node 123 | for node in hg.get_nodes() 124 | if len(hg.get_neighbors(node, order=order, size=size)) == 0 125 | ] 126 | 127 | 128 | def is_isolated( 129 | hg: Hypergraph | DirectedHypergraph | TemporalHypergraph, 130 | node, 131 | order=None, 132 | size=None, 133 | ): 134 | """ 135 | Return True if the given node is isolated. 136 | Parameters 137 | ---------- 138 | hg : Hypergraph. The hypergraph to check. 139 | node : Node. The node to check. 140 | order : int. The order of the hyperedges to consider. If None, all hyperedges are considered. 141 | size : int. The size of the hyperedges to consider. If None, all hyperedges are considered. 142 | 143 | Returns 144 | ------- 145 | bool. True if the node is isolated, False otherwise. 146 | """ 147 | if order is not None and size is not None: 148 | raise ValueError("Order and size cannot be both specified.") 149 | return len(list(hg.get_neighbors(node, order=order, size=size))) == 0 150 | 151 | 152 | def is_connected(hg: Hypergraph, order=None, size=None): 153 | """ 154 | Return True if the hypergraph is connected. 155 | Parameters 156 | ---------- 157 | hg : Hypergraph. The hypergraph to check. 158 | order : int. The order of the hyperedges to consider. If None, all hyperedges are considered. 159 | size : int. The size of the hyperedges to consider. If None, all hyperedges are considered. 160 | 161 | Returns 162 | ------- 163 | bool. True if the hypergraph is connected, False otherwise. 164 | """ 165 | if order is not None and size is not None: 166 | raise ValueError("Order and size cannot be both specified.") 167 | return len(hg.connected_components(order=order, size=size)) == 1 168 | -------------------------------------------------------------------------------- /hypergraphx/utils/community.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def normalize_array(u: np.array, axis: int) -> np.array: 5 | """Return the normalized array u over a given axis. 6 | E.g., if u is a matrix NxK and axis=1, then this function returns the matrix u normalized by row. 7 | 8 | Parameters 9 | ---------- 10 | u: numpy array. 11 | axis: axis along which the normalization is performed. 12 | """ 13 | den1 = u.sum(axis=axis, keepdims=True) 14 | nzz = den1 == 0.0 15 | den1[nzz] = 1.0 16 | return u / den1 17 | 18 | 19 | def calculate_permutation_matrix(u_ref: np.array, u_pred: np.array) -> np.array: 20 | """Calculate the permutation matrix to overcome the column switching between two matrices. 21 | 22 | Parameters 23 | ---------- 24 | u_ref: reference matrix. 25 | u_pred: matrix to switch. 26 | 27 | Returns 28 | ------- 29 | P: permutation matrix of the same dimension as u_ref. 30 | """ 31 | # Check the matrices have the same shape. 32 | if u_ref.shape != u_pred.shape: 33 | msg = f"u_ref and u_pred must have the same shape!" 34 | raise ValueError(msg) 35 | 36 | N, RANK = u_ref.shape 37 | M = np.dot(np.transpose(u_pred), u_ref) / float(N) # dim = RANK x RANK 38 | rows = np.zeros(RANK) 39 | columns = np.zeros(RANK) 40 | P = np.zeros((RANK, RANK)) # permutation matrix 41 | for t in range(RANK): 42 | # Find the max element in the remaining sub-matrix, 43 | # the one with rows and columns removed from previous iterations 44 | max_entry = 0.0 45 | c_index = 0 46 | r_index = 0 47 | for i in range(RANK): 48 | if columns[i] == 0: 49 | for j in range(RANK): 50 | if rows[j] == 0: 51 | if M[j, i] > max_entry: 52 | max_entry = M[j, i] 53 | c_index = i 54 | r_index = j 55 | if max_entry > 0: 56 | P[r_index, c_index] = 1 57 | columns[c_index] = 1 58 | rows[r_index] = 1 59 | if (np.sum(P, axis=1) == 0).any(): 60 | row = np.where(np.sum(P, axis=1) == 0)[0] 61 | if (np.sum(P, axis=0) == 0).any(): 62 | col = np.where(np.sum(P, axis=0) == 0)[0] 63 | P[row, col] = 1 64 | return P 65 | -------------------------------------------------------------------------------- /hypergraphx/utils/labeling.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | from sklearn.preprocessing import LabelEncoder 4 | 5 | 6 | def relabel_edge(mapping: LabelEncoder, edge: Tuple): 7 | """ 8 | Relabel an edge using a mapping. 9 | 10 | Parameters 11 | ---------- 12 | mapping: LabelEncoder 13 | The mapping to use 14 | edge: Tuple 15 | The edge to relabel 16 | 17 | Returns 18 | ------- 19 | Tuple 20 | The relabeled edge 21 | """ 22 | return tuple(mapping.transform(edge)) 23 | 24 | 25 | def relabel_edges(mapping: LabelEncoder, edges: List[Tuple]): 26 | """ 27 | Relabel a list of edges using a mapping. 28 | 29 | Parameters 30 | ---------- 31 | mapping: LabelEncoder 32 | The mapping to use 33 | edges: List[Tuple] 34 | The edges to relabel 35 | 36 | Returns 37 | ------- 38 | List[Tuple] 39 | The relabeled edges 40 | """ 41 | return [relabel_edge(mapping, edge) for edge in edges] 42 | 43 | 44 | def inverse_relabel_edge(mapping: LabelEncoder, edge: Tuple): 45 | """ 46 | Revert edge relabeling using a mapping. 47 | 48 | Parameters 49 | ---------- 50 | mapping: LabelEncoder 51 | The mapping to use 52 | edge: Tuple 53 | The edge to relabel 54 | 55 | Returns 56 | ------- 57 | Tuple 58 | The relabeled edge 59 | """ 60 | return tuple(mapping.inverse_transform(edge)) 61 | 62 | 63 | def inverse_relabel_edges(mapping: LabelEncoder, edges: List[Tuple]): 64 | """ 65 | Revert edges relabeling using a mapping. 66 | 67 | Parameters 68 | ---------- 69 | mapping: LabelEncoder 70 | The mapping to use 71 | edges: List[Tuple] 72 | The edges to relabel 73 | 74 | Returns 75 | ------- 76 | List[Tuple] 77 | The relabeled edges 78 | """ 79 | return [inverse_relabel_edge(mapping, edge) for edge in edges] 80 | 81 | 82 | def map_node(mapping: LabelEncoder, node): 83 | """ 84 | Map a node using a mapping. 85 | 86 | Parameters 87 | ---------- 88 | mapping: LabelEncoder 89 | The mapping to use 90 | node: Any 91 | The node to map 92 | 93 | Returns 94 | ------- 95 | Any 96 | The mapped node 97 | """ 98 | return mapping.transform([node])[0] 99 | 100 | 101 | def map_nodes(mapping: LabelEncoder, nodes: List): 102 | """ 103 | Map a list of nodes using a mapping. 104 | 105 | Parameters 106 | ---------- 107 | mapping: LabelEncoder 108 | The mapping to use 109 | nodes: List 110 | The nodes to map 111 | 112 | Returns 113 | ------- 114 | List 115 | The mapped nodes 116 | """ 117 | return mapping.transform(nodes) 118 | 119 | 120 | def inverse_map_nodes(mapping: LabelEncoder, nodes: List): 121 | """ 122 | Revert node mapping using a mapping. 123 | 124 | Parameters 125 | ---------- 126 | mapping: LabelEncoder 127 | The mapping to use 128 | nodes: List 129 | The nodes to map 130 | 131 | Returns 132 | ------- 133 | List 134 | The mapped nodes 135 | """ 136 | return mapping.inverse_transform(nodes) 137 | 138 | 139 | def get_inverse_mapping(mapping: LabelEncoder): 140 | """ 141 | Get the inverse mapping of a LabelEncoder. 142 | 143 | Parameters 144 | ---------- 145 | mapping: LabelEncoder 146 | The mapping to invert 147 | 148 | Returns 149 | ------- 150 | dict 151 | The inverse mapping 152 | """ 153 | return dict(zip(mapping.transform(mapping.classes_), mapping.classes_)) 154 | -------------------------------------------------------------------------------- /hypergraphx/utils/visits.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | from hypergraphx import Hypergraph 4 | 5 | 6 | def _bfs(hg: Hypergraph, start, max_depth=None, order=None, size=None): 7 | """ 8 | Breadth-first search of the hypergraph starting from the given node. 9 | Parameters 10 | ---------- 11 | hg : Hypergraph. The hypergraph to search. 12 | start : Node. The node to start the search from. 13 | max_depth : int. The maximum depth to search. If None, the search is not limited. 14 | order : int. The order of the hyperedges to consider. If None, all hyperedges are considered. 15 | size : int. The size of the hyperedges to consider. If None, all hyperedges are considered. 16 | 17 | Returns 18 | ------- 19 | set. The nodes visited during the search. 20 | 21 | Raises 22 | ------ 23 | ValueError. If the start node is not in the hypergraph. 24 | """ 25 | if not hg.check_node(start): 26 | raise ValueError(f"Node {start} not in hypergraph.") 27 | visited = set() 28 | queue = deque([(start, 0)]) 29 | 30 | add_visited = visited.add 31 | get_neighbors = hg.get_neighbors 32 | 33 | while queue: 34 | node, depth = queue.popleft() 35 | if node not in visited: 36 | add_visited(node) 37 | if max_depth is None or depth < max_depth: 38 | neighbors = get_neighbors(node, order=order, size=size) 39 | queue.extend((n, depth + 1) for n in neighbors if n not in visited) 40 | 41 | return visited 42 | 43 | 44 | def _dfs(hg: Hypergraph, start, max_depth=None, order=None, size=None): 45 | """ 46 | Depth-first search of the hypergraph starting from the given node. 47 | Parameters 48 | ---------- 49 | hg : Hypergraph. The hypergraph to search. 50 | start : Node. The node to start the search from. 51 | max_depth : int. The maximum depth to search. If None, the search is not limited. 52 | order : int. The order of the hyperedges to consider. If None, all hyperedges are considered. 53 | size : int. The size of the hyperedges to consider. If None, all hyperedges are considered. 54 | 55 | Returns 56 | ------- 57 | set. The nodes visited during the search. 58 | 59 | Raises 60 | ------ 61 | ValueError. If the start node is not in the hypergraph. 62 | """ 63 | if not hg.check_node(start): 64 | raise ValueError(f"Node {start} not in hypergraph.") 65 | visited = set() 66 | stack = [(start, 0)] 67 | 68 | add_visited = visited.add 69 | get_neighbors = hg.get_neighbors 70 | 71 | while stack: 72 | node, depth = stack.pop() 73 | if node not in visited: 74 | add_visited(node) 75 | if max_depth is None or depth < max_depth: 76 | new_depth = depth + 1 77 | neighbors = get_neighbors(node, order=order, size=size) 78 | stack.extend((n, new_depth) for n in neighbors if n not in visited) 79 | 80 | return visited 81 | -------------------------------------------------------------------------------- /hypergraphx/viz/__init__.py: -------------------------------------------------------------------------------- 1 | from hypergraphx.viz.draw_communities import draw_communities 2 | from hypergraphx.viz.draw_hypergraph import draw_hypergraph 3 | from hypergraphx.viz.draw_projections import draw_bipartite, draw_clique 4 | from hypergraphx.viz.plot_motifs import plot_motifs 5 | -------------------------------------------------------------------------------- /hypergraphx/viz/draw_communities.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple, Union 2 | 3 | import matplotlib.pyplot as plt 4 | import networkx as nx 5 | import numpy as np 6 | 7 | from hypergraphx import Hypergraph 8 | from hypergraphx.representations.projections import clique_projection 9 | 10 | 11 | def draw_communities( 12 | hypergraph: Hypergraph, 13 | u: np.array, 14 | col: dict, 15 | figsize: tuple = (7, 7), 16 | ax: Optional[plt.Axes] = None, 17 | pos: Optional[dict] = None, 18 | edge_color: str = "lightgrey", 19 | edge_width: float = 0.3, 20 | threshold_group: float = 0.1, 21 | wedge_color: str = "lightgrey", 22 | wedge_width: float = 1.5, 23 | with_node_labels: bool = True, 24 | label_size: float = 10, 25 | label_col: str = "black", 26 | node_size: Union[None, float, int, dict] = None, 27 | c_node_size: float = 0.004, 28 | title: Optional[str] = None, 29 | title_size: float = 15, 30 | seed: int = 20, 31 | scale: int = 2, 32 | iterations: int = 100, 33 | opt_dist: float = 0.2, 34 | ): 35 | """Visualize the node memberships of a hypergraph. Nodes are colored according to their memberships, 36 | which can be either hard- or soft-membership, and the node size is proportional to the degree in the hypergraph. 37 | Edges are the pairwise interactions of the hypergraph clique projection. 38 | 39 | Parameters 40 | ---------- 41 | hypergraph: the hypergraph to visualize. 42 | u: membership matrix of dimension NxK, where N is the number of nodes and K is the number of communities. 43 | col: dictionary of colors for nodes, where key represent the group id and values are colors. 44 | figsize: size of the figure to use when ax=None. 45 | ax: axes to use for the visualization. 46 | pos: dictionary of positions for nodes, with node as keys and values as a coordinate list or tuple. 47 | edge_color: color of the edges. 48 | edge_width: width of the edges. 49 | threshold_group: minimum membership value to keep in the plot. 50 | wedge_color: color of the wedge borders. 51 | wedge_width: width of the wedge borders. 52 | with_node_labels: flag to print the node labels. 53 | label_size: fontsize of the node labels. 54 | label_col: color of the node labels. 55 | node_size: sizes of nodes. 56 | c_node_size: constant to regularize the node size proportional to the node degree (when node_size=None). 57 | title: plot title. 58 | title_size: fontsize of the title. 59 | seed: random seed for fixing the position with the spring layout. 60 | scale: scale factor for positions. 61 | iterations: maximum number of iterations taken for fixing the position with the spring layout. 62 | opt_dist: optimal distance between nodes. 63 | """ 64 | # Initialize figure. 65 | if ax is None: 66 | plt.figure(figsize=figsize) 67 | plt.subplot(1, 1, 1) 68 | ax = plt.gca() 69 | 70 | # Get the clique projection of the hypergraph. 71 | G = clique_projection(hypergraph, keep_isolated=True) 72 | 73 | # Extract node positions. 74 | if pos is None: 75 | pos = nx.spring_layout( 76 | G, k=opt_dist, iterations=iterations, seed=seed, scale=scale 77 | ) 78 | 79 | # Get node degrees and node sizes. 80 | degree = hypergraph.degree_sequence() 81 | if node_size is None: 82 | # Proportional to the node degree. 83 | node_size = {n: degree[n] * c_node_size for n in G.nodes()} 84 | elif type(node_size) != np.array: 85 | node_size = {n: node_size for n in G.nodes()} 86 | 87 | # Get node mappings. 88 | _, mappingID2Name = hypergraph.binary_incidence_matrix(return_mapping=True) 89 | mappingName2ID = {n: i for i, n in mappingID2Name.items()} 90 | 91 | # Plot edges. 92 | nx.draw_networkx_edges(G, pos, width=edge_width, edge_color=edge_color, ax=ax) 93 | 94 | # Plot nodes. 95 | for n in G.nodes(): 96 | wedge_sizes, wedge_colors = extract_pie_properties( 97 | mappingName2ID[n], u, col, threshold=threshold_group 98 | ) 99 | if len(wedge_sizes) > 0: 100 | plt.pie( 101 | wedge_sizes, 102 | center=pos[n], 103 | colors=wedge_colors, 104 | radius=node_size[n], 105 | wedgeprops={"edgecolor": wedge_color, "linewidth": wedge_width}, 106 | normalize=True, 107 | ) 108 | if with_node_labels: 109 | ax.annotate( 110 | n, 111 | (pos[n][0] - 0.1, pos[n][1] - 0.06), 112 | fontsize=label_size, 113 | color=label_col, 114 | ) 115 | if title is not None: 116 | plt.title(title, fontsize=title_size) 117 | ax.axis("equal") 118 | plt.axis("off") 119 | plt.tight_layout() 120 | 121 | 122 | def extract_pie_properties( 123 | i: int, u: np.array, colors: dict, threshold: float = 0.1 124 | ) -> Tuple[np.array, np.array]: 125 | """Given a node, it extracts the wedge sizes and the respective colors for the pie chart 126 | that represents its membership. 127 | 128 | Parameters 129 | ---------- 130 | i: node id. 131 | u: membership matrix. 132 | colors: dictionary of colors, where key represent the group id and values are colors. 133 | threshold: threshold for node membership. 134 | 135 | Returns 136 | ------- 137 | wedge_sizes: wedge sizes. 138 | wedge_colors: sequence of colors through which the pie chart will cycle. 139 | """ 140 | valid_groups = np.where(u[i] > threshold)[0] 141 | wedge_sizes = u[i][valid_groups] 142 | wedge_colors = [colors[k] for k in valid_groups] 143 | return wedge_sizes, wedge_colors 144 | -------------------------------------------------------------------------------- /hypergraphx/viz/draw_hypergraph.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import Optional, Union 3 | 4 | import matplotlib.pyplot as plt 5 | import networkx as nx 6 | import numpy as np 7 | 8 | from hypergraphx import Hypergraph 9 | from hypergraphx.representations.projections import clique_projection 10 | 11 | 12 | def Sum_points(P1, P2): 13 | x1, y1 = P1 14 | x2, y2 = P2 15 | return x1 + x2, y1 + y2 16 | 17 | 18 | def Multiply_point(multiplier, P): 19 | x, y = P 20 | return float(x) * float(multiplier), float(y) * float(multiplier) 21 | 22 | 23 | def Check_if_object_is_polygon(Cartesian_coords_list): 24 | if ( 25 | Cartesian_coords_list[0] 26 | == Cartesian_coords_list[len(Cartesian_coords_list) - 1] 27 | ): 28 | return True 29 | else: 30 | return False 31 | 32 | 33 | class Object: 34 | def __init__(self, Cartesian_coords_list): 35 | self.Cartesian_coords_list = Cartesian_coords_list 36 | 37 | def Find_Q_point_position(self, P1, P2): 38 | Summand1 = Multiply_point(float(3) / float(4), P1) 39 | Summand2 = Multiply_point(float(1) / float(4), P2) 40 | Q = Sum_points(Summand1, Summand2) 41 | return Q 42 | 43 | def Find_R_point_position(self, P1, P2): 44 | Summand1 = Multiply_point(float(1) / float(4), P1) 45 | Summand2 = Multiply_point(float(3) / float(4), P2) 46 | R = Sum_points(Summand1, Summand2) 47 | return R 48 | 49 | def Smooth_by_Chaikin(self, number_of_refinements): 50 | refinement = 1 51 | copy_first_coord = Check_if_object_is_polygon(self.Cartesian_coords_list) 52 | obj = Object(self.Cartesian_coords_list) 53 | while refinement <= number_of_refinements: 54 | self.New_cartesian_coords_list = [] 55 | 56 | for num, tuple in enumerate(self.Cartesian_coords_list): 57 | if num + 1 == len(self.Cartesian_coords_list): 58 | pass 59 | else: 60 | P1, P2 = (tuple, self.Cartesian_coords_list[num + 1]) 61 | Q = obj.Find_Q_point_position(P1, P2) 62 | R = obj.Find_R_point_position(P1, P2) 63 | self.New_cartesian_coords_list.append(Q) 64 | self.New_cartesian_coords_list.append(R) 65 | 66 | if copy_first_coord: 67 | self.New_cartesian_coords_list.append(self.New_cartesian_coords_list[0]) 68 | 69 | self.Cartesian_coords_list = self.New_cartesian_coords_list 70 | refinement += 1 71 | return self.Cartesian_coords_list 72 | 73 | 74 | def draw_hypergraph( 75 | hypergraph: Hypergraph, 76 | figsize: tuple = (12, 7), 77 | ax: Optional[plt.Axes] = None, 78 | pos: Optional[dict] = None, 79 | edge_color: str = "lightgrey", 80 | hyperedge_color_by_order: Optional[dict] = None, 81 | hyperedge_facecolor_by_order: Optional[dict] = None, 82 | edge_width: float = 1.2, 83 | hyperedge_alpha: Union[float, np.array] = 0.8, 84 | node_size: Union[int, np.array] = 150, 85 | node_color: Union[str, np.array] = "#E2E0DD", 86 | node_facecolor: Union[str, np.array] = "black", 87 | node_shape: str = "o", 88 | with_node_labels: bool = False, 89 | label_size: float = 10, 90 | label_col: str = "black", 91 | seed: int = 10, 92 | scale: int = 1, 93 | iterations: int = 100, 94 | opt_dist: float = 0.5, 95 | ): 96 | """Visualize a hypergraph.""" 97 | # Initialize figure. 98 | if ax is None: 99 | plt.figure(figsize=figsize) 100 | plt.subplot(1, 1, 1) 101 | ax = plt.gca() 102 | 103 | # Extract node positions based on the hypergraph clique projection. 104 | if pos is None: 105 | pos = nx.spring_layout( 106 | clique_projection(hypergraph, keep_isolated=True), 107 | iterations=iterations, 108 | seed=seed, 109 | scale=scale, 110 | k=opt_dist, 111 | ) 112 | 113 | # Set color hyperedges of size > 2 (order > 1). 114 | if hyperedge_color_by_order is None: 115 | hyperedge_color_by_order = {2: "#FFBC79", 3: "#79BCFF", 4: "#4C9F4C"} 116 | if hyperedge_facecolor_by_order is None: 117 | hyperedge_facecolor_by_order = {2: "#FFBC79", 3: "#79BCFF", 4: "#4C9F4C"} 118 | 119 | # Extract edges (hyperedges of size=2/order=1). 120 | edges = hypergraph.get_edges(order=1) 121 | 122 | # Initialize empty graph with the nodes and the pairwise interactions of the hypergraph. 123 | G = nx.Graph() 124 | G.add_nodes_from(hypergraph.get_nodes()) 125 | for e in edges: 126 | G.add_edge(e[0], e[1]) 127 | 128 | # Plot the graph. 129 | if type(node_shape) == str: 130 | node_shape = {n: node_shape for n in G.nodes()} 131 | for nid, n in enumerate(list(G.nodes())): 132 | nx.draw_networkx_nodes( 133 | G, 134 | pos, 135 | [n], 136 | node_size=node_size, 137 | node_shape=node_shape[n], 138 | node_color=node_color, 139 | edgecolors=node_facecolor, 140 | ax=ax, 141 | ) 142 | if with_node_labels: 143 | ax.annotate( 144 | n, 145 | (pos[n][0] - 0.1, pos[n][1] - 0.06), 146 | fontsize=label_size, 147 | color=label_col, 148 | ) 149 | 150 | # Plot the hyperedges (size>2/order>1). 151 | for hye in list(hypergraph.get_edges()): 152 | if len(hye) > 2: 153 | points = [] 154 | for node in hye: 155 | points.append((pos[node][0], pos[node][1])) 156 | # Center of mass of points. 157 | x_c = np.mean([x for x, y in points]) 158 | y_c = np.mean([y for x, y in points]) 159 | # Order points in a clockwise fashion. 160 | points = sorted(points, key=lambda x: np.arctan2(x[1] - y_c, x[0] - x_c)) 161 | 162 | if len(points) == 3: 163 | points = [ 164 | (x_c + 2.5 * (x - x_c), y_c + 2.5 * (y - y_c)) for x, y in points 165 | ] 166 | else: 167 | points = [ 168 | (x_c + 1.8 * (x - x_c), y_c + 1.8 * (y - y_c)) for x, y in points 169 | ] 170 | Cartesian_coords_list = points + [points[0]] 171 | 172 | obj = Object(Cartesian_coords_list) 173 | Smoothed_obj = obj.Smooth_by_Chaikin(number_of_refinements=12) 174 | 175 | # Visualisation. 176 | x1 = [i for i, j in Smoothed_obj] 177 | y1 = [j for i, j in Smoothed_obj] 178 | 179 | order = len(hye) - 1 180 | 181 | if order not in hyperedge_color_by_order.keys(): 182 | std_color = "#" + "%06x" % random.randint(0, 0xFFFFFF) 183 | hyperedge_color_by_order[order] = std_color 184 | 185 | if order not in hyperedge_facecolor_by_order.keys(): 186 | std_face_color = "#" + "%06x" % random.randint(0, 0xFFFFFF) 187 | hyperedge_facecolor_by_order[order] = std_face_color 188 | 189 | color = hyperedge_color_by_order[order] 190 | facecolor = hyperedge_facecolor_by_order[order] 191 | ax.fill(x1, y1, alpha=hyperedge_alpha, c=color, edgecolor=facecolor) 192 | 193 | nx.draw_networkx_edges(G, pos, width=edge_width, edge_color=edge_color, ax=ax) 194 | 195 | ax.axis("equal") 196 | plt.axis("equal") 197 | -------------------------------------------------------------------------------- /hypergraphx/viz/draw_multilayer_projection.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/hypergraphx/viz/draw_multilayer_projection.py -------------------------------------------------------------------------------- /hypergraphx/viz/draw_projections.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import networkx as nx 3 | 4 | from hypergraphx import Hypergraph 5 | from hypergraphx.representations.projections import ( 6 | bipartite_projection, 7 | clique_projection, 8 | ) 9 | 10 | 11 | def draw_bipartite(h: Hypergraph, pos=None, ax=None, align="vertical", **kwargs): 12 | """ 13 | Draws a bipartite graph representation of the hypergraph. 14 | Parameters 15 | ---------- 16 | h : Hypergraph. 17 | The hypergraph to be projected. 18 | pos : dict. 19 | A dictionary with nodes as keys and positions as values. 20 | ax : matplotlib.axes.Axes. 21 | The axes to draw the graph on. 22 | kwargs : dict. 23 | Keyword arguments to be passed to networkx.draw_networkx. 24 | align : str. 25 | The alignment of the nodes. Can be 'vertical' or 'horizontal'. 26 | 27 | Returns 28 | ------- 29 | ax : matplotlib.axes.Axes. 30 | The axes the graph was drawn on. 31 | """ 32 | g, id_to_obj = bipartite_projection(h) 33 | 34 | if pos is None: 35 | pos = nx.bipartite_layout( 36 | g, nodes=[n for n, d in g.nodes(data=True) if d["bipartite"] == 0] 37 | ) 38 | 39 | if ax is None: 40 | ax = plt.gca() 41 | 42 | nx.draw_networkx(g, pos=pos, ax=ax, **kwargs) 43 | plt.show() 44 | return ax 45 | 46 | 47 | def draw_clique(h: Hypergraph, pos=None, ax=None, **kwargs): 48 | """ 49 | Draws a clique projection of the hypergraph. 50 | Parameters 51 | ---------- 52 | h : Hypergraph. 53 | The hypergraph to be projected. 54 | pos : dict. 55 | A dictionary with nodes as keys and positions as values. 56 | ax : matplotlib.axes.Axes. 57 | The axes to draw the graph on. 58 | kwargs : dict. 59 | Keyword arguments to be passed to networkx.draw_networkx. 60 | 61 | Returns 62 | ------- 63 | ax : matplotlib.axes.Axes. The axes the graph was drawn on. 64 | """ 65 | g = clique_projection(h) 66 | 67 | if pos is None: 68 | pos = nx.spring_layout(g) 69 | 70 | if ax is None: 71 | ax = plt.gca() 72 | 73 | nx.draw_networkx(g, pos=pos, ax=ax, **kwargs) 74 | plt.show() 75 | return ax 76 | -------------------------------------------------------------------------------- /hypergraphx/viz/draw_simplicial.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import networkx as nx 3 | 4 | from hypergraphx.representations.projections import clique_projection 5 | 6 | 7 | def find_triplets(list): 8 | triplets = [] 9 | for i in range(len(list)): 10 | for j in range(i + 1, len(list)): 11 | for k in range(j + 1, len(list)): 12 | triplets.append([list[i], list[j], list[k]]) 13 | return triplets 14 | 15 | 16 | def draw_SC( 17 | HG, 18 | pos=None, 19 | link_color="black", 20 | hyperlink_color_by_order={2: "r", 3: "orange", 4: "green"}, 21 | link_width=2, 22 | node_size=150, 23 | node_color="#5494DA", 24 | with_labels=False, 25 | ax=None, 26 | ): 27 | G = clique_projection(HG, keep_isolated=True) 28 | if pos == None: 29 | pos = nx.spring_layout(G) 30 | for h_edge in HG.get_edges(): 31 | if len(h_edge) > 2: 32 | order = len(h_edge) - 1 33 | 34 | if order >= 5: 35 | alpha = 0.1 36 | else: 37 | alpha = 0.5 38 | 39 | if order not in hyperlink_color_by_order.keys(): 40 | hyperlink_color_by_order[order] = "Black" 41 | color = hyperlink_color_by_order[order] 42 | 43 | x_coor = [] 44 | y_coor = [] 45 | triplets = find_triplets(h_edge) 46 | for triplet in triplets: 47 | for node in triplet: 48 | x_coor.append(pos[node][0]) 49 | y_coor.append(pos[node][1]) 50 | # print(triplet) 51 | 52 | plt.fill(x_coor, y_coor, alpha=alpha, c=color) 53 | 54 | nx.draw( 55 | G, 56 | pos=pos, 57 | with_labels=with_labels, 58 | node_color=node_color, 59 | edge_color=link_color, 60 | width=link_width, 61 | node_size=node_size, 62 | ax=ax, 63 | ) 64 | -------------------------------------------------------------------------------- /hypergraphx/viz/plot_motifs.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import seaborn as sns 3 | 4 | 5 | def _sort_for_visualization(motifs: list): 6 | """ 7 | Sort motifs for visualization. 8 | Motifs are sorted in such a way to show first lower order motifs, then higher order motifs. 9 | 10 | Parameters 11 | ---------- 12 | motifs : list 13 | List of motifs to sort 14 | 15 | Returns 16 | ------- 17 | list 18 | Sorted list of motifs 19 | """ 20 | import numpy as np 21 | 22 | motifs = np.roll(motifs, 3) 23 | return motifs 24 | 25 | 26 | def plot_motifs(motifs: list, save_name: str = None): 27 | """ 28 | Plot motifs. Motifs are sorted in such a way to show first lower order motifs, then higher order motifs. 29 | 30 | Parameters 31 | ---------- 32 | motifs : list 33 | List of motifs to plot 34 | 35 | save_name : str, optional 36 | Name of the file to save the plot, by default None 37 | 38 | Raises 39 | ------ 40 | ValueError 41 | Motifs must be a list of length 6. 42 | 43 | Returns 44 | ------- 45 | None 46 | """ 47 | if len(motifs) != 6: 48 | raise ValueError("Motifs must be a list of length 6.") 49 | motifs = _sort_for_visualization(motifs) 50 | cols = ["#cd3031" if (x < 0) else "#557fa3" for x in motifs] 51 | g = sns.barplot(x=["I", "II", "III", "IV", "V", "VI"], y=motifs, palette=cols) 52 | g.axhline(0, color="black", linewidth=0.5) 53 | plt.ylim(-1, 1) 54 | plt.ylabel("Motif abundance score") 55 | sns.despine() 56 | if save_name != None: 57 | plt.savefig("{}".format(save_name), bbox_inches="tight") 58 | -------------------------------------------------------------------------------- /images/hypergraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/images/hypergraph.png -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/logo/logo.png -------------------------------------------------------------------------------- /logo/logo_cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/logo/logo_cropped.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | networkx 4 | pandas 5 | scikit-learn 6 | pytest 7 | matplotlib 8 | seaborn 9 | -------------------------------------------------------------------------------- /requirements_docs.txt: -------------------------------------------------------------------------------- 1 | furo -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description_file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import re 4 | 5 | from setuptools import find_packages, setup 6 | 7 | 8 | def read_version(): 9 | init_file = os.path.join(os.path.dirname(__file__), "hypergraphx", "__init__.py") 10 | with open(init_file, "r") as f: 11 | content = f.read() 12 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", content, re.M) 13 | if version_match: 14 | return version_match.group(1) 15 | raise RuntimeError("Unable to find version string.") 16 | 17 | 18 | def read(*paths, **kwargs): 19 | content = "" 20 | with io.open( 21 | os.path.join(os.path.dirname(__file__), *paths), 22 | encoding=kwargs.get("encoding", "utf8"), 23 | ) as open_file: 24 | content = open_file.read().strip() 25 | return content 26 | 27 | 28 | setup( 29 | name="hypergraphx", 30 | version=read_version(), 31 | license="BSD-3-Clause license", 32 | description="HGX is a multi-purpose, open-source Python library for higher-order network analysis", 33 | long_description=read("README.md"), 34 | long_description_content_type="text/markdown", 35 | author="HGX-Team", 36 | author_email="lotitoqf@gmail.com", 37 | url="https://github.com/HGX-Team/hypergraphx", 38 | keywords=["hypergraphs", "networks"], 39 | packages=find_packages(exclude=["tests", ".github"]), 40 | install_requires=[ 41 | "numpy", 42 | "scipy", 43 | "networkx", 44 | "pandas", 45 | "scikit-learn", 46 | "pytest", 47 | "matplotlib", 48 | "seaborn", 49 | ], 50 | classifiers=[ 51 | "Development Status :: 3 - Alpha", 52 | "Intended Audience :: Developers", 53 | "Topic :: Software Development :: Build Tools", 54 | "License :: OSI Approved :: BSD License", 55 | "Programming Language :: Python :: 3", 56 | "Programming Language :: Python :: 3.4", 57 | "Programming Language :: Python :: 3.5", 58 | "Programming Language :: Python :: 3.6", 59 | ], 60 | ) 61 | -------------------------------------------------------------------------------- /test_data/small_hypergraph1/hyperedges.txt: -------------------------------------------------------------------------------- 1 | 0 1 2 | 0 2 3 | 0 3 4 | 2 1 5 | 1 3 6 | 4 5 1 7 | 1 5 8 | 2 3 5 9 | 0 1 2 3 4 5 10 | -------------------------------------------------------------------------------- /test_data/small_hypergraph2/hyperedges.txt: -------------------------------------------------------------------------------- 1 | 0 1 2 | 0 1 3 | 0 1 4 | 0 2 5 | 0 3 6 | 2 1 7 | 1 3 8 | 4 5 1 9 | 1 5 10 | 2 3 5 11 | 5 3 2 12 | 6 7 13 | 7 9 0 10 14 | 2 7 15 | 0 10 16 | 1 10 17 | 9 10 18 | -------------------------------------------------------------------------------- /test_data/small_hypergraph3/hyperedges.txt: -------------------------------------------------------------------------------- 1 | 0 2 2 | 1 3 3 | 2 4 4 | 0 7 5 | 0 1 2 3 6 | 0 1 2 3 4 5 6 7 | 2 4 6 8 | 7 0 9 | 5 8 10 | 9 10 11 | -------------------------------------------------------------------------------- /test_data/with_isolated_nodes/hyperedges.txt: -------------------------------------------------------------------------------- 1 | 0 2 2 | 100 200 3 | 10 1 4 | 0 33 10 1 27 84 119 5 | -------------------------------------------------------------------------------- /test_data/with_literal_nodes/hyperedges.txt: -------------------------------------------------------------------------------- 1 | a b 2 | a c 3 | a some_name 4 | custom_node a some_name 5 | a very big hyperedge with some nodes 6 | z h a b c d e f g h i j 7 | -------------------------------------------------------------------------------- /test_data/workplace/workplace_meta.csv: -------------------------------------------------------------------------------- 1 | nodeID,nodeName,class,classID 2 | 0,35,DISQ,0 3 | 1,131,DISQ,0 4 | 2,184,DISQ,0 5 | 3,185,DISQ,0 6 | 4,210,DISQ,0 7 | 5,743,DISQ,0 8 | 6,751,DISQ,0 9 | 7,253,DISQ,0 10 | 8,255,DISQ,0 11 | 9,265,DISQ,0 12 | 10,778,DISQ,0 13 | 11,268,DISQ,0 14 | 12,273,DISQ,0 15 | 13,826,DISQ,0 16 | 14,845,DISQ,0 17 | 15,17,DMCT,1 18 | 16,21,DMCT,1 19 | 17,48,DMCT,1 20 | 18,50,DMCT,1 21 | 19,66,DMCT,1 22 | 20,80,DMCT,1 23 | 21,101,DMCT,1 24 | 22,102,DMCT,1 25 | 23,105,DMCT,1 26 | 24,118,DMCT,1 27 | 25,119,DMCT,1 28 | 26,134,DMCT,1 29 | 27,179,DMCT,1 30 | 28,209,DMCT,1 31 | 29,240,DMCT,1 32 | 30,762,DMCT,1 33 | 31,771,DMCT,1 34 | 32,784,DMCT,1 35 | 33,275,DMCT,1 36 | 34,285,DMCT,1 37 | 35,804,DMCT,1 38 | 36,335,DMCT,1 39 | 37,875,DMCT,1 40 | 38,938,DMCT,1 41 | 39,431,DMCT,1 42 | 40,511,DMCT,1 43 | 41,513,DSE,2 44 | 42,15,DSE,2 45 | 43,29,DSE,2 46 | 44,39,DSE,2 47 | 45,56,DSE,2 48 | 46,79,DSE,2 49 | 47,603,DSE,2 50 | 48,95,DSE,2 51 | 49,113,DSE,2 52 | 50,120,DSE,2 53 | 51,123,DSE,2 54 | 52,132,DSE,2 55 | 53,662,DSE,2 56 | 54,164,DSE,2 57 | 55,172,DSE,2 58 | 56,181,DSE,2 59 | 57,194,DSE,2 60 | 58,196,DSE,2 61 | 59,205,DSE,2 62 | 60,222,DSE,2 63 | 61,223,DSE,2 64 | 62,242,DSE,2 65 | 63,765,DSE,2 66 | 64,267,DSE,2 67 | 65,779,DSE,2 68 | 66,272,DSE,2 69 | 67,786,DSE,2 70 | 68,791,DSE,2 71 | 69,819,DSE,2 72 | 70,311,DSE,2 73 | 71,939,DSE,2 74 | 72,987,DSE,2 75 | 73,494,DSE,2 76 | 74,496,DSE,2 77 | 75,87,SFLE,3 78 | 76,116,SFLE,3 79 | 77,211,SFLE,3 80 | 78,213,SFLE,3 81 | 79,533,SRH,4 82 | 80,63,SRH,4 83 | 81,601,SRH,4 84 | 82,122,SRH,4 85 | 83,150,SRH,4 86 | 84,153,SRH,4 87 | 85,154,SRH,4 88 | 86,709,SRH,4 89 | 87,271,SRH,4 90 | 88,448,SRH,4 91 | 89,481,SRH,4 92 | 90,492,SRH,4 93 | 91,499,SRH,4 94 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from pathlib import Path 3 | from typing import Dict, List, Tuple 4 | 5 | import numpy as np 6 | import pytest 7 | 8 | from hypergraphx import Hypergraph 9 | 10 | DATA_DIR = Path(__file__).resolve().parent.parent / "test_data" 11 | ALL_SMALL_NUMERICAL_DATASETS = ( 12 | "small_hypergraph1", 13 | "small_hypergraph2", 14 | "small_hypergraph3", 15 | "with_isolated_nodes", 16 | ) 17 | ALL_SMALL_DATASETS = ALL_SMALL_NUMERICAL_DATASETS + ("with_literal_nodes",) 18 | ALL_DATASETS = ALL_SMALL_DATASETS + ("justice",) 19 | 20 | 21 | def _read_hye_list(hye_file: Path) -> List[Tuple[int]]: 22 | hye_list = [] 23 | with open(hye_file, "r") as file: 24 | for hye in file.readlines(): 25 | hye = hye.strip("\n").split(" ") 26 | hye = map(int, hye) 27 | hye = tuple(sorted(hye)) 28 | hye_list.append(hye) 29 | 30 | counts = Counter(hye_list) 31 | if all(count == 1 for count in counts.values()): 32 | weights = None 33 | else: 34 | hye_list = list(counts.keys()) 35 | weights = np.fromiter(counts.values(), dtype=int) 36 | 37 | return hye_list, weights 38 | 39 | 40 | @pytest.fixture(scope="package") 41 | def synthetic_small_numerical_datasets() -> Dict[str, Hypergraph]: 42 | datasets = dict() 43 | for dataset in ALL_SMALL_NUMERICAL_DATASETS: 44 | hye_file = DATA_DIR / dataset / "hyperedges.txt" 45 | hye_list, weights = _read_hye_list(hye_file) 46 | weighted = weights is not None 47 | datasets[dataset] = Hypergraph( 48 | edge_list=hye_list, weighted=weighted, weights=weights 49 | ) 50 | 51 | return datasets 52 | 53 | 54 | @pytest.fixture(scope="package") 55 | def synthetic_literal_dataset() -> Dict[str, Hypergraph]: 56 | dataset_name = "with_literal_nodes" 57 | hye_file = DATA_DIR / dataset_name / "hyperedges.txt" 58 | with open(hye_file, "r") as file: 59 | hye_list = list(map(lambda hye: hye.strip("\n").split(" "), file.readlines())) 60 | return {dataset_name: Hypergraph(hye_list)} 61 | 62 | 63 | @pytest.fixture(scope="package") 64 | def justice_dataset() -> Dict[str, Hypergraph]: 65 | hye_list, _ = _read_hye_list(DATA_DIR / "justice_data" / "hyperedges.txt") 66 | 67 | with open(DATA_DIR / "justice_data" / "weights.txt") as file: 68 | weight_list = list(map(int, file.readlines())) 69 | 70 | return {"justice": Hypergraph(hye_list, weighted=True, weights=weight_list)} 71 | 72 | 73 | @pytest.fixture(scope="package", params=ALL_DATASETS) 74 | def loaded_hypergraph( 75 | synthetic_small_numerical_datasets, 76 | synthetic_literal_dataset, 77 | justice_dataset, 78 | request, 79 | ) -> Hypergraph: 80 | all_data_dict = { 81 | **synthetic_small_numerical_datasets, 82 | **synthetic_literal_dataset, 83 | **justice_dataset, 84 | } 85 | return all_data_dict[request.param] 86 | -------------------------------------------------------------------------------- /tests/core/directed_hypergraphs/test_reciprocity.py: -------------------------------------------------------------------------------- 1 | from hypergraphx import DirectedHypergraph 2 | from hypergraphx.measures.directed.reciprocity import exact_reciprocity, weak_reciprocity 3 | 4 | # STRONG RECIPROCITY 5 | 6 | def test_basic_exact_reciprocity(): 7 | # Test with simple reciprocated edges 8 | edges = [ 9 | ((1,), (2,)), 10 | ((2,), (1,)), 11 | ((3,), (4,)), 12 | ((4,), (3,)) 13 | ] 14 | h = DirectedHypergraph(edge_list=edges) 15 | result = exact_reciprocity(h, 3) 16 | 17 | # Expect 100% reciprocity for edges of size 2 18 | assert result[2] == 1.0, "Expected 100% reciprocity for edges of size 2" 19 | assert result[3] == 0, f"Expected no reciprocity for edges of size 3" 20 | 21 | 22 | def test_no_exact_reciprocity(): 23 | # Test with no reciprocated edges 24 | edges = [ 25 | ((1,), (2,)), 26 | ((3,), (4,)), 27 | ((5,), (6,)) 28 | ] 29 | h = DirectedHypergraph(edge_list=edges) 30 | result = exact_reciprocity(h, 3) 31 | 32 | # Expect 0% reciprocity across all edge sizes 33 | for i in range(2, 3): 34 | assert result[i] == 0, f"Expected no reciprocity for edges of size {i}" 35 | 36 | 37 | def test_mixed_edge_sizes_exact_reciprocity(): 38 | # Test with mixed edge sizes 39 | edges = [ 40 | ((1,), (2,)), # size 2, no reciprocation 41 | ((2,), (1,)), # size 2, reciprocated with above 42 | ((3, 4), (5,)), # size 3, no reciprocation 43 | ((5,), (3, 4)), # size 3, reciprocated with above 44 | ((6,), (7, 8)), # size 3, no reciprocation 45 | ((9, 10), (11, 12, 13)), # size 5, no reciprocation 46 | ] 47 | h = DirectedHypergraph(edge_list=edges) 48 | result = exact_reciprocity(h, 6) 49 | 50 | assert result[2] == 1.0, "Expected 100% reciprocity for edges of size 2" 51 | assert result[3] == 2/3, "Expected 50% reciprocity for edges of size 3" 52 | assert result[5] == 0.0, "Expected 0% reciprocity for edges of size 5" 53 | for i in [4, 6]: 54 | assert result[i] == 0, f"Expected no reciprocity for edges of size {i}" 55 | 56 | 57 | # WEAK RECIPROCITY 58 | 59 | def test_basic_weak_reciprocity(): 60 | # Test with simple weak reciprocity where there are reverse node pairs 61 | edges = [ 62 | ((1,), (2,)), # Size 2, reciprocated with (2,), (1,) 63 | ((2,), (1,)), # Size 2, reciprocated with above 64 | ((3,), (4,)), # Size 2, no reciprocation 65 | ((5,), (6, 7)), # Size 3, no reciprocation 66 | ] 67 | hypergraph = DirectedHypergraph(edge_list=edges) 68 | result = weak_reciprocity(hypergraph, max_hyperedge_size=3) 69 | 70 | # Expected: 50% reciprocity for size 2, 0% for size 3 71 | assert result[2] == 2/3, "Expected 50% weak reciprocity for edges of size 2" 72 | assert result[3] == 0.0, "Expected no weak reciprocity for edges of size 3" 73 | 74 | def test_no_weak_reciprocity(): 75 | # Test with no reciprocated edges, no reverse pairs 76 | edges = [ 77 | ((1,), (2,)), # Size 2, no reciprocation 78 | ((3,), (4,)), # Size 2, no reciprocation 79 | ((5,), (6,)), # Size 2, no reciprocation 80 | ] 81 | hypergraph = DirectedHypergraph(edge_list=edges) 82 | result = weak_reciprocity(hypergraph, max_hyperedge_size=2) 83 | 84 | # Expected: 0 reciprocity for all sizes 85 | for i in range(2, 3): 86 | assert result[i] == 0, f"Expected no reciprocity for edges of size {i}" 87 | 88 | def test_mixed_edge_sizes_weak_reciprocity(): 89 | # Test with a mix of reciprocated and non-reciprocated edges across sizes 90 | edges = [ 91 | ((1,), (2,)), # Size 2, reciprocated with (2,), (1,) 92 | ((2,), (1,)), # Size 2, reciprocated with above 93 | ((3, 4), (5,)), # Size 3, no reciprocation 94 | ((6,), (7, 8)), # Size 3, no reciprocation 95 | ((9, 10), (11, 12)), # Size 4, reciprocated with (11, 12), (9, 10) 96 | ((11,), (10,)) # Size 2, reciprocated with above 97 | ] 98 | hypergraph = DirectedHypergraph(edge_list=edges) 99 | result = weak_reciprocity(hypergraph, max_hyperedge_size=4) 100 | 101 | assert result[2] == 1.0, "Expected 66.6% weak reciprocity for edges of size 2" 102 | assert result[3] == 0.0, "Expected 0% weak reciprocity for edges of size 3" 103 | assert result[4] == 1.0, "Expected 100% weak reciprocity for edges of size 4" 104 | 105 | def test_empty_hypergraph(): 106 | # Test with no edges 107 | edges = [] 108 | hypergraph = DirectedHypergraph(edge_list=edges) 109 | result = weak_reciprocity(hypergraph, max_hyperedge_size=5) 110 | 111 | # Expected: 0 reciprocity for all edge sizes up to max_hyperedge_size 112 | for i in range(2, 6): 113 | assert result[i] == 0, f"Expected no reciprocity for edges of size {i}" 114 | 115 | def test_large_edge_size_out_of_bounds(): 116 | # Test with edges larger than max_hyperedge_size (should be ignored) 117 | edges = [ 118 | ((1, 2, 3), (4, 5, 6)), # Size 6, should be ignored 119 | ((10,), (11,)), # Size 2, no reciprocity 120 | ] 121 | hypergraph = DirectedHypergraph(edge_list=edges) 122 | result = weak_reciprocity(hypergraph, max_hyperedge_size=5) 123 | 124 | # Only edge size 2 should be considered, with no reciprocity 125 | assert result[2] == 0, "Expected no weak reciprocity for edge size 2" 126 | for i in range(3, 6): 127 | assert result[i] == 0, f"Expected no reciprocity for edges of size {i}" 128 | -------------------------------------------------------------------------------- /tests/core/directed_hypergraphs/test_signature.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from hypergraphx import DirectedHypergraph 4 | from hypergraphx.measures.directed import hyperedge_signature_vector 5 | 6 | 7 | def test_hyperedge_signature_vector_basic(): 8 | """Test basic functionality of hyperedge signature vector calculation.""" 9 | edges = [ 10 | ((1, 2), (3, 4)), # Hyperedge with source size 2, target size 2 11 | ((5, ), (6, 7, 8)), # Hyperedge with source size 1, target size 3 12 | ] 13 | 14 | hypergraph = DirectedHypergraph(edges) 15 | 16 | # Test without max_hyperedge_size 17 | result = hyperedge_signature_vector(hypergraph) 18 | print(result) 19 | expected = np.array([0, 0, 1, 0, 1, 0, 0, 0, 0]) # 3x3 matrix flattened 20 | assert np.array_equal(result, expected), f"Expected {expected}, got {result}" 21 | 22 | def test_hyperedge_signature_vector_with_max_size(): 23 | """Test hyperedge signature vector with max_hyperedge_size specified.""" 24 | edges = [ 25 | ((1, 2), (3, 4)), # Hyperedge with size 4 (2 + 2) 26 | ((5,), (6, 7, 8)), # Hyperedge with size 4 (1 + 3) 27 | ((9, 10, 11), (12,)), # Hyperedge with size 4 (3 + 1) 28 | ] 29 | hypergraph = DirectedHypergraph(edges) 30 | 31 | # Specify max_hyperedge_size = 2 (no edges should be counted) 32 | result = hyperedge_signature_vector(hypergraph, max_hyperedge_size=2) 33 | print(result) 34 | expected = np.array([0]) # all edges exceed max size 2 35 | assert np.array_equal(result, expected), f"Expected {expected}, got {result}" 36 | 37 | def test_hyperedge_signature_vector_empty_hypergraph(): 38 | """Test the function with an empty hypergraph.""" 39 | hypergraph = DirectedHypergraph(edge_list=[]) 40 | result = hyperedge_signature_vector(hypergraph) 41 | expected = np.array([]) # Expecting an empty array for an empty hypergraph 42 | assert np.array_equal(result, expected), f"Expected {expected}, got {result}" 43 | 44 | def test_hyperedge_signature_vector_large_edges_ignored(): 45 | """Test that edges larger than max_hyperedge_size are ignored.""" 46 | edges = [ 47 | ((1, 2), (3, 4)), # Hyperedge with size 4 (2 + 2) 48 | ((5,), (6, 7, 8)), # Hyperedge with size 4 (1 + 3) 49 | ((9, 10, 11, 12), (13, 14, 15, 16)), # Hyperedge with size 8 (4 + 4) 50 | ] 51 | hypergraph = DirectedHypergraph(edges) 52 | 53 | # Specify max_hyperedge_size = 4 (only edges with size <= 4 should be counted) 54 | result = hyperedge_signature_vector(hypergraph, max_hyperedge_size=4) 55 | expected = np.array([0, 0, 1, 0, 1, 0, 0, 0, 0]) # 3x3 matrix flattened 56 | assert np.array_equal(result, expected), f"Expected {expected}, got {result}" -------------------------------------------------------------------------------- /tests/linalg/test_adjacency.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import sparse 3 | 4 | from hypergraphx import Hypergraph 5 | from hypergraphx.linalg.linalg import adjacency_matrix 6 | 7 | 8 | # Fixture loaded_hypergraph defined inside the package-level conftest.py 9 | 10 | 11 | ######################################################################################## 12 | # method Hypergraph.adjacency_matrix 13 | def test_adjacency_type(loaded_hypergraph: Hypergraph): 14 | adj = loaded_hypergraph.adjacency_matrix() 15 | assert isinstance(adj, sparse.csr_array) 16 | 17 | 18 | def test_adjacency_shape(loaded_hypergraph: Hypergraph): 19 | N = loaded_hypergraph.num_nodes() 20 | adj = loaded_hypergraph.adjacency_matrix() 21 | assert adj.shape == (N, N) 22 | 23 | 24 | def test_adjacency_is_symmetric(loaded_hypergraph: Hypergraph): 25 | adj = adjacency_matrix(loaded_hypergraph) 26 | assert not (adj != adj.transpose()).data.any() 27 | 28 | 29 | def test_adjacency_diagonal_is_zero(loaded_hypergraph: Hypergraph): 30 | adj = adjacency_matrix(loaded_hypergraph) 31 | assert np.all(adj.diagonal() == 0) 32 | 33 | 34 | def test_adjacency_from_dense_incidence(loaded_hypergraph: Hypergraph): 35 | N = loaded_hypergraph.num_nodes() 36 | dense_incidence = loaded_hypergraph.binary_incidence_matrix().todense() 37 | dense_adj = dense_incidence @ dense_incidence.transpose() 38 | dense_adj[np.arange(N), np.arange(N)] = 0 # set diagonal to 0 39 | 40 | sparse_adj = loaded_hypergraph.adjacency_matrix() 41 | 42 | assert np.all(dense_adj == sparse_adj.todense()) 43 | 44 | 45 | def test_adj_mapping_type(loaded_hypergraph: Hypergraph): 46 | _, mapping = loaded_hypergraph.adjacency_matrix(return_mapping=True) 47 | assert isinstance(mapping, dict) 48 | 49 | 50 | def test_adj_mapping_has_values_only_nodes_in_hypergraph(loaded_hypergraph: Hypergraph): 51 | _, mapping = loaded_hypergraph.adjacency_matrix(return_mapping=True) 52 | assert set(mapping.values()).issubset(set(loaded_hypergraph.get_nodes())) 53 | 54 | 55 | def test_adj_mapping_maps_back_all_and_only_nodes_in_hypergraph( 56 | loaded_hypergraph: Hypergraph, 57 | ): 58 | _, mapping = loaded_hypergraph.adjacency_matrix(return_mapping=True) 59 | back_mapped_nodes = {mapping[i] for i in range(loaded_hypergraph.num_nodes())} 60 | hypergraph_nodes = set(loaded_hypergraph.get_nodes()) 61 | assert back_mapped_nodes == hypergraph_nodes 62 | 63 | 64 | ######################################################################################## 65 | # Method Hypergraph.dual_random_walk_adjacency 66 | def test_rw_walk_adjacency_type(loaded_hypergraph: Hypergraph): 67 | adj = loaded_hypergraph.dual_random_walk_adjacency() 68 | assert isinstance(adj, sparse.csc_array) 69 | 70 | 71 | def test_rw_walk_adjacency_shape(loaded_hypergraph: Hypergraph): 72 | adj = loaded_hypergraph.dual_random_walk_adjacency() 73 | E = loaded_hypergraph.num_edges() 74 | assert adj.shape == (E, E) 75 | 76 | 77 | def test_rw_walk_adjacency_only_contains_ones(loaded_hypergraph: Hypergraph): 78 | adj = loaded_hypergraph.dual_random_walk_adjacency() 79 | assert np.all(adj.data == 1) 80 | 81 | 82 | def test_rw_walk_adjacency_diagonal_is_one(loaded_hypergraph: Hypergraph): 83 | adj = loaded_hypergraph.dual_random_walk_adjacency() 84 | assert np.all(adj.diagonal() == 1) 85 | 86 | 87 | def test_rw_walk_comparing_with_explicit_construction(loaded_hypergraph: Hypergraph): 88 | adj = loaded_hypergraph.dual_random_walk_adjacency() 89 | 90 | E = loaded_hypergraph.num_edges() 91 | explicit_adj = np.zeros((E, E)) 92 | edge_sets = [set(edge) for edge in loaded_hypergraph.get_edges()] 93 | for i, e in enumerate(edge_sets): 94 | for j, f in enumerate(edge_sets): 95 | explicit_adj[i, j] = bool(e & f) 96 | 97 | assert np.all(adj.todense() == explicit_adj) 98 | 99 | 100 | def test_rw_adj_mapping_type(loaded_hypergraph: Hypergraph): 101 | _, mapping = loaded_hypergraph.dual_random_walk_adjacency(return_mapping=True) 102 | assert isinstance(mapping, dict) 103 | 104 | 105 | def test_rw_adj_mapping_has_values_only_nodes_in_hypergraph( 106 | loaded_hypergraph: Hypergraph, 107 | ): 108 | _, mapping = loaded_hypergraph.dual_random_walk_adjacency(return_mapping=True) 109 | assert set(mapping.values()).issubset(set(loaded_hypergraph.get_nodes())) 110 | 111 | 112 | def test_rw_adj_mapping_maps_back_all_and_only_nodes_in_hypergraph( 113 | loaded_hypergraph: Hypergraph, 114 | ): 115 | _, mapping = loaded_hypergraph.dual_random_walk_adjacency(return_mapping=True) 116 | back_mapped_nodes = {mapping[i] for i in range(loaded_hypergraph.num_nodes())} 117 | hypergraph_nodes = set(loaded_hypergraph.get_nodes()) 118 | assert back_mapped_nodes == hypergraph_nodes 119 | -------------------------------------------------------------------------------- /tests/linalg/test_hye_list_to_binary_incidence.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | from scipy import sparse 4 | 5 | from hypergraphx.linalg.linalg import hye_list_to_binary_incidence 6 | 7 | hye_lists = [ 8 | [ 9 | (0, 1, 2), 10 | (1, 2), 11 | (0, 1), 12 | (3, 0, 2), 13 | ], 14 | [ 15 | (0, 1, 2, 3, 4, 5, 6), 16 | (1, 2), 17 | (0, 1), 18 | (2, 5, 1), 19 | (2, 5, 3), 20 | ], 21 | ] 22 | hye_lists_with_repetitions_and_null = [ 23 | [ 24 | (0, 1), 25 | (1, 2), 26 | (0, 1), 27 | (3, 0, 2), 28 | ], 29 | [ 30 | (0, 1), 31 | (1, 2), 32 | (1, 0), 33 | (3, 0, 2), 34 | ], 35 | [ 36 | (0,), 37 | (), 38 | (0, 1, 2), 39 | (3,), 40 | (), 41 | ], 42 | [ 43 | (0,), 44 | (), 45 | (0, 1, 2), 46 | (3,), 47 | (), 48 | (0, 1, 2), 49 | (), 50 | (), 51 | (), 52 | ], 53 | [ 54 | (0, 1, 2, 3, 4, 5, 6), 55 | (1, 2), 56 | (0, 1), 57 | (2, 5, 1), 58 | (2, 1, 5), 59 | (2, 5, 3), 60 | ], 61 | [ 62 | (0, 1, 1, 1, 1, 1, 2), 63 | (1, 2, 2, 1), 64 | (0, 1), 65 | (2, 5, 1), 66 | (2, 1, 5), 67 | (2, 5, 3), 68 | ], 69 | ] 70 | shapes = [ 71 | None, 72 | (4, 4), 73 | (4, 5), 74 | (4, 9), 75 | (4, 1000), 76 | (100, 4), 77 | (100, 5), 78 | ] 79 | 80 | 81 | def is_fail_config(hye_list, shape): 82 | N = max(map(max, (hye for hye in hye_list if hye))) + 1 83 | E = len(hye_list) 84 | 85 | return shape is not None and (shape[0] < N or shape[1] < E) 86 | 87 | 88 | failing_configs = [ 89 | (hye_list, shape) 90 | for hye_list in hye_lists 91 | for shape in shapes 92 | if is_fail_config(hye_list, shape) 93 | ] 94 | correct_configs = [ 95 | (hye_list, shape) 96 | for hye_list in hye_lists 97 | for shape in shapes 98 | if not is_fail_config(hye_list, shape) 99 | ] 100 | 101 | 102 | @pytest.fixture(scope="class") 103 | def expected_shape(hye_list, shape): 104 | N = max(map(max, (hye for hye in hye_list if hye))) + 1 105 | E = len(hye_list) 106 | 107 | not_valid = shape is not None and (shape[0] < N or shape[1] < E) 108 | if not_valid: 109 | return 110 | 111 | if shape is not None: 112 | expected_shape = shape 113 | else: 114 | expected_shape = (N, E) 115 | 116 | return expected_shape 117 | 118 | 119 | @pytest.mark.parametrize("hye_list,shape", correct_configs, scope="class") 120 | class TestHyeListToBinaryIncidenceFromListCorrect: 121 | def test_incidence_type(self, hye_list, shape, expected_shape): 122 | sparse_incidence = hye_list_to_binary_incidence(hye_list, shape) 123 | print(type(sparse_incidence)) 124 | assert isinstance(sparse_incidence, sparse.coo_array) 125 | 126 | def test_incidence_shape(self, hye_list, shape, expected_shape): 127 | sparse_incidence = hye_list_to_binary_incidence(hye_list, shape) 128 | assert sparse_incidence.shape == expected_shape 129 | 130 | def test_incidence_only_contains_ones(self, hye_list, shape, expected_shape): 131 | sparse_incidence = hye_list_to_binary_incidence(hye_list, shape) 132 | assert np.all(sparse_incidence.data == 1) 133 | 134 | def test_with_dense(self, hye_list, shape, expected_shape): 135 | sparse_incidence = hye_list_to_binary_incidence(hye_list, shape) 136 | 137 | dense = np.zeros(expected_shape) 138 | for idx, hye in enumerate(hye_list): 139 | dense[hye, idx] = 1 140 | assert np.all(sparse_incidence.todense() == dense) 141 | 142 | 143 | @pytest.mark.parametrize("hye_list,shape", failing_configs) 144 | def test_raises_error(hye_list, shape): 145 | N = max(map(max, (hye for hye in hye_list if hye))) + 1 146 | E = len(hye_list) 147 | 148 | if shape is not None: 149 | if shape[0] < N or shape[1] < E: 150 | with pytest.raises(ValueError): 151 | hye_list_to_binary_incidence(hye_list, shape) 152 | -------------------------------------------------------------------------------- /tests/linalg/test_incidence.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import sparse 3 | 4 | import hypergraphx.linalg as hl 5 | from hypergraphx import Hypergraph 6 | 7 | 8 | # Fixture loaded_hypergraph defined inside the package-level conftest.py 9 | 10 | 11 | def test_binary_incidence_type(loaded_hypergraph: Hypergraph): 12 | inc = loaded_hypergraph.binary_incidence_matrix(return_mapping=False) 13 | assert isinstance(inc, sparse.csr_array) 14 | 15 | 16 | def test_binary_incidence_only_contains_ones(loaded_hypergraph: Hypergraph): 17 | inc = loaded_hypergraph.binary_incidence_matrix(return_mapping=False) 18 | assert np.all(inc.data == 1) 19 | 20 | 21 | def test_binary_incidence_shape(loaded_hypergraph: Hypergraph): 22 | inc = loaded_hypergraph.binary_incidence_matrix(return_mapping=False) 23 | assert inc.shape == (loaded_hypergraph.num_nodes(), loaded_hypergraph.num_edges()) 24 | 25 | 26 | def test_mapping_type(loaded_hypergraph: Hypergraph): 27 | _, mapping = loaded_hypergraph.binary_incidence_matrix(return_mapping=True) 28 | assert isinstance(mapping, dict) 29 | 30 | 31 | def test_mapping_has_values_only_nodes_in_hypergraph(loaded_hypergraph: Hypergraph): 32 | _, mapping = loaded_hypergraph.binary_incidence_matrix(return_mapping=True) 33 | assert set(mapping.values()).issubset(set(loaded_hypergraph.get_nodes())) 34 | 35 | 36 | def test_mapping_maps_back_all_and_only_nodes_in_hypergraph( 37 | loaded_hypergraph: Hypergraph, 38 | ): 39 | _, mapping = loaded_hypergraph.binary_incidence_matrix(return_mapping=True) 40 | back_mapped_nodes = {mapping[i] for i in range(loaded_hypergraph.num_nodes())} 41 | hypergraph_nodes = set(loaded_hypergraph.get_nodes()) 42 | assert back_mapped_nodes == hypergraph_nodes 43 | 44 | 45 | def test_binary_incidence_from_dense(loaded_hypergraph: Hypergraph): 46 | def transform_hye_to_new_indices(hye, mapping_): 47 | return tuple(mapping_[node] for node in hye) 48 | 49 | inc, mapping = loaded_hypergraph.binary_incidence_matrix(return_mapping=True) 50 | inverse_mapping = dict(zip(mapping.values(), mapping.keys())) 51 | 52 | hye_list = [ 53 | transform_hye_to_new_indices(hye, inverse_mapping) 54 | for hye in loaded_hypergraph.get_edges() 55 | ] 56 | dense_incidence = np.zeros( 57 | (loaded_hypergraph.num_nodes(), loaded_hypergraph.num_edges()) 58 | ) 59 | for i, hye in enumerate(hye_list): 60 | dense_incidence[hye, i] = 1 61 | 62 | assert np.all(inc.todense() == dense_incidence) 63 | 64 | 65 | def test_incidence(loaded_hypergraph: Hypergraph): 66 | def transform_hye_to_new_indices(hye, mapping_): 67 | return tuple(mapping_[node] for node in hye) 68 | 69 | inc, mapping = loaded_hypergraph.incidence_matrix(return_mapping=True) 70 | inverse_mapping = dict(zip(mapping.values(), mapping.keys())) 71 | 72 | hye_list = [ 73 | transform_hye_to_new_indices(hye, inverse_mapping) 74 | for hye in loaded_hypergraph.get_edges() 75 | ] 76 | incidence = np.zeros((loaded_hypergraph.num_nodes(), loaded_hypergraph.num_edges())) 77 | weights_arr = loaded_hypergraph.get_weights() 78 | for i, hye in enumerate(hye_list): 79 | incidence[hye, i] = weights_arr[i] 80 | 81 | assert np.all(inc.todense() == incidence) 82 | 83 | 84 | def test_incidence_by_order(loaded_hypergraph: Hypergraph): 85 | def transform_hye_to_new_indices(hye, mapping_): 86 | return tuple(mapping_[node] for node in hye) 87 | 88 | order_to_test = 2 89 | 90 | inc, mapping = hl.incidence_matrix_by_order( 91 | loaded_hypergraph, order=order_to_test, return_mapping=True 92 | ) 93 | 94 | inverse_mapping = dict(zip(mapping.values(), mapping.keys())) 95 | 96 | subhypergraph_by_order = loaded_hypergraph.get_edges( 97 | order=order_to_test, subhypergraph=True, keep_isolated_nodes=False 98 | ) 99 | hye_list = [ 100 | transform_hye_to_new_indices(hye, inverse_mapping) 101 | for hye in subhypergraph_by_order.get_edges() 102 | ] 103 | incidence = np.zeros( 104 | (subhypergraph_by_order.num_nodes(), subhypergraph_by_order.num_edges()) 105 | ) 106 | weights_arr = subhypergraph_by_order.get_weights() 107 | for i, hye in enumerate(hye_list): 108 | incidence[hye, i] = weights_arr[i] 109 | 110 | assert np.all(inc.todense() == incidence) 111 | 112 | 113 | def test_incidence_all_orders(loaded_hypergraph: Hypergraph): 114 | def transform_hye_to_new_indices(hye, mapping_): 115 | return tuple(mapping_[node] for node in hye) 116 | 117 | inc_arr = hl.incidence_matrices_all_orders(loaded_hypergraph) 118 | 119 | for order_to_test in range(1, loaded_hypergraph.max_order() + 1): 120 | 121 | inc, mapping = hl.incidence_matrix_by_order( 122 | loaded_hypergraph, order=order_to_test, return_mapping=True 123 | ) 124 | inverse_mapping = dict(zip(mapping.values(), mapping.keys())) 125 | 126 | subhypergraph_by_order = loaded_hypergraph.get_edges( 127 | order=order_to_test, subhypergraph=True, keep_isolated_nodes=False 128 | ) 129 | hye_list = [ 130 | transform_hye_to_new_indices(hye, inverse_mapping) 131 | for hye in subhypergraph_by_order.get_edges() 132 | ] 133 | incidence = np.zeros( 134 | (subhypergraph_by_order.num_nodes(), subhypergraph_by_order.num_edges()) 135 | ) 136 | weights_arr = subhypergraph_by_order.get_weights() 137 | for i, hye in enumerate(hye_list): 138 | incidence[hye, i] = weights_arr[i] 139 | 140 | assert np.all(inc_arr[order_to_test].todense() == incidence) 141 | -------------------------------------------------------------------------------- /tests/measures/test_sub_hypergraph_centrality.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from hypergraphx import Hypergraph 5 | from hypergraphx.measures.sub_hypergraph_centrality import subhypergraph_centrality 6 | 7 | 8 | # Fixture loaded_hypergraph defined inside the package-level conftest.py 9 | 10 | 11 | @pytest.fixture 12 | def hypergraph_with_sub_hc(loaded_hypergraph: Hypergraph): 13 | sub_hc = subhypergraph_centrality(loaded_hypergraph) 14 | return loaded_hypergraph, sub_hc 15 | 16 | 17 | def test_sub_hc_type(hypergraph_with_sub_hc): 18 | _, sub_hc = hypergraph_with_sub_hc 19 | assert isinstance(sub_hc, np.ndarray) 20 | 21 | 22 | def test_sub_hc_shape(hypergraph_with_sub_hc): 23 | hypergraph, sub_hc = hypergraph_with_sub_hc 24 | assert sub_hc.shape == (hypergraph.num_nodes(),) 25 | -------------------------------------------------------------------------------- /tests/utils/test_visits.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from hypergraphx import Hypergraph 4 | from hypergraphx.utils.visits import _bfs, _dfs 5 | 6 | 7 | def test_bfs_basic(): 8 | """Test BFS on a simple hypergraph with no max depth, order, or size constraints.""" 9 | edge_list = [ 10 | (1, 2, 3), # Hyperedge connecting 1, 2, 3 11 | (2, 4), # Hyperedge connecting 2, 4 12 | ] 13 | hg = Hypergraph(edge_list) 14 | result = _bfs(hg, start=1) 15 | expected = {1, 2, 3, 4} 16 | assert result == expected, f"Expected {expected}, got {result}" 17 | 18 | def test_bfs_with_max_depth(): 19 | """Test BFS with max depth constraint.""" 20 | edge_list = [ 21 | (1, 2, 3), # Hyperedge connecting 1, 2, 3 22 | (2, 4, 5), # Hyperedge connecting 2, 4, 5 23 | (5, 6), # Hyperedge connecting 5, 6 24 | ] 25 | hg = Hypergraph(edge_list) 26 | result = _bfs(hg, start=1, max_depth=1) 27 | expected = {1, 2, 3} # Only nodes within 1 depth from 1 should be visited 28 | assert result == expected, f"Expected {expected}, got {result}" 29 | 30 | def test_dfs_basic(): 31 | """Test DFS on a simple hypergraph with no max depth, order, or size constraints.""" 32 | edge_list = [ 33 | (1, 2, 3), # Hyperedge connecting 1, 2, 3 34 | (2, 4), # Hyperedge connecting 2, 4 35 | ] 36 | hg = Hypergraph(edge_list) 37 | result = _dfs(hg, start=1) 38 | expected = {1, 2, 3, 4} 39 | assert result == expected, f"Expected {expected}, got {result}" 40 | 41 | def test_dfs_with_max_depth(): 42 | """Test DFS with max depth constraint.""" 43 | edge_list = [ 44 | (1, 2, 3), # Hyperedge connecting 1, 2, 3 45 | (2, 4, 5), # Hyperedge connecting 2, 4, 5 46 | (5, 6), # Hyperedge connecting 5, 6 47 | ] 48 | hg = Hypergraph(edge_list) 49 | result = _dfs(hg, start=1, max_depth=1) 50 | expected = {1, 2, 3} # Only nodes within 1 depth from 1 should be visited 51 | assert result == expected, f"Expected {expected}, got {result}" 52 | 53 | def test_bfs_with_order_and_size(): 54 | """Test BFS with specific order and size constraints.""" 55 | edge_list = [ 56 | (1, 2), # Hyperedge of order 1 57 | (1, 3, 4), # Hyperedge of order 2 58 | (2, 5, 6), # Hyperedge of order 2 59 | (3, 7, 8), # Hyperedge of order 2 60 | ] 61 | hg = Hypergraph(edge_list) 62 | result = _bfs(hg, start=1, order=2) # Only consider edges of order 2 63 | expected = {1, 3, 4, 7, 8} # Nodes reachable through edges of order 2 64 | assert result == expected, f"Expected {expected}, got {result}" 65 | 66 | def test_dfs_with_order_and_size(): 67 | """Test DFS with specific order and size constraints.""" 68 | edge_list = [ 69 | (1, 2), # Hyperedge of order 1 70 | (1, 3, 4), # Hyperedge of order 2 71 | (2, 5, 6), # Hyperedge of order 2 72 | (3, 7, 8), # Hyperedge of order 2 73 | ] 74 | hg = Hypergraph(edge_list) 75 | result = _dfs(hg, start=1, order=2) # Only consider edges of order 2 76 | expected = {1, 3, 4, 7, 8} # Nodes reachable through edges of order 2 77 | assert result == expected, f"Expected {expected}, got {result}" 78 | 79 | def test_bfs_empty_hypergraph(): 80 | """Test BFS on an empty hypergraph, expecting ValueError due to missing start node.""" 81 | hg = Hypergraph(edge_list=[]) 82 | with pytest.raises(ValueError, match="Node 1 not in hypergraph."): 83 | _bfs(hg, start=1) 84 | 85 | def test_dfs_empty_hypergraph(): 86 | """Test DFS on an empty hypergraph, expecting ValueError due to missing start node.""" 87 | hg = Hypergraph(edge_list=[]) 88 | with pytest.raises(ValueError, match="Node 1 not in hypergraph."): 89 | _dfs(hg, start=1) 90 | -------------------------------------------------------------------------------- /tutorials/_example_data/social_contagion.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/tutorials/_example_data/social_contagion.npy -------------------------------------------------------------------------------- /tutorials/_example_data/temporal_correlations.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HGX-Team/hypergraphx/cad822b7c1c615b4bff485aefc882c421c9a746f/tutorials/_example_data/temporal_correlations.npy -------------------------------------------------------------------------------- /tutorials/activity_driven.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import sys\n", 10 | "from collections import Counter\n", 11 | "\n", 12 | "import numpy as np\n", 13 | "import matplotlib.pyplot as plt\n", 14 | "\n", 15 | "sys.path.append(\"..\")\n", 16 | "from hypergraphx.communities.hy_mmsbm.model import HyMMSBM\n", 17 | "from hypergraphx.core.temporal_hypergraph import TemporalHypergraph\n", 18 | "from hypergraphx.generation.hy_mmsbm_sampling import HyMMSBMSampler\n", 19 | "from hypergraphx.linalg.linalg import *\n", 20 | "from hypergraphx.dynamics.randwalk import *\n", 21 | "from hypergraphx.generation.activity_driven import *\n", 22 | "import random" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 2, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "def rnd_pwl(xmin, xmax, g, size=1):\n", 32 | " r = np.random.random(size=size)\n", 33 | " return (r*( xmax**(1.-g) - xmin**(1.-g) ) + xmin**(1.-g) )**(1./(1.-g))\n", 34 | "\n", 35 | "orders = [1,2,5]\n", 36 | "beta_act = 2.25\n", 37 | "eps = 0.001\n", 38 | "N = 100\n", 39 | "activities = rnd_pwl(eps, 1.0, beta_act, N) \n", 40 | "activities_per_order = {order: activities for order in orders}" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 3, 46 | "metadata": {}, 47 | "outputs": [], 48 | "source": [ 49 | "HG = HOADmodel(N, activities_per_order, time=100)" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 4, 55 | "metadata": {}, 56 | "outputs": [ 57 | { 58 | "data": { 59 | "text/plain": [ 60 | "array([0.00122684, 0.0030016 , 0.00115932, 0.00232638, 0.00123835,\n", 61 | " 0.00146535, 0.01942779, 0.00155625, 0.00158992, 0.00117872,\n", 62 | " 0.00175645, 0.00120718, 0.00166537, 0.0011417 , 0.00111237,\n", 63 | " 0.00224994, 0.00124012, 0.00108481, 0.00169755, 0.00108913,\n", 64 | " 0.00258711, 0.0018887 , 0.00139368, 0.00207586, 0.00112409,\n", 65 | " 0.00202954, 0.00135815, 0.00280731, 0.00119374, 0.00239654,\n", 66 | " 0.00145397, 0.00196627, 0.00517187, 0.0019846 , 0.00245724,\n", 67 | " 0.00273485, 0.00279709, 0.00210364, 0.00404598, 0.00395033,\n", 68 | " 0.00138353, 0.00288704, 0.0037446 , 0.0023175 , 0.00162702,\n", 69 | " 0.00100739, 0.00183573, 0.00129032, 0.00234852, 0.00165624,\n", 70 | " 0.00150109, 0.00169891, 0.00111026, 0.0017071 , 0.0079329 ,\n", 71 | " 0.00201479, 0.00122535, 0.00246889, 0.00207982, 0.00255218,\n", 72 | " 0.00500995, 0.00154157, 0.00337264, 0.00143736, 0.00138248,\n", 73 | " 0.00147987, 0.00121604, 0.00161813, 0.00101783, 0.00245326,\n", 74 | " 0.00224526, 0.0012563 , 0.00116481, 0.00209183, 0.00128031,\n", 75 | " 0.00226608, 0.00180236, 0.00105422, 0.00232507, 0.00604522,\n", 76 | " 0.00198912, 0.00190691, 0.0033083 , 0.00222755, 0.00118875,\n", 77 | " 0.00102142, 0.00129231, 0.00102529, 0.01145748, 0.00485772,\n", 78 | " 0.00252576, 0.00178571, 0.00263117, 0.00146426, 0.00157329,\n", 79 | " 0.00484214, 0.00192983, 0.00132683, 0.0013145 , 0.00407684])" 80 | ] 81 | }, 82 | "execution_count": 4, 83 | "metadata": {}, 84 | "output_type": "execute_result" 85 | } 86 | ], 87 | "source": [ 88 | "activities_per_order[1]" 89 | ] 90 | } 91 | ], 92 | "metadata": { 93 | "kernelspec": { 94 | "display_name": "hgx-installation", 95 | "language": "python", 96 | "name": "python3" 97 | }, 98 | "language_info": { 99 | "codemirror_mode": { 100 | "name": "ipython", 101 | "version": 3 102 | }, 103 | "file_extension": ".py", 104 | "mimetype": "text/x-python", 105 | "name": "python", 106 | "nbconvert_exporter": "python", 107 | "pygments_lexer": "ipython3", 108 | "version": "3.9.17" 109 | }, 110 | "orig_nbformat": 4 111 | }, 112 | "nbformat": 4, 113 | "nbformat_minor": 2 114 | } 115 | -------------------------------------------------------------------------------- /tutorials/directed_measures.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import hypergraphx as hgx" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 2, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "edges = [\n", 19 | " ((1,), (2,)), # Edge 1: Simple directed edge from 1 to 2\n", 20 | " ((2,), (1,)), # Edge 2: Simple directed edge from 2 to 1 (reciprocal with Edge 1)\n", 21 | " ((2, 3), (4,)), # Edge 2: Directed hyperedge with sources 2, 3 and target 4\n", 22 | " ((4,), (2, 3)), # Edge 3: Directed hyperedge from 4 to targets 2, 5\n", 23 | " ((5,), (1,)), # Edge 4: Directed edge from 5 to 1 (reciprocal with Edge 1)\n", 24 | " ((6,), (3, 4)), # Edge 5: Directed hyperedge from 6 to targets 3 and 4\n", 25 | " ((7,), (2, 6)), # Edge 6: Directed hyperedge from 7 to targets 2 and 6\n", 26 | " ((3, 8), (5,)), # Edge 7: Directed hyperedge with sources 3, 8 and target 5\n", 27 | " ((4, 5), (7, 8)) # Edge 8: Directed hyperedge with sources 4, 5 and targets 7, 8\n", 28 | "]" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 3, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "h = hgx.DirectedHypergraph(edges)" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 4, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "from hypergraphx.measures.directed import in_degree, out_degree, in_degree_sequence, out_degree_sequence" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 5, 52 | "metadata": {}, 53 | "outputs": [ 54 | { 55 | "name": "stdout", 56 | "output_type": "stream", 57 | "text": [ 58 | "In-degree of node 1: 1\n", 59 | "In-degree of node 2: 2\n", 60 | "Out-degree of node 1: 2\n", 61 | "Out-degree of node 2: 3\n" 62 | ] 63 | } 64 | ], 65 | "source": [ 66 | "print(\"In-degree of node 1:\", in_degree(h, 1))\n", 67 | "print(\"In-degree of node 2:\", in_degree(h, 2))\n", 68 | "print(\"Out-degree of node 1:\", out_degree(h, 1))\n", 69 | "print(\"Out-degree of node 2:\", out_degree(h, 2))" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": 6, 75 | "metadata": {}, 76 | "outputs": [ 77 | { 78 | "name": "stdout", 79 | "output_type": "stream", 80 | "text": [ 81 | "In-degree sequence: {1: 1, 2: 2, 3: 2, 4: 2, 5: 2, 6: 1, 7: 1, 8: 1}\n", 82 | "Out-degree sequence: {1: 2, 2: 3, 3: 2, 4: 2, 5: 1, 6: 1, 7: 1, 8: 1}\n" 83 | ] 84 | } 85 | ], 86 | "source": [ 87 | "print(\"In-degree sequence: \", in_degree_sequence(h))\n", 88 | "print(\"Out-degree sequence: \", out_degree_sequence(h))" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": 7, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "from hypergraphx.measures.directed import hyperedge_signature_vector" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 8, 103 | "metadata": {}, 104 | "outputs": [ 105 | { 106 | "name": "stdout", 107 | "output_type": "stream", 108 | "text": [ 109 | "Hyperedge signature vector: [3. 3. 0. 2. 1. 0. 0. 0. 0.]\n" 110 | ] 111 | } 112 | ], 113 | "source": [ 114 | "v = hyperedge_signature_vector(h)\n", 115 | "print(\"Hyperedge signature vector: \", v)" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 9, 121 | "metadata": {}, 122 | "outputs": [], 123 | "source": [ 124 | "from hypergraphx.measures.directed import exact_reciprocity, strong_reciprocity, weak_reciprocity" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": 10, 130 | "metadata": {}, 131 | "outputs": [ 132 | { 133 | "name": "stdout", 134 | "output_type": "stream", 135 | "text": [ 136 | "Exact reciprocity: {2: 0.6666666666666666, 3: 0.4, 4: 0.0}\n", 137 | "Strong reciprocity: {2: 0.6666666666666666, 3: 0.4, 4: 0.0}\n", 138 | "Weak reciprocity: {2: 0.6666666666666666, 3: 0.6, 4: 1.0}\n" 139 | ] 140 | } 141 | ], 142 | "source": [ 143 | "print(\"Exact reciprocity: \", exact_reciprocity(h, max_hyperedge_size=4))\n", 144 | "print(\"Strong reciprocity: \", strong_reciprocity(h, max_hyperedge_size=4))\n", 145 | "print(\"Weak reciprocity: \", weak_reciprocity(h, max_hyperedge_size=4))" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": 11, 151 | "metadata": {}, 152 | "outputs": [], 153 | "source": [ 154 | "from hypergraphx.motifs.directed_motifs import *" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": 12, 160 | "metadata": {}, 161 | "outputs": [ 162 | { 163 | "name": "stdout", 164 | "output_type": "stream", 165 | "text": [ 166 | "Computing observed motifs of order 3...\n" 167 | ] 168 | } 169 | ], 170 | "source": [ 171 | "m = compute_directed_motifs(h, order=3, runs_config_model=0)" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 13, 177 | "metadata": {}, 178 | "outputs": [ 179 | { 180 | "data": { 181 | "text/plain": [ 182 | "[((((1,), (2, 3)),), 2),\n", 183 | " ((((1,), (2, 3)), ((2, 3), (1,))), 1),\n", 184 | " ((((1, 2), (3,)),), 1)]" 185 | ] 186 | }, 187 | "execution_count": 13, 188 | "metadata": {}, 189 | "output_type": "execute_result" 190 | } 191 | ], 192 | "source": [ 193 | "m['observed']" 194 | ] 195 | } 196 | ], 197 | "metadata": { 198 | "kernelspec": { 199 | "display_name": "hgx-installation", 200 | "language": "python", 201 | "name": "python3" 202 | }, 203 | "language_info": { 204 | "codemirror_mode": { 205 | "name": "ipython", 206 | "version": 3 207 | }, 208 | "file_extension": ".py", 209 | "mimetype": "text/x-python", 210 | "name": "python", 211 | "nbconvert_exporter": "python", 212 | "pygments_lexer": "ipython3", 213 | "version": "3.9.17" 214 | } 215 | }, 216 | "nbformat": 4, 217 | "nbformat_minor": 2 218 | } 219 | --------------------------------------------------------------------------------