├── requirements ├── release.txt ├── tutorial.txt ├── test.txt ├── default.txt ├── developer.txt └── documentation.txt ├── hypercontagion ├── utils │ ├── __init__.py │ └── utilities.py ├── visualization │ ├── __init__.py │ └── animation.py ├── sim │ ├── __init__.py │ ├── functions.py │ ├── opinions.py │ └── epidemics.py ├── __init__.py └── exception.py ├── tutorials ├── animation.mp4 ├── tutorial_3_animations.ipynb ├── tutorial_2_opinion_formation_modeling.ipynb └── tutorial_1_epidemic_modeling.ipynb ├── .gitattributes ├── docs ├── source │ ├── _static │ │ └── custom.css │ ├── about.rst │ ├── api │ │ ├── utilities.rst │ │ ├── sim.rst │ │ ├── utils │ │ │ ├── hypercontagion.utils.utilities.rst │ │ │ └── hypercontagion.utils.decorators.rst │ │ └── sim │ │ │ ├── hypercontagion.sim.functions.rst │ │ │ ├── hypercontagion.sim.epidemics.rst │ │ │ └── hypercontagion.sim.opinions.rst │ ├── _templates │ │ └── autosummary │ │ │ ├── class.rst │ │ │ └── module.rst │ ├── index.rst │ └── conf.py ├── make.bat └── Makefile ├── CITATION.cff ├── MANIFEST.in ├── CHANGELOG.md ├── tests ├── sim │ ├── test_opinions.py │ ├── test_functions.py │ └── test_epidemics.py └── conftest.py ├── Makefile ├── .readthedocs.yaml ├── make.bat ├── setup.py ├── CONTRIBUTING.md ├── .github └── workflows │ └── test.yml ├── .gitignore ├── README.md ├── LICENSE.md └── CODE_OF_CONDUCT.md /requirements/release.txt: -------------------------------------------------------------------------------- 1 | twine>=3.4 2 | wheel>=0.36 -------------------------------------------------------------------------------- /requirements/tutorial.txt: -------------------------------------------------------------------------------- 1 | jupyter>=1.0 2 | matplotlib>=3.3 -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | pytest>=6.2 2 | pytest-cov>=2.12 3 | coverage>=6.0 -------------------------------------------------------------------------------- /requirements/default.txt: -------------------------------------------------------------------------------- 1 | xgi>=0.3 2 | numpy>=1.19 3 | networkx>=2.0 4 | celluloid>=0.2.0 -------------------------------------------------------------------------------- /hypercontagion/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from . import utilities 2 | from .utilities import * 3 | -------------------------------------------------------------------------------- /hypercontagion/visualization/__init__.py: -------------------------------------------------------------------------------- 1 | from . import animation 2 | from .animation import * 3 | -------------------------------------------------------------------------------- /requirements/developer.txt: -------------------------------------------------------------------------------- 1 | black[jupyter]>=24.3 2 | pre-commit>=2.12 3 | isort==5.10.1 4 | pylint>=2.10 5 | -------------------------------------------------------------------------------- /tutorials/animation.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwlandry/hypercontagion/HEAD/tutorials/animation.mp4 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | *.ipynb linguist-documentation 5 | -------------------------------------------------------------------------------- /requirements/documentation.txt: -------------------------------------------------------------------------------- 1 | sphinx~=4.0 2 | sphinx_copybutton 3 | sphinx-rtd-theme>=0.4.2 4 | numpydoc>=1.1 5 | pillow>=8.2 6 | matplotlib>=3.3 -------------------------------------------------------------------------------- /docs/source/_static/custom.css: -------------------------------------------------------------------------------- 1 | .wy-menu p { 2 | margin-block-start: 1em; 3 | margin-block-end: 0; 4 | padding-inline-start: 0.5em; 5 | } 6 | -------------------------------------------------------------------------------- /docs/source/about.rst: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | For some introductory tutorials, see the `Tutorials `_. -------------------------------------------------------------------------------- /hypercontagion/sim/__init__.py: -------------------------------------------------------------------------------- 1 | from . import epidemics, functions, opinions 2 | from .epidemics import * 3 | from .functions import * 4 | from .opinions import * 5 | -------------------------------------------------------------------------------- /docs/source/api/utilities.rst: -------------------------------------------------------------------------------- 1 | ######### 2 | utilities 3 | ######### 4 | 5 | .. rubric:: Modules 6 | 7 | .. autosummary:: 8 | :toctree: utils 9 | 10 | ~hypercontagion.utils.decorators 11 | ~hypercontagion.utils.utilities -------------------------------------------------------------------------------- /hypercontagion/__init__.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | 3 | from . import sim, utils, visualization 4 | from .sim import * 5 | from .utils import * 6 | from .visualization import * 7 | 8 | __version__ = pkg_resources.require("hypercontagion")[0].version 9 | -------------------------------------------------------------------------------- /hypercontagion/exception.py: -------------------------------------------------------------------------------- 1 | class HyperContagionException(Exception): 2 | """Base class for exceptions in HyperContagion.""" 3 | 4 | 5 | class HyperContagionError(HyperContagionException): 6 | """Exception for a serious error in HyperContagion""" 7 | -------------------------------------------------------------------------------- /docs/source/api/sim.rst: -------------------------------------------------------------------------------- 1 | ########### 2 | sim package 3 | ########### 4 | 5 | .. rubric:: Modules 6 | 7 | .. autosummary:: 8 | :toctree: sim 9 | 10 | ~hypercontagion.sim.epidemics 11 | ~hypercontagion.sim.opinions 12 | ~hypercontagion.sim.functions -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # YAML 1.2 2 | --- 3 | authors: 4 | - 5 | family-names: Landry 6 | given-names: Nicholas 7 | - 8 | family-names: Miller 9 | given-names: Joel 10 | 11 | cff-version: "1.1.0" 12 | license: "BSD-3" 13 | message: "If you use this software, please cite it using these metadata." 14 | title: hypercontagion 15 | version: "0.1.2" -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include setup.py 3 | include README.md 4 | include LICENSE.md 5 | include CITATION.cff 6 | include CONTRIBUTING.md 7 | include CODE_OF_CONDUCT.md 8 | 9 | recursive-include data *.txt 10 | recursive-include tutorials *.ipynb 11 | recursive-include logo *.pdf *.png *.svg 12 | recursive-include requirements *.txt *.md 13 | 14 | global-exclude *~ 15 | global-exclude *.pyc 16 | global-exclude .svn -------------------------------------------------------------------------------- /docs/source/api/utils/hypercontagion.utils.utilities.rst: -------------------------------------------------------------------------------- 1 | hypercontagion.utils.utilities 2 | ============================== 3 | 4 | .. currentmodule:: hypercontagion.utils.utilities 5 | 6 | .. automodule:: hypercontagion.utils.utilities 7 | 8 | .. rubric:: Functions 9 | 10 | .. autofunction:: _process_trans_SIR_ 11 | .. autofunction:: _process_rec_SIR_ 12 | .. autofunction:: _process_trans_SIS_ 13 | .. autofunction:: _process_rec_SIS_ 14 | -------------------------------------------------------------------------------- /docs/source/api/sim/hypercontagion.sim.functions.rst: -------------------------------------------------------------------------------- 1 | hypercontagion.sim.functions 2 | ============================ 3 | 4 | .. currentmodule:: hypercontagion.sim.functions 5 | 6 | .. automodule:: hypercontagion.sim.functions 7 | 8 | .. rubric:: Functions 9 | 10 | .. autofunction:: collective_contagion 11 | .. autofunction:: individual_contagion 12 | .. autofunction:: threshold 13 | .. autofunction:: majority_vote 14 | .. autofunction:: size_dependent -------------------------------------------------------------------------------- /docs/source/api/utils/hypercontagion.utils.decorators.rst: -------------------------------------------------------------------------------- 1 | hypercontagion.utils.decorators 2 | =============================== 3 | 4 | .. currentmodule:: hypercontagion.utils.decorators 5 | 6 | .. automodule:: hypercontagion.utils.decorators 7 | 8 | .. rubric:: Functions 9 | 10 | .. autofunction:: preserve_random_state 11 | .. autofunction:: random_state 12 | .. autofunction:: np_random_state 13 | .. autofunction:: py_random_state 14 | .. autofunction:: argmap -------------------------------------------------------------------------------- /docs/source/api/sim/hypercontagion.sim.epidemics.rst: -------------------------------------------------------------------------------- 1 | hypercontagion.sim.epidemics 2 | ============================ 3 | 4 | .. currentmodule:: hypercontagion.sim.epidemics 5 | 6 | .. automodule:: hypercontagion.sim.epidemics 7 | 8 | .. rubric:: Functions 9 | 10 | .. autofunction:: discrete_SIR 11 | .. autofunction:: discrete_SIS 12 | .. autofunction:: Gillespie_SIR 13 | .. autofunction:: Gillespie_SIS 14 | .. autofunction:: event_driven_SIR 15 | .. autofunction:: event_driven_SIS -------------------------------------------------------------------------------- /docs/source/api/sim/hypercontagion.sim.opinions.rst: -------------------------------------------------------------------------------- 1 | hypercontagion.sim.opinions 2 | =========================== 3 | 4 | .. currentmodule:: hypercontagion.sim.opinions 5 | 6 | .. automodule:: hypercontagion.sim.opinions 7 | 8 | .. rubric:: Functions 9 | 10 | .. autofunction:: simulate_random_group_continuous_state_1D 11 | .. autofunction:: simulate_random_node_and_group_discrete_state 12 | .. autofunction:: synchronous_update_continuous_state_1D 13 | .. autofunction:: hegselmann_krause 14 | .. autofunction:: deffuant_weisbuch 15 | .. autofunction:: discordance 16 | .. autofunction:: voter_model -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # v0.1.2 4 | * Added compatibility for XGI 0.5.3 [#2](https://github.com/nwlandry/hypercontagion/pull/2) (@nwlandry). 5 | * Sped up epidemic functions [#2](https://github.com/nwlandry/hypercontagion/pull/2) (@nwlandry). 6 | * Removed random state decorators (@nwlandry). 7 | 8 | # v0.1.1 9 | * Removed unnecessary files ([#1](https://github.com/nwlandry/hypercontagion/pull/1)). 10 | * Added a function to visualize contagion processes ([#1](https://github.com/nwlandry/hypercontagion/pull/1)). 11 | * Formatted notebooks with black and updated the requirements file to match ([#1](https://github.com/nwlandry/hypercontagion/pull/1)). 12 | 13 | Contributors: @nwlandry -------------------------------------------------------------------------------- /tests/sim/test_opinions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import hypercontagion as hc 4 | 5 | 6 | def test_discordance(): 7 | assert abs(hc.discordance(np.array([0, 1, 2]), np.array([1, 0, 0])) - 1 / 3) < 1e-6 8 | assert abs(hc.discordance(np.array([0, 1, 2]), np.array([0, 0, 0])) - 0) < 1e-6 9 | 10 | 11 | def test_deffuant_weisbuch(): 12 | assert 0 == 0 13 | 14 | 15 | def test_hegselmann_krause(): 16 | assert 0 == 0 17 | 18 | 19 | def simulate_random_group_continuous_state_1D(): 20 | assert 0 == 0 21 | 22 | 23 | def test_simulate_random_node_and_group_discrete_state(): 24 | assert 0 == 0 25 | 26 | 27 | def test_synchronous_update_continuous_state_1D(): 28 | assert 0 == 0 29 | -------------------------------------------------------------------------------- /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 = source 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 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.9" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/source/conf.py 17 | 18 | # Optionally declare the Python requirements required to build your docs 19 | python: 20 | install: 21 | - method: pip 22 | path: . 23 | extra_requirements: 24 | - documentation 25 | - method: setuptools 26 | path: . 27 | 28 | formats: 29 | - pdf 30 | - epub -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | :show-inheritance: 7 | :members: 8 | 9 | {% block attributes %} 10 | {%- if attributes %} 11 | .. rubric:: {{ _('Attributes') }} 12 | 13 | .. autosummary:: 14 | {% for item in attributes %} 15 | ~{{ name }}.{{ item }} 16 | {%- endfor %} 17 | {%- endif %} 18 | {% endblock %} 19 | 20 | {% block methods %} 21 | {%- if methods %} 22 | .. rubric:: {{ _('Methods') }} 23 | 24 | .. autosummary:: 25 | :nosignatures: 26 | {% for item in methods if item != '__init__' and item not in inherited_members %} 27 | ~{{ name }}.{{ item }} 28 | {%- endfor %} 29 | {%- endif %} 30 | {%- endblock %} 31 | 32 | -------------------------------------------------------------------------------- /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=docs/source 11 | set BUILDDIR=docs/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 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def func_args_1(): 6 | return {"node": 1, "status": {1: "S", 2: "S", 3: "I"}, "edge": [1, 2, 3]} 7 | 8 | 9 | @pytest.fixture 10 | def func_args_2(): 11 | return {"node": 1, "status": {1: "S", 2: "I", 3: "I"}, "edge": [1, 2, 3]} 12 | 13 | 14 | @pytest.fixture 15 | def func_args_3(): 16 | return {"node": 2, "status": {1: "S", 2: "I", 3: "I", 4: "R"}, "edge": [1, 2, 3, 4]} 17 | 18 | 19 | @pytest.fixture 20 | def func_args_4(): 21 | return { 22 | "node": 5, 23 | "status": {1: "S", 2: "I", 3: "I", 4: "R", 5: "S"}, 24 | "edge": [1, 2, 3, 4, 5], 25 | } 26 | 27 | 28 | @pytest.fixture 29 | def func_args_5(): 30 | return { 31 | "node": 3, 32 | "status": {1: "I", 2: "I", 3: "S", 4: "I", 5: "I"}, 33 | "edge": [1, 2, 3, 4, 5], 34 | } 35 | 36 | 37 | @pytest.fixture 38 | def func_args_6(): 39 | return { 40 | "node": 3, 41 | "status": {1: "S", 2: "S", 3: "S", 4: "R", 5: "R"}, 42 | "edge": [1, 2, 3, 4, 5], 43 | } 44 | 45 | 46 | @pytest.fixture 47 | def edgelist1(): 48 | return [[1, 2, 3], [4], [5, 6], [6, 7, 8]] 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import setuptools 4 | from setuptools import setup 5 | 6 | __version__ = "0.1.2" 7 | 8 | if sys.version_info < (3, 8): 9 | sys.exit("hypercontagion requires Python 3.8 or later.") 10 | 11 | name = "hypercontagion" 12 | 13 | version = __version__ 14 | 15 | authors = "Nicholas Landry" 16 | 17 | author_email = "nicholas.landry@colorado.edu" 18 | 19 | url = "https://github.com/nwlandry/hypercontagion" 20 | 21 | description = "HyperContagion is a Python library for the simulation of contagion on complex systems with group (higher-order) interactions." 22 | 23 | 24 | def parse_requirements_file(filename): 25 | with open(filename) as fid: 26 | requires = [l.strip() for l in fid.readlines() if not l.startswith("#")] 27 | return requires 28 | 29 | 30 | extras_require = { 31 | dep: parse_requirements_file("requirements/" + dep + ".txt") 32 | for dep in ["developer", "documentation", "release", "test", "tutorial"] 33 | } 34 | 35 | install_requires = parse_requirements_file("requirements/default.txt") 36 | 37 | license = "3-Clause BSD license" 38 | 39 | setup( 40 | name=name, 41 | packages=setuptools.find_packages(), 42 | version=version, 43 | author=authors, 44 | author_email=author_email, 45 | url=url, 46 | description=description, 47 | install_requires=install_requires, 48 | extras_require=extras_require, 49 | ) 50 | -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/module.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline }} 2 | 3 | .. currentmodule:: {{ fullname }} 4 | 5 | .. automodule:: {{ fullname }} 6 | 7 | {%- block attributes %} 8 | {%- if attributes %} 9 | .. rubric:: Module Attributes 10 | 11 | .. autosummary:: 12 | {%- for item in attributes %} 13 | {{ item }} 14 | {%- endfor %} 15 | {%- endif %} 16 | {%- endblock %} 17 | 18 | {%- block classes %} 19 | {%- if classes %} 20 | 21 | .. rubric:: Classes 22 | 23 | .. autosummary:: 24 | :toctree: . 25 | :nosignatures: 26 | {% for class in classes %} 27 | {{ class }} 28 | {% endfor %} 29 | {%- endif %} 30 | {%- endblock %} 31 | 32 | {%- block functions %} 33 | {% if functions %} 34 | .. rubric:: {{ _('Functions') }} 35 | {% for item in functions %} 36 | .. autofunction:: {{ item }} 37 | {%- endfor %} 38 | {%- endif %} 39 | {%- endblock %} 40 | 41 | {% block exceptions %} 42 | {% if exceptions %} 43 | .. rubric:: {{ _('Exceptions') }} 44 | 45 | .. autosummary:: 46 | {% for item in exceptions %} 47 | {{ item }} 48 | {%- endfor %} 49 | {% endif %} 50 | {% endblock %} 51 | 52 | {% block modules %} 53 | {% if modules %} 54 | .. rubric:: Modules 55 | 56 | .. autosummary:: 57 | :toctree: 58 | :recursive: 59 | {% for item in modules %} 60 | {{ item }} 61 | {%- endfor %} 62 | {% endif %} 63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via an [issue](../../issues/new). Feature additions, bug fixes, etc. should all be addressed with a pull request (PR). 4 | 5 | Please note we have a [code of conduct](/CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 6 | 7 | ## Pull Request process 8 | 9 | 1. Download the dependencies in the developer [requirements file](/requirements/developer.txt). 10 | 2. [Optional, but STRONGLY preferred] Label commits according to [Conventional Commits](https://www.conventionalcommits.org) style. 11 | 3. [Optional, but STRONGLY preferred] Add unit tests for features being added or bugs being fixed. 12 | 4. [Optional, but STRONGLY preferred] Include any new method/function in the corresponding docs file. 13 | 5. Run `pytest` to verify all unit tests pass. 14 | 6. [Optional, but STRONGLY preferred] Run `pylint hypercontagion/ --disable all --enable W0611` and remove any unnecessary dependencies. 15 | 7. [Optional, but STRONGLY preferred] Run `isort .` to sort any new import statements. 16 | 8. [Optional, but STRONGLY preferred] Run `black .` for consistent styling. 17 | 9. Update the "Current Version" section of CHANGELOG.md with overview of changes to the interface and add the usernames of all contributors. 18 | 10. Submit Pull Request with a list of changes, links to issues that it addresses (if applicable) 19 | 11. You may merge the Pull Request in once you have the sign-off of at least one other developer, or if you do not have permission to do that, you may request the reviewer to merge it for you. 20 | 21 | ## New Version process 22 | 1. Make sure that the Github Actions workflow runs without any errors. 23 | 2. Increase the version number in [setup.py](setup.py), [conf.py](docs/source/conf.py), and [CITATION.cff](CITATION.cff) to the new version agreed upon by the core developers. The versioning scheme we use is [SemVer](http://semver.org/). 24 | 3. Change the "Current Version" heading to the version number and put a new blank "Current Version" heading above this. 25 | 4. Upload to PyPI. 26 | 27 | ## Attribution 28 | 29 | This Contributing Statement is adapted from [this template by @PurpleBooth](https://gist.github.com/PurpleBooth/b24679402957c63ec426). -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | tutorials: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: ['3.8', '3.9', '3.10', '3.11'] 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Install packages 26 | run: | 27 | sudo apt-get install -y rename 28 | python -m pip install --upgrade pip wheel setuptools 29 | python -m pip install -r requirements/default.txt -r requirements/test.txt -r requirements/tutorial.txt 30 | python -m pip install . 31 | python -m pip list 32 | - name: Prepare tutorial notebooks 33 | run: | 34 | # go into tutorial location 35 | cd tutorials/ 36 | # convert each notebook into a python file 37 | jupyter nbconvert --to script tutorial*ipynb 38 | # rename the files so they are found by pytest: add 'test' at the beggining 39 | rename 's/(.*)\.py/test $1.py/' * 40 | # rename the files so they are found by pytest: change spaces to underscores 41 | rename 's/ /_/g' test* 42 | # modify each file by adding one level of indentation to each line 43 | sed -i 's/^/ /' test*.py 44 | # modify each file by adding 'def test_func():' at the top 45 | sed -i '1s;^;def test_func():\n;' test*.py 46 | - name: Test tutorial notebooks 47 | run: | 48 | # run pytest ONLY on the tutorials 49 | cd tutorials/ 50 | pytest --color=yes 51 | 52 | 53 | pytest: 54 | runs-on: ${{ matrix.os }}-latest 55 | strategy: 56 | matrix: 57 | os: [ubuntu, macos, windows] 58 | python-version: ['3.8', '3.9', '3.10', '3.11'] 59 | steps: 60 | - uses: actions/checkout@v2 61 | - name: Set up Python ${{ matrix.python-version }} 62 | uses: actions/setup-python@v2 63 | with: 64 | python-version: ${{ matrix.python-version }} 65 | 66 | - name: Install packages 67 | run: | 68 | python -m pip install --upgrade pip wheel setuptools 69 | python -m pip install -r requirements/default.txt -r requirements/test.txt -r requirements/documentation.txt 70 | python -m pip install . 71 | python -m pip list 72 | 73 | - name: Test HyperContagion 74 | run: | 75 | 76 | pytest --color=yes -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | .ipynb_checkpoints/ 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # pytype static type analyzer 136 | .pytype/ 137 | 138 | # Cython debug symbols 139 | cython_debug/ 140 | 141 | Figures/ 142 | test.* 143 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HyperContagion 2 | 3 | HyperContagion is a Python package for the simulation and visualization of contagion processes on complex systems with group (higher-order) interactions. 4 | 5 | * [**Source**](../../) 6 | * [**Bug reports**](../../issues) 7 | * [**GitHub Discussions**](../../discussions) 8 | 9 | ## Table of Contents: 10 | - [Installation](#installation) 11 | - [Getting Started](#getting-started) 12 | - [Documentation](#documentation) 13 | - [Contributing](#contributing) 14 | - [How to Cite](#how-to-cite) 15 | - [Code of Conduct](#code-of-conduct) 16 | - [License](#license) 17 | - [Funding](#funding) 18 | - [Other Resources](#other-resources) 19 | 20 | ## Installation 21 | HyperContagion runs on Python 3.8 or higher. 22 | 23 | To install the latest version of HyperContagion, run the following command: 24 | ```sh 25 | pip install hypercontagion 26 | ``` 27 | 28 | To install this package locally: 29 | * Clone this repository 30 | * Navigate to the folder on your local machine 31 | * Run the following command: 32 | ```sh 33 | pip install -e .["all"] 34 | ``` 35 | * If that command does not work, you may try the following instead 36 | ````zsh 37 | pip install -e .\[all\] 38 | ```` 39 | 40 | ## Getting Started 41 | 42 | To get started, take a look at the [demos](/demos/) illustrating the library's basic functionality. 43 | 44 | ## Documentation 45 | 46 | ## Contributing 47 | Contributions are always welcome. Please report any bugs that you find [here](../../issues). Or, even better, fork the repository on [GitHub](../../) and create a pull request (PR). We welcome all changes, big or small, and we will help you make the PR if you are new to `git` (just ask on the issue and/or see our [contributing guidelines](CONTRIBUTING.md)). 48 | 49 | ## How to Cite 50 | 51 | We acknowledge the importance of good software to support research, and we note 52 | that research becomes more valuable when it is communicated effectively. To 53 | demonstrate the value of HyperContagion, we ask that you cite HyperContagion in your work. 54 | Currently, the best way to cite HyperContagion is to go to our 55 | [repository page](../../) (if you haven't already) and 56 | click the "cite this repository" button on the right sidebar. This will generate 57 | a citation in your preferred format, and will also integrate well with citation managers. 58 | 59 | ## Code of Conduct 60 | 61 | ## License 62 | Released under the 3-Clause BSD license (see [`LICENSE.md`](LICENSE.md)) 63 | 64 | Copyright (C) 2021 HyperContagion Developers 65 | 66 | Nicholas Landry 67 | 68 | The HyperContagion library has copied or modified code from the EoN and Epipack libraries, the licenses of which can be found in our [license file](LICENSE.md) 69 | 70 | ## Funding 71 | The HyperContagion package has been supported by NSF Grant 2121905, ["HNDS-I: Using Hypergraphs to Study Spreading Processes in Complex Social Networks"](https://www.nsf.gov/awardsearch/showAward?AWD_ID=2121905). -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. toctree:: 3 | :maxdepth: 2 4 | :caption: Home 5 | :hidden: 6 | 7 | About 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Tutorials 12 | :hidden: 13 | 14 | See on GitHub 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | :caption: API Reference 19 | :hidden: 20 | 21 | Simulation 22 | Utilities 23 | 24 | About 25 | ===== 26 | 27 | The `HyperContagion `_ 28 | library provides algorithms for simulating and visualizing contagion processes on complex systems 29 | with group (higher-order) interactions. 30 | 31 | - Repository: https://github.com/nwlandry/hypercontagion 32 | - PyPI: https://pypi.org/project/hypercontagion/ 33 | - Documentation: https://hypercontagion.readthedocs.io/ 34 | 35 | 36 | Installation 37 | ============ 38 | 39 | To install and use HyperContagion as an end user, execute 40 | 41 | .. code:: bash 42 | 43 | pip install hypercontagion 44 | 45 | To install for development purposes, first clone the repository and then execute 46 | 47 | .. code:: bash 48 | 49 | pip install -e .['all'] 50 | 51 | If that command does not work, you may try the following instead 52 | 53 | .. code:: zsh 54 | 55 | pip install -e .\[all\] 56 | 57 | HyperContagion was developed and tested for Python 8-3.11 on Mac OS, Windows, and Ubuntu. 58 | 59 | 60 | Academic References 61 | =================== 62 | 63 | * `The Why, How, and When of Representations for Complex Systems 64 | `_, Leo Torres, Ann S. Blevins, Danielle Bassett, 65 | and Tina Eliassi-Rad. 66 | 67 | * `Networks beyond pairwise interactions: Structure and dynamics 68 | `_, Federico Battiston, Giulia 69 | Cencetti, Iacopo Iacopini, Vito Latora, Maxime Lucas, Alice Patania, Jean-Gabriel 70 | Young, and Giovanni Petri. 71 | 72 | * `What are higher-order networks? `_, Christian Bick, 73 | Elizabeth Gross, Heather A. Harrington, Michael T. Schaub. 74 | 75 | 76 | Contributing 77 | ============ 78 | 79 | If you want to contribute to this project, please make sure to read the 80 | `code of conduct 81 | `_ 82 | and the `contributing guidelines 83 | `_. 84 | 85 | The best way to contribute to HyperContagion is by submitting a bug or request a new feature by 86 | opening a `new issue `_. 87 | 88 | To get more actively involved, you are invited to browse the `issues page 89 | `_ and choose one that you can 90 | work on. The core developers will be happy to help you understand the codebase and any 91 | other doubts you may have while working on your contribution. 92 | 93 | Contributors 94 | ============ 95 | 96 | The core HyperContagion team members: 97 | 98 | * Nicholas Landry 99 | * Joel Miller 100 | 101 | 102 | License 103 | ======= 104 | 105 | This project is licensed under the `BSD 3-Clause License 106 | `_. 107 | 108 | Copyright (C) 2021 HyperContagion Developers 109 | -------------------------------------------------------------------------------- /hypercontagion/sim/functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides predefined contagion functions for use in the hypercontagion library. 3 | """ 4 | 5 | import random 6 | 7 | 8 | # built-in functions 9 | def collective_contagion(node, status, edge): 10 | """Collective contagion function. 11 | 12 | Parameters 13 | ---------- 14 | node : hashable 15 | node ID 16 | status : dict 17 | keys are node IDs and values are their statuses. 18 | edge : iterable 19 | hyperedge 20 | 21 | Returns 22 | ------- 23 | int 24 | 0 if no transmission can occur, 1 if it can. 25 | """ 26 | for i in set(edge).difference({node}): 27 | if status[i] != "I": 28 | return 0 29 | return 1 30 | 31 | 32 | def individual_contagion(node, status, edge): 33 | """Individual contagion function. 34 | 35 | Parameters 36 | ---------- 37 | node : hashable 38 | node ID 39 | status : dict 40 | keys are node IDs and values are their statuses. 41 | edge : iterable 42 | hyperedge 43 | 44 | Returns 45 | ------- 46 | int 47 | 0 if no transmission can occur, 1 if it can. 48 | """ 49 | for i in set(edge).difference({node}): 50 | if status[i] == "I": 51 | return 1 52 | return 0 53 | 54 | 55 | def threshold(node, status, edge, threshold=0.5): 56 | """Threshold contagion process. 57 | 58 | Contagion may spread if greater than a specified fraction 59 | of hyperedge neighbors are infected. 60 | 61 | Parameters 62 | ---------- 63 | node : hashable 64 | node ID 65 | status : dict 66 | keys are node IDs and values are their statuses. 67 | edge : iterable of hashables 68 | nodes in the hyperedge 69 | threshold : float, default: 0.5 70 | the critical fraction of hyperedge neighbors above 71 | which contagion spreads. 72 | 73 | Returns 74 | ------- 75 | int 76 | 0 if no transmission can occur, 1 if it can. 77 | """ 78 | neighbors = set(edge).difference({node}) 79 | try: 80 | c = sum([status[i] == "I" for i in neighbors]) / len(neighbors) 81 | except: 82 | c = 0 83 | 84 | if c < threshold: 85 | return 0 86 | elif c >= threshold: 87 | return 1 88 | 89 | 90 | def majority_vote(node, status, edge): 91 | """Majority vote contagion process. 92 | 93 | Contagion may spread if the majority of a node's 94 | hyperedge neighbors are infected. If it's a tie, 95 | the result is random. 96 | 97 | Parameters 98 | ---------- 99 | node : hashable 100 | node ID 101 | status : dict 102 | keys are node IDs and values are their statuses. 103 | edge : iterable of hashables 104 | nodes in the hyperedge 105 | 106 | Returns 107 | ------- 108 | int 109 | 0 if no transmission can occur, 1 if it can. 110 | """ 111 | neighbors = set(edge).difference({node}) 112 | try: 113 | c = sum([status[i] == "I" for i in neighbors]) / len(neighbors) 114 | except: 115 | c = 0 116 | if c < 0.5: 117 | return 0 118 | elif c > 0.5: 119 | return 1 120 | else: 121 | return random.choice([0, 1]) 122 | 123 | 124 | def size_dependent(node, status, edge): 125 | 126 | return sum([status[i] == "I" for i in set(edge).difference({node})]) 127 | -------------------------------------------------------------------------------- /tutorials/tutorial_3_animations.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import xgi\n", 10 | "import hypercontagion as hc\n", 11 | "import matplotlib.pyplot as plt\n", 12 | "import time\n", 13 | "import numpy as np\n", 14 | "import random\n", 15 | "import networkx as nx\n", 16 | "from IPython.display import HTML\n", 17 | "import matplotlib.animation as animation" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "H = xgi.load_xgi_data(\"diseasome\")\n", 27 | "H.cleanup()" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "xgi.unique_edge_sizes(H)" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "gamma = 0.05\n", 46 | "beta = 0.05\n", 47 | "tau = {i: beta for i in xgi.unique_edge_sizes(H)}\n", 48 | "rho = 0.1" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "transition_events = hc.discrete_SIR(\n", 58 | " H, tau, gamma, tmin=0, dt=1, rho=rho, return_event_data=True\n", 59 | ")" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "pos = xgi.pairwise_spring_layout(H)" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "metadata": {}, 75 | "outputs": [], 76 | "source": [ 77 | "S_color = \"white\"\n", 78 | "I_color = 'firebrick'\n", 79 | "R_color = 'steelblue'\n", 80 | "\n", 81 | "node_colors = {\"S\": S_color, \"I\": I_color, \"R\": R_color}\n", 82 | "edge_colors = {\"S\": S_color, \"I\": I_color, \"R\": R_color, \"OFF\": \"grey\"}\n", 83 | "fps = 1\n", 84 | "fig = plt.figure(figsize=(10, 10))\n", 85 | "anim = hc.contagion_animation(\n", 86 | " fig, H, transition_events, pos, node_colors, edge_colors, fps=fps, node_size=10, dyad_lw=1.5\n", 87 | ")\n", 88 | "# FFwriter = animation.FFMpegWriter(fps=fps)\n", 89 | "# anim.save('animation.mp4', writer = FFwriter)" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": null, 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "HTML(anim.to_jshtml())" 99 | ] 100 | } 101 | ], 102 | "metadata": { 103 | "kernelspec": { 104 | "display_name": "Python 3.10.4 ('hypergraph')", 105 | "language": "python", 106 | "name": "python3" 107 | }, 108 | "language_info": { 109 | "codemirror_mode": { 110 | "name": "ipython", 111 | "version": 3 112 | }, 113 | "file_extension": ".py", 114 | "mimetype": "text/x-python", 115 | "name": "python", 116 | "nbconvert_exporter": "python", 117 | "pygments_lexer": "ipython3", 118 | "version": "3.10.0" 119 | }, 120 | "orig_nbformat": 4, 121 | "vscode": { 122 | "interpreter": { 123 | "hash": "fdeb83b6e5b2333358b6ba79181fac315f1a722b4574d7079c134c9ae27f7c53" 124 | } 125 | } 126 | }, 127 | "nbformat": 4, 128 | "nbformat_minor": 2 129 | } 130 | -------------------------------------------------------------------------------- /hypercontagion/visualization/animation.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | import matplotlib.pyplot as plt 4 | import xgi 5 | from celluloid import Camera 6 | 7 | __all__ = ["contagion_animation"] 8 | 9 | 10 | def contagion_animation( 11 | fig, H, transition_events, pos, node_colors, edge_colors, dt=1, fps=1, **args 12 | ): 13 | """Generate an animation object of contagion process. 14 | 15 | Parameters 16 | ---------- 17 | fig : figure handle 18 | The figure to plot onto 19 | H : xgi.Hypergraph 20 | The hypergraph on which the simulation occurs 21 | transition_events : list of dict 22 | The output of the epidemic simulation functions with `return_event_data=True` 23 | pos : dict of list 24 | a dict with node IDs as keys and [x, y] coordinates as values 25 | node_colors : dict of str or tuple 26 | a dict with state values as keys and colors as values. 27 | edge_colors : dict of str or tuple 28 | a dict with state values as keys and colors as values. 29 | dt : float > 0, default: 1 30 | the timestep at which to take snapshots of the system states 31 | fps : int, default: 1 32 | frames per second in the animation. 33 | **args : 34 | optional xgi draw args 35 | 36 | Returns 37 | ------- 38 | animation object 39 | The resulting animation 40 | """ 41 | 42 | node_state = defaultdict(lambda: "S") 43 | 44 | camera = Camera(fig) 45 | 46 | event_interval_list = get_events_in_equal_time_intervals(transition_events, dt) 47 | for interval_time in event_interval_list: 48 | events = event_interval_list[interval_time] 49 | 50 | edge_state = defaultdict(lambda: "OFF") 51 | 52 | # update edge and node states 53 | for event in events: 54 | status = event["new_state"] 55 | source = event["source"] 56 | target = event["target"] 57 | if source is not None: 58 | edge_state[source] = status 59 | # update node states 60 | node_state[target] = status 61 | 62 | node_fc = {n: node_colors[node_state[n]] for n in H.nodes} 63 | dyad_color = { 64 | e: edge_colors[edge_state[e]] for e in H.edges.filterby("size", 2) 65 | } 66 | edge_fc = { 67 | e: edge_colors[edge_state[e]] for e in H.edges.filterby("size", 2, "geq") 68 | } 69 | 70 | # draw hypergraph 71 | xgi.draw( 72 | H, pos=pos, node_fc=node_fc, edge_fc=edge_fc, dyad_color=dyad_color, **args 73 | ) 74 | plt.tight_layout() 75 | camera.snap() 76 | 77 | return camera.animate(interval=1000 / fps) 78 | 79 | 80 | def get_events_in_equal_time_intervals(transition_events, dt): 81 | """Converts an event stream into events per time interval. 82 | 83 | Parameters 84 | ---------- 85 | transition_events : list of dict 86 | output of epidemic simulations with `return_event_data=True` 87 | dt : float > 0 88 | the time interval over which to aggregate events. 89 | 90 | Returns 91 | ------- 92 | dict of lists of dicts 93 | the key is the start time of the time interval and 94 | the values are the list of events 95 | """ 96 | tmin = transition_events[0]["time"] 97 | 98 | new_events = defaultdict(list) 99 | 100 | t = tmin 101 | for event in transition_events: 102 | if event["time"] < t + dt: 103 | new_events[t].append(event) 104 | else: 105 | t += dt 106 | new_events[t].append(event) 107 | 108 | return new_events 109 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | HyperContagion is distributed with the 3-clause BSD license. 2 | 3 | Copyright (C) 2021, HyperContagion Developers 4 | 5 | Nicholas Landry 6 | 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are 11 | met: 12 | 13 | * Redistributions of source code must retain the above copyright 14 | notice, this list of conditions and the following disclaimer. 15 | 16 | * Redistributions in binary form must reproduce the above 17 | copyright notice, this list of conditions and the following 18 | disclaimer in the documentation and/or other materials provided 19 | with the distribution. 20 | 21 | * Neither the name of the NetworkX Developers nor the names of its 22 | contributors may be used to endorse or promote products derived 23 | from this software without specific prior written permission. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 27 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 28 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 29 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 30 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 31 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 32 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 33 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 34 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | 37 | 38 | ------------------------------------------------------------------ 39 | EoN 40 | 41 | Copyright (c) 2016 Joel C Miller 42 | 43 | Permission is hereby granted, free of charge, to any person obtaining a copy 44 | of this software and associated documentation files (the "Software"), to deal 45 | in the Software without restriction, including without limitation the rights 46 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 47 | copies of the Software, and to permit persons to whom the Software is 48 | furnished to do so, subject to the following conditions: 49 | 50 | The above copyright notice and this permission notice shall be included in all 51 | copies or substantial portions of the Software. 52 | 53 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 54 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 55 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 56 | AUTHORS OR COPYRIGHT HOLDERS OR SPRINGER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 57 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 58 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 59 | SOFTWARE. 60 | 61 | --------------------------------------------------------------------------------- 62 | epipack 63 | 64 | Copyright 2020, Benjamin F. Maier 65 | 66 | Permission is hereby granted, free of charge, to any person obtaining a copy of 67 | this software and associated documentation files (the "Software"), to deal in 68 | the Software without restriction, including without limitation the rights to 69 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 70 | of the Software, and to permit persons to whom the Software is furnished to do 71 | so, subject to the following conditions: 72 | 73 | The above copyright notice and this permission notice shall be included in all 74 | copies or substantial portions of the Software. 75 | 76 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 77 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 78 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 79 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 80 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 81 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 82 | SOFTWARE. 83 | 84 | -------------------------------------------------------------------------------- /tests/sim/test_functions.py: -------------------------------------------------------------------------------- 1 | from hypercontagion import ( 2 | collective_contagion, 3 | individual_contagion, 4 | majority_vote, 5 | size_dependent, 6 | threshold, 7 | ) 8 | 9 | 10 | def test_collective_contagion( 11 | func_args_1, func_args_2, func_args_3, func_args_4, func_args_5 12 | ): 13 | assert ( 14 | collective_contagion( 15 | func_args_1["node"], func_args_1["status"], func_args_1["edge"] 16 | ) 17 | == 0 18 | ) 19 | assert ( 20 | collective_contagion( 21 | func_args_2["node"], func_args_2["status"], func_args_2["edge"] 22 | ) 23 | == 1 24 | ) 25 | assert ( 26 | collective_contagion( 27 | func_args_3["node"], func_args_3["status"], func_args_3["edge"] 28 | ) 29 | == 0 30 | ) 31 | assert ( 32 | collective_contagion( 33 | func_args_4["node"], func_args_4["status"], func_args_4["edge"] 34 | ) 35 | == 0 36 | ) 37 | assert ( 38 | collective_contagion( 39 | func_args_5["node"], func_args_5["status"], func_args_5["edge"] 40 | ) 41 | == 1 42 | ) 43 | 44 | 45 | def test_individual_contagion( 46 | func_args_1, func_args_2, func_args_3, func_args_4, func_args_5, func_args_6 47 | ): 48 | assert ( 49 | individual_contagion( 50 | func_args_1["node"], func_args_1["status"], func_args_1["edge"] 51 | ) 52 | == 1 53 | ) 54 | assert ( 55 | individual_contagion( 56 | func_args_2["node"], func_args_2["status"], func_args_2["edge"] 57 | ) 58 | == 1 59 | ) 60 | assert ( 61 | individual_contagion( 62 | func_args_3["node"], func_args_3["status"], func_args_3["edge"] 63 | ) 64 | == 1 65 | ) 66 | assert ( 67 | individual_contagion( 68 | func_args_4["node"], func_args_4["status"], func_args_4["edge"] 69 | ) 70 | == 1 71 | ) 72 | assert ( 73 | individual_contagion( 74 | func_args_6["node"], func_args_6["status"], func_args_6["edge"] 75 | ) 76 | == 0 77 | ) 78 | 79 | 80 | def test_threshold(func_args_1, func_args_2, func_args_3, func_args_4): 81 | assert ( 82 | threshold(func_args_1["node"], func_args_1["status"], func_args_1["edge"]) == 1 83 | ) 84 | assert ( 85 | threshold( 86 | func_args_1["node"], 87 | func_args_1["status"], 88 | func_args_1["edge"], 89 | threshold=0.51, 90 | ) 91 | == 0 92 | ) 93 | assert ( 94 | threshold(func_args_2["node"], func_args_2["status"], func_args_2["edge"]) == 1 95 | ) 96 | assert ( 97 | threshold(func_args_3["node"], func_args_3["status"], func_args_3["edge"]) == 0 98 | ) 99 | assert ( 100 | threshold( 101 | func_args_3["node"], 102 | func_args_3["status"], 103 | func_args_3["edge"], 104 | threshold=0.3, 105 | ) 106 | == 1 107 | ) 108 | assert ( 109 | threshold(func_args_4["node"], func_args_4["status"], func_args_4["edge"]) == 1 110 | ) 111 | assert ( 112 | threshold( 113 | func_args_4["node"], 114 | func_args_4["status"], 115 | func_args_4["edge"], 116 | threshold=0.51, 117 | ) 118 | == 0 119 | ) 120 | 121 | 122 | def test_majority_vote(func_args_1, func_args_2, func_args_3, func_args_4, func_args_5): 123 | assert majority_vote( 124 | func_args_1["node"], func_args_1["status"], func_args_1["edge"] 125 | ) in {0, 1} 126 | assert ( 127 | majority_vote(func_args_2["node"], func_args_2["status"], func_args_2["edge"]) 128 | == 1 129 | ) 130 | assert ( 131 | majority_vote(func_args_3["node"], func_args_3["status"], func_args_3["edge"]) 132 | == 0 133 | ) 134 | assert majority_vote( 135 | func_args_4["node"], func_args_4["status"], func_args_4["edge"] 136 | ) in {0, 1} 137 | assert ( 138 | majority_vote(func_args_5["node"], func_args_5["status"], func_args_5["edge"]) 139 | == 1 140 | ) 141 | 142 | 143 | def test_size_dependent( 144 | func_args_1, func_args_2, func_args_3, func_args_4, func_args_5 145 | ): 146 | assert ( 147 | size_dependent(func_args_1["node"], func_args_1["status"], func_args_1["edge"]) 148 | == 1 149 | ) 150 | assert ( 151 | size_dependent(func_args_2["node"], func_args_2["status"], func_args_2["edge"]) 152 | == 2 153 | ) 154 | assert ( 155 | size_dependent(func_args_3["node"], func_args_3["status"], func_args_3["edge"]) 156 | == 1 157 | ) 158 | assert ( 159 | size_dependent(func_args_4["node"], func_args_4["status"], func_args_4["edge"]) 160 | == 2 161 | ) 162 | assert ( 163 | size_dependent(func_args_5["node"], func_args_5["status"], func_args_5["edge"]) 164 | == 4 165 | ) 166 | -------------------------------------------------------------------------------- /tests/sim/test_epidemics.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import xgi 3 | 4 | import hypercontagion as hc 5 | from hypercontagion.sim.functions import threshold 6 | 7 | 8 | def test_discrete_SIR(edgelist1): 9 | H = xgi.Hypergraph(edgelist1) 10 | 11 | tmin = 10 12 | tmax = 20 13 | dt = 0.1 14 | gamma = 1 15 | tau = {1: 10, 2: 10, 3: 10} 16 | t, S, I, R = hc.discrete_SIR( 17 | H, tau, gamma, initial_infecteds=[4], tmin=tmin, tmax=tmax, dt=dt, seed=0 18 | ) 19 | 20 | assert np.all(S + I + R == H.num_nodes) 21 | assert np.min(t) == tmin 22 | assert np.max(t) < tmax 23 | assert abs((t[1] - t[0]) - dt) < 1e-10 24 | assert S[-1] == H.num_nodes - 1 25 | assert I[-1] == 0 26 | assert R[-1] == 1 27 | 28 | gamma = 0 29 | t, S, I, R = hc.discrete_SIR( 30 | H, 31 | tau, 32 | gamma, 33 | initial_infecteds=[6], 34 | tmin=tmin, 35 | tmax=tmax, 36 | dt=dt, 37 | threshold=0.5, 38 | seed=0, 39 | ) 40 | 41 | assert np.all(S + I + R == H.num_nodes) 42 | assert S[-1] == 4 43 | assert I[-1] == 4 44 | assert R[-1] == 0 45 | 46 | 47 | def test_discrete_SIS(edgelist1): 48 | H = xgi.Hypergraph(edgelist1) 49 | 50 | tmin = 10 51 | tmax = 20 52 | dt = 0.1 53 | gamma = 1 54 | tau = {1: 10, 2: 10, 3: 10} 55 | t, S, I = hc.discrete_SIS( 56 | H, tau, gamma, initial_infecteds=[4], tmin=tmin, tmax=tmax, dt=dt, seed=0 57 | ) 58 | 59 | assert np.all(S + I == H.num_nodes) 60 | assert np.min(t) == tmin 61 | assert np.max(t) < tmax 62 | assert abs((t[1] - t[0]) - dt) < 1e-10 63 | assert S[-1] == H.num_nodes 64 | assert I[-1] == 0 65 | 66 | gamma = 0 67 | t, S, I = hc.discrete_SIS( 68 | H, 69 | tau, 70 | gamma, 71 | initial_infecteds=[6], 72 | tmin=tmin, 73 | tmax=tmax, 74 | dt=dt, 75 | threshold=0.5, 76 | seed=0, 77 | ) 78 | 79 | assert np.all(S + I == H.num_nodes) 80 | assert S[-1] == 4 81 | assert I[-1] == 4 82 | 83 | 84 | def test_Gillespie_SIR(edgelist1): 85 | H = xgi.Hypergraph(edgelist1) 86 | 87 | tmin = 10 88 | tmax = 20 89 | 90 | gamma = 10 91 | tau = {1: 10, 2: 10, 3: 10} 92 | t, S, I, R = hc.Gillespie_SIR( 93 | H, tau, gamma, initial_infecteds=[4], tmin=tmin, tmax=tmax, seed=0 94 | ) 95 | 96 | assert np.all(S + I + R == H.num_nodes) 97 | assert np.min(t) == tmin 98 | assert np.max(t) < tmax 99 | assert I[-1] == 0 100 | assert R[-1] == 1 101 | 102 | gamma = 0 103 | 104 | t, S, I, R = hc.Gillespie_SIR( 105 | H, 106 | tau, 107 | gamma, 108 | initial_infecteds=[6], 109 | tmin=tmin, 110 | tmax=tmax, 111 | threshold=0.5, 112 | seed=0, 113 | ) 114 | 115 | assert np.all(S + I + R == H.num_nodes) 116 | assert np.min(t) == tmin 117 | assert np.max(t) < tmax 118 | assert I[-1] == 4 119 | assert R[-1] == 0 120 | 121 | 122 | def test_Gillespie_SIS(edgelist1): 123 | H = xgi.Hypergraph(edgelist1) 124 | 125 | tmin = 10 126 | tmax = 30 127 | 128 | gamma = 0 129 | tau = {1: 10, 2: 10, 3: 10} 130 | t, S, I = hc.Gillespie_SIS( 131 | H, 132 | tau, 133 | gamma, 134 | initial_infecteds=[6], 135 | tmin=tmin, 136 | tmax=tmax, 137 | threshold=0.5, 138 | seed=0, 139 | ) 140 | 141 | assert np.all(S + I == H.num_nodes) 142 | assert np.min(t) == tmin 143 | assert np.max(t) < tmax 144 | assert I[-1] == 4 145 | 146 | t, S, I = hc.Gillespie_SIS( 147 | H, 148 | tau, 149 | gamma, 150 | initial_infecteds=[6], 151 | tmin=tmin, 152 | tmax=tmax, 153 | threshold=0.51, 154 | seed=0, 155 | ) 156 | 157 | assert I[-1] == 2 158 | 159 | gamma = 100 160 | tau = {1: 0, 2: 0, 3: 0} 161 | t, S, I = hc.Gillespie_SIS( 162 | H, tau, gamma, initial_infecteds=[6], tmin=tmin, tmax=tmax, seed=0 163 | ) 164 | 165 | assert np.all(S + I == H.num_nodes) 166 | assert np.min(t) == tmin 167 | assert np.max(t) < tmax 168 | assert S[-1] == H.num_nodes 169 | 170 | 171 | def test_event_driven_SIR(edgelist1): 172 | H = xgi.Hypergraph(edgelist1) 173 | 174 | tmin = 10 175 | tmax = 20 176 | 177 | gamma = 10 178 | tau = {1: 100, 2: 100, 3: 100} 179 | t, S, I, R = hc.event_driven_SIR( 180 | H, tau, gamma, initial_infecteds=[4], tmin=tmin, tmax=tmax, seed=0 181 | ) 182 | 183 | assert np.all(S + I + R == H.num_nodes) 184 | assert np.min(t) == tmin 185 | assert np.max(t) < tmax 186 | assert I[-1] == 0 187 | assert R[-1] == 1 188 | 189 | gamma = 0 190 | t, S, I, R = hc.event_driven_SIR( 191 | H, tau, gamma, initial_infecteds=[6], tmin=tmin, tmax=100, seed=0 192 | ) 193 | 194 | assert np.all(S + I + R == H.num_nodes) 195 | assert np.min(t) == tmin 196 | assert np.max(t) < tmax 197 | assert I[-1] == 4 198 | assert R[-1] == 0 199 | 200 | gamma = 100 201 | tau = {1: 0, 2: 0, 3: 0} 202 | t, S, I, R = hc.event_driven_SIR( 203 | H, tau, gamma, initial_infecteds=[6], tmin=tmin, tmax=tmax, seed=0 204 | ) 205 | 206 | assert S[-1] == H.num_nodes - 1 207 | assert R[-1] == 1 208 | 209 | 210 | def test_event_driven_SIS(edgelist1): 211 | H = xgi.Hypergraph(edgelist1) 212 | 213 | tmin = 10 214 | tmax = 20 215 | 216 | gamma = 1000 217 | tau = {1: 0, 2: 0, 3: 0} 218 | t, S, I = hc.event_driven_SIS( 219 | H, tau, gamma, initial_infecteds=[4], tmin=tmin, tmax=tmax, seed=0 220 | ) 221 | 222 | assert np.all(S + I == H.num_nodes) 223 | assert np.min(t) == tmin 224 | assert np.max(t) < tmax 225 | assert I[-1] == 0 226 | 227 | gamma = 0 228 | tau = {1: 100, 2: 100, 3: 100} 229 | t, S, I = hc.event_driven_SIS( 230 | H, tau, gamma, initial_infecteds=[6], tmin=tmin, tmax=100, seed=0 231 | ) 232 | 233 | assert np.all(S + I == H.num_nodes) 234 | assert np.min(t) == tmin 235 | assert np.max(t) < tmax 236 | assert I[-1] == 4 237 | -------------------------------------------------------------------------------- /tutorials/tutorial_2_opinion_formation_modeling.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Import Packages" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import xgi\n", 17 | "import hypercontagion as hc\n", 18 | "import matplotlib.pyplot as plt\n", 19 | "import numpy as np\n", 20 | "import random" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "# Set Up the Hypergraph\n", 28 | "* Specify the number of nodes\n", 29 | "* Specify the hyperdegree distribution\n", 30 | "* Generates a configuration model of the hypergraph" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "n = 1000\n", 40 | "k1 = {i: random.randint(5, 10) for i in range(n)}\n", 41 | "k2 = {i: sorted(k1.values())[i] for i in range(n)}\n", 42 | "H = xgi.chung_lu_hypergraph(k1, k2)" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "# Opinion Models\n", 50 | "* Discrete state, random update (Voter model)\n", 51 | "* Discrete state, deterministic update (Majority rule)\n", 52 | "* Continuous state, random update (Deffuant-Weisbuch model)\n", 53 | "* Continuous state, deterministic update (Hegelmann-Krause)" 54 | ] 55 | }, 56 | { 57 | "cell_type": "markdown", 58 | "metadata": {}, 59 | "source": [ 60 | "## Voter Model\n", 61 | "* Binary opinion: For/Against or Yes/No\n", 62 | "* Randomly choose a hyperedge and if a majority of neighbors believe the opposite, then you change your mind " 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "metadata": {}, 68 | "source": [ 69 | "### Run simulation" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "yes_and_no = [random.choice([\"Yes\", \"No\"]) for i in range(n)]\n", 79 | "yes_and_no = np.array(yes_and_no, dtype=object)\n", 80 | "t, voter_model_states = hc.simulate_random_node_and_group_discrete_state(\n", 81 | " H, yes_and_no, tmin=0, tmax=10000\n", 82 | ")" 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "metadata": {}, 88 | "source": [ 89 | "### Plot Results" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": null, 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "yes = np.count_nonzero(voter_model_states == \"Yes\", axis=0)\n", 99 | "no = np.count_nonzero(voter_model_states == \"No\", axis=0)\n", 100 | "plt.figure()\n", 101 | "plt.plot(t, yes / n, label=\"For\")\n", 102 | "plt.plot(t, no / n, label=\"Against\")\n", 103 | "plt.xlabel(\"Time\")\n", 104 | "plt.ylabel(\"Fraction of population\")\n", 105 | "plt.legend()\n", 106 | "plt.show()" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": {}, 112 | "source": [ 113 | "## Deffuant-Weisbuch" 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "metadata": {}, 119 | "source": [ 120 | "$\\epsilon$ is a \"cautiousness\" parameter" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": null, 126 | "metadata": {}, 127 | "outputs": [], 128 | "source": [ 129 | "epsilon = 0.3\n", 130 | "initial_states = np.random.uniform(low=-1.0, high=1.0, size=n)\n", 131 | "t, states_DW = hc.simulate_random_group_continuous_state_1D(\n", 132 | " H, initial_states, tmin=0, tmax=2000, epsilon=epsilon\n", 133 | ")" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "metadata": {}, 139 | "source": [ 140 | "### Plot Results" 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "metadata": {}, 147 | "outputs": [], 148 | "source": [ 149 | "plt.figure()\n", 150 | "plt.plot(t, states_DW[::10, :].T)\n", 151 | "plt.xlabel(\"Time\")\n", 152 | "plt.ylabel(\"Ideology\")\n", 153 | "plt.show()" 154 | ] 155 | }, 156 | { 157 | "cell_type": "markdown", 158 | "metadata": {}, 159 | "source": [ 160 | "## Hegselmann-Krause" 161 | ] 162 | }, 163 | { 164 | "cell_type": "markdown", 165 | "metadata": {}, 166 | "source": [ 167 | "### Run Simulation" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": null, 173 | "metadata": {}, 174 | "outputs": [], 175 | "source": [ 176 | "initial_states = np.random.uniform(low=-1.0, high=1.0, size=n)\n", 177 | "t, states_HK = hc.synchronous_update_continuous_state_1D(\n", 178 | " H, initial_states, tmin=0, tmax=100\n", 179 | ")" 180 | ] 181 | }, 182 | { 183 | "cell_type": "markdown", 184 | "metadata": {}, 185 | "source": [ 186 | "### Plot Results" 187 | ] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "execution_count": null, 192 | "metadata": {}, 193 | "outputs": [], 194 | "source": [ 195 | "plt.figure()\n", 196 | "plt.plot(t, states_HK[::20, :].T)\n", 197 | "plt.xlabel(\"Time\")\n", 198 | "plt.ylabel(\"Ideology\")\n", 199 | "plt.xlim([0, 40])\n", 200 | "plt.show()" 201 | ] 202 | } 203 | ], 204 | "metadata": { 205 | "celltoolbar": "Raw Cell Format", 206 | "kernelspec": { 207 | "display_name": "hyper", 208 | "language": "python", 209 | "name": "python3" 210 | }, 211 | "language_info": { 212 | "codemirror_mode": { 213 | "name": "ipython", 214 | "version": 3 215 | }, 216 | "file_extension": ".py", 217 | "mimetype": "text/x-python", 218 | "name": "python", 219 | "nbconvert_exporter": "python", 220 | "pygments_lexer": "ipython3", 221 | "version": "3.10.0 (default, Mar 3 2022, 03:54:28) [Clang 12.0.0 ]" 222 | }, 223 | "vscode": { 224 | "interpreter": { 225 | "hash": "006b130b0afef3e20a59d32b3e368dadb49787729b49e0c4fc1ec3e01c886557" 226 | } 227 | } 228 | }, 229 | "nbformat": 4, 230 | "nbformat_minor": 4 231 | } 232 | -------------------------------------------------------------------------------- /tutorials/tutorial_1_epidemic_modeling.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Import Packages" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import xgi\n", 17 | "import hypercontagion as hc\n", 18 | "import matplotlib.pyplot as plt\n", 19 | "import time\n", 20 | "import numpy as np\n", 21 | "import random" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "# Set Up the Hypergraph\n", 29 | "* Specify the number of nodes\n", 30 | "* Specify the hyperdegree distribution\n", 31 | "* Generates a configuration model of the hypergraph" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "n = 1000\n", 41 | "k1 = {i: random.randint(5, 10) for i in range(n)}\n", 42 | "k2 = {i: sorted(k1.values())[i] for i in range(n)}\n", 43 | "H = xgi.chung_lu_hypergraph(k1, k2)" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "# Epidemic Simulations" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "## Epidemic Parameters\n", 58 | "* Initial size is the number of initial infected nodes\n", 59 | "* $\\gamma$ is the healing rate of a node\n", 60 | "* $\\mathbf{\\beta}$ is the infection rate for each hyperedge size (keys are the hyperedge size and the value is the infection rate)" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "initial_size = 100\n", 70 | "gamma = 0.05\n", 71 | "tau = {i: 0.1 for i in xgi.unique_edge_sizes(H)}" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "## Run an SIR simulation on hypergraphs\n", 79 | "* First simulation is is with discrete time steps (DTMC)\n", 80 | "* Second simulation is the Gillespie algorithm (CTMC)" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": null, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "start = time.time()\n", 90 | "t1, S1, I1, R1 = hc.discrete_SIR(H, tau, gamma, tmin=0, tmax=100, dt=1, rho=0.1)\n", 91 | "print(time.time() - start)\n", 92 | "\n", 93 | "start = time.time()\n", 94 | "t2, S2, I2, R2 = hc.Gillespie_SIR(H, tau, gamma, tmin=0, tmax=100, rho=0.1)\n", 95 | "print(time.time() - start)\n", 96 | "\n", 97 | "start = time.time()\n", 98 | "t3, S3, I3, R3 = hc.event_driven_SIR(H, tau, gamma, tmin=0, tmax=100, dt=1, rho=0.1)\n", 99 | "print(time.time() - start)" 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "metadata": {}, 105 | "source": [ 106 | "### Plot the results" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "plt.figure()\n", 116 | "plt.plot(t1, S1 / n, \"g--\", label=\"S (discrete)\")\n", 117 | "plt.plot(t1, I1 / n, \"r--\", label=\"I (discrete)\")\n", 118 | "plt.plot(t1, R1 / n, \"b--\", label=\"R (discrete)\")\n", 119 | "plt.plot(t2, S2 / n, \"g-\", label=\"S (Gillespie)\")\n", 120 | "plt.plot(t2, I2 / n, \"r-\", label=\"I (Gillespie)\")\n", 121 | "plt.plot(t2, R2 / n, \"b-\", label=\"R (Gillespie)\")\n", 122 | "plt.plot(t3, S3 / n, \"g-.\", label=\"S (event-driven)\")\n", 123 | "plt.plot(t3, I3 / n, \"r-.\", label=\"I (event-driven)\")\n", 124 | "plt.plot(t3, R3 / n, \"b-.\", label=\"R (event-driven)\")\n", 125 | "plt.legend()\n", 126 | "plt.xlabel(\"Time\")\n", 127 | "plt.ylabel(\"Fraction of population\")\n", 128 | "plt.show()" 129 | ] 130 | }, 131 | { 132 | "cell_type": "markdown", 133 | "metadata": {}, 134 | "source": [ 135 | "## Run an SIS simulation on hypergraphs\n", 136 | "* First simulation is is with discrete time steps (DTMC)\n", 137 | "* Second simulation is the Gillespie algorithm (CTMC)" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "start = time.time()\n", 147 | "t1, S1, I1 = hc.discrete_SIS(H, tau, gamma, tmin=0, tmax=50, dt=1, rho=0.5)\n", 148 | "print(time.time() - start)\n", 149 | "\n", 150 | "start = time.time()\n", 151 | "t2, S2, I2 = hc.Gillespie_SIS(H, tau, gamma, tmin=0, tmax=50, rho=0.5)\n", 152 | "print(time.time() - start)\n", 153 | "\n", 154 | "start = time.time()\n", 155 | "t3, S3, I3 = hc.event_driven_SIS(H, tau, gamma, tmin=0, tmax=50, rho=0.5)\n", 156 | "print(time.time() - start)" 157 | ] 158 | }, 159 | { 160 | "cell_type": "markdown", 161 | "metadata": {}, 162 | "source": [ 163 | "### Plot the results" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": null, 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | "plt.figure()\n", 173 | "plt.plot(t1, S1 / n, \"g--\", label=\"S (discrete)\")\n", 174 | "plt.plot(t1, I1 / n, \"r--\", label=\"I (discrete)\")\n", 175 | "plt.plot(t2, S2 / n, \"g-\", label=\"S (Gillespie)\")\n", 176 | "plt.plot(t2, I2 / n, \"r-\", label=\"I (Gillespie)\")\n", 177 | "plt.plot(t3, S3 / n, \"g-.\", label=\"S (event-driven)\")\n", 178 | "plt.plot(t3, I3 / n, \"r-.\", label=\"I (event-driven)\")\n", 179 | "plt.legend()\n", 180 | "plt.xlabel(\"Time\")\n", 181 | "plt.ylabel(\"Fraction of population\")\n", 182 | "plt.show()" 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": null, 188 | "metadata": {}, 189 | "outputs": [], 190 | "source": [] 191 | } 192 | ], 193 | "metadata": { 194 | "celltoolbar": "Raw Cell Format", 195 | "kernelspec": { 196 | "display_name": "Python 3.10.5 64-bit", 197 | "language": "python", 198 | "name": "python3" 199 | }, 200 | "language_info": { 201 | "codemirror_mode": { 202 | "name": "ipython", 203 | "version": 3 204 | }, 205 | "file_extension": ".py", 206 | "mimetype": "text/x-python", 207 | "name": "python", 208 | "nbconvert_exporter": "python", 209 | "pygments_lexer": "ipython3", 210 | "version": "3.10.5" 211 | }, 212 | "vscode": { 213 | "interpreter": { 214 | "hash": "88880b30d22c3e82d444bc5a40c679bbf2837a658927a3eed0b1fab0c002b813" 215 | } 216 | } 217 | }, 218 | "nbformat": 4, 219 | "nbformat_minor": 4 220 | } 221 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | 14 | import os 15 | import sys 16 | 17 | sys.path.insert(0, os.path.abspath(".")) 18 | sys.path.append(os.path.join(os.path.dirname(__name__), "hypercontagion")) 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = "HyperContagion" 24 | copyright = "Copyright (C) 2021 HyperContagion Developers" 25 | author = "Nicholas W. Landry and Joel C. Miller" 26 | 27 | # The full version, including alpha/beta/rc tags 28 | release = "0.1.2" 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ["_templates"] 39 | 40 | # copybutton options 41 | copybutton_prompt_text = "myinputprompt" 42 | copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " 43 | copybutton_prompt_is_regexp = True 44 | 45 | # The language for content autogenerated by Sphinx. Refer to documentation 46 | # for a list of supported languages. 47 | # 48 | # This is also used if you do content translation via gettext catalogs. 49 | # Usually you set "language" from the command line for these cases. 50 | language = None 51 | 52 | # List of patterns, relative to source directory, that match files and 53 | # directories to ignore when looking for source files. 54 | # This pattern also affects html_static_path and html_extra_path. 55 | exclude_patterns = [] 56 | 57 | 58 | # -- Options for HTML output ------------------------------------------------- 59 | 60 | # The theme to use for HTML and HTML Help pages. See the documentation for 61 | # a list of builtin themes. 62 | # 63 | 64 | # Add any paths that contain custom static files (such as style sheets) here, 65 | # relative to this directory. They are copied after the builtin static files, 66 | # so a file named "default.css" will overwrite the builtin "default.css". 67 | html_static_path = ["_static"] 68 | html_css_files = ["custom.css"] 69 | 70 | # If your documentation needs a minimal Sphinx version, state it here. 71 | needs_sphinx = "1.3" 72 | 73 | # Add any Sphinx extension module names here, as strings. They can be 74 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 75 | # ones. 76 | extensions = [ 77 | "sphinx.ext.napoleon", 78 | "sphinx.ext.autosummary", 79 | "sphinx.ext.autodoc", 80 | "sphinx.ext.coverage", 81 | "sphinx.ext.doctest", 82 | "sphinx.ext.intersphinx", 83 | "sphinx.ext.mathjax", 84 | "sphinx.ext.todo", 85 | "sphinx.ext.viewcode", 86 | "sphinx_copybutton", 87 | ] 88 | 89 | # Automatically generate stub pages when using the .. autosummary directive 90 | autosummary_generate = True 91 | autosummary_generate_overwrite = False 92 | 93 | # Add any paths that contain templates here, relative to this directory. 94 | templates_path = ["_templates"] 95 | 96 | # The suffix(es) of source filenames. 97 | # You can specify multiple suffix as a list of string: 98 | source_suffix = [".rst", ".md"] 99 | # source_suffix = ".rst" 100 | 101 | # The encoding of source files. 102 | # source_encoding = 'utf-8-sig' 103 | 104 | # The master toctree document. 105 | master_doc = "index" 106 | 107 | # The language for content autogenerated by Sphinx. Refer to documentation 108 | # for a list of supported languages. 109 | # 110 | # This is also used if you do content translation via gettext catalogs. 111 | # Usually you set "language" from the command line for these cases. 112 | language = None 113 | 114 | # There are two options for replacing |today|: either, you set today to some 115 | # non-false value, then it is used: 116 | # today = '' 117 | # Else, today_fmt is used as the format for a strftime call. 118 | today_fmt = "%B %d, %Y" 119 | 120 | # List of patterns, relative to source directory, that match files and 121 | # directories to ignore when looking for source files. 122 | exclude_patterns = [] 123 | 124 | # The reST default role (used for this markup: `text`) to use for all 125 | # documents. 126 | # default_role = None 127 | 128 | # If true, '()' will be appended to :func: etc. cross-reference text. 129 | # add_function_parentheses = True 130 | 131 | # If true, the current module name will be prepended to all description 132 | # unit titles (such as .. function::). 133 | # add_module_names = True 134 | 135 | # If true, sectionauthor and moduleauthor directives will be shown in the 136 | # output. They are ignored by default. 137 | # show_authors = False 138 | 139 | # The name of the Pygments (syntax highlighting) style to use. 140 | # pygments_style = "sphinx" 141 | pygments_style = "friendly" 142 | 143 | # A list of ignored prefixes for module index sorting. 144 | # modindex_common_prefix = [] 145 | 146 | # If true, keep warnings as "system message" paragraphs in the built documents. 147 | # keep_warnings = False 148 | 149 | # If true, `todo` and `todoList` produce output, else they produce nothing. 150 | todo_include_todos = True 151 | 152 | 153 | # -- Options for HTML output ---------------------------------------------- 154 | 155 | # The theme to use for HTML and HTML Help pages. See the documentation for 156 | # a list of builtin themes. 157 | # html_theme = 'nature' 158 | html_theme = "sphinx_rtd_theme" 159 | # html_theme = 'pyramid' 160 | 161 | # Theme options are theme-specific and customize the look and feel of a theme 162 | # further. For a list of options available for each theme, see the 163 | # documentation. 164 | # html_theme_options = {} 165 | 166 | # Add any paths that contain custom themes here, relative to this directory. 167 | # html_theme_path = ["_static"] 168 | # html_static_path = ["_static"] 169 | 170 | html_show_sphinx = True 171 | 172 | htmlhelp_basename = "HyperContagionDoc" 173 | 174 | # -- Options for LaTeX output --------------------------------------------- 175 | 176 | latex_elements = { 177 | # The paper size ('letterpaper' or 'a4paper'). 178 | #'papersize': 'letterpaper', 179 | # The font size ('10pt', '11pt' or '12pt'). 180 | #'pointsize': '10pt', 181 | # Additional stuff for the LaTeX preamble. 182 | #'preamble': '', 183 | # Latex figure (float) alignment 184 | #'figure_align': 'htbp', 185 | } 186 | 187 | # Grouping the document tree into LaTeX files. List of tuples 188 | # (source start file, target name, title, 189 | # author, documentclass [howto, manual, or own class]). 190 | latex_documents = [ 191 | ( 192 | master_doc, 193 | "hypercontagion.tex", 194 | "HypercContagion Documentation", 195 | "Nicholas W. Landry and Joel C. Miller", 196 | "manual", 197 | ), 198 | ] 199 | 200 | man_pages = [ 201 | (master_doc, "hypercontagion", "HyperContagion Documentation", [author], 1) 202 | ] 203 | 204 | texinfo_documents = [ 205 | ( 206 | master_doc, 207 | "HyperContagion", 208 | "HyperContagion Documentation", 209 | author, 210 | "HyperContagion", 211 | "A library to simulate higher-order contagion", 212 | "Miscellaneous", 213 | ), 214 | ] 215 | 216 | epub_title = project 217 | epub_author = author 218 | epub_publisher = author 219 | epub_copyright = copyright 220 | 221 | 222 | epub_exclude_files = ["search.html"] 223 | 224 | 225 | def setup(app): 226 | app.add_js_file("copybutton.js") 227 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\HyperNetX.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\HyperNetX.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end -------------------------------------------------------------------------------- /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 = source 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 | 18 | # User-friendly check for sphinx-build 19 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 20 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 21 | endif 22 | 23 | # Internal variables. 24 | PAPEROPT_a4 = -D latex_paper_size=a4 25 | PAPEROPT_letter = -D latex_paper_size=letter 26 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 27 | # the i18n builder cannot share the environment and doctrees with the others 28 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 29 | 30 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 31 | 32 | help: 33 | @echo "Please use \`make ' where is one of" 34 | @echo " html to make standalone HTML files" 35 | @echo " dirhtml to make HTML files named index.html in directories" 36 | @echo " singlehtml to make a single large HTML file" 37 | @echo " pickle to make pickle files" 38 | @echo " json to make JSON files" 39 | @echo " htmlhelp to make HTML files and a HTML help project" 40 | @echo " qthelp to make HTML files and a qthelp project" 41 | @echo " applehelp to make an Apple Help Book" 42 | @echo " devhelp to make HTML files and a Devhelp project" 43 | @echo " epub to make an epub" 44 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 45 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 46 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 47 | @echo " text to make text files" 48 | @echo " man to make manual pages" 49 | @echo " texinfo to make Texinfo files" 50 | @echo " info to make Texinfo files and run them through makeinfo" 51 | @echo " gettext to make PO message catalogs" 52 | @echo " changes to make an overview of all changed/added/deprecated items" 53 | @echo " xml to make Docutils-native XML files" 54 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 55 | @echo " linkcheck to check all external links for integrity" 56 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 57 | @echo " coverage to run coverage check of the documentation (if enabled)" 58 | 59 | clean: 60 | rm -rf $(BUILDDIR)/* 61 | 62 | html: 63 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 64 | @echo 65 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 66 | 67 | dirhtml: 68 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 69 | @echo 70 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 71 | 72 | singlehtml: 73 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 74 | @echo 75 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 76 | 77 | pickle: 78 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 79 | @echo 80 | @echo "Build finished; now you can process the pickle files." 81 | 82 | json: 83 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 84 | @echo 85 | @echo "Build finished; now you can process the JSON files." 86 | 87 | htmlhelp: 88 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 89 | @echo 90 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 91 | ".hhp project file in $(BUILDDIR)/htmlhelp." 92 | 93 | qthelp: 94 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 95 | @echo 96 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 97 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 98 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/HyperNetX.qhcp" 99 | @echo "To view the help file:" 100 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/HyperNetX.qhc" 101 | 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | devhelp: 111 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 112 | @echo 113 | @echo "Build finished." 114 | @echo "To view the help file:" 115 | @echo "# mkdir -p $$HOME/.local/share/devhelp/HyperNetX" 116 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/HyperNetX" 117 | @echo "# devhelp" 118 | 119 | epub: 120 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 121 | @echo 122 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 123 | 124 | latex: 125 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 126 | @echo 127 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 128 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 129 | "(use \`make latexpdf' here to do that automatically)." 130 | 131 | latexpdf: 132 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 133 | @echo "Running LaTeX files through pdflatex..." 134 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 135 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 136 | 137 | latexpdfja: 138 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 139 | @echo "Running LaTeX files through platex and dvipdfmx..." 140 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 141 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 142 | 143 | text: 144 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 145 | @echo 146 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 147 | 148 | man: 149 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 150 | @echo 151 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 152 | 153 | texinfo: 154 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 155 | @echo 156 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 157 | @echo "Run \`make' in that directory to run these through makeinfo" \ 158 | "(use \`make info' here to do that automatically)." 159 | 160 | info: 161 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 162 | @echo "Running Texinfo files through makeinfo..." 163 | make -C $(BUILDDIR)/texinfo info 164 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 165 | 166 | gettext: 167 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 168 | @echo 169 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 170 | 171 | changes: 172 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 173 | @echo 174 | @echo "The overview file is in $(BUILDDIR)/changes." 175 | 176 | linkcheck: 177 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 178 | @echo 179 | @echo "Link check complete; look for any errors in the above output " \ 180 | "or in $(BUILDDIR)/linkcheck/output.txt." 181 | 182 | doctest: 183 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 184 | @echo "Testing of doctests in the sources finished, look at the " \ 185 | "results in $(BUILDDIR)/doctest/output.txt." 186 | 187 | coverage: 188 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 189 | @echo "Testing of coverage in the sources finished, look at the " \ 190 | "results in $(BUILDDIR)/coverage/python.txt." 191 | 192 | xml: 193 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 194 | @echo 195 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 196 | 197 | pseudoxml: 198 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 199 | @echo 200 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." -------------------------------------------------------------------------------- /hypercontagion/sim/opinions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Opinion formation models on hypergraphs. 3 | """ 4 | 5 | import random 6 | 7 | import numpy as np 8 | 9 | 10 | # built-in functions 11 | def voter_model(node, edge, status, p_adoption=1): 12 | """the voter model given a hyperedge 13 | 14 | Parameters 15 | ---------- 16 | node : hashable 17 | node whose opinion may change 18 | edge : iterable 19 | a list of the members of a hyperedge. must include the node. 20 | status : dict 21 | keys are node IDs, statuses are values 22 | p_adoption : float, default: 1 23 | probability that the node will adopt the consensus. 24 | 25 | Returns 26 | ------- 27 | str 28 | new node status 29 | """ 30 | neighbors = [n for n in edge if n != node] 31 | opinions = set(status[neighbors]) # get unique opinions 32 | if len(opinions) == 1: 33 | if random.random() <= p_adoption: 34 | status[node] = opinions.pop() 35 | return status 36 | 37 | 38 | # continuous output 39 | def discordance(edge, status): 40 | """Computes the discordance of a hyperedge. 41 | 42 | Parameters 43 | ---------- 44 | edge : tuple 45 | a list of an edge's members 46 | status : numpy array 47 | opinions of the nodes 48 | 49 | Returns 50 | ------- 51 | float 52 | discordance of the hyperedge 53 | """ 54 | try: 55 | e = list(edge) 56 | return 1 / (len(e) - 1) * np.sum(np.power(status[e] - np.mean(status[e]), 2)) 57 | except ZeroDivisionError: 58 | return float("Inf") # handles singleton edges 59 | 60 | 61 | def deffuant_weisbuch(edge, status, epsilon=0.5, update="average", m=0.1): 62 | """the deffuant weisbuch model for updating the statuses of nodes in an edge 63 | 64 | Parameters 65 | ---------- 66 | edge : iterable 67 | list of nodes 68 | status : numpy array 69 | node statuses 70 | epsilon : float, default 71 | confidence bound 72 | update : str, default: "average" 73 | if "average" the opinions of all nodes in the hyperedge 74 | are updated to the average. If "cautious", the nodes are 75 | moved toward the average. 76 | 77 | m : float between 0 and 1, default: 0.1 78 | the fraction of the possible distance to move the node opinions 79 | to the centroid. 80 | 81 | Returns 82 | ------- 83 | iterable 84 | the updated statuses 85 | """ 86 | status = status.copy() 87 | e = list(edge) 88 | if discordance(e, status) < epsilon: 89 | if update == "average": 90 | status[e] = np.mean(status[e]) 91 | return status 92 | elif update == "cautious": 93 | status[e] = status[e] + m * (np.mean(status[e]) - status[e]) 94 | return status 95 | else: 96 | return status 97 | 98 | 99 | def hegselmann_krause(H, status, epsilon=0.1): 100 | """The Hegselmann-Krause model. 101 | 102 | Parameters 103 | ---------- 104 | H : xgi.Hypergraph 105 | the hypergraph of interest 106 | status : iterable 107 | statuses of the nodes. 108 | epsilon : float, default: 0.1 109 | confidence bound 110 | 111 | Returns 112 | ------- 113 | iterable 114 | new opinions 115 | """ 116 | 117 | members = H.edges.members(dtype=dict) 118 | memberships = H.nodes.memberships() 119 | 120 | new_status = status.copy() 121 | for node in H.nodes: 122 | new_status[node] = 0 123 | numberOfLikeMinded = 0 124 | for edge_id in memberships[node]: 125 | edge = list(members[edge_id]) 126 | if discordance(edge, status) < epsilon: 127 | new_status[node] += np.mean(status[edge]) 128 | numberOfLikeMinded += 1 129 | try: 130 | new_status[node] *= 1.0 / numberOfLikeMinded 131 | except: 132 | new_status[node] = status[node] 133 | return new_status 134 | 135 | 136 | def simulate_random_group_continuous_state_1D( 137 | H, initial_states, function=deffuant_weisbuch, tmin=0, tmax=100, dt=1, **args 138 | ): 139 | """Simulate an opinion formation process where states are continuous and 140 | random groups are chosen. 141 | 142 | Parameters 143 | ---------- 144 | H : xgi.Hypergraph 145 | the hypergraph of interest 146 | initial_states : numpy array 147 | initial node states 148 | function : update function, default: deffuant_weisbuch 149 | node update function 150 | tmin : int, default: 0 151 | the time at which the simulation starts 152 | tmax : int, default: 100 153 | the time at which the simulation terminates 154 | dt : float > 0, default: 1 155 | the time step to take. 156 | 157 | Returns 158 | ------- 159 | numpy array, numpy array 160 | a 1D array of the times and a 2D array of the states. 161 | """ 162 | members = H.edges.members(dtype=dict) 163 | 164 | time = tmin 165 | timesteps = int((tmax - tmin) / dt) + 2 166 | states = np.empty((H.num_nodes, timesteps)) 167 | times = np.empty(timesteps) 168 | step = 0 169 | times[step] = time 170 | states[:, step] = initial_states.copy() 171 | while time <= tmax: 172 | time += dt 173 | step += 1 174 | # randomly select hyperedge 175 | edge = members[random.choice(list(members))] 176 | 177 | states[:, step] = function(edge, states[:, step - 1], **args) 178 | times[step] = time 179 | 180 | return times, states 181 | 182 | 183 | def simulate_random_node_and_group_discrete_state( 184 | H, initial_states, function=voter_model, tmin=0, tmax=100, dt=1, **args 185 | ): 186 | """Simulate an opinion formation process where states are discrete and 187 | states are updated synchronously. 188 | 189 | Parameters 190 | ---------- 191 | H : xgi.Hypergraph 192 | the hypergraph of interest 193 | initial_states : numpy array 194 | initial node states 195 | function : update function, default: deffuant_weisbuch 196 | node update function 197 | tmin : int, default: 0 198 | the time at which the simulation starts 199 | tmax : int, default: 100 200 | the time at which the simulation terminates 201 | dt : float > 0, default: 1 202 | the time step to take. 203 | 204 | Returns 205 | ------- 206 | numpy array, numpy array 207 | a 1D array of the times and a 2D array of the states. 208 | """ 209 | members = H.edges.members(dtype=dict) 210 | time = tmin 211 | timesteps = int((tmax - tmin) / dt) + 2 212 | states = np.empty((H.num_nodes, timesteps), dtype=object) 213 | times = np.empty(timesteps) 214 | step = 0 215 | times[step] = time 216 | states[:, step] = initial_states.copy() 217 | while time <= tmax: 218 | time += dt 219 | step += 1 220 | # randomly select node 221 | node = random.choice(list(H.nodes)) 222 | # randomly select neighbors of the node 223 | edge = members[random.choice(list(members))] 224 | 225 | states[:, step] = function(node, edge, states[:, step - 1], **args) 226 | times[step] = time 227 | 228 | return times, states 229 | 230 | 231 | def synchronous_update_continuous_state_1D( 232 | H, initial_states, function=hegselmann_krause, tmin=0, tmax=100, dt=1, **args 233 | ): 234 | """Simulate an opinion formation process where states are continuous and 235 | states are updated synchronously. 236 | 237 | Parameters 238 | ---------- 239 | H : xgi.Hypergraph 240 | the hypergraph of interest 241 | initial_states : numpy array 242 | initial node states 243 | function : update function, default: deffuant_weisbuch 244 | node update function 245 | tmin : int, default: 0 246 | the time at which the simulation starts 247 | tmax : int, default: 100 248 | the time at which the simulation terminates 249 | dt : float > 0, default: 1 250 | the time step to take. 251 | 252 | Returns 253 | ------- 254 | numpy array, numpy array 255 | a 1D array of the times and a 2D array of the states. 256 | """ 257 | time = tmin 258 | timesteps = int((tmax - tmin) / dt) + 2 259 | states = np.empty((H.num_nodes, timesteps)) 260 | times = np.empty(timesteps) 261 | step = 0 262 | times[step] = time 263 | states[:, step] = initial_states.copy() 264 | while time <= tmax: 265 | time += dt 266 | step += 1 267 | states[:, step] = function(H, states[:, step - 1], **args) 268 | times[step] = time 269 | 270 | return times, states 271 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | > TL;DR Be excellent to each other; we're a community after all. If you run into issues with others in our community, please contact a HyperContagion Community Dev or Moderator. 4 | 5 | ## Purpose 6 | 7 | The HyperContagion Community includes members of varying skills, languages, personalities, cultural backgrounds, and experiences from around the globe. Through these differences, we continue to grow and collectively improve upon an open-source animation engine. When working in a community, it is important to remember that you are interacting with humans on the other end of your screen. This code of conduct will guide your interactions and keep HyperContagion a positive environment for our developers, users, and fundamentally our growing community. 8 | 9 | 10 | 11 | ## Our Community 12 | 13 | Members of the HyperContagion Community are respectful, open, and considerate. Behaviors that reinforce these values contribute to our positive environment, and include: 14 | 15 | - **Being respectful.** Respectful of others, their positions, experiences, viewpoints, skills, commitments, time, and efforts. 16 | 17 | - **Being open.** Open to collaboration, whether it's on problems, Pull Requests, issues, or otherwise. 18 | 19 | - **Being considerate.** Considerate of their peers -- other HyperContagion users and developers. 20 | 21 | - **Focusing on what is best for the community.** We're respectful of the processes set forth in the community, and we work within them. 22 | 23 | - **Showing empathy towards other community members.** We're attentive in our communications, whether in person or online, and we're tactful when approaching differing views. 24 | 25 | - **Gracefully accepting constructive criticism.** When we disagree, we are courteous in raising our issues. 26 | 27 | - **Using welcoming and inclusive language.** We're accepting of all who wish to take part in our activities, fostering an environment where anyone can participate and everyone can make a difference. 28 | 29 | 30 | 31 | ## Our Standards 32 | 33 | Every member of our community has the right to have their identity respected. The HyperContagion Community is dedicated to providing a positive environment for everyone, regardless of age, gender identity and expression, sexual orientation, disability, physical appearance, body size, ethnicity, nationality, race, religion (or lack thereof), education, or socioeconomic status. 34 | 35 | 36 | 37 | ## Inappropriate Behavior 38 | 39 | Examples of unacceptable behavior by participants include: 40 | 41 | * Harassment of any participants in any form 42 | * Deliberate intimidation, stalking, or following 43 | * Logging or taking screenshots of online activity for harassment purposes 44 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 45 | * Violent threats or language directed against another person 46 | * Incitement of violence or harassment towards any individual, including encouraging a person to commit suicide or to engage in self-harm 47 | * Creating additional online accounts in order to harass another person or circumvent a ban 48 | * Sexual language and imagery in online communities or any conference venue, including talks 49 | * Insults, put-downs, or jokes that are based upon stereotypes, that are exclusionary, or that hold others up for ridicule 50 | * Excessive swearing 51 | * Unwelcome sexual attention or advances 52 | * Unwelcome physical contact, including simulated physical contact (eg, textual descriptions like "hug" or "backrub") without consent or after a request to stop 53 | * Pattern of inappropriate social contact, such as requesting/assuming inappropriate levels of intimacy with others 54 | * Sustained disruption of online community discussions, in-person presentations, or other in-person events 55 | * Continued one-on-one communication after requests to cease 56 | * Other conduct that is inappropriate for a professional audience including people of many different backgrounds 57 | Community members asked to stop any inappropriate behavior are expected to comply immediately. 58 | 59 | 60 | ## HyperContagion Community Online Spaces 61 | 62 | This Code of Conduct applies to the following online spaces: 63 | 64 | - The [HyperContagion GitHub Project](https://github.com/nwlandry/hypercontagion) 65 | 66 | This Code of Conduct applies to every member in official HyperContagion Community online spaces, including: 67 | 68 | - Moderators 69 | 70 | - Maintainers 71 | 72 | - Developers 73 | 74 | - Reviewers 75 | 76 | - Contributors 77 | 78 | - Users 79 | 80 | - All community members 81 | 82 | 83 | 84 | ## Consequences 85 | 86 | If a member's behavior violates this code of conduct, the HyperContagion Community Code of Conduct team may take any action they deem appropriate, including, but not limited to: warning the offender, temporary bans, deletion of offending messages, and expulsion from the community and its online spaces. The full list of consequences for inappropriate behavior is listed below in the Enforcement Procedures. 87 | 88 | Thank you for helping make this a welcoming, friendly community for everyone. 89 | 90 | 91 | ## Contact Information 92 | 93 | If you believe someone is violating the code of conduct, or have any other concerns, please contact a HyperContagion Community Dev or Moderator immediately. They can be reached via email (See the README). 94 | 95 |
96 | 97 | ## Enforcement Procedures 98 | 99 | This document summarizes the procedures the HyperContagion Community Code of Conduct team uses to enforce the Code of Conduct. 100 | 101 | ### Summary of processes 102 | 103 | When the team receives a report of a possible Code of Conduct violation, it will: 104 | 105 | 1. Acknowledge the receipt of the report. 106 | 1. Evaluate conflicts of interest. 107 | 1. Call a meeting of code of conduct team members without a conflict of interest. 108 | 1. Evaluate the reported incident. 109 | 1. Propose a behavioral modification plan. 110 | 1. Propose consequences for the reported behavior. 111 | 1. Vote on behavioral modification plan and consequences for the reported person. 112 | 1. Contact HyperContagion Community moderators to approve the behavioral modification plan and consequences. 113 | 1. Follow up with the reported person. 114 | 1. Decide further responses. 115 | 1. Follow up with the reporter. 116 | 117 | 118 | ### Acknowledge the report 119 | 120 | Reporters should receive an acknowledgment of the receipt of their report within 48 hours. 121 | 122 | 123 | ### Conflict of interest policy 124 | 125 | Examples of conflicts of interest include: 126 | 127 | * You have a romantic or platonic relationship with either the reporter or the reported person. It's fine to participate if they are an acquaintance. 128 | * The reporter or reported person is someone you work closely with. This could be someone on your team or someone who works on the same project as you. 129 | * The reporter or reported person is a maintainer who regularly reviews your contributions 130 | * The reporter or reported person is your metamour. 131 | * The reporter or reported person is your family member 132 | Committee members do not need to state why they have a conflict of interest, only that one exists. Other team members should not ask why the person has a conflict of interest. 133 | 134 | Anyone who has a conflict of interest will remove themselves from the discussion of the incident, and recluse themselves from voting on a response to the report. 135 | 136 | 137 | 138 | ### Evaluating a report 139 | 140 | #### Jurisdiction 141 | 142 | * *Is this a Code of Conduct violation?* Is this behavior on our list of inappropriate behavior? Is it borderline inappropriate behavior? Does it violate our community norms? 143 | * *Did this occur in a space that is within our Code of Conduct's scope?* If the incident occurred outside the community, but a community member's mental health or physical safety may be negatively impacted if no action is taken, the incident may be in scope. Private conversations in community spaces are also in scope. 144 | #### Impact 145 | 146 | * *Did this incident occur in a private conversation or a public space?* Incidents that all community members can see will have a more negative impact. 147 | * *Does this behavior negatively impact a marginalized group in our community?* Is the reporter a person from a marginalized group in our community? How is the reporter being negatively impacted by the reported person's behavior? Are members of the marginalized group likely to disengage with the community if no action was taken on this report? 148 | * *Does this incident involve a community leader?* Community members often look up to community leaders to set the standard of acceptable behavior 149 | #### Risk 150 | 151 | * *Does this incident include sexual harassment?* 152 | * *Does this pose a safety risk?* Does the behavior put a person's physical safety at risk? Will this incident severely negatively impact someone's mental health? 153 | * *Is there a risk of this behavior being repeated?* Does the reported person understand why their behavior was inappropriate? Is there an established pattern of behavior from past reports? 154 | 155 | 156 | Reports which involve higher risk or higher impact may face more severe consequences than reports which involve lower risk or lower impact. 157 | 158 | 159 | 160 | ### Propose consequences 161 | 162 | What follows are examples of possible consequences to an incident report. This consequences list is not inclusive, and the HyperContagion Community Code of Conduct team reserves the right to take any action it deems necessary. 163 | 164 | Possible private responses to an incident include: 165 | 166 | * Nothing, if the behavior was determined to not be a Code of Conduct violation 167 | * A warning 168 | * A final warning 169 | * Temporarily removing the reported person from the community's online space(s) 170 | * Permanently removing the reported person from the community's online space(s) 171 | * Publishing an account of the incident 172 | 173 | 174 | ### Team vote 175 | 176 | Some team members may have a conflict of interest and may be excluded from discussions of a particular incident report. Excluding those members, decisions on the behavioral modification plans and consequences will be determined by a two-thirds majority vote of the HyperContagion Community Code of Conduct team. 177 | 178 | 179 | 180 | ### Moderators approval 181 | 182 | Once the team has approved the behavioral modification plans and consequences, they will communicate the recommended response to the HyperContagion Community moderators. The team should not state who reported this incident. They should attempt to anonymize any identifying information from the report. 183 | 184 | Moderators are required to respond with whether they accept the recommended response to the report. If they disagree with the recommended response, they should provide a detailed response or additional context as to why they disagree. Moderators are encouraged to respond within a week. 185 | 186 | In cases where the moderators disagree on the suggested resolution for a report, the HyperContagion Community Code of Conduct team may choose to notify the HyperContagion Community Moderators. 187 | 188 | 189 | 190 | ### Follow up with the reported person 191 | 192 | The HyperContagion Community Code of Conduct team will work with the HyperContagion Community moderators to draft a response to the reported person. The response should contain: 193 | 194 | * A description of the person's behavior in neutral language 195 | * The negative impact of that behavior 196 | * A concrete behavioral modification plan 197 | * Any consequences of their behavior 198 | The team should not state who reported this incident. They should attempt to anonymize any identifying information from the report. The reported person should be discouraged from contacting the reporter to discuss the report. If they wish to apologize to the reporter, the team can accept the apology on behalf of the reporter. 199 | 200 | 201 | 202 | ### Decide further responses 203 | 204 | If the reported person provides additional context, the HyperContagion Community Code of Conduct team may need to re-evaluate the behavioral modification plan and consequences. 205 | 206 | ### Follow up with the reporter 207 | 208 | A person who makes a report should receive a follow-up response stating what action was taken in response to the report. If the team decided no response was needed, they should provide an explanation why it was not a Code of Conduct violation. Reports that are not made in good faith (such as "reverse sexism" or "reverse racism") may receive no response. 209 | 210 | The follow-up should be sent no later than one week after the receipt of the report. If deliberation or follow-up with the reported person takes longer than one week, the team should send a status update to the reporter. 211 | 212 | 213 | 214 | ### Changes to Code of Conduct 215 | 216 | When discussing a change to the HyperContagion Community code of conduct or enforcement procedures, the HyperContagion Community Code of Conduct team will follow this decision-making process: 217 | 218 | * **Brainstorm options.** Team members should discuss any relevant context and brainstorm a set of possible options. It is important to provide constructive feedback without getting side-tracked from the main question. 219 | * **Vote.** Proposed changes to the code of conduct will be decided by a two-thirds majority of all voting members of the Code of Conduct team. Team members are listed in the charter. Currently active voting members are listed in the following section. 220 | * **Board Vote.** Once a working draft is in place for the Code of Conduct and procedures, the Code of Conduct team shall provide the HyperContagion Community Moderators with a draft of the changes. The HyperContagion Community Moderators will vote on the changes at a board meeting. 221 | 222 | 223 | ### Current list of voting members 224 | 225 | - All available Community Developers (i.e. those with "write" permissions, or above, on the HyperContagion Community GitHub organization). 226 | 227 | 228 | 229 | ## License 230 | 231 | This Code of Conduct is licensed under the [Creative Commons Attribution-ShareAlike 3.0 Unported License](https://creativecommons.org/licenses/by-sa/3.0/). 232 | 233 | 234 | 235 | ## Attributions 236 | 237 | This Code of Conduct was forked from the code of conduct from the [Python Software Foundation](https://www.python.org/psf/conduct/) and the Manim community and adapted by the HyperContagion Community. -------------------------------------------------------------------------------- /hypercontagion/utils/utilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains useful classes and functions for use in the hypercontagion library. 3 | """ 4 | 5 | import heapq 6 | import random 7 | from collections import Counter, defaultdict 8 | 9 | import numpy as np 10 | 11 | __all__ = [ 12 | "EventQueue", 13 | "MockSamplableSet", 14 | "SamplingDict", 15 | "_process_trans_SIR_", 16 | "_process_rec_SIR_", 17 | "_process_trans_SIS_", 18 | "_process_rec_SIS_", 19 | ] 20 | 21 | 22 | class EventQueue: 23 | r""" 24 | This class is used to store and act on a priority queue of events for 25 | event-driven simulations. It is based on heapq. 26 | Each queue is given a tmax (default is infinity) so that any event at later 27 | time is ignored. 28 | 29 | This is a priority queue of 4-tuples of the form 30 | ``(t, counter, function, function_arguments)`` 31 | The ``'counter'`` is present just to break ties, which generally only occur when 32 | multiple events are put in place for the initial condition, but could also 33 | occur in cases where events tend to happen at discrete times. 34 | note that the function is understood to have its first argument be t, and 35 | the tuple ``function_arguments`` does not include this first t. 36 | So function is called as 37 | ``function(t, *function_arguments)`` 38 | Previously I used a class of events, but sorting using the __lt__ function 39 | I wrote was significantly slower than simply using tuples. 40 | """ 41 | 42 | def __init__(self, tmax=float("Inf")): 43 | self._Q_ = [] 44 | self.tmax = tmax 45 | self.counter = 0 # tie-breaker for putting things in priority queue 46 | 47 | def add(self, time, function, args=()): 48 | """Add event to the queue. 49 | 50 | Parameters 51 | ---------- 52 | time : float 53 | time of the event 54 | function : function name 55 | name of the function to run when the event is popped. 56 | args : keyword args 57 | args of the function excluding time. 58 | """ 59 | if time < self.tmax: 60 | heapq.heappush(self._Q_, (time, self.counter, function, args)) 61 | self.counter += 1 62 | 63 | def pop_and_run(self): 64 | """Pops the next event off the queue and performs the function""" 65 | t, counter, function, args = heapq.heappop(self._Q_) 66 | function(t, *args) 67 | 68 | def __len__(self): 69 | """this allows us to use commands like ``while Q:`` 70 | 71 | Returns 72 | ------- 73 | int 74 | number of events currently in the queue 75 | """ 76 | return len(self._Q_) 77 | 78 | 79 | class SamplingDict: 80 | """ 81 | The Gillespie algorithm will involve a step that samples a random element 82 | from a set based on its weight. This is awkward in Python. 83 | 84 | So I'm introducing a new class based on a stack overflow answer by 85 | Amber (http://stackoverflow.com/users/148870/amber) 86 | for a question by 87 | tba (http://stackoverflow.com/users/46521/tba) 88 | found at 89 | http://stackoverflow.com/a/15993515/2966723 90 | 91 | This will allow selecting a random element uniformly, and then use 92 | rejection sampling to make sure it's been selected with the appropriate 93 | weight. 94 | """ 95 | 96 | def __init__(self, weighted=False): 97 | self.item_to_position = {} 98 | self.items = [] 99 | 100 | self.weighted = weighted 101 | if self.weighted: 102 | self.weight = defaultdict(int) # presume all weights positive 103 | self.max_weight = 0 104 | self._total_weight = 0 105 | self.max_weight_count = 0 106 | 107 | def __len__(self): 108 | """Number of items in the dict 109 | 110 | Returns 111 | ------- 112 | int 113 | number of items in dict 114 | """ 115 | return len(self.items) 116 | 117 | def __contains__(self, item): 118 | """Whether an item exists in the dictionary""" 119 | return item in self.item_to_position 120 | 121 | def _update_max_weight(self): 122 | """Internal function to help with the rejection sampling""" 123 | C = Counter( 124 | self.weight.values() 125 | ) # may be a faster way to do this, we only need to count the max. 126 | self.max_weight = max(C.keys()) 127 | self.max_weight_count = C[self.max_weight] 128 | 129 | def insert(self, item, weight=None): 130 | """insert an item into the sampling dict 131 | 132 | Parameters 133 | ---------- 134 | item : hashable 135 | the ID of the item 136 | weight : float, default: None 137 | the weight of the item, if None, unweighted. 138 | 139 | Notes 140 | ----- 141 | If not present, then inserts the thing (with weight if appropriate) 142 | if already there, replaces the weight unless weight is 0 143 | 144 | If weight is 0, then it removes the item and doesn't replace. 145 | 146 | replaces weight if already present, does not increment weight. 147 | """ 148 | 149 | if self.__contains__(item): 150 | self.remove(item) 151 | if weight != 0: 152 | self.update(item, weight_increment=weight) 153 | 154 | def update(self, item, weight_increment=None): 155 | """_summary_ 156 | 157 | Parameters 158 | ---------- 159 | item : hashable 160 | ID of the item 161 | weight_increment : float, default: None 162 | how much to increment the weight if weighted. 163 | 164 | Raises 165 | ------ 166 | Exception 167 | if weighted and no weight increment specified. 168 | 169 | Notes 170 | ----- 171 | If not present, then inserts the item (with weight if appropriate) 172 | if already there, increments weight 173 | 174 | increments weight if already present, cannot overwrite weight. 175 | """ 176 | if ( 177 | weight_increment is not None 178 | ): # will break if passing a weight to unweighted case 179 | if weight_increment > 0 or self.weight[item] != self.max_weight: 180 | self.weight[item] = self.weight[item] + weight_increment 181 | self._total_weight += weight_increment 182 | if self.weight[item] > self.max_weight: 183 | self.max_weight_count = 1 184 | self.max_weight = self.weight[item] 185 | elif self.weight[item] == self.max_weight: 186 | self.max_weight_count += 1 187 | else: # it's a negative increment and was at max 188 | self.max_weight_count -= 1 189 | self.weight[item] = self.weight[item] + weight_increment 190 | self._total_weight += weight_increment 191 | self.max_weight_count -= 1 192 | if self.max_weight_count == 0: 193 | self._update_max_weight 194 | elif self.weighted: 195 | raise Exception("if weighted, must assign weight_increment") 196 | 197 | if item in self: # we've already got it, do nothing else 198 | return 199 | self.items.append(item) 200 | self.item_to_position[item] = len(self.items) - 1 201 | 202 | def remove(self, choice): 203 | """Remove item and update weights 204 | 205 | Parameters 206 | ---------- 207 | choice : hashable 208 | item ID 209 | """ 210 | position = self.item_to_position.pop( 211 | choice 212 | ) # why don't we pop off the last item and put it in the choice index? 213 | last_item = self.items.pop() 214 | if position != len(self.items): 215 | self.items[position] = last_item 216 | self.item_to_position[last_item] = position 217 | 218 | if self.weighted: 219 | weight = self.weight.pop(choice) 220 | self._total_weight -= weight 221 | if weight == self.max_weight: 222 | # if we find ourselves in this case often 223 | # it may be better just to let max_weight be the 224 | # largest weight *ever* encountered, even if all remaining weights are less 225 | # 226 | self.max_weight_count -= 1 227 | if self.max_weight_count == 0 and len(self) > 0: 228 | self._update_max_weight() 229 | 230 | def choose_random(self): 231 | """chooses a random node. If there is a weight, it will use rejection 232 | sampling to choose a random node until it succeeds""" 233 | if self.weighted: 234 | while True: 235 | choice = random.choice(self.items) 236 | if random.random() < self.weight[choice] / self.max_weight: 237 | break 238 | return choice 239 | 240 | else: 241 | return random.choice(self.items) 242 | 243 | def random_removal(self): 244 | """uses other class methods to choose and then remove a random item""" 245 | choice = self.choose_random() 246 | self.remove(choice) 247 | return choice 248 | 249 | def total_weight(self): 250 | """Get the sum of all the weights in the dict.""" 251 | if self.weighted: 252 | return self._total_weight 253 | else: 254 | return len(self) 255 | 256 | def update_total_weight(self): 257 | """Update the sum of weights.""" 258 | self._total_weight = sum(self.weight[item] for item in self.items) 259 | 260 | 261 | def choice(arr, p): 262 | """ 263 | Returns a random element from ``arr`` with probability given in array ``p``. 264 | If ``arr`` is not an iterable, the function returns the index of the chosen element. 265 | """ 266 | ndx = np.argmax(np.random.rand() < np.cumsum(p)) 267 | try: 268 | return arr[ndx] 269 | except TypeError as e: 270 | return ndx 271 | 272 | 273 | class MockSamplableSet: 274 | """ 275 | A set of items that can be sampled with probability 276 | proportional to a corresponding item weight. 277 | 278 | Mimicks the behavior of github.com/gstonge/SamplableSet 279 | without being as efficient. 280 | 281 | Works similar to Python's set, with ``__getitem__``, 282 | ``__setitem__``, ``__delitem__``, ``__iter__``, 283 | ``__len__``, ``__contains__``. 284 | 285 | Parameters 286 | ========== 287 | min_weight : float 288 | minimum possible weight 289 | max_weight : float 290 | maximum possible weight 291 | weighted_elements : list, default = [] 292 | list of 2-tuples, first entry an item, second entry a weight 293 | cpp_type : str, default = 'int' 294 | The type of the items. 295 | 296 | Attributes 297 | ========== 298 | min_weight : float 299 | minimum possible weight 300 | max_weight : float 301 | maximum possible weight 302 | items : numpy.ndarray 303 | list of items in this set 304 | weights : numpy.ndarray 305 | list of corresponding weights 306 | """ 307 | 308 | def __init__(self, min_weight, max_weight, weighted_elements=[], cpp_type="int"): 309 | 310 | self.min_weight = min_weight 311 | self.max_weight = max_weight 312 | 313 | if type(weighted_elements) == dict: 314 | weighted_elements = list(weighted_elements.items()) 315 | 316 | self.items = np.array([e[0] for e in weighted_elements], dtype=cpp_type) 317 | self.weights = np.array([e[1] for e in weighted_elements], dtype=float) 318 | sort_ndx = np.argsort(self.items) 319 | self.items = self.items[sort_ndx] 320 | self.weights = self.weights[sort_ndx] 321 | self._total_weight = self.weights.sum() 322 | 323 | if np.any(self.weights < self.min_weight): 324 | raise ValueError("There are weights below the limit.") 325 | 326 | if np.any(self.weights > self.max_weight): 327 | raise ValueError("There are weights above the limit.") 328 | 329 | def sample(self): 330 | """ 331 | Random sample from the set, sampled 332 | with probability proportional to items' weight. 333 | 334 | Returns 335 | ======= 336 | item : cpp_type 337 | An item from the set 338 | weight : float 339 | The weight of the item 340 | """ 341 | 342 | # ndx = np.random.choice(len(self.items),p=self.weights/self._total_weight) 343 | # ndx = np.argwhere(np.random.rand() self.max_weight: 363 | raise ValueError( 364 | "Inserting element-weight pair " 365 | + str(key) 366 | + " " 367 | + str(value) 368 | + " \n" 369 | + "has weight value out of bounds of " 370 | + str(self.min_weight) 371 | + " " 372 | + str(self.max_weight) 373 | ) 374 | found_key, ndx = self._find_key(key) 375 | if not found_key: 376 | self.items = np.insert(self.items, ndx, key) 377 | self.weights = np.insert(self.weights, ndx, value) 378 | else: 379 | self.weights[ndx] = value 380 | 381 | self._total_weight = self.weights.sum() 382 | 383 | def _find_key(self, key): 384 | ndx = np.searchsorted(self.items, key) 385 | return (not ((ndx == len(self.items) or self.items[ndx] != key)), ndx) 386 | 387 | def __iter__(self): 388 | self._ndx = 0 389 | return self 390 | 391 | def __next__(self): 392 | if self._ndx < len(self.items): 393 | i, w = self.items[self._ndx], self.weights[self._ndx] 394 | self._ndx += 1 395 | return (i, w) 396 | else: 397 | raise StopIteration 398 | 399 | def __len__(self): 400 | return len(self.items) 401 | 402 | def __contains__(self, key): 403 | return self._find_key(key)[0] 404 | 405 | def total_weight(self): 406 | """Obtain the total weight of the set""" 407 | return self._total_weight 408 | 409 | def clear(self): 410 | """Reset the set. Not implemented yet.""" 411 | pass 412 | 413 | 414 | def _process_trans_SIR_( 415 | t, 416 | times, 417 | S, 418 | I, 419 | R, 420 | Q, 421 | H, 422 | status, 423 | transmission_function, 424 | gamma, 425 | tau, 426 | source, 427 | target, 428 | rec_time, 429 | pred_inf_time, 430 | events, 431 | ): 432 | 433 | if status[target] == "S": # nothing happens if already infected. 434 | status[target] = "I" 435 | times.append(t) 436 | events.append( 437 | { 438 | "time": t, 439 | "source": source, 440 | "target": target, 441 | "old_state": "S", 442 | "new_state": "I", 443 | } 444 | ) 445 | S.append(S[-1] - 1) # one less susceptible 446 | I.append(I[-1] + 1) # one more infected 447 | R.append(R[-1]) # no change to recovered 448 | 449 | rec_time[target] = t + rec_delay(gamma) 450 | if rec_time[target] < Q.tmax: 451 | Q.add( 452 | rec_time[target], 453 | _process_rec_SIR_, 454 | args=(times, S, I, R, status, target, events), 455 | ) 456 | 457 | for edge_id in H.nodes.memberships(target): 458 | edge = H.edges.members(edge_id) 459 | for nbr in edge: 460 | if status[nbr] == "S": 461 | inf_time = t + trans_delay(tau, edge) 462 | 463 | # create statuses at the time requested 464 | temp_status = defaultdict(lambda: "R") 465 | for node in edge: 466 | if status[node] == "I" and rec_time[node] > inf_time: 467 | temp_status[node] = "I" 468 | elif status[node] == "S": 469 | temp_status[node] = "S" 470 | 471 | contagion = transmission_function(nbr, temp_status, edge) 472 | if contagion != 0 and inf_time < pred_inf_time[nbr]: 473 | Q.add( 474 | inf_time, 475 | _process_trans_SIR_, 476 | args=( 477 | times, 478 | S, 479 | I, 480 | R, 481 | Q, 482 | H, 483 | status, 484 | transmission_function, 485 | gamma, 486 | tau, 487 | edge_id, 488 | nbr, 489 | rec_time, 490 | pred_inf_time, 491 | events, 492 | ), 493 | ) 494 | pred_inf_time[nbr] = inf_time 495 | 496 | 497 | def _process_rec_SIR_(t, times, S, I, R, status, node, events): 498 | times.append(t) 499 | events.append( 500 | {"time": t, "source": None, "target": node, "old_state": "I", "new_state": "R"} 501 | ) 502 | S.append(S[-1]) # no change to number susceptible 503 | I.append(I[-1] - 1) # one less infected 504 | R.append(R[-1] + 1) # one more recovered 505 | status[node] = "R" 506 | 507 | 508 | def _process_trans_SIS_( 509 | t, 510 | times, 511 | S, 512 | I, 513 | Q, 514 | H, 515 | status, 516 | transmission_function, 517 | gamma, 518 | tau, 519 | source, 520 | target, 521 | rec_time, 522 | pred_inf_time, 523 | events, 524 | ): 525 | 526 | if status[target] == "S": 527 | status[target] = "I" 528 | events.append( 529 | { 530 | "time": t, 531 | "source": source, 532 | "target": target, 533 | "old_state": "S", 534 | "new_state": "I", 535 | } 536 | ) 537 | I.append(I[-1] + 1) # one more infected 538 | S.append(S[-1] - 1) # one less susceptible 539 | times.append(t) 540 | 541 | rec_time[target] = t + rec_delay(gamma) 542 | 543 | if rec_time[target] < Q.tmax: 544 | Q.add( 545 | rec_time[target], 546 | _process_rec_SIS_, 547 | args=(times, S, I, status, target, events), 548 | ) 549 | 550 | for edge_id in H.nodes.memberships(target): 551 | edge = H.edges.members(edge_id) 552 | for nbr in edge: 553 | if status[nbr] == "S": 554 | inf_time = t + trans_delay(tau, edge) 555 | 556 | # create statuses at the time requested 557 | temp_status = defaultdict(lambda: "S") 558 | for node in edge: 559 | if status[node] == "I" and rec_time[node] >= inf_time: 560 | temp_status[node] = "I" 561 | 562 | contagion = transmission_function(nbr, temp_status, edge) 563 | if contagion != 0 and inf_time < pred_inf_time[nbr]: 564 | Q.add( 565 | inf_time, 566 | _process_trans_SIS_, 567 | args=( 568 | times, 569 | S, 570 | I, 571 | Q, 572 | H, 573 | status, 574 | transmission_function, 575 | gamma, 576 | tau, 577 | edge_id, 578 | nbr, 579 | rec_time, 580 | pred_inf_time, 581 | events, 582 | ), 583 | ) 584 | pred_inf_time[nbr] = inf_time 585 | 586 | 587 | def _process_rec_SIS_(t, times, S, I, status, node, events): 588 | times.append(t) 589 | events.append( 590 | {"time": t, "source": None, "target": node, "old_state": "I", "new_state": "S"} 591 | ) 592 | S.append(S[-1] + 1) # one more susceptible 593 | I.append(I[-1] - 1) # one less infected 594 | status[node] = "S" 595 | 596 | 597 | def rec_delay(rate): 598 | try: 599 | return random.expovariate(rate) 600 | except: 601 | return float("Inf") 602 | 603 | 604 | def trans_delay(tau, edge): 605 | try: 606 | return random.expovariate(tau[len(edge)]) 607 | except ZeroDivisionError: 608 | return np.inf 609 | -------------------------------------------------------------------------------- /hypercontagion/sim/epidemics.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classic epidemiological models extended to higher-order contagion. 3 | """ 4 | 5 | import random 6 | from collections import defaultdict 7 | 8 | import numpy as np 9 | import xgi 10 | 11 | from ..exception import HyperContagionError 12 | from ..utils import EventQueue, SamplingDict, _process_trans_SIR_, _process_trans_SIS_ 13 | from .functions import majority_vote, threshold 14 | 15 | 16 | def discrete_SIR( 17 | H, 18 | tau, 19 | gamma, 20 | transmission_function=threshold, 21 | initial_infecteds=None, 22 | initial_recovereds=None, 23 | recovery_weight=None, 24 | transmission_weight=None, 25 | rho=None, 26 | tmin=0, 27 | tmax=float("Inf"), 28 | dt=1.0, 29 | return_event_data=False, 30 | seed=None, 31 | **args 32 | ): 33 | """Simulates the discrete SIR model for hypergraphs. 34 | 35 | Parameters 36 | ---------- 37 | H : xgi.Hypergraph 38 | The hypergraph on which to simulate the SIR contagion process 39 | tau : dict 40 | Keys are edge sizes and values are transmission rates 41 | gamma : float 42 | Healing rate 43 | transmission_function : lambda function, default: threshold 44 | The contagion function that determines whether transmission is possible. 45 | initial_infecteds : iterable, default: None 46 | Initially infected node IDs. 47 | initial_recovereds : iterable, default: None 48 | Initially recovered node IDs. 49 | recovery_weight : hashable, default: None 50 | Hypergraph node attribute that weights the healing rate. 51 | transmission_weight : hashable, default: None 52 | Hypergraph edge attribute that weights the transmission rate. 53 | rho : float, default: None 54 | Fraction initially infected. Cannot be specified if 55 | `initial_infecteds` is defined. 56 | tmin : float, default: 0 57 | Time at which the simulation starts. 58 | tmax : float, default: float("Inf") 59 | Time at which the simulation terminates if there are still 60 | infected nodes. 61 | dt : float, default: 1.0 62 | The time step of the simulation. 63 | return_event_data : bool, default: False 64 | Whether to track each individual transition event that occurs. 65 | seed : integer, random_state, or None (default) 66 | Indicator of random number generation state. 67 | 68 | Returns 69 | ------- 70 | tuple of np.arrays 71 | t, S, I, R 72 | 73 | Raises 74 | ------ 75 | HyperContagionError 76 | If the user specifies both rho and initial_infecteds. 77 | """ 78 | if seed is not None: 79 | random.seed(seed) 80 | members = H.edges.members(dtype=dict) 81 | memberships = H.nodes.memberships() 82 | 83 | if rho is not None and initial_infecteds is not None: 84 | raise HyperContagionError("cannot define both initial_infecteds and rho") 85 | 86 | if return_event_data: 87 | events = list() 88 | 89 | if initial_infecteds is None: 90 | if rho is None: 91 | initial_number = 1 92 | else: 93 | initial_number = int(round(H.num_nodes * rho)) 94 | initial_infecteds = random.sample(list(H.nodes), initial_number) 95 | 96 | if initial_recovereds is None: 97 | initial_recovereds = [] 98 | 99 | if transmission_weight is not None: 100 | 101 | def edgeweight(item): 102 | return item[transmission_weight] 103 | 104 | else: 105 | 106 | def edgeweight(item): 107 | return 1 108 | 109 | if recovery_weight is not None: 110 | 111 | def nodeweight(u): 112 | return H.nodes[u][recovery_weight] 113 | 114 | else: 115 | 116 | def nodeweight(u): 117 | return 1 118 | 119 | status = defaultdict(lambda: "S") 120 | for node in initial_infecteds: 121 | status[node] = "I" 122 | 123 | if return_event_data: 124 | events.append( 125 | { 126 | "time": tmin, 127 | "source": None, 128 | "target": node, 129 | "old_state": "S", 130 | "new_state": "I", 131 | } 132 | ) 133 | 134 | for node in initial_recovereds: 135 | status[node] = "R" 136 | 137 | if return_event_data: 138 | events.append( 139 | { 140 | "time": tmin, 141 | "source": None, 142 | "target": node, 143 | "old_state": "I", 144 | "new_state": "R", 145 | } 146 | ) 147 | 148 | if return_event_data: 149 | for node in ( 150 | set(H.nodes).difference(initial_infecteds).difference(initial_recovereds) 151 | ): 152 | events.append( 153 | { 154 | "time": tmin, 155 | "source": None, 156 | "target": node, 157 | "old_state": None, 158 | "new_state": "S", 159 | } 160 | ) 161 | 162 | I = [len(initial_infecteds)] 163 | R = [len(initial_recovereds)] 164 | S = [H.num_nodes - I[0] - R[0]] 165 | times = [tmin] 166 | t = tmin 167 | 168 | new_status = status 169 | 170 | while t <= tmax and I[-1] != 0: 171 | S.append(S[-1]) 172 | I.append(I[-1]) 173 | R.append(R[-1]) 174 | 175 | for node in H.nodes: 176 | if status[node] == "I": 177 | # heal 178 | if random.random() <= gamma * dt * nodeweight(node): 179 | new_status[node] = "R" 180 | R[-1] += 1 181 | I[-1] += -1 182 | 183 | if return_event_data: 184 | events.append( 185 | { 186 | "time": t, 187 | "source": None, 188 | "target": node, 189 | "old_state": "I", 190 | "new_state": "R", 191 | } 192 | ) 193 | else: 194 | new_status[node] = "I" 195 | elif status[node] == "S": 196 | # infect by neighbors of all sizes 197 | for edge_id in memberships[node]: 198 | edge = members[edge_id] 199 | if tau[len(edge)] > 0: 200 | if random.random() <= tau[len(edge)] * transmission_function( 201 | node, status, edge, **args 202 | ) * dt * edgeweight(edge_id): 203 | new_status[node] = "I" 204 | S[-1] += -1 205 | I[-1] += 1 206 | 207 | if return_event_data: 208 | events.append( 209 | { 210 | "time": t, 211 | "source": edge_id, 212 | "target": node, 213 | "old_state": "S", 214 | "new_state": "I", 215 | } 216 | ) 217 | break 218 | else: 219 | new_status[node] == "S" 220 | status = new_status.copy() 221 | t += dt 222 | times.append(t) 223 | if return_event_data: 224 | return events 225 | else: 226 | return np.array(times), np.array(S), np.array(I), np.array(R) 227 | 228 | 229 | def discrete_SIS( 230 | H, 231 | tau, 232 | gamma, 233 | transmission_function=threshold, 234 | initial_infecteds=None, 235 | recovery_weight=None, 236 | transmission_weight=None, 237 | rho=None, 238 | tmin=0, 239 | tmax=float("Inf"), 240 | dt=1.0, 241 | return_event_data=False, 242 | seed=None, 243 | **args 244 | ): 245 | """Simulates the discrete SIS model for hypergraphs. 246 | 247 | Parameters 248 | ---------- 249 | H : xgi.Hypergraph 250 | The hypergraph on which to simulate the SIR contagion process 251 | tau : dict 252 | Keys are edge sizes and values are transmission rates 253 | gamma : float 254 | Healing rate 255 | transmission_function : lambda function, default: threshold 256 | The contagion function that determines whether transmission is possible. 257 | initial_infecteds : iterable, default: None 258 | Initially infected node IDs. 259 | initial_recovereds : iterable, default: None 260 | Initially recovered node IDs. 261 | recovery_weight : hashable, default: None 262 | Hypergraph node attribute that weights the healing rate. 263 | transmission_weight : hashable, default: None 264 | Hypergraph edge attribute that weights the transmission rate. 265 | rho : float, default: None 266 | Fraction initially infected. Cannot be specified if 267 | `initial_infecteds` is defined. 268 | tmin : float, default: 0 269 | Time at which the simulation starts. 270 | tmax : float, default: float("Inf") 271 | Time at which the simulation terminates if there are still 272 | infected nodes. 273 | dt : float, default: 1.0 274 | The time step of the simulation. 275 | return_event_data : bool, default: False 276 | Whether to track each individual transition event that occurs. 277 | seed : integer, random_state, or None (default) 278 | Indicator of random number generation state. 279 | 280 | Returns 281 | ------- 282 | tuple of np.arrays 283 | t, S, I 284 | 285 | Raises 286 | ------ 287 | HyperContagionError 288 | If the user specifies both rho and initial_infecteds. 289 | """ 290 | 291 | if seed is not None: 292 | random.seed(seed) 293 | 294 | members = H.edges.members(dtype=dict) 295 | memberships = H.nodes.memberships() 296 | 297 | if rho is not None and initial_infecteds is not None: 298 | raise HyperContagionError("cannot define both initial_infecteds and rho") 299 | 300 | if return_event_data: 301 | events = list() 302 | 303 | if initial_infecteds is None: 304 | if rho is None: 305 | initial_number = 1 306 | else: 307 | initial_number = int(round(H.num_nodes * rho)) 308 | initial_infecteds = random.sample(list(H.nodes), initial_number) 309 | 310 | if transmission_weight is not None: 311 | 312 | def edgeweight(item): 313 | return item[transmission_weight] 314 | 315 | else: 316 | 317 | def edgeweight(item): 318 | return 1 319 | 320 | if recovery_weight is not None: 321 | 322 | def nodeweight(u): 323 | return H.nodes[u][recovery_weight] 324 | 325 | else: 326 | 327 | def nodeweight(u): 328 | return 1 329 | 330 | status = defaultdict(lambda: "S") 331 | for node in initial_infecteds: 332 | status[node] = "I" 333 | 334 | if return_event_data: 335 | events.append( 336 | { 337 | "time": tmin, 338 | "source": None, 339 | "target": node, 340 | "old_state": "S", 341 | "new_state": "I", 342 | } 343 | ) 344 | 345 | if return_event_data: 346 | for node in set(H.nodes).difference(initial_infecteds): 347 | events.append( 348 | { 349 | "time": tmin, 350 | "source": None, 351 | "target": node, 352 | "old_state": "I", 353 | "new_state": "S", 354 | } 355 | ) 356 | 357 | I = [len(initial_infecteds)] 358 | S = [H.num_nodes - I[0]] 359 | times = [tmin] 360 | t = tmin 361 | new_status = status 362 | 363 | while t <= tmax and I[-1] != 0: 364 | S.append(S[-1]) 365 | I.append(I[-1]) 366 | 367 | for node in H.nodes: 368 | if status[node] == "I": 369 | # heal 370 | if random.random() <= gamma * dt * nodeweight(node): 371 | new_status[node] = "S" 372 | S[-1] += 1 373 | I[-1] += -1 374 | 375 | if return_event_data: 376 | events.append( 377 | { 378 | "time": t, 379 | "source": None, 380 | "target": node, 381 | "old_state": "I", 382 | "new_state": "S", 383 | } 384 | ) 385 | else: 386 | new_status[node] = "I" 387 | else: 388 | # infect by neighbors of all sizes 389 | for edge_id in memberships[node]: 390 | edge = members[edge_id] 391 | if tau[len(edge)] > 0: 392 | if random.random() <= tau[len(edge)] * transmission_function( 393 | node, status, edge, **args 394 | ) * dt * edgeweight(edge_id): 395 | new_status[node] = "I" 396 | S[-1] += -1 397 | I[-1] += 1 398 | 399 | if return_event_data: 400 | events.append( 401 | { 402 | "time": t, 403 | "source": edge_id, 404 | "target": node, 405 | "old_state": "S", 406 | "new_state": "I", 407 | } 408 | ) 409 | break 410 | else: 411 | new_status[node] == "S" 412 | status = new_status.copy() 413 | t += dt 414 | times.append(t) 415 | if return_event_data: 416 | return events 417 | else: 418 | return np.array(times), np.array(S), np.array(I) 419 | 420 | 421 | def Gillespie_SIR( 422 | H, 423 | tau, 424 | gamma, 425 | transmission_function=threshold, 426 | initial_infecteds=None, 427 | initial_recovereds=None, 428 | rho=None, 429 | tmin=0, 430 | tmax=float("Inf"), 431 | recovery_weight=None, 432 | transmission_weight=None, 433 | return_event_data=False, 434 | seed=None, 435 | **args 436 | ): 437 | """Simulates the SIR model for hypergraphs with the Gillespie algorithm. 438 | 439 | Parameters 440 | ---------- 441 | H : xgi.Hypergraph 442 | The hypergraph on which to simulate the SIR contagion process 443 | tau : dict 444 | Keys are edge sizes and values are transmission rates 445 | gamma : float 446 | Healing rate 447 | transmission_function : lambda function, default: threshold 448 | The contagion function that determines whether transmission is possible. 449 | initial_infecteds : iterable, default: None 450 | Initially infected node IDs. 451 | initial_recovereds : iterable, default: None 452 | Initially recovered node IDs. 453 | recovery_weight : hashable, default: None 454 | Hypergraph node attribute that weights the healing rate. 455 | transmission_weight : hashable, default: None 456 | Hypergraph edge attribute that weights the transmission rate. 457 | rho : float, default: None 458 | Fraction initially infected. Cannot be specified if 459 | `initial_infecteds` is defined. 460 | tmin : float, default: 0 461 | Time at which the simulation starts. 462 | tmax : float, default: float("Inf") 463 | Time at which the simulation terminates if there are still 464 | infected nodes. 465 | return_event_data : bool, default: False 466 | Whether to track each individual transition event that occurs. 467 | seed : integer, random_state, or None (default) 468 | Indicator of random number generation state. 469 | 470 | Returns 471 | ------- 472 | tuple of np.arrays 473 | t, S, I, R 474 | 475 | Raises 476 | ------ 477 | HyperContagionError 478 | If the user specifies both rho and initial_infecteds. 479 | """ 480 | if seed is not None: 481 | random.seed(seed) 482 | 483 | members = H.edges.members(dtype=dict) 484 | memberships = H.nodes.memberships() 485 | 486 | if rho is not None and initial_infecteds is not None: 487 | raise HyperContagionError("cannot define both initial_infecteds and rho") 488 | 489 | if return_event_data: 490 | events = list() 491 | 492 | if transmission_weight is not None: 493 | 494 | def edgeweight(item): 495 | return item[transmission_weight] 496 | 497 | else: 498 | 499 | def edgeweight(item): 500 | return None 501 | 502 | if recovery_weight is not None: 503 | 504 | def nodeweight(u): 505 | return H.nodes[u][recovery_weight] 506 | 507 | else: 508 | 509 | def nodeweight(u): 510 | return None 511 | 512 | if initial_infecteds is None: 513 | if rho is None: 514 | initial_number = 1 515 | else: 516 | initial_number = int(round(H.num_nodes * rho)) 517 | initial_infecteds = random.sample(list(H.nodes), initial_number) 518 | 519 | if initial_recovereds is None: 520 | initial_recovereds = [] 521 | 522 | I = [len(initial_infecteds)] 523 | R = [len(initial_recovereds)] 524 | S = [H.num_nodes - I[0] - R[0]] 525 | times = [tmin] 526 | 527 | t = tmin 528 | 529 | status = defaultdict(lambda: "S") 530 | for node in initial_infecteds: 531 | status[node] = "I" 532 | 533 | if return_event_data: 534 | events.append( 535 | { 536 | "time": tmin, 537 | "source": None, 538 | "target": node, 539 | "old_state": "S", 540 | "new_state": "I", 541 | } 542 | ) 543 | 544 | for node in initial_recovereds: 545 | status[node] = "R" 546 | 547 | if return_event_data: 548 | events.append( 549 | { 550 | "time": tmin, 551 | "source": None, 552 | "target": node, 553 | "old_state": "I", 554 | "new_state": "R", 555 | } 556 | ) 557 | 558 | if return_event_data: 559 | for node in ( 560 | set(H.nodes).difference(initial_infecteds).difference(initial_recovereds) 561 | ): 562 | events.append( 563 | { 564 | "time": tmin, 565 | "source": None, 566 | "target": node, 567 | "old_state": None, 568 | "new_state": "S", 569 | } 570 | ) 571 | 572 | if recovery_weight is None: 573 | infecteds = SamplingDict() 574 | else: 575 | infecteds = SamplingDict(weighted=True) 576 | 577 | unique_edge_sizes = xgi.unique_edge_sizes(H) 578 | IS_links = dict() 579 | for size in unique_edge_sizes: 580 | if transmission_weight is None: 581 | IS_links[size] = SamplingDict() 582 | else: 583 | IS_links[size] = SamplingDict(weighted=True) 584 | 585 | for node in initial_infecteds: 586 | infecteds.update(node, weight_increment=nodeweight(node)) 587 | for edge_id in memberships[node]: 588 | edge = members[edge_id] 589 | for nbr in edge: 590 | if status[nbr] == "S": 591 | contagion = transmission_function(nbr, status, edge, **args) 592 | if contagion != 0: 593 | IS_links[len(edge)].update( 594 | (edge_id, nbr), 595 | weight_increment=edgeweight(edge_id), 596 | ) # need to be able to multiply by the contagion? 597 | 598 | total_rates = dict() 599 | total_rates[0] = gamma * infecteds.total_weight() # I_weight_sum 600 | for size in unique_edge_sizes: 601 | total_rates[size] = tau[size] * IS_links[size].total_weight() # IS_weight_sum 602 | 603 | total_rate = sum(total_rates.values()) 604 | 605 | if total_rate > 0: 606 | delay = random.expovariate(total_rate) 607 | else: 608 | print("Total rate is zero and no events will happen!") 609 | delay = float("Inf") 610 | 611 | t += delay 612 | 613 | while infecteds and t < tmax: 614 | while True: 615 | choice = random.choice( 616 | list(total_rates.keys()) 617 | ) # Is there a faster way to do this? 618 | if random.random() < total_rates[choice] / total_rate: 619 | break 620 | if choice == 0: # recover 621 | # does weighted choice and removes it 622 | recovering_node = infecteds.random_removal() 623 | status[recovering_node] = "R" 624 | 625 | if return_event_data: 626 | events.append( 627 | { 628 | "time": t, 629 | "source": None, 630 | "target": recovering_node, 631 | "old_state": "I", 632 | "new_state": "R", 633 | } 634 | ) 635 | 636 | for edge_id in memberships[recovering_node]: 637 | edge = members[edge_id] 638 | for nbr in edge: 639 | if status[nbr] == "S" and (edge_id, nbr) in IS_links[len(edge)]: 640 | contagion = transmission_function(nbr, status, edge, **args) 641 | if contagion == 0: 642 | try: 643 | IS_links[len(edge)].remove((edge_id, nbr)) 644 | except: 645 | pass 646 | 647 | times.append(t) 648 | S.append(S[-1]) 649 | I.append(I[-1] - 1) 650 | R.append(R[-1] + 1) 651 | else: # transmit 652 | source, recipient = IS_links[ 653 | choice 654 | ].choose_random() # we don't use remove since that complicates the later removal of edges. 655 | status[recipient] = "I" 656 | 657 | infecteds.update(recipient, weight_increment=nodeweight(recipient)) 658 | 659 | if return_event_data: 660 | events.append( 661 | { 662 | "time": t, 663 | "source": source, 664 | "target": recipient, 665 | "old_state": "S", 666 | "new_state": "I", 667 | } 668 | ) 669 | 670 | for edge_id in memberships[recipient]: 671 | try: 672 | IS_links[len(members[edge_id])].remove((edge_id, recipient)) 673 | except: 674 | pass 675 | 676 | for edge_id in memberships[recipient]: 677 | edge = members[edge_id] 678 | for nbr in edge: 679 | if status[nbr] == "S": 680 | contagion = transmission_function(nbr, status, edge, **args) 681 | if contagion != 0: 682 | IS_links[len(edge)].update( 683 | (edge_id, nbr), 684 | weight_increment=edgeweight(edge_id), 685 | ) 686 | 687 | times.append(t) 688 | S.append(S[-1] - 1) 689 | I.append(I[-1] + 1) 690 | R.append(R[-1]) 691 | 692 | total_rates[0] = gamma * infecteds.total_weight() # I_weight_sum 693 | for size in unique_edge_sizes: 694 | total_rates[size] = ( 695 | tau[size] * IS_links[size].total_weight() 696 | ) # IS_weight_sum 697 | 698 | total_rate = sum(total_rates.values()) 699 | if total_rate > 0: 700 | delay = random.expovariate(total_rate) 701 | else: 702 | delay = float("Inf") 703 | t += delay 704 | 705 | if return_event_data: 706 | return events 707 | else: 708 | return np.array(times), np.array(S), np.array(I), np.array(R) 709 | 710 | 711 | def Gillespie_SIS( 712 | H, 713 | tau, 714 | gamma, 715 | transmission_function=threshold, 716 | initial_infecteds=None, 717 | rho=None, 718 | tmin=0, 719 | tmax=100, 720 | recovery_weight=None, 721 | transmission_weight=None, 722 | return_event_data=False, 723 | seed=None, 724 | **args 725 | ): 726 | """Simulates the SIS model for hypergraphs with the Gillespie algorithm. 727 | 728 | Parameters 729 | ---------- 730 | H : xgi.Hypergraph 731 | The hypergraph on which to simulate the SIR contagion process 732 | tau : dict 733 | Keys are edge sizes and values are transmission rates 734 | gamma : float 735 | Healing rate 736 | transmission_function : lambda function, default: threshold 737 | The contagion function that determines whether transmission is possible. 738 | initial_infecteds : iterable, default: None 739 | Initially infected node IDs. 740 | initial_recovereds : iterable, default: None 741 | Initially recovered node IDs. 742 | recovery_weight : hashable, default: None 743 | Hypergraph node attribute that weights the healing rate. 744 | transmission_weight : hashable, default: None 745 | Hypergraph edge attribute that weights the transmission rate. 746 | rho : float, default: None 747 | Fraction initially infected. Cannot be specified if 748 | `initial_infecteds` is defined. 749 | tmin : float, default: 0 750 | Time at which the simulation starts. 751 | tmax : float, default: float("Inf") 752 | Time at which the simulation terminates if there are still 753 | infected nodes. 754 | return_event_data : bool, default: False 755 | Whether to track each individual transition event that occurs. 756 | seed : integer, random_state, or None (default) 757 | Indicator of random number generation state. 758 | 759 | Returns 760 | ------- 761 | tuple of np.arrays 762 | t, S, I 763 | 764 | Raises 765 | ------ 766 | HyperContagionError 767 | If the user specifies both rho and initial_infecteds. 768 | """ 769 | if seed is not None: 770 | random.seed(seed) 771 | 772 | members = H.edges.members(dtype=dict) 773 | memberships = H.nodes.memberships() 774 | 775 | if rho is not None and initial_infecteds is not None: 776 | raise HyperContagionError("cannot define both initial_infecteds and rho") 777 | 778 | if return_event_data: 779 | events = list() 780 | 781 | if transmission_weight is not None: 782 | 783 | def edgeweight(item): 784 | return item[transmission_weight] 785 | 786 | else: 787 | 788 | def edgeweight(item): 789 | return None 790 | 791 | if recovery_weight is not None: 792 | 793 | def nodeweight(u): 794 | return H.nodes[u][recovery_weight] 795 | 796 | else: 797 | 798 | def nodeweight(u): 799 | return None 800 | 801 | if initial_infecteds is None: 802 | if rho is None: 803 | initial_number = 1 804 | else: 805 | initial_number = int(round(H.num_nodes * rho)) 806 | initial_infecteds = random.sample(list(H.nodes), initial_number) 807 | 808 | I = [len(initial_infecteds)] 809 | S = [H.num_nodes - I[0]] 810 | times = [tmin] 811 | 812 | t = tmin 813 | 814 | status = defaultdict(lambda: "S") 815 | for node in initial_infecteds: 816 | status[node] = "I" 817 | 818 | if return_event_data: 819 | events.append( 820 | { 821 | "time": tmin, 822 | "source": None, 823 | "target": node, 824 | "old_state": "S", 825 | "new_state": "I", 826 | } 827 | ) 828 | 829 | if return_event_data: 830 | for node in set(H.nodes).difference(initial_infecteds): 831 | events.append( 832 | { 833 | "time": tmin, 834 | "source": None, 835 | "target": node, 836 | "old_state": "I", 837 | "new_state": "S", 838 | } 839 | ) 840 | 841 | if recovery_weight is None: 842 | infecteds = SamplingDict() 843 | else: 844 | infecteds = SamplingDict(weighted=True) 845 | 846 | unique_edge_sizes = xgi.unique_edge_sizes(H) 847 | 848 | IS_links = dict() 849 | for size in unique_edge_sizes: 850 | if transmission_weight is None: 851 | IS_links[size] = SamplingDict() 852 | else: 853 | IS_links[size] = SamplingDict(weighted=True) 854 | 855 | for node in initial_infecteds: 856 | infecteds.update(node, weight_increment=nodeweight(node)) 857 | for edge_id in memberships[ 858 | node 859 | ]: # must have this in a separate loop after assigning status of node 860 | # handle weighted vs. unweighted? 861 | edge = members[edge_id] 862 | for nbr in edge: # there may be self-loops so account for this later 863 | if status[nbr] == "S": 864 | contagion = transmission_function(nbr, status, edge, **args) 865 | if contagion != 0: 866 | IS_links[len(edge)].update( 867 | (edge_id, nbr), 868 | weight_increment=edgeweight(edge_id), 869 | ) # need to be able to multiply by the contagion? 870 | 871 | total_rates = dict() 872 | total_rates[0] = gamma * infecteds.total_weight() # I_weight_sum 873 | for size in unique_edge_sizes: 874 | total_rates[size] = tau[size] * IS_links[size].total_weight() # IS_weight_sum 875 | 876 | total_rate = sum(total_rates.values()) 877 | if total_rate > 0: 878 | delay = random.expovariate(total_rate) 879 | else: 880 | print("Total rate is zero and no events will happen!") 881 | delay = float("Inf") 882 | 883 | t += delay 884 | 885 | while infecteds and t < tmax: 886 | # rejection sampling 887 | while True: 888 | choice = random.choice(list(total_rates.keys())) 889 | if random.random() < total_rates[choice] / total_rate: 890 | break 891 | 892 | if choice == 0: # recover 893 | recovering_node = ( 894 | infecteds.random_removal() 895 | ) # chooses a node at random and removes it 896 | status[recovering_node] = "S" 897 | 898 | if return_event_data: 899 | events.append( 900 | { 901 | "time": t, 902 | "source": None, 903 | "target": recovering_node, 904 | "old_state": "I", 905 | "new_state": "S", 906 | } 907 | ) 908 | 909 | # Find the SI links for the recovered node to get reinfected 910 | for edge_id in memberships[recovering_node]: 911 | edge = members[edge_id] 912 | contagion = transmission_function(recovering_node, status, edge, **args) 913 | if contagion != 0: 914 | IS_links[len(edge)].update( 915 | (edge_id, recovering_node), 916 | weight_increment=edgeweight(edge_id), 917 | ) 918 | 919 | # reduce the number of infected links because of the healing 920 | for edge_id in memberships[recovering_node]: 921 | edge = members[edge_id] 922 | for nbr in edge: 923 | # if the key doesn't exist, don't attempt to remove it 924 | if status[nbr] == "S" and (edge_id, nbr) in IS_links[len(edge)]: 925 | contagion = transmission_function(nbr, status, edge, **args) 926 | if contagion == 0: 927 | try: 928 | IS_links[len(edge)].remove((edge_id, nbr)) 929 | except: 930 | pass 931 | 932 | times.append(t) 933 | S.append(S[-1] + 1) 934 | I.append(I[-1] - 1) 935 | else: 936 | source, recipient = IS_links[choice].choose_random() 937 | status[recipient] = "I" 938 | 939 | infecteds.update(recipient, weight_increment=nodeweight(recipient)) 940 | 941 | if return_event_data: 942 | events.append( 943 | { 944 | "time": t, 945 | "source": source, 946 | "target": recipient, 947 | "old_state": "S", 948 | "new_state": "I", 949 | } 950 | ) 951 | 952 | for edge_id in memberships[recipient]: 953 | try: 954 | IS_links[len(members[edge_id])].remove((edge_id, recipient)) 955 | except: 956 | pass 957 | 958 | for edge_id in memberships[recipient]: 959 | edge = members[edge_id] 960 | for nbr in edge: 961 | if status[nbr] == "S": 962 | contagion = transmission_function(nbr, status, edge, **args) 963 | if contagion != 0: 964 | IS_links[len(edge)].update( 965 | (edge_id, nbr), 966 | weight_increment=edgeweight(edge_id), 967 | ) 968 | times.append(t) 969 | S.append(S[-1] - 1) 970 | I.append(I[-1] + 1) 971 | 972 | total_rates[0] = gamma * infecteds.total_weight() 973 | for size in unique_edge_sizes: 974 | total_rates[size] = tau[size] * IS_links[size].total_weight() 975 | total_rate = sum(total_rates.values()) 976 | if total_rate > 0: 977 | delay = random.expovariate(total_rate) 978 | else: 979 | delay = float("Inf") 980 | t += delay 981 | 982 | if return_event_data: 983 | return events 984 | else: 985 | return np.array(times), np.array(S), np.array(I) 986 | 987 | 988 | def event_driven_SIR( 989 | H, 990 | tau, 991 | gamma, 992 | transmission_function=majority_vote, 993 | initial_infecteds=None, 994 | initial_recovereds=None, 995 | rho=None, 996 | tmin=0, 997 | tmax=float("Inf"), 998 | return_event_data=False, 999 | seed=None, 1000 | **args 1001 | ): 1002 | """Simulates the SIR model for hypergraphs with the event-driven algorithm. 1003 | 1004 | Parameters 1005 | ---------- 1006 | H : xgi.Hypergraph 1007 | The hypergraph on which to simulate the SIR contagion process 1008 | tau : dict 1009 | Keys are edge sizes and values are transmission rates 1010 | gamma : float 1011 | Healing rate 1012 | transmission_function : lambda function, default: threshold 1013 | The contagion function that determines whether transmission is possible. 1014 | initial_infecteds : iterable, default: None 1015 | Initially infected node IDs. 1016 | initial_recovereds : iterable, default: None 1017 | Initially recovered node IDs. 1018 | recovery_weight : hashable, default: None 1019 | Hypergraph node attribute that weights the healing rate. 1020 | transmission_weight : hashable, default: None 1021 | Hypergraph edge attribute that weights the transmission rate. 1022 | rho : float, default: None 1023 | Fraction initially infected. Cannot be specified if 1024 | `initial_infecteds` is defined. 1025 | tmin : float, default: 0 1026 | Time at which the simulation starts. 1027 | tmax : float, default: float("Inf") 1028 | Time at which the simulation terminates if there are still 1029 | infected nodes. 1030 | return_event_data : bool, default: False 1031 | Whether to track each individual transition event that occurs. 1032 | 1033 | Returns 1034 | ------- 1035 | tuple of np.arrays 1036 | t, S, I, R 1037 | 1038 | Raises 1039 | ------ 1040 | HyperContagionError 1041 | If the user specifies both rho and initial_infecteds. 1042 | """ 1043 | if seed is not None: 1044 | random.seed(seed) 1045 | 1046 | if rho is not None and initial_infecteds is not None: 1047 | raise HyperContagionError("cannot define both initial_infecteds and rho") 1048 | 1049 | events = list() 1050 | 1051 | # now we define the initial setup. 1052 | status = defaultdict(lambda: "S") # node status defaults to 'S' 1053 | rec_time = defaultdict(lambda: tmin - 1) # node recovery time defaults to -1 1054 | if initial_recovereds is not None: 1055 | for node in initial_recovereds: 1056 | status[node] = "R" 1057 | rec_time[node] = ( 1058 | tmin - 1 1059 | ) # default value for these. Ensures that the recovered nodes appear with a time 1060 | events.append((tmin, None, node, "I", "R")) 1061 | pred_inf_time = defaultdict(lambda: float("Inf")) 1062 | # infection time defaults to \infty --- this could be set to tmax, 1063 | # probably with a slight improvement to performance. 1064 | 1065 | Q = EventQueue(tmax) 1066 | 1067 | if initial_infecteds is None: 1068 | if rho is None: 1069 | initial_number = 1 1070 | else: 1071 | initial_number = int(round(H.num_nodes * rho)) 1072 | initial_infecteds = random.sample(list(H.nodes), initial_number) 1073 | 1074 | if initial_recovereds is None: 1075 | initial_recovereds = [] 1076 | 1077 | I = [0] 1078 | R = [0] 1079 | S = [H.num_nodes] 1080 | times = [tmin] 1081 | 1082 | for u in initial_infecteds: 1083 | pred_inf_time[u] = tmin 1084 | Q.add( 1085 | tmin, 1086 | _process_trans_SIR_, 1087 | args=( 1088 | times, 1089 | S, 1090 | I, 1091 | R, 1092 | Q, 1093 | H, 1094 | status, 1095 | transmission_function, 1096 | gamma, 1097 | tau, 1098 | None, 1099 | u, 1100 | rec_time, 1101 | pred_inf_time, 1102 | events, 1103 | ), 1104 | ) 1105 | 1106 | while Q: # all the work is done in this while loop. 1107 | Q.pop_and_run() 1108 | 1109 | if return_event_data: 1110 | return events 1111 | else: 1112 | times = times[len(initial_infecteds) :] 1113 | S = S[len(initial_infecteds) :] 1114 | I = I[len(initial_infecteds) :] 1115 | R = R[len(initial_infecteds) :] 1116 | return np.array(times), np.array(S), np.array(I), np.array(R) 1117 | 1118 | 1119 | def event_driven_SIS( 1120 | H, 1121 | tau, 1122 | gamma, 1123 | transmission_function=majority_vote, 1124 | initial_infecteds=None, 1125 | rho=None, 1126 | tmin=0, 1127 | tmax=float("Inf"), 1128 | return_event_data=False, 1129 | seed=None, 1130 | **args 1131 | ): 1132 | """Simulates the SIS model for hypergraphs with the event-driven algorithm. 1133 | 1134 | Parameters 1135 | ---------- 1136 | H : xgi.Hypergraph 1137 | The hypergraph on which to simulate the SIR contagion process 1138 | tau : dict 1139 | Keys are edge sizes and values are transmission rates 1140 | gamma : float 1141 | Healing rate 1142 | transmission_function : lambda function, default: threshold 1143 | The contagion function that determines whether transmission is possible. 1144 | initial_infecteds : iterable, default: None 1145 | Initially infected node IDs. 1146 | initial_recovereds : iterable, default: None 1147 | Initially recovered node IDs. 1148 | recovery_weight : hashable, default: None 1149 | Hypergraph node attribute that weights the healing rate. 1150 | transmission_weight : hashable, default: None 1151 | Hypergraph edge attribute that weights the transmission rate. 1152 | rho : float, default: None 1153 | Fraction initially infected. Cannot be specified if 1154 | `initial_infecteds` is defined. 1155 | tmin : float, default: 0 1156 | Time at which the simulation starts. 1157 | tmax : float, default: float("Inf") 1158 | Time at which the simulation terminates if there are still 1159 | infected nodes. 1160 | return_event_data : bool, default: False 1161 | Whether to track each individual transition event that occurs. 1162 | 1163 | Returns 1164 | ------- 1165 | tuple of np.arrays 1166 | t, S, I 1167 | 1168 | Raises 1169 | ------ 1170 | HyperContagionError 1171 | If the user specifies both rho and initial_infecteds. 1172 | """ 1173 | if seed is not None: 1174 | random.seed(seed) 1175 | 1176 | if rho is not None and initial_infecteds is not None: 1177 | raise HyperContagionError("cannot define both initial_infecteds and rho") 1178 | 1179 | events = list() 1180 | 1181 | # now we define the initial setup. 1182 | status = defaultdict(lambda: "S") # node status defaults to 'S' 1183 | rec_time = defaultdict(lambda: tmin - 1) # node recovery time defaults to -1 1184 | 1185 | pred_inf_time = defaultdict(lambda: float("Inf")) 1186 | # infection time defaults to \infty --- this could be set to tmax, 1187 | # probably with a slight improvement to performance. 1188 | 1189 | Q = EventQueue(tmax) 1190 | 1191 | if initial_infecteds is None: 1192 | if rho is None: 1193 | initial_number = 1 1194 | else: 1195 | initial_number = int(round(H.num_nodes * rho)) 1196 | initial_infecteds = random.sample(list(H.nodes), initial_number) 1197 | 1198 | I = [0] 1199 | S = [H.num_nodes] 1200 | times = [tmin] 1201 | 1202 | for u in initial_infecteds: 1203 | pred_inf_time[u] = tmin 1204 | Q.add( 1205 | tmin, 1206 | _process_trans_SIS_, 1207 | args=( 1208 | times, 1209 | S, 1210 | I, 1211 | Q, 1212 | H, 1213 | status, 1214 | transmission_function, 1215 | gamma, 1216 | tau, 1217 | None, 1218 | u, 1219 | rec_time, 1220 | pred_inf_time, 1221 | events, 1222 | ), 1223 | ) 1224 | 1225 | while Q: # all the work is done in this while loop. 1226 | Q.pop_and_run() 1227 | 1228 | if return_event_data: 1229 | return events 1230 | else: 1231 | times = times[len(initial_infecteds) :] 1232 | S = S[len(initial_infecteds) :] 1233 | I = I[len(initial_infecteds) :] 1234 | return np.array(times), np.array(S), np.array(I) 1235 | --------------------------------------------------------------------------------