├── dynetx ├── test │ ├── __init__.py │ ├── test_algorithms.py │ ├── test_read_write.py │ ├── test_assortativity.py │ ├── test_functions.py │ ├── test_dyngraph.py │ └── test_dyndigraph.py ├── readwrite │ ├── json_graph │ │ ├── __init__.py │ │ └── node_link.py │ ├── __init__.py │ └── edgelist.py ├── algorithms │ ├── __init__.py │ ├── assortativity.py │ └── paths.py ├── classes │ ├── __init__.py │ └── function.py ├── utils │ ├── __init__.py │ ├── transform.py │ ├── misc.py │ └── decorators.py └── __init__.py ├── conda ├── conda_build_config.yaml ├── build.sh └── meta.yaml ├── requirements.txt ├── MANIFEST.in ├── docs ├── dynetx.png ├── dynetx_old.png ├── dynetx_logo_full.png ├── reference │ ├── algorithms │ │ ├── algorithms.rst │ │ └── paths.rst │ ├── read_write.rst │ ├── read_write │ │ ├── json_graph.rst │ │ └── edgelist.rst │ ├── reference.rst │ ├── classes │ │ ├── gtypes.rst │ │ ├── dynetx.rst │ │ └── dydinetx.rst │ └── functions.rst ├── download.rst ├── overview.rst ├── index.rst ├── conf.py ├── installing.rst ├── tutorial.rst ├── make.bat └── Makefile ├── requirements_rtfd.txt ├── .coveragerc ├── .github ├── dependabot.yml └── workflows │ ├── conda_publish_from_pip.yml │ ├── publish.yml │ ├── python-package-conda.yml │ ├── build_and_upload_pypi.yml │ └── test_ubuntu.yml ├── environment.yml ├── setup.cfg ├── .travis.yml ├── .readthedocs.yaml ├── LICENSE ├── .gitignore ├── README.md └── setup.py /dynetx/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conda/conda_build_config.yaml: -------------------------------------------------------------------------------- 1 | python: 2 | - 3.7 3 | - 3.8 4 | - 3.9 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | future 3 | tqdm 4 | networkx 5 | decorator 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include requirements.txt 3 | include README.md -------------------------------------------------------------------------------- /docs/dynetx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GiulioRossetti/dynetx/HEAD/docs/dynetx.png -------------------------------------------------------------------------------- /dynetx/readwrite/json_graph/__init__.py: -------------------------------------------------------------------------------- 1 | from dynetx.readwrite.json_graph.node_link import * -------------------------------------------------------------------------------- /docs/dynetx_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GiulioRossetti/dynetx/HEAD/docs/dynetx_old.png -------------------------------------------------------------------------------- /conda/build.sh: -------------------------------------------------------------------------------- 1 | $PYTHON setup.py install --single-version-externally-managed --record=record.txt 2 | -------------------------------------------------------------------------------- /docs/dynetx_logo_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GiulioRossetti/dynetx/HEAD/docs/dynetx_logo_full.png -------------------------------------------------------------------------------- /dynetx/readwrite/__init__.py: -------------------------------------------------------------------------------- 1 | from dynetx.readwrite.edgelist import * 2 | from dynetx.readwrite.json_graph import * -------------------------------------------------------------------------------- /dynetx/algorithms/__init__.py: -------------------------------------------------------------------------------- 1 | from dynetx.algorithms.paths import * 2 | from dynetx.algorithms.assortativity import * 3 | -------------------------------------------------------------------------------- /dynetx/classes/__init__.py: -------------------------------------------------------------------------------- 1 | from .dyndigraph import DynDiGraph 2 | from .dyngraph import DynGraph 3 | from .function import * -------------------------------------------------------------------------------- /dynetx/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from dynetx.utils.decorators import * 2 | from dynetx.utils.misc import * 3 | from dynetx.utils.transform import * -------------------------------------------------------------------------------- /requirements_rtfd.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme 2 | tqdm 3 | mock 4 | numpy 5 | decorators 6 | networkx 7 | pytest>=5.4.3 8 | docutils==0.15.2 9 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | setup.py 4 | dynetx/utils/* 5 | dynetx/test/* 6 | 7 | [report] 8 | exclude_lines = 9 | pragma: no cover 10 | pass -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: dynetx 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - python>=3.8 7 | - pendulum 8 | - numpy 9 | - future 10 | - tqdm 11 | - networkx>=2.4 12 | - decorator 13 | -------------------------------------------------------------------------------- /docs/reference/algorithms/algorithms.rst: -------------------------------------------------------------------------------- 1 | ********** 2 | Algorithms 3 | ********** 4 | 5 | Dynetx implements standard temporal network measures 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | paths.rst 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/read_write.rst: -------------------------------------------------------------------------------- 1 | ************************** 2 | Reading and writing graphs 3 | ************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | read_write/edgelist 9 | read_write/json_graph 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | -------------------------------------------------------------------------------- /docs/download.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | Download 3 | ******** 4 | 5 | -------- 6 | Software 7 | -------- 8 | 9 | Source and binary releases: https://pypi.python.org/pypi/dynetx 10 | 11 | Github (latest development): https://github.com/GiulioRossetti/dynetx 12 | 13 | ------------- 14 | Documentation 15 | ------------- 16 | 17 | -------------------------------------------------------------------------------- /dynetx/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import dynetx.classes 4 | from dynetx.classes import DynGraph 5 | from dynetx.classes import DynDiGraph 6 | 7 | from dynetx.classes.function import * 8 | 9 | import dynetx.readwrite 10 | from dynetx.readwrite import * 11 | 12 | import dynetx.utils 13 | from dynetx.utils import * -------------------------------------------------------------------------------- /docs/reference/read_write/json_graph.rst: -------------------------------------------------------------------------------- 1 | JSON 2 | ==== 3 | 4 | --------- 5 | JSON data 6 | --------- 7 | Generate and parse JSON serializable data for DyNetx graphs. 8 | 9 | .. automodule:: dynetx.readwrite.json_graph 10 | .. autosummary:: 11 | :toctree: generated/ 12 | 13 | node_link_data 14 | node_link_graph 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | - "3.7" 5 | - "3.8" 6 | 7 | before_install: 8 | - pip install pytest pytest-cov 9 | - pip install coveralls 10 | 11 | install: 12 | - pip install . 13 | - pip install -r requirements.txt 14 | 15 | # command to run tests 16 | script: 17 | - py.test --cov=./ --cov-config=.coveragerc 18 | after_success: 19 | - coveralls 20 | -------------------------------------------------------------------------------- /docs/reference/read_write/edgelist.rst: -------------------------------------------------------------------------------- 1 | 2 | Edge List 3 | ========= 4 | 5 | .. automodule:: dynetx.readwrite.edgelist 6 | 7 | Interaction Graph 8 | ----------------- 9 | 10 | .. autosummary:: 11 | :toctree: generated/ 12 | 13 | write_interactions 14 | read_interactions 15 | 16 | Snapshot Graphs 17 | --------------- 18 | 19 | .. autosummary:: 20 | :toctree: generated/ 21 | 22 | write_snapshots 23 | read_snapshots -------------------------------------------------------------------------------- /docs/reference/reference.rst: -------------------------------------------------------------------------------- 1 | ********* 2 | Reference 3 | ********* 4 | 5 | In this section are introduced the components that constitute ``DyNetx``, namely 6 | 7 | - The implemented dynamic graph models 8 | - The implemented algorithms 9 | 10 | 11 | In ``DyNetx`` are implemented the following **Dynamic Graph** models: 12 | 13 | .. toctree:: 14 | :maxdepth: 1 15 | 16 | classes/gtypes.rst 17 | algorithms/algorithms.rst 18 | functions.rst 19 | read_write 20 | -------------------------------------------------------------------------------- /.github/workflows/conda_publish_from_pip.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | branches: [master] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | name: Conda skeleton publish 9 | steps: 10 | - name: Publish conda package from PyPI package 11 | uses: silvxlabs/conda-skeleton-publish@v2 12 | with: 13 | pypi_package: "dynetx" 14 | python_version: "3.9" 15 | upload_channel: "giuliorossetti" 16 | access_token: ${{ secrets.ANACONDA_TOKEN }} 17 | stable: true -------------------------------------------------------------------------------- /dynetx/utils/transform.py: -------------------------------------------------------------------------------- 1 | __author__ = 'rossetti' 2 | __license__ = "GPL" 3 | __email__ = "giulio.rossetti@gmail.com" 4 | 5 | __all__ = [ 6 | 'compact_timeslot', 7 | ] 8 | 9 | 10 | # @todo: define remapping snapshot methods 11 | def compact_timeslot(sind_list): 12 | """ 13 | Test method. Compact all snapshots into a single one. 14 | 15 | :param sind_list: 16 | :return: 17 | """ 18 | tls = sorted(sind_list) 19 | conversion = {val: idx for idx, val in enumerate(tls)} 20 | return conversion 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish_conda 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: # on demand 7 | branches: [master] 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: publish-to-conda 15 | uses: jegp/conda-package-publish-action@v1.6 #GiulioRossetti/conda-package-publish-action@v1.1.0 16 | with: 17 | subdir: 'conda' 18 | AnacondaToken: ${{ secrets.ANACONDA_TOKEN }} 19 | # platforms: 'all' 20 | override: true 21 | # dry_run: true 22 | -------------------------------------------------------------------------------- /docs/reference/algorithms/paths.rst: -------------------------------------------------------------------------------- 1 | 2 | Paths 3 | ===== 4 | 5 | Compute the time respecting paths between nodes in the graph. 6 | 7 | These algorithms work with undirected and directed graphs. 8 | 9 | Time respecting paths 10 | --------------------- 11 | 12 | .. automodule:: dynetx.algorithms.paths 13 | 14 | .. autosummary:: 15 | :toctree: generated_alg/ 16 | 17 | time_respecting_paths 18 | all_time_respecting_paths 19 | annotate_paths 20 | path_duration 21 | path_length 22 | 23 | 24 | Temporal Directed Acyclic Graph 25 | ------------------------------- 26 | 27 | .. autosummary:: 28 | :toctree: generated_alg/ 29 | 30 | temporal_dag -------------------------------------------------------------------------------- /conda/meta.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: "dynetx" 3 | version: "0.3.1" 4 | 5 | source: 6 | git_rev: v0.3.1 7 | git_url: https://github.com/GiulioRossetti/dynetx 8 | 9 | requirements: 10 | host: 11 | - python 12 | - setuptools 13 | build: 14 | - python 15 | - setuptools 16 | run: 17 | - python>=3.7 18 | - numpy 19 | - future 20 | - tqdm 21 | - networkx>=2.4 22 | - decorator 23 | 24 | about: 25 | home: https://github.com/GiulioRossetti/dynetx 26 | license: BSD-2-Clause 27 | license_familY: BSD 28 | license_file: LICENSE 29 | summary: "Dynamic Network library" 30 | 31 | extra: 32 | recipe-maintainers: 33 | - GiulioRossetti 34 | -------------------------------------------------------------------------------- /docs/reference/classes/gtypes.rst: -------------------------------------------------------------------------------- 1 | *********** 2 | Graph types 3 | *********** 4 | 5 | DyNetx provides data structures and methods for storing graphs. 6 | 7 | The choice of graph class depends on the structure of the 8 | graph you want to represent. 9 | 10 | Which graph class should I use? 11 | =============================== 12 | 13 | ================== ======================== 14 | Dynamic Graph Type DyNetx Class 15 | ================== ======================== 16 | Undirected DynGraph 17 | Directed DynDiGraph 18 | ================== ======================== 19 | 20 | Basic graph types 21 | ================= 22 | 23 | .. toctree:: 24 | :maxdepth: 2 25 | 26 | dynetx.rst 27 | dydinetx.rst 28 | 29 | -------------------------------------------------------------------------------- /.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-22.04 11 | tools: 12 | python: "3.10" 13 | # You can also specify other tool versions: 14 | # nodejs: "16" 15 | # rust: "1.55" 16 | # golang: "1.17" 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | configuration: docs/conf.py 21 | 22 | # If using Sphinx, optionally build your docs in additional formats such as PDF 23 | # formats: 24 | # - pdf 25 | 26 | # Optionally declare the Python requirements required to build your docs 27 | python: 28 | install: 29 | - requirements: requirements_rtfd.txt -------------------------------------------------------------------------------- /docs/reference/functions.rst: -------------------------------------------------------------------------------- 1 | ********* 2 | Functions 3 | ********* 4 | 5 | Functional interface to graph methods and assorted utilities. 6 | 7 | .. automodule:: dynetx.classes.function 8 | 9 | Graph 10 | ----- 11 | .. autosummary:: 12 | :toctree: generated/ 13 | 14 | degree 15 | degree_histogram 16 | density 17 | create_empty_copy 18 | is_directed 19 | add_star 20 | add_path 21 | add_cycle 22 | 23 | 24 | Nodes 25 | ----- 26 | .. autosummary:: 27 | :toctree: generated/ 28 | 29 | nodes 30 | number_of_nodes 31 | all_neighbors 32 | non_neighbors 33 | 34 | 35 | Interactions 36 | ------------ 37 | .. autosummary:: 38 | :toctree: generated/ 39 | 40 | 41 | interactions 42 | number_of_interactions 43 | non_interactions 44 | 45 | 46 | Freezing graph structure 47 | ------------------------ 48 | .. autosummary:: 49 | :toctree: generated/ 50 | 51 | freeze 52 | is_frozen 53 | 54 | Snapshots and Interaction Stream 55 | -------------------------------- 56 | .. autosummary:: 57 | :toctree: generated/ 58 | 59 | stream_interactions 60 | time_slice 61 | temporal_snapshots_ids 62 | interactions_per_snapshots 63 | inter_event_time_distribution 64 | -------------------------------------------------------------------------------- /.github/workflows/python-package-conda.yml: -------------------------------------------------------------------------------- 1 | name: Python Package using Conda 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build-linux: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | max-parallel: 5 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python 3.8 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: 3.8 19 | - name: Add conda to system path 20 | run: | 21 | # $CONDA is an environment variable pointing to the root of the miniconda directory 22 | echo $CONDA/bin >> $GITHUB_PATH 23 | - name: Install dependencies 24 | run: | 25 | conda env update --file environment.yml --name base 26 | - name: Lint with flake8 27 | run: | 28 | conda install flake8 29 | # stop the build if there are Python syntax errors or undefined names 30 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 31 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 32 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 33 | - name: Test with pytest 34 | run: | 35 | conda install pytest 36 | pytest 37 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | Overview 3 | ******** 4 | 5 | ``DyNetx`` is a Python language software package for describing, model, and study dynamic complex networks. 6 | 7 | ---------------- 8 | Who uses DyNetx? 9 | ---------------- 10 | 11 | The potential audience for ``DyNetx`` includes mathematicians, physicists, biologists, computer scientists, and social scientists. 12 | 13 | ----- 14 | Goals 15 | ----- 16 | 17 | ``DyNetx`` is built upon the NetworkX_ python library and is intended to provide: 18 | 19 | - tools for the study dynamic social, biological, and infrastructure networks, 20 | - a rapid development environment for collaborative, multidisciplinary, projects. 21 | 22 | ------------------------- 23 | The Python DyNetx library 24 | ------------------------- 25 | 26 | ``DyNetx`` is a powerful Python package that allows simple and flexible modelling of dynamic networks. 27 | 28 | Most importantly, ``DyNetx``, as well as the Python programming language, is free, well-supported, and a joy to use. 29 | 30 | ------------- 31 | Free software 32 | ------------- 33 | 34 | ``DyNetx`` is free software; you can redistribute it and/or modify it under the terms of the BSD License. 35 | We welcome contributions from the community. 36 | 37 | .. _NetworkX: https://networkx.github.io 38 | -------------------------------------------------------------------------------- /.github/workflows/build_and_upload_pypi.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | #workflow_dispatch: 15 | 16 | jobs: 17 | deploy: 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up Python 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: '3.x' 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install build 31 | 32 | - name: Build package 33 | run: python -m build 34 | 35 | - name: Publish distribution 📦 to PyPI 36 | if: startsWith(github.ref, 'refs/tags') 37 | uses: pypa/gh-action-pypi-publish@master 38 | with: 39 | user: __token__ 40 | password: ${{ secrets.PYPI_API_TOKEN }} 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2017, Giulio Rossetti 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /docs/reference/classes/dynetx.rst: -------------------------------------------------------------------------------- 1 | ************************* 2 | Undirected Dynamic Graphs 3 | ************************* 4 | 5 | Overview 6 | -------- 7 | 8 | .. currentmodule:: dynetx 9 | .. autoclass:: DynGraph 10 | 11 | Adding and removing nodes and edges 12 | ----------------------------------- 13 | 14 | .. autosummary:: 15 | :toctree: generated/ 16 | 17 | DynGraph.__init__ 18 | DynGraph.add_interaction 19 | DynGraph.add_interactions_from 20 | DynGraph.add_star 21 | DynGraph.add_path 22 | DynGraph.add_cycle 23 | 24 | 25 | Iterating over nodes and edges 26 | ------------------------------ 27 | 28 | .. autosummary:: 29 | :toctree: generated/ 30 | 31 | DynGraph.interactions 32 | DynGraph.interactions_iter 33 | DynGraph.neighbors 34 | DynGraph.neighbors_iter 35 | DynGraph.nodes 36 | DynGraph.nodes_iter 37 | 38 | Information about graph structure 39 | --------------------------------- 40 | 41 | .. autosummary:: 42 | :toctree: generated/ 43 | 44 | DynGraph.has_interaction 45 | DynGraph.number_of_interactions 46 | DynGraph.degree 47 | DynGraph.degree_iter 48 | DynGraph.size 49 | DynGraph.order 50 | DynGraph.has_node 51 | DynGraph.number_of_nodes 52 | DynGraph.to_directed 53 | DynGraph.update_node_attr 54 | DynGraph.update_node_attr_from 55 | 56 | 57 | Dynamic Representation: Access Snapshots and Interactions 58 | --------------------------------------------------------- 59 | 60 | .. autosummary:: 61 | :toctree: generated/ 62 | 63 | DynGraph.stream_interactions 64 | DynGraph.time_slice 65 | DynGraph.temporal_snapshots_ids 66 | DynGraph.interactions_per_snapshots 67 | DynGraph.inter_event_time_distribution -------------------------------------------------------------------------------- /dynetx/utils/misc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Miscellaneous Helpers for DyNetx. 3 | """ 4 | # Copyright (C) 2004-2015 by 5 | # Aric Hagberg 6 | # Dan Schult 7 | # Pieter Swart 8 | # All rights reserved. 9 | # BSD license. 10 | import sys 11 | # itertools.accumulate is only available on Python 3.2 or later. 12 | # 13 | # Once support for Python versions less than 3.2 is dropped, this code should 14 | # be removed. 15 | try: 16 | from itertools import accumulate 17 | except ImportError: 18 | import operator 19 | 20 | # The code for this function is from the Python 3.5 documentation, 21 | # distributed under the PSF license: 22 | # 23 | def accumulate(iterable, func=operator.add): 24 | it = iter(iterable) 25 | try: 26 | total = next(it) 27 | except StopIteration: 28 | return 29 | yield total 30 | for element in it: 31 | total = func(total, element) 32 | yield total 33 | 34 | __author__ = '\n'.join(['Aric Hagberg (hagberg@lanl.gov)', 35 | 'Dan Schult(dschult@colgate.edu)', 36 | 'Ben Edwards(bedwards@cs.unm.edu)']) 37 | 38 | 39 | PY2 = sys.version_info[0] == 2 40 | if PY2: 41 | def make_str(x): 42 | """Return the string representation of t.""" 43 | if isinstance(x, str): 44 | return x 45 | else: 46 | return str(str(x), 'unicode-escape') 47 | else: 48 | def make_str(x): 49 | """Return the string representation of t.""" 50 | return str(x) 51 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. DyNet documentation master file, created by 2 | sphinx-quickstart on Thu Aug 3 14:05:17 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | DyNetx - Dynamic Network library 7 | ================================ 8 | .. image:: https://badge.fury.io/py/dynetx.svg 9 | :target: https://badge.fury.io/py/dynetx.svg 10 | 11 | .. image:: https://img.shields.io/pypi/pyversions/dynetx.svg 12 | :target: https://badge.fury.io/py/dynetx 13 | 14 | .. image:: https://coveralls.io/repos/GiulioRossetti/dynetx/badge.png?branch=master 15 | :target: https://coveralls.io/r/GiulioRossetti/dynetx?branch=master 16 | 17 | .. image:: https://pyup.io/repos/github/GiulioRossetti/dynetx/shield.svg 18 | :target: https://pyup.io/repos/github/GiulioRossetti/dynetx/ 19 | 20 | 21 | 22 | DyNetx is a Python software package that extends ``networkx`` with dynamic network models and algorithms. 23 | 24 | =============== =================== ================== ========== =============== 25 | **Date** **Python Versions** **Main Author** **GitHub** **pypl** 26 | June 26, 2024 3.x `Giulio Rossetti`_ `Source`_ `Distribution`_ 27 | =============== =================== ================== ========== =============== 28 | 29 | 30 | Contents: 31 | 32 | .. toctree:: 33 | :maxdepth: 2 34 | 35 | overview.rst 36 | download.rst 37 | installing.rst 38 | tutorial.rst 39 | reference/reference.rst 40 | 41 | 42 | .. _`Giulio Rossetti`: http://www.about.giuliorossetti.net 43 | .. _`Source`: https://github.com/GiulioRossetti/dynet 44 | .. _`Distribution`: https://pypi.python.org/pypi/dynet 45 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /.github/workflows/test_ubuntu.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Test and Coverage (Ubuntu) 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | workflow_dispatch: 12 | schedule: 13 | - cron: "0 6 1 * *" # The 1st day of each month at 06:00 UTC 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | python-version: ["3.10", "3.11"] 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: conda-incubator/setup-miniconda@v3 27 | with: 28 | auto-update-conda: true 29 | python-version: ${{ matrix.python-version }} 30 | 31 | 32 | - name: Install pip dependencies 33 | run: | 34 | 35 | python -m pip install --upgrade pip 36 | pip install . 37 | pip install -r requirements.txt 38 | python -m pip install flake8 pytest 39 | pip install pytest pytest-cov 40 | pip install coveralls 41 | 42 | - name: Lint with flake8 43 | run: | 44 | # stop the build if there are Python syntax errors or undefined names 45 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 46 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 47 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 48 | 49 | - name: Test with pytest 50 | run: | 51 | pytest --cov-config=.coveragerc --cov=./ --cov-report=xml 52 | 53 | - name: codecov 54 | uses: codecov/codecov-action@v3 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DyNetx - Dynamic Network analysis library 2 | [![codecov](https://codecov.io/gh/GiulioRossetti/dynetx/branch/master/graph/badge.svg?token=ILcIwMJDpm)](https://codecov.io/gh/GiulioRossetti/dynetx) 3 | [![Documentation Status](https://readthedocs.org/projects/dynetx/badge/?version=latest)](http://dynetx.readthedocs.io/en/latest/?badge=latest) 4 | [![pyversions](https://img.shields.io/pypi/pyversions/dynetx.svg)](https://badge.fury.io/py/dynetx) 5 | [![PyPI version](https://badge.fury.io/py/dynetx.svg)](https://badge.fury.io/py/dynetx) 6 | [![Downloads](https://pepy.tech/badge/dynetx)](https://pepy.tech/project/dynetx) 7 | [![Downloads](https://pepy.tech/badge/dynetx/month)](https://pepy.tech/project/dynetx) 8 | [![DOI](https://zenodo.org/badge/98746676.svg)](https://zenodo.org/badge/latestdoi/98746676) 9 | [![SBD++](https://img.shields.io/badge/Available%20on-SoBigData%2B%2B-green)](https://sobigdata.d4science.org/group/sobigdata-gateway/explore?siteId=20371853) 10 | 11 | ![plot](./docs/dynetx_logo_full.png) 12 | 13 | 14 | DyNetx provides implementations of dynamic networks in python (it is built upon [networkx](http://networkx.github.io)). 15 | 16 | The project documentation can be found on [ReadTheDocs](http://dynetx.readthedocs.io). 17 | 18 | If you would like to test ``DyNetx`` functionalities without installing anything on your machine consider using the preconfigured Jupyter Hub instances offered by [SoBigData++](https://sobigdata.d4science.org/group/sobigdata-gateway/explore?siteId=20371853). 19 | 20 | 21 | ## Citation 22 | 23 | > Rossetti, G. (2020) DyNetx: dynamic network analysis library, v0.2.1, Zenodo, doi: 10.5281/zenodo.3953119 24 | 25 | 26 | ## Installation 27 | 28 | In order to install the library use pip: 29 | ```bash 30 | pip install dynetx 31 | ``` 32 | 33 | or conda: 34 | 35 | ```bash 36 | conda install -c giuliorossetti dynetx 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /docs/reference/classes/dydinetx.rst: -------------------------------------------------------------------------------- 1 | *********************** 2 | Directed Dynamic Graphs 3 | *********************** 4 | 5 | Overview 6 | -------- 7 | 8 | .. currentmodule:: dynetx 9 | .. autoclass:: DynDiGraph 10 | 11 | Adding and removing nodes and edges 12 | ----------------------------------- 13 | 14 | .. autosummary:: 15 | :toctree: generated/ 16 | 17 | DynDiGraph.__init__ 18 | DynDiGraph.add_interaction 19 | DynDiGraph.add_interactions_from 20 | 21 | 22 | Iterating over nodes and edges 23 | ------------------------------ 24 | 25 | .. autosummary:: 26 | :toctree: generated/ 27 | 28 | DynDiGraph.interactions 29 | DynDiGraph.interactions_iter 30 | DynDiGraph.in_interactions 31 | DynDiGraph.in_interactions_iter 32 | DynDiGraph.out_interactions 33 | DynDiGraph.out_interactions_iter 34 | DynDiGraph.neighbors 35 | DynDiGraph.neighbors_iter 36 | DynDiGraph.successors 37 | DynDiGraph.successors_iter 38 | DynDiGraph.predecessors 39 | DynDiGraph.predecessors_iter 40 | DynDiGraph.nodes 41 | DynDiGraph.nodes_iter 42 | 43 | Information about graph structure 44 | --------------------------------- 45 | 46 | .. autosummary:: 47 | :toctree: generated/ 48 | 49 | DynDiGraph.has_interaction 50 | DynDiGraph.has_successor 51 | DynDiGraph.has_predecessor 52 | DynDiGraph.number_of_interactions 53 | DynDiGraph.degree 54 | DynDiGraph.degree_iter 55 | DynDiGraph.in_degree 56 | DynDiGraph.in_degree_iter 57 | DynDiGraph.out_degree 58 | DynDiGraph.out_degree_iter 59 | DynDiGraph.size 60 | DynDiGraph.order 61 | DynDiGraph.has_node 62 | DynDiGraph.number_of_nodes 63 | DynDiGraph.to_undirected 64 | DynDiGraph.update_node_attr 65 | DynDiGraph.update_node_attr_from 66 | 67 | 68 | Dynamic Representation: Access Snapshots and Interactions 69 | --------------------------------------------------------- 70 | 71 | .. autosummary:: 72 | :toctree: generated/ 73 | 74 | DynDiGraph.stream_interactions 75 | DynDiGraph.time_slice 76 | DynDiGraph.temporal_snapshots_ids 77 | DynDiGraph.interactions_per_snapshots 78 | DynDiGraph.inter_event_time_distribution 79 | DynDiGraph.inter_in_event_time_distribution 80 | DynDiGraph.inter_out_event_time_distribution -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from codecs import open 3 | from os import path 4 | 5 | __author__ = 'Giulio Rossetti' 6 | __license__ = "BSD" 7 | __email__ = "giulio.rossetti@gmail.com" 8 | 9 | # Get the long description from the README file 10 | # with open(path.join(here, 'README.md'), encoding='utf-8') as f: 11 | # long_description = f.read() 12 | 13 | here = path.abspath(path.dirname(__file__)) 14 | 15 | # Get the long description from the README file 16 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 17 | long_description = f.read() 18 | 19 | with open(path.join(here, 'requirements.txt'), encoding='utf-8') as f: 20 | requirements = f.read().splitlines() 21 | 22 | 23 | setup(name='dynetx', 24 | version='0.3.2', 25 | license='BSD-2-Clause', 26 | description='Dynamic Network library', 27 | url='https://github.com/GiulioRossetti/dynetx', 28 | author='Giulio Rossetti', 29 | author_email='giulio.rossetti@gmail.com', 30 | classifiers=[ 31 | # How mature is this project? Common values are 32 | # 3 - Alpha 33 | # 4 - Beta 34 | # 5 - Production/Stable 35 | 'Development Status :: 4 - Beta ', 36 | 37 | # Indicate who your project is intended for 38 | 'Intended Audience :: Developers', 39 | 'Topic :: Software Development :: Build Tools', 40 | 41 | # Pick your license as you wish (should match "license" above) 42 | 'License :: OSI Approved :: BSD License', 43 | 44 | "Operating System :: OS Independent", 45 | 46 | # Specify the Python versions you support here. In particular, ensure 47 | # that you indicate whether you support Python 2, Python 3 or both. 48 | 'Programming Language :: Python', 49 | 'Programming Language :: Python :: 3' 50 | ], 51 | keywords='dynamic-networks', 52 | install_requires=requirements, 53 | long_description=long_description, 54 | long_description_content_type='text/markdown', 55 | packages=find_packages(exclude=["*.test", "*.test.*", "test.*", "test", "dynetx.test", "dynetx.test.*"]), 56 | ) 57 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | 2 | # -*- coding: utf-8 -*- 3 | # 4 | # DyNetx documentation build configuration file, created by 5 | # sphinx-quickstart on Thu Aug 3 14:05:17 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | import sys, os 16 | import sphinx_rtd_theme 17 | 18 | sys.path.append(os.path.join(os.path.dirname(__file__), "..")) 19 | try: 20 | from dynetx import __version__ 21 | except ImportError: 22 | __version__ = u'0.3.0' 23 | 24 | html_theme = "sphinx_rtd_theme" 25 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 26 | 27 | version = __version__ 28 | # The full version, including alpha/beta/rc tags. 29 | release = version 30 | 31 | html_theme_options = { 32 | "collapse_navigation": False, 33 | "display_version": False, 34 | "navigation_depth": 3, 35 | } 36 | 37 | # -- Project information ----------------------------------------------------- 38 | 39 | project = u'DyNetx' 40 | copyright = u'2017, Giulio Rossetti' 41 | author = u'Giulio Rossetti' 42 | 43 | autodoc_mock_imports = [ 44 | 'decorator', 'decorator.decorator', 'networkx', 'numpy', 'networkx.utils', 'pygtk', 'gtk', 'gobject', 'argparse', 'networkx', 45 | 'scipy' 46 | ] 47 | 48 | # -- General configuration --------------------------------------------------- 49 | 50 | # Add any Sphinx extension module names here, as strings. They can be 51 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 52 | # ones. 53 | extensions = [ 54 | "sphinx.ext.mathjax", 55 | "sphinx.ext.githubpages", 56 | "sphinx.ext.autodoc", 57 | "sphinx.ext.autosummary", 58 | ] 59 | 60 | 61 | # Add any paths that contain templates here, relative to this directory. 62 | templates_path = ["_templates"] 63 | 64 | # List of patterns, relative to source directory, that match files and 65 | # directories to ignore when looking for source files. 66 | # This pattern also affects html_static_path and html_extra_path. 67 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 68 | 69 | # -- Options for HTML output ------------------------------------------------- 70 | 71 | # The theme to use for HTML and HTML Help pages. See the documentation for 72 | # a list of builtin themes. 73 | # 74 | # html_theme = 'alabaster' 75 | 76 | html_logo = 'dynetx.png' 77 | 78 | # The name of an image file (relative to this directory) to use as a favicon of 79 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 80 | # pixels large. 81 | # html_favicon = None 82 | 83 | # Add any paths that contain custom static files (such as style sheets) here, 84 | # relative to this directory. They are copied after the builtin static files, 85 | # so a file named "default.css" will overwrite the builtin "default.css". 86 | #html_static_path = ['_static'] 87 | -------------------------------------------------------------------------------- /dynetx/test/test_algorithms.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import dynetx as dn 3 | import networkx as nx 4 | import dynetx.algorithms as al 5 | 6 | 7 | def get_network(): 8 | g = dn.DynGraph() 9 | g.add_interaction("A", "B", 1, 4) 10 | g.add_interaction("B", "D", 2, 5) 11 | g.add_interaction("A", "C", 4, 8) 12 | g.add_interaction("B", "D", 2, 4) 13 | g.add_interaction("B", "C", 6, 10) 14 | g.add_interaction("B", "D", 2, 4) 15 | g.add_interaction("A", "B", 7, 9) 16 | return g 17 | 18 | 19 | class AlgorithmsTestCase(unittest.TestCase): 20 | 21 | def test_DAG(self): 22 | g = get_network() 23 | DAG, sources, targets, _, _ = al.temporal_dag(g, "D", "C", start=1, end=9) 24 | self.assertIsInstance(DAG, nx.DiGraph) 25 | self.assertIsInstance(sources, list) 26 | self.assertIsInstance(targets, list) 27 | 28 | DAG, sources, targets, _, _ = al.temporal_dag(g, "D", start=1, end=9) 29 | self.assertIsInstance(DAG, nx.DiGraph) 30 | self.assertIsInstance(sources, list) 31 | self.assertIsInstance(targets, list) 32 | 33 | def test_ping_pong(self): 34 | g = dn.DynGraph() 35 | g.add_interaction("A", "B", 0, 2) 36 | g.add_interaction("B", "C", 1, 4) 37 | g.add_interaction("A", "C", 2, 4) 38 | g.add_interaction("D", "E", 0, 2) 39 | g.add_interaction("C", "E", 3, 5) 40 | g.add_interaction("A", "E", 4, 6) 41 | 42 | labs = {"A": 'x', 43 | "B": 'y', 44 | "C": 'x', 45 | "D": 'y', 46 | "E": 'x', 47 | } 48 | 49 | for n in g.nodes(): 50 | g.add_node(n, lab=labs[n]) 51 | 52 | ress = al.delta_conformity(g, start=0, delta=5, alphas=[1], labels=['lab'], 53 | path_type="shortest") 54 | 55 | ressa = al.delta_conformity(g, start=0, delta=5, alphas=[1], labels=['lab'], 56 | path_type="foremost") 57 | 58 | self.assertIsInstance(ress, dict) 59 | self.assertIsInstance(ressa, dict) 60 | 61 | def test_time_respecting_paths(self): 62 | g = get_network() 63 | pts = al.time_respecting_paths(g, "A", "D", start=1, end=9) 64 | 65 | for p in pts: 66 | self.assertIsInstance(p, tuple) 67 | 68 | self.assertEqual(len(al.time_respecting_paths(g, "D", "C", start=20, end=40)), 0) 69 | 70 | pts = al.time_respecting_paths(g, "A", "D", start=1, end=9, sample=0.5) 71 | 72 | for p in pts: 73 | self.assertIsInstance(p, tuple) 74 | 75 | def test_all_time_respecting_paths(self): 76 | g = get_network() 77 | pts = al.all_time_respecting_paths(g, start=1, end=9) 78 | 79 | for p in pts: 80 | self.assertIsInstance(p, tuple) 81 | 82 | def test_annotated_paths(self): 83 | g = get_network() 84 | pts = al.time_respecting_paths(g, "D", "C", start=2, end=9) 85 | 86 | for _, ap in pts.items(): 87 | v = al.annotate_paths(ap) 88 | for k, i in v.items(): 89 | self.assertIn(k, ['shortest', 'fastest', 'foremost', 'fastest_shortest', 'shortest_fastest']) 90 | self.assertIsInstance(i, list) 91 | -------------------------------------------------------------------------------- /docs/installing.rst: -------------------------------------------------------------------------------- 1 | ********** 2 | Installing 3 | ********** 4 | 5 | Before installing ``DyNetx``, you need to have setuptools installed. 6 | 7 | 8 | Note 9 | ---- 10 | 11 | In case of misaligned versions between pypl and GitHub, the documentation will refer to the GitHub version. 12 | 13 | ============= 14 | Quick install 15 | ============= 16 | 17 | Get ``DyNetx`` from the Python Package Index at pypl_. 18 | 19 | or install it with 20 | 21 | .. code-block:: python 22 | 23 | pip install dynetx 24 | 25 | and an attempt will be made to find and install an appropriate version that matches your operating system and Python version. 26 | 27 | You can install the development version with 28 | 29 | .. code-block:: python 30 | 31 | pip install git://github.com/GiulioRossetti/dynetx.git 32 | 33 | ====================== 34 | Installing from source 35 | ====================== 36 | 37 | You can install from source by downloading a source archive file (tar.gz or zip) or by checking out the source files from the GitHub source code repository. 38 | 39 | ``DyNetx`` is a pure Python package; you don’t need a compiler to build or install it. 40 | 41 | ------------------- 42 | Source archive file 43 | ------------------- 44 | Download the source (tar.gz or zip file) from pypl_ or get the latest development version from GitHub_ 45 | 46 | Unpack and change directory to the source directory (it should have the files README.txt and setup.py). 47 | 48 | Run python setup.py install to build and install 49 | 50 | ------ 51 | GitHub 52 | ------ 53 | Clone the DyNetx repository (see GitHub_ for options) 54 | 55 | .. code-block:: python 56 | 57 | git clone https://github.com/GiulioRossetti/dynetx.git 58 | 59 | Change directory to ndlib 60 | 61 | Run python setup.py install to build and install 62 | 63 | If you don’t have permission to install software on your system, you can install into another directory using the --user, --prefix, or --home flags to setup.py. 64 | 65 | For example 66 | 67 | .. code-block:: python 68 | 69 | python setup.py install --prefix=/home/username/python 70 | 71 | or 72 | 73 | .. code-block:: python 74 | 75 | python setup.py install --home=~ 76 | 77 | or 78 | 79 | .. code-block:: python 80 | 81 | python setup.py install --user 82 | 83 | If you didn’t install in the standard Python site-packages directory you will need to set your PYTHONPATH variable to the alternate location. See http://docs.python.org/2/install/index.html#search-path for further details. 84 | 85 | ============ 86 | Requirements 87 | ============ 88 | ------ 89 | Python 90 | ------ 91 | 92 | To use DyNetx you need Python 2.7, 3.2 or later. 93 | 94 | The easiest way to get Python and most optional packages is to install the Enthought Python distribution “Canopy” or using Anaconda. 95 | 96 | There are several other distributions that contain the key packages you need for scientific computing. 97 | 98 | ----------------- 99 | Required packages 100 | ----------------- 101 | The following are packages required by ``DyNetx``. 102 | 103 | ^^^^^^^^ 104 | NetworkX 105 | ^^^^^^^^ 106 | ``DyNetx`` extends the networkx python library adding dynamic network facilities. 107 | 108 | Download: http://networkx.github.io/download.html 109 | 110 | 111 | .. _pypl: https://pypi.python.org/pypi/dynetx/ 112 | .. _GitHub: https://github.com/GiulioRossetti/dynetx/ 113 | -------------------------------------------------------------------------------- /dynetx/readwrite/json_graph/node_link.py: -------------------------------------------------------------------------------- 1 | from dynetx.utils import make_str 2 | import dynetx as dn 3 | from itertools import chain, count 4 | 5 | __all__ = ['node_link_data', 'node_link_graph'] 6 | _attrs = dict(id='id', source='source', target='target') 7 | 8 | 9 | def node_link_data(G, attrs=_attrs): 10 | """Return data in node-link format that is suitable for JSON serialization 11 | and use in Javascript documents. 12 | 13 | Parameters 14 | ---------- 15 | G : DyNetx graph 16 | 17 | attrs : dict 18 | A dictionary that contains three keys 'id', 'source' and 'target'. 19 | The corresponding values provide the attribute names for storing DyNetx-internal graph data. The values should be unique. 20 | Default value :samp:`dict(id='id', source='source', target='target')`. 21 | 22 | Returns 23 | ------- 24 | data : dict 25 | A dictionary with node-link formatted data. 26 | 27 | Examples 28 | -------- 29 | >>> from dynetx.readwrite import json_graph 30 | >>> import dynetx as dn 31 | >>> G = dn.DynGraph([(1,2)]) 32 | >>> data = json_graph.node_link_data(G) 33 | 34 | To serialize with json 35 | 36 | >>> import json 37 | >>> s = json.dumps(data) 38 | 39 | Notes 40 | ----- 41 | Graph, node, and link attributes are stored in this format. Note that 42 | attribute keys will be converted to strings in order to comply with 43 | JSON. 44 | 45 | See Also 46 | -------- 47 | node_link_graph 48 | 49 | """ 50 | id_ = attrs['id'] 51 | 52 | data = {'directed': G.is_directed(), 'graph': G.graph, 53 | 'nodes': [dict(chain(G._node[n].items(), [(id_, n)])) for n in G], 'links': []} 54 | 55 | for u, v, timeline in G.interactions_iter(): 56 | for t in timeline['t']: 57 | for tid in range(t[0], t[-1]+1): 58 | data['links'].append({"source": u, "target": v, "time": tid}) 59 | 60 | return data 61 | 62 | 63 | def node_link_graph(data, directed=False, attrs=_attrs): 64 | """Return graph from node-link data format. 65 | 66 | Parameters 67 | ---------- 68 | data : dict 69 | node-link formatted graph data 70 | 71 | directed : bool 72 | If True, and direction not specified in data, return a directed graph. 73 | 74 | attrs : dict 75 | A dictionary that contains three keys 'id', 'source', 'target'. 76 | The corresponding values provide the attribute names for storing 77 | Dynetx-internal graph data. Default value: 78 | :samp:`dict(id='id', source='source', target='target')`. 79 | 80 | Returns 81 | ------- 82 | G : DyNetx graph 83 | A DyNetx graph object 84 | 85 | Examples 86 | -------- 87 | >>> from dynetx.readwrite import json_graph 88 | >>> import dynetx as dn 89 | >>> G = dn.DynGraph([(1,2)]) 90 | >>> data = json_graph.node_link_data(G) 91 | >>> H = json_graph.node_link_graph(data) 92 | 93 | See Also 94 | -------- 95 | node_link_data 96 | """ 97 | 98 | directed = data.get('directed', directed) 99 | graph = dn.DynGraph() 100 | if directed: 101 | graph = graph.to_directed() 102 | 103 | id_ = attrs['id'] 104 | mapping = [] 105 | graph.graph = data.get('graph', {}) 106 | c = count() 107 | for d in data['nodes']: 108 | node = d.get(id_, next(c)) 109 | mapping.append(node) 110 | nodedata = dict((make_str(k), v) for k, v in d.items() if k != id_) 111 | graph.add_node(node, **nodedata) 112 | for d in data['links']: 113 | graph.add_interaction(d['source'], d["target"], d['time']) 114 | 115 | return graph 116 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | *************** 2 | DyNetx Tutorial 3 | *************** 4 | 5 | DyNetx is built upon networkx and is designed to configure, model and analyze dynamic networks. 6 | 7 | In this tutorial we will introduce the ``DynGraph`` object that can be used to describe undirected, temporal graphs. 8 | 9 | Creating a graph 10 | ---------------- 11 | 12 | Create an empty dynamic graph with no nodes and no edges. 13 | 14 | .. code:: python 15 | 16 | import dynetx as dn 17 | g = dn.DynGraph(edge_removal=True) 18 | 19 | During the construction phase the ``edge_removal`` parameter allows to specify if the dynamic graph will allow edge removal or not. 20 | 21 | ^^^^^^^^^^^^ 22 | Interactions 23 | ^^^^^^^^^^^^ 24 | 25 | ``G`` can be grown by adding one interaction at a time. 26 | Every interaction is univocally defined by its endpoints, ``u`` and ``v``, as well as its timestamp ``t``. 27 | 28 | .. code:: python 29 | 30 | g.add_interaction(u=1, v=2, t=0) 31 | 32 | Moreover, also interaction duration can be specified at creation time, by setting kwarg ``e`` equal to the last timestamp at which the interaction is present: 33 | 34 | .. code:: python 35 | 36 | g.add_interaction(u=1, v=2, t=0, e=3) 37 | 38 | In the above example the interaction ``(1, 2)`` appear at time ``0`` and vanish at time ``3``, thus being present in ``[0, 2]``. 39 | 40 | Interaction list can also be added: in such scenario all the interactions in the list will have a same timestamp (i.e. they will belong to a same network *snapshot*) 41 | 42 | .. code:: python 43 | 44 | g.add_interactions_from([(1, 2), (2, 3), (3, 1)], t=2) 45 | 46 | The same method can be used to add any ``ebunch`` of interaction. An *ebunch* is any iterable container of interaction-tuples. 47 | 48 | .. code:: python 49 | 50 | g.add_interactions_from(H.edges(), t=2) 51 | 52 | 53 | ^^^^^ 54 | Nodes 55 | ^^^^^ 56 | 57 | Flattened node degree can be computed via the usual ``degree`` method exposed by ``networkx`` graph objects. 58 | In order to get the time dependent degree a parameter ``t``, identifying the desired snapshot, must be specified. 59 | 60 | Similarly, the ``neighbors`` method has been extended with a similar optional filtering parameter ``t``. 61 | 62 | Read graph from file 63 | -------------------- 64 | 65 | ``DyNetx`` allows to read/write networks from files in two formats: 66 | 67 | - snapshot graph (extended edgelist) 68 | - interaction graph (extended edgelist) 69 | 70 | The former format describes the dynamic graph one edge per row as a 3-tuple 71 | 72 | .. code:: bash 73 | 74 | n1 n2 t1 75 | 76 | where 77 | 78 | - ``n1`` and ``n2`` are nodes 79 | - ``t1`` is the timestamp of interaction appearance 80 | 81 | The latter format describes the dynamic graph one interaction per row as a 4-tuple 82 | 83 | .. code:: bash 84 | 85 | n1 n2 op t1 86 | 87 | where 88 | 89 | - ``n1`` and ``n2`` are nodes 90 | - ``t1`` is the timestamp of interaction appearance 91 | - ``op`` identify either the insertion, ``+``, or deletion, ``-`` of the edge 92 | 93 | ^^^^^^^^^^^^^^ 94 | Snapshot Graph 95 | ^^^^^^^^^^^^^^ 96 | 97 | In order to read a snapshot graph file 98 | 99 | .. code:: python 100 | 101 | g = dn.read_snapshots(graph_filename, nodetype=int, timestamptype=int) 102 | 103 | in order to save a graph in the same format 104 | 105 | .. code:: python 106 | 107 | dn.write_snapshots(graph, graph_filename) 108 | 109 | 110 | ^^^^^^^^^^^^^^^^^ 111 | Interaction Graph 112 | ^^^^^^^^^^^^^^^^^ 113 | 114 | In order to read an interaction graph file 115 | 116 | 117 | .. code:: python 118 | 119 | g = dn.read_interactions(graph_filename, nodetype=int, timestamptype=int) 120 | 121 | in order to save a graph in the same format 122 | 123 | .. code:: python 124 | 125 | dn.write_interactions(graph, graph_filename) 126 | 127 | 128 | Snapshots and Interactions 129 | -------------------------- 130 | 131 | The timestamps associated to graph edges can be retrieved through 132 | 133 | .. code:: python 134 | 135 | g.temporal_snapshots_ids() 136 | 137 | Similarly, the number of interactions in a given snapshot can be obtained via 138 | 139 | .. code:: python 140 | 141 | g.number_of_interactions(t=snapshot_id) 142 | 143 | if the parameter ``t`` is not specified a dictionary snapshot->edges number will be returned. 144 | 145 | 146 | Slicing a Dynamic Network 147 | ------------------------- 148 | 149 | Once loaded a graph it is possible to extract from it a time slice, i.e., a time-span graph 150 | 151 | .. code:: python 152 | 153 | s = g.time_slice(t_from=2, t_to=3) 154 | 155 | the resulting ``DynGraph`` stored in ``s`` will be composed by nodes and interactions existing within the time span ``[2, 3]``. 156 | 157 | 158 | Obtain the Interaction Stream 159 | ----------------------------- 160 | 161 | A dynamic network can be also described as stream of interactions, a chronologically ordered list of interactions 162 | 163 | .. code:: python 164 | 165 | for e in g.stream_interactions(): 166 | print e 167 | 168 | the ``stream_interactions`` method returns a generator that streams the interactions in ``g``, where ``e`` is a 4-tuple ``(u, v, op, t)`` 169 | 170 | - ``u, v`` are nodes 171 | - ``op`` is a edge creation or deletion event (respectively ``+``, ``-``) 172 | - ``t`` is the interactions timestamp 173 | 174 | -------------------------------------------------------------------------------- /dynetx/test/test_read_write.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import dynetx as dn 3 | from dynetx.readwrite import json_graph 4 | import os 5 | 6 | 7 | class ReadWriteTestCase(unittest.TestCase): 8 | 9 | def test_snapshots_interactions(self): 10 | g = dn.DynGraph() 11 | g.add_interaction(1, 2, 2) 12 | g.add_interaction(1, 2, 2, e=6) 13 | g.add_interaction(1, 2, 7, e=11) 14 | g.add_interaction(1, 2, 8, e=15) 15 | g.add_interaction(1, 2, 18) 16 | g.add_interaction(1, 2, 19) 17 | dn.write_snapshots(g, "test.txt", delimiter=" ") 18 | h = dn.read_snapshots("test.txt", nodetype=int, timestamptype=int) 19 | self.assertEqual(g.number_of_interactions(), h.number_of_interactions()) 20 | self.assertEqual(list(g.stream_interactions()), list(h.stream_interactions())) 21 | dn.write_interactions(h, "test.txt", delimiter=" ") 22 | h = dn.read_interactions("test.txt", nodetype=int, timestamptype=int, keys=True) 23 | dn.write_snapshots(h, "test.txt", delimiter=" ") 24 | os.remove("test.txt") 25 | 26 | def test_snapshots(self): 27 | g = dn.DynGraph() 28 | g.add_interaction(1, 2, 2) 29 | g.add_interaction(1, 2, 2, e=6) 30 | g.add_interaction(1, 2, 7, e=11) 31 | g.add_interaction(1, 2, 8, e=15) 32 | g.add_interaction(1, 2, 18) 33 | g.add_interaction(1, 2, 19) 34 | dn.write_snapshots(g, "test.txt", delimiter=" ") 35 | h = dn.read_snapshots("test.txt", nodetype=int, timestamptype=int) 36 | self.assertEqual(g.number_of_interactions(), h.number_of_interactions()) 37 | self.assertEqual(list(g.stream_interactions()), list(h.stream_interactions())) 38 | os.remove("test.txt") 39 | 40 | def test_snapshots_directed(self): 41 | g = dn.DynGraph() 42 | g.add_interaction(1, 2, 2) 43 | g.add_interaction(1, 2, 2, e=6) 44 | g.add_interaction(1, 2, 7, e=11) 45 | g.add_interaction(1, 2, 8, e=15) 46 | g.add_interaction(1, 2, 18) 47 | g.add_interaction(1, 2, 19) 48 | dn.write_snapshots(g, "test.txt", delimiter=" ") 49 | h = dn.read_snapshots("test.txt", directed=True, nodetype=int, timestamptype=int) 50 | self.assertEqual(g.number_of_interactions(), h.number_of_interactions()) 51 | self.assertEqual(list(g.stream_interactions()), list(h.stream_interactions())) 52 | os.remove("test.txt") 53 | 54 | def test_interaction_graph(self): 55 | g = dn.DynGraph() 56 | g.add_interaction(1, 2, 2) 57 | g.add_interaction(1, 2, 2, e=6) 58 | g.add_interaction(1, 2, 7, e=11) 59 | g.add_interaction(1, 2, 8, e=15) 60 | g.add_interaction(1, 2, 18) 61 | g.add_interaction(1, 2, 19) 62 | dn.write_interactions(g, "test2.txt", delimiter=" ") 63 | h = dn.read_interactions("test2.txt", nodetype=int, timestamptype=int) 64 | self.assertEqual(list(g.stream_interactions()), list(h.stream_interactions())) 65 | self.assertEqual(g.number_of_interactions(), h.number_of_interactions()) 66 | os.remove("test2.txt") 67 | 68 | def test_interaction_graph_directed(self): 69 | g = dn.DynGraph() 70 | g.add_interaction(1, 2, 2) 71 | g.add_interaction(1, 2, 2, e=6) 72 | g.add_interaction(1, 2, 7, e=11) 73 | g.add_interaction(1, 2, 8, e=15) 74 | g.add_interaction(1, 2, 18) 75 | g.add_interaction(1, 2, 19) 76 | dn.write_interactions(g, "test2.txt", delimiter=" ") 77 | h = dn.read_interactions("test2.txt", directed=True, nodetype=int, timestamptype=int) 78 | self.assertEqual(list(g.stream_interactions()), list(h.stream_interactions())) 79 | self.assertEqual(g.number_of_interactions(), h.number_of_interactions()) 80 | os.remove("test2.txt") 81 | 82 | def test_interaction_graph_flag(self): 83 | g = dn.DynGraph() 84 | g.add_interaction(1, 2, 2) 85 | g.add_interaction(1, 2, 2, e=6) 86 | g.add_interaction(1, 2, 7, e=11) 87 | g.add_interaction(1, 2, 8, e=15) 88 | g.add_interaction(1, 2, 18) 89 | g.add_interaction(1, 2, 19) 90 | dn.write_interactions(g, "test3.txt", delimiter=" ") 91 | h = dn.read_interactions("test3.txt", nodetype=int, timestamptype=int, keys=True) 92 | # self.assertEqual(list(g.stream_interactions()), list(h.stream_interactions())) 93 | self.assertEqual(g.number_of_interactions(), h.number_of_interactions()) 94 | os.remove("test3.txt") 95 | 96 | def test_snapshot_graph_flag(self): 97 | g = dn.DynGraph() 98 | g.add_interaction(1, 2, 2) 99 | g.add_interaction(1, 2, 2, e=6) 100 | g.add_interaction(1, 2, 7, e=11) 101 | g.add_interaction(1, 2, 8, e=15) 102 | g.add_interaction(1, 2, 18) 103 | g.add_interaction(1, 2, 19) 104 | dn.write_snapshots(g, "test4.txt", delimiter=" ") 105 | h = dn.read_snapshots("test4.txt", nodetype=int, timestamptype=int, keys=True) 106 | # self.assertEqual(list(g.stream_interactions()), list(h.stream_interactions())) 107 | self.assertEqual(g.number_of_interactions(), h.number_of_interactions()) 108 | os.remove("test4.txt") 109 | 110 | def test_json_directed(self): 111 | g = dn.DynDiGraph() 112 | g.add_interaction(1, 2, 2) 113 | g.add_interaction(2, 1, 2) 114 | g.add_interaction(1, 2, 2, e=6) 115 | g.add_interaction(1, 2, 7, e=11) 116 | g.add_interaction(1, 2, 8, e=15) 117 | g.add_interaction(1, 2, 18) 118 | g.add_interaction(1, 2, 19) 119 | data = json_graph.node_link_data(g) 120 | h = json_graph.node_link_graph(data) 121 | self.assertIsInstance(h, dn.DynDiGraph) 122 | 123 | def test_json_undirected(self): 124 | g = dn.DynGraph() 125 | g.add_interaction(1, 2, 2) 126 | g.add_interaction(1, 2, 2, e=6) 127 | g.add_interaction(1, 2, 7, e=11) 128 | g.add_interaction(1, 2, 8, e=15) 129 | g.add_interaction(1, 2, 18) 130 | g.add_interaction(1, 2, 19) 131 | data = json_graph.node_link_data(g) 132 | h = json_graph.node_link_graph(data) 133 | self.assertIsInstance(h, dn.DynGraph) 134 | 135 | 136 | if __name__ == '__main__': 137 | unittest.main() 138 | -------------------------------------------------------------------------------- /dynetx/utils/decorators.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from os.path import splitext 3 | 4 | import networkx as nx 5 | from decorator import decorator 6 | 7 | __all__ = [ 8 | 'open_file', 9 | 'not_implemented', 10 | ] 11 | 12 | 13 | def not_implemented(): 14 | """Decorator to mark methods as not implemented 15 | 16 | 17 | Returns 18 | ------- 19 | _require : function 20 | The decorated function. 21 | 22 | Raises 23 | ------ 24 | NetworkXNotImplemented 25 | If any of the packages cannot be imported 26 | 27 | Examples 28 | -------- 29 | Decorate functions like this:: 30 | 31 | @not_implemented() 32 | def sp_function(): 33 | pass 34 | 35 | """ 36 | @decorator 37 | def _not_implemented(f, *args, **kwargs): 38 | 39 | raise nx.NetworkXNotImplemented('Method not implemented for dynamic graphs') 40 | 41 | return _not_implemented 42 | 43 | 44 | def _open_gz(path, mode): 45 | import gzip 46 | return gzip.open(path,mode=mode) 47 | 48 | 49 | def _open_bz2(path, mode): 50 | import bz2 51 | return bz2.BZ2File(path,mode=mode) 52 | 53 | # To handle new extensions, define a function accepting a `path` and `mode`. 54 | # Then add the extension to _dispatch_dict. 55 | _dispatch_dict = defaultdict(lambda: open) 56 | _dispatch_dict['.gz'] = _open_gz 57 | _dispatch_dict['.bz2'] = _open_bz2 58 | _dispatch_dict['.gzip'] = _open_gz 59 | 60 | 61 | def open_file(path_arg, mode='r'): 62 | """Decorator to ensure clean opening and closing of files. 63 | 64 | Parameters 65 | ---------- 66 | path_arg : int 67 | Location of the path argument in args. Even if the argument is a 68 | named positional argument (with a default value), you must specify its 69 | index as a positional argument. 70 | mode : str 71 | String for opening mode. 72 | 73 | Returns 74 | ------- 75 | _open_file : function 76 | Function which cleanly executes the io. 77 | 78 | Examples 79 | -------- 80 | Decorate functions like this:: 81 | 82 | @open_file(0,'r') 83 | def read_function(pathname): 84 | pass 85 | 86 | @open_file(1,'w') 87 | def write_function(G,pathname): 88 | pass 89 | 90 | @open_file(1,'w') 91 | def write_function(G, pathname='graph.dot') 92 | pass 93 | 94 | @open_file('path', 'w+') 95 | def another_function(arg, **kwargs): 96 | path = kwargs['path'] 97 | pass 98 | """ 99 | # Note that this decorator solves the problem when a path argument is 100 | # specified as a string, but it does not handle the situation when the 101 | # function wants to accept a default of None (and then handle it). 102 | # Here is an example: 103 | # 104 | # @open_file('path') 105 | # def some_function(arg1, arg2, path=None): 106 | # if path is None: 107 | # fobj = tempfile.NamedTemporaryFile(delete=False) 108 | # close_fobj = True 109 | # else: 110 | # # `path` could have been a string or file object or something 111 | # # similar. In any event, the decorator has given us a file object 112 | # # and it will close it for us, if it should. 113 | # fobj = path 114 | # close_fobj = False 115 | # 116 | # try: 117 | # fobj.write('blah') 118 | # finally: 119 | # if close_fobj: 120 | # fobj.close() 121 | # 122 | # Normally, we'd want to use "with" to ensure that fobj gets closed. 123 | # However, recall that the decorator will make `path` a file object for 124 | # us, and using "with" would undesirably close that file object. Instead, 125 | # you use a try block, as shown above. When we exit the function, fobj will 126 | # be closed, if it should be, by the decorator. 127 | 128 | @decorator 129 | def _open_file(func, *args, **kwargs): 130 | 131 | # Note that since we have used @decorator, *args, and **kwargs have 132 | # already been resolved to match the function signature of func. This 133 | # means default values have been propagated. For example, the function 134 | # func(x, y, a=1, b=2, **kwargs) if called as func(0,1,b=5,c=10) would 135 | # have args=(0,1,1,5) and kwargs={'c':10}. 136 | 137 | # First we parse the arguments of the decorator. The path_arg could 138 | # be an positional argument or a keyword argument. Even if it is 139 | try: 140 | # path_arg is a required positional argument 141 | # This works precisely because we are using @decorator 142 | path = args[path_arg] 143 | except TypeError: 144 | # path_arg is a keyword argument. It is "required" in the sense 145 | # that it must exist, according to the decorator specification, 146 | # It can exist in `kwargs` by a developer specified default value 147 | # or it could have been explicitly set by the user. 148 | try: 149 | path = kwargs[path_arg] 150 | except KeyError: 151 | # Could not find the keyword. Thus, no default was specified 152 | # in the function signature and the user did not provide it. 153 | msg = 'Missing required keyword argument: {0}' 154 | raise nx.NetworkXError(msg.format(path_arg)) 155 | else: 156 | is_kwarg = True 157 | except IndexError: 158 | # A "required" argument was missing. This can only happen if 159 | # the decorator of the function was incorrectly specified. 160 | # So this probably is not a user error, but a developer error. 161 | msg = "path_arg of open_file decorator is incorrect" 162 | raise nx.NetworkXError(msg) 163 | else: 164 | is_kwarg = False 165 | 166 | # Now we have the path_arg. There are two types of input to consider: 167 | # 1) string representing a path that should be opened 168 | # 2) an already opened file object 169 | if isinstance(path, str): 170 | ext = splitext(path)[1] 171 | fobj = _dispatch_dict[ext](path, mode=mode) 172 | close_fobj = True 173 | elif hasattr(path, 'read'): 174 | # path is already a file-like object 175 | fobj = path 176 | close_fobj = False 177 | else: 178 | # could be None, in which case the algorithm will deal with it 179 | fobj = path 180 | close_fobj = False 181 | 182 | # Insert file object into args or kwargs. 183 | if is_kwarg: 184 | new_args = args 185 | kwargs[path_arg] = fobj 186 | else: 187 | # args is a tuple, so we must convert to list before modifying it. 188 | new_args = list(args) 189 | new_args[path_arg] = fobj 190 | 191 | # Finally, we call the original function, making sure to close the fobj. 192 | try: 193 | result = func(*new_args, **kwargs) 194 | finally: 195 | if close_fobj: 196 | fobj.close() 197 | 198 | return result 199 | 200 | return _open_file 201 | -------------------------------------------------------------------------------- /dynetx/test/test_assortativity.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import dynetx.algorithms as al 3 | import dynetx as dn 4 | import numpy as np 5 | import json 6 | import random 7 | import os 8 | 9 | 10 | class ConformityTestCase(unittest.TestCase): 11 | 12 | def test_delta_conformity(self): 13 | g = dn.DynGraph() 14 | 15 | labels = ['SI', 'NO'] 16 | nodes = ['A', 'B', 'C', 'D'] 17 | 18 | for node in nodes: 19 | g.add_node(node, labels=random.choice(labels)) 20 | 21 | g.add_interaction("A", "B", 1, 4) 22 | g.add_interaction("B", "D", 2, 5) 23 | g.add_interaction("A", "C", 4, 8) 24 | g.add_interaction("B", "D", 2, 4) 25 | g.add_interaction("B", "C", 6, 10) 26 | g.add_interaction("B", "D", 2, 4) 27 | g.add_interaction("A", "B", 7, 9) 28 | 29 | res = al.delta_conformity(g, 1, 5, list(np.arange(1, 4, 0.2)), ['labels'], profile_size=1) 30 | 31 | with open(f"conformity.json", "w") as o: 32 | json.dump(res, o) 33 | 34 | for k, v in res.items(): 35 | for z, t in v.items(): 36 | for _, val in t.items(): 37 | self.assertTrue(-1 <= float("{:.4f}".format(val)) <= 1) 38 | 39 | os.remove("conformity.json") 40 | 41 | def test_delta_annotated_conformity(self): 42 | g = dn.DynGraph() 43 | 44 | labels = ['SI', 'NO'] 45 | nodes = ['A', 'B', 'C', 'D'] 46 | 47 | for node in nodes: 48 | g.add_node(node, labels=random.choice(labels)) 49 | 50 | g.add_interaction("A", "B", 1, 4) 51 | g.add_interaction("B", "D", 2, 5) 52 | g.add_interaction("A", "C", 4, 8) 53 | g.add_interaction("B", "D", 2, 4) 54 | g.add_interaction("B", "C", 6, 10) 55 | g.add_interaction("B", "D", 2, 4) 56 | g.add_interaction("A", "B", 7, 9) 57 | 58 | res = al.delta_conformity(g, 1, 10, list(np.arange(1, 4, 0.2)), ['labels'], profile_size=1, path_type="fastest") 59 | 60 | with open(f"conformity_annotated.json", "w") as o: 61 | json.dump(res, o) 62 | 63 | for k, v in res.items(): 64 | for z, t in v.items(): 65 | for _, val in t.items(): 66 | self.assertTrue(-1 <= float("{:.4f}".format(val)) <= 1) 67 | 68 | os.remove("conformity_annotated.json") 69 | 70 | def test_hierarchical_delta_conformity(self): 71 | 72 | g = dn.DynGraph() 73 | 74 | nodes = ['A', 'B', 'C', 'D'] 75 | 76 | labels = ['one', 'two', 'three', 'four'] 77 | age = ["A", "B", "C"] 78 | hierarchy = {'labels': {'one': 1, 'two': 2, 'three': 3, 'four': 4}} 79 | 80 | for node in nodes: 81 | g.add_node(node, labels=random.choice(labels), age=random.choice(age)) 82 | 83 | g.add_interaction("A", "B", 1, 4) 84 | g.add_interaction("B", "D", 2, 5) 85 | g.add_interaction("A", "C", 4, 8) 86 | g.add_interaction("B", "D", 2, 4) 87 | g.add_interaction("B", "C", 6, 10) 88 | g.add_interaction("B", "D", 2, 4) 89 | g.add_interaction("A", "B", 7, 9) 90 | 91 | res = al.delta_conformity(g, 1, 5, list(np.arange(1, 4, 0.2)), ['labels', 'age'], profile_size=2, 92 | hierarchies=hierarchy) 93 | 94 | with open(f"conformity_hierarchy.json", "w") as o: 95 | json.dump(res, o) 96 | 97 | for k, v in res.items(): 98 | for z, t in v.items(): 99 | for _, val in t.items(): 100 | self.assertTrue(-1 <= float("{:.4f}".format(val)) <= 1) 101 | 102 | os.remove("conformity_hierarchy.json") 103 | 104 | def test_sliding_delta_conformity(self): 105 | g = dn.DynGraph() 106 | 107 | labels = ['SI', 'NO'] 108 | nodes = ['A', 'B', 'C', 'D'] 109 | 110 | for node in nodes: 111 | g.add_node(node, labels=random.choice(labels)) 112 | 113 | g.add_interaction("A", "B", 1, 4) 114 | g.add_interaction("B", "D", 2, 5) 115 | g.add_interaction("A", "C", 4, 8) 116 | g.add_interaction("B", "D", 2, 4) 117 | g.add_interaction("B", "C", 6, 10) 118 | g.add_interaction("B", "D", 2, 4) 119 | g.add_interaction("A", "B", 7, 9) 120 | 121 | res = al.sliding_delta_conformity(g, 2, list(np.arange(1, 4, 0.2)), ['labels'], profile_size=1) 122 | 123 | with open(f"sliding_conformity.json", "w") as o: 124 | json.dump(res, o) 125 | 126 | for k, v in res.items(): 127 | for z, t in v.items(): 128 | for _, val in t.items(): 129 | self.assertIsInstance(val, list) 130 | for _, c in val: 131 | self.assertTrue(-1 <= float("{:.4f}".format(c)) <= 1) 132 | 133 | os.remove("sliding_conformity.json") 134 | 135 | def test_delta_conformity_dynamic_attributes(self): 136 | g = dn.DynGraph() 137 | 138 | labels = ['SI', 'NO'] 139 | 140 | g.add_interaction("A", "B", 1, 4) 141 | g.add_interaction("B", "D", 2, 5) 142 | g.add_interaction("A", "C", 4, 8) 143 | g.add_interaction("B", "D", 2, 4) 144 | g.add_interaction("B", "C", 6, 10) 145 | g.add_interaction("B", "D", 2, 4) 146 | g.add_interaction("A", "B", 7, 9) 147 | 148 | for node in g.nodes(): 149 | g.add_node(node, 150 | labels={k: random.choices(labels, weights=[0.7, 0.3], k=1)[0] for k in g.node_presence(node)}) 151 | 152 | res = al.delta_conformity(g, 1, 5, list(np.arange(1, 4, 0.2)), ['labels'], profile_size=1) 153 | 154 | with open(f"conformity.json", "w") as o: 155 | json.dump(res, o) 156 | 157 | for k, v in res.items(): 158 | for z, t in v.items(): 159 | for _, val in t.items(): 160 | self.assertTrue(-1 <= float("{:.4f}".format(val)) <= 1) 161 | 162 | os.remove("conformity.json") 163 | 164 | def test_sliding_delta_conformity_dynamic_attributes(self): 165 | g = dn.DynGraph() 166 | 167 | labels = ['SI', 'NO'] 168 | 169 | g.add_interaction("A", "B", 1, 4) 170 | g.add_interaction("B", "D", 2, 5) 171 | g.add_interaction("A", "C", 4, 8) 172 | g.add_interaction("B", "D", 2, 4) 173 | g.add_interaction("B", "C", 6, 10) 174 | g.add_interaction("B", "D", 2, 4) 175 | g.add_interaction("A", "B", 7, 9) 176 | 177 | for node in g.nodes(): 178 | g.add_node(node, 179 | labels={k: random.choices(labels, weights=[0.5, 0.5], k=1)[0] for k in g.node_presence(node)}) 180 | 181 | res = al.sliding_delta_conformity(g, 2, list(np.arange(1, 4, 0.2)), ['labels'], profile_size=1) 182 | 183 | with open(f"sliding_conformity.json", "w") as o: 184 | json.dump(res, o) 185 | 186 | for k, v in res.items(): 187 | for z, t in v.items(): 188 | for _, val in t.items(): 189 | self.assertIsInstance(val, list) 190 | for _, c in val: 191 | self.assertTrue(-1 <= float("{:.4f}".format(c)) <= 1) 192 | 193 | os.remove("sliding_conformity.json") 194 | 195 | 196 | if __name__ == '__main__': 197 | unittest.main() 198 | -------------------------------------------------------------------------------- /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% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 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. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DyNet.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DyNet.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(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/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " dummy to check syntax errors of document sources" 51 | 52 | .PHONY: clean 53 | clean: 54 | rm -rf $(BUILDDIR)/* 55 | 56 | .PHONY: html 57 | html: 58 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 61 | 62 | .PHONY: dirhtml 63 | dirhtml: 64 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 65 | @echo 66 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 67 | 68 | .PHONY: singlehtml 69 | singlehtml: 70 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 71 | @echo 72 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 73 | 74 | .PHONY: pickle 75 | pickle: 76 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 77 | @echo 78 | @echo "Build finished; now you can process the pickle files." 79 | 80 | .PHONY: json 81 | json: 82 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 83 | @echo 84 | @echo "Build finished; now you can process the JSON files." 85 | 86 | .PHONY: htmlhelp 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 | .PHONY: qthelp 94 | qthelp: 95 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 96 | @echo 97 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 98 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 99 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DyNet.qhcp" 100 | @echo "To view the help file:" 101 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DyNet.qhc" 102 | 103 | .PHONY: applehelp 104 | applehelp: 105 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 106 | @echo 107 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 108 | @echo "N.B. You won't be able to view it unless you put it in" \ 109 | "~/Library/Documentation/Help or install it in your application" \ 110 | "bundle." 111 | 112 | .PHONY: devhelp 113 | devhelp: 114 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 115 | @echo 116 | @echo "Build finished." 117 | @echo "To view the help file:" 118 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DyNet" 119 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DyNet" 120 | @echo "# devhelp" 121 | 122 | .PHONY: epub 123 | epub: 124 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 125 | @echo 126 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 127 | 128 | .PHONY: epub3 129 | epub3: 130 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 131 | @echo 132 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 133 | 134 | .PHONY: latex 135 | latex: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo 138 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 139 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 140 | "(use \`make latexpdf' here to do that automatically)." 141 | 142 | .PHONY: latexpdf 143 | latexpdf: 144 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 145 | @echo "Running LaTeX files through pdflatex..." 146 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 147 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 148 | 149 | .PHONY: latexpdfja 150 | latexpdfja: 151 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 152 | @echo "Running LaTeX files through platex and dvipdfmx..." 153 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 154 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 155 | 156 | .PHONY: text 157 | text: 158 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 159 | @echo 160 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 161 | 162 | .PHONY: man 163 | man: 164 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 165 | @echo 166 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 167 | 168 | .PHONY: texinfo 169 | texinfo: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo 172 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 173 | @echo "Run \`make' in that directory to run these through makeinfo" \ 174 | "(use \`make info' here to do that automatically)." 175 | 176 | .PHONY: info 177 | info: 178 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 179 | @echo "Running Texinfo files through makeinfo..." 180 | make -C $(BUILDDIR)/texinfo info 181 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 182 | 183 | .PHONY: gettext 184 | gettext: 185 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 186 | @echo 187 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 188 | 189 | .PHONY: changes 190 | changes: 191 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 192 | @echo 193 | @echo "The overview file is in $(BUILDDIR)/changes." 194 | 195 | .PHONY: linkcheck 196 | linkcheck: 197 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 198 | @echo 199 | @echo "Link check complete; look for any errors in the above output " \ 200 | "or in $(BUILDDIR)/linkcheck/output.txt." 201 | 202 | .PHONY: doctest 203 | doctest: 204 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 205 | @echo "Testing of doctests in the sources finished, look at the " \ 206 | "results in $(BUILDDIR)/doctest/output.txt." 207 | 208 | .PHONY: coverage 209 | coverage: 210 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 211 | @echo "Testing of coverage in the sources finished, look at the " \ 212 | "results in $(BUILDDIR)/coverage/python.txt." 213 | 214 | .PHONY: xml 215 | xml: 216 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 217 | @echo 218 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 219 | 220 | .PHONY: pseudoxml 221 | pseudoxml: 222 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 223 | @echo 224 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 225 | 226 | .PHONY: dummy 227 | dummy: 228 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 229 | @echo 230 | @echo "Build finished. Dummy builder generates no files." 231 | -------------------------------------------------------------------------------- /dynetx/readwrite/edgelist.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read and write DyNetx graphs as edge lists. 3 | 4 | The multi-line adjacency list format is useful for graphs with nodes 5 | that can be meaningfully represented as strings. 6 | 7 | With the edgelist format simple edge data can be stored but node or graph data is not. 8 | There is no way of representing isolated nodes unless the node has a self-loop edge. 9 | 10 | Format 11 | ------ 12 | You can read or write three formats of edge lists with these functions. 13 | 14 | Node pairs with **timestamp** (u, v, t): 15 | 16 | >>> 1 2 0 17 | 18 | Sequence of **Interaction** events (u, v, +/-, t): 19 | 20 | >>> 1 2 + 0 21 | >>> 1 2 - 3 22 | """ 23 | 24 | from dynetx.utils import open_file, make_str, compact_timeslot 25 | from dynetx import DynGraph 26 | from dynetx import DynDiGraph 27 | 28 | __author__ = 'Giulio Rossetti' 29 | __license__ = "BSD-Clause-2" 30 | __email__ = "giulio.rossetti@gmail.com" 31 | 32 | __all__ = ['write_interactions', 33 | 'generate_interactions', 34 | 'parse_interactions', 35 | 'read_interactions', 36 | 'generate_snapshots', 37 | 'write_snapshots', 38 | 'parse_snapshots', 39 | 'read_snapshots'] 40 | 41 | 42 | def generate_interactions(G, delimiter=' '): 43 | for e in G.stream_interactions(): 44 | yield delimiter.join(map(make_str, e)) 45 | 46 | 47 | @open_file(1, mode='wb') 48 | def write_interactions(G, path, delimiter=' ', encoding='utf-8'): 49 | """Write a DyNetx graph in interaction list format. 50 | 51 | 52 | Parameters 53 | ---------- 54 | 55 | G : graph 56 | A DyNetx graph. 57 | 58 | path : basestring 59 | The desired output filename 60 | 61 | delimiter : character 62 | Column delimiter 63 | 64 | encoding: str 65 | Text enconding, default utf-8 66 | """ 67 | for line in generate_interactions(G, delimiter): 68 | line += '\n' 69 | path.write(line.encode(encoding)) 70 | 71 | 72 | @open_file(0, mode='rb') 73 | def read_interactions(path, comments="#", directed=False, delimiter=None, 74 | nodetype=None, timestamptype=None, encoding='utf-8', keys=False): 75 | """Read a DyNetx graph from interaction list format. 76 | 77 | 78 | Parameters 79 | ---------- 80 | 81 | path : basestring 82 | The desired output filename 83 | 84 | delimiter : character 85 | Column delimiter 86 | 87 | comments: character 88 | Comments row identifier 89 | 90 | directed: bool 91 | Whether the graph is directed or not 92 | 93 | nodetype: object 94 | node type 95 | 96 | timestamptype: object 97 | timestamp type 98 | 99 | encoding: str 100 | File encoding, default utf-8 101 | 102 | keys: bool 103 | 104 | """ 105 | ids = None 106 | lines = (line.decode(encoding) for line in path) 107 | if keys: 108 | ids = read_ids(path.name, delimiter=delimiter, timestamptype=timestamptype) 109 | 110 | return parse_interactions(lines, comments=comments, directed=directed, delimiter=delimiter, nodetype=nodetype, 111 | timestamptype=timestamptype, keys=ids) 112 | 113 | 114 | def parse_interactions(lines, comments='#', directed=False, delimiter=None, nodetype=None, timestamptype=None, 115 | keys=None): 116 | if not directed: 117 | G = DynGraph() 118 | else: 119 | G = DynDiGraph() 120 | 121 | for line in lines: 122 | 123 | p = line.find(comments) 124 | if p >= 0: 125 | line = line[:p] 126 | if not len(line): 127 | continue 128 | 129 | s = line.strip().split(delimiter) 130 | 131 | if len(s) != 4: 132 | continue 133 | else: 134 | u = s.pop(0) 135 | v = s.pop(0) 136 | op = s.pop(0) 137 | s = s.pop(0) 138 | 139 | if nodetype is not None: 140 | try: 141 | u = nodetype(u) 142 | v = nodetype(v) 143 | except: 144 | raise TypeError("Failed to convert nodes %s,%s to type %s." % (u, v, nodetype)) 145 | 146 | if timestamptype is not None: 147 | try: 148 | s = timestamptype(s) 149 | except: 150 | raise TypeError("Failed to convert timestamp %s to type %s." % (s, nodetype)) 151 | 152 | if keys is not None: 153 | s = keys[s] 154 | 155 | if op == '+': 156 | G.add_interaction(u, v, t=s) 157 | else: 158 | timestamps = G.adj[u][v]['t'] 159 | if len(timestamps) > 0 and timestamps[-1][1] < s: 160 | for t in range(timestamps[-1][1], s): 161 | G.add_interaction(u, v, t=t) 162 | 163 | return G 164 | 165 | 166 | def generate_snapshots(G, delimiter=' '): 167 | for u, v, d in G.interactions(): 168 | if 't' not in d: 169 | raise NotImplemented 170 | for t in d['t']: 171 | e = [u, v, t[0]] 172 | if t[1] is not None: 173 | if t[0] != t[1]: 174 | for s in range(t[0], t[1] + 1): 175 | e = [u, v, s] 176 | yield delimiter.join(map(make_str, e)) 177 | else: 178 | yield delimiter.join(map(make_str, e)) 179 | else: 180 | yield delimiter.join(map(make_str, e)) 181 | 182 | 183 | @open_file(1, mode='wb') 184 | def write_snapshots(G, path, delimiter=' ', encoding='utf-8'): 185 | """Write a DyNetx graph in snapshot graph list format. 186 | 187 | 188 | Parameters 189 | ---------- 190 | 191 | G : graph 192 | A DyNetx graph. 193 | 194 | path : basestring 195 | The desired output filename 196 | 197 | delimiter : character 198 | Column delimiter 199 | 200 | encoding: str 201 | Encoding string, default utf-8 202 | """ 203 | for line in generate_snapshots(G, delimiter): 204 | line += '\n' 205 | path.write(line.encode(encoding)) 206 | 207 | 208 | def parse_snapshots(lines, comments='#', directed=False, delimiter=None, nodetype=None, timestamptype=None, keys=None): 209 | if not directed: 210 | G = DynGraph() 211 | else: 212 | G = DynDiGraph() 213 | 214 | for line in lines: 215 | p = line.find(comments) 216 | if p >= 0: 217 | line = line[:p] 218 | if not len(line): 219 | continue 220 | # split line, should have 2 or more 221 | s = line.strip().split(delimiter) 222 | if len(s) < 3: 223 | continue 224 | if len(s) == 3: 225 | u = s.pop(0) 226 | v = s.pop(0) 227 | t = s.pop(0) 228 | e = None 229 | else: 230 | u = s.pop(0) 231 | v = s.pop(0) 232 | t = s.pop(0) 233 | e = s.pop(0) 234 | 235 | if nodetype is not None: 236 | try: 237 | u = nodetype(u) 238 | v = nodetype(v) 239 | except: 240 | raise TypeError("Failed to convert nodes %s,%s to type %s." % (u, v, nodetype)) 241 | 242 | if timestamptype is not None: 243 | try: 244 | t = timestamptype(t) 245 | if e is not None: 246 | e = timestamptype(e) 247 | except: 248 | raise TypeError("Failed to convert timestamp %s to type %s." % (t, nodetype)) 249 | 250 | if keys is not None: 251 | t = keys[t] 252 | if e is not None: 253 | e = keys[e] 254 | G.add_interaction(u, v, t=t, e=e) 255 | return G 256 | 257 | 258 | @open_file(0, mode='rb') 259 | def read_snapshots(path, comments="#", directed=False, delimiter=None, 260 | nodetype=None, timestamptype=None, encoding='utf-8', keys=False): 261 | """Read a DyNetx graph from snapshot graph list format. 262 | 263 | 264 | Parameters 265 | ---------- 266 | 267 | path : basestring 268 | The desired output filename 269 | 270 | delimiter : character 271 | Column delimiter 272 | 273 | comments: character 274 | Comments row identifier 275 | 276 | directed: bool 277 | Whether the graph is directed or not 278 | 279 | nodetype: object 280 | node type 281 | 282 | timestamptype: object 283 | timestamp type 284 | 285 | encoding: str 286 | File encoding, default utf-8 287 | 288 | keys: bool 289 | """ 290 | ids = None 291 | lines = (line.decode(encoding) for line in path) 292 | if keys: 293 | ids = read_ids(path.name, delimiter=delimiter, timestamptype=timestamptype) 294 | 295 | return parse_snapshots(lines, comments=comments, directed=directed, delimiter=delimiter, nodetype=nodetype, 296 | timestamptype=timestamptype, keys=ids) 297 | 298 | 299 | def read_ids(path, delimiter=None, timestamptype=None): 300 | f = open(path) 301 | ids = {} 302 | for line in f: 303 | s = line.rstrip().split(delimiter) 304 | ids[timestamptype(s[-1])] = None 305 | if len(line) == 4: 306 | if s[-2] not in ['+', '-']: 307 | ids[timestamptype(s[-2])] = None 308 | 309 | f.flush() 310 | f.close() 311 | 312 | ids = compact_timeslot(ids.keys()) 313 | return ids 314 | -------------------------------------------------------------------------------- /dynetx/test/test_functions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import dynetx as dn 3 | 4 | 5 | class FunctionTestCase(unittest.TestCase): 6 | 7 | def test_functions(self): 8 | g = dn.DynGraph() 9 | g.add_interaction(1, 2, 2) 10 | g.add_interaction(1, 2, 2, e=6) 11 | g.add_interaction(1, 2, 7, e=11) 12 | g.add_interaction(1, 2, 8, e=15) 13 | g.add_interaction(1, 2, 18) 14 | g.add_interaction(1, 2, 19) 15 | 16 | self.assertEqual(len(dn.nodes(g)), 2) 17 | self.assertEqual(len(dn.nodes(g, t=0)), 0) 18 | self.assertEqual(len(dn.nodes(g, t=2)), 2) 19 | self.assertEqual(len(dn.interactions(g)), 1) 20 | self.assertEqual(len(dn.interactions(g, t=0)), 0) 21 | self.assertEqual(len(dn.interactions(g, t=2)), 1) 22 | self.assertDictEqual(dn.degree(g), {1: 1, 2: 1}) 23 | self.assertDictEqual(dn.degree(g, t=0), {1: 0, 2: 0}) 24 | self.assertDictEqual(dn.degree(g, [1, 2]), {1: 1, 2: 1}) 25 | self.assertEqual(len(dn.neighbors(g, 1)), 1) 26 | self.assertEqual(len(dn.neighbors(g, 1, t=2)), 1) 27 | self.assertEqual(len(dn.neighbors(g, 1, t=0)), 0) 28 | self.assertEqual(dn.number_of_nodes(g), 2) 29 | self.assertEqual(dn.number_of_nodes(g, t=0), 0) 30 | self.assertEqual(dn.number_of_nodes(g, t=2), 2) 31 | self.assertEqual(dn.number_of_interactions(g, t=0), 0) 32 | self.assertEqual(dn.number_of_interactions(g, t=2), 1) 33 | self.assertEqual(dn.number_of_interactions(g), 1) 34 | self.assertEqual(dn.density(g), 1.0) 35 | self.assertEqual(dn.density(g, t=0), 0) 36 | self.assertEqual(dn.density(g, t=2), 0) 37 | self.assertEqual(dn.degree_histogram(g), [0,2]) 38 | self.assertEqual(dn.degree_histogram(g, t=0), [2]) 39 | self.assertEqual(dn.degree_histogram(g, t=2), [0,2]) 40 | self.assertEqual(dn.is_directed(g), False) 41 | 42 | dn.add_cycle(g, [1, 2, 3, 4], t=30) 43 | dn.add_path(g, [4, 6, 7, 8], t=40) 44 | dn.add_star(g, [1, 2, 3, 4], t=50) 45 | dn.subgraph(g, [1, 3, 4]) 46 | dn.create_empty_copy(g) 47 | 48 | self.assertEqual(len(dn.all_neighbors(g, 1, t=0)), 0) 49 | self.assertEqual(len(dn.all_neighbors(g, 1)), 3) 50 | self.assertEqual(len(dn.all_neighbors(g, 1, t=2)), 1) 51 | self.assertEqual(len(list(dn.non_neighbors(g, 1, t=0))), 6) 52 | self.assertEqual(len(list(dn.non_neighbors(g, 1))), 3) 53 | self.assertEqual(len(list(dn.non_neighbors(g, 1, t=2))), 5) 54 | dn.non_interactions(g, 2) 55 | dn.non_interactions(g, 0) 56 | dn.non_interactions(g) 57 | self.assertEqual(dn.is_empty(g), False) 58 | h = dn.DynGraph() 59 | self.assertEqual(dn.is_empty(h), True) 60 | h = dn.time_slice(g, 2, 4) 61 | self.assertEqual(len(h.nodes()), 2) 62 | dn.stream_interactions(g) 63 | dn.temporal_snapshots_ids(g) 64 | dn.interactions_per_snapshots(g, 0) 65 | dn.interactions_per_snapshots(g, 2) 66 | dn.interactions_per_snapshots(g) 67 | dn.inter_event_time_distribution(g) 68 | dn.inter_event_time_distribution(g, 1) 69 | dn.inter_event_time_distribution(g, 1, 2) 70 | dn.set_node_attributes(g, values={n: 0 for n in g.nodes()}, name="test") 71 | try: 72 | dn.set_node_attributes(g, values={90000: 0}, name="test") 73 | except: 74 | pass 75 | try: 76 | dn.set_node_attributes(g, "test1", name="test") 77 | except: 78 | pass 79 | dn.set_node_attributes(g, values={n: {"a": 3} for n in g.nodes()}) 80 | try: 81 | dn.set_node_attributes(g, values={9000: {"a": 3}}) 82 | except: 83 | pass 84 | 85 | dn.get_node_attributes(g, name="test") 86 | try: 87 | dn.set_edge_attributes(3, "dog") 88 | except: 89 | pass 90 | try: 91 | dn.get_edge_attributes(g, "test") 92 | except: 93 | pass 94 | 95 | dn.freeze(g) 96 | self.assertEqual(dn.is_frozen(g), True) 97 | 98 | h = g.to_directed() 99 | dn.all_neighbors(h, 1) 100 | 101 | self.assertEqual(len(list(dn.non_interactions(g))), 13) 102 | 103 | def test_functions_directed(self): 104 | g = dn.DynDiGraph() 105 | g.add_interaction(1, 2, 2) 106 | g.add_interaction(1, 2, 2, e=6) 107 | g.add_interaction(1, 2, 7, e=11) 108 | g.add_interaction(1, 2, 8, e=15) 109 | g.add_interaction(1, 2, 18) 110 | g.add_interaction(1, 2, 19) 111 | 112 | self.assertEqual(len(dn.nodes(g)), 2) 113 | self.assertEqual(len(dn.nodes(g, t=0)), 0) 114 | self.assertEqual(len(dn.nodes(g, t=2)), 2) 115 | self.assertEqual(len(dn.interactions(g)), 1) 116 | self.assertEqual(len(dn.interactions(g, t=0)), 0) 117 | self.assertEqual(len(dn.interactions(g, t=2)), 1) 118 | self.assertDictEqual(dn.degree(g), {1: 1, 2: 1}) 119 | self.assertDictEqual(dn.degree(g, t=0), {1: 0, 2: 0}) 120 | self.assertDictEqual(dn.degree(g, [1, 2]), {1: 1, 2: 1}) 121 | self.assertEqual(len(dn.neighbors(g, 1)), 1) 122 | self.assertEqual(len(dn.neighbors(g, 1, t=2)), 1) 123 | self.assertEqual(len(dn.neighbors(g, 1, t=0)), 0) 124 | self.assertEqual(dn.number_of_nodes(g), 2) 125 | self.assertEqual(dn.number_of_nodes(g, t=0), 0) 126 | self.assertEqual(dn.number_of_nodes(g, t=2), 2) 127 | self.assertEqual(dn.number_of_interactions(g, t=0), 0) 128 | self.assertEqual(dn.number_of_interactions(g, t=2), 1) 129 | self.assertEqual(dn.number_of_interactions(g), 1) 130 | self.assertEqual(dn.density(g), 0.5) 131 | self.assertEqual(dn.density(g, t=0), 0) 132 | self.assertEqual(dn.density(g, t=2), 0) 133 | self.assertEqual(dn.degree_histogram(g), [0,2]) 134 | self.assertEqual(dn.degree_histogram(g, t=0), [2]) 135 | self.assertEqual(dn.degree_histogram(g, t=2), [0,2]) 136 | self.assertEqual(dn.is_directed(g), True) 137 | 138 | g.add_interaction(1, 2, 30) 139 | g.add_interaction(2, 3, 30) 140 | g.add_interaction(3, 4, 30) 141 | g.add_interaction(4, 1, 30) 142 | 143 | g.add_interaction(4, 6, 40) 144 | g.add_interaction(6, 7, 40) 145 | g.add_interaction(7, 8, 40) 146 | 147 | g.add_interaction(1, 2, 50) 148 | g.add_interaction(1, 3, 50) 149 | g.add_interaction(1, 4, 50) 150 | 151 | dn.add_star(g, [1, 2, 3, 4], t=50) 152 | dn.subgraph(g, [1, 3, 4]) 153 | dn.create_empty_copy(g) 154 | 155 | self.assertEqual(len(list(dn.all_neighbors(g, 1, t=0))), 0) 156 | self.assertEqual(len(list(dn.all_neighbors(g, 1))), 4) 157 | self.assertEqual(len(list(dn.all_neighbors(g, 1, t=2))), 1) 158 | self.assertEqual(len(list(dn.non_neighbors(g, 1, t=0))), 6) 159 | self.assertEqual(len(list(dn.non_neighbors(g, 1))), 3) 160 | self.assertEqual(len(list(dn.non_neighbors(g, 1, t=2))), 5) 161 | dn.non_interactions(g, 2) 162 | dn.non_interactions(g, 0) 163 | dn.non_interactions(g) 164 | self.assertEqual(dn.is_empty(g), False) 165 | h = dn.DynGraph() 166 | self.assertEqual(dn.is_empty(h), True) 167 | h = dn.time_slice(g, 2, 4) 168 | self.assertEqual(len(h.nodes()), 2) 169 | dn.stream_interactions(g) 170 | dn.temporal_snapshots_ids(g) 171 | dn.interactions_per_snapshots(g, 0) 172 | dn.interactions_per_snapshots(g, 2) 173 | dn.interactions_per_snapshots(g) 174 | dn.inter_event_time_distribution(g) 175 | dn.inter_event_time_distribution(g, 1) 176 | dn.inter_event_time_distribution(g, 1, 2) 177 | dn.set_node_attributes(g, values={n: 0 for n in g.nodes()}, name="test") 178 | try: 179 | dn.set_node_attributes(g, values={90000: 0}, name="test") 180 | except: 181 | pass 182 | try: 183 | dn.set_node_attributes(g, "test1", name="test") 184 | except: 185 | pass 186 | dn.set_node_attributes(g, values={n: {"a": 3} for n in g.nodes()}) 187 | try: 188 | dn.set_node_attributes(g, values={9000: {"a": 3}}) 189 | except: 190 | pass 191 | 192 | dn.get_node_attributes(g, name="test") 193 | try: 194 | dn.set_edge_attributes(3, "dog") 195 | except: 196 | pass 197 | try: 198 | dn.get_edge_attributes(g, "test") 199 | except: 200 | pass 201 | 202 | dn.freeze(g) 203 | self.assertEqual(dn.is_frozen(g), True) 204 | 205 | self.assertEqual(len(g.interactions()), 8) 206 | 207 | h = g.to_undirected() 208 | self.assertEqual(len(h.interactions()), 8) 209 | dn.all_neighbors(h, 1) 210 | 211 | self.assertEqual(len(list(dn.non_interactions(g))), 13) 212 | 213 | h = g.to_undirected(reciprocal=True) 214 | self.assertEqual(h.number_of_interactions(), 0) 215 | 216 | g.add_interaction(2, 1, 50) 217 | h = g.to_undirected(reciprocal=True) 218 | self.assertEqual(h.number_of_interactions(), 1) 219 | 220 | g.add_interaction(10, 11, 3, e=7) 221 | g.add_interaction(11, 10, 3, e=7) 222 | h = g.to_undirected(reciprocal=True) 223 | self.assertEqual(h.number_of_interactions(), 2) 224 | 225 | g.add_interaction(12, 14, 3, e=5) 226 | g.add_interaction(14, 12, 4, e=14) 227 | 228 | h = g.to_undirected(reciprocal=True) 229 | self.assertEqual(h.number_of_interactions(), 3) 230 | 231 | if __name__ == '__main__': 232 | unittest.main() 233 | -------------------------------------------------------------------------------- /dynetx/algorithms/assortativity.py: -------------------------------------------------------------------------------- 1 | from itertools import combinations 2 | from tqdm import tqdm 3 | from collections import defaultdict 4 | import dynetx as dn 5 | from .paths import * 6 | import networkx as nx 7 | 8 | __all__ = ["delta_conformity", "sliding_delta_conformity"] 9 | 10 | 11 | def __label_frequency(g: dn.DynGraph, u: object, nodes: list, labels: list, hierarchies: dict = None, 12 | t_dist: dict = None, start=None) -> float: 13 | """ 14 | Compute the similarity of node profiles 15 | :param g: a networkx Graph object 16 | :param u: node id 17 | :param labels: list of node categorical labels 18 | :param hierarchies: dict of labels hierarchies 19 | :return: node profiles similarity score in [-1, 1] 20 | """ 21 | s = 1 22 | 23 | for label in labels: 24 | 25 | a_u = g._node[u][label] 26 | 27 | if isinstance(a_u, dict): 28 | a_u = a_u[start] 29 | 30 | # set of nodes at given distance 31 | sgn = {} 32 | for v in nodes: 33 | 34 | a_v = g._node[v][label] 35 | 36 | if isinstance(a_v, dict): 37 | t = t_dist[v] 38 | if t in a_v: 39 | a_v = a_v[t] 40 | else: 41 | continue 42 | 43 | # indicator function that exploits label hierarchical structure 44 | sgn[v] = 1 if a_u == a_v else __distance(label, a_u, a_v, hierarchies) 45 | v_neigh = list(g.neighbors(v, t_dist[v])) 46 | # compute the frequency for the given node at distance n over neighbors label 47 | 48 | f_label = 0 49 | for x in v_neigh: 50 | a_x = g._node[x][label] 51 | # tx = t_dist[x] 52 | if isinstance(a_x, dict): 53 | a_x = a_x[t_dist[v]] 54 | if a_x == a_v: 55 | f_label += 1 56 | 57 | f_label = (f_label / len(v_neigh)) if len(v_neigh) > 0 else 0 58 | f_label = f_label if f_label > 0 else 1 59 | sgn[v] *= f_label 60 | s *= sum(sgn.values()) / len(nodes) 61 | 62 | return s 63 | 64 | 65 | def __distance(label: str, v1: str, v2: str, hierarchies: dict = None) -> float: 66 | """ 67 | Compute the distance of two labels in a plain hierarchy 68 | :param label: label name 69 | :param v1: first label value 70 | :param v2: second label value 71 | :param hierarchies: labels hierarchies 72 | """ 73 | if hierarchies is None or label not in hierarchies: 74 | return -1 75 | 76 | return -abs(hierarchies[label][v1] - hierarchies[label][v2]) / (len(hierarchies[label]) - 1) 77 | 78 | 79 | def __normalize(u: object, scores: list, max_dist: int, alphas: list): 80 | """ 81 | Normalize the computed scores in [-1, 1] 82 | :param u: node 83 | :param scores: datastructure containing the computed scores for u 84 | :param alphas: list of damping factor 85 | :return: scores updated 86 | """ 87 | for alpha in alphas: 88 | norm = sum([(d ** -alpha) for d in range(1, max_dist + 1)]) 89 | 90 | for profile in scores["%.2f" % alpha]: 91 | scores["%.2f" % alpha][profile][u] /= norm 92 | 93 | return scores 94 | 95 | 96 | def __remap_path_distances(temporal_distances): 97 | """ 98 | Mapping shortest paths temporal distances in hop distances 99 | 100 | :param temporal_distances: a dictionary of 101 | :return: a dictionary 102 | """ 103 | res = {} 104 | tids = sorted(set(temporal_distances.values())) 105 | tids = {t: pos + 1 for pos, t in enumerate(tids)} 106 | for k, v in list(temporal_distances.items()): 107 | res[k] = tids[v] 108 | return res 109 | 110 | 111 | def delta_conformity(dg, start: int, delta: int, alphas: list, labels: list, profile_size: int = 1, 112 | hierarchies: dict = None, path_type="shortest", progress_bar: bool = False, 113 | sample: float = 1) -> dict: 114 | """ 115 | Compute the Delta-Conformity for the considered dynamic graph 116 | :param dg: a dynetx Graph object composed by a single component 117 | :param start: the starting temporal id 118 | :param delta: the max duration of time respecting paths 119 | :param alphas: list of damping factors 120 | :param labels: list of node categorical labels 121 | :param profile_size: 122 | :param hierarchies: label hierarchies 123 | :param path_type: time respecting path type. String among: shortest, fastest, foremost, fastest_shortest and shortest_fastest (default: shortest) 124 | :param progress_bar: wheter to show the progress bar, default false 125 | :return: conformity value for each node in [-1, 1] 126 | 127 | -- Example -- 128 | >> g = dn.DynGraph() 129 | >> 130 | >> labels = ['SI', 'NO'] 131 | >> nodes = ['A', 'B', 'C', 'D'] 132 | >> 133 | >> for node in nodes: 134 | >> g.add_node(node, labels=random.choice(labels)) 135 | >> 136 | >> g.add_interaction("A", "B", 1, 4) 137 | >> g.add_interaction("B", "D", 2, 5) 138 | >> g.add_interaction("A", "C", 4, 8) 139 | >> g.add_interaction("B", "D", 2, 4) 140 | >> g.add_interaction("B", "C", 6, 10) 141 | >> g.add_interaction("B", "D", 2, 4) 142 | >> g.add_interaction("A", "B", 7, 9) 143 | >> 144 | >> res = al.delta_conformity(g, 1, 5, list(np.arange(1, 4, 0.2)), ['labels'], profile_size=1, path_type="fastest") 145 | 146 | """ 147 | 148 | if profile_size > len(labels): 149 | raise ValueError("profile_size must be <= len(labels)") 150 | 151 | if len(alphas) < 1 or len(labels) < 1: 152 | raise ValueError("At list one value must be specified for both alphas and labels") 153 | 154 | profiles = [] 155 | for i in range(1, profile_size + 1): 156 | profiles.extend(combinations(labels, i)) 157 | end = start + delta 158 | g = dg.time_slice(t_from=start, t_to=end) 159 | 160 | res = {"%.2f" % a: {"_".join(profile): {n: 0 for n in g.nodes(t=start)} for profile in profiles} for a in alphas} 161 | 162 | tids = g.temporal_snapshots_ids() 163 | 164 | if len(tids) == 0: 165 | return None 166 | 167 | mid = max(tids) 168 | mmid = min(tids) 169 | 170 | sp = all_time_respecting_paths(g, max(start, mmid), min(mid, end)) 171 | t_distances = defaultdict(lambda: defaultdict(int)) 172 | for k, v in list(sp.items()): 173 | ss = annotate_paths(v)[path_type] 174 | ss = [len(x) for x in ss] 175 | if k[0] != k[1]: 176 | t_distances[k[0]][k[1]] = min(ss) 177 | 178 | distances = defaultdict(lambda: defaultdict(int)) 179 | for k in t_distances: 180 | distances[k] = __remap_path_distances(t_distances[k]) 181 | 182 | for u in tqdm(g.nodes(t=start), disable=not progress_bar): 183 | 184 | sp = dict(distances[u]) 185 | 186 | dist_to_nodes = defaultdict(list) 187 | for node, dist in list(sp.items()): 188 | dist_to_nodes[dist].append(node) 189 | sp = dist_to_nodes 190 | 191 | for dist, nodes in list(sp.items()): 192 | if dist != 0: 193 | for profile in profiles: 194 | sim = __label_frequency(g, u, nodes, list(profile), hierarchies, t_distances[u], start) 195 | 196 | for alpha in alphas: 197 | partial = sim / (dist ** alpha) 198 | p_name = "_".join(profile) 199 | res["%.2f" % alpha][p_name][u] += partial 200 | 201 | if len(sp) > 0: 202 | res = __normalize(u, res, max(sp.keys()), alphas) 203 | 204 | return res 205 | 206 | 207 | def sliding_delta_conformity(dg, delta: int, alphas: list, labels: list, profile_size: int = 1, 208 | hierarchies: dict = None, path_type="shortest", progress_bar: bool = False, 209 | sample: float = 1) -> dict: 210 | """ 211 | Compute the Delta-Conformity for the considered dynamic graph on a sliding window of predefined size 212 | 213 | :param dg: a dynetx Graph object composed by a single component 214 | :param delta: the max duration of time respecting paths 215 | :param alphas: list of damping factors 216 | :param labels: list of node categorical labels 217 | :param profile_size: 218 | :param hierarchies: label hierarchies 219 | :param path_type: time respecting path type. String among: shortest, fastest, foremost, fastest_shortest and shortest_fastest (default: shortest) 220 | :param progress_bar: whether to show the progress bar, default false 221 | :return: conformity trend value for each node 222 | 223 | -- Example -- 224 | 225 | >> g = dn.DynGraph() 226 | >> 227 | >> labels = ['SI', 'NO'] 228 | >> nodes = ['A', 'B', 'C', 'D'] 229 | >> 230 | >> for node in nodes: 231 | >> g.add_node(node, labels=random.choice(labels)) 232 | >> 233 | >> g.add_interaction("A", "B", 1, 4) 234 | >> g.add_interaction("B", "D", 2, 5) 235 | >> g.add_interaction("A", "C", 4, 8) 236 | >> g.add_interaction("B", "D", 2, 4) 237 | >> g.add_interaction("B", "C", 6, 10) 238 | >> g.add_interaction("B", "D", 2, 4) 239 | >> g.add_interaction("A", "B", 7, 9) 240 | >> 241 | >> res = al.sliding_delta_conformity(g, 2, list(np.arange(1, 4, 0.2)), ['labels'], profile_size=1, path_type="fastest") 242 | 243 | """ 244 | tids = dn.temporal_snapshots_ids(dg) 245 | 246 | alpha_attribute_node_to_seq = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) 247 | 248 | for t in tqdm(tids, disable=not progress_bar): 249 | if t + delta < tids[-1]: 250 | dconf = delta_conformity(dg, t, delta, alphas, labels, profile_size, hierarchies, path_type, 251 | progress_bar=not progress_bar, sample=sample) 252 | if dconf is None: 253 | continue 254 | for alpha, data in list(dconf.items()): 255 | for attribute, node_values in list(data.items()): 256 | for n, v in list(node_values.items()): 257 | alpha_attribute_node_to_seq[alpha][attribute][n].append((t + delta, v)) 258 | 259 | return alpha_attribute_node_to_seq 260 | -------------------------------------------------------------------------------- /dynetx/test/test_dyngraph.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import dynetx as dn 3 | 4 | 5 | class DynGraphTestCase(unittest.TestCase): 6 | 7 | def test_coverage(self): 8 | G = dn.DynGraph() 9 | G.add_interaction(0, 1, t=0) 10 | G.add_interaction(0, 2, t=0) 11 | G.add_interaction(0, 1, t=1) 12 | G.add_interaction(0, 2, t=2) 13 | G.add_interaction(0, 3, t=2) 14 | self.assertEqual(G.coverage(), 2/3) 15 | self.assertEqual(G.node_contribution(1), 2/3) 16 | self.assertEqual(G.edge_contribution(0, 1), 2/3) 17 | self.assertEqual(G.edge_contribution(0, 3), 1/3) 18 | 19 | def test_uniformity(self): 20 | G = dn.DynGraph() 21 | G.add_interaction(0, 1, t=0) 22 | G.add_interaction(0, 2, t=0) 23 | G.add_interaction(0, 1, t=1) 24 | G.add_interaction(0, 2, t=1) 25 | self.assertEqual(G.uniformity(), 1) 26 | G.add_interaction(3, 4, t=1) 27 | G.add_interaction(5, 6, t=1) 28 | self.assertEqual(G.uniformity(), 2/3) 29 | self.assertEqual(G.node_pair_uniformity(0, 1), 1) 30 | self.assertEqual(G.node_pair_uniformity(0, 3), 0.5) 31 | 32 | def test_density(self): 33 | G = dn.DynGraph() 34 | G.add_interaction(0, 1, t=0) 35 | G.add_interaction(0, 2, t=0) 36 | G.add_interaction(0, 1, t=1) 37 | G.add_interaction(0, 2, t=1) 38 | self.assertEqual(G.density(), 2/3) 39 | self.assertEqual(G.pair_density(0, 1), 1) 40 | G.add_interaction(1, 3, t=2) 41 | self.assertEqual(G.pair_density(0, 3), 0) 42 | G.add_interaction(0, 3, t=2) 43 | self.assertEqual(G.pair_density(0, 1), 2/3) 44 | self.assertAlmostEqual(G.node_density(0), 0.5555555555555556) 45 | self.assertEqual(G.node_presence(0), set([0, 1, 2])) 46 | 47 | def test_self_loop(self): 48 | G = dn.DynGraph() 49 | G.add_interaction(0, 1, t=0) 50 | G.add_interaction(0, 2, t=0) 51 | G.add_interaction(0, 0, t=0) 52 | G.add_interaction(1, 1, t=0) 53 | G.add_interaction(2, 2, t=0) 54 | G.add_interaction(2, 2, t=2) 55 | ints = G.interactions(t=0) 56 | self.assertEqual(len(ints), 5) 57 | self.assertEqual(G.has_interaction(0, 0, t=0), True) 58 | 59 | def test_dyngraph_add_interaction(self): 60 | g = dn.DynGraph() 61 | self.assertIsInstance(g, dn.DynGraph) 62 | 63 | g.add_interaction(1, 2, 2) 64 | g.add_interaction(1, 2, 2, e=6) 65 | g.add_interaction(1, 2, 7, e=11) 66 | g.add_interaction(1, 2, 8, e=15) 67 | g.add_interaction(1, 2, 18) 68 | g.add_interaction(1, 2, 19) 69 | 70 | its = g.interactions() 71 | self.assertEqual(len(its), 1) 72 | 73 | g.add_interactions_from([(1, 3), (1, 5)], t=2) 74 | 75 | its = g.interactions() 76 | self.assertEqual(len(its), 3) 77 | 78 | its = g.interactions(t=18) 79 | self.assertEqual(len(its), 1) 80 | 81 | its = g.interactions(t=20) 82 | self.assertEqual(len(its), 0) 83 | 84 | self.assertEqual(len(list(g.neighbors_iter(1))), 3) 85 | self.assertEqual(len(list(g.neighbors_iter(1, 7))), 1) 86 | self.assertEqual(len(list(g.neighbors_iter(1, 0))), 0) 87 | self.assertEqual(g.order(), len(g.nodes())) 88 | 89 | self.assertEqual(g.has_node(42), False) 90 | 91 | self.assertEqual(g.has_node(42, 3), False) 92 | g.add_cycle([3, 4, 5, 6], t=34) 93 | try: 94 | g.time_slice(2, 1) 95 | except: 96 | pass 97 | 98 | g.interactions_iter([1, 2]) 99 | 100 | try: 101 | g.add_interaction(1, 5) 102 | except: 103 | pass 104 | 105 | try: 106 | g.add_interactions_from([(1, 4), (3, 6)]) 107 | except: 108 | pass 109 | 110 | try: 111 | g.remove_edge(1, 2) 112 | except: 113 | pass 114 | 115 | try: 116 | g.remove_edges_from([(1, 2)]) 117 | except: 118 | pass 119 | try: 120 | g.remove_node(1) 121 | except: 122 | pass 123 | try: 124 | g.remove_nodes_from([1, 2]) 125 | except: 126 | pass 127 | 128 | self.assertEqual(g.number_of_interactions(1, 90), 0) 129 | 130 | def test_nodes(self): 131 | g = dn.DynGraph() 132 | g.add_star([0, 1, 2, 3, 4], t=5) 133 | nds = len(g.nodes()) 134 | self.assertEqual(nds, 5) 135 | 136 | g.add_star([5, 1, 2, 3, 4], t=6) 137 | nds = len(g.nodes()) 138 | self.assertEqual(nds, 6) 139 | 140 | nds = len(g.nodes(t=6)) 141 | self.assertEqual(nds, 5) 142 | 143 | nds = len(g.nodes(t=9)) 144 | self.assertEqual(nds, 0) 145 | 146 | self.assertEqual(g.has_node(0), True) 147 | self.assertEqual(g.has_node(0, 5), True) 148 | self.assertEqual(g.has_node(0, 6), False) 149 | self.assertEqual(g.has_node(0, 0), False) 150 | 151 | def test_number_of_interactions(self): 152 | g = dn.DynGraph() 153 | g.add_path([0, 1, 2, 3, 4], t=5) 154 | g.add_path([4, 5, 6, 7, 8], t=6) 155 | 156 | its = g.number_of_interactions() 157 | self.assertEqual(its, 8) 158 | 159 | its = g.number_of_interactions(0) 160 | self.assertEqual(its, None) 161 | 162 | its = g.number_of_interactions(0, 1) 163 | self.assertEqual(its, 1) 164 | 165 | its = g.number_of_interactions(0, 1, 5) 166 | self.assertEqual(its, 1) 167 | 168 | its = g.number_of_interactions(0, 1, 6) 169 | self.assertEqual(its, 0) 170 | 171 | def test_has_interaction(self): 172 | g = dn.DynGraph() 173 | g.add_path([0, 1, 2, 3, 4], t=5) 174 | g.add_path([4, 5, 6, 7, 8], t=6) 175 | 176 | self.assertEqual(g.has_interaction(0, 1), True) 177 | self.assertEqual(g.has_interaction(0, 1, 5), True) 178 | self.assertEqual(g.has_interaction(0, 1, 6), False) 179 | self.assertEqual(g.has_interaction(0, 1, 9), False) 180 | 181 | def test_neighbores(self): 182 | g = dn.DynGraph() 183 | g.add_path([0, 1, 2, 3, 4], t=5) 184 | g.add_path([4, 5, 6, 7, 8], t=6) 185 | 186 | ng = len(g.neighbors(0)) 187 | self.assertEqual(ng, 1) 188 | 189 | ng = len(g.neighbors(0, 5)) 190 | self.assertEqual(ng, 1) 191 | 192 | ng = len(g.neighbors(0, 6)) 193 | self.assertEqual(ng, 0) 194 | 195 | ng = len(g.neighbors(0, 0)) 196 | self.assertEqual(ng, 0) 197 | 198 | def test_degree(self): 199 | g = dn.DynGraph() 200 | g.add_path([0, 1, 2, 3, 4], t=5) 201 | g.add_path([4, 5, 6, 7, 8], t=6) 202 | 203 | ng = g.degree(4) 204 | self.assertEqual(ng, 2) 205 | 206 | ng = g.degree(4, 5) 207 | self.assertEqual(ng, 1) 208 | 209 | ng = g.degree(4, 6) 210 | self.assertEqual(ng, 1) 211 | 212 | ng = g.degree(4, 0) 213 | self.assertEqual(ng, 0) 214 | 215 | def test_number_of_nodes(self): 216 | g = dn.DynGraph() 217 | g.add_path([0, 1, 2, 3, 4], t=5) 218 | g.add_path([4, 5, 6, 7, 8], t=6) 219 | 220 | nn = g.number_of_nodes() 221 | self.assertEqual(nn, 9) 222 | 223 | nn = g.number_of_nodes(t=5) 224 | self.assertEqual(nn, 5) 225 | 226 | nn = g.number_of_nodes(t=0) 227 | self.assertEqual(nn, 0) 228 | 229 | avg = g.avg_number_of_nodes() 230 | self.assertEqual(avg, 5) 231 | 232 | def test_update_node_attr(self): 233 | g = dn.DynGraph() 234 | 235 | for n in [0, 1, 2, 3, 4, 5, 6, 7, 8]: 236 | g.add_node(n, Label="A") 237 | 238 | for n in g.nodes(): 239 | g.update_node_attr(n, Label="B") 240 | 241 | for n in g.nodes(data=True): 242 | self.assertEqual(n[1]['Label'], "B") 243 | 244 | g.update_node_attr_from([0, 1, 2], Label="C") 245 | self.assertEqual(g._node[0]['Label'], "C") 246 | 247 | def test_add_node_attr(self): 248 | g = dn.DynGraph() 249 | 250 | for n in [0, 1, 2, 3, 4, 5, 6, 7, 8]: 251 | g.add_node(n, Label="A") 252 | 253 | g.add_nodes_from([9, 10, 11, 12], Label="A") 254 | 255 | g.add_path([0, 1, 2, 3, 4], t=5) 256 | g.add_path([4, 5, 6, 7, 8], t=6) 257 | 258 | for n in g.nodes(data=True): 259 | self.assertEqual(n[1]['Label'], "A") 260 | 261 | nds5 = [] 262 | for n in g.nodes(data=True, t=5): 263 | nds5.append(n[0]) 264 | self.assertEqual(n[1]['Label'], "A") 265 | 266 | self.assertListEqual(nds5, [0, 1, 2, 3, 4]) 267 | 268 | def test_time_slice_node_attr(self): 269 | g = dn.DynGraph() 270 | 271 | for n in [0, 1, 2, 3, 4, 5, 6, 7, 8]: 272 | g.add_node(n, Label="A") 273 | 274 | g.add_path([0, 1, 2, 3, 4], t=5) 275 | g.add_path([4, 5, 6, 7, 8], t=6) 276 | 277 | h = g.time_slice(5) 278 | for n in h.nodes(data=True): 279 | self.assertEqual(n[1]['Label'], "A") 280 | 281 | self.assertIsInstance(h, dn.DynGraph) 282 | self.assertEqual(h.number_of_nodes(), 5) 283 | self.assertEqual(h.number_of_interactions(), 4) 284 | 285 | 286 | 287 | def test_time_slice(self): 288 | g = dn.DynGraph() 289 | g.add_path([0, 1, 2, 3, 4], t=5) 290 | g.add_path([4, 5, 6, 7, 8], t=6) 291 | 292 | h = g.time_slice(5) 293 | self.assertIsInstance(h, dn.DynGraph) 294 | self.assertEqual(h.number_of_nodes(), 5) 295 | self.assertEqual(h.number_of_interactions(), 4) 296 | 297 | h = g.time_slice(5, 5) 298 | self.assertIsInstance(h, dn.DynGraph) 299 | self.assertEqual(h.number_of_nodes(), 5) 300 | self.assertEqual(h.number_of_interactions(), 4) 301 | 302 | h = g.time_slice(5, 6) 303 | self.assertIsInstance(h, dn.DynGraph) 304 | self.assertEqual(h.number_of_nodes(), 9) 305 | self.assertEqual(h.number_of_interactions(), 8) 306 | 307 | h = g.time_slice(0) 308 | self.assertIsInstance(h, dn.DynGraph) 309 | self.assertEqual(h.number_of_nodes(), 0) 310 | self.assertEqual(h.number_of_interactions(), 0) 311 | 312 | def test_temporal_snapshots_ids(self): 313 | g = dn.DynGraph() 314 | g.add_path([0, 1, 2, 3, 4], t=5) 315 | g.add_path([4, 5, 6, 7, 8], t=6) 316 | tsd = g.temporal_snapshots_ids() 317 | 318 | self.assertEqual(tsd, [5, 6]) 319 | 320 | def test_interactions_per_snapshots(self): 321 | g = dn.DynGraph() 322 | g.add_path([0, 1, 2, 3, 4], t=5) 323 | g.add_path([4, 5, 6, 7, 8], t=6) 324 | 325 | tsd = g.interactions_per_snapshots() 326 | self.assertDictEqual(tsd, {5: 4, 6: 4}) 327 | 328 | tsd = g.interactions_per_snapshots(t=5) 329 | self.assertEqual(tsd, 4) 330 | 331 | tsd = g.interactions_per_snapshots(t=0) 332 | self.assertEqual(tsd, 0) 333 | 334 | def test_inter_event_time(self): 335 | g = dn.DynGraph() 336 | g.add_path([0, 1, 2, 3, 4], t=2) 337 | g.add_path([4, 5, 6, 7, 8], t=3) 338 | 339 | ivt = g.inter_event_time_distribution() 340 | self.assertDictEqual(ivt, {0: 6, 1: 1}) 341 | 342 | ivt = g.inter_event_time_distribution(4) 343 | self.assertDictEqual(ivt, {1: 1}) 344 | 345 | ivt = g.inter_event_time_distribution(0) 346 | self.assertDictEqual(ivt, {}) 347 | 348 | ivt = g.inter_event_time_distribution(0, 1) 349 | self.assertDictEqual(ivt, {}) 350 | 351 | def test_stream_interactions(self): 352 | g = dn.DynGraph() 353 | g.add_interaction(1, 2, 2) 354 | g.add_interaction(1, 2, 2, e=6) 355 | g.add_interaction(1, 2, 7, e=11) 356 | g.add_interaction(1, 2, 8, e=15) 357 | g.add_interaction(1, 2, 18) 358 | g.add_interaction(1, 2, 19) 359 | g.add_interactions_from([(1, 3), (1, 5)], t=2, e=3) 360 | 361 | sres = list(g.stream_interactions()) 362 | 363 | cres = [(1, 2, '+', 2), (1, 3, '+', 2), (1, 5, '+', 2), (1, 3, '-', 3), 364 | (1, 5, '-', 3), (1, 2, '-', 6), (1, 2, '+', 7), (1, 2, '-', 15), (1, 2, '+', 18)] 365 | self.assertEqual(sorted(sres), sorted(cres)) 366 | 367 | def test_accumulative_growth(self): 368 | g = dn.DynGraph(edge_removal=False) 369 | g.add_interaction(1, 2, 2) 370 | g.add_interaction(1, 2, 2, e=6) 371 | g.add_interaction(1, 2, 7, e=11) 372 | g.add_interaction(1, 2, 8, e=15) 373 | g.add_interaction(1, 2, 18) 374 | g.add_interaction(1, 2, 19) 375 | g.add_interactions_from([(1, 3), (1, 5)], t=2, e=3) 376 | sres = list(g.stream_interactions()) 377 | cres = [(1, 2, '+', 2), (1, 5, '+', 2), (1, 3, '+', 2)] 378 | self.assertEqual(sorted(sres), sorted(cres)) 379 | self.assertEqual(g.has_interaction(1, 2, 18), True) 380 | self.assertEqual(g.has_interaction(1, 2, 40), False) 381 | try: 382 | g.add_interaction(2, 1, 7) 383 | except: 384 | pass 385 | 386 | def test_conversion(self): 387 | G = dn.DynGraph() 388 | G.add_interaction(0, 1, t=0) 389 | G.add_interaction(0, 2, t=0) 390 | G.add_interaction(0, 0, t=0) 391 | G.add_interaction(1, 1, t=0) 392 | G.add_interaction(2, 2, t=0) 393 | G.add_interaction(2, 2, t=2) 394 | 395 | H = G.to_directed() 396 | self.assertIsInstance(H, dn.DynDiGraph) 397 | self.assertEqual(H.number_of_nodes(), 3) 398 | self.assertEqual(H.number_of_edges(), 5) 399 | 400 | 401 | if __name__ == '__main__': 402 | unittest.main() 403 | -------------------------------------------------------------------------------- /dynetx/algorithms/paths.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import itertools 3 | from collections import defaultdict 4 | import tqdm 5 | import random 6 | import numpy as np 7 | import copy 8 | 9 | __author__ = 'Giulio Rossetti' 10 | __license__ = "BSD-Clause-2" 11 | __email__ = "giulio.rossetti@gmail.com" 12 | 13 | __all__ = ['time_respecting_paths', 'all_time_respecting_paths', 'annotate_paths', 'temporal_dag', 'path_duration', 14 | 'path_length'] 15 | 16 | 17 | def temporal_dag(G, u, v=None, start=None, end=None): 18 | """ 19 | Creates a rooted temporal DAG assuming interaction chains of length 1 within each network snapshot. 20 | 21 | Parameters 22 | ---------- 23 | G : a DynGraph or DynDiGraph object 24 | The graph to use for computing DAG 25 | u : a node id 26 | A node in G 27 | v : a node id 28 | A node in G, default None 29 | start : temporal id 30 | min temporal id for bounding the DAG, default None 31 | end : temporal id to conclude the search 32 | max temporal id for bounding the DAG, default None 33 | 34 | Returns 35 | -------- 36 | DAG: a directed graph 37 | A DAG rooted in u (networkx DiGraph object) 38 | sources: source node ids 39 | List of temporal occurrences of u 40 | targets: target node ids 41 | List of temporal occurrences of v 42 | node_type: type 43 | network node_type 44 | tid_type: type 45 | network temporal id type 46 | 47 | Examples 48 | -------- 49 | 50 | >>> import dynetx as dn 51 | >>> g = dn.DynGraph() 52 | >>> g.add_interaction("A", "B", 1, 4) 53 | >>> g.add_interaction("B", "D", 2, 5) 54 | >>> g.add_interaction("A", "C", 4, 8) 55 | >>> g.add_interaction("B", "D", 2, 4) 56 | >>> g.add_interaction("B", "C", 6, 10) 57 | >>> g.add_interaction("B", "D", 2, 4) 58 | >>> g.add_interaction("A", "B", 7, 9) 59 | >>> DAG, sources, targets, _, _ = al.temporal_dag(g, "D", "C", start=1, end=9) 60 | 61 | """ 62 | ids = G.temporal_snapshots_ids() 63 | if len(ids) == 0: 64 | return nx.DiGraph(), [], [], int, int 65 | 66 | tid_type = type(ids[0]) 67 | node_type = type(u) 68 | 69 | # correcting missing values 70 | if end is None: 71 | end = ids[-1] 72 | 73 | if start is None: 74 | start = ids[0] 75 | 76 | if start < min(ids) or start > end or end > max(ids) or start > max(ids): 77 | raise ValueError(f"The specified interval {[start, end]} is not a proper subset of the network timestamps " 78 | f"{[min(ids), max(ids)]}.") 79 | 80 | # adjusting temporal window 81 | start = list([i >= start for i in ids]).index(True) 82 | end = end if end == ids[-1] else list([i > end for i in ids]).index(True) 83 | ids = ids[start:end+1] 84 | 85 | # creating empty DAG 86 | DG = nx.DiGraph() 87 | DG.add_node(u) 88 | active = {u: None} 89 | sources, targets = {}, {} 90 | 91 | for tid in ids: 92 | to_remove = [] 93 | to_add = [] 94 | for an in active: 95 | neighbors = {f"{n}_{tid}": None for n in G.neighbors(node_type(str(an).split("_")[0]), tid)} 96 | if v is not None: 97 | if f"{v}_{tid}" in neighbors: 98 | targets[f"{v}_{tid}"] = None 99 | else: 100 | for k in neighbors: 101 | targets[k] = None 102 | 103 | if len(neighbors) == 0 and an != u: 104 | to_remove.append(an) 105 | 106 | for n in neighbors: 107 | if isinstance(an, node_type): 108 | if not isinstance(an, str) or (isinstance(an, str) and '_' not in an): 109 | an = f"{an}_{tid}" 110 | sources[an] = None 111 | 112 | DG.add_edge(an, n) 113 | to_add.append(n) 114 | 115 | for n in to_add: 116 | active[n] = None 117 | 118 | for rm in to_remove: 119 | del active[rm] 120 | 121 | return DG, list(sources), list(targets), node_type, tid_type 122 | 123 | 124 | def time_respecting_paths(G, u, v=None, start=None, end=None, sample=1): 125 | """ 126 | Computes all the simple time respecting paths among u and v within [start, stop]. 127 | It assumes interaction chains of length 1 within each network snapshot. 128 | 129 | Parameters 130 | ---------- 131 | G : a DynGraph or DynDiGraph object 132 | The graph to use for computing DAG 133 | u : a node id 134 | A node in G 135 | v : a node id 136 | A node in G 137 | start : temporal id 138 | min temporal id for bounding the DAG, default None 139 | end : temporal id to conclude the search 140 | max temporal id for bounding the DAG, default None 141 | sample : percentage of connected node pairs for which compute the time respecting paths. Default 1. 142 | 143 | Returns 144 | -------- 145 | paths: dict 146 | A dictionary that associate to each node pair (u,v) the list of time respecting paths connecting them. 147 | 148 | Examples 149 | -------- 150 | 151 | >>> import dynetx as dn 152 | >>> g = dn.DynGraph() 153 | >>> g.add_interaction("A", "B", 1, 4) 154 | >>> g.add_interaction("B", "D", 2, 5) 155 | >>> g.add_interaction("A", "C", 4, 8) 156 | >>> g.add_interaction("B", "D", 2, 4) 157 | >>> g.add_interaction("B", "C", 6, 10) 158 | >>> g.add_interaction("B", "D", 2, 4) 159 | >>> g.add_interaction("A", "B", 7, 9) 160 | >>> paths = al.time_respecting_paths(g, "D", "C", start=1, end=9) 161 | 162 | """ 163 | if not G.has_node(u, start): 164 | return [] 165 | 166 | DAG, sources, targets, n_type, t_type = temporal_dag(G, u, v=v, start=start, end=end) 167 | 168 | 169 | pairs = [(x, y) for x in sources for y in targets] 170 | if sample < 1: 171 | to_sample = int(len(pairs) * sample) 172 | pairs_idx = np.random.choice(len(pairs), size=to_sample, replace=False) 173 | pairs = np.array(pairs)[pairs_idx] 174 | 175 | paths = [] 176 | for pair in pairs: 177 | path = list(nx.all_simple_paths(DAG, pair[0], pair[1])) 178 | 179 | for p in path: 180 | pt = [] 181 | for first, second in zip(p, p[1:]): 182 | u = first.split("_") 183 | if len(u) == 2: 184 | u = u[0] 185 | else: 186 | u = "_".join(u[0:-1]) 187 | 188 | v = second.split("_") 189 | if len(v) == 2: 190 | t = v[1] 191 | v = v[0] 192 | else: 193 | t = v[-1] 194 | v = "_".join(v[0:-1]) 195 | 196 | pt.append((n_type(u), n_type(v), t_type(t))) 197 | # check ping pong 198 | 199 | flag = True 200 | if len(pt) > 1: 201 | s = pt[0] 202 | for l in pt[1:]: 203 | if l[0] == s[1] and l[1] == s[0] or l[2] == s[2]: 204 | flag = False 205 | continue 206 | s = l 207 | 208 | if flag: 209 | paths.append(pt) 210 | 211 | pa = list(dict.fromkeys([tuple(x) for x in paths])) 212 | 213 | res = defaultdict(list) 214 | for p in pa: 215 | k = (p[0][0], p[-1][1]) 216 | res[k].append(p) 217 | 218 | return res 219 | 220 | 221 | def all_time_respecting_paths(G, start=None, end=None, sample=1, min_t=None): 222 | """ 223 | Computes all the simple paths among network node pairs. 224 | It assumes interaction chains of length 1 within each network snapshot. 225 | 226 | Parameters 227 | ---------- 228 | G : a DynGraph or DynDiGraph object 229 | The graph to use for computing DAG 230 | start : temporal id 231 | min temporal id for bounding the DAG, default None 232 | end : temporal id to conclude the search 233 | max temporal id for bounding the DAG, default None 234 | sample : percentage of paths to compute. Default, 1 235 | min_t : temporal id for the source nodes, Default, None (all possible tids in [start, end]) 236 | 237 | Returns 238 | -------- 239 | paths: dictionary 240 | A dictionary that associate to each node pair (u,v) the list of paths connecting them. 241 | 242 | Examples 243 | -------- 244 | 245 | >>> import dynetx as dn 246 | >>> g = dn.DynGraph() 247 | >>> g.add_interaction("A", "B", 1, 4) 248 | >>> g.add_interaction("B", "D", 2, 5) 249 | >>> g.add_interaction("A", "C", 4, 8) 250 | >>> g.add_interaction("B", "D", 2, 4) 251 | >>> g.add_interaction("B", "C", 6, 10) 252 | >>> g.add_interaction("B", "D", 2, 4) 253 | >>> g.add_interaction("A", "B", 7, 9) 254 | >>> paths = al.all_time_respecting_paths(g, start=1, end=9) 255 | 256 | """ 257 | res = {} 258 | for u in tqdm.tqdm(G.nodes(t=min_t)): 259 | paths = time_respecting_paths(G, u, v=None, start=start, end=end, sample=sample) #list 260 | if len(paths) > 0: 261 | for k, path in paths.items(): 262 | v = k[-1] 263 | res[(u, v)] = path 264 | 265 | return res 266 | 267 | 268 | def annotate_paths(paths): 269 | """ 270 | Annotate a set of paths identifying peculiar types of paths. 271 | 272 | - **shortest**: topological shortest paths 273 | - **fastest**: paths that have minimal duration 274 | - **foremost**: first paths that reach the destination 275 | - **shortest fastest**: minimum length path among minimum duration ones 276 | - **fastest shortest**: minimum duration path among minimum length ones 277 | 278 | Parameters 279 | ---------- 280 | paths : list 281 | a list of paths among a same node pair 282 | 283 | Returns 284 | -------- 285 | annotated: dictionary 286 | A mapping for shortest, fastest, foremost, fastest_shortest and shortest_fastest paths. 287 | 288 | Examples 289 | -------- 290 | 291 | >>> import dynetx as dn 292 | >>> g = dn.DynGraph() 293 | >>> g.add_interaction("A", "B", 1, 4) 294 | >>> g.add_interaction("B", "D", 2, 5) 295 | >>> g.add_interaction("A", "C", 4, 8) 296 | >>> g.add_interaction("B", "D", 2, 4) 297 | >>> g.add_interaction("B", "C", 6, 10) 298 | >>> g.add_interaction("B", "D", 2, 4) 299 | >>> g.add_interaction("A", "B", 7, 9) 300 | >>> paths = al.time_respecting_paths(g, "D", "C", start=1, end=9) 301 | >>> annotated = al.annotate_paths(paths) 302 | 303 | """ 304 | annotated = {"shortest": None, "fastest": None, "shortest_fastest": None, 305 | "fastest_shortest": None, "foremost": None} 306 | 307 | min_to_reach = None 308 | shortest = None 309 | fastest = None 310 | 311 | for path in paths: 312 | length = path_length(path) 313 | duration = path_duration(path) 314 | reach = path[-1][-1] 315 | 316 | if shortest is None or length < shortest: 317 | shortest = length 318 | annotated['shortest'] = [copy.copy(path)] 319 | elif length == shortest: 320 | annotated['shortest'].append(copy.copy(path)) 321 | 322 | if fastest is None or duration < fastest: 323 | fastest = duration 324 | annotated['fastest'] = [copy.copy(path)] 325 | elif duration == fastest: 326 | annotated['fastest'].append(copy.copy(path)) 327 | 328 | if min_to_reach is None or reach < min_to_reach: 329 | min_to_reach = reach 330 | annotated['foremost'] = [copy.copy(path)] 331 | elif reach == min_to_reach: 332 | annotated['foremost'].append(copy.copy(path)) 333 | 334 | fastest_shortest = {tuple(path): path_duration(path) for path in annotated['shortest']} 335 | minval = min(fastest_shortest.values()) 336 | fastest_shortest = list([x for x in fastest_shortest if fastest_shortest[x] == minval]) 337 | 338 | shortest_fastest = {tuple(path): path_length(path) for path in annotated['fastest']} 339 | minval = min(shortest_fastest.values()) 340 | shortest_fastest = list([x for x in shortest_fastest if shortest_fastest[x] == minval]) 341 | 342 | annotated['fastest_shortest'] = [list(p) for p in fastest_shortest] 343 | annotated['shortest_fastest'] = [list(p) for p in shortest_fastest] 344 | return annotated 345 | 346 | 347 | def path_length(path): 348 | """ 349 | Computes the topological length of a given path. 350 | 351 | Parameters 352 | ---------- 353 | path : a path 354 | list of interactions forming a path among a node pair 355 | 356 | Returns 357 | -------- 358 | length: int 359 | The number of interactions composing the path 360 | 361 | Examples 362 | -------- 363 | 364 | >>> import dynetx as dn 365 | >>> g = dn.DynGraph() 366 | >>> g.add_interaction("A", "B", 1, 4) 367 | >>> g.add_interaction("B", "D", 2, 5) 368 | >>> g.add_interaction("A", "C", 4, 8) 369 | >>> g.add_interaction("B", "D", 2, 4) 370 | >>> g.add_interaction("B", "C", 6, 10) 371 | >>> g.add_interaction("B", "D", 2, 4) 372 | >>> g.add_interaction("A", "B", 7, 9) 373 | >>> paths = al.time_respecting_paths(g, "D", "C", start=1, end=9) 374 | >>> for p in paths: 375 | >>> print(al.path_length(p)) 376 | 377 | """ 378 | return len(path) 379 | 380 | 381 | def path_duration(path): 382 | """ 383 | Computes the timespan of a given path. 384 | 385 | Parameters 386 | ---------- 387 | path : a path 388 | list of interactions forming a path among a node pair 389 | 390 | Returns 391 | -------- 392 | duration: int 393 | The duration of the path 394 | 395 | Examples 396 | -------- 397 | 398 | >>> import dynetx as dn 399 | >>> g = dn.DynGraph() 400 | >>> g.add_interaction("A", "B", 1, 4) 401 | >>> g.add_interaction("B", "D", 2, 5) 402 | >>> g.add_interaction("A", "C", 4, 8) 403 | >>> g.add_interaction("B", "D", 2, 4) 404 | >>> g.add_interaction("B", "C", 6, 10) 405 | >>> g.add_interaction("B", "D", 2, 4) 406 | >>> g.add_interaction("A", "B", 7, 9) 407 | >>> paths = al.time_respecting_paths(g, "D", "C", start=1, end=9) 408 | >>> for p in paths: 409 | >>> print(al.path_duration(p)) 410 | 411 | """ 412 | return path[-1][-1] - path[0][-1] 413 | -------------------------------------------------------------------------------- /dynetx/test/test_dyndigraph.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import dynetx as dn 3 | 4 | 5 | class DynDiGraphTestCase(unittest.TestCase): 6 | 7 | def test_self_loop(self): 8 | G = dn.DynDiGraph() 9 | G.add_interaction(0, 1, t=0) 10 | G.add_interaction(0, 2, t=0) 11 | G.add_interaction(0, 0, t=0) 12 | G.add_interaction(1, 1, t=0) 13 | G.add_interaction(2, 2, t=0) 14 | G.add_interaction(2, 2, t=2) 15 | ints = G.interactions(t=0) 16 | self.assertEqual(len(ints), 5) 17 | self.assertEqual(G.has_interaction(0, 0, t=0), True) 18 | self.assertEqual(G.has_interaction(2, 0, t=0), False) 19 | 20 | def test_dyndigraph_add_interaction(self): 21 | g = dn.DynDiGraph() 22 | self.assertIsInstance(g, dn.DynDiGraph) 23 | 24 | g.add_interaction(1, 2, 2) 25 | g.add_interaction(1, 2, 2, e=6) 26 | g.add_interaction(1, 2, 7, e=11) 27 | g.add_interaction(1, 2, 8, e=15) 28 | g.add_interaction(1, 2, 18) 29 | g.add_interaction(1, 2, 19) 30 | 31 | its = g.interactions() 32 | self.assertEqual(len(its), 1) 33 | 34 | g.add_interactions_from([(1, 3), (1, 5)], t=2) 35 | 36 | its = g.interactions() 37 | self.assertEqual(len(its), 3) 38 | 39 | its = g.interactions(t=18) 40 | self.assertEqual(len(its), 1) 41 | 42 | its = g.interactions(t=20) 43 | self.assertEqual(len(its), 0) 44 | 45 | self.assertEqual(len(list(g.neighbors_iter(1))), 3) 46 | self.assertEqual(len(list(g.neighbors_iter(1, 7))), 1) 47 | self.assertEqual(len(list(g.neighbors_iter(1, 0))), 0) 48 | self.assertEqual(g.order(), len(g.nodes())) 49 | 50 | self.assertEqual(g.has_node(42), False) 51 | 52 | self.assertEqual(g.has_node(42, 3), False) 53 | g.add_interaction(3, 4, 34) 54 | g.add_interaction(4, 5, 34) 55 | g.add_interaction(5, 6, 34) 56 | g.add_interaction(6, 4, 34) 57 | try: 58 | g.time_slice(2, 1) 59 | except: 60 | pass 61 | 62 | g.interactions_iter([1, 2]) 63 | 64 | try: 65 | g.add_interaction(1, 5) 66 | except: 67 | pass 68 | 69 | try: 70 | g.add_interactions_from([(1, 4), (3, 6)]) 71 | except: 72 | pass 73 | 74 | try: 75 | g.remove_edge(1, 2) 76 | except: 77 | pass 78 | 79 | try: 80 | g.remove_edges_from([(1, 2)]) 81 | except: 82 | pass 83 | try: 84 | g.remove_node(1) 85 | except: 86 | pass 87 | try: 88 | g.remove_nodes_from([1, 2]) 89 | except: 90 | pass 91 | 92 | self.assertEqual(g.number_of_interactions(1, 90), 0) 93 | 94 | def test_nodes(self): 95 | 96 | g = dn.DynDiGraph() 97 | g.add_interaction(0, 1, 5) 98 | g.add_interaction(0, 2, 5) 99 | g.add_interaction(0, 3, 5) 100 | g.add_interaction(0, 4, 5) 101 | 102 | nds = len(g.nodes()) 103 | self.assertEqual(nds, 5) 104 | 105 | g.add_interaction(5, 1, 6) 106 | g.add_interaction(5, 2, 6) 107 | g.add_interaction(2, 3, 6) 108 | g.add_interaction(5, 4, 6) 109 | 110 | nds = len(g.nodes()) 111 | self.assertEqual(nds, 6) 112 | 113 | nds = len(g.nodes(t=6)) 114 | self.assertEqual(nds, 5) 115 | 116 | nds = len(g.nodes(t=9)) 117 | self.assertEqual(nds, 0) 118 | 119 | self.assertEqual(g.has_node(0), True) 120 | self.assertEqual(g.has_node(0, 5), True) 121 | self.assertEqual(g.has_node(0, 6), False) 122 | self.assertEqual(g.has_node(0, 0), False) 123 | 124 | def test_number_of_interactions(self): 125 | g = dn.DynDiGraph() 126 | g.add_interaction(0, 1, 5) 127 | g.add_interaction(1, 2, 5) 128 | g.add_interaction(2, 3, 5) 129 | g.add_interaction(3, 4, 5) 130 | g.add_interaction(4, 5, 6) 131 | g.add_interaction(5, 6, 6) 132 | g.add_interaction(6, 7, 6) 133 | g.add_interaction(7, 8, 6) 134 | 135 | its = g.number_of_interactions() 136 | self.assertEqual(its, 8) 137 | 138 | its = g.number_of_interactions(0) 139 | self.assertEqual(its, None) 140 | 141 | its = g.number_of_interactions(0, 1) 142 | self.assertEqual(its, 1) 143 | 144 | its = g.number_of_interactions(0, 1, 5) 145 | self.assertEqual(its, 1) 146 | 147 | its = g.number_of_interactions(0, 1, 6) 148 | self.assertEqual(its, 0) 149 | 150 | def test_has_interaction(self): 151 | g = dn.DynDiGraph() 152 | g.add_interaction(0, 1, 5) 153 | g.add_interaction(1, 2, 5) 154 | g.add_interaction(2, 3, 5) 155 | g.add_interaction(3, 4, 5) 156 | g.add_interaction(4, 5, 6) 157 | g.add_interaction(5, 6, 6) 158 | g.add_interaction(6, 7, 6) 159 | g.add_interaction(7, 8, 6) 160 | 161 | self.assertEqual(g.has_interaction(0, 1), True) 162 | self.assertEqual(g.has_interaction(1, 0), False) 163 | self.assertEqual(g.has_interaction(0, 1, 5), True) 164 | self.assertEqual(g.has_interaction(1, 0, 5), False) 165 | self.assertEqual(g.has_interaction(0, 1, 6), False) 166 | self.assertEqual(g.has_interaction(0, 1, 9), False) 167 | 168 | def test_in_out_interactions(self): 169 | g = dn.DynDiGraph() 170 | g.add_interaction(0, 1, 5) 171 | g.add_interaction(1, 2, 5) 172 | g.add_interaction(2, 3, 5) 173 | g.add_interaction(3, 4, 5) 174 | g.add_interaction(4, 5, 6) 175 | g.add_interaction(5, 6, 6) 176 | g.add_interaction(6, 7, 6) 177 | g.add_interaction(7, 8, 6) 178 | 179 | sr = g.in_interactions() 180 | self.assertEqual(sr, [(0, 1, {'t': [[5, 5]]}), (1, 2, {'t': [[5, 5]]}), (2, 3, {'t': [[5, 5]]}), 181 | (3, 4, {'t': [[5, 5]]}), (4, 5, {'t': [[6, 6]]}), (5, 6, {'t': [[6, 6]]}), 182 | (6, 7, {'t': [[6, 6]]}), (7, 8, {'t': [[6, 6]]})]) 183 | 184 | sr = g.in_interactions([0, 1]) 185 | self.assertEqual(sr, [(0, 1, {'t': [[5, 5]]})]) 186 | 187 | sr = g.in_interactions([9, 10]) 188 | self.assertEqual(sr, []) 189 | 190 | sr = g.in_interactions([0, 1], 5) 191 | self.assertEqual(sr, [(0, 1, {'t': [5]})]) 192 | 193 | sr = g.in_interactions([0, 1], 7) 194 | self.assertEqual(sr, []) 195 | 196 | sr = g.out_interactions() 197 | self.assertEqual(sr, [(0, 1, {'t': [[5, 5]]}), (1, 2, {'t': [[5, 5]]}), (2, 3, {'t': [[5, 5]]}), 198 | (3, 4, {'t': [[5, 5]]}), (4, 5, {'t': [[6, 6]]}), (5, 6, {'t': [[6, 6]]}), 199 | (6, 7, {'t': [[6, 6]]}), (7, 8, {'t': [[6, 6]]})]) 200 | 201 | sr = g.out_interactions([0]) 202 | self.assertEqual(sr, [(0, 1, {'t': [[5, 5]]})]) 203 | 204 | sr = g.out_interactions([9, 10]) 205 | self.assertEqual(sr, []) 206 | 207 | sr = g.out_interactions([0, 1], 5) 208 | self.assertEqual(sr, [(0, 1, {'t': [5]}), (1, 2, {'t': [5]})]) 209 | 210 | sr = g.out_interactions([0, 1], 7) 211 | self.assertEqual(sr, []) 212 | 213 | def test_update_node_attr(self): 214 | g = dn.DynDiGraph() 215 | 216 | for n in [0, 1, 2, 3, 4, 5, 6, 7, 8]: 217 | g.add_node(n, Label="A") 218 | 219 | for n in g.nodes(): 220 | g.update_node_attr(n, Label="B") 221 | 222 | for n in g.nodes(data=True): 223 | self.assertEqual(n[1]['Label'], "B") 224 | 225 | g.update_node_attr_from([0, 1, 2], Label="C") 226 | self.assertEqual(g._node[0]['Label'], "C") 227 | 228 | def test_neighbors(self): 229 | g = dn.DynDiGraph() 230 | g.add_interaction(0, 1, 5) 231 | g.add_interaction(1, 2, 5) 232 | g.add_interaction(2, 3, 5) 233 | g.add_interaction(3, 4, 5) 234 | g.add_interaction(4, 5, 6) 235 | g.add_interaction(5, 6, 6) 236 | g.add_interaction(6, 7, 6) 237 | g.add_interaction(7, 8, 6) 238 | 239 | ng = len(g.neighbors(0)) 240 | self.assertEqual(ng, 1) 241 | 242 | ng = len(g.neighbors(0, 5)) 243 | self.assertEqual(ng, 1) 244 | 245 | ng = len(g.neighbors(0, 6)) 246 | self.assertEqual(ng, 0) 247 | 248 | ng = len(g.neighbors(0, 0)) 249 | self.assertEqual(ng, 0) 250 | 251 | ng = len(g.predecessors(0)) 252 | self.assertEqual(ng, 0) 253 | 254 | ng = len(g.successors(0)) 255 | self.assertEqual(ng, 1) 256 | 257 | ng = len(g.predecessors(1)) 258 | self.assertEqual(ng, 1) 259 | 260 | ng = len(g.successors(1)) 261 | self.assertEqual(ng, 1) 262 | 263 | ng = len(g.predecessors(1, 5)) 264 | self.assertEqual(ng, 1) 265 | 266 | ng = len(g.successors(1, 5)) 267 | self.assertEqual(ng, 1) 268 | 269 | ng = len(g.predecessors(1, 6)) 270 | self.assertEqual(ng, 0) 271 | 272 | ng = len(g.successors(1, 6)) 273 | self.assertEqual(ng, 0) 274 | 275 | def test_degree(self): 276 | g = dn.DynDiGraph() 277 | g.add_interaction(0, 1, 5) 278 | g.add_interaction(0, 2, 5) 279 | g.add_interaction(0, 3, 5) 280 | g.add_interaction(0, 4, 5) 281 | g.add_interaction(4, 5, 6) 282 | g.add_interaction(4, 6, 6) 283 | g.add_interaction(4, 7, 6) 284 | g.add_interaction(4, 8, 6) 285 | 286 | ng = g.degree(4) 287 | self.assertEqual(ng, 5) 288 | 289 | ng = g.in_degree(4) 290 | self.assertEqual(ng, 1) 291 | 292 | ng = g.out_degree(4) 293 | self.assertEqual(ng, 4) 294 | 295 | ng = g.degree(4, 5) 296 | self.assertEqual(ng, 1) 297 | 298 | ng = g.in_degree(4, 5) 299 | self.assertEqual(ng, 1) 300 | 301 | ng = g.out_degree(4, 5) 302 | self.assertEqual(ng, 0) 303 | 304 | ng = g.degree(4, 6) 305 | self.assertEqual(ng, 4) 306 | 307 | ng = g.degree(4, 0) 308 | self.assertEqual(ng, 0) 309 | 310 | ng = g.in_degree(4, 6) 311 | self.assertEqual(ng, 0) 312 | 313 | ng = g.out_degree(4, 0) 314 | self.assertEqual(ng, 0) 315 | 316 | def test_number_of_nodes(self): 317 | g = dn.DynDiGraph() 318 | g.add_interaction(0, 1, 5) 319 | g.add_interaction(0, 2, 5) 320 | g.add_interaction(0, 3, 5) 321 | g.add_interaction(0, 4, 5) 322 | g.add_interaction(4, 5, 6) 323 | g.add_interaction(4, 6, 6) 324 | g.add_interaction(4, 7, 6) 325 | g.add_interaction(4, 8, 6) 326 | 327 | nn = g.number_of_nodes() 328 | self.assertEqual(nn, 9) 329 | 330 | nn = g.number_of_nodes(t=5) 331 | self.assertEqual(nn, 5) 332 | 333 | nn = g.number_of_nodes(t=0) 334 | self.assertEqual(nn, 0) 335 | 336 | nds = g.avg_number_of_nodes() 337 | self.assertEqual(nds, 5) 338 | 339 | def test_time_slice(self): 340 | g = dn.DynDiGraph() 341 | g.add_interaction(0, 1, 5) 342 | g.add_interaction(0, 2, 5) 343 | g.add_interaction(0, 3, 5) 344 | g.add_interaction(0, 4, 5) 345 | g.add_interaction(4, 5, 6) 346 | g.add_interaction(4, 6, 6) 347 | g.add_interaction(4, 7, 6) 348 | g.add_interaction(4, 8, 6) 349 | 350 | h = g.time_slice(5) 351 | self.assertIsInstance(h, dn.DynDiGraph) 352 | self.assertEqual(h.number_of_nodes(), 5) 353 | self.assertEqual(h.number_of_interactions(), 4) 354 | 355 | h = g.time_slice(5, 5) 356 | self.assertIsInstance(h, dn.DynDiGraph) 357 | self.assertEqual(h.number_of_nodes(), 5) 358 | self.assertEqual(h.number_of_interactions(), 4) 359 | 360 | h = g.time_slice(5, 6) 361 | self.assertIsInstance(h, dn.DynDiGraph) 362 | self.assertEqual(h.number_of_nodes(), 9) 363 | self.assertEqual(h.number_of_interactions(), 8) 364 | 365 | h = g.time_slice(0) 366 | self.assertIsInstance(h, dn.DynDiGraph) 367 | self.assertEqual(h.number_of_nodes(), 0) 368 | self.assertEqual(h.number_of_interactions(), 0) 369 | 370 | def test_temporal_snapshots_ids(self): 371 | g = dn.DynDiGraph() 372 | g.add_interaction(0, 1, 5) 373 | g.add_interaction(1, 2, 5) 374 | g.add_interaction(2, 3, 5) 375 | g.add_interaction(3, 4, 5) 376 | g.add_interaction(4, 5, 6) 377 | g.add_interaction(5, 6, 6) 378 | g.add_interaction(6, 7, 6) 379 | g.add_interaction(7, 8, 6) 380 | tsd =g.temporal_snapshots_ids() 381 | 382 | self.assertEqual(tsd, [5, 6]) 383 | 384 | def test_interactions_per_snapshots(self): 385 | g = dn.DynDiGraph() 386 | g.add_interaction(0, 1, 5) 387 | g.add_interaction(1, 2, 5) 388 | g.add_interaction(2, 3, 5) 389 | g.add_interaction(3, 4, 5) 390 | g.add_interaction(4, 5, 6) 391 | g.add_interaction(5, 6, 6) 392 | g.add_interaction(6, 7, 6) 393 | g.add_interaction(7, 8, 6) 394 | 395 | tsd = g.interactions_per_snapshots() 396 | self.assertDictEqual(tsd, {5: 4, 6: 4}) 397 | 398 | tsd = g.interactions_per_snapshots(t=5) 399 | self.assertEqual(tsd, 4) 400 | 401 | tsd = g.interactions_per_snapshots(t=0) 402 | self.assertEqual(tsd, 0) 403 | 404 | def test_inter_out_event_time(self): 405 | g = dn.DynDiGraph() 406 | g.add_interaction(0, 1, 2) 407 | g.add_interaction(0, 2, 2) 408 | g.add_interaction(0, 3, 2) 409 | g.add_interaction(0, 4, 2) 410 | g.add_interaction(4, 5, 3) 411 | g.add_interaction(4, 6, 3) 412 | g.add_interaction(4, 7, 3) 413 | g.add_interaction(4, 8, 3) 414 | g.add_interaction(0, 2, 4) 415 | 416 | # complete 417 | 418 | ivt = g.inter_event_time_distribution() 419 | self.assertDictEqual(ivt, {0: 6, 1: 2}) 420 | 421 | ivt = g.inter_event_time_distribution(4) 422 | self.assertDictEqual(ivt, {0: 3, 1: 1}) 423 | 424 | ivt = g.inter_event_time_distribution(0) 425 | self.assertDictEqual(ivt, {0: 3, 2: 1}) 426 | 427 | ivt = g.inter_event_time_distribution(0, 1) 428 | self.assertDictEqual(ivt, {}) 429 | 430 | ivt = g.inter_event_time_distribution(0, 2) 431 | self.assertDictEqual(ivt, {2: 1}) 432 | 433 | ivt = g.inter_event_time_distribution(2, 0) 434 | self.assertDictEqual(ivt, {2: 1}) 435 | 436 | # out 437 | 438 | ivt = g.inter_out_event_time_distribution() 439 | self.assertDictEqual(ivt, {0: 6, 1: 2}) 440 | 441 | ivt = g.inter_out_event_time_distribution(4) 442 | self.assertDictEqual(ivt, {0: 3}) 443 | 444 | ivt = g.inter_out_event_time_distribution(0) 445 | self.assertDictEqual(ivt, {0: 3, 2: 1}) 446 | 447 | ivt = g.inter_out_event_time_distribution(0, 1) 448 | self.assertDictEqual(ivt, {}) 449 | 450 | ivt = g.inter_out_event_time_distribution(0, 2) 451 | self.assertDictEqual(ivt, {2: 1}) 452 | 453 | # in 454 | 455 | ivt = g.inter_in_event_time_distribution() 456 | self.assertDictEqual(ivt, {0: 6, 1: 2}) 457 | 458 | ivt = g.inter_in_event_time_distribution(4) 459 | self.assertDictEqual(ivt, {}) 460 | 461 | ivt = g.inter_in_event_time_distribution(2) 462 | self.assertDictEqual(ivt, {2: 1}) 463 | 464 | ivt = g.inter_in_event_time_distribution(0, 1) 465 | self.assertDictEqual(ivt, {}) 466 | 467 | ivt = g.inter_in_event_time_distribution(2, 0) 468 | self.assertDictEqual(ivt, {2: 1}) 469 | 470 | ivt = g.inter_in_event_time_distribution(0, 2) 471 | self.assertDictEqual(ivt, {}) 472 | 473 | 474 | def test_stream_interactions(self): 475 | g = dn.DynDiGraph() 476 | g.add_interaction(1, 2, 2) 477 | g.add_interaction(1, 2, 2, e=6) 478 | g.add_interaction(1, 2, 7, e=11) 479 | g.add_interaction(1, 2, 8, e=15) 480 | g.add_interaction(1, 2, 18) 481 | g.add_interaction(1, 2, 19) 482 | g.add_interactions_from([(1, 3), (1, 5)], t=2, e=3) 483 | 484 | sres = list(g.stream_interactions()) 485 | 486 | cres = [(1, 2, '+', 2), (1, 3, '+', 2), (1, 5, '+', 2), (1, 3, '-', 3), 487 | (1, 5, '-', 3), (1, 2, '-', 6), (1, 2, '+', 7), (1, 2, '-', 15), (1, 2, '+', 18)] 488 | self.assertEqual(sorted(sres), sorted(cres)) 489 | 490 | def test_accumulative_growth(self): 491 | g = dn.DynDiGraph(edge_removal=False) 492 | g.add_interaction(1, 2, 2) 493 | g.add_interaction(1, 2, 2, e=6) 494 | g.add_interaction(1, 2, 7, e=11) 495 | g.add_interaction(1, 2, 8, e=15) 496 | g.add_interaction(1, 2, 18) 497 | g.add_interaction(1, 2, 19) 498 | g.add_interactions_from([(1, 3), (1, 5)], t=2, e=3) 499 | sres = list(g.stream_interactions()) 500 | cres = [(1, 2, '+', 2), (1, 5, '+', 2), (1, 3, '+', 2)] 501 | self.assertEqual(sorted(sres), sorted(cres)) 502 | self.assertEqual(g.has_interaction(1, 2, 18), True) 503 | self.assertEqual(g.has_interaction(1, 2, 40), False) 504 | try: 505 | g.add_interaction(2, 1, 7) 506 | except: 507 | pass 508 | 509 | def test_conversion(self): 510 | G = dn.DynDiGraph() 511 | G.add_interaction(0, 1, t=0) 512 | G.add_interaction(0, 2, t=0) 513 | G.add_interaction(0, 0, t=0) 514 | G.add_interaction(1, 1, t=0) 515 | G.add_interaction(2, 2, t=0) 516 | G.add_interaction(2, 2, t=2) 517 | 518 | H = G.to_undirected() 519 | self.assertIsInstance(H, dn.DynGraph) 520 | self.assertEqual(H.number_of_nodes(), 3) 521 | self.assertEqual(H.number_of_edges(), 3) 522 | 523 | if __name__ == '__main__': 524 | unittest.main() 525 | -------------------------------------------------------------------------------- /dynetx/classes/function.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | from dynetx.utils import not_implemented 4 | from collections import Counter 5 | from itertools import chain 6 | 7 | import networkx as nx 8 | 9 | 10 | __author__ = 'Giulio Rossetti' 11 | __license__ = "BSD-Clause-2" 12 | __email__ = "giulio.rossetti@gmail.com" 13 | 14 | 15 | __all__ = ['nodes', 'interactions', 'degree', 'degree_histogram', 'neighbors', 16 | 'number_of_nodes', 'number_of_interactions', 'density', 17 | 'is_directed', 'freeze', 'is_frozen', 'subgraph', 18 | 'add_star', 'add_path', 'add_cycle', 19 | 'create_empty_copy', 'set_node_attributes', 20 | 'get_node_attributes', 'set_edge_attributes', 21 | 'get_edge_attributes', 'all_neighbors', 'non_neighbors', 22 | 'non_interactions', 'is_empty', 'time_slice', 'stream_interactions', 'interactions_per_snapshots', 23 | 'temporal_snapshots_ids', 'inter_event_time_distribution'] 24 | 25 | 26 | def nodes(G, t=None): 27 | """Return a list of the nodes in the graph at a given snapshot. 28 | 29 | Parameters 30 | ---------- 31 | 32 | G : Graph opject 33 | DyNetx graph object 34 | 35 | t : snapshot id (default=None) 36 | If None the the method returns all the nodes of the flattened graph. 37 | 38 | Returns 39 | ------- 40 | nlist : list 41 | A list of nodes. If data=True a list of two-tuples containing 42 | (node, node data dictionary). 43 | 44 | Examples 45 | -------- 46 | >>> import dynetx as dn 47 | >>> G = dn.DynGraph() # or DiGraph, MultiGraph, MultiDiGraph, etc 48 | >>> G.add_path([0,1,2], 0) 49 | >>> dn.nodes(G, t=0) 50 | [0, 1, 2] 51 | >>> G.add_edge(1, 4, t=1) 52 | >>> dn.nodes(G, t=0) 53 | [0, 1, 2] 54 | """ 55 | return G.nodes(t) 56 | 57 | 58 | def interactions(G, nbunch=None, t=None): 59 | """Return the list of edges present in a given snapshot. 60 | 61 | Edges are returned as tuples 62 | in the order (node, neighbor). 63 | 64 | Parameters 65 | ---------- 66 | 67 | G : Graph opject 68 | DyNetx graph object 69 | 70 | nbunch : iterable container, optional (default= all nodes) 71 | A container of nodes. The container will be iterated 72 | through once. 73 | 74 | t : snapshot id (default=None) 75 | If None the the method returns all the edges of the flattened graph. 76 | 77 | Returns 78 | -------- 79 | edge_list: list of edge tuples 80 | Edges that are adjacent to any node in nbunch, or a list 81 | of all edges if nbunch is not specified. 82 | 83 | Notes 84 | ----- 85 | Nodes in nbunch that are not in the graph will be (quietly) ignored. 86 | For directed graphs this returns the out-edges. 87 | 88 | Examples 89 | -------- 90 | >>> G = dn.DynGraph() 91 | >>> G.add_path([0,1,2], t=0) 92 | >>> G.add_edge(2,3, t=1) 93 | >>> dn.interactions(G, t=0) 94 | [(0, 1), (1, 2)] 95 | >>> dn.interactions(G) 96 | [(0, 1), (1, 2), (2, 3)] 97 | >>> dn.interactions(G, [0,3], t=0) 98 | [(0, 1)] 99 | """ 100 | return G.interactions(nbunch, t=t) 101 | 102 | 103 | def degree(G, nbunch=None, t=None): 104 | """Return the degree of a node or nodes at time t. 105 | 106 | The node degree is the number of edges adjacent to that node. 107 | 108 | Parameters 109 | ---------- 110 | 111 | G : Graph opject 112 | DyNetx graph object 113 | 114 | nbunch : iterable container, optional (default=all nodes) 115 | A container of nodes. The container will be iterated 116 | through once. 117 | 118 | t : snapshot id (default=None) 119 | If None will be returned the degree of nodes on the flattened graph. 120 | 121 | 122 | Returns 123 | ------- 124 | nd : dictionary, or number 125 | A dictionary with nodes as keys and degree as values or 126 | a number if a single node is specified. 127 | 128 | Examples 129 | -------- 130 | >>> G = dn.DynGraph() 131 | >>> G.add_path([0,1,2,3], t=0) 132 | >>> dn.degree(G, 0, t=0) 133 | 1 134 | >>> dn.degree(G, [0,1], t=1) 135 | {0: 0, 1: 0} 136 | >>> list(dn.degree(G, [0,1], t=0).values()) 137 | [1, 2] 138 | """ 139 | return G.degree(nbunch, t) 140 | 141 | 142 | def neighbors(G, n, t=None): 143 | """Return a list of the nodes connected to the node n at time t. 144 | 145 | Parameters 146 | ---------- 147 | 148 | G : Graph opject 149 | DyNetx graph object 150 | 151 | n : node 152 | A node in the graph 153 | 154 | t : snapshot id (default=None) 155 | If None will be returned the neighbors of the node on the flattened graph. 156 | 157 | 158 | Returns 159 | ------- 160 | nlist : list 161 | A list of nodes that are adjacent to n. 162 | 163 | Raises 164 | ------ 165 | NetworkXError 166 | If the node n is not in the graph. 167 | 168 | Examples 169 | -------- 170 | >>> G = dn.DynGraph() 171 | >>> G.add_path([0,1,2,3], t=0) 172 | >>> dn.neighbors(G, 0, t=0) 173 | [1] 174 | >>> dn.neighbors(G, 0, t=1) 175 | [] 176 | """ 177 | return G.neighbors(n, t) 178 | 179 | 180 | def number_of_nodes(G, t=None): 181 | """Return the number of nodes in the t snpashot of a dynamic graph. 182 | 183 | Parameters 184 | ---------- 185 | 186 | G : Graph opject 187 | DyNetx graph object 188 | 189 | t : snapshot id (default=None) 190 | If None return the number of nodes in the flattened graph. 191 | 192 | 193 | Returns 194 | ------- 195 | nnodes : int 196 | The number of nodes in the graph. 197 | 198 | See Also 199 | -------- 200 | order which is identical 201 | 202 | Examples 203 | -------- 204 | >>> G = dn.DynGraph() # or DiGraph, MultiGraph, MultiDiGraph, etc 205 | >>> G.add_path([0,1,2], t=0) 206 | >>> dn.number_of_nodes(G, 0) 207 | 3 208 | """ 209 | return G.number_of_nodes(t) 210 | 211 | 212 | def number_of_interactions(G, u=None, v=None, t=None): 213 | """Return the number of edges between two nodes at time t. 214 | 215 | Parameters 216 | ---------- 217 | G : a dynetx graph object 218 | u : node, optional (default=all edges) 219 | v : node, optional (default=all edges) 220 | t : snapshot id (default=None) 221 | If None will be returned the number of edges on the flattened graph. 222 | 223 | If u and v are specified, return the number of edges between u and v. 224 | Otherwise return the total number of all edges. 225 | 226 | Returns 227 | ------- 228 | nedges : int 229 | The number of edges in the graph. If nodes u and v are specified 230 | return the number of edges between those nodes. 231 | 232 | Examples 233 | -------- 234 | >>> import dynetx as dn 235 | >>> G = dn.DynGraph() 236 | >>> G.add_path([0,1,2,3], t=0) 237 | >>> dn.number_of_interactions(G, t=0) 238 | """ 239 | return G.number_of_interactions(u, v, t) 240 | 241 | 242 | def density(G, t=None): 243 | r"""Return the density of a graph at timestamp t. 244 | The density for undirected graphs is 245 | 246 | .. math:: 247 | d = \frac{2m}{n(n-1)}, 248 | 249 | and for directed graphs is 250 | 251 | .. math:: 252 | d = \frac{m}{n(n-1)}, 253 | 254 | where `n` is the number of nodes and `m` is the number of edges in `G`. 255 | 256 | Parameters 257 | ---------- 258 | 259 | G : Graph opject 260 | DyNetx graph object 261 | 262 | 263 | t : snapshot id (default=None) 264 | If None the density will be computed on the flattened graph. 265 | 266 | 267 | Notes 268 | ----- 269 | 270 | The density is 0 for a graph without edges and 1 for a complete graph. 271 | 272 | Self loops are counted in the total number of edges so graphs with self 273 | loops can have density higher than 1. 274 | """ 275 | n = number_of_nodes(G, t) 276 | m = number_of_interactions(G, t) 277 | if m == 0 or m is None or n <= 1: 278 | return 0 279 | 280 | d = m / (n * (n - 1)) 281 | if not G.is_directed(): 282 | d *= 2 283 | return d 284 | 285 | 286 | def degree_histogram(G, t=None): 287 | """Return a list of the frequency of each degree value. 288 | 289 | Parameters 290 | ---------- 291 | 292 | G : Graph opject 293 | DyNetx graph object 294 | 295 | 296 | t : snapshot id (default=None) 297 | snapshot id 298 | 299 | 300 | Returns 301 | ------- 302 | hist : list 303 | A list of frequencies of degrees. 304 | The degree values are the index in the list. 305 | 306 | Notes 307 | ----- 308 | Note: the bins are width one, hence len(list) can be large 309 | (Order(number_of_edges)) 310 | """ 311 | counts = Counter(d for n, d in G.degree(t=t).items()) 312 | return [counts.get(i, 0) for i in range(max(counts) + 1)] 313 | 314 | 315 | def is_directed(G): 316 | """ Return True if graph is directed.""" 317 | return G.is_directed() 318 | 319 | 320 | def frozen(): 321 | """Dummy method for raising errors when trying to modify frozen graphs""" 322 | raise nx.NetworkXError("Frozen graph can't be modified") 323 | 324 | 325 | def freeze(G): 326 | """Modify graph to prevent further change by adding or removing nodes or edges. 327 | 328 | Node and edge data can still be modified. 329 | 330 | Parameters 331 | ---------- 332 | 333 | G : graph 334 | A NetworkX graph 335 | 336 | 337 | Notes 338 | ----- 339 | To "unfreeze" a graph you must make a copy by creating a new graph object. 340 | 341 | See Also 342 | -------- 343 | is_frozen 344 | """ 345 | G.add_node = frozen 346 | G.add_nodes_from = frozen 347 | G.remove_node = frozen 348 | G.remove_nodes_from = frozen 349 | G.add_edge = frozen 350 | G.add_edges_from = frozen 351 | G.remove_edge = frozen 352 | G.remove_edges_from = frozen 353 | G.clear = frozen 354 | G.frozen = True 355 | return G 356 | 357 | 358 | def is_frozen(G): 359 | """Return True if graph is frozen. 360 | 361 | Parameters 362 | ---------- 363 | G : graph 364 | A DyNetx graph 365 | 366 | See Also 367 | -------- 368 | freeze 369 | """ 370 | try: 371 | return G.frozen 372 | except AttributeError: 373 | return False 374 | 375 | 376 | def add_star(G, nodes, t, **attr): 377 | """Add a star at time t. 378 | 379 | The first node in nodes is the middle of the star. It is connected 380 | to all other nodes. 381 | 382 | Parameters 383 | ---------- 384 | G : graph 385 | A DyNetx graph 386 | 387 | nodes : iterable container 388 | A container of nodes. 389 | 390 | t : snapshot id (default=None) 391 | snapshot id 392 | 393 | See Also 394 | -------- 395 | add_path, add_cycle 396 | 397 | Examples 398 | -------- 399 | >>> import dynetx as dn 400 | >>> G = dn.DynGraph() 401 | >>> dn.add_star(G, [0,1,2,3], t=0) 402 | """ 403 | nlist = iter(nodes) 404 | v = next(nlist) 405 | edges = ((v, n) for n in nlist) 406 | G.add_interactions_from(edges, t, **attr) 407 | 408 | 409 | def add_path(G, nodes, t, **attr): 410 | """Add a path at time t. 411 | 412 | Parameters 413 | ---------- 414 | G : graph 415 | A DyNetx graph 416 | 417 | nodes : iterable container 418 | A container of nodes. 419 | 420 | t : snapshot id (default=None) 421 | snapshot id 422 | 423 | See Also 424 | -------- 425 | add_path, add_cycle 426 | 427 | Examples 428 | -------- 429 | >>> G = dn.DynGraph() 430 | >>> dn.add_path(G, [0,1,2,3], t=0) 431 | """ 432 | nlist = list(nodes) 433 | edges = zip(nlist[:-1], nlist[1:]) 434 | G.add_interactions_from(edges, t, **attr) 435 | 436 | 437 | def add_cycle(G, nodes, t, **attr): 438 | """Add a cycle at time t. 439 | 440 | Parameters 441 | ---------- 442 | 443 | G : graph 444 | A DyNetx graph 445 | 446 | nodes : iterable container 447 | A container of nodes. 448 | 449 | t : snapshot id (default=None) 450 | snapshot id 451 | 452 | See Also 453 | -------- 454 | add_path, add_cycle 455 | 456 | Examples 457 | -------- 458 | >>> G = dn.DynGraph() 459 | >>> dn.add_cycle(G, [0,1,2,3], t=0) 460 | """ 461 | nlist = list(nodes) 462 | edges = zip(nlist, nlist[1:] + [nlist[0]]) 463 | G.add_interactions_from(edges, t, **attr) 464 | 465 | 466 | def subgraph(G, nbunch): 467 | """Return the subgraph induced on nodes in nbunch. 468 | 469 | Parameters 470 | ---------- 471 | G : graph 472 | A DyNetx graph 473 | 474 | nbunch : list, iterable 475 | A container of nodes that will be iterated through once (thus 476 | it should be an iterator or be iterable). Each element of the 477 | container should be a valid node type: any hashable type except 478 | None. If nbunch is None, return all edges data in the graph. 479 | Nodes in nbunch that are not in the graph will be (quietly) 480 | ignored. 481 | 482 | """ 483 | return G.subgraph(nbunch) 484 | 485 | 486 | def create_empty_copy(G, with_data=True): 487 | """Return a copy of the graph G with all of the edges removed. 488 | Parameters 489 | ---------- 490 | 491 | G : graph 492 | A DyNetx graph 493 | 494 | with_data : bool (default=True) 495 | Include data. 496 | 497 | Notes 498 | ----- 499 | Graph and edge data is not propagated to the new graph. 500 | """ 501 | H = G.__class__() 502 | H.add_nodes_from(G.nodes(data=with_data)) 503 | if with_data: 504 | H.graph.update(G.graph) 505 | return H 506 | 507 | 508 | def set_node_attributes(G, values, name=None): 509 | """Set node attributes from dictionary of nodes and values 510 | 511 | Parameters 512 | ---------- 513 | G : DyNetx Graph 514 | 515 | name : string 516 | Attribute name 517 | 518 | values: dict 519 | Dictionary of attribute values keyed by node. If `values` is not a 520 | dictionary, then it is treated as a single attribute value that is then 521 | applied to every node in `G`. 522 | 523 | """ 524 | # Set node attributes based on type of `values` 525 | if name is not None: # `values` must not be a dict of dict 526 | try: # `values` is a dict 527 | for n, v in values.items(): 528 | try: 529 | G._node[n][name] = values[n] 530 | except KeyError: 531 | pass 532 | except AttributeError: # `values` is a constant 533 | for n in G: 534 | G._node[n][name] = values 535 | else: # `values` must be dict of dict 536 | for n, d in values.items(): 537 | try: 538 | G._node[n].update(d) 539 | except KeyError: 540 | pass 541 | 542 | 543 | def get_node_attributes(G, name): 544 | """Get node attributes from graph 545 | 546 | Parameters 547 | ---------- 548 | G : DyNetx Graph 549 | 550 | name : string 551 | Attribute name 552 | 553 | Returns 554 | ------- 555 | Dictionary of attributes keyed by node. 556 | """ 557 | return {n: d[name] for n, d in G._node.items() if name in d} 558 | 559 | 560 | @not_implemented() 561 | def set_edge_attributes(values, name=None): 562 | pass 563 | 564 | 565 | @not_implemented() 566 | def get_edge_attributes(G, name): 567 | pass 568 | 569 | 570 | def all_neighbors(graph, node, t=None): 571 | """ Returns all of the neighbors of a node in the graph at time t. 572 | 573 | If the graph is directed returns predecessors as well as successors. 574 | 575 | Parameters 576 | ---------- 577 | 578 | graph : DyNetx graph 579 | Graph to find neighbors. 580 | 581 | node : node 582 | The node whose neighbors will be returned. 583 | 584 | t : snapshot id (default=None) 585 | If None the neighbors are identified on the flattened graph. 586 | 587 | Returns 588 | ------- 589 | 590 | neighbors : iterator 591 | Iterator of neighbors 592 | """ 593 | if graph.is_directed(): 594 | values = chain(graph.predecessors(node, t=t), graph.successors(node, t=t)) 595 | else: 596 | values = graph.neighbors(node, t=t) 597 | return values 598 | 599 | 600 | def non_neighbors(graph, node, t=None): 601 | """Returns the non-neighbors of the node in the graph at time t. 602 | 603 | Parameters 604 | ---------- 605 | graph : DyNetx graph 606 | Graph to find neighbors. 607 | 608 | node : node 609 | The node whose neighbors will be returned. 610 | 611 | t : snapshot id (default=None) 612 | If None the non-neighbors are identified on the flattened graph. 613 | 614 | 615 | Returns 616 | ------- 617 | non_neighbors : iterator 618 | Iterator of nodes in the graph that are not neighbors of the node. 619 | """ 620 | if graph.is_directed(): 621 | values = chain(graph.predecessors(node, t=t), graph.successors(node, t=t)) 622 | else: 623 | values = graph.neighbors(node, t=t) 624 | 625 | nbors = set(values) | {node} 626 | return (nnode for nnode in graph if nnode not in nbors) 627 | 628 | 629 | def non_interactions(graph, t=None): 630 | """Returns the non-existent edges in the graph at time t. 631 | 632 | Parameters 633 | ---------- 634 | 635 | graph : NetworkX graph. 636 | Graph to find non-existent edges. 637 | 638 | t : snapshot id (default=None) 639 | If None the non-existent edges are identified on the flattened graph. 640 | 641 | 642 | Returns 643 | ------- 644 | non_edges : iterator 645 | Iterator of edges that are not in the graph. 646 | """ 647 | # if graph.is_directed(): 648 | # for u in graph: 649 | # for v in non_neighbors(graph, u, t): 650 | # yield (u, v) 651 | #else: 652 | nodes = set(graph) 653 | while nodes: 654 | u = nodes.pop() 655 | for v in nodes - set(graph[u]): 656 | yield u, v 657 | 658 | 659 | def is_empty(G): 660 | """Returns ``True`` if ``G`` has no edges. 661 | 662 | Parameters 663 | ---------- 664 | 665 | G : graph 666 | A DyNetx graph. 667 | 668 | Returns 669 | ------- 670 | 671 | bool 672 | ``True`` if ``G`` has no edges, and ``False`` otherwise. 673 | 674 | Notes 675 | ----- 676 | An empty graph can have nodes but not edges. The empty graph with zero 677 | nodes is known as the null graph. This is an O(n) operation where n is the 678 | number of nodes in the graph. 679 | """ 680 | return not any(G.adj.values()) 681 | 682 | 683 | def time_slice(G, t_from, t_to=None): 684 | """Return an iterator for (node, degree) at time t. 685 | 686 | The node degree is the number of edges adjacent to the node. 687 | 688 | Parameters 689 | ---------- 690 | 691 | G : graph 692 | A DyNetx graph. 693 | 694 | t_from : snapshot id, mandatory 695 | 696 | t_to : snapshot id, optional (default=None) 697 | If None t_to will be set equal to t_from 698 | 699 | Returns 700 | ------- 701 | H : a DynGraph object 702 | the graph described by interactions in [t_from, t_to] 703 | 704 | Examples 705 | -------- 706 | >>> import dynetx as dn 707 | >>> G = dn.DynGraph() 708 | >>> G.add_path([0,1,2,3], t=0) 709 | >>> G.add_path([0,4,5,6], t=1) 710 | >>> G.add_path([7,1,2,3], t=2) 711 | >>> H = dn.time_slice(G, 0) 712 | >>> H.edges() 713 | [(0, 1), (1, 2), (1, 3)] 714 | >>> H = dn.time_slice(G, 0, 1) 715 | >>> H.edges() 716 | [(0, 1), (1, 2), (1, 3), (0, 4), (4, 5), (5, 6)] 717 | """ 718 | return G.time_slice(t_from, t_to) 719 | 720 | 721 | def stream_interactions(G): 722 | """Generate a temporal ordered stream of interactions. 723 | 724 | Parameters 725 | ---------- 726 | 727 | G : graph 728 | A DyNetx graph. 729 | 730 | Returns 731 | ------- 732 | 733 | nd_iter : an iterator 734 | The iterator returns a 4-tuples of (node, node, op, timestamp). 735 | 736 | Examples 737 | -------- 738 | >>> import dynetx as dn 739 | >>> G = dn.DynGraph() 740 | >>> G.add_path([0,1,2,3], t=0) 741 | >>> G.add_path([3,4,5,6], t=1) 742 | >>> list(dn.stream_interactions(G)) 743 | [(0, 1, '+', 0), (1, 2, '+', 0), (2, 3, '+', 0), (3, 4, '+', 1), (4, 5, '+', 1), (5, 6, '+', 1)] 744 | """ 745 | return G.stream_interactions() 746 | 747 | 748 | def temporal_snapshots_ids(G): 749 | """Return the ordered list of snapshot ids present in the dynamic graph. 750 | 751 | Parameters 752 | ---------- 753 | 754 | G : graph 755 | A DyNetx graph. 756 | 757 | Returns 758 | ------- 759 | 760 | nd : list 761 | a list of snapshot ids 762 | 763 | Examples 764 | -------- 765 | >>> import dynetx as dn 766 | >>> G = dn.DynGraph() 767 | >>> G.add_path([0,1,2,3], t=0) 768 | >>> G.add_path([0,4,5,6], t=1) 769 | >>> G.add_path([7,1,2,3], t=2) 770 | >>> dn.temporal_snapshots(G) 771 | [0, 1, 2] 772 | """ 773 | return G.temporal_snapshots_ids() 774 | 775 | 776 | def interactions_per_snapshots(G, t=None): 777 | """Return the number of interactions within snapshot t. 778 | 779 | Parameters 780 | ---------- 781 | 782 | G : graph 783 | A DyNetx graph. 784 | 785 | t : snapshot id (default=None) 786 | If None will be returned total number of interactions across all snapshots 787 | 788 | Returns 789 | ------- 790 | 791 | nd : dictionary, or number 792 | A dictionary with snapshot ids as keys and interaction count as values or 793 | a number if a single snapshot id is specified. 794 | 795 | Examples 796 | -------- 797 | >>> import dynetx as dn 798 | >>> G = dn.DynGraph() 799 | >>> G.add_path([0,1,2,3], t=0) 800 | >>> G.add_path([0,4,5,6], t=1) 801 | >>> G.add_path([7,1,2,3], t=2) 802 | >>> dn.number_of_interactions(G, t=0) 803 | 3 804 | >>> dn.interactions_per_snapshots(G) 805 | {0: 3, 1: 3, 2: 3} 806 | """ 807 | return G.interactions_per_snapshots(t) 808 | 809 | 810 | def inter_event_time_distribution(G, u=None, v=None): 811 | """Return the distribution of inter event time. 812 | If u and v are None the dynamic graph intere event distribution is returned. 813 | If u is specified the inter event time distribution of interactions involving u is returned. 814 | If u and v are specified the inter event time distribution of (u, v) interactions is returned 815 | 816 | Parameters 817 | ---------- 818 | G : graph 819 | A DyNetx graph. 820 | u : node id 821 | v : node id 822 | 823 | Returns 824 | ------- 825 | 826 | nd : dictionary 827 | A dictionary from inter event time to number of occurrences 828 | """ 829 | return G.inter_event_time_distribution(u, v) 830 | --------------------------------------------------------------------------------