├── .bettercodehub.yml ├── .circleci └── config.yml ├── .coveragerc ├── .flake8 ├── .gitignore ├── .readthedocs.yml ├── LICENSE.txt ├── Makefile ├── README.md ├── _config.yml ├── docs ├── Makefile ├── README.md ├── conf.py ├── index.rst ├── license.rst ├── make.bat ├── reference │ ├── classes │ │ ├── HavelHakimi.rst │ │ └── index.rst │ ├── functions │ │ ├── degree.rst │ │ ├── distance.rst │ │ ├── graph_operations.rst │ │ ├── index.rst │ │ └── neighborhoods.rst │ ├── index.rst │ └── invariants │ │ ├── chromatic.rst │ │ ├── clique.rst │ │ ├── disparity.rst │ │ ├── distance_measures.rst │ │ ├── domination.rst │ │ ├── dsi.rst │ │ ├── independence.rst │ │ ├── index.rst │ │ ├── matching.rst │ │ ├── power_domination.rst │ │ ├── residue.rst │ │ ├── topological_indices.rst │ │ └── zero_forcing.rst ├── requirements.txt └── tutorial.rst ├── grinpy ├── __init__.py ├── classes │ ├── __init__.py │ ├── functions.py │ └── havel_hakimi.py ├── functions │ ├── __init__.py │ ├── degree.py │ ├── distance.py │ ├── graph_operations.py │ ├── neighborhoods.py │ └── structural_properties.py ├── invariants │ ├── __init__.py │ ├── chromatic.py │ ├── clique.py │ ├── disparity.py │ ├── distance_measures.py │ ├── domination.py │ ├── dsi.py │ ├── independence.py │ ├── matching.py │ ├── power_domination.py │ ├── residue.py │ ├── topological_indices.py │ ├── vertex_cover.py │ └── zero_forcing.py └── utils │ ├── __init__.py │ ├── combinations.py │ └── sequences.py ├── pyproject.toml ├── requirements.txt ├── requirements_dev.txt ├── setup.cfg ├── setup.py └── tests ├── README.md ├── __init__.py ├── test_chromatic.py ├── test_clique.py ├── test_degree.py ├── test_disparity.py ├── test_distance_measures.py ├── test_domination.py ├── test_dsi.py ├── test_functions.py ├── test_graph_operations.py ├── test_havel_hakimi.py ├── test_independence.py ├── test_matching.py ├── test_neighborhoods.py ├── test_power_domination.py ├── test_residue.py ├── test_structural_properties.py ├── test_topological_indices.py ├── test_vertex_cover.py └── test_zero_forcing.py /.bettercodehub.yml: -------------------------------------------------------------------------------- 1 | component_depth: 2 2 | languages: 3 | - python 4 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Python CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-python/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | lint: 8 | docker: 9 | - image: circleci/python:3 10 | 11 | working_directory: ~/repo 12 | 13 | steps: 14 | - checkout 15 | 16 | # Download and cache dependencies 17 | - restore_cache: 18 | keys: 19 | - v1-dependencies-{{ checksum "requirements.txt" }}-{{ checksum "requirements_dev.txt" }}-{{ checksum "docs/requirements_dev.txt" }} 20 | # fallback to using the latest cache if no exact match is found 21 | - v1-dependencies- 22 | 23 | - run: 24 | name: install dependencies 25 | command: | 26 | python3 -m venv venv 27 | . venv/bin/activate 28 | pip install -r requirements.txt -r requirements_dev.txt -r docs/requirements.txt 29 | 30 | - save_cache: 31 | paths: 32 | - ./venv 33 | key: v1-dependencies-{{ checksum "requirements.txt" }}-{{ checksum "requirements_dev.txt" }}-{{ checksum "docs/requirements_dev.txt" }} 34 | 35 | - run: 36 | name: lint code 37 | command: | 38 | . venv/bin/activate 39 | make lint 40 | 41 | - run: 42 | name: check code format 43 | command: | 44 | . venv/bin/activate 45 | make check-formatting 46 | 47 | - run: 48 | name: lint docs 49 | command: | 50 | . venv/bin/activate 51 | doc8 docs/ 52 | build: 53 | docker: 54 | - image: circleci/python:3 55 | 56 | working_directory: ~/repo 57 | 58 | steps: 59 | - checkout 60 | 61 | # Download and cache dependencies 62 | - restore_cache: 63 | keys: 64 | - v1-dependencies-{{ checksum "requirements.txt" }}-{{ checksum "requirements_dev.txt" }}-{{ checksum "docs/requirements.txt" }} 65 | # fallback to using the latest cache if no exact match is found 66 | - v1-dependencies- 67 | 68 | - run: 69 | name: install dependencies 70 | command: | 71 | python3 -m venv venv 72 | . venv/bin/activate 73 | pip install -r requirements.txt -r requirements_dev.txt -r docs/requirements.txt 74 | 75 | - save_cache: 76 | paths: 77 | - ./venv 78 | key: v1-dependencies-{{ checksum "requirements.txt" }}-{{ checksum "requirements_dev.txt" }}-{{ checksum "docs/requirements.txt" }} 79 | 80 | - run: 81 | name: run tests 82 | command: | 83 | . venv/bin/activate 84 | make test 85 | 86 | - store_artifacts: 87 | path: test-reports 88 | destination: test-reports 89 | 90 | - run: 91 | name: upload coverage report 92 | command: | 93 | . venv/bin/activate 94 | codecov -t=$CODECOV_TOKEN 95 | 96 | - run: 97 | name: build html docs 98 | command: | 99 | . venv/bin/activate 100 | cd docs 101 | make html 102 | 103 | - store_artifacts: 104 | path: docs/_build 105 | destination: docs 106 | deploy: 107 | docker: 108 | - image: circleci/python:3 109 | 110 | working_directory: ~/repo 111 | 112 | steps: 113 | - checkout 114 | 115 | # Download and cache dependencies 116 | - restore_cache: 117 | keys: 118 | - v1-dependencies-{{ checksum "requirements.txt" }}-{{ checksum "requirements_dev.txt" }}-{{ checksum "docs/requirements.txt" }} 119 | # fallback to using the latest cache if no exact match is found 120 | - v1-dependencies- 121 | 122 | - run: 123 | name: install dependencies 124 | command: | 125 | python3 -m venv venv 126 | . venv/bin/activate 127 | pip install -r requirements.txt -r requirements_dev.txt -r docs/requirements.txt 128 | 129 | - save_cache: 130 | paths: 131 | - ./venv 132 | key: v1-dependencies-{{ checksum "requirements.txt" }}-{{ checksum "requirements_dev.txt" }}-{{ checksum "docs/requirements.txt" }} 133 | 134 | - run: 135 | name: verify git tag vs. version 136 | command: | 137 | python3 -m venv venv 138 | . venv/bin/activate 139 | python setup.py verify 140 | 141 | - run: 142 | name: init .pypirc 143 | command: | 144 | echo -e "[pypi]" >> ~/.pypirc 145 | echo -e "username = somacdivad" >> ~/.pypirc 146 | echo -e "password = $PYPI_PASSWORD" >> ~/.pypirc 147 | 148 | - run: 149 | name: create packages 150 | command: | 151 | make package 152 | 153 | - run: 154 | name: upload to pypi 155 | command: | 156 | . venv/bin/activate 157 | twine upload dist/* 158 | 159 | workflows: 160 | version: 2 161 | build: 162 | jobs: 163 | - lint: 164 | filters: 165 | tags: 166 | only: /.*/ 167 | - build: 168 | requires: 169 | - lint 170 | filters: 171 | tags: 172 | only: /.*/ 173 | - deploy: 174 | requires: 175 | - build 176 | filters: 177 | tags: 178 | only: /[0-9]+(\.[0-9]+)((a|b)[0-9]+)*$/ 179 | branches: 180 | ignore: /.*/ 181 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | tests/ 4 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | D203, 4 | E501 5 | exclude = 6 | .git, 7 | __pycache__, 8 | docs/source/conf.py, 9 | old, 10 | build, 11 | dist 12 | max-complexity = 10 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # celery beat schedule file 94 | celerybeat-schedule 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Environments 100 | .env 101 | .venv 102 | env/ 103 | venv/ 104 | ENV/ 105 | env.bak/ 106 | venv.bak/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | .dmypy.json 121 | dmypy.json 122 | 123 | # Pyre type checker 124 | .pyre/ 125 | 126 | .vscode 127 | 128 | docs/reference/classes/generated/ 129 | docs/reference/functions/generated/ 130 | docs/reference/invariants/generated/ 131 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | version: 2 6 | 7 | sphinx: 8 | configuration: docs/conf.py 9 | 10 | formats: all 11 | 12 | python: 13 | version: 3.7 14 | install: 15 | - requirements: docs/requirements.txt 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | GrinPy is distributed with the 3-clause BSD license. As an extension of the 5 | NetworkX package, we list the pertinent copyright information as requested 6 | by the NetworkX authors. 7 | 8 | :: 9 | 10 | GrinPy 11 | ------ 12 | Copyright (C) 2017-2019, GrinPy Developers 13 | David Amos 14 | Randy Davila 15 | 16 | NetworkX 17 | -------- 18 | Copyright (C) 2004-2019, NetworkX Developers 19 | Aric Hagberg 20 | Dan Schult 21 | Pieter Swart 22 | All rights reserved. 23 | 24 | Redistribution and use in source and binary forms, with or without 25 | modification, are permitted provided that the following conditions are 26 | met: 27 | 28 | * Redistributions of source code must retain the above copyright 29 | notice, this list of conditions and the following disclaimer. 30 | 31 | * Redistributions in binary form must reproduce the above 32 | copyright notice, this list of conditions and the following 33 | disclaimer in the documentation and/or other materials provided 34 | with the distribution. 35 | 36 | * Neither the name of the NetworkX Developers nor the names of its 37 | contributors may be used to endorse or promote products derived 38 | from this software without specific prior written permission. 39 | 40 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 41 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 42 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 43 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 44 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 45 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 46 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 47 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 48 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 49 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 50 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | lint: 2 | flake8 grinpy/ 3 | 4 | check-formatting: 5 | black --check grinpy/ 6 | 7 | format: 8 | black grinpy/ 9 | 10 | test: 11 | pytest -vv --cov=grinpy/ . 12 | 13 | package: 14 | python setup.py sdist 15 | python setup.py bdist_wheel 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/somacdivad/grinpy.svg?style=svg)](https://circleci.com/gh/somacdivad/grinpy) [![Documentation Status](https://readthedocs.org/projects/grinpy/badge/)](http://grinpy.readthedocs.io/en/latest/) [![BCH compliance](https://bettercodehub.com/edge/badge/somacdivad/grinpy)](https://bettercodehub.com/) [![codecov](https://codecov.io/gh/somacdivad/grinpy/branch/master/graph/badge.svg)](https://codecov.io/gh/somacdivad/grinpy) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) [![PyPI version](https://badge.fury.io/py/grinpy.svg)](https://badge.fury.io/py/grinpy) 2 | 3 | # GrinPy 4 | *A NetworkX extension for calculating graph invariants.* 5 | 6 | ### What is it? 7 | GrinPy is an extension for NetworkX used for calculating graph invariants of 8 | simple graphs. 9 | 10 | NP-hard invariants included are: 11 | 12 | * Chromatic number 13 | * Clique number 14 | * Independence number 15 | * Domination number 16 | * Total domination number 17 | * Connected domination number 18 | * Independent domination number 19 | * Power domination number 20 | * Zero forcing number 21 | * Total zero forcing number 22 | * Connected zero forcing number 23 | * Minimum maximal matching number 24 | * Generalized *k* versions of almost all of the above invariants 25 | 26 | Other invariants included are: 27 | 28 | * Annihilation number 29 | * Matching number 30 | * Residue 31 | * Slater number 32 | * Sub-*k*-domination number 33 | * Topological indices, like the Randić and Zagreb indices 34 | 35 | In addition to the graph invariants listed above, we have included some 36 | simple checks for structural properties of a graph: 37 | 38 | * `is_triangle_free` 39 | * `is_bull_free` 40 | * `is_claw_free` 41 | 42 | ### How do I use it? 43 | Full documentation is available at [https://grinpy.rtfd.io](https://grinpy.rtfd.io). 44 | 45 | You can install GrinPy from the command line with `pip`: 46 | 47 | ``` 48 | pip install grinpy 49 | ``` 50 | 51 | Here is a sample of how to calculate the independence number: 52 | ```python 53 | >>> import grinpy as gp 54 | >>> G = gp.petersen_graph() 55 | >>> gp.independence_number(G) 56 | 4 57 | ``` 58 | 59 | GrinPy automatically imports [NetworkX](https://github.com/networkx/networkx) and provides all of the NetworkX classes and methods in the same interface. 60 | 61 | ### Why does it exist? 62 | The motivation for this project is to filter a database of graphs into an 63 | ordered tree of subsets. This database will be used in an experimental automated 64 | conjecturing program. In creating the required packages for this database, we 65 | realized that a Python package for calculating graph invariants would be 66 | useful for professional research and for graph theory education. 67 | 68 | ### License 69 | Released under the 3-Clause BSD license (see `LICENSE.txt`): 70 | 71 | Copyright (C) 2017-2019 GrinPy Developers 72 | David Amos 73 | Randy Davila 74 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = GrinPy 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # How to build the docs 2 | 3 | GrinPy uses Sphinx for generating the API documentation. Pre-built docs can 4 | be found at [https://grinpy.rtfd.io](https://grinpy.rtfd.io). 5 | 6 | ## Instructions 7 | To install the Python packages required to build the documentation, enter 8 | 9 | ``` 10 | pip install -r requirements.txt 11 | ``` 12 | 13 | in the `docs/` directory. 14 | 15 | Then enter 16 | 17 | ``` 18 | make html 19 | ``` 20 | 21 | to build the HTML documentation. The HTML files can be found in the 22 | `_build/html` subdirectory. 23 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # GrinPy documentation build configuration file, created by 5 | # sphinx-quickstart on Sun Dec 3 20:56:57 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 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | import os 24 | import sys 25 | from datetime import date 26 | 27 | sys.path.insert(0, os.path.abspath("..")) 28 | 29 | # Environment variable to know if the docs are being built on rtd. 30 | on_rtd = os.environ.get("READTHEDOCS", None) == "True" 31 | 32 | 33 | # -- General configuration ------------------------------------------------ 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | "sphinx.ext.autosummary", 44 | "sphinx.ext.autodoc", 45 | "sphinx.ext.coverage", 46 | "sphinx.ext.doctest", 47 | "sphinx.ext.mathjax", 48 | "sphinx.ext.napoleon", 49 | "sphinx.ext.githubpages", 50 | "nb2plots", 51 | ] 52 | 53 | # generate autosummary pages 54 | autosummary_generate = True 55 | 56 | # Add any paths that contain templates here, relative to this directory. 57 | templates_path = ["_templates"] 58 | 59 | # The suffix(es) of source filenames. 60 | # You can specify multiple suffix as a list of string: 61 | # 62 | # source_suffix = ['.rst', '.md'] 63 | source_suffix = ".rst" 64 | 65 | # The master toctree document. 66 | master_doc = "index" 67 | 68 | # General information about the project. 69 | project = "GrinPy" 70 | copyright = "2017-{}, David Amos, Randy Davila".format(date.today().year) 71 | author = "David Amos, Randy Davila" 72 | 73 | # The version info for the project you're documenting, acts as replacement for 74 | # |version| and |release|, also used in various other places throughout the 75 | # built documents. 76 | # 77 | # The short X.Y version. 78 | version = "latest" 79 | # The full version, including alpha/beta/rc tags. 80 | release = "latest" 81 | 82 | # The language for content autogenerated by Sphinx. Refer to documentation 83 | # for a list of supported languages. 84 | # 85 | # This is also used if you do content translation via gettext catalogs. 86 | # Usually you set "language" from the command line for these cases. 87 | language = None 88 | 89 | # List of patterns, relative to source directory, that match files and 90 | # directories to ignore when looking for source files. 91 | # This patterns also effect to html_static_path and html_extra_path 92 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 93 | 94 | # The name of the Pygments (syntax highlighting) style to use. 95 | pygments_style = "sphinx" 96 | 97 | # If true, `todo` and `todoList` produce output, else they produce nothing. 98 | todo_include_todos = False 99 | 100 | 101 | # -- Options for HTML output ---------------------------------------------- 102 | 103 | # The theme to use for HTML and HTML Help pages. See the documentation for 104 | # a list of builtin themes. 105 | # 106 | if not on_rtd: 107 | import sphinx_rtd_theme 108 | 109 | html_theme = "sphinx_rtd_theme" 110 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 111 | 112 | # Theme options are theme-specific and customize the look and feel of a theme 113 | # further. For a list of options available for each theme, see the 114 | # documentation. 115 | # 116 | # html_theme_options = {} 117 | 118 | # Add any paths that contain custom static files (such as style sheets) here, 119 | # relative to this directory. They are copied after the builtin static files, 120 | # so a file named "default.css" will overwrite the builtin "default.css". 121 | html_static_path = [] 122 | 123 | # Custom sidebar templates, must be a dictionary that maps document names 124 | # to template names. 125 | # 126 | # This is required for the alabaster theme 127 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 128 | html_sidebars = { 129 | "**": [ 130 | "about.html", 131 | "navigation.html", 132 | "relations.html", # needs 'show_related': True theme option to display 133 | "searchbox.html", 134 | "donate.html", 135 | ] 136 | } 137 | 138 | 139 | # -- Options for HTMLHelp output ------------------------------------------ 140 | 141 | # Output file base name for HTML help builder. 142 | htmlhelp_basename = "GrinPydoc" 143 | 144 | 145 | # -- Options for LaTeX output --------------------------------------------- 146 | 147 | latex_elements = { 148 | # The paper size ('letterpaper' or 'a4paper'). 149 | # 150 | # 'papersize': 'letterpaper', 151 | # The font size ('10pt', '11pt' or '12pt'). 152 | # 153 | # 'pointsize': '10pt', 154 | # Additional stuff for the LaTeX preamble. 155 | # 156 | # 'preamble': '', 157 | # Latex figure (float) alignment 158 | # 159 | # 'figure_align': 'htbp', 160 | } 161 | 162 | # Grouping the document tree into LaTeX files. List of tuples 163 | # (source start file, target name, title, 164 | # author, documentclass [howto, manual, or own class]). 165 | latex_documents = [(master_doc, "GrinPy.tex", "GrinPy Documentation", "David Amos, Randy Davila", "manual")] 166 | 167 | 168 | # -- Options for manual page output --------------------------------------- 169 | 170 | # One entry per manual page. List of tuples 171 | # (source start file, name, description, authors, manual section). 172 | man_pages = [(master_doc, "grinpy", "GrinPy Documentation", [author], 1)] 173 | 174 | 175 | # -- Options for Texinfo output ------------------------------------------- 176 | 177 | # Grouping the document tree into Texinfo files. List of tuples 178 | # (source start file, target name, title, author, 179 | # dir menu entry, description, category) 180 | texinfo_documents = [ 181 | ( 182 | master_doc, 183 | "GrinPy", 184 | "GrinPy Documentation", 185 | author, 186 | "GrinPy", 187 | "One line description of project.", 188 | "Miscellaneous", 189 | ) 190 | ] 191 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. GrinPy documentation master file, created by 2 | sphinx-quickstart on Sun Dec 3 20:56:57 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to GrinPy 7 | ================= 8 | 9 | GrinPy is a NetworkX extension for calculating graph invariants. This extension 10 | imports all of NetworkX into the same interface as GrinPy for easy of use and 11 | provides the following extensions: 12 | 13 | - extended functional interface for graph properties 14 | - calculation of NP-hard invariants such as: independence number, domination 15 | number and zero forcing number 16 | - calculation of several invariants that are known to be related to the 17 | NP-hard invariants, such as the residue, the annihilation number and the 18 | sub-domination number 19 | 20 | Our goal is to provide the most comprehensive list of invariants. We will be 21 | continuing to add to this list as time goes on, and we invite others to join 22 | us by contributing their own implementations of algorithms for computing new 23 | or existing GrinPy invariants. 24 | 25 | Audience 26 | -------- 27 | We envision GrinPy's primary audience to be professional mathematicians and 28 | students of mathematics. Computer scientists, electrical engineers, physicists, 29 | biologists, chemists and social scientists may also find GrinPy's extensions 30 | to the standard NetworkX package useful. 31 | 32 | History 33 | ------- 34 | Grinpy was originally created to aid the developers, David Amos and 35 | Randy Davila, in creating an ordered tree of graph databases for use in an 36 | experimental automated conjecturing program. It quickly became clear that 37 | a Python package for calculating graph invariants would be useful. GrinPy was 38 | created in November 2017 and is still in its infancy. We look forward to what 39 | the future brings! 40 | 41 | Free Software 42 | ------------- 43 | GrinPy is free software; you can redistribute it and/or modify it under the 44 | terms of the :doc:`3-clause BSD license `, the same license that 45 | NetworkX is released under. We greatly appreciate contributions. Please join us 46 | on `Github `_. 47 | 48 | Documentation 49 | ------------- 50 | 51 | .. only:: html 52 | 53 | :Release: |version| 54 | :Date: |today| 55 | 56 | .. toctree:: 57 | :maxdepth: 2 58 | 59 | tutorial 60 | reference/index 61 | license 62 | 63 | 64 | 65 | Indices and tables 66 | ================== 67 | 68 | * :ref:`genindex` 69 | * :ref:`modindex` 70 | * :ref:`search` 71 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../LICENSE.txt 2 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=GrinPy 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/reference/classes/HavelHakimi.rst: -------------------------------------------------------------------------------- 1 | *********** 2 | HavelHakimi 3 | *********** 4 | 5 | Overview 6 | ======== 7 | .. currentmodule:: grinpy 8 | .. autoclass:: HavelHakimi 9 | 10 | Methods 11 | ======= 12 | .. autosummary:: 13 | :toctree: generated/ 14 | 15 | HavelHakimi.__init__ 16 | HavelHakimi.depth 17 | HavelHakimi.get_elimination_sequence 18 | HavelHakimi.get_initial_sequence 19 | HavelHakimi.is_graphic 20 | HavelHakimi.get_process 21 | HavelHakimi.residue 22 | -------------------------------------------------------------------------------- /docs/reference/classes/index.rst: -------------------------------------------------------------------------------- 1 | .. _classes: 2 | 3 | Classes 4 | ********* 5 | 6 | :Release: |release| 7 | :Date: |today| 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | HavelHakimi 13 | -------------------------------------------------------------------------------- /docs/reference/functions/degree.rst: -------------------------------------------------------------------------------- 1 | ****** 2 | Degree 3 | ****** 4 | 5 | .. automodule:: grinpy.functions.degree 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | degree_sequence 11 | min_degree 12 | max_degree 13 | average_degree 14 | number_of_nodes_of_degree_k 15 | number_of_degree_one_nodes 16 | number_of_min_degree_nodes 17 | number_of_max_degree_nodes 18 | neighborhood_degree_list 19 | closed_neighborhood_degree_list 20 | -------------------------------------------------------------------------------- /docs/reference/functions/distance.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | Distance 3 | ******** 4 | 5 | .. automodule:: grinpy.functions.distance 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | distance 11 | -------------------------------------------------------------------------------- /docs/reference/functions/graph_operations.rst: -------------------------------------------------------------------------------- 1 | **************** 2 | Graph Operations 3 | **************** 4 | 5 | .. automodule:: grinpy.functions.graph_operations 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | contract_nodes 11 | -------------------------------------------------------------------------------- /docs/reference/functions/index.rst: -------------------------------------------------------------------------------- 1 | .. _functions: 2 | 3 | Functions 4 | ********* 5 | 6 | :Release: |release| 7 | :Date: |today| 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | degree 13 | distance 14 | graph_operations 15 | neighborhoods 16 | -------------------------------------------------------------------------------- /docs/reference/functions/neighborhoods.rst: -------------------------------------------------------------------------------- 1 | ************* 2 | Neighborhoods 3 | ************* 4 | 5 | .. automodule:: grinpy.functions.neighborhoods 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | are_neighbors 11 | closed_neighborhood 12 | common_neighbors 13 | neighborhood 14 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | .. _reference: 2 | 3 | Reference 4 | ********* 5 | 6 | :Release: |release| 7 | :Date: |today| 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | classes/index 13 | functions/index 14 | invariants/index 15 | -------------------------------------------------------------------------------- /docs/reference/invariants/chromatic.rst: -------------------------------------------------------------------------------- 1 | **************** 2 | Chromatic Number 3 | **************** 4 | 5 | .. automodule:: grinpy.invariants.chromatic 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | chromatic_number 11 | -------------------------------------------------------------------------------- /docs/reference/invariants/clique.rst: -------------------------------------------------------------------------------- 1 | ************* 2 | Clique Number 3 | ************* 4 | 5 | .. automodule:: grinpy.invariants.clique 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | clique_number 11 | -------------------------------------------------------------------------------- /docs/reference/invariants/disparity.rst: -------------------------------------------------------------------------------- 1 | ********* 2 | Disparity 3 | ********* 4 | 5 | .. automodule:: grinpy.invariants.disparity 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | vertex_disparity 11 | closed_vertex_disparity 12 | disparity_sequence 13 | closed_disparity_sequence 14 | CW_disparity 15 | closed_CW_disparity 16 | inverse_disparity 17 | closed_inverse_disparity 18 | average_vertex_disparity 19 | average_closed_vertex_disparity 20 | k_disparity 21 | closed_k_disparity 22 | irregularity 23 | -------------------------------------------------------------------------------- /docs/reference/invariants/distance_measures.rst: -------------------------------------------------------------------------------- 1 | ***************** 2 | Distance Measures 3 | ***************** 4 | 5 | .. automodule:: grinpy.invariants.distance_measures 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | triameter 11 | -------------------------------------------------------------------------------- /docs/reference/invariants/domination.rst: -------------------------------------------------------------------------------- 1 | ********** 2 | Domination 3 | ********** 4 | 5 | .. automodule:: grinpy.invariants.domination 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | is_k_dominating_set 11 | is_total_dominating_set 12 | min_k_dominating_set 13 | min_dominating_set 14 | min_total_dominating_set 15 | domination_number 16 | k_domination_number 17 | total_domination_number 18 | -------------------------------------------------------------------------------- /docs/reference/invariants/dsi.rst: -------------------------------------------------------------------------------- 1 | *** 2 | DSI 3 | *** 4 | 5 | .. automodule:: grinpy.invariants.dsi 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | sub_k_domination_number 11 | slater 12 | sub_total_domination_number 13 | annihilation_number 14 | -------------------------------------------------------------------------------- /docs/reference/invariants/independence.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Independence 3 | ************ 4 | 5 | .. automodule:: grinpy.invariants.independence 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | is_independent_set 11 | is_k_independent_set 12 | max_k_independent_set 13 | max_independent_set 14 | independence_number 15 | k_independence_number 16 | -------------------------------------------------------------------------------- /docs/reference/invariants/index.rst: -------------------------------------------------------------------------------- 1 | .. _invariants: 2 | 3 | Invariants 4 | ********** 5 | 6 | :Release: |release| 7 | :Date: |today| 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | chromatic 13 | clique 14 | disparity 15 | distance_measures 16 | domination 17 | dsi 18 | independence 19 | matching 20 | power_domination 21 | residue 22 | topological_indices 23 | zero_forcing 24 | -------------------------------------------------------------------------------- /docs/reference/invariants/matching.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | Matching 3 | ******** 4 | 5 | .. automodule:: grinpy.invariants.matching 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | max_matching 11 | matching_number 12 | min_maximal_matching 13 | min_maximal_matching_number 14 | -------------------------------------------------------------------------------- /docs/reference/invariants/power_domination.rst: -------------------------------------------------------------------------------- 1 | **************** 2 | Power Domination 3 | **************** 4 | 5 | .. automodule:: grinpy.invariants.power_domination 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | is_power_dominating_set 11 | min_power_dominating_set 12 | power_domination_number 13 | -------------------------------------------------------------------------------- /docs/reference/invariants/residue.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Residue 3 | ******* 4 | 5 | .. automodule:: grinpy.invariants.residue 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | residue 11 | k_residue 12 | -------------------------------------------------------------------------------- /docs/reference/invariants/topological_indices.rst: -------------------------------------------------------------------------------- 1 | ******************* 2 | Topological Indices 3 | ******************* 4 | 5 | .. automodule:: grinpy.invariants.topological_indices 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | randic_index 11 | generalized_randic_index 12 | augmented_randic_index 13 | harmonic_index 14 | atom_bond_connectivity_index 15 | sum_connectivity_index 16 | first_zagreb_index 17 | second_zagreb_index 18 | -------------------------------------------------------------------------------- /docs/reference/invariants/zero_forcing.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Zero Forcing 3 | ************ 4 | 5 | .. automodule:: grinpy.invariants.zero_forcing 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | is_k_forcing_vertex 11 | is_k_forcing_active_set 12 | is_k_forcing_set 13 | min_k_forcing_set 14 | k_forcing_number 15 | is_zero_forcing_vertex 16 | is_zero_forcing_active_set 17 | is_zero_forcing_set 18 | min_zero_forcing_set 19 | zero_forcing_number 20 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | doc8==0.8.0 2 | nb2plots==0.6 3 | sphinx==2.0.1 4 | sphinx_rtd_theme==0.4.3 5 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | .. -*- coding: utf-8 -*- 2 | 3 | Tutorial 4 | ======== 5 | 6 | .. currentmodule:: grinpy 7 | 8 | This guide can help you start working with GrinPy. We assume basic knowledge 9 | of NetworkX. For more information on how to use NetworkX, see the `NetworkX 10 | Documentation `_. 11 | 12 | Calculating the Independence Number 13 | ----------------------------------- 14 | 15 | For this example we will create a cycle of order 5. 16 | 17 | .. nbplot:: 18 | 19 | >>> import grinpy as gp 20 | >>> G = gp.cycle_graph(5) 21 | 22 | In order to compute the independence number of the cycle, we simply call the 23 | ``independence_number()`` function on the graph: 24 | 25 | .. nbplot:: 26 | 27 | >>> gp.independence_number(G) 28 | 2 29 | 30 | It's that simple! 31 | 32 | .. note:: In this release (version |version|), all methods are defined only for simple graphs. In future releases, we will expand to digraphs and multigraphs. 33 | 34 | Get a Maximum Independent Set 35 | ----------------------------- 36 | 37 | If we are interested in finding a maximum independent set in the graph: 38 | 39 | .. nbplot:: 40 | 41 | >>> gp.max_independent_set(G) 42 | [0, 2] 43 | 44 | Determine if a Given Set is Independent 45 | --------------------------------------- 46 | 47 | We may check whether or not a given set is independent: 48 | 49 | .. nbplot:: 50 | 51 | >>> gp.is_independent_set(G, [0, 1]) 52 | False 53 | >>> gp.is_independent_set(G, [1, 3]) 54 | True 55 | 56 | General Notes 57 | ------------- 58 | 59 | The vast majority of NP-hard invariants will include three methods 60 | corresponding to the above examples. That is, for each invariant, there will 61 | be three methods: 62 | 63 | - Calculate the invariant 64 | - Get a set of nodes realizing the invariant 65 | - Determine whether or not a given set of nodes meets some necessary 66 | condition for the invariant. 67 | -------------------------------------------------------------------------------- /grinpy/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | 11 | """Top-level package for GrinPy.""" 12 | 13 | from __future__ import absolute_import 14 | 15 | import sys 16 | 17 | if sys.version_info[:2] < (3, 5): 18 | m = "Python 3.4 or later is required for GrinPy (%d.%d detected)." 19 | raise ImportError(m % sys.version_info[:2]) 20 | del sys 21 | 22 | __author__ = """David Amos, Randy Davila""" 23 | __email__ = "somacdivad@gmail.com, davilar@uhd.edu" 24 | __version__ = "19.5a0" 25 | 26 | # import NetworkX dependency 27 | import networkx # noqa 28 | from networkx import * # noqa 29 | 30 | import grinpy.classes # noqa 31 | from grinpy.classes import * # noqa 32 | 33 | import grinpy.functions # noqa 34 | from grinpy.functions import * # noqa 35 | 36 | import grinpy.invariants # noqa 37 | from grinpy.invariants import * # noqa 38 | -------------------------------------------------------------------------------- /grinpy/classes/__init__.py: -------------------------------------------------------------------------------- 1 | from grinpy.classes.havel_hakimi import * # noqa 2 | from grinpy.classes.functions import * # noqa 3 | -------------------------------------------------------------------------------- /grinpy/classes/functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functional interface for class methods.""" 11 | 12 | import grinpy as gp 13 | from grinpy.functions.degree import degree_sequence 14 | 15 | __all__ = ["is_graphic", "havel_hakimi_process", "elimination_sequence"] 16 | 17 | 18 | def is_graphic(sequence): 19 | """Return whether or not the input sequence is graphic. 20 | 21 | A sequence of positive integers is said to be a *graphic sequence* if there 22 | exist a graph whose degree sequence is equal to the input sequence, up to 23 | ordering. 24 | 25 | Parameters 26 | ---------- 27 | sequence : list, iterable 28 | A list or other iterable container of integers. 29 | 30 | Returns 31 | ------- 32 | bool 33 | True if the input sequence is graphic. False otherwise. 34 | """ 35 | hh = gp.HavelHakimi(sequence) 36 | return hh.is_graphic() 37 | 38 | 39 | def havel_hakimi_process(G): 40 | """Return an instance of the HavelHakimi class initialized with the degree 41 | sequence of the graph. 42 | 43 | Parameters 44 | ---------- 45 | G : NetworkX graph 46 | An undirected graph. 47 | 48 | Returns 49 | ------- 50 | object 51 | An instance of the HavelHakimi class initialized with the degree 52 | sequence of the graph. 53 | 54 | See Also 55 | -------- 56 | HavelHakimi 57 | """ 58 | return gp.HavelHakimi(degree_sequence(G)) 59 | 60 | 61 | def elimination_sequence(G): 62 | """Return the elimination sequence of the graph. 63 | 64 | The *elimination sequence* of a graph is the elimination sequence generated 65 | by the Havel Hakimi process performed on the degree sequence of the graph. 66 | 67 | Parameters 68 | ---------- 69 | G : NetworkX graph 70 | An undirected graph. 71 | 72 | Returns 73 | ------- 74 | list 75 | The elimination sequence of the graph. 76 | """ 77 | return havel_hakimi_process(G).get_elimination_sequence() 78 | -------------------------------------------------------------------------------- /grinpy/classes/havel_hakimi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """HavelHakimi class from performing and keeping track of the Havel Hakimi 11 | Process on a sequence.""" 12 | 13 | from collections.abc import Iterable 14 | 15 | from grinpy.utils import contains_only_zeros 16 | 17 | 18 | class HavelHakimi: 19 | """Class for performing and keeping track of the Havel Hakimi process on a 20 | sequence of positive integers. 21 | 22 | Parameters 23 | ---------- 24 | sequence : input sequence 25 | The sequence of integers to initialize the Havel Hakimi process. 26 | """ 27 | 28 | def __init__(self, sequence): 29 | if not isinstance(sequence, Iterable): 30 | raise TypeError("`sequence` must iterable") 31 | 32 | for x in sequence: 33 | if not float(x).is_integer(): 34 | raise TypeError("Sequence must contain only integers.") 35 | # make sure all entries in the sequence are of type int and sort in non-increasing order 36 | S = [int(x) for x in sequence] 37 | S.sort(reverse=True) 38 | process_sequence = [list(S)] # keeps track of resulting sequences at each step 39 | elim_sequence = [] # keeps track of the elements eliminated at each step 40 | while S[0] > 0 and S[0] < len(S): 41 | # so long as the largest element d of the sequence is positive, remove d from the sequence and subtract 1 from the next d elements 42 | d = S.pop(0) 43 | for i in range(d): 44 | S[i] = S[i] - 1 45 | S.sort(reverse=True) 46 | # append the resulting sequence to the process sequence 47 | process_sequence.append(list(S)) 48 | # append the removed element to the elimination sequence 49 | elim_sequence.append(d) 50 | # finally, append the 0s in the last step of the Havel Hakimi process to the elimination sequence 51 | if contains_only_zeros(process_sequence[-1]): 52 | elim_sequence.extend(S) 53 | # set class attributes 54 | self.process = process_sequence 55 | self.eliminationSequence = elim_sequence 56 | 57 | def depth(self): 58 | """Return the depth of the Havel Hakimi process. 59 | 60 | Returns 61 | ------- 62 | int 63 | The depth of the Havel Hakimi process. 64 | """ 65 | return len(self.process) - 1 66 | 67 | def get_elimination_sequence(self): 68 | """Return the elimination sequence of the Havel Hakimi process. 69 | 70 | Returns 71 | ------- 72 | list 73 | The elimination sequence of the Havel Hakimi process. 74 | """ 75 | return self.eliminationSequence 76 | 77 | def get_initial_sequence(self): 78 | """Return the initial sequence passed to the Havel Hakimi class for 79 | initialization. 80 | 81 | Returns 82 | ------- 83 | list 84 | The initial sequence passed to the Havel Hakimi class. 85 | """ 86 | return self.process[0] 87 | 88 | def is_graphic(self): 89 | """Return whether or not the initial sequence is graphic. 90 | 91 | Returns 92 | ------- 93 | bool 94 | True if the initial sequence is graphic. False otherwise. 95 | """ 96 | return contains_only_zeros(self.process[-1]) 97 | 98 | def get_process(self): 99 | """Return the list of sequence produced during the Havel Hakimi process. 100 | The first element in the list is the initial sequence. 101 | 102 | Returns 103 | ------- 104 | list 105 | The list of sequences produced by the Havel Hakimi process. 106 | """ 107 | return self.process 108 | 109 | def residue(self): 110 | """Return the residue of the sequence. 111 | 112 | Returns 113 | ------- 114 | int 115 | The residue of the initial sequence. If the sequence is not graphic, 116 | this will be None. 117 | """ 118 | return len(self.process[-1]) if self.is_graphic() else None 119 | -------------------------------------------------------------------------------- /grinpy/functions/__init__.py: -------------------------------------------------------------------------------- 1 | from grinpy.functions.degree import * # noqa 2 | from grinpy.functions.distance import * # noqa 3 | from grinpy.functions.graph_operations import * # noqa 4 | from grinpy.functions.neighborhoods import * # noqa 5 | from grinpy.functions.structural_properties import * # noqa 6 | -------------------------------------------------------------------------------- /grinpy/functions/degree.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Assorted degree related graph utilities. 11 | """ 12 | 13 | import collections 14 | from grinpy import degree, nodes, number_of_nodes 15 | from grinpy.functions.neighborhoods import closed_neighborhood, neighborhood, set_neighborhood, set_closed_neighborhood 16 | 17 | __all__ = [ 18 | "degree_sequence", 19 | "min_degree", 20 | "max_degree", 21 | "average_degree", 22 | "number_of_nodes_of_degree_k", 23 | "number_of_degree_one_nodes", 24 | "number_of_min_degree_nodes", 25 | "number_of_max_degree_nodes", 26 | "neighborhood_degree_list", 27 | "closed_neighborhood_degree_list", 28 | "is_regular", 29 | "is_k_regular", 30 | "is_sub_cubic", 31 | "is_cubic", 32 | ] 33 | 34 | 35 | def degree_sequence(G): 36 | """Return the degree sequence of G. 37 | 38 | The degree sequence of a graph is the sequence of degrees of the nodes 39 | in the graph. 40 | 41 | Parameters 42 | ---------- 43 | G : NetworkX graph 44 | An undirected graph. 45 | 46 | Returns 47 | ------- 48 | list 49 | The degree sequence of the graph. 50 | 51 | Examples 52 | -------- 53 | >>> G = nx.path_graph(3) # Path on 3 nodes 54 | >>> nx.degree_sequence(G) 55 | [1, 2, 1] 56 | """ 57 | return [degree(G, v) for v in nodes(G)] 58 | 59 | 60 | def min_degree(G): 61 | """Return the minimum degree of G. 62 | 63 | The minimum degree of a graph is the smallest degree of any node in the 64 | graph. 65 | 66 | Parameters 67 | ---------- 68 | G : NetworkX graph 69 | An undirected graph. 70 | 71 | Returns 72 | ------- 73 | int 74 | The minimum degree of the graph. 75 | 76 | Examples 77 | -------- 78 | >>> G = nx.path_graph(3) # Path on 3 nodes 79 | >>> nx.min_degree(G) 80 | 1 81 | """ 82 | D = degree_sequence(G) 83 | D.sort() 84 | return D[0] 85 | 86 | 87 | def max_degree(G): 88 | """Return the maximum degree of G. 89 | 90 | The maximum degree of a graph is the largest degree of any node in the 91 | graph. 92 | 93 | Parameters 94 | ---------- 95 | G : NetworkX graph 96 | An undirected graph. 97 | 98 | Returns 99 | ------- 100 | int 101 | The maximum degree of the graph. 102 | 103 | Examples 104 | -------- 105 | >>> G = nx.path_graph(3) # Path on 3 nodes 106 | >>> nx.min_degree(G) 107 | 2 108 | """ 109 | D = degree_sequence(G) 110 | D.sort(reverse=True) 111 | return D[0] 112 | 113 | 114 | def average_degree(G): 115 | """Return the average degree of G. 116 | 117 | The average degree of a graph is the average of the degrees of all nodes 118 | in the graph. 119 | 120 | Parameters 121 | ---------- 122 | G : NetworkX graph 123 | An undirected graph. 124 | 125 | Returns 126 | ------- 127 | float 128 | The average degree of the graph. 129 | 130 | Examples 131 | -------- 132 | >>> G = nx.star_graph(3) # Star on 4 nodes 133 | >>> nx.average_degree(G) 134 | 1.5 135 | """ 136 | return sum(degree_sequence(G)) / number_of_nodes(G) 137 | 138 | 139 | def number_of_nodes_of_degree_k(G, k): 140 | """Return the number of nodes of the graph with degree equal to k. 141 | 142 | Parameters 143 | ---------- 144 | G : NetworkX graph 145 | An undirected graph. 146 | 147 | k : int 148 | A positive integer. 149 | 150 | Returns 151 | ------- 152 | int 153 | The number of nodes in the graph with degree equal to k. 154 | 155 | See Also 156 | -------- 157 | number_of_leaves, number_of_min_degree_nodes, number_of_max_degree_nodes 158 | 159 | Examples 160 | -------- 161 | >>> G = nx.path_graph(3) # Path on 3 nodes 162 | >>> nx.number_of_nodes_of_degree_k(G, 1) 163 | 2 164 | """ 165 | return sum(1 for v in nodes(G) if degree(G, v) == k) 166 | 167 | 168 | def number_of_degree_one_nodes(G): 169 | """Return the number of nodes of the graph with degree equal to 1. 170 | 171 | A vertex with degree equal to 1 is also called a *leaf*. 172 | 173 | Parameters 174 | ---------- 175 | G : NetworkX graph 176 | An undirected graph. 177 | 178 | Returns 179 | ------- 180 | int 181 | The number of nodes in the graph with degree equal to 1. 182 | 183 | See Also 184 | -------- 185 | number_of_nodes_of_degree_k, number_of_min_degree_nodes, 186 | number_of_max_degree_nodes 187 | 188 | Examples 189 | -------- 190 | >>> G = nx.path_graph(3) # Path on 3 nodes 191 | >>> nx.number_of_leaves(G) 192 | 2 193 | """ 194 | return number_of_nodes_of_degree_k(G, 1) 195 | 196 | 197 | def number_of_min_degree_nodes(G): 198 | """Return the number of nodes of the graph with degree equal to the minimum 199 | degree of the graph. 200 | 201 | Parameters 202 | ---------- 203 | G : NetworkX graph 204 | An undirected graph. 205 | 206 | Returns 207 | ------- 208 | int 209 | The number of nodes in the graph with degree equal to the minimum 210 | degree. 211 | 212 | See Also 213 | -------- 214 | number_of_nodes_of_degree_k, number_of_leaves, number_of_max_degree_nodes, 215 | min_degree 216 | 217 | Examples 218 | -------- 219 | >>> G = nx.path_graph(3) # Path on 3 nodes 220 | >>> nx.number_of_min_degree_nodes(G) 221 | 2 222 | """ 223 | return number_of_nodes_of_degree_k(G, min_degree(G)) 224 | 225 | 226 | def number_of_max_degree_nodes(G): 227 | """Return the number of nodes of the graph with degree equal to the maximum 228 | degree of the graph. 229 | 230 | Parameters 231 | ---------- 232 | G : NetworkX graph 233 | An undirected graph. 234 | 235 | Returns 236 | ------- 237 | int 238 | The number of nodes in the graph with degree equal to the maximum 239 | degree. 240 | 241 | See Also 242 | -------- 243 | number_of_nodes_of_degree_k, number_of_leaves, number_of_min_degree_nodes, 244 | max_degree 245 | 246 | Examples 247 | -------- 248 | >>> G = nx.path_graph(3) # Path on 3 nodes 249 | >>> nx.number_of_max_degree_nodes(G) 250 | 1 251 | """ 252 | return number_of_nodes_of_degree_k(G, max_degree(G)) 253 | 254 | 255 | def neighborhood_degree_list(G, nbunch): 256 | """Return a list of the unique degrees of all neighbors of nodes in 257 | `nbunch`. 258 | 259 | Parameters 260 | ---------- 261 | G : NetworkX graph 262 | An undirected graph. 263 | 264 | nbunch : 265 | A single node or iterable container of nodes. 266 | 267 | Returns 268 | ------- 269 | list 270 | A list of the degrees of all nodes in the neighborhood of the nodes 271 | in `nbunch`. 272 | 273 | See Also 274 | -------- 275 | closed_neighborhood_degree_list, neighborhood 276 | 277 | Examples 278 | -------- 279 | >>> import grinpy as gp 280 | >>> G = gp.path_graph(3) # Path on 3 nodes 281 | >>> gp.neighborhood_degree_list(G, 1) 282 | [1, 2] 283 | """ 284 | if isinstance(nodes, collections.abc.Iterable): 285 | return list(set(degree(G, u) for u in set_neighborhood(G, nbunch))) 286 | else: 287 | return list(set(degree(G, u) for u in neighborhood(G, nbunch))) 288 | 289 | 290 | def closed_neighborhood_degree_list(G, nbunch): 291 | """Return a list of the unique degrees of all nodes in the closed 292 | neighborhood of the nodes in `nbunch`. 293 | 294 | Parameters 295 | ---------- 296 | G : NetworkX graph 297 | An undirected graph. 298 | 299 | nbunch : 300 | A single node or iterable container of nodes. 301 | 302 | Returns 303 | ------- 304 | list 305 | A list of the degrees of all nodes in the closed neighborhood of the 306 | nodes in `nbunch`. 307 | 308 | See Also 309 | -------- 310 | closed_neighborhood, neighborhood_degree_list 311 | 312 | Examples 313 | -------- 314 | >>> import grinpy as gp 315 | >>> G = gp.path_graph(3) # Path on 3 nodes 316 | >>> gp.closed_neighborhood_degree_list(G, 1) 317 | [1, 2, 2] 318 | """ 319 | if isinstance(nodes, collections.abc.Iterable): 320 | return list(set(degree(G, u) for u in set_closed_neighborhood(G, nbunch))) 321 | else: 322 | return list(set(degree(G, u) for u in closed_neighborhood(G, nbunch))) 323 | 324 | 325 | def is_regular(G): 326 | """ Return True if G is regular, and False otherwise. 327 | 328 | A graph is *regular* if each node has the same degree. 329 | 330 | Parameters 331 | ---------- 332 | G : NetworkX graph 333 | An undirected graph 334 | 335 | Returns 336 | ------- 337 | boolean 338 | True if regular, false otherwise. 339 | """ 340 | return min_degree(G) == max_degree(G) 341 | 342 | 343 | def is_k_regular(G, k): 344 | """ Return True if the graph is regular of degree k and False otherwise. 345 | 346 | A graph is *regular of degree k* if all nodes have degree equal to *k*. 347 | 348 | Parameters 349 | ---------- 350 | G : NetworkX graph 351 | An undirected graph 352 | 353 | k : int 354 | An integer 355 | 356 | Returns 357 | ------- 358 | boolean 359 | True if all nodes have degree equal to *k*, False otherwise. 360 | """ 361 | # check that k is an integer 362 | if not float(k).is_integer(): 363 | raise TypeError("Expected k to be an integer.") 364 | k = int(k) 365 | for v in nodes(G): 366 | if not degree(G, v) == k: 367 | return False 368 | return True 369 | 370 | 371 | def is_sub_cubic(G): 372 | """ Return True if *G* sub-cubic, and False otherwise. 373 | 374 | A graph is *sub-cubic* if its maximum degree is at most 3. 375 | 376 | Parameters 377 | ---------- 378 | G : NetworkX graph 379 | An undirected graph. 380 | 381 | Returns 382 | ------- 383 | boolean 384 | True if *G* is sub-cubic, False otherwise. 385 | """ 386 | return max_degree(G) <= 3 387 | 388 | 389 | def is_cubic(G): 390 | """ Return True if *G* is cubic, and False otherwise. 391 | 392 | A graph is *cubic* if it is regular of degree 3. 393 | 394 | Parameters 395 | ---------- 396 | G : NetworkX graph 397 | An undirected graph 398 | 399 | Returns 400 | ------- 401 | boolean 402 | True if *G* is cubic, False otherwise. 403 | """ 404 | return is_k_regular(G, 3) 405 | -------------------------------------------------------------------------------- /grinpy/functions/distance.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing distances in graphs""" 11 | 12 | import networkx as nx 13 | 14 | 15 | def distance(G, source, target): 16 | r"""Return the distance in ``G`` between the ``source`` and ``target`` nodes. 17 | 18 | The *distance* between two nodes in a graph is the length of a 19 | shortest path between the nodes. 20 | 21 | Parameters 22 | ---------- 23 | G : NetworkX graph 24 | An undirected connected graph with order at least 3. 25 | 26 | source : node in G 27 | 28 | target : node in G 29 | 30 | Returns 31 | ------- 32 | int 33 | The distance between ``source`` and ``target`` in ``G`` 34 | """ 35 | return nx.shortest_path_length(G, source=source, target=target) 36 | -------------------------------------------------------------------------------- /grinpy/functions/graph_operations.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for performing operations on graphs""" 11 | 12 | from collections.abc import Iterable 13 | 14 | from grinpy.functions.neighborhoods import neighborhood 15 | 16 | 17 | def contract_nodes(G, nbunch, new_node=None): 18 | """ Contract the nodes in nbunch. 19 | 20 | Contracting a set S of nodes in a graph G first removes the nodes in S from 21 | G then creates a new node *v* with new edges *(v, u)* for all nodes *u* that 22 | are neighbors of a nodes in S. 23 | 24 | Parameters 25 | ---------- 26 | 27 | G : NetworkX graph 28 | An undirected graph. 29 | 30 | nbunch : 31 | A single node or iterable container of nodes in G. 32 | 33 | new_node : 34 | The node to be added. If no node is given, it defaults to the minimum 35 | node in nbunch according to the natural ordering of nodes. 36 | 37 | Notes 38 | ----- 39 | This method does not return a value. It alters the graph in place. 40 | """ 41 | # check if nbunch is an iterable; if not, convert to a list 42 | if not isinstance(nbunch, Iterable): 43 | nbunch = [nbunch] 44 | 45 | nbunch = [n for n in nbunch if n in G] 46 | # get all neighbors of nodes in nbunch that are not also in nbunch 47 | N = set().union(*[neighborhood(G, n) for n in nbunch]).difference(nbunch) 48 | # remove the nodes in nbunch from G, then add a new node with approriate edges 49 | G.remove_nodes_from(nbunch) 50 | new_node = new_node or min(nbunch) 51 | G.add_node(new_node) 52 | G.add_edges_from(map(lambda n: (new_node, n), N)) 53 | -------------------------------------------------------------------------------- /grinpy/functions/neighborhoods.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing neighborhoods of vertices and sets of vertices.""" 11 | 12 | from grinpy import neighbors 13 | 14 | __all__ = [ 15 | "neighborhood", 16 | "closed_neighborhood", 17 | "are_neighbors", 18 | "common_neighbors", 19 | "set_neighborhood", 20 | "set_closed_neighborhood", 21 | ] 22 | 23 | 24 | def neighborhood(G, v): 25 | """Return a list of all neighbors of v. 26 | 27 | Parameters 28 | ---------- 29 | G : NetworkX graph 30 | An undirected graph. 31 | 32 | v : 33 | A node in G. 34 | 35 | Returns 36 | ------- 37 | list 38 | A list containing all nodes that are a neighbor of v. 39 | 40 | See Also 41 | -------- 42 | closed_neighborhood 43 | 44 | Examples 45 | -------- 46 | >>> G = nx.path_graph(3) # Path on 3 nodes 47 | >>> nx.neighborhood(G, 1) 48 | [0, 2] 49 | """ 50 | return list(neighbors(G, v)) 51 | 52 | 53 | def set_neighborhood(G, nodes): 54 | """Return a list of all neighbors of every node in nodes. 55 | 56 | Parameters 57 | ---------- 58 | G : NetworkX graph 59 | An undirected graph. 60 | 61 | nodes : 62 | An interable container of nodes in G. 63 | 64 | Returns 65 | ------- 66 | list 67 | A list containing all nodes that are a neighbor of some node in nodes. 68 | 69 | See Also 70 | -------- 71 | set_closed_neighborhood 72 | """ 73 | # TODO: write unit test 74 | N = set() 75 | for n in nodes: 76 | N |= set(neighborhood(G, n)) 77 | return list(N) 78 | 79 | 80 | def closed_neighborhood(G, v): 81 | """Return a list with v and of all neighbors of v. 82 | 83 | Parameters 84 | ---------- 85 | G : NetworkX graph 86 | An undirected graph. 87 | 88 | v : 89 | A node in G. 90 | 91 | Returns 92 | ------- 93 | list 94 | A list containing v and all nodes that are a neighbor of v. 95 | 96 | See Also 97 | -------- 98 | neighborhood 99 | 100 | Examples 101 | -------- 102 | >>> G = nx.path_graph(3) # Path on 3 nodes 103 | >>> nx.closed_neighborhood(G, 1) 104 | [0, 1, 2] 105 | """ 106 | return list(set(neighborhood(G, v)).union([v])) 107 | 108 | 109 | def set_closed_neighborhood(G, nodes): 110 | """Return a list containing every node in nodes all neighbors their neighbors. 111 | 112 | Parameters 113 | ---------- 114 | G : NetworkX graph 115 | An undirected graph. 116 | 117 | nodes : 118 | An interable container of nodes in G. 119 | 120 | Returns 121 | ------- 122 | list 123 | A list containing every node in nodes and all nodes of their neighbors. 124 | 125 | See Also 126 | -------- 127 | set_neighborhood 128 | """ 129 | # TODO: write unit test 130 | N = set(set_neighborhood(G, nodes)).union(nodes) 131 | return list(N) 132 | 133 | 134 | def are_neighbors(G, u, v): 135 | """Returns true if u is adjacent to v. Otherwise, 136 | returns false. 137 | 138 | Parameters 139 | ---------- 140 | G : NetworkX graph 141 | An undirected graph. 142 | 143 | u : node 144 | A node in the graph. 145 | 146 | v : node 147 | A node in the graph. 148 | 149 | 150 | Returns 151 | ------- 152 | bool 153 | True if u is a neighbor of v in G, False otherwise. 154 | 155 | 156 | Examples 157 | -------- 158 | >>> G = nx.star_graph(3) # Star on 4 nodes 159 | >>> nx.are_neighbors(G, 0, 1) 160 | True 161 | >>> nx.are_neighbors(G, 1, 2) 162 | False 163 | """ 164 | return u in neighborhood(G, v) 165 | 166 | 167 | def common_neighbors(G, nodes): 168 | """Returns a list of all nodes in G that are adjacent to every node in `nodes`. 169 | 170 | Parameters 171 | ---------- 172 | G : NetworkX graph 173 | An undirected graph. 174 | 175 | nbunch : 176 | A single node or iterable container 177 | 178 | Returns 179 | ------- 180 | list 181 | All nodes adjacent to every node in nbunch. If nbunch contains only a 182 | single node, that nodes neighborhood is returned. 183 | """ 184 | S = set(neighborhood(G, nodes[0])) 185 | for n in nodes: 186 | S = S.intersection(set(neighborhood(G, n))) 187 | return list(S) 188 | -------------------------------------------------------------------------------- /grinpy/functions/structural_properties.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing various structural properites.""" 11 | 12 | import grinpy as gp 13 | from grinpy import nodes 14 | from grinpy.functions.neighborhoods import neighborhood 15 | import itertools 16 | 17 | __all__ = ["is_triangle_free", "is_bull_free", "is_claw_free", "is_complete_graph"] 18 | 19 | 20 | def is_complete_graph(G): 21 | """Returns True if *G* is a complete graph, and False otherwise. 22 | 23 | Parameters 24 | ---------- 25 | G : NetworkX graph 26 | An undirected graph. 27 | 28 | Returns 29 | ------- 30 | boolean 31 | True if G is a complete graph, False otherwise. 32 | """ 33 | V = set(nodes(G)) 34 | for v in V: 35 | if not set(neighborhood(G, v)) == V.difference({v}): 36 | return False 37 | return True 38 | 39 | 40 | def is_triangle_free(G): 41 | """Returns True if *G* is triangle-free, and False otherwise. 42 | 43 | A graph is *triangle-free* if it contains no induced subgraph isomorphic to 44 | the complete graph on 3 vertices. 45 | 46 | Parameters 47 | ---------- 48 | G : NetworkX graph 49 | An undirected graph. 50 | 51 | Returns 52 | ------- 53 | boolean 54 | True if G is triangle-free, False otherwise. 55 | 56 | Examples 57 | -------- 58 | >>> G = gp.complete_graph(4) 59 | >>> gp.is_triangle_free(G) 60 | False 61 | >>> G = gp.cycle_graph(5) 62 | >> gp.is_triangle_free(G) 63 | True 64 | """ 65 | # define a triangle graph, also known as the complete graph K_3 66 | triangle = gp.complete_graph(3) 67 | 68 | # enumerate over all possible combinations of 3 vertices contained in G 69 | for S in set(itertools.combinations(G.nodes(), 3)): 70 | H = G.subgraph(list(S)) 71 | if gp.is_isomorphic(H, triangle): 72 | return False 73 | # if the above loop completes, the graph is triangle free 74 | return True 75 | 76 | 77 | def is_bull_free(G): 78 | """Returns True if *G* is bull-free, and False otherwise. 79 | 80 | A graph is *bull-free* if it contains no induced subgraph isomorphic to the 81 | bull graph, where the bull graph is the complete graph on 3 vertices with 82 | two pendants added to two of its vertices. 83 | 84 | Parameters 85 | ---------- 86 | G : NetworkX graph 87 | An undirected graph. 88 | 89 | Returns 90 | ------- 91 | boolean 92 | True if *G* is bull-free, false otherwise. 93 | 94 | Examples 95 | -------- 96 | >>> G = gp.complete_graph(4) 97 | >>> gp.is_bull_free(G) 98 | True 99 | """ 100 | # define a bull graph, also known as the graph obtained from the complete graph K_3 by addiing two pendants 101 | bull = gp.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (2, 4)]) 102 | 103 | # enumerate over all possible combinations of 5 vertices contained in G 104 | for S in set(itertools.combinations(G.nodes(), 5)): 105 | H = G.subgraph(list(S)) 106 | if gp.is_isomorphic(H, bull): 107 | return False 108 | # if the above loop completes, the graph is bull-free 109 | return True 110 | 111 | 112 | def is_claw_free(G): 113 | """Returns True if *G* is claw-free, and False otherwise. 114 | 115 | A graph is *claw-free* if it contains no induce subgraph isomorphic to the 116 | star on four vertices. 117 | 118 | Parameters 119 | ---------- 120 | G : NetworkX graph 121 | An undirected graph. 122 | 123 | Returns 124 | ------- 125 | boolean 126 | True if *G* is claw-free, and False otherwise. 127 | 128 | Examples 129 | -------- 130 | >>> G = gp.complete_graph(4) 131 | >>> gp.is_claw_free(G) 132 | True 133 | >>> G = gp.star_graph(4) 134 | >> gp.is_claw_free(G) 135 | False 136 | """ 137 | # define a claw graph, also known as the complete bipartite graph K_1,3 138 | claw = gp.star_graph(3) 139 | 140 | # enumerate over all possible combinations of 4 vertices contained in G 141 | for S in set(itertools.combinations(G.nodes(), 4)): 142 | H = G.subgraph(list(S)) 143 | if gp.is_isomorphic(H, claw): 144 | return False 145 | # if the above loop completes, the graph is claw-free 146 | return True 147 | -------------------------------------------------------------------------------- /grinpy/invariants/__init__.py: -------------------------------------------------------------------------------- 1 | from grinpy.invariants.chromatic import * # noqa 2 | from grinpy.invariants.clique import * # noqa 3 | from grinpy.invariants.disparity import * # noqa 4 | from grinpy.invariants.distance_measures import * # noqa 5 | from grinpy.invariants.domination import * # noqa 6 | from grinpy.invariants.dsi import * # noqa 7 | from grinpy.invariants.independence import * # noqa 8 | from grinpy.invariants.matching import * # noqa 9 | from grinpy.invariants.power_domination import * # noqa 10 | from grinpy.invariants.residue import * # noqa 11 | from grinpy.invariants.topological_indices import * # noqa 12 | from grinpy.invariants.vertex_cover import * # noqa 13 | from grinpy.invariants.zero_forcing import * # noqa 14 | -------------------------------------------------------------------------------- /grinpy/invariants/chromatic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing the chromatic number of a graph.""" 11 | 12 | import itertools 13 | 14 | import numpy as np 15 | from pulp import LpBinary, LpMinimize, LpProblem, LpVariable, lpSum 16 | 17 | from grinpy import is_connected 18 | from grinpy.functions.graph_operations import contract_nodes 19 | from grinpy.functions.neighborhoods import are_neighbors, common_neighbors 20 | from grinpy.functions.structural_properties import is_complete_graph 21 | from grinpy.utils.combinations import pairs_of_nodes 22 | 23 | __all__ = ["chromatic_number", "min_proper_coloring"] 24 | 25 | 26 | def min_proper_coloring_ilp(G): 27 | """Return a smallest proper coloring the graph. 28 | 29 | A *k*-proper coloring is a function 30 | 31 | .. math:: 32 | 33 | c : V \\to \\{1, \\dotsb, k\\} \\mthrm{ such that } c(u) \\neq c(v) 34 | \\mathrm{ for all } \\{u, v\\} \\in E 35 | 36 | where *V* and *E* are the vertex and edge set of *G*. 37 | 38 | A smallest proper coloring is a proper coloring with minimum *k*. 39 | 40 | This method using integer programming to compute a smallest proper 41 | coloring. It solves the following integer linear program: minimize 42 | 43 | .. math:: 44 | 45 | \\sum_{1 \\leq j \\leq n} x_j 46 | 47 | 48 | subject to 49 | 50 | .. math:: 51 | 52 | \\sum_{1 \\leq j \\leq n} c_v^j = 1 \\mathrm{ for all } v \\in V 53 | c_v^j + c_u^j \\leq 1 \\mathrm{ for all } j \\in \\{1, \\dotsb, n\\}, \\{u, v\\} \\in E 54 | c_v^j \\leq y_j \\mathrm{ for all } j \\in \\{1, \\dotsb, n\\}, v \\in V 55 | y_j, c_v^j \\in \\{0, 1\\} \\mathrm{ for all } j \\in \\{1, \\dotsb, n\\}, v \\in V 56 | 57 | Note that this method is not imported by default when importing 58 | GrinPy. See `min_proper_coloring`. 59 | 60 | Parameters 61 | ---------- 62 | G : NetworkX graph 63 | An undirected graph. 64 | 65 | Returns 66 | ------- 67 | dict 68 | A dictionary whose keys are colors and values are sets of 69 | nodes colored that color in a smallest proper coloring. 70 | 71 | See Also 72 | -------- 73 | min_proper_coloring 74 | 75 | """ 76 | prob = LpProblem("min_proper_coloring", LpMinimize) 77 | colors = {i: LpVariable("x_{}".format(i), 0, 1, LpBinary) for i in range(G.order())} 78 | node_colors = { 79 | node: [LpVariable("c_{}_{}".format(node, i), 0, 1, LpBinary) for i in range(G.order())] for node in G.nodes() 80 | } 81 | 82 | # Set the min proper coloring objective function 83 | prob += lpSum([colors[i] for i in colors]) 84 | 85 | # Set constraints 86 | for node in G.nodes(): 87 | prob += sum(node_colors[node]) == 1 88 | 89 | for edge, i in itertools.product(G.edges(), range(G.order())): 90 | prob += sum(node_colors[edge[0]][i] + node_colors[edge[1]][i]) <= 1 91 | 92 | for node, i in itertools.product(G.nodes(), range(G.order())): 93 | prob += node_colors[node][i] <= colors[i] 94 | 95 | prob.solve() 96 | solution_set = {color: [node for node in node_colors if node_colors[node][color].value() == 1] for color in colors} 97 | return solution_set 98 | 99 | 100 | def min_proper_coloring(G): 101 | """Return a smallest proper coloring the graph. 102 | 103 | A *k*-proper coloring is a function 104 | 105 | .. math:: 106 | 107 | c : V \\to \\{1, \\dotsb, k\\} \\mthrm{ such that } c(u) \\neq c(v) 108 | \\mathrm{ for all } \\{u, v\\} \\in E 109 | 110 | where *V* and *E* are the vertex and edge set of *G*. 111 | 112 | A smallest proper coloring is a proper coloring with minimum *k*. 113 | 114 | This method using integer programming to compute a smallest proper 115 | coloring by calling the `min_proper_coloring_ilp` method. 116 | 117 | Parameters 118 | ---------- 119 | G : NetworkX graph 120 | An undirected graph. 121 | 122 | Returns 123 | ------- 124 | dict 125 | A dictionary whose keys are colors and values are sets of 126 | nodes colored that color in a smallest proper coloring. 127 | 128 | See Also 129 | -------- 130 | min_proper_coloring_ilp 131 | 132 | """ 133 | return min_proper_coloring_ilp(G) 134 | 135 | 136 | def chromatic_number_ilp(G): 137 | """ 138 | Return the chromatic number of G. 139 | 140 | This method calculates the chromatic number by solving an integer 141 | linear program. 142 | 143 | Parameters 144 | ---------- 145 | G : NetworkX graph 146 | An undirected graph. 147 | 148 | Returns 149 | ------- 150 | int 151 | The chromatic number of G. 152 | 153 | """ 154 | coloring = min_proper_coloring_ilp(G) 155 | colors = [color for color in coloring if len(coloring[color]) > 0] 156 | return len(colors) 157 | 158 | 159 | def chromatic_number_ram_rama(G): 160 | """Return the chromatic number of G. 161 | 162 | The *chromatic number* of a graph G is the size of a mininum coloring of 163 | the nodes in G such that no two adjacent nodes have the same color. 164 | 165 | The method for computing the chromatic number is an implementation of the 166 | algorithm discovered by Ram and Rama. 167 | 168 | Parameters 169 | ---------- 170 | G : NetworkX graph 171 | An undirected graph. 172 | 173 | Returns 174 | ------- 175 | int 176 | The chromatic number of G. 177 | 178 | References 179 | ---------- 180 | A.M. Ram, R. Rama, An alternate method to find the chromatic number of a 181 | finite, connected graph, *arXiv preprint 182 | arXiv:1309.3642*, (2013) 183 | 184 | """ 185 | if not is_connected(G): 186 | raise TypeError("Invalid graph: not connected") 187 | 188 | if is_complete_graph(G): 189 | return G.order() 190 | 191 | # get list of pairs of non neighbors in G 192 | N = [list(p) for p in pairs_of_nodes(G) if not are_neighbors(G, p[0], p[1])] 193 | 194 | # get a pair of non neighbors who have the most common neighbors 195 | num_common_neighbors = list(map(lambda p: len(common_neighbors(G, p)), N)) 196 | P = N[np.argmax(num_common_neighbors)] 197 | 198 | # Collapse the nodes in P and repeat the above process 199 | H = G.copy() 200 | contract_nodes(H, P) 201 | return chromatic_number(H) 202 | 203 | 204 | def chromatic_number(G, method="ilp"): 205 | """Return the chromatic number of G. 206 | 207 | The *chromatic number* of a graph G is the size of a mininum 208 | coloring of the nodes in G such that no two adjacent nodes have the 209 | same color. 210 | 211 | Parameters 212 | ---------- 213 | G : NetworkX graph 214 | An undirected graph. 215 | 216 | method: string 217 | The method to use for finding the maximum matching. Use 218 | 'ilp' for integer linear program or 'ram-rama' for the Ram-Rama 219 | algorithm. Defaults to 'ilp'. 220 | 221 | Returns 222 | ------- 223 | set 224 | A set of edges comprising a maximum matching in *G*. 225 | 226 | See Also 227 | -------- 228 | max_matching 229 | 230 | """ 231 | chromatic_number_func = {"ram-rama": chromatic_number_ram_rama, "ilp": chromatic_number_ilp}.get(method, None) 232 | 233 | if chromatic_number_func: 234 | return chromatic_number_func(G) 235 | 236 | raise ValueError('Invalid `method` argument "{}"'.format(method)) 237 | -------------------------------------------------------------------------------- /grinpy/invariants/clique.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing independence related invariants for a graph.""" 11 | 12 | from grinpy import graph_clique_number 13 | 14 | __all__ = ["clique_number"] 15 | 16 | 17 | def clique_number(G, cliques=None): 18 | """Return the clique number of the graph. 19 | 20 | A *clique* in a graph *G* is a complete subgraph. The *clique number* is the 21 | size of a largest clique. 22 | 23 | This function is a wrapper for the NetworkX :func:`graph_clique_number` 24 | method in `networkx.algorithms.clique`. 25 | 26 | Parameters 27 | ---------- 28 | G : NetworkX graph 29 | An undirected graph. 30 | 31 | cliques : list 32 | A list of cliques, each of which is itself a list of nodes. If not 33 | specified, the list of all cliques will be computed, as by 34 | :func:`networkx.algorithms.clique.find_cliques()`. 35 | 36 | Returns 37 | ------- 38 | int 39 | The size of a largest clique in `G` 40 | 41 | Notes 42 | -------- 43 | You should provide `cliques` if you have already computed the list of 44 | maximal cliques, in order to avoid an exponential time search for maximal 45 | cliques. 46 | """ 47 | return graph_clique_number(G, cliques) 48 | -------------------------------------------------------------------------------- /grinpy/invariants/disparity.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing disparity related invariants. 11 | """ 12 | 13 | from grinpy import nodes, number_of_nodes 14 | from grinpy.functions.degree import closed_neighborhood_degree_list, neighborhood_degree_list 15 | 16 | __all__ = [ 17 | "vertex_disparity", 18 | "closed_vertex_disparity", 19 | "disparity_sequence", 20 | "closed_disparity_sequence", 21 | "CW_disparity", 22 | "closed_CW_disparity", 23 | "inverse_disparity", 24 | "closed_inverse_disparity", 25 | "average_vertex_disparity", 26 | "average_closed_vertex_disparity", 27 | "k_disparity", 28 | "closed_k_disparity", 29 | "irregularity", 30 | ] 31 | 32 | 33 | def vertex_disparity(G, v): 34 | """Return number of distinct degrees of neighbors of v. 35 | 36 | Parameters 37 | ---------- 38 | G : NetworkX graph 39 | An undirected graph. 40 | 41 | v : node 42 | A node in G. 43 | 44 | Returns 45 | ------- 46 | int 47 | The number of distinct degrees of neighbors of v. 48 | 49 | See Also 50 | -------- 51 | closed_vertex_disparity 52 | """ 53 | if v not in nodes(G): 54 | raise (ValueError) 55 | 56 | return len(neighborhood_degree_list(G, v)) 57 | 58 | 59 | def closed_vertex_disparity(G, v): 60 | """Return number of distinct degrees of nodes in the closed neighborhood 61 | of v. 62 | 63 | Parameters 64 | ---------- 65 | G : NetworkX graph 66 | An undirected graph. 67 | 68 | v : node 69 | A node in G. 70 | 71 | Returns 72 | ------- 73 | int 74 | The number of distinct degrees of nodes in the closed neighborhood 75 | of v. 76 | 77 | See Also 78 | -------- 79 | vertex_disparity 80 | """ 81 | if v not in nodes(G): 82 | raise (ValueError) 83 | 84 | return len(closed_neighborhood_degree_list(G, v)) 85 | 86 | 87 | def disparity_sequence(G): 88 | """Return the sequence of disparities of each node in the graph. 89 | 90 | Parameters 91 | ---------- 92 | G : NetworkX graph 93 | An undirected graph. 94 | 95 | Returns 96 | ------- 97 | list 98 | The sequence of disparities of each node in the graph. 99 | 100 | See Also 101 | -------- 102 | closed_disparity_sequence, vertex_disparity 103 | """ 104 | return [vertex_disparity(G, v) for v in nodes(G)] 105 | 106 | 107 | def closed_disparity_sequence(G): 108 | """Return the sequence of closed disparities of each node in the graph. 109 | 110 | Parameters 111 | ---------- 112 | G : NetworkX graph 113 | An undirected graph. 114 | 115 | Returns 116 | ------- 117 | list 118 | The sequence of closed disparities of each node in the graph. 119 | 120 | See Also 121 | -------- 122 | closed_vertex_disparity, disparity_sequence 123 | """ 124 | return [closed_vertex_disparity(G, v) for v in nodes(G)] 125 | 126 | 127 | def CW_disparity(G): 128 | r"""Return the Caro-Wei disparity of the graph. 129 | 130 | The *Caro-Wei disparity* of a graph is defined as: 131 | 132 | .. math:: 133 | \sum_{v \in V(G)}\frac{1}{1 + disp(v)} 134 | 135 | where *V(G)* is the set of nodes of *G* and *disp(v)* is the disparity of 136 | the vertex v. 137 | 138 | This invariant is inspired by the Caro-Wei bound for the independence number 139 | of a graph, hence the name. 140 | 141 | Parameters 142 | ---------- 143 | G : NetworkX graph 144 | An undirected graph. 145 | 146 | Returns 147 | ------- 148 | float 149 | The Caro-Wei disparity of the graph. 150 | 151 | See Also 152 | -------- 153 | closed_CW_disparity, closed_inverse_disparity, inverse_disparity 154 | """ 155 | return sum(1 / (1 + x) for x in disparity_sequence(G)) 156 | 157 | 158 | def closed_CW_disparity(G): 159 | r"""Return the closed Caro-Wei disparity of the graph. 160 | 161 | The *closed Caro-Wei disparity* of a graph is defined as: 162 | 163 | .. math:: 164 | \sum_{v \in V(G)}\frac{1}{1 + cdisp(v)} 165 | 166 | where *V(G)* is the set of nodes of *G* and *cdisp(v)* is the closed 167 | disparity of the vertex v. 168 | 169 | This invariant is inspired by the Caro-Wei bound for the independence number 170 | of a graph, hence the name. 171 | 172 | Parameters 173 | ---------- 174 | G : NetworkX graph 175 | An undirected graph. 176 | 177 | Returns 178 | ------- 179 | float 180 | The closed Caro-Wei disparity of the graph. 181 | 182 | See Also 183 | -------- 184 | CW_disparity, closed_inverse_disparity, inverse_disparity 185 | """ 186 | return sum(1 / (1 + x) for x in closed_disparity_sequence(G)) 187 | 188 | 189 | def inverse_disparity(G): 190 | r"""Return the inverse disparity of the graph. 191 | 192 | The *inverse disparity* of a graph is defined as: 193 | 194 | .. math:: 195 | \sum_{v \in V(G)}\frac{1}{disp(v)} 196 | 197 | where *V(G)* is the set of nodes of *G* and *disp(v)* is the disparity 198 | of the vertex v. 199 | 200 | Parameters 201 | ---------- 202 | G : NetworkX graph 203 | An undirected graph. 204 | 205 | Returns 206 | ------- 207 | float 208 | The inverse disparity of the graph. 209 | 210 | See Also 211 | -------- 212 | CW_disparity, closed_CW_disparity, closed_inverse_disparity 213 | """ 214 | return sum(1 / x for x in disparity_sequence(G)) 215 | 216 | 217 | def closed_inverse_disparity(G): 218 | r"""Return the closed inverse disparity of the graph. 219 | 220 | The *closed inverse disparity* of a graph is defined as: 221 | 222 | .. math:: 223 | \sum_{v \in V(G)}\frac{1}{cdisp(v)} 224 | 225 | where *V(G)* is the set of nodes of *G* and *cdisp(v)* is the closed 226 | disparity of the vertex v. 227 | 228 | Parameters 229 | ---------- 230 | G : NetworkX graph 231 | An undirected graph. 232 | 233 | Returns 234 | ------- 235 | float 236 | The closed inverse disparity of the graph. 237 | 238 | See Also 239 | -------- 240 | CW_disparity, closed_CW_disparity, inverse_disparity 241 | """ 242 | return sum(1 / x for x in closed_disparity_sequence(G)) 243 | 244 | 245 | def average_vertex_disparity(G): 246 | """Return the average vertex disparity of the graph. 247 | 248 | Parameters 249 | ---------- 250 | G : NetworkX graph 251 | An undirected graph. 252 | 253 | Returns 254 | ------- 255 | int 256 | The average vertex disparity of the graph. 257 | 258 | See Also 259 | -------- 260 | average_closed_vertex_disparity, vertex_disparity 261 | """ 262 | D = disparity_sequence(G) 263 | return sum(D) / len(D) 264 | 265 | 266 | def average_closed_vertex_disparity(G): 267 | """Return the average closed vertex disparity of the graph. 268 | 269 | Parameters 270 | ---------- 271 | G : NetworkX graph 272 | An undirected graph. 273 | 274 | Returns 275 | ------- 276 | int 277 | The average closed vertex disparity of the graph. 278 | 279 | See Also 280 | -------- 281 | average_vertex_disparity, closed_vertex_disparity 282 | """ 283 | D = closed_disparity_sequence(G) 284 | return sum(D) / len(D) 285 | 286 | 287 | def k_disparity(G, k): 288 | r"""Return the k-disparity of the graph. 289 | 290 | The *k-disparity* of a graph is defined as: 291 | 292 | .. math:: 293 | \frac{2}{k(k+1)}\sum_{i=0}^{k-i}(k-i)d_i 294 | 295 | where *k* is a positive integer and *d_i* is the i-th element in the 296 | disparity sequence, ordered in weakly decreasing order. 297 | 298 | Parameters 299 | ---------- 300 | G : NetworkX graph 301 | An undirected graph. 302 | 303 | Returns 304 | ------- 305 | float 306 | The k-disparity of the graph. 307 | 308 | See Also 309 | -------- 310 | closed_k_disparity 311 | """ 312 | D = disparity_sequence(G) 313 | D.sort(reverse=True) 314 | s = sum((k - i) * D[i] for i in range(k)) 315 | return (2 * s) / (k * (k + 1)) 316 | 317 | 318 | def closed_k_disparity(G, k): 319 | r"""Return the closed k-disparity of the graph. 320 | 321 | The *closed k-disparity* of a graph is defined as: 322 | 323 | .. math:: 324 | \frac{2}{k(k+1)}\sum_{i=0}^{k-1}(k-i)d_i 325 | 326 | where *k* is a positive integer and *d_i* is the i-th element in the 327 | closed disparity sequence, ordered in weakly decreasing order. 328 | 329 | Parameters 330 | ---------- 331 | G : NetworkX graph 332 | An undirected graph. 333 | 334 | Returns 335 | ------- 336 | float 337 | The closed k-disparity of the graph. 338 | 339 | See Also 340 | -------- 341 | k_disparity 342 | """ 343 | D = closed_disparity_sequence(G) 344 | D.sort(reverse=True) 345 | s = sum((k - i) * D[i] for i in range(k)) 346 | return (2 * s) / (k * (k + 1)) 347 | 348 | 349 | def irregularity(G): 350 | r"""Return the irregularity measure of the graph. 351 | 352 | The *irregularity* of an *n*-vertex graph is defined as: 353 | 354 | .. math:: 355 | \frac{2}{n(n+1)}\sum_{i=0}^{n-i}(n-i)d_i 356 | 357 | where *d_i* is the i-th element in the closed disparity sequence, ordered 358 | in weakly decreasing order. 359 | 360 | Parameters 361 | ---------- 362 | G : NetworkX graph 363 | An undirected graph. 364 | 365 | Returns 366 | ------- 367 | float 368 | The irregularity of the graph. 369 | 370 | See Also 371 | -------- 372 | k_disparity 373 | """ 374 | return closed_k_disparity(G, number_of_nodes(G)) 375 | -------------------------------------------------------------------------------- /grinpy/invariants/distance_measures.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing distance-related invariants in a graph""" 11 | 12 | import itertools 13 | import functools 14 | 15 | from grinpy.functions.distance import distance 16 | 17 | 18 | def triameter(G): 19 | r"""Returns the triameter of the graph G with at least 3 nodes. 20 | 21 | The *triameter* of a connected graph G with vertex set *V* is defined as the 22 | following maximum value 23 | 24 | .. math:: 25 | \max\{d(v,w) + d(w,z) + d(v,z): v,w,z \in V: \} 26 | 27 | Parameters 28 | ---------- 29 | G : NetworkX graph 30 | An undirected connected graph with order at least 3. 31 | 32 | Returns 33 | ------- 34 | int 35 | The triameter of the graph G. 36 | 37 | References 38 | ---------- 39 | A. Das, The triameter of graphs, ArXiv preprint arXiv:1804.01088, 2018. 40 | https://arxiv.org/pdf/1804.01088.pdf 41 | """ 42 | _distance = functools.partial(distance, G) 43 | distance_sums = ( 44 | sum((_distance(*pair) for pair in itertools.combinations(nodes, 2))) 45 | for nodes in itertools.combinations(G.nodes(), 3) 46 | ) 47 | return max(distance_sums) 48 | -------------------------------------------------------------------------------- /grinpy/invariants/domination.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing dominating sets in a graph.""" 11 | from itertools import combinations 12 | 13 | from pulp import LpBinary, LpMinimize, LpProblem, LpVariable, lpSum 14 | 15 | from grinpy import ( 16 | closed_neighborhood, 17 | is_connected, 18 | is_dominating_set, 19 | neighborhood, 20 | nodes, 21 | number_of_nodes, 22 | number_of_nodes_of_degree_k, 23 | set_neighborhood, 24 | ) 25 | from grinpy.invariants.dsi import sub_k_domination_number, sub_total_domination_number 26 | from grinpy.invariants.independence import is_independent_set 27 | 28 | __all__ = [ 29 | "is_k_dominating_set", 30 | "is_total_dominating_set", 31 | "is_connected_k_dominating_set", 32 | "is_connected_dominating_set", 33 | "min_k_dominating_set", 34 | "min_dominating_set", 35 | "min_total_dominating_set", 36 | "min_connected_k_dominating_set", 37 | "min_connected_dominating_set", 38 | "domination_number", 39 | "k_domination_number", 40 | "total_domination_number", 41 | "connected_k_domination_number", 42 | "connected_domination_number", 43 | "is_independent_k_dominating_set", 44 | "is_independent_dominating_set", 45 | "min_independent_k_dominating_set", 46 | "min_independent_dominating_set", 47 | "independent_k_domination_number", 48 | "independent_domination_number", 49 | ] 50 | 51 | 52 | def is_k_dominating_set(G, nodes, k): 53 | """Return whether or not nodes comprises a k-dominating set. 54 | 55 | A *k-dominating set* is a set of nodes with the property that every 56 | node in the graph is either in the set or adjacent at least 1 and at 57 | most k nodes in the set. 58 | 59 | This is a generalization of the well known concept of a dominating 60 | set (take k = 1). 61 | 62 | Parameters 63 | ---------- 64 | G : NetworkX graph 65 | An undirected graph. 66 | 67 | nodes : list, set 68 | An iterable container of nodes in G. 69 | 70 | k : int 71 | A positive integer. 72 | 73 | Returns 74 | ------- 75 | boolean 76 | True if the nodes in nbunch comprise a k-dominating set, and 77 | False otherwise. 78 | 79 | """ 80 | # check that k is a positive integer 81 | if not float(k).is_integer(): 82 | raise TypeError("Expected k to be an integer.") 83 | k = int(k) 84 | if k < 1: 85 | raise ValueError("Expected k to be a positive integer.") 86 | # check if nbunch is an iterable; if not, convert to a list 87 | S = set(n for n in nodes if n in G) 88 | if k == 1: 89 | return is_dominating_set(G, S) 90 | else: 91 | # loop through the nodes in the complement of S and determine 92 | # if they are adjacent to atleast k nodes in S 93 | others = set(G.nodes()).difference(S) 94 | for v in others: 95 | if len(set(neighborhood(G, v)).intersection(S)) < k: 96 | return False 97 | # if the above loop completes, nbunch is a k-dominating set 98 | return True 99 | 100 | 101 | def is_total_dominating_set(G, nodes): 102 | """Return whether or not nodes comprises a total dominating set. 103 | 104 | A * total dominating set* is a set of nodes with the property that 105 | every node in the graph is adjacent to some node in the set. 106 | 107 | Parameters 108 | ---------- 109 | G : NetworkX graph 110 | An undirected graph. 111 | 112 | nodes : list, set 113 | An iterable container of nodes in G. 114 | 115 | Returns 116 | ------- 117 | boolean 118 | True if the nodes in nbunch comprise a k-dominating set, and 119 | False otherwise. 120 | 121 | """ 122 | # exclude any nodes that aren't in G 123 | S = set(n for n in nodes if n in G) 124 | return set(set_neighborhood(G, S)) == set(G.nodes()) 125 | 126 | 127 | def is_connected_k_dominating_set(G, nodes, k): 128 | """Return whether or not *nodes* is a connected *k*-dominating set of *G*. 129 | 130 | A set *D* is a *connected k-dominating set* of *G* is *D* is a 131 | *k*-dominating set in *G* and the subgraph of *G* induced by *D* is 132 | a connected graph. 133 | 134 | Parameters 135 | ---------- 136 | G : NetworkX graph 137 | An undirected graph. 138 | 139 | nodes : list, set 140 | An iterable container of nodes in G. 141 | 142 | k : int 143 | A positive integer 144 | 145 | Returns 146 | ------- 147 | boolean 148 | True if *nbunch* is a connected *k*-dominating set in *G*, and false 149 | otherwise. 150 | 151 | """ 152 | # check that k is a positive integer 153 | if not float(k).is_integer(): 154 | raise TypeError("Expected k to be an integer.") 155 | k = int(k) 156 | if k < 1: 157 | raise ValueError("Expected k to be a positive integer.") 158 | S = set(n for n in nodes if n in G) 159 | H = G.subgraph(S) 160 | return is_connected(H) and is_k_dominating_set(G, S, k) 161 | 162 | 163 | def is_connected_dominating_set(G, nodes): 164 | """Return whether or not *nodes* is a connected dominating set of *G*. 165 | 166 | A set *D* is a *connected dominating set* of *G* is *D* is a 167 | dominating set in *G* and the subgraph of *G* induced by *D* is a 168 | connected graph. 169 | 170 | Parameters 171 | ---------- 172 | G : NetworkX graph 173 | An undirected graph. 174 | 175 | nodes : list, set 176 | An iterable container of nodes in G. 177 | 178 | Returns 179 | ------- 180 | boolean 181 | True if *nbunch* is a connected *k*-dominating set in *G*, and false 182 | otherwise. 183 | 184 | """ 185 | return is_connected_k_dominating_set(G, nodes, 1) 186 | 187 | 188 | def min_k_dominating_set(G, k): 189 | """Return a smallest k-dominating set in the graph. 190 | 191 | The method to compute the set is brute force except that the subsets 192 | searched begin with those whose cardinality is equal to the 193 | sub-k-domination number of the graph, which was defined by Amos et 194 | al. and shown to be a tractable lower bound for the k-domination 195 | number. 196 | 197 | Parameters 198 | ---------- 199 | G : NetworkX graph 200 | An undirected graph. 201 | 202 | k : int 203 | A positive integer. 204 | 205 | Returns 206 | ------- 207 | list 208 | A list of nodes in a smallest k-dominating set in the graph. 209 | 210 | References 211 | ---------- 212 | D. Amos, J. Asplund, and R. Davila, The sub-k-domination number of a 213 | graph with applications to k-domination, *arXiv preprint 214 | arXiv:1611.02379*, (2016) 215 | 216 | """ 217 | # check that k is a positive integer 218 | if not float(k).is_integer(): 219 | raise TypeError("Expected k to be an integer.") 220 | k = int(k) 221 | if k < 1: 222 | raise ValueError("Expected k to be a positive integer.") 223 | # use the sub-k-domination number to compute a starting point for the 224 | # search range 225 | rangeMin = sub_k_domination_number(G, k) 226 | # loop through subsets of nodes of G in increasing order of size until a 227 | # dominating set is found 228 | for i in range(rangeMin, number_of_nodes(G) + 1): 229 | for S in combinations(nodes(G), i): 230 | if is_k_dominating_set(G, S, k): 231 | return list(S) 232 | 233 | 234 | def min_connected_k_dominating_set(G, k): 235 | """Return a smallest connected k-dominating set in the graph. 236 | 237 | The method to compute the set is brute force. 238 | 239 | Parameters 240 | ---------- 241 | G : NetworkX graph 242 | An undirected graph. 243 | 244 | k : int 245 | A positive integer. 246 | 247 | Returns 248 | ------- 249 | list 250 | A list of nodes in a smallest k-dominating set in the graph. 251 | 252 | """ 253 | # check that k is a positive integer 254 | if not float(k).is_integer(): 255 | raise TypeError("Expected k to be an integer.") 256 | k = int(k) 257 | if k < 1: 258 | raise ValueError("Expected k to be a positive integer.") 259 | # Only proceed with search if graph is connected 260 | if not is_connected(G): 261 | return [] 262 | for i in range(1, number_of_nodes(G) + 1): 263 | for S in combinations(nodes(G), i): 264 | if is_connected_k_dominating_set(G, S, k): 265 | return list(S) 266 | 267 | 268 | def min_connected_dominating_set(G, k): 269 | """Return a smallest connected dominating set in the graph. 270 | 271 | The method to compute the set is brute force. 272 | 273 | Parameters 274 | ---------- 275 | G : NetworkX graph 276 | An undirected graph. 277 | 278 | Returns 279 | ------- 280 | list 281 | A list of nodes in a smallest connected dominating set in the 282 | graph. 283 | 284 | """ 285 | return min_connected_k_dominating_set(G, 1) 286 | 287 | 288 | def min_dominating_set_bf(G): 289 | """Return a smallest dominating set in the graph. 290 | 291 | The method to compute the set is brute force except that the subsets 292 | searched begin with those whose cardinality is equal to the 293 | sub-domination number of the graph, which was defined by Amos et al. 294 | and shown to be a tractable lower bound for the k-domination number. 295 | 296 | Parameters 297 | ---------- 298 | G : NetworkX graph 299 | An undirected graph. 300 | 301 | Returns 302 | ------- 303 | list 304 | A list of nodes in a smallest dominating set in the graph. 305 | 306 | See Also 307 | -------- 308 | min_k_dominating_set 309 | 310 | References 311 | ---------- 312 | D. Amos, J. Asplund, B. Brimkov and R. Davila, The sub-k-domination 313 | number of a graph with applications to k-domination, *arXiv preprint 314 | arXiv:1611.02379*, (2016) 315 | 316 | """ 317 | return min_k_dominating_set(G, 1) 318 | 319 | 320 | def min_dominating_set_ilp(G): 321 | """Return a smallest dominating set in the graph. 322 | 323 | A dominating set in a graph *G* is a set *D* of nodes of *G* for 324 | which every node not in *D* has a neighbor in *D*. 325 | 326 | This method using integer programming to compute a smallest 327 | dominating set. It solves the following integer program: minimize 328 | 329 | .. math:: 330 | 331 | \\sum_{v \\in V} x_v 332 | 333 | subject to 334 | 335 | ... math:: 336 | 337 | x_v + \\sum_{u \\in N(v)} x_u \\geq 1 \\mathrm{ for all } v \\in V 338 | 339 | where *V* is the set of nodes of G and *N(v)* is the set of 340 | neighbors of the vertex *v*. 341 | 342 | Parameters 343 | ---------- 344 | G : NetworkX graph 345 | An undirected graph. 346 | 347 | Returns 348 | ------- 349 | set 350 | A set of nodes in a smallest dominating set in the graph. 351 | 352 | See Also 353 | -------- 354 | min_k_dominating_set 355 | 356 | """ 357 | prob = LpProblem("min_total_dominating_set", LpMinimize) 358 | variables = {node: LpVariable("x{}".format(i + 1), 0, 1, LpBinary) for i, node in enumerate(G.nodes())} 359 | 360 | # Set the total domination number objective function 361 | prob += lpSum([variables[n] for n in variables]) 362 | 363 | # Set constraints 364 | for node in G.nodes(): 365 | combination = [variables[n] for n in variables if n in closed_neighborhood(G, node)] 366 | prob += lpSum(combination) >= 1 367 | 368 | prob.solve() 369 | solution_set = {node for node in variables if variables[node].value() == 1} 370 | return solution_set 371 | 372 | 373 | def min_dominating_set(G, method="ilp"): 374 | """Return a smallest dominating set in the graph. 375 | 376 | A dominating set in a graph *G* is a set *D* of nodes of *G* for 377 | which every node not in *D* has a neighbor in *D*. 378 | 379 | Parameters 380 | ---------- 381 | G : NetworkX graph 382 | An undirected graph. 383 | 384 | method: string 385 | The method to use for finding a minimum dominating set. Use 386 | 'ilp' for integer linear program or 'bf' for brute force. 387 | Defaults to 'ilp'. 388 | 389 | Returns 390 | ------- 391 | set 392 | A set of nodes in a smallest dominating set in the graph. 393 | 394 | See Also 395 | -------- 396 | min_k_dominating_set 397 | 398 | """ 399 | dominating_set_func = {"bf": min_dominating_set_bf, "ilp": min_dominating_set_ilp}.get(method, None) 400 | 401 | if dominating_set_func: 402 | return dominating_set_func(G) 403 | 404 | raise ValueError('Invalid `method` arguemnt "{}"'.format(method)) 405 | 406 | 407 | def min_total_dominating_set_bf(G): 408 | """Return a smallest total dominating set in the graph. 409 | 410 | The method to compute the set is brute force except that the subsets 411 | searched begin with those whose cardinality is equal to the 412 | sub-total-domination number of the graph, which was defined by 413 | Davila and shown to be a tractable lower bound for the k-domination 414 | number. 415 | 416 | Parameters 417 | ---------- 418 | G: NetworkX graph 419 | An undirected graph. 420 | 421 | Returns 422 | ------- 423 | list 424 | A list of nodes in a smallest total dominating set in the graph. 425 | 426 | References 427 | ---------- 428 | R. Davila, A note on sub-total domination in graphs. *arXiv preprint 429 | arXiv: 1701.07811*, (2017) 430 | 431 | """ 432 | # use naive lower bound for domination to compute a starting point 433 | # for the search range 434 | rangeMin = sub_total_domination_number(G) 435 | 436 | if number_of_nodes_of_degree_k(G, 0) > 0: 437 | return set() 438 | 439 | for i in range(rangeMin, number_of_nodes(G) + 1): 440 | for S in combinations(nodes(G), i): 441 | if is_total_dominating_set(G, S): 442 | return list(S) 443 | 444 | 445 | def min_total_dominating_set_ilp(G): 446 | """Return a smallest total dominating set in the graph. 447 | 448 | This method solves an integer linear program in order to find a 449 | smallest total dominating set. It solves the following integer 450 | program: minimize 451 | 452 | .. math:: 453 | 454 | \\sum_{v \\in V} x_v 455 | 456 | subject to 457 | 458 | ... math:: 459 | 460 | \\sum_{u \\in N(v)} x_u \\geq 1 \\mathrm{ for all } v \\in V 461 | 462 | where *V* is the set of nodes of G and *N(v)* is the set of 463 | neighbors of the vertex *v*. 464 | 465 | Parameters 466 | ---------- 467 | G: NetworkX graph 468 | An undirected graph. 469 | 470 | Returns 471 | ------- 472 | set 473 | A set of nodes in a smallest total dominating set in the graph. 474 | 475 | References 476 | ---------- 477 | R. Davila, A note on sub-total domination in graphs. *arXiv preprint 478 | arXiv: 1701.07811*, (2017) 479 | 480 | """ 481 | prob = LpProblem("min_total_dominating_set", LpMinimize) 482 | variables = {node: LpVariable("x{}".format(i + 1), 0, 1, LpBinary) for i, node in enumerate(G.nodes())} 483 | 484 | # Set the total domination number objective function 485 | prob += lpSum([variables[n] for n in variables]) 486 | 487 | # Set constraints 488 | for node in G.nodes(): 489 | combination = [variables[n] for n in variables if n in neighborhood(G, node)] 490 | prob += lpSum(combination) >= 1 491 | 492 | prob.solve() 493 | solution_set = {node for node in variables if variables[node].value() == 1} 494 | return solution_set 495 | 496 | 497 | def min_total_dominating_set(G, method="ilp"): 498 | """Return a smallest total dominating set in the graph. 499 | 500 | Parameters 501 | ---------- 502 | G: NetworkX graph 503 | An undirected graph. 504 | 505 | method: string 506 | The method to use for finding a minimum total dominating set. 507 | Use 'ilp' for integer linear program or 'bf' for brute force. 508 | Defaults to 'ilp'. 509 | 510 | Returns 511 | ------- 512 | set 513 | A set of nodes in a smallest total dominating set in the graph. 514 | 515 | References 516 | ---------- 517 | R. Davila, A note on sub-total domination in graphs. *arXiv preprint 518 | arXiv: 1701.07811*, (2017) 519 | 520 | """ 521 | total_dominating_set_func = {"bf": min_total_dominating_set_bf, "ilp": min_total_dominating_set_ilp}.get( 522 | method, None 523 | ) 524 | 525 | if total_dominating_set_func: 526 | return total_dominating_set_func(G) 527 | 528 | raise ValueError('Invalid `method` argument "{}"'.format(method)) 529 | 530 | 531 | def domination_number(G, method="ilp"): 532 | """Return the domination number the graph. 533 | 534 | The * domination number * of a graph is the cardinality of a smallest 535 | dominating set of nodes in the graph. 536 | 537 | This method calls the `min_dominating_set_ip` method in order to 538 | compute a smallest dominating set, then returns the length of that 539 | set. 540 | 541 | Parameters 542 | ---------- 543 | G: NetworkX graph 544 | An undirected graph. 545 | 546 | method: string 547 | The method to use for calculating the domination number. Use 548 | 'ilp' for integer linear program or 'bf' for brute force. 549 | Defaults to 'ilp'. 550 | 551 | Returns 552 | ------- 553 | int 554 | The domination number of the graph. 555 | 556 | See Also 557 | -------- 558 | min_dominating_set, k_domination_number 559 | 560 | """ 561 | try: 562 | return len(min_dominating_set(G, method=method)) 563 | except ValueError: 564 | raise 565 | 566 | 567 | def k_domination_number(G, k): 568 | """Return the k-domination number the graph. 569 | 570 | The * k-domination number * of a graph is the cardinality of a 571 | smallest k-dominating set of nodes in the graph. 572 | 573 | The method to compute this number is modified brute force. 574 | 575 | Parameters 576 | ---------- 577 | G: NetworkX graph 578 | An undirected graph. 579 | 580 | Returns 581 | ------- 582 | int 583 | The k-domination number of the graph. 584 | 585 | See Also 586 | -------- 587 | min_k_dominating_set, domination_number 588 | 589 | """ 590 | # check that k is a positive integer 591 | if not float(k).is_integer(): 592 | raise TypeError("Expected k to be an integer.") 593 | k = int(k) 594 | if k < 1: 595 | raise ValueError("Expected k to be a positive integer.") 596 | return len(min_k_dominating_set(G, k)) 597 | 598 | 599 | def connected_k_domination_number(G, k): 600 | """Return the connected k-domination number the graph. 601 | 602 | The * connected k-domination number * of a graph is the cardinality 603 | of a smallest k-dominating set of nodes in the graph that induces a 604 | connected subgraph. 605 | 606 | The method to compute this number is brute force. 607 | 608 | Parameters 609 | ---------- 610 | G: NetworkX graph 611 | An undirected graph. 612 | 613 | Returns 614 | ------- 615 | int 616 | The connected k-domination number of the graph. 617 | 618 | """ 619 | # check that k is a positive integer 620 | if not float(k).is_integer(): 621 | raise TypeError("Expected k to be an integer.") 622 | k = int(k) 623 | if k < 1: 624 | raise ValueError("Expected k to be a positive integer.") 625 | return len(min_connected_k_dominating_set(G, k)) 626 | 627 | 628 | def connected_domination_number(G): 629 | """Return the connected domination number the graph. 630 | 631 | The * connected domination number * of a graph is the cardinality of a 632 | smallest dominating set of nodes in the graph that induces a 633 | connected subgraph. 634 | 635 | The method to compute this number is brute force. 636 | 637 | Parameters 638 | ---------- 639 | G: NetworkX graph 640 | An undirected graph. 641 | 642 | Returns 643 | ------- 644 | int 645 | The connected domination number of the graph. 646 | 647 | """ 648 | return connected_k_domination_number(G, 1) 649 | 650 | 651 | def total_domination_number(G, method="ilp"): 652 | """Return the total domination number the graph. 653 | 654 | The * total domination number * of a graph is the cardinality of a 655 | smallest total dominating set of nodes in the graph. 656 | 657 | The method to compute this number is modified brute force. 658 | 659 | Parameters 660 | ---------- 661 | G: NetworkX graph 662 | An undirected graph. 663 | 664 | method: string 665 | The method to use for calulating the total domination number. 666 | Use 'ilp' for integer linear program or 'bf' for brute force. 667 | Defaults to 'ilp'. 668 | 669 | Returns 670 | ------- 671 | int 672 | The total domination number of the graph. 673 | 674 | """ 675 | try: 676 | return len(min_total_dominating_set(G, method=method)) 677 | except ValueError as exc: 678 | raise ValueError(exc) 679 | 680 | 681 | def is_independent_k_dominating_set(G, nodes, k): 682 | """Return whether or not *nodes * is an independent k-dominating set in *G. 683 | 684 | Parameters 685 | ---------- 686 | G: NetworkX graph 687 | An undirected graph. 688 | 689 | nodes: list, set 690 | An iterable container of nodes in G. 691 | 692 | k: int 693 | A positive integer. 694 | 695 | Returns 696 | ------- 697 | boolean 698 | True if the nodes in nbunch comprise an independent k-dominating 699 | set, and False otherwise. 700 | 701 | """ 702 | return is_k_dominating_set(G, nodes, k) and is_independent_set(G, nodes) 703 | 704 | 705 | def is_independent_dominating_set(G, nodes): 706 | """Return whether or not *nodes * is an independent k-dominating set in *G*. 707 | 708 | Parameters 709 | ---------- 710 | G: NetworkX graph 711 | An undirected graph. 712 | 713 | nodes: list, set 714 | An iterable container of nodes in G. 715 | 716 | Returns 717 | ------- 718 | boolean 719 | True if the nodes in nbunch comprise an independent dominating 720 | set, and False otherwise. 721 | 722 | """ 723 | return is_k_dominating_set(G, nodes, 1) and is_independent_set(G, nodes) 724 | 725 | 726 | def min_independent_k_dominating_set(G, k): 727 | """Return a smallest independent k-dominating set in the graph. 728 | 729 | The method to compute the set is brute force. 730 | 731 | Parameters 732 | ---------- 733 | G: NetworkX graph 734 | An undirected graph. 735 | 736 | Returns 737 | ------- 738 | list 739 | A list of nodes in a smallest independent k-dominating set in 740 | the graph. 741 | 742 | """ 743 | # loop through subsets of nodes of G in increasing order of size until a 744 | # total dominating set is found 745 | for i in range(1, number_of_nodes(G) + 1): 746 | for S in combinations(nodes(G), i): 747 | if is_independent_k_dominating_set(G, S, k): 748 | return list(S) 749 | 750 | 751 | def min_independent_dominating_set_bf(G): 752 | """Return a smallest independent dominating set in the graph. 753 | 754 | The method to compute the set is brute force. 755 | 756 | Parameters 757 | ---------- 758 | G: NetworkX graph 759 | An undirected graph. 760 | 761 | Returns 762 | ------- 763 | list 764 | A list of nodes in a smallest independent dominating set in the 765 | graph. 766 | 767 | """ 768 | return min_independent_k_dominating_set(G, 1) 769 | 770 | 771 | def min_independent_dominating_set_ilp(G): 772 | """Return a smallest independent dominating set in the graph. 773 | 774 | This method solves an integer program to compute a smallest 775 | independent dominating set. It solves the following integer program: 776 | minimize 777 | 778 | .. math:: 779 | 780 | \\sum_{v \\in V} x_v 781 | 782 | subject to 783 | 784 | ... math:: 785 | 786 | x_v + \\sum_{u \\in N(v)} x_u \\geq 1 \\mathrm{ for all } v \\in V 787 | \\sum_{\\{u, v\\} \\in E} x_u + x_v \\leq 1 \\mathrm{ for all } e \\in E 788 | 789 | where *E* and *V* are the set of edges and nodes of G, and *N(v)* is 790 | the set of neighbors of the vertex *v*. 791 | 792 | Parameters 793 | ---------- 794 | G: NetworkX graph 795 | An undirected graph. 796 | 797 | Returns 798 | ------- 799 | set 800 | A set of nodes in a smallest independent dominating set in the 801 | graph. 802 | 803 | """ 804 | prob = LpProblem("min_total_dominating_set", LpMinimize) 805 | variables = {node: LpVariable("x{}".format(i + 1), 0, 1, LpBinary) for i, node in enumerate(G.nodes())} 806 | 807 | # Set the domination number objective function 808 | prob += lpSum(variables) 809 | 810 | # Set constraints for domination 811 | for node in G.nodes(): 812 | combination = [variables[n] for n in variables if n in closed_neighborhood(G, node)] 813 | prob += lpSum(combination) >= 1 814 | 815 | # Set constraints for independence 816 | for e in G.edges(): 817 | prob += variables[e[0]] + variables[e[1]] <= 1 818 | 819 | prob.solve() 820 | solution_set = {node for node in variables if variables[node].value() == 1} 821 | return solution_set 822 | 823 | 824 | def min_independent_dominating_set(G, method="ilp"): 825 | """Return a smallest independent dominating set in the graph. 826 | 827 | Parameters 828 | ---------- 829 | G: NetworkX graph 830 | An undirected graph. 831 | 832 | method: string 833 | The method to use for finding a smallest independent dominating 834 | set. Use 'ilp' for integer linear program or 'bf' for brute 835 | force. Defaults to 'ilp'. 836 | 837 | Returns 838 | ------- 839 | set 840 | A set of nodes in a smallest independent dominating set in the 841 | graph. 842 | 843 | """ 844 | independent_dominating_set_func = { 845 | "bf": min_independent_dominating_set_bf, 846 | "ilp": min_independent_dominating_set_ilp, 847 | }.get(method, None) 848 | 849 | if independent_dominating_set_func: 850 | return independent_dominating_set_func(G) 851 | 852 | raise ValueError('Invalid `method` argument "{}"'.format(method)) 853 | 854 | 855 | def independent_k_domination_number(G, k): 856 | """Return the independnet k-domination number the graph. 857 | 858 | The * independent k-domination number * of a graph is the cardinality 859 | of a smallest independent k-dominating set of nodes in the graph. 860 | 861 | The method to compute this number is brute force. 862 | 863 | Parameters 864 | ---------- 865 | G: NetworkX graph 866 | An undirected graph. 867 | 868 | Returns 869 | ------- 870 | int 871 | The independent k-domination number of the graph. 872 | 873 | """ 874 | return len(min_independent_k_dominating_set(G, k)) 875 | 876 | 877 | def independent_domination_number(G, method="ilp"): 878 | """Return the independnet domination number the graph. 879 | 880 | The * independent domination number * of a graph is the cardinality of 881 | a smallest independent dominating set of nodes in the graph. 882 | 883 | Parameters 884 | ---------- 885 | G: NetworkX graph 886 | An undirected graph. 887 | 888 | method: string 889 | The method to use for calculating the independent dominationg 890 | number. Use 'ilp' for integer linear program or 'bf' for brute 891 | force. Defaults to 'ilp'. 892 | 893 | Returns 894 | ------- 895 | int 896 | The independent domination number of the graph. 897 | 898 | """ 899 | try: 900 | return len(min_independent_dominating_set(G, method=method)) 901 | except ValueError: 902 | raise 903 | -------------------------------------------------------------------------------- /grinpy/invariants/dsi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing DSI style invariants.""" 11 | 12 | 13 | from grinpy import degree_sequence, number_of_edges 14 | 15 | __all__ = ["sub_k_domination_number", "slater", "sub_total_domination_number", "annihilation_number"] 16 | 17 | 18 | # methods 19 | def sub_k_domination_number(G, k): 20 | r"""Return the sub-k-domination number of the graph. 21 | 22 | The *sub-k-domination number* of a graph G with *n* nodes is defined as the 23 | smallest positive integer t such that the following relation holds: 24 | 25 | .. math:: 26 | t + \frac{1}{k}\sum_{i=0}^t d_i \geq n 27 | 28 | where 29 | 30 | .. math:: 31 | {d_1 \geq d_2 \geq \cdots \geq d_n} 32 | 33 | is the degree sequence of the graph. 34 | 35 | Parameters 36 | ---------- 37 | G : NetworkX graph 38 | An undirected graph. 39 | 40 | k : int 41 | A positive integer. 42 | 43 | Returns 44 | ------- 45 | int 46 | The sub-k-domination number of a graph. 47 | 48 | See Also 49 | -------- 50 | slater 51 | 52 | Examples 53 | -------- 54 | >>> G = nx.cycle_graph(4) 55 | >>> nx.sub_k_domination_number(G, 1) 56 | True 57 | 58 | References 59 | ---------- 60 | D. Amos, J. Asplund, B. Brimkov and R. Davila, The sub-k-domination number 61 | of a graph with applications to k-domination, *arXiv preprint 62 | arXiv:1611.02379*, (2016) 63 | """ 64 | # check that k is a positive integer 65 | if not float(k).is_integer(): 66 | raise TypeError("Expected k to be an integer.") 67 | k = int(k) 68 | if k < 1: 69 | raise ValueError("Expected k to be a positive integer.") 70 | D = degree_sequence(G) 71 | D.sort(reverse=True) 72 | n = len(D) 73 | for i in range(n + 1): 74 | if i + (sum(D[:i]) / k) >= n: 75 | return i 76 | # if above loop completes, return None 77 | return None 78 | 79 | 80 | def slater(G): 81 | r"""Return the Slater invariant for the graph. 82 | 83 | The Slater invariant of a graph G is a lower bound for the domination 84 | number of a graph defined by: 85 | 86 | .. math:: 87 | sl(G) = \min\{t : t + \sum_{i=0}^t d_i \geq n\} 88 | 89 | where 90 | 91 | .. math:: 92 | {d_1 \geq d_2 \geq \cdots \geq d_n} 93 | 94 | is the degree sequence of the graph ordered in non-increasing order and *n* 95 | is the order of G. 96 | 97 | Amos et al. rediscovered this invariant and generalized it into what is 98 | now known as the sub-*k*-domination number. 99 | 100 | Parameters 101 | ---------- 102 | G : NetworkX graph 103 | An undirected graph. 104 | 105 | Returns 106 | ------- 107 | int 108 | The Slater invariant for the graph. 109 | 110 | See Also 111 | -------- 112 | sub_k_domination_number 113 | 114 | References 115 | ---------- 116 | D. Amos, J. Asplund, B. Brimkov and R. Davila, The sub-k-domination number 117 | of a graph with applications to k-domination, *arXiv preprint 118 | arXiv:1611.02379*, (2016) 119 | 120 | P.J. Slater, Locating dominating sets and locating-dominating set, *Graph 121 | Theory, Combinatorics and Applications: Proceedings of the 7th Quadrennial 122 | International Conference on the Theory and Applications of Graphs*, 123 | 2: 2073-1079 (1995) 124 | """ 125 | return sub_k_domination_number(G, 1) 126 | 127 | 128 | def sub_total_domination_number(G): 129 | r"""Return the sub-total domination number of the graph. 130 | 131 | The sub-total domination number is defined as: 132 | 133 | .. math:: 134 | sub_{t}(G) = \min\{t : \sum_{i=0}^t d_i \geq n\} 135 | 136 | where 137 | 138 | .. math:: 139 | {d_1 \geq d_2 \geq \cdots \geq d_n} 140 | 141 | is the degree sequence of the graph ordered in non-increasing order and *n* 142 | is the order of the graph. 143 | 144 | This invariant was defined and investigated by Randy Davila. 145 | 146 | Parameters 147 | ---------- 148 | G : NetworkX graph 149 | An undirected graph. 150 | 151 | Returns 152 | ------- 153 | int 154 | The sub-total domination number of the graph. 155 | 156 | References 157 | ---------- 158 | R. Davila, A note on sub-total domination in graphs. *arXiv preprint 159 | arXiv:1701.07811*, (2017) 160 | """ 161 | D = degree_sequence(G) 162 | D.sort(reverse=True) 163 | n = len(D) 164 | for i in range(n + 1): 165 | if sum(D[:i]) >= n: 166 | return i 167 | # if above loop completes, return None 168 | return None 169 | 170 | 171 | def annihilation_number(G): 172 | r"""Return the annihilation number of the graph. 173 | 174 | The annihilation number of a graph G is defined as: 175 | 176 | .. math:: 177 | a(G) = \max\{t : \sum_{i=0}^t d_i \leq m\} 178 | 179 | where 180 | 181 | .. math:: 182 | {d_1 \leq d_2 \leq \cdots \leq d_n} 183 | 184 | is the degree sequence of the graph ordered in non-decreasing order and *m* 185 | is the number of edges in G. 186 | 187 | Parameters 188 | ---------- 189 | G : NetworkX graph 190 | An undirected graph. 191 | 192 | Returns 193 | ------- 194 | int 195 | The annihilation number of the graph. 196 | """ 197 | D = degree_sequence(G) 198 | D.sort() # sort in non-decreasing order 199 | n = len(D) 200 | m = number_of_edges(G) 201 | # sum over degrees in the sequence until the sum is larger than the number of edges in the graph 202 | for i in reversed(range(n + 1)): 203 | if sum(D[:i]) <= m: 204 | return i 205 | -------------------------------------------------------------------------------- /grinpy/invariants/independence.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing independence related invariants for a graph.""" 11 | 12 | # imports 13 | from itertools import combinations 14 | 15 | from pulp import LpBinary, LpMaximize, LpProblem, LpVariable, lpSum 16 | 17 | from grinpy import neighborhood, nodes, number_of_nodes, set_neighborhood 18 | from grinpy.invariants.dsi import annihilation_number 19 | 20 | __all__ = [ 21 | "independence_number", 22 | "is_independent_set", 23 | "is_k_independent_set", 24 | "k_independence_number", 25 | "max_independent_set", 26 | "max_k_independent_set", 27 | ] 28 | 29 | 30 | # methods 31 | def is_independent_set(G, nodes): 32 | """Return whether or not the *nodes* comprises an independent set. 33 | 34 | An set *S* of nodes in *G* is called an *independent set* if no two 35 | nodes in S are neighbors of one another. 36 | 37 | Parameters 38 | ---------- 39 | G : NetworkX graph 40 | An undirected graph. 41 | 42 | nodes : list, set 43 | An iterable container of nodes in G. Only nodes existing in G 44 | will be considered. Any other nodes will be ignored. 45 | 46 | Returns 47 | ------- 48 | bool 49 | True if the the nodes in *nodes* comprise an independent set, 50 | False otherwise. 51 | 52 | See Also 53 | -------- 54 | is_k_independent_set 55 | 56 | """ 57 | S = set(n for n in nodes if n in G) 58 | return set(set_neighborhood(G, S)).intersection(S) == set() 59 | 60 | 61 | def is_k_independent_set(G, nodes, k): 62 | """Return whether or not `nodes` is a k-independent set in G. 63 | 64 | A set *S* of nodes in *G* is called a *k-independent set* it every 65 | node in S has at most *k*-1 neighbors in S. Notice that a 66 | 1-independent set is equivalent to an independent set. 67 | 68 | Parameters 69 | ---------- 70 | G : NetworkX graph 71 | An undirected graph. 72 | 73 | nodes : list, set 74 | An iterable container of nodes in G. 75 | 76 | k : int 77 | A positive integer. 78 | 79 | Returns 80 | ------- 81 | bool 82 | True if the nodes in *nodes* comprise a k-independent set, False 83 | otherwise. 84 | 85 | See Also 86 | -------- 87 | is_independent_set 88 | 89 | """ 90 | if k == 1: 91 | return is_independent_set(G, nodes) 92 | else: 93 | S = set(n for n in nodes if n in G) 94 | for v in S: 95 | N = set(neighborhood(G, v)) 96 | if len(N.intersection(S)) >= k: 97 | return False 98 | return True 99 | 100 | 101 | def max_k_independent_set(G, k): 102 | """Return a largest k-independent set of nodes in *G*. 103 | 104 | The method used is brute force, except when *k*=1. In this case, 105 | the search starts with subsets of *G* with cardinality equal to the 106 | annihilation number of *G*, which was shown by Pepper to be an upper 107 | bound for the independence number of a graph, and then continues 108 | checking smaller subsets until a maximum independent set is found. 109 | 110 | Parameters 111 | ---------- 112 | G : NetworkX graph 113 | An undirected graph. 114 | 115 | k : int 116 | A positive integer. 117 | 118 | Returns 119 | ------- 120 | list 121 | A list of nodes comprising a largest k-independent set in *G*. 122 | 123 | See Also 124 | -------- 125 | max_independent_set 126 | 127 | """ 128 | # set the maximum for the loop range 129 | rangeMax = number_of_nodes(G) + 1 130 | if k == 1: 131 | rangeMax = annihilation_number(G) + 1 132 | 133 | # loop through subsets of nodes of G in decreasing order of size 134 | # until a k-independent set is found 135 | for i in reversed(range(rangeMax)): 136 | for S in combinations(nodes(G), i): 137 | if is_k_independent_set(G, S, k): 138 | return set(S) 139 | 140 | 141 | def max_independent_set_bf(G): 142 | """Return a largest independent set of nodes in *G*. 143 | 144 | The method used is a modified brute force search. The search starts 145 | with subsets of *G* with cardinality equal to the annihilation 146 | number of *G*, which was shown by Pepper to be an upper bound for 147 | the independence number of a graph, and then continues checking 148 | smaller subsets until a maximum independent set is found. 149 | 150 | Parameters 151 | ---------- 152 | G : NetworkX graph 153 | An undirected graph. 154 | 155 | Returns 156 | ------- 157 | list 158 | A list of nodes comprising a largest independent set in *G*. 159 | 160 | See Also 161 | -------- 162 | annihilation_number, max_independent_set, max_k_independent_set 163 | 164 | """ 165 | return max_k_independent_set(G, 1) 166 | 167 | 168 | def max_independent_set_ilp(G): 169 | """Return a largest independent set of nodes in *G*. 170 | 171 | This method uses integer programming to solve for a largest 172 | independent set. It solves the following integer program: 173 | maximize 174 | 175 | .. math:: 176 | 177 | \\sum_{v \\in V} x_v 178 | 179 | subject to 180 | 181 | ... math:: 182 | 183 | \\sum_{\\{u, v\\} \\in E} x_u + x_v \\leq 1 \\mathrm{ for all } e \\in E 184 | 185 | where *E* and *V* are the set of edges and nodes of G, and *N(v)* is 186 | the set of neighbors of the vertex *v*. 187 | 188 | Parameters 189 | ---------- 190 | G : NetworkX graph 191 | An undirected graph. 192 | 193 | Returns 194 | ------- 195 | set 196 | A set of nodes comprising a largest independent set in *G*. 197 | 198 | See Also 199 | -------- 200 | max_k_independent_set 201 | 202 | """ 203 | prob = LpProblem("min_total_dominating_set", LpMaximize) 204 | variables = {node: LpVariable("x{}".format(i + 1), 0, 1, LpBinary) for i, node in enumerate(G.nodes())} 205 | 206 | # Set the domination number objective function 207 | prob += lpSum(variables) 208 | 209 | # Set constraints for independence 210 | for e in G.edges(): 211 | prob += variables[e[0]] + variables[e[1]] <= 1 212 | 213 | prob.solve() 214 | solution_set = {node for node in variables if variables[node].value() == 1} 215 | return solution_set 216 | 217 | 218 | def max_independent_set(G, method="ilp"): 219 | """Return a largest independent set of nodes in *G*. 220 | 221 | Parameters 222 | ---------- 223 | G : NetworkX graph 224 | An undirected graph. 225 | 226 | method: string 227 | The method to use for computing the independence number. Use 228 | 'ilp' for integer linear program or 'bf' for brute force. 229 | Defaults to 'ilp'. 230 | 231 | Returns 232 | ------- 233 | set 234 | A set of nodes comprising a largest independent set in *G*. 235 | 236 | See Also 237 | -------- 238 | max_independent_set_bf, max_independent_set_ilp, 239 | max_k_independent_set 240 | 241 | """ 242 | independent_set_func = {"ilp": max_independent_set_ilp, "bf": max_independent_set_bf}.get(method, None) 243 | 244 | if independent_set_func: 245 | return independent_set_func(G) 246 | 247 | raise ValueError('Invalid `method` argument "{}".'.format(method)) 248 | 249 | 250 | def independence_number(G, method="ilp"): 251 | """Return the independence number of G. 252 | 253 | The *independence number* of a graph is the cardinality of a largest 254 | independent set of nodes in the graph. 255 | 256 | Parameters 257 | ---------- 258 | G : NetworkX graph 259 | An undirected graph. 260 | 261 | method: string 262 | The method to use for computing the independence number. Use 263 | 'ilp' for integer linear program or 'bf' for brute force. 264 | Defaults to 'ilp'. 265 | 266 | Returns 267 | ------- 268 | int 269 | The independence number of *G*. 270 | 271 | See Also 272 | -------- 273 | k_independence_number 274 | 275 | """ 276 | try: 277 | return len(max_independent_set(G, method=method)) 278 | except ValueError: 279 | raise 280 | 281 | 282 | def k_independence_number(G, k): 283 | """Return a the k-independence number of G. 284 | 285 | The *k-independence number* of a graph is the cardinality of a 286 | largest k-independent set of nodes in the graph. 287 | 288 | Parameters 289 | ---------- 290 | G : NetworkX graph 291 | An undirected graph. 292 | 293 | k : int 294 | A positive integer. 295 | 296 | Returns 297 | ------- 298 | int 299 | The k-independence number of *G*. 300 | 301 | See Also 302 | -------- 303 | independence_number 304 | 305 | """ 306 | return len(max_k_independent_set(G, k)) 307 | -------------------------------------------------------------------------------- /grinpy/invariants/matching.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing matching related invariants for a graph.""" 11 | 12 | from itertools import combinations 13 | 14 | from pulp import LpBinary, LpMaximize, LpProblem, lpSum, LpVariable 15 | 16 | from grinpy import edges, is_matching, is_maximal_matching, number_of_edges 17 | 18 | __all__ = ["matching_number", "min_maximal_matching", "min_maximal_matching_number"] 19 | 20 | 21 | def max_matching_bf(G): 22 | """Return a maximum matching in G. 23 | 24 | A *maximum matching* is a largest set of edges such that no two 25 | edges in the set have a common endpoint. 26 | 27 | Parameters 28 | ---------- 29 | G : NetworkX graph 30 | An undirected graph. 31 | 32 | Returns 33 | ------- 34 | set 35 | A set of edges in a maximum matching. 36 | 37 | """ 38 | if number_of_edges(G) == 0: 39 | return set() 40 | 41 | for i in reversed(range(1, number_of_edges(G) + 1)): 42 | for S in combinations(edges(G), i): 43 | if is_matching(G, set(S)): 44 | return set(S) 45 | 46 | 47 | def max_matching_ilp(G): 48 | """Return a largest matching in *G*. 49 | 50 | This method uses integer programming to solve for a maximum 51 | matching. It solves the following integer program: maximize 52 | 53 | .. math:: 54 | 55 | \\sum_{e \\in E} x_e 56 | 57 | subject to 58 | 59 | ... math:: 60 | 61 | \\sum_{e \\sim u} x_e \\leq 1 \\mathrm{ for all } u \\in V 62 | 63 | where *E* and *V* are the set of edges and nodes of G, and *e* ~ *u* 64 | denotes "*e* is incident to *u*." 65 | 66 | Parameters 67 | ---------- 68 | G : NetworkX graph 69 | An undirected graph. 70 | 71 | Returns 72 | ------- 73 | set 74 | A set of edges comprising a maximum matching in *G*. 75 | 76 | See Also 77 | -------- 78 | max_matching 79 | 80 | """ 81 | prob = LpProblem("min_total_dominating_set", LpMaximize) 82 | variables = {edge: LpVariable("x{}".format(i + 1), 0, 1, LpBinary) for i, edge in enumerate(G.edges())} 83 | 84 | # Set the maximum matching objective function 85 | prob += lpSum(variables) 86 | 87 | # Set constraints 88 | for node in G.nodes(): 89 | incident_edges = [variables[edge] for edge in variables if node in edge] 90 | prob += sum(incident_edges) <= 1 91 | 92 | prob.solve() 93 | solution_set = {edge for edge in variables if variables[edge].value() == 1} 94 | return solution_set 95 | 96 | 97 | def max_matching(G, method="ilp"): 98 | """Return a largest matching in *G*. 99 | 100 | Parameters 101 | ---------- 102 | G : NetworkX graph 103 | An undirected graph. 104 | 105 | method: string 106 | The method to use for finding the maximum matching. Use 107 | 'ilp' for integer linear program or 'bf' for brute force. 108 | Defaults to 'ilp'. 109 | 110 | Returns 111 | ------- 112 | set 113 | A set of edges comprising a maximum matching in *G*. 114 | 115 | See Also 116 | -------- 117 | max_matching 118 | 119 | """ 120 | max_matching_func = {"bf": max_matching_bf, "ilp": max_matching_ilp}.get(method, None) 121 | 122 | if max_matching_func: 123 | return max_matching_func(G) 124 | 125 | raise ValueError('Invalid `method` argument "{}"'.format(method)) 126 | 127 | 128 | def matching_number(G, method="ilp"): 129 | """Return the matching number of G. 130 | 131 | The *matching number* of a graph G is the cardinality of a maximum 132 | matching in G. 133 | 134 | Parameters 135 | ---------- 136 | G : NetworkX graph 137 | An undirected graph. 138 | 139 | Returns 140 | ------- 141 | int 142 | The matching number of G. 143 | 144 | """ 145 | try: 146 | return len(max_matching(G, method)) 147 | except ValueError: 148 | raise 149 | 150 | 151 | def min_maximal_matching(G): 152 | """Return a smallest maximal matching in G. 153 | 154 | A *maximal matching* is a maximal set of edges such that no two 155 | edges in the set have a common endpoint. 156 | 157 | Parameters 158 | ---------- 159 | G : NetworkX graph 160 | An undirected graph. 161 | 162 | Returns 163 | ------- 164 | set 165 | A set of edges in a smallest maximal matching. 166 | 167 | """ 168 | if number_of_edges(G) == 0: 169 | return set() 170 | 171 | for i in range(1, number_of_edges(G) + 1): 172 | for S in combinations(edges(G), i): 173 | if is_maximal_matching(G, set(S)): 174 | return set(S) 175 | 176 | 177 | def min_maximal_matching_number(G): 178 | """Return the minimum maximal matching number of G. 179 | 180 | The *minimum maximal matching number* of a graph G is the 181 | cardinality of a smallest maximal matching in G. 182 | 183 | Parameters 184 | ---------- 185 | G : NetworkX graph 186 | An undirected graph. 187 | 188 | Returns 189 | ------- 190 | int 191 | The minimum maximal matching number of G. 192 | 193 | """ 194 | return len(min_maximal_matching(G)) 195 | -------------------------------------------------------------------------------- /grinpy/invariants/power_domination.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing power domination related invariants of a graph.""" 11 | 12 | from grinpy import set_closed_neighborhood 13 | from grinpy.invariants.zero_forcing import is_k_forcing_set 14 | from itertools import combinations 15 | 16 | __all__ = [ 17 | "is_k_power_dominating_set", 18 | "min_k_power_dominating_set", 19 | "k_power_domination_number", 20 | "is_power_dominating_set", 21 | "min_power_dominating_set", 22 | "power_domination_number", 23 | ] 24 | 25 | 26 | def is_k_power_dominating_set(G, nodes, k): 27 | """Return whether or not the nodes in `nodes` comprise a k-power dominating 28 | set. 29 | 30 | Parameters 31 | ---------- 32 | G : NetworkX graph 33 | An undirected graph. 34 | 35 | nodes : list, set 36 | An iterable container of nodes in G. 37 | 38 | k : int 39 | A positive integer. 40 | 41 | Returns 42 | ------- 43 | boolean 44 | True if the nodes in `nodes` comprise a k-power dominating set, False 45 | otherwise. 46 | """ 47 | return is_k_forcing_set(G, set_closed_neighborhood(G, nodes), k) 48 | 49 | 50 | def min_k_power_dominating_set(G, k): 51 | """Return a smallest k-power dominating set of nodes in *G*. 52 | 53 | The method used to compute the set is brute force. 54 | 55 | Parameters 56 | ---------- 57 | G : NetworkX graph 58 | An undirected graph. 59 | 60 | Returns 61 | ------- 62 | list 63 | A list of nodes in a smallest k-power dominating set in *G*. 64 | """ 65 | for i in range(1, G.order() + 1): 66 | for S in combinations(G.nodes(), i): 67 | if is_k_power_dominating_set(G, S, k): 68 | return list(S) 69 | 70 | 71 | def k_power_domination_number(G, k): 72 | """Return the k-power domination number of *G*. 73 | 74 | Parameters 75 | ---------- 76 | G : NetworkX graph 77 | An undirected graph. 78 | 79 | Returns 80 | ------- 81 | int 82 | The k-power domination number of *G*. 83 | """ 84 | return len(min_k_power_dominating_set(G, k)) 85 | 86 | 87 | def is_power_dominating_set(G, nodes): 88 | """Return whether or not the nodes in `nodes` comprise a power dominating 89 | set. 90 | 91 | Parameters 92 | ---------- 93 | G : NetworkX graph 94 | An undirected graph. 95 | 96 | nodes : list, set 97 | An iterable container of nodes in G. 98 | 99 | Returns 100 | ------- 101 | boolean 102 | True if the nodes in `nodes` comprise a power dominating set, False 103 | otherwise. 104 | """ 105 | return is_k_power_dominating_set(G, nodes, 1) 106 | 107 | 108 | def min_power_dominating_set(G): 109 | """Return a smallest power dominating set of nodes in *G*. 110 | 111 | The method used to compute the set is brute force. 112 | 113 | Parameters 114 | ---------- 115 | G : NetworkX graph 116 | An undirected graph. 117 | 118 | Returns 119 | ------- 120 | list 121 | A list of nodes in a smallest power dominating set in *G*. 122 | """ 123 | return min_k_power_dominating_set(G, 1) 124 | 125 | 126 | def power_domination_number(G): 127 | """Return the power domination number of *G*. 128 | 129 | Parameters 130 | ---------- 131 | G : NetworkX graph 132 | An undirected graph. 133 | 134 | Returns 135 | ------- 136 | int 137 | The power domination number of *G*. 138 | """ 139 | return k_power_domination_number(G, 1) 140 | -------------------------------------------------------------------------------- /grinpy/invariants/residue.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing the residue and related invariants.""" 11 | 12 | from grinpy import havel_hakimi_process, elimination_sequence 13 | from grinpy.invariants.independence import independence_number 14 | 15 | __all__ = ["residue", "k_residue", "k_residual_index"] 16 | 17 | 18 | def residue(G): 19 | """Return the *residue* of *G*. 20 | 21 | The *residue* of a graph *G* is the number of zeros obtained in final 22 | sequence of the Havel Hakimi process. 23 | 24 | Parameters 25 | ---------- 26 | G : NetworkX graph 27 | An undirected graph. 28 | 29 | Returns 30 | ------- 31 | int 32 | The residue of *G*. 33 | 34 | See Also 35 | -------- 36 | k_residue, havel_hakimi_process 37 | """ 38 | return havel_hakimi_process(G).residue() 39 | 40 | 41 | def k_residue(G, k): 42 | r"""Return the *k-residue* of *G*. 43 | 44 | The *k-residue* of a graph *G* is defined as follows: 45 | 46 | .. math:: 47 | \frac{1}{k}\sum_{i=0}^{k-1}(k - i)f(i) 48 | 49 | where *f(i)* is the frequency of *i* in the elmination sequence of the 50 | graph. The elimination sequence is the sequence of deletions made during the 51 | Havel Hakimi process together with the zeros obtained in the final step. 52 | 53 | Parameters 54 | ---------- 55 | G : NetworkX graph 56 | An undirected graph. 57 | 58 | Returns 59 | ------- 60 | float 61 | The k-residue of *G*. 62 | 63 | See Also 64 | -------- 65 | residue, havel_hakimi_process, elimination_sequence 66 | """ 67 | E = elimination_sequence(G) 68 | return sum((k - i) * E.count(i) for i in range(k)) / k 69 | 70 | 71 | def k_residual_index(G): 72 | """Return the k-residual_index of G. 73 | 74 | The *k-residual index* of a graph *G* is the smallest integer k such that 75 | the k-residue of *G* is greathe than or equal to the independence number of 76 | *G*. 77 | 78 | Parameters 79 | ---------- 80 | G : NetworkX graph 81 | An undirected graph. 82 | 83 | Returns 84 | ------- 85 | int 86 | The smallest integer k such that k_residue(G,k) >= independence_number(G). 87 | 88 | See Also 89 | -------- 90 | k_independence_number, k_residue 91 | 92 | Notes 93 | ----- 94 | It should be noted that the k-residual index was originally conjectured to 95 | be an upper bound on the independence number by Siemion Faijtlowizc and 96 | his original conjecturing program Graffiti. This was told to Davila by 97 | personal communication with Ryan Pepper, a former PhD student of 98 | Faijtlowicz. 99 | """ 100 | k = 1 101 | while k_residue(G, k) < independence_number(G): 102 | k += 1 103 | return k 104 | -------------------------------------------------------------------------------- /grinpy/invariants/topological_indices.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing the topological indicies of a graph. 11 | 12 | Many of these indicies were developed in relation to chemical graph theory, and 13 | some have been related to quantum theory. 14 | """ 15 | 16 | import functools 17 | import math 18 | 19 | import networkx as nx 20 | 21 | __all__ = [ 22 | "randic_index", 23 | "generalized_randic_index", 24 | "augmented_randic_index", 25 | "harmonic_index", 26 | "atom_bond_connectivity_index", 27 | "sum_connectivity_index", 28 | "first_zagreb_index", 29 | "second_zagreb_index", 30 | ] 31 | 32 | 33 | def _topological_index(G, func): 34 | """Return the topological index of ``G`` determined by ``func``""" 35 | 36 | return math.fsum(func(*edge) for edge in G.edges()) 37 | 38 | 39 | def randic_index(G): 40 | r"""Returns the Randić Index of the graph ``G``. 41 | 42 | The *Randić index* of a graph *G* with edge set *E* is defined as the 43 | following sum: 44 | 45 | .. math:: 46 | \sum_{vw \in E} \frac{1}{\sqrt{d_G(v) \times d_G(w)}} 47 | 48 | Parameters 49 | ---------- 50 | G : NetworkX graph 51 | An undirected graph. 52 | 53 | Returns 54 | ------- 55 | float 56 | The Randić Index of a ``G``. 57 | 58 | References 59 | ---------- 60 | 61 | Ivan Gutman, Degree-Based Topological Indices, Croat. Chem. Acta 86 (4) 62 | (2013) 351–361. http://dx.doi.org/10.5562/cca2294 63 | """ 64 | _degree = functools.partial(nx.degree, G) 65 | return _topological_index(G, func=lambda x, y: 1 / math.sqrt(_degree(x) * _degree(y))) 66 | 67 | 68 | def generalized_randic_index(G, k): 69 | r"""Returns the generalized Randić Index of the graph ``G``. 70 | 71 | The *generalized Randić index* of a graph *G* with edge set *E* is defined as the 72 | following sum: 73 | 74 | .. math:: 75 | \sum_{vw \in E} \big(d_G(v) \times d_G(w)\big)^{k} 76 | 77 | where `k` is some real number. 78 | 79 | Parameters 80 | ---------- 81 | G : NetworkX graph 82 | An undirected graph. 83 | 84 | k : float 85 | Exponent parameter for generalized Randić index. 86 | 87 | Returns 88 | ------- 89 | float 90 | The generalized Randić Index of a ``G``. 91 | 92 | References 93 | ---------- 94 | 95 | Ivan Gutman, Degree-Based Topological Indices, Croat. Chem. Acta 86 (4) 96 | (2013) 351–361. http://dx.doi.org/10.5562/cca2294 97 | """ 98 | _degree = functools.partial(nx.degree, G) 99 | return _topological_index(G, func=lambda x, y: pow(_degree(x) * _degree(y), k)) 100 | 101 | 102 | def augmented_randic_index(G): 103 | r"""Returns the augmented Randić Index of the graph ``G``. 104 | 105 | The *augmented Randić index* of a graph G with edge set *E* is defined as the 106 | following sum 107 | 108 | .. math:: 109 | \sum_{vw \in E} \frac{1}{\max(d_G(v), d_G(w))} 110 | 111 | Parameters 112 | ---------- 113 | G : NetworkX graph 114 | An undirected graph. 115 | 116 | Returns 117 | ------- 118 | float 119 | The augmented Randic index of ``G``. 120 | 121 | References 122 | ---------- 123 | 124 | Ivan Gutman, Degree-Based Topological Indices, Croat. Chem. Acta 86 (4) 125 | (2013) 351–361. http://dx.doi.org/10.5562/cca2294 126 | 127 | *Note*: The above reference appears to be wrong. Help with the correct reference is greatly appreciated. 128 | """ 129 | _degree = functools.partial(nx.degree, G) 130 | return _topological_index(G, func=lambda x, y: 1 / max(_degree(x), _degree(y))) 131 | 132 | 133 | def harmonic_index(G): 134 | r"""Returns the Harmonic Index of the graph ``G``. 135 | 136 | The *harmonic index* of a graph *G* with edge set *E* is defined as the 137 | following sum: 138 | 139 | .. math:: 140 | \sum_{vw \in E} \frac{2}{d_G(v) + d_G(w)} 141 | 142 | This invariant was originally introduced by Siemion Fajtlowicz. 143 | 144 | Parameters 145 | ---------- 146 | G : NetworkX graph 147 | An undirected graph. 148 | 149 | Returns 150 | ------- 151 | float 152 | The harmonic index of ``G``. 153 | 154 | References 155 | ---------- 156 | 157 | Ivan Gutman, Degree-Based Topological Indices, Croat. Chem. Acta 86 (4) 158 | (2013) 351–361. http://dx.doi.org/10.5562/cca2294 159 | """ 160 | _degree = functools.partial(nx.degree, G) 161 | return _topological_index(G, func=lambda x, y: 2 / (_degree(x) + _degree(y))) 162 | 163 | 164 | def atom_bond_connectivity_index(G): 165 | r"""Returns the atom bond connectivity Index of the graph ``G``. 166 | 167 | The *atom bond connectivity index* of a graph *G* with edge set *E* is defined as the 168 | following sum: 169 | 170 | .. math:: 171 | \sum_{vw \in E} \sqrt{\frac{d_G(v) + d_G(w) - 2)}{d_G(v) \times d_G(w)}} 172 | 173 | Parameters 174 | ---------- 175 | G : NetworkX graph 176 | An undirected graph. 177 | 178 | Returns 179 | ------- 180 | float 181 | The atom bond connectivity index of ``G``. 182 | 183 | References 184 | ---------- 185 | 186 | Ivan Gutman, Degree-Based Topological Indices, Croat. Chem. Acta 86 (4) 187 | (2013) 351–361. http://dx.doi.org/10.5562/cca2294 188 | """ 189 | _degree = functools.partial(nx.degree, G) 190 | return _topological_index( 191 | G, func=lambda x, y: math.sqrt((_degree(x) + _degree(y) - 2) / (_degree(x) * _degree(y))) 192 | ) 193 | 194 | 195 | def sum_connectivity_index(G): 196 | r"""Returns the sum connectivity index of the graph ``G``. 197 | 198 | The *sum connectivity index* of a graph *G* with edge set *E* is defined as the 199 | following sum: 200 | 201 | .. math:: 202 | \sum_{vw \in E} \frac{1}{\sqrt{d_G(v) + d_G(w)}} 203 | 204 | Parameters 205 | ---------- 206 | G : NetworkX graph 207 | An undirected graph. 208 | 209 | Returns 210 | ------- 211 | float 212 | The sum connectivity index of ``G``. 213 | 214 | References 215 | ---------- 216 | 217 | Ivan Gutman, Degree-Based Topological Indices, Croat. Chem. Acta 86 (4) 218 | (2013) 351–361. http://dx.doi.org/10.5562/cca2294 219 | """ 220 | _degree = functools.partial(nx.degree, G) 221 | return _topological_index(G, func=lambda x, y: 1 / math.sqrt(_degree(x) + _degree(y))) 222 | 223 | 224 | def first_zagreb_index(G): 225 | r"""Returns the first Zagreb index of the graph ``G``. 226 | 227 | The *first Zagreb index* of a graph *G* with vertex set *V* is defined as the 228 | following sum: 229 | 230 | .. math:: 231 | \sum_{v \in E} d_G(v)^{2} 232 | 233 | Remarkably, this sum is equiavlent to the following: 234 | 235 | .. math:: 236 | \sum_{vw \in E} d_G(v) + d_G(w) 237 | 238 | where *E* is the edge set of *G*. 239 | 240 | Parameters 241 | ---------- 242 | G : NetworkX graph 243 | An undirected graph. 244 | 245 | Returns 246 | ------- 247 | float 248 | The first Zagreb index of ``G``. 249 | 250 | References 251 | ---------- 252 | 253 | Ivan Gutman, Degree-Based Topological Indices, Croat. Chem. Acta 86 (4) 254 | (2013) 351–361. http://dx.doi.org/10.5562/cca2294 255 | """ 256 | _degree = functools.partial(nx.degree, G) 257 | return _topological_index(G, func=lambda x, y: _degree(x) + _degree(y)) 258 | 259 | 260 | def second_zagreb_index(G): 261 | r"""Returns the second Zagreb index of the graph ``G``. 262 | 263 | The *second Zagreb index* of a graph *G* with edge set *E* is defined as the 264 | following sum: 265 | 266 | .. math:: 267 | \sum_{vw \in E} d_G(v) \times d_G(w) 268 | 269 | Parameters 270 | ---------- 271 | G : NetworkX graph 272 | An undirected graph. 273 | 274 | Returns 275 | ------- 276 | float 277 | The second Zagreb index of ``G``. 278 | 279 | References 280 | ---------- 281 | 282 | Ivan Gutman, Degree-Based Topological Indices, Croat. Chem. Acta 86 (4) 283 | (2013) 351–361. http://dx.doi.org/10.5562/cca2294 284 | """ 285 | _degree = functools.partial(nx.degree, G) 286 | return _topological_index(G, func=lambda x, y: _degree(x) * _degree(y)) 287 | -------------------------------------------------------------------------------- /grinpy/invariants/vertex_cover.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing vertex covers and related invariants in a graph.""" 11 | 12 | 13 | from pulp import LpBinary, LpMinimize, LpProblem, LpVariable, lpSum 14 | 15 | 16 | def min_vertex_cover_ilp(G): 17 | """Return a smallest vertex cover in the graph G. 18 | 19 | This method uses an ILP to solve for a smallest vertex cover. 20 | Specifically, the ILP minimizes 21 | 22 | .. math:: 23 | 24 | \\sum_{v \\in V} x_v 25 | 26 | subject to 27 | 28 | .. math:: 29 | 30 | x_v + x_u \\geq 1 \\mathrm{for all } \\{u, v\\} \\in E 31 | x_v \\in \\{0, 1\\} \\mathrm{for all } v \\in V 32 | 33 | where *V* and *E* are the vertex and edge sets of *G*. 34 | 35 | Parameters 36 | ---------- 37 | G : NetworkX graph 38 | An undirected graph. 39 | 40 | Returns 41 | ------- 42 | set 43 | A set of nodes in a smallest vertex cover. 44 | 45 | """ 46 | prob = LpProblem("min_vertex_cover", LpMinimize) 47 | variables = {node: LpVariable("x{}".format(i + 1), 0, 1, LpBinary) for i, node in enumerate(G.nodes())} 48 | 49 | # Set the total domination number objective function 50 | prob += lpSum([variables[n] for n in variables]) 51 | 52 | # Set constraints 53 | for edge in G.edges(): 54 | prob += variables[edge[0]] + variables[edge[1]] >= 1 55 | 56 | prob.solve() 57 | solution_set = {node for node in variables if variables[node].value() == 1} 58 | return solution_set 59 | 60 | 61 | def min_vertex_cover(G, method="ilp"): 62 | """Return a smallest vertex cover of G. 63 | 64 | A *vertex cover* of a graph *G* is a set of vertices with the 65 | property that every edge in the graph is incident to at least one 66 | vertex in the set. 67 | 68 | Parameters 69 | ---------- 70 | G : NetworkX graph 71 | An undirected graph. 72 | 73 | method: string 74 | The method to use for finding a smallest vertex cover. 75 | Currently, the only option is 'ilp'. Defaults to 'ilp'. 76 | 77 | Returns 78 | ------- 79 | set 80 | A set of nodes in a smallest vertex cover. 81 | 82 | """ 83 | vertex_cover_func = {"ilp": min_vertex_cover_ilp}.get(method, None) 84 | 85 | if vertex_cover_func: 86 | return vertex_cover_func(G) 87 | 88 | raise ValueError('Invalid `method` argument "{}"'.format(method)) 89 | 90 | 91 | def vertex_cover_number(G): 92 | """Return a the size of smallest vertex cover in the graph G. 93 | 94 | Parameters 95 | ---------- 96 | G : NetworkX graph 97 | An undirected graph. 98 | 99 | Returns 100 | ------- 101 | number 102 | The size of a smallest vertex cover of G. 103 | 104 | """ 105 | return len(min_vertex_cover_ilp(G)) 106 | -------------------------------------------------------------------------------- /grinpy/invariants/zero_forcing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017-2019 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Functions for computing zero forcing related invariants of a graph.""" 11 | 12 | from itertools import combinations 13 | from grinpy import is_connected, min_degree, neighborhood, number_of_nodes_of_degree_k 14 | 15 | __all__ = [ 16 | "is_k_forcing_vertex", 17 | "is_k_forcing_active_set", 18 | "is_k_forcing_set", 19 | "min_k_forcing_set", 20 | "k_forcing_number", 21 | "is_zero_forcing_vertex", 22 | "is_zero_forcing_active_set", 23 | "is_zero_forcing_set", 24 | "min_zero_forcing_set", 25 | "zero_forcing_number", 26 | "is_total_zero_forcing_set", 27 | "min_total_zero_forcing_set", 28 | "total_zero_forcing_number", 29 | "is_connected_k_forcing_set", 30 | "is_connected_zero_forcing_set", 31 | "min_connected_k_forcing_set", 32 | "min_connected_zero_forcing_set", 33 | "connected_k_forcing_number", 34 | "connected_zero_forcing_number", 35 | ] 36 | 37 | 38 | def is_k_forcing_vertex(G, v, nodes, k): 39 | """Return whether or not *v* can *k*-force relative to the set of nodes 40 | in `nodes`. 41 | 42 | Parameters 43 | ---------- 44 | G : NetworkX graph 45 | An undirected graph. 46 | 47 | v : node 48 | A single node in *G*. 49 | 50 | nodes : list, set 51 | An iterable container of nodes in G. 52 | 53 | k : int 54 | A positive integer. 55 | 56 | Returns 57 | ------- 58 | boolean 59 | True if *v* can *k*-force relative to the nodes in `nodes`. False 60 | otherwise. 61 | """ 62 | # check that k is a positive integer 63 | if not float(k).is_integer(): 64 | raise TypeError("Expected k to be an integer.") 65 | k = int(k) 66 | if k < 1: 67 | raise ValueError("Expected k to be a positive integer.") 68 | S = set(n for n in nodes if n in G) 69 | n = len(set(neighborhood(G, v)).difference(S)) 70 | return v in S and n >= 1 and n <= k 71 | 72 | 73 | def is_k_forcing_active_set(G, nodes, k): 74 | """Return whether or not at least one node in `nodes` can *k*-force. 75 | 76 | Parameters 77 | ---------- 78 | G : NetworkX graph 79 | An undirected graph. 80 | 81 | nbunch : 82 | A single node or iterable container or nodes. 83 | 84 | k : int 85 | A positive integer. 86 | 87 | Returns 88 | ------- 89 | boolean 90 | True if at least one of the nodes in `nodes` can *k*-force. False 91 | otherwise. 92 | """ 93 | S = set(n for n in nodes if n in G) 94 | for v in S: 95 | if is_k_forcing_vertex(G, v, S, k): 96 | return True 97 | return False 98 | 99 | 100 | def is_k_forcing_set(G, nodes, k): 101 | """Return whether or not the nodes in `nodes` comprise a *k*-forcing set in 102 | *G*. 103 | 104 | Parameters 105 | ---------- 106 | G : NetworkX graph 107 | An undirected graph. 108 | 109 | nodes : list, set 110 | An iterable container of nodes in G. 111 | 112 | k : int 113 | A positive integer. 114 | 115 | Returns 116 | ------- 117 | boolean 118 | True if the nodes in `nodes` comprise a *k*-forcing set in *G*. False 119 | otherwise. 120 | """ 121 | Z = set(n for n in nodes if n in G) 122 | while is_k_forcing_active_set(G, Z, k): 123 | Z_temp = Z.copy() 124 | for v in Z: 125 | if is_k_forcing_vertex(G, v, Z, k): 126 | Z_temp |= set(neighborhood(G, v)) 127 | Z = Z_temp 128 | return Z == set(G.nodes()) 129 | 130 | 131 | def min_k_forcing_set(G, k): 132 | """Return a smallest *k*-forcing set in *G*. 133 | 134 | The method used to compute the set is brute force. 135 | 136 | Parameters 137 | ---------- 138 | G : NetworkX graph 139 | An undirected graph. 140 | 141 | k : int 142 | A positive integer. 143 | 144 | Returns 145 | ------- 146 | list 147 | A list of nodes in a smallest *k*-forcing set in *G*. 148 | """ 149 | # use naive lower bound to compute a starting point for the search range 150 | rangeMin = min_degree(G) if k == 1 else 1 151 | # loop through subsets of nodes of G in increasing order of size until a zero forcing set is found 152 | for i in range(rangeMin, G.order() + 1): 153 | for S in combinations(G.nodes(), i): 154 | if is_k_forcing_set(G, S, k): 155 | return list(S) 156 | 157 | 158 | def k_forcing_number(G, k): 159 | """Return the *k*-forcing number of *G*. 160 | 161 | The *k*-forcing number of a graph is the cardinality of a smallest 162 | *k*-forcing set in the graph. 163 | 164 | Parameters 165 | ---------- 166 | G : NetworkX graph 167 | An undirected graph. 168 | 169 | k : int 170 | A positive integer. 171 | 172 | Returns 173 | ------- 174 | int 175 | The *k*-forcing number of *G*. 176 | """ 177 | return len(min_k_forcing_set(G, k)) 178 | 179 | 180 | def is_zero_forcing_vertex(G, v, nbunch): 181 | """Return whether or not *v* can force relative to the set of nodes 182 | in nbunch. 183 | 184 | Parameters 185 | ---------- 186 | G : NetworkX graph 187 | An undirected graph. 188 | 189 | v : node 190 | A single node in *G*. 191 | 192 | nbunch : 193 | A single node or iterable container or nodes. 194 | 195 | Returns 196 | ------- 197 | boolean 198 | True if *v* can force relative to the nodes in nbunch. False 199 | otherwise. 200 | """ 201 | return is_k_forcing_vertex(G, v, nbunch, 1) 202 | 203 | 204 | def is_zero_forcing_active_set(G, nbunch): 205 | """Return whether or not at least one node in nbunch can force. 206 | 207 | Parameters 208 | ---------- 209 | G : NetworkX graph 210 | An undirected graph. 211 | 212 | nbunch : 213 | A single node or iterable container or nodes. 214 | 215 | Returns 216 | ------- 217 | boolean 218 | True if at least one of the nodes in nbunch can force. False 219 | otherwise. 220 | """ 221 | return is_k_forcing_active_set(G, nbunch, 1) 222 | 223 | 224 | def is_zero_forcing_set(G, nbunch): 225 | """Return whether or not the nodes in nbunch comprise a zero forcing set in 226 | *G*. 227 | 228 | Parameters 229 | ---------- 230 | G : NetworkX graph 231 | An undirected graph. 232 | 233 | nbunch : 234 | A single node or iterable container or nodes. 235 | 236 | Returns 237 | ------- 238 | boolean 239 | True if the nodes in nbunch comprise a zero forcing set in *G*. False 240 | otherwise. 241 | """ 242 | return is_k_forcing_set(G, nbunch, 1) 243 | 244 | 245 | def min_zero_forcing_set(G): 246 | """Return a smallest zero forcing set in *G*. 247 | 248 | The method used to compute the set is brute force. 249 | 250 | Parameters 251 | ---------- 252 | G : NetworkX graph 253 | An undirected graph. 254 | 255 | Returns 256 | ------- 257 | list 258 | A list of nodes in a smallest zero forcing set in *G*. 259 | """ 260 | return min_k_forcing_set(G, 1) 261 | 262 | 263 | def zero_forcing_number(G): 264 | """Return the zero forcing number of *G*. 265 | 266 | The zero forcing number of a graph is the cardinality of a smallest 267 | zero forcing set in the graph. 268 | 269 | Parameters 270 | ---------- 271 | G : NetworkX graph 272 | An undirected graph. 273 | 274 | Returns 275 | ------- 276 | int 277 | The zero forcing number of *G*. 278 | """ 279 | return len(min_zero_forcing_set(G)) 280 | 281 | 282 | def is_total_zero_forcing_set(G, nodes): 283 | """Return whether or not the nodes in `nodes` comprise a total zero forcing 284 | set in *G*. 285 | 286 | A *total zero forcing set* in a graph *G* is a zero forcing set that does 287 | not induce any isolated vertices. 288 | 289 | Parameters 290 | ---------- 291 | G : NetworkX graph 292 | An undirected graph. 293 | 294 | nodes : list, set 295 | An iterable container of nodes in G. 296 | 297 | Returns 298 | ------- 299 | boolean 300 | True if the nodes in `nodes` comprise a total zero forcing set in *G*. 301 | False otherwise. 302 | """ 303 | S = set(n for n in nodes if n in G) 304 | for v in S: 305 | if set(neighborhood(G, v)).intersection(S) == set(): 306 | return False 307 | return is_zero_forcing_set(G, S) 308 | 309 | 310 | def min_total_zero_forcing_set(G): 311 | """Return a smallest total zero forcing set in *G*. 312 | 313 | The method used to compute the set is brute force. 314 | 315 | Parameters 316 | ---------- 317 | G : NetworkX graph 318 | An undirected graph. 319 | 320 | Returns 321 | ------- 322 | list 323 | A list of nodes in a smallest zero forcing set in *G*. 324 | """ 325 | # only start search if graph has no isolates 326 | if number_of_nodes_of_degree_k(G, 0) > 0: 327 | return None 328 | for i in range(2, G.order() + 1): 329 | for S in combinations(G.nodes(), i): 330 | if is_total_zero_forcing_set(G, S): 331 | return list(S) 332 | # if the above loop completes, return None (should not occur) 333 | return None 334 | 335 | 336 | def total_zero_forcing_number(G): 337 | """Return the total zero forcing number of *G*. 338 | 339 | The *total zero forcing number* of a graph is the cardinality of a smallest 340 | total zero forcing set in the graph. 341 | 342 | Parameters 343 | ---------- 344 | G : NetworkX graph 345 | An undirected graph. 346 | 347 | Returns 348 | ------- 349 | int 350 | The total zero forcing number of *G*. 351 | """ 352 | Z = min_total_zero_forcing_set(G) 353 | if Z is None: 354 | return None 355 | else: 356 | return len(min_total_zero_forcing_set(G)) 357 | 358 | 359 | def is_connected_k_forcing_set(G, nodes, k): 360 | """Return whether or not the nodes in `nodes` comprise a connected k-forcing 361 | set in *G*. 362 | 363 | A *connected k-forcing set* in a graph *G* is a k-forcing set that induces 364 | a connected subgraph. 365 | 366 | Parameters 367 | ---------- 368 | G : NetworkX graph 369 | An undirected graph. 370 | 371 | nodes : list, set 372 | An iterable container of nodes in G. 373 | 374 | k : int 375 | A positive integer. 376 | 377 | Returns 378 | ------- 379 | boolean 380 | True if the nodes in nbunch comprise a connected k-forcing set in *G*. 381 | False otherwise. 382 | """ 383 | # check that k is a positive integer 384 | if not float(k).is_integer(): 385 | raise TypeError("Expected k to be an integer.") 386 | k = int(k) 387 | if k < 1: 388 | raise ValueError("Expected k to be a positive integer.") 389 | S = set(n for n in nodes if n in G) 390 | H = G.subgraph(S) 391 | return is_connected(H) and is_k_forcing_set(G, S, k) 392 | 393 | 394 | def is_connected_zero_forcing_set(G, nodes): 395 | """Return whether or not the nodes in `nodes` comprise a connected zero 396 | forcing set in *G*. 397 | 398 | A *connected zero forcing set* in a graph *G* is a zero forcing set that 399 | induces a connected subgraph. 400 | 401 | Parameters 402 | ---------- 403 | G : NetworkX graph 404 | An undirected graph. 405 | 406 | nodes : list, set 407 | An iterable container of nodes in G. 408 | 409 | Returns 410 | ------- 411 | boolean 412 | True if the nodes in `nodes` comprise a connected zero forcing set in 413 | *G*. False otherwise. 414 | """ 415 | return is_connected_k_forcing_set(G, nodes, 1) 416 | 417 | 418 | def min_connected_k_forcing_set(G, k): 419 | """Return a smallest connected k-forcing set in *G*. 420 | 421 | The method used to compute the set is brute force. 422 | 423 | Parameters 424 | ---------- 425 | G : NetworkX graph 426 | An undirected graph. 427 | 428 | k : int 429 | A positive integer 430 | 431 | Returns 432 | ------- 433 | list 434 | A list of nodes in a smallest connected k-forcing set in *G*. 435 | """ 436 | # check that k is a positive integer 437 | if not float(k).is_integer(): 438 | raise TypeError("Expected k to be an integer.") 439 | k = int(k) 440 | if k < 1: 441 | raise ValueError("Expected k to be a positive integer.") 442 | # only start search if graph is connected 443 | if not is_connected(G): 444 | return None 445 | for i in range(1, G.order() + 1): 446 | for S in combinations(G.nodes(), i): 447 | if is_connected_k_forcing_set(G, S, k): 448 | return list(S) 449 | 450 | 451 | def min_connected_zero_forcing_set(G): 452 | """Return a smallest connected zero forcing set in *G*. 453 | 454 | The method used to compute the set is brute force. 455 | 456 | Parameters 457 | ---------- 458 | G : NetworkX graph 459 | An undirected graph. 460 | 461 | k : int 462 | A positive integer 463 | 464 | Returns 465 | ------- 466 | list 467 | A list of nodes in a smallest connected zero forcing set in *G*. 468 | """ 469 | return min_connected_k_forcing_set(G, 1) 470 | 471 | 472 | def connected_k_forcing_number(G, k): 473 | """Return the connected k-forcing number of *G*. 474 | 475 | The *connected k-forcing number* of a graph is the cardinality of a smallest 476 | connected k-forcing set in the graph. 477 | 478 | Parameters 479 | ---------- 480 | G : NetworkX graph 481 | An undirected graph. 482 | 483 | Returns 484 | ------- 485 | int 486 | The connected k-forcing number of *G*. 487 | """ 488 | # check that k is a positive integer 489 | if not float(k).is_integer(): 490 | raise TypeError("Expected k to be an integer.") 491 | k = int(k) 492 | if k < 1: 493 | raise ValueError("Expected k to be a positive integer.") 494 | Z = min_connected_k_forcing_set(G, k) 495 | if Z is None: 496 | return None 497 | else: 498 | return len(Z) 499 | 500 | 501 | def connected_zero_forcing_number(G): 502 | """Return the connected zero forcing number of *G*. 503 | 504 | The *connected zero forcing number* of a graph is the cardinality of a 505 | smallest connected zero forcing set in the graph. 506 | 507 | Parameters 508 | ---------- 509 | G : NetworkX graph 510 | An undirected graph. 511 | 512 | Returns 513 | ------- 514 | int 515 | The connected k-forcing number of *G*. 516 | """ 517 | return connected_k_forcing_number(G, 1) 518 | -------------------------------------------------------------------------------- /grinpy/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from grinpy.utils.sequences import * # noqa 2 | from grinpy.utils.combinations import * # noqa 3 | -------------------------------------------------------------------------------- /grinpy/utils/combinations.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Utility functions for dealing with combinations of things.""" 11 | 12 | import itertools 13 | from grinpy import nodes 14 | 15 | 16 | def pairs_of_nodes(G): 17 | return itertools.combinations(nodes(G), 2) 18 | -------------------------------------------------------------------------------- /grinpy/utils/sequences.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017 by 4 | # David Amos 5 | # Randy Davila 6 | # BSD license. 7 | # 8 | # Authors: David Amos 9 | # Randy Davila 10 | """Utility functions for dealing with sequences.""" 11 | 12 | 13 | def contains_only_zeros(sequence): 14 | return sequence.count(0) == len(sequence) 15 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Black configuration 2 | 3 | [tool.black] 4 | line-length = 119 5 | target-version = ["py36"] 6 | include = '\.pyi?$' 7 | exclude = ''' 8 | /( 9 | \.eggs 10 | | \.git 11 | | \.hg 12 | | \.mypy_cache 13 | | \.tox 14 | | \.venv 15 | | _build 16 | | buck-out 17 | | build 18 | | dist 19 | | \venv 20 | | \.env 21 | )/ 22 | ''' 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | networkx>=2.1 2 | numpy>=1.14.2 3 | pulp>=1.6.8 4 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | black==19.3b0 2 | bumpversion==0.5.3 3 | codecov==2.0.15 4 | coverage==4.5.3 5 | flake8==3.7.7 6 | pydocstyle==3.0.0 7 | pytest==4.5.0 8 | pytest-cov==2.7.1 9 | pytest-runner==5.1 10 | tox==3.12.1 11 | twine==1.13.0 12 | wheel==0.33.4 13 | watchdog==0.9.0 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [flake8] 5 | exclude = .git 6 | max-line-length = 79 7 | 8 | [doc8] 9 | 10 | ignore-path=docs/reference/*/generated,docs/_build 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """The setup script.""" 5 | 6 | import os 7 | from setuptools import setup, find_packages 8 | from setuptools.command.install import install 9 | import sys 10 | 11 | VERSION = "19.5a0" 12 | 13 | with open("README.md") as readme_file: 14 | readme = readme_file.read() 15 | 16 | with open("HISTORY.md") as history_file: 17 | history = history_file.read() 18 | 19 | requirements = ["networkx>=2.1", "numpy>=1.14.2", "pulp>=1.6.8"] 20 | 21 | setup_requirements = ["pytest-runner"] 22 | 23 | test_requirements = ["pytest", "pytest-cov"] 24 | 25 | 26 | class VerifyVersionCommand(install): 27 | """Custom command to verify that the git tag matches our version""" 28 | 29 | description = "verify that the git tag matches our version" 30 | 31 | def run(self): 32 | tag = os.getenv("CIRCLE_TAG") 33 | 34 | if tag != VERSION: 35 | info = "Git tag: {0} does not match the version of this app: {1}".format(tag, VERSION) 36 | sys.exit(info) 37 | 38 | 39 | setup( 40 | author="David Amos", 41 | author_email="somacdivad@gmail.com", 42 | classifiers=[ 43 | "Development Status :: 2 - Pre-Alpha", 44 | "Intended Audience :: Science/Research", 45 | "Intended Audience :: Education", 46 | "License :: OSI Approved :: BSD License", 47 | "Operating System :: OS Independent", 48 | "Programming Language :: Python :: 3", 49 | "Programming Language :: Python :: 3.5", 50 | "Programming Language :: Python :: 3.6", 51 | "Programming Language :: Python :: 3.7", 52 | "Topic :: Scientific/Engineering :: Mathematics", 53 | "Topic :: Software Development :: Libraries :: Python Modules", 54 | "Natural Language :: English", 55 | ], 56 | description="Graph invariants in Python.", 57 | install_requires=requirements, 58 | license="BSD license", 59 | long_description=readme + "\n\n" + history, 60 | long_description_content_type="text/markdown", 61 | include_package_data=True, 62 | keywords="grinpy", 63 | name="grinpy", 64 | packages=find_packages(), 65 | setup_requires=setup_requirements, 66 | test_suite="tests", 67 | tests_require=test_requirements, 68 | url="https://github.com/somacdivad/grinpy", 69 | version=VERSION, 70 | zip_safe=False, 71 | cmdclass={"verify": VerifyVersionCommand}, 72 | ) 73 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Instructions for running unit tests 2 | 3 | You can run the unit tests by entering 4 | 5 | ``` 6 | pytest 7 | ``` 8 | 9 | in the command line. 10 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somacdivad/grinpy/597f9109b84f1c1aa8c8dd2ac5b572a05ba474de/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_chromatic.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | 3 | 4 | def test_chromatic_number_of_complete_graph_is_order(): 5 | for i in range(1, 11): 6 | G = gp.complete_graph(i) 7 | assert gp.chromatic_number(G, method="ram-rama") == G.order() 8 | assert gp.chromatic_number(G, method="ilp") == G.order() 9 | 10 | 11 | def test_chromatic_number_of_petersen_graph_is_3(): 12 | G = gp.petersen_graph() 13 | assert gp.chromatic_number(G, method="ram-rama") == 3 14 | assert gp.chromatic_number(G, method="ilp") == 3 15 | -------------------------------------------------------------------------------- /tests/test_clique.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | 3 | 4 | class TestIndependence: 5 | def test_clique_number_of_complete_graph_is_order(self): 6 | for i in range(1, 11): 7 | G = gp.complete_graph(i) 8 | assert gp.clique_number(G) == G.order() 9 | -------------------------------------------------------------------------------- /tests/test_degree.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | import pytest 3 | 4 | 5 | class TestDegree: 6 | def setup_class(self): 7 | G = gp.Graph() 8 | G.add_edge(0, 1) 9 | G.add_edge(0, 2) 10 | G.add_edge(1, 2) 11 | G.add_edge(0, 3) 12 | G.add_edge(3, 4) 13 | G.add_edge(3, 5) 14 | self.G = G 15 | 16 | def test_degree_sequence(self): 17 | D = gp.degree_sequence(self.G) 18 | assert D == [3, 2, 2, 3, 1, 1] 19 | 20 | def test_max_degree(self): 21 | maxDegree = gp.max_degree(self.G) 22 | assert maxDegree == 3 23 | 24 | def test_min_degree(self): 25 | minDegree = gp.min_degree(self.G) 26 | assert minDegree == 1 27 | 28 | def test_average_degree(self): 29 | avgDegree = gp.average_degree(self.G) 30 | assert avgDegree == 2 31 | 32 | def test_number_of_nodes_of_degree_k(self): 33 | numNodes = gp.number_of_nodes_of_degree_k(self.G, 2) 34 | assert numNodes == 2 35 | 36 | def test_number_of_degree_one_nodes(self): 37 | numNodes = gp.number_of_degree_one_nodes(self.G) 38 | assert numNodes == 2 39 | 40 | def test_number_of_min_degree_nodes(self): 41 | numNodes = gp.number_of_min_degree_nodes(self.G) 42 | assert numNodes == 2 43 | 44 | def test_number_of_max_degree_nodes(self): 45 | numNodes = gp.number_of_max_degree_nodes(self.G) 46 | assert numNodes == 2 47 | 48 | def test_complete_graph_is_regular(self): 49 | G = gp.complete_graph(4) 50 | assert gp.is_regular(G) == True 51 | 52 | def test_star_is_not_regular(self): 53 | G = gp.star_graph(2) 54 | assert gp.is_regular(G) == False 55 | 56 | def test_non_integral_value_raises_TypeError_is_k_regular(self): 57 | with pytest.raises(TypeError): 58 | G = gp.trivial_graph() 59 | gp.is_k_regular(G, 1.5) 60 | 61 | def test_K5_is_4_regular(self): 62 | G = gp.complete_graph(5) 63 | assert gp.is_k_regular(G, 4) == True 64 | 65 | def test_star_is_not_2_regular(self): 66 | G = gp.star_graph(2) 67 | assert gp.is_k_regular(G, 2) == False 68 | 69 | def test_K4_is_cubic(self): 70 | G = gp.complete_graph(4) 71 | assert gp.is_cubic(G) == True 72 | 73 | def test_K4_is_not_cubic(self): 74 | G = gp.complete_graph(5) 75 | assert gp.is_cubic(G) == False 76 | 77 | def test_sub_cubic(self): 78 | assert gp.is_sub_cubic(self.G) == True 79 | 80 | def test_K5_is_not_sub_cubic(self): 81 | G = gp.complete_graph(5) 82 | assert gp.is_sub_cubic(G) == False 83 | -------------------------------------------------------------------------------- /tests/test_disparity.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | import pytest 3 | 4 | 5 | class TestDisparity: 6 | def setup_class(self): 7 | # The test graph for these functions is the simple graph obtained from 8 | # the disjoint union of K_3 and P_3 by joining one vertex of K_3 with 9 | # the degree two vertex of P_3. 10 | G = gp.Graph() 11 | G.add_edge(0, 1) 12 | G.add_edge(0, 2) 13 | G.add_edge(1, 2) 14 | G.add_edge(0, 3) 15 | G.add_edge(3, 4) 16 | G.add_edge(3, 5) 17 | self.G = G 18 | 19 | def test_vertex_disparity(self): 20 | G = self.G 21 | assert gp.vertex_disparity(G, 0) == 2 22 | assert gp.vertex_disparity(G, 1) == 2 23 | assert gp.vertex_disparity(G, 2) == 2 24 | assert gp.vertex_disparity(G, 3) == 2 25 | assert gp.vertex_disparity(G, 4) == 1 26 | assert gp.vertex_disparity(G, 5) == 1 27 | 28 | def test_vertex_disparity_of_vertex_not_in_graph(self): 29 | G = self.G 30 | with pytest.raises(ValueError): 31 | gp.vertex_disparity(G, 6) 32 | 33 | def test_closed_vertex_disparity(self): 34 | G = self.G 35 | assert gp.closed_vertex_disparity(G, 0) == 2 36 | assert gp.closed_vertex_disparity(G, 1) == 2 37 | assert gp.closed_vertex_disparity(G, 2) == 2 38 | assert gp.closed_vertex_disparity(G, 3) == 2 39 | assert gp.closed_vertex_disparity(G, 4) == 2 40 | assert gp.closed_vertex_disparity(G, 5) == 2 41 | 42 | def test_closed_vertex_disparity_of_vertex_not_in_graph(self): 43 | G = self.G 44 | with pytest.raises(ValueError): 45 | gp.closed_vertex_disparity(G, 6) 46 | 47 | def test_disparity_sequence(self): 48 | G = self.G 49 | assert gp.disparity_sequence(G) == [2, 2, 2, 2, 1, 1] 50 | 51 | def test_closed_disparity_sequence(self): 52 | G = self.G 53 | assert gp.closed_disparity_sequence(G) == [2, 2, 2, 2, 2, 2] 54 | 55 | def test_CW_disparity(self): 56 | G = self.G 57 | assert gp.CW_disparity(G) == sum([1 / 3, 1 / 3, 1 / 3, 1 / 3, 1 / 2, 1 / 2]) 58 | 59 | def test_closed_CW_disparity(self): 60 | G = self.G 61 | assert gp.closed_CW_disparity(G) == sum([1 / 3, 1 / 3, 1 / 3, 1 / 3, 1 / 3, 1 / 3]) 62 | 63 | def test_inverse_disparity(self): 64 | G = self.G 65 | assert gp.inverse_disparity(G) == sum([1 / 2, 1 / 2, 1 / 2, 1 / 2, 1 / 1, 1 / 1]) 66 | 67 | def test_closed_inverse_disparity(self): 68 | G = self.G 69 | assert gp.closed_inverse_disparity(G) == sum([1 / 2, 1 / 2, 1 / 2, 1 / 2, 1 / 2, 1 / 2]) 70 | 71 | def test_average_vertex_disparity(self): 72 | G = self.G 73 | assert gp.average_vertex_disparity(G) == 10 / 6 74 | 75 | def test_average_closed_vertex_disparity(self): 76 | G = self.G 77 | assert gp.average_closed_vertex_disparity(G) == 2.0 78 | 79 | def test_k_disparity(self): 80 | G = self.G 81 | assert gp.k_disparity(G, 1) == 2.0 82 | assert gp.k_disparity(G, 2) == 2.0 83 | assert gp.k_disparity(G, 3) == 2.0 84 | assert gp.k_disparity(G, 4) == 2.0 85 | assert gp.k_disparity(G, 5) == 29 / 15 86 | assert gp.k_disparity(G, 6) == 39 / 21 87 | 88 | def test_closed_k_disparity(self): 89 | G = self.G 90 | assert gp.closed_k_disparity(G, 1) == 2.0 91 | assert gp.closed_k_disparity(G, 2) == 2.0 92 | assert gp.closed_k_disparity(G, 3) == 2.0 93 | assert gp.closed_k_disparity(G, 4) == 2.0 94 | assert gp.closed_k_disparity(G, 5) == 2.0 95 | assert gp.closed_k_disparity(G, 6) == 2.0 96 | 97 | def test_irregularity_complete_graph(self): 98 | for i in range(2, 10): 99 | G = gp.complete_graph(i) 100 | assert gp.irregularity(G) == 1.0 101 | 102 | def test_irregularity_star(self): 103 | for i in range(2, 10): 104 | G = gp.star_graph(i) 105 | assert gp.irregularity(G) == 2.0 106 | -------------------------------------------------------------------------------- /tests/test_distance_measures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import grinpy as gp 4 | 5 | 6 | class TestDistanceMeasures: 7 | @pytest.mark.parametrize( 8 | "graph, expected_value", 9 | ( 10 | (gp.cycle_graph(3), 3), 11 | (gp.cycle_graph(4), 4), 12 | (gp.cycle_graph(5), 5), 13 | (gp.path_graph(3), 4), 14 | (gp.path_graph(4), 6), 15 | (gp.path_graph(5), 8), 16 | ), 17 | ) 18 | def test_triameter(self, graph, expected_value): 19 | """Ensure triameter returns the expected value for a given graph""" 20 | assert gp.triameter(graph) == expected_value 21 | -------------------------------------------------------------------------------- /tests/test_domination.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | import pytest 3 | 4 | 5 | def test_non_integral_value_for_k_raises_error_in_is_k_dom_set(): 6 | with pytest.raises(TypeError): 7 | G = gp.star_graph(2) 8 | gp.is_k_dominating_set(G, [0], 1.5) 9 | 10 | 11 | def test_0_value_for_k_raises_error_in_is_k_dom_set(): 12 | with pytest.raises(ValueError): 13 | G = gp.star_graph(2) 14 | gp.is_k_dominating_set(G, [0], 0) 15 | 16 | 17 | def test_non_int_value_for_k_raises_error_in_min_k_dom_set(): 18 | with pytest.raises(TypeError): 19 | G = gp.star_graph(2) 20 | gp.min_k_dominating_set(G, 1.5) 21 | 22 | 23 | def test_0_value_for_k_raises_error_in_min_k_dom_set(): 24 | with pytest.raises(ValueError): 25 | G = gp.star_graph(2) 26 | gp.min_k_dominating_set(G, 0) 27 | 28 | 29 | def test_non_int_value_for_k_raises_error_in_k_dom_num(): 30 | with pytest.raises(TypeError): 31 | G = gp.star_graph(2) 32 | gp.k_domination_number(G, 1.5) 33 | 34 | 35 | def test_0_value_for_k_raises_error_in_k_dom_num(): 36 | with pytest.raises(ValueError): 37 | G = gp.star_graph(2) 38 | gp.k_domination_number(G, 0) 39 | 40 | 41 | def test_integral_float_for_k_works(): 42 | G = gp.star_graph(2) 43 | assert gp.is_k_dominating_set(G, [0], 1.0) is True 44 | 45 | 46 | def test_max_degree_vertex_is_dominating_set_of_star(): 47 | for i in range(1, 9): 48 | G = gp.star_graph(i) 49 | assert gp.is_k_dominating_set(G, [0], 1) is True 50 | 51 | 52 | def test_min_degree_vertex_is_not_dominating_set_of_star(): 53 | for i in range(2, 9): 54 | G = gp.star_graph(i) 55 | assert gp.is_k_dominating_set(G, [1], 1) is False 56 | 57 | 58 | def test_dominating_set_with_nodes_not_in_graph(): 59 | G = gp.star_graph(3) 60 | assert gp.is_k_dominating_set(G, [4], 1) is False 61 | assert gp.is_k_dominating_set(G, [0, 4], 1) is True 62 | 63 | 64 | def test_max_degree_vertex_is_not_2_dominating_set_of_star(): 65 | for i in range(1, 9): 66 | G = gp.star_graph(i) 67 | assert gp.is_k_dominating_set(G, [0], 2) is False 68 | 69 | 70 | def test_min_degree_vertices_are_2_dominating_set_of_star(): 71 | for i in range(2, 9): 72 | G = gp.star_graph(i) 73 | nodes = [i for i in range(1, i + 2)] 74 | assert gp.is_k_dominating_set(G, nodes, 2) is True 75 | 76 | 77 | def test_2_dominating_set_with_nodes_not_in_graph(): 78 | G = gp.star_graph(3) 79 | nodes = [1, 2, 3, 4] 80 | assert gp.is_k_dominating_set(G, [4], 1) is False 81 | assert gp.is_k_dominating_set(G, nodes, 1) is True 82 | 83 | 84 | def test_no_single_node_is_total_dominating_set_of_star(): 85 | G = gp.star_graph(3) 86 | for v in gp.nodes(G): 87 | assert gp.is_total_dominating_set(G, [v]) is False 88 | 89 | 90 | def test_adjacent_vertices_are_total_dominating_set_of_star(): 91 | G = gp.star_graph(3) 92 | for v in gp.nodes(G): 93 | for u in gp.nodes(G): 94 | if gp.are_neighbors(G, u, v): 95 | assert gp.is_total_dominating_set(G, [u, v]) is True 96 | 97 | 98 | def test_non_adjacent_vertices_not_total_dominating_set_of_star(): 99 | G = gp.star_graph(3) 100 | for v in gp.nodes(G): 101 | for u in gp.nodes(G): 102 | if not gp.are_neighbors(G, u, v): 103 | assert gp.is_total_dominating_set(G, [u, v]) is False 104 | 105 | 106 | def test_center_vertex_of_star_is_connected_dominating_set(): 107 | G = gp.star_graph(3) 108 | assert gp.is_connected_dominating_set(G, [0]) is True 109 | 110 | 111 | def test_leaves_of_star_are_not_connected_dominating_set(): 112 | G = gp.star_graph(3) 113 | D = [1, 2, 3] 114 | assert gp.is_connected_dominating_set(G, D) is False 115 | 116 | 117 | def test_3_adjacent_vertice_is_connected_2_dominating_set_of_4_cycle(): 118 | G = gp.cycle_graph(4) 119 | assert gp.is_connected_k_dominating_set(G, [0, 1, 2], 2) is True 120 | 121 | 122 | def test_non_adjacent_vertices_not_connected_2_dom_set_of_4_cycle(): 123 | G = gp.cycle_graph(4) 124 | assert gp.is_connected_k_dominating_set(G, [0, 2], 2) is False 125 | 126 | 127 | def test_connected_domination_number_of_star_is_1(): 128 | G = gp.star_graph(3) 129 | assert gp.connected_domination_number(G) == 1 130 | 131 | 132 | def test_connected_domination_number_of_P5_is_3(): 133 | G = gp.path_graph(5) 134 | assert gp.connected_domination_number(G) == 3 135 | 136 | 137 | def leaves_of_star_is_independent_dominating_set(): 138 | G = gp.star_graph(3) 139 | D = [1, 2, 3] 140 | assert gp.is_independent_dominating_set(G, D) is True 141 | 142 | 143 | def center_node_and_leaf_is_not_ind_dom_set_of_star(): 144 | G = gp.star_graph(3) 145 | assert gp.is_independent_dominating_set(G, [0, 1]) is False 146 | 147 | 148 | def test_independent_domination_num_of_monster_is_3(): 149 | G = gp.star_graph(3) 150 | G.add_edge(3, 4) 151 | G.add_edge(3, 5) 152 | assert gp.independent_domination_number(G, method="bf") == 3 153 | assert gp.independent_domination_number(G, method="ilp") == 3 154 | 155 | 156 | def test_non_int_value_for_k_raises_error_in_connected_k_dom_set(): 157 | with pytest.raises(TypeError): 158 | G = gp.star_graph(2) 159 | gp.is_connected_k_dominating_set(G, [0], 1.5) 160 | 161 | 162 | def test_0_value_for_k_raises_error_in_connected_k_dom_set(): 163 | with pytest.raises(ValueError): 164 | G = gp.star_graph(2) 165 | gp.is_connected_k_dominating_set(G, [0], 0) 166 | 167 | 168 | def test_non_int_value_for_k_raises_error_in_min_connected_k_dom_set(): 169 | with pytest.raises(TypeError): 170 | G = gp.star_graph(2) 171 | gp.min_connected_k_dominating_set(G, 1.5) 172 | 173 | 174 | def test_0_value_for_k_raises_error_in_min_connected_k_dom_set(): 175 | with pytest.raises(ValueError): 176 | G = gp.star_graph(2) 177 | gp.min_connected_k_dominating_set(G, 0) 178 | 179 | 180 | def test_non_int_value_for_k_raises_error_in_connected_k_dom_num(): 181 | with pytest.raises(TypeError): 182 | G = gp.star_graph(2) 183 | gp.connected_k_domination_number(G, 1.5) 184 | 185 | 186 | def test_0_value_for_k_raises_error_in_connected_k_dom_num(): 187 | with pytest.raises(ValueError): 188 | G = gp.star_graph(2) 189 | gp.connected_k_domination_number(G, 0) 190 | 191 | 192 | def test_non_int_value_for_k_raises_error_in_ind_k_dom_set(): 193 | with pytest.raises(TypeError): 194 | G = gp.star_graph(2) 195 | gp.is_independent_k_dominating_set(G, [0], 1.5) 196 | 197 | 198 | def test_0_value_for_k_raises_error_in_ind_k_dom_set(): 199 | with pytest.raises(ValueError): 200 | G = gp.star_graph(2) 201 | gp.is_independent_k_dominating_set(G, [0], 0) 202 | 203 | 204 | def test_non_int_value_for_k_raises_error_in_min_ind_k_dom_set(): 205 | with pytest.raises(TypeError): 206 | G = gp.star_graph(2) 207 | gp.min_independent_k_dominating_set(G, 1.5) 208 | 209 | 210 | def test_0_value_for_k_raises_error_in_min_ind_k_dom_set(): 211 | with pytest.raises(ValueError): 212 | G = gp.star_graph(2) 213 | gp.min_independent_k_dominating_set(G, 0) 214 | 215 | 216 | def test_min_ind_dom_set_ip_returns_same_as_bf_for_peterson_graph(): 217 | G = gp.petersen_graph() 218 | bf = len(gp.min_independent_dominating_set(G, method="bf")) 219 | ip = len(gp.min_independent_dominating_set(G, method="ilp")) 220 | assert bf == ip 221 | 222 | 223 | def test_non_int_value_for_k_raises_error_in_ind_k_dom_num(): 224 | with pytest.raises(TypeError): 225 | G = gp.star_graph(2) 226 | gp.independent_k_domination_number(G, 1.5) 227 | 228 | 229 | def test_0_value_for_k_raises_error_in_ind_k_dom_num(): 230 | with pytest.raises(ValueError): 231 | G = gp.star_graph(2) 232 | gp.independent_k_domination_number(G, 0) 233 | 234 | 235 | def test_min_conn_dominating_for_disconnected_graph_is_0(): 236 | G = gp.Graph() 237 | G.add_edge(1, 2) 238 | G.add_edge(3, 4) 239 | assert gp.connected_domination_number(G) == 0 240 | 241 | 242 | def test_tot_dom_for_graph_with_isolates_is_0(): 243 | G = gp.empty_graph(5) 244 | assert gp.total_domination_number(G, method="bf") == 0 245 | assert gp.total_domination_number(G, method="ilp") == 0 246 | 247 | 248 | def test_domination_number_of_star_is_1(): 249 | for i in range(1, 9): 250 | G = gp.star_graph(i) 251 | assert gp.domination_number(G, method="bf") == 1 252 | assert gp.domination_number(G, method="ilp") == 1 253 | 254 | 255 | def test_2_domination_number_of_star_is_order_minus_1(): 256 | for i in range(2, 9): 257 | G = gp.star_graph(i) 258 | assert gp.k_domination_number(G, 2) == G.order() - 1 259 | 260 | 261 | def test_total_domination_number_of_star_is_2(): 262 | for i in range(1, 9): 263 | G = gp.star_graph(i) 264 | assert gp.total_domination_number(G, method="bf") == 2 265 | assert gp.total_domination_number(G, method="ilp") == 2 266 | -------------------------------------------------------------------------------- /tests/test_dsi.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | from math import ceil, floor 3 | import pytest 4 | 5 | 6 | class TestDSI: 7 | def test_non_integral_value_for_k_raises_TypeError(self): 8 | with pytest.raises(TypeError): 9 | G = gp.star_graph(2) 10 | gp.sub_k_domination_number(G, 1.5) 11 | 12 | def test_zero_value_for_k_raises_ValueError(self): 13 | with pytest.raises(ValueError): 14 | G = gp.star_graph(2) 15 | gp.sub_k_domination_number(G, 0) 16 | 17 | def test_integral_float_for_k_works(self): 18 | G = gp.star_graph(2) 19 | assert gp.sub_k_domination_number(G, 1.0) == 1 20 | 21 | def test_slater_of_complete_graph_is_1(self): 22 | for i in range(1, 10): 23 | G = gp.complete_graph(i) 24 | assert gp.slater(G) == 1 25 | 26 | def test_slater_of_cycle_is_third_of_nodes(self): 27 | for i in range(3, 13): 28 | G = gp.cycle_graph(i) 29 | assert gp.slater(G) == ceil(G.order() / 3) 30 | 31 | def test_sub_2_domination_number_of_complete_graph(self): 32 | for i in range(1, 10): 33 | G = gp.complete_graph(i) 34 | n = G.order() 35 | val = n / (1 + (0.5 * (n - 1))) 36 | assert gp.sub_k_domination_number(G, 2) == ceil(val) 37 | 38 | def test_sub_2_domination_number_of_cycle_is_half_of_nodes(self): 39 | for i in range(3, 13): 40 | G = gp.cycle_graph(i) 41 | assert gp.sub_k_domination_number(G, 2) == ceil(G.order() / 2) 42 | 43 | def test_sub_total_domination_number_of_complete_graph_is_2(self): 44 | for i in range(2, 10): 45 | G = gp.complete_graph(i) 46 | assert gp.sub_total_domination_number(G) == 2 47 | 48 | def test_sub_total_domination_number_of_empty_graph_is_None(self): 49 | for i in range(1, 9): 50 | G = gp.empty_graph(i) 51 | assert gp.sub_total_domination_number(G) == None 52 | 53 | def test_sub_total_domination_number_of_cycle_is_half_of_nodes(self): 54 | for i in range(3, 13): 55 | G = gp.cycle_graph(i) 56 | assert gp.sub_total_domination_number(G) == ceil(G.order() / 2) 57 | 58 | def test_annihilation_number_of_trivial_graph_is_1(self): 59 | G = gp.trivial_graph() 60 | assert gp.annihilation_number(G) == 1 61 | 62 | def test_annihilation_number_of_complete_graph_is_half_of_nodes(self): 63 | for i in range(2, 11): 64 | G = gp.complete_graph(i) 65 | assert gp.annihilation_number(G) == floor(G.order() / 2) 66 | 67 | def test_annihilation_number_of_star_is_order_minus_1(self): 68 | for i in range(2, 11): 69 | G = gp.star_graph(i) 70 | assert gp.annihilation_number(G) == G.order() - 1 71 | -------------------------------------------------------------------------------- /tests/test_functions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import grinpy as gp 4 | 5 | 6 | class TestFunctions: 7 | def test_sequence_of_zeros_is_graphic(self): 8 | assert gp.is_graphic([0, 0, 0, 0]) == True 9 | 10 | def test_descending_sequence_of_integers_is_not_graphic(self): 11 | assert gp.is_graphic([5, 4, 3, 2, 1]) == False 12 | 13 | def test_elimination_sequence_of_complete_graph(self): 14 | G = gp.complete_graph(5) 15 | assert gp.elimination_sequence(G) == [4, 3, 2, 1, 0] 16 | 17 | @pytest.mark.parametrize( 18 | "graph, source, target, expected_value", 19 | ( 20 | (gp.path_graph(5), 0, 4, 4), 21 | (gp.path_graph(5), 1, 4, 3), 22 | (gp.path_graph(5), 2, 4, 2), 23 | (gp.path_graph(5), 3, 4, 1), 24 | (gp.path_graph(5), 4, 4, 0), 25 | ), 26 | ) 27 | def test_distance(self, graph, source, target, expected_value): 28 | """Ensure that distance returns the expected value for a given graph""" 29 | assert gp.distance(graph, source, target) == expected_value 30 | -------------------------------------------------------------------------------- /tests/test_graph_operations.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | 3 | 4 | class TestGraphOperations: 5 | def test_contracting_two_nodes(self): 6 | G = gp.complete_graph(3) 7 | gp.contract_nodes(G, [0, 1]) 8 | assert G.has_node(0) == True 9 | assert G.has_node(2) == True 10 | assert G.has_node(1) == False 11 | assert G.has_edge(0, 2) == True 12 | assert G.has_edge(0, 1) == False 13 | assert G.has_edge(1, 2) == False 14 | 15 | def test_contracting_single_node_does_not_change_graph(self): 16 | G = gp.complete_graph(3) 17 | gp.contract_nodes(G, 0) 18 | assert G.has_node(0) == True 19 | assert G.has_node(2) == True 20 | assert G.has_node(1) == True 21 | assert G.has_edge(0, 2) == True 22 | assert G.has_edge(0, 1) == True 23 | assert G.has_edge(1, 2) == True 24 | -------------------------------------------------------------------------------- /tests/test_havel_hakimi.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | import pytest 3 | 4 | 5 | class TestHavelHakimi: 6 | def test_non_integer_values_raises_TypeError(self): 7 | with pytest.raises(TypeError): 8 | hh = gp.HavelHakimi([3, 3, 1.5, 1]) 9 | 10 | def test_non_iterable_raises_TypeError(self): 11 | with pytest.raises(TypeError): 12 | hh = gp.HavelHakimi(0) 13 | 14 | def test_havel_hakimi_with_integral_floats(self): 15 | hh = gp.HavelHakimi([1.0, 1.0]) 16 | assert hh.residue() == 1 17 | 18 | def test_descending_sequence_of_integers_is_not_graphic(self): 19 | hh = gp.HavelHakimi([5, 4, 3, 2, 1]) 20 | assert hh.is_graphic() == False 21 | 22 | def test_sequence_of_zeros_is_graphic(self): 23 | hh = gp.HavelHakimi([0, 0, 0, 0, 0]) 24 | assert hh.is_graphic() == True 25 | 26 | def test_process_of_compete_graph(self): 27 | G = gp.complete_graph(4) 28 | hh = gp.HavelHakimi(gp.degree_sequence(G)) 29 | p = [[3, 3, 3, 3], [2, 2, 2], [1, 1], [0]] 30 | assert hh.get_process() == p 31 | 32 | def test_elimination_sequence_of_complete_graph(self): 33 | G = gp.complete_graph(4) 34 | hh = gp.HavelHakimi(gp.degree_sequence(G)) 35 | e = [3, 2, 1, 0] 36 | assert hh.get_elimination_sequence() == e 37 | 38 | def test_initial_sequence(self): 39 | G = gp.complete_graph(4) 40 | hh = gp.HavelHakimi(gp.degree_sequence(G)) 41 | assert hh.get_initial_sequence() == [3, 3, 3, 3] 42 | 43 | def test_depth_of_complete_graph_is_order_minus_1(self): 44 | for i in range(2, 12): 45 | G = gp.complete_graph(i) 46 | hh = gp.HavelHakimi(gp.degree_sequence(G)) 47 | assert hh.depth() == G.order() - 1 48 | -------------------------------------------------------------------------------- /tests/test_independence.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | 3 | 4 | def test_single_vertex_is_independent_set(): 5 | G = gp.trivial_graph() 6 | assert gp.is_independent_set(G, [0]) is True 7 | 8 | 9 | def test_single_vertex_is_2_independent_set(): 10 | G = gp.trivial_graph() 11 | assert gp.is_k_independent_set(G, [0], 2) is True 12 | 13 | 14 | def test_set_of_leaves_of_star_is_independent_set(): 15 | for i in range(2, 10): 16 | G = gp.star_graph(i) 17 | ind_set = set(j for j in range(1, i + 1)) 18 | assert gp.is_independent_set(G, ind_set) is True 19 | 20 | 21 | def test_empty_set_is_independent_set_of_trivial_graph(): 22 | G = gp.trivial_graph() 23 | assert gp.is_independent_set(G, set()) is True 24 | 25 | 26 | def test_adjacent_vertices_of_star_is_not_independent_set(): 27 | G = gp.star_graph(3) 28 | assert gp.is_independent_set(G, [0, 1]) is False 29 | assert gp.is_independent_set(G, [0, 2]) is False 30 | 31 | 32 | def test_set_of_leaves_of_star_is_2_independent_set(): 33 | for i in range(2, 10): 34 | G = gp.star_graph(i) 35 | ind_set = set(j for j in range(1, i + 1)) 36 | assert gp.is_k_independent_set(G, ind_set, 2) is True 37 | 38 | 39 | def test_empty_set_is_2_independent_set_of_trivial_graph(): 40 | G = gp.trivial_graph() 41 | assert gp.is_k_independent_set(G, set(), 2) is True 42 | 43 | 44 | def test_center_and_two_leaves_of_star_is_not_2_independent_set(): 45 | G = gp.star_graph(3) 46 | assert gp.is_independent_set(G, [0, 1, 2]) is False 47 | 48 | 49 | def test_independence_number_of_complete_graph_is_1(): 50 | for i in range(1, 13): 51 | G = gp.complete_graph(i) 52 | assert gp.independence_number(G, method="bf") == 1 53 | assert gp.independence_number(G, method="ilp") == 1 54 | 55 | 56 | def test_independence_number_of_star_is_order_minus_1(): 57 | for i in range(1, 10): 58 | G = gp.star_graph(i) 59 | assert gp.independence_number(G, method="bf") == G.order() - 1 60 | assert gp.independence_number(G, method="ilp") == G.order() - 1 61 | 62 | 63 | def test_2_independence_number_of_trivial_graph_is_1(): 64 | G = gp.trivial_graph() 65 | assert gp.k_independence_number(G, 2) == 1 66 | 67 | 68 | def test_2_independence_number_of_complete_graph_is_2(): 69 | for i in range(2, 11): 70 | G = gp.complete_graph(i) 71 | assert gp.k_independence_number(G, 2) == 2 72 | 73 | 74 | def test_2_independence_number_of_C5_is_3(): 75 | G = gp.cycle_graph(5) 76 | assert gp.k_independence_number(G, 2) == 3 77 | 78 | 79 | def test_max_independent_set_of_empty_graph_is_all_nodes(): 80 | for i in range(1, 11): 81 | G = gp.empty_graph(i) 82 | assert gp.max_independent_set(G, method="bf") == set(gp.nodes(G)) 83 | assert gp.max_independent_set(G, method="ilp") == set(gp.nodes(G)) 84 | -------------------------------------------------------------------------------- /tests/test_matching.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | import math 3 | 4 | 5 | def test_matching_number_of_empty_graph_is_0(): 6 | for i in range(1, 11): 7 | G = gp.empty_graph(i) 8 | assert gp.matching_number(G, method="bf") == 0 9 | assert gp.matching_number(G, method="ilp") == 0 10 | 11 | 12 | def test_matching_number_of_path_is_ceil_of_half_of_edges(): 13 | for i in range(2, 12): 14 | P = gp.path_graph(i) 15 | m = math.ceil(gp.number_of_edges(P) / 2) 16 | assert gp.matching_number(P, method="bf") == m 17 | assert gp.matching_number(P, method="ilp") == m 18 | 19 | 20 | def test_matching_number_of_star_is_1(): 21 | for i in range(1, 11): 22 | G = gp.star_graph(i) 23 | assert gp.matching_number(G, method="bf") == 1 24 | assert gp.matching_number(G, method="ilp") == 1 25 | 26 | 27 | def test_min_maximal_matching_number_of_star_is_1(): 28 | for i in range(1, 11): 29 | G = gp.star_graph(i) 30 | assert gp.min_maximal_matching_number(G) == 1 31 | 32 | 33 | def test_min_maximal_matching_of_P2_through_P4_is_1(): 34 | for i in range(2, 5): 35 | P = gp.path_graph(i) 36 | assert gp.min_maximal_matching_number(P) == 1 37 | -------------------------------------------------------------------------------- /tests/test_neighborhoods.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | 3 | 4 | class TestNeighborhoods: 5 | def setup_class(self): 6 | G = gp.Graph() 7 | G.add_edge(0, 1) 8 | G.add_edge(0, 2) 9 | G.add_edge(1, 2) 10 | G.add_edge(0, 3) 11 | G.add_edge(3, 4) 12 | G.add_edge(3, 5) 13 | self.G = G 14 | 15 | def test_neighborhood(self): 16 | G = self.G 17 | N0 = gp.neighborhood(G, 0) 18 | N1 = gp.neighborhood(G, 1) 19 | N2 = gp.neighborhood(G, 2) 20 | N3 = gp.neighborhood(G, 3) 21 | N4 = gp.neighborhood(G, 4) 22 | N5 = gp.neighborhood(G, 5) 23 | assert N0 == [1, 2, 3] 24 | assert N1 == [0, 2] 25 | assert N2 == [0, 1] 26 | assert N3 == [0, 4, 5] 27 | assert N4 == [3] 28 | assert N5 == [3] 29 | 30 | def test_closed_neighborhood(self): 31 | G = self.G 32 | N0 = gp.closed_neighborhood(G, 0) 33 | N1 = gp.closed_neighborhood(G, 1) 34 | N2 = gp.closed_neighborhood(G, 2) 35 | N3 = gp.closed_neighborhood(G, 3) 36 | N4 = gp.closed_neighborhood(G, 4) 37 | N5 = gp.closed_neighborhood(G, 5) 38 | assert N0 == [0, 1, 2, 3] 39 | assert N1 == [0, 1, 2] 40 | assert N2 == [0, 1, 2] 41 | assert N3 == [0, 3, 4, 5] 42 | assert N4 == [3, 4] 43 | assert N5 == [3, 5] 44 | 45 | def test_are_neighbors(self): 46 | G = self.G 47 | t1 = gp.are_neighbors(G, 0, 1) 48 | t2 = gp.are_neighbors(G, 0, 4) 49 | assert t1 == True 50 | assert t2 == False 51 | 52 | def test_common_neighbors_of_pair_of_nodes_in_K3_is_third_node(self): 53 | G = gp.complete_graph(3) 54 | assert gp.common_neighbors(G, [0, 1]) == [2] 55 | 56 | def test_common_neighbors_of_single_node_in_K3_is_other_two_nodes(self): 57 | G = gp.complete_graph(3) 58 | assert gp.common_neighbors(G, [0]) == [1, 2] 59 | -------------------------------------------------------------------------------- /tests/test_power_domination.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | from math import ceil, floor 3 | 4 | 5 | class TestPowerDomination: 6 | def test_center_node_is_power_dominating_set_of_star(self): 7 | for i in range(1, 11): 8 | G = gp.star_graph(i) 9 | assert gp.is_power_dominating_set(G, [0]) == True 10 | 11 | def test_leaf_is_not_power_dominating_set_of_star(self): 12 | for i in range(3, 13): 13 | G = gp.star_graph(i) 14 | for j in range(1, i + 1): 15 | assert gp.is_power_dominating_set(G, [j]) == False 16 | 17 | def test_empty_set_is_not_power_dominating_set_of_trivial_graph(self): 18 | G = gp.trivial_graph() 19 | assert gp.is_power_dominating_set(G, set()) == False 20 | 21 | def test_power_domination_number_of_complete_graph_is_1(self): 22 | for i in range(1, 11): 23 | G = gp.complete_graph(i) 24 | assert gp.power_domination_number(G) == 1 25 | 26 | def test_power_domination_number_of_barbell_is_2(self): 27 | for i in range(3, 13): 28 | G = gp.barbell_graph(i, i) 29 | assert gp.power_domination_number(G) == 2 30 | -------------------------------------------------------------------------------- /tests/test_residue.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | from math import ceil 3 | 4 | 5 | class TestResidue: 6 | def test_residue_of_complete_graph_is_1(self): 7 | for i in range(1, 11): 8 | G = gp.complete_graph(i) 9 | assert gp.residue(G) == 1 10 | 11 | def test_residue_of_cycle_is_third_of_order(self): 12 | for i in range(3, 13): 13 | G = gp.cycle_graph(i) 14 | assert gp.residue(G) == ceil(G.order() / 3) 15 | 16 | def test_2_residue_of_complete_graph_is_three_halves(self): 17 | for i in range(3, 13): 18 | G = gp.complete_graph(i) 19 | assert gp.k_residue(G, 2) == 1.5 20 | 21 | def test_k_residual_index_of_peterson_graph_is_2(self): 22 | G = gp.petersen_graph() 23 | assert gp.k_residual_index(G) == 2 24 | 25 | def test_k_residual_index_of_trivial_graph_is_1(self): 26 | G = gp.trivial_graph() 27 | assert gp.k_residual_index(G) == 1 28 | -------------------------------------------------------------------------------- /tests/test_structural_properties.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | 3 | 4 | class TestDegree: 5 | def test_K5_is_complete_graph(self): 6 | G = gp.complete_graph(5) 7 | assert gp.is_complete_graph(G) == True 8 | 9 | def test_C5_is_not_complete_graph(self): 10 | G = gp.cycle_graph(5) 11 | assert gp.is_complete_graph(G) == False 12 | 13 | def test_K4_is_not_triangle_free(self): 14 | G = gp.complete_graph(4) 15 | assert gp.is_triangle_free(G) == False 16 | 17 | def test_C4_is_triangle_free(self): 18 | G = gp.cycle_graph(4) 19 | assert gp.is_triangle_free(G) == True 20 | 21 | def test_K4_is_bull_free(self): 22 | G = gp.complete_graph(4) 23 | assert gp.is_bull_free(G) == True 24 | 25 | def test_bull_is_not_bull_free(self): 26 | G = gp.complete_graph(3) 27 | G.add_edge(1, 3) 28 | G.add_edge(2, 4) 29 | assert gp.is_bull_free(G) == False 30 | 31 | def test_K4_is_claw_free(self): 32 | G = gp.complete_graph(4) 33 | assert gp.is_claw_free(G) == True 34 | 35 | def test_S4_is_not_claw_free(self): 36 | G = gp.star_graph(4) 37 | assert gp.is_claw_free(G) == False 38 | -------------------------------------------------------------------------------- /tests/test_topological_indices.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import pytest 4 | 5 | import grinpy as gp 6 | 7 | 8 | class TestTopologicalIndices: 9 | @pytest.mark.parametrize( 10 | "graph, expected_value", 11 | ((gp.path_graph(2), 1 ** -0.5), (gp.cycle_graph(3), 1.5), (gp.cycle_graph(4), 2), (gp.cycle_graph(5), 2.5)), 12 | ) 13 | def test_randic_index(self, graph, expected_value): 14 | """Ensure randic_index returns the expected value for a given graph""" 15 | assert gp.randic_index(graph) == expected_value 16 | 17 | @pytest.mark.parametrize( 18 | "graph, k, expected_value", 19 | ( 20 | (gp.path_graph(2), -0.5, 1 ** -0.5), 21 | (gp.cycle_graph(3), -0.5, 1.5), 22 | (gp.cycle_graph(4), -0.5, 2), 23 | (gp.cycle_graph(5), -0.5, 2.5), 24 | ), 25 | ) 26 | def test_generalized_randic_index(self, graph, k, expected_value): 27 | """Ensure randic_index returns the expected value for a given graph""" 28 | assert gp.generalized_randic_index(graph, k) == expected_value 29 | 30 | @pytest.mark.parametrize( 31 | "graph, expected_value", ((gp.path_graph(2), 1), (gp.cycle_graph(3), 1.5), (gp.complete_graph(4), 2)) 32 | ) 33 | def test_augmented_randic_index(self, graph, expected_value): 34 | """Ensure augmented_randic_index returns the expected value for a given graph""" 35 | assert gp.augmented_randic_index(graph) == expected_value 36 | 37 | @pytest.mark.parametrize( 38 | "graph, expected_value", ((gp.path_graph(2), 1), (gp.cycle_graph(3), 1.5), (gp.complete_graph(4), 2)) 39 | ) 40 | def test_harmonic_index(self, graph, expected_value): 41 | """Ensure augmented_randic_index returns the expected value for a given graph""" 42 | assert gp.harmonic_index(graph) == expected_value 43 | 44 | @pytest.mark.parametrize( 45 | "graph, expected_value", 46 | ((gp.path_graph(2), 0), (gp.cycle_graph(3), 3 * math.sqrt(2) / 2), (gp.complete_graph(4), 4.0)), 47 | ) 48 | def test_atom_bond_connectivity_index(self, graph, expected_value): 49 | """Ensure augmented_randic_index returns the expected value for a given graph""" 50 | assert gp.atom_bond_connectivity_index(graph) == expected_value 51 | 52 | @pytest.mark.parametrize( 53 | "graph, expected_value", 54 | ((gp.path_graph(2), 1 / math.sqrt(2)), (gp.cycle_graph(3), 1.5), (gp.complete_graph(4), 6 / math.sqrt(6))), 55 | ) 56 | def test_sum_connectivity_index(self, graph, expected_value): 57 | """Ensure augmented_randic_index returns the expected value for a given graph""" 58 | assert gp.sum_connectivity_index(graph) == expected_value 59 | 60 | @pytest.mark.parametrize( 61 | "graph, expected_value", ((gp.path_graph(2), 2.0), (gp.cycle_graph(3), 12.0), (gp.complete_graph(4), 36.0)) 62 | ) 63 | def test_first_zagreb_index(self, graph, expected_value): 64 | """Ensure augmented_randic_index returns the expected value for a given graph""" 65 | assert gp.first_zagreb_index(graph) == expected_value 66 | 67 | @pytest.mark.parametrize( 68 | "graph, expected_value", ((gp.path_graph(2), 1.0), (gp.cycle_graph(3), 12.0), (gp.complete_graph(4), 54.0)) 69 | ) 70 | def test_second_zagreb_index(self, graph, expected_value): 71 | """Ensure augmented_randic_index returns the expected value for a given graph""" 72 | assert gp.second_zagreb_index(graph) == expected_value 73 | -------------------------------------------------------------------------------- /tests/test_vertex_cover.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | 3 | 4 | def test_min_vertex_cover(): 5 | G = gp.empty_graph() 6 | assert isinstance(gp.min_vertex_cover(G, method="ilp"), set) 7 | 8 | 9 | def test_vertex_cover_of_null_graph(): 10 | G = gp.empty_graph() 11 | assert gp.min_vertex_cover(G, method="ilp") == set() 12 | assert gp.vertex_cover_number(G) == 0 13 | 14 | 15 | def test_vertex_cover_of_empty_graph(): 16 | G = gp.empty_graph(4) 17 | assert gp.vertex_cover_number(G) == 0 18 | 19 | 20 | def test_vertex_cover_number_of_K4_is_3(): 21 | G = gp.complete_graph(4) 22 | assert gp.vertex_cover_number(G) == 3 23 | -------------------------------------------------------------------------------- /tests/test_zero_forcing.py: -------------------------------------------------------------------------------- 1 | import grinpy as gp 2 | import pytest 3 | 4 | 5 | class TestZeroForcing: 6 | def test_non_integral_value_for_k_raises_TypeError_in_is_k_forcing(self): 7 | with pytest.raises(TypeError): 8 | G = gp.star_graph(2) 9 | gp.is_k_forcing_vertex(G, 1, [1], 1.5) 10 | 11 | def test_0_value_for_k_raises_ValueError_in_is_k_forcing(self): 12 | with pytest.raises(ValueError): 13 | G = gp.star_graph(2) 14 | gp.is_k_forcing_vertex(G, 1, [1], 0) 15 | 16 | def test_integral_float_for_k_works(self): 17 | G = gp.star_graph(2) 18 | assert gp.is_k_forcing_vertex(G, 1, [1], 1.0) == True 19 | 20 | def test_leaf_is_zero_forcing_vertex_for_star(self): 21 | G = gp.star_graph(2) 22 | assert gp.is_zero_forcing_vertex(G, 1, [1]) == True 23 | 24 | def test_center_is_not_zero_forcing_vertex_for_star(self): 25 | G = gp.star_graph(2) 26 | assert gp.is_zero_forcing_vertex(G, 0, [0]) == False 27 | 28 | def test_no_vertex_is_zero_forcing_vertex_for_empty_set(self): 29 | G = gp.star_graph(2) 30 | assert gp.is_zero_forcing_vertex(G, 0, set()) == False 31 | assert gp.is_zero_forcing_vertex(G, 1, set()) == False 32 | assert gp.is_zero_forcing_vertex(G, 2, set()) == False 33 | 34 | def test_center_of_S3_is_3_forcing_vertex(self): 35 | G = gp.star_graph(3) 36 | assert gp.is_k_forcing_vertex(G, 0, [0], 3) == True 37 | 38 | def test_center_of_S3_is_not_2_forcing_vertex(self): 39 | G = gp.star_graph(3) 40 | assert gp.is_k_forcing_vertex(G, 0, [0], 2) == False 41 | 42 | def test_leaf_of_star_is_zero_forcing_active_set(self): 43 | G = gp.star_graph(2) 44 | assert gp.is_zero_forcing_active_set(G, [1]) == True 45 | 46 | def test_center_of_star_is_not_zero_forcing_active_set(self): 47 | G = gp.star_graph(2) 48 | assert gp.is_zero_forcing_active_set(G, [0]) == False 49 | 50 | def test_empy_set_is_not_zero_forcing_active_set(self): 51 | G = gp.star_graph(2) 52 | assert gp.is_zero_forcing_active_set(G, set()) == False 53 | 54 | def test_leaf_is_zero_forcing_set_of_path(self): 55 | G = gp.path_graph(3) 56 | assert gp.is_zero_forcing_set(G, [0]) == True 57 | 58 | def test_leaf_is_not_zero_forcing_set_of_S3(self): 59 | G = gp.star_graph(3) 60 | assert gp.is_zero_forcing_set(G, [1]) == False 61 | 62 | def test_leaf_is_max_degree_minus_one_forcing_set_for_star(self): 63 | for i in range(3, 13): 64 | G = gp.star_graph(i) 65 | D = gp.max_degree(G) 66 | assert gp.is_k_forcing_set(G, [1], D - 1) == True 67 | 68 | def test_zero_forcing_number_of_star_is_order_minus_2(self): 69 | for i in range(2, 12): 70 | G = gp.star_graph(i) 71 | assert gp.zero_forcing_number(G) == G.order() - 2 72 | 73 | def test_zero_forcing_number_of_petersen_graph_is_5(self): 74 | G = gp.petersen_graph() 75 | assert gp.zero_forcing_number(G) == 5 76 | 77 | def test_2_forcing_number_of_petersen_graph_is_2(self): 78 | G = gp.petersen_graph() 79 | assert gp.k_forcing_number(G, 2) == 2 80 | 81 | def test_leaf_is_not_total_forcing_set_of_path(self): 82 | G = gp.path_graph(3) 83 | assert gp.is_total_zero_forcing_set(G, [0]) == False 84 | 85 | def test_pair_of_adjacent_nodes_is_total_forcing_set_of_path(self): 86 | G = gp.path_graph(6) 87 | assert gp.is_total_zero_forcing_set(G, [2, 3]) == True 88 | 89 | def test_total_zero_forcing_number_of_path_is_2(self): 90 | G = gp.path_graph(5) 91 | assert gp.total_zero_forcing_number(G) == 2 92 | 93 | def test_connected_zero_forcing_number_of_monster_is_4(self): 94 | G = gp.star_graph(3) 95 | G.add_edge(3, 4) 96 | G.add_edge(3, 5) 97 | assert gp.connected_zero_forcing_number(G) == 4 98 | 99 | def test_non_int_value_for_k_raises_error_in_is_connected_k_forcing(self): 100 | with pytest.raises(TypeError): 101 | G = gp.star_graph(2) 102 | gp.is_connected_k_forcing_set(G, [0], 1.5) 103 | 104 | def test_0_value_for_k_raises_error_in_is_connected_k_forcing(self): 105 | with pytest.raises(ValueError): 106 | G = gp.star_graph(2) 107 | gp.is_connected_k_forcing_set(G, [0], 0) 108 | 109 | def test_non_int_value_for_k_raises_error_in_min_connected_k_forcing(self): 110 | with pytest.raises(TypeError): 111 | G = gp.star_graph(2) 112 | gp.min_connected_k_forcing_set(G, 1.5) 113 | 114 | def test_0_value_for_k_raises_error_in_min_connected_k_forcing(self): 115 | with pytest.raises(ValueError): 116 | G = gp.star_graph(2) 117 | gp.min_connected_k_forcing_set(G, 0) 118 | 119 | def test_non_int_value_for_k_raises_error_in_connected_k_forcing_num(self): 120 | with pytest.raises(TypeError): 121 | G = gp.star_graph(2) 122 | gp.connected_k_forcing_number(G, 1.5) 123 | 124 | def test_0_value_for_k_raises_error_in_connected_k_forcing_num(self): 125 | with pytest.raises(ValueError): 126 | G = gp.star_graph(2) 127 | gp.connected_k_forcing_number(G, 0) 128 | 129 | def test_total_zero_forcing_num_of_trivial_graph_is_None(self): 130 | G = gp.trivial_graph() 131 | assert gp.total_zero_forcing_number(G) == None 132 | 133 | def test_endpoint_is_connected_forcing_set_of_path(self): 134 | G = gp.path_graph(2) 135 | assert gp.is_connected_zero_forcing_set(G, [0]) 136 | 137 | def test_connected_zero_forcing_num_of_disconnected_graph_is_None(self): 138 | G = gp.empty_graph(5) 139 | assert gp.connected_zero_forcing_number(G) == None 140 | --------------------------------------------------------------------------------