diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..a0f9c2e --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,19 @@ +#!/bin/sh + +echo "Script $0 triggered ..." + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +echo "Starting ruff analysis..." + +# quietly run ruff +ruff . --fix + +# use return code to abort commit if necessary +if [ $? != "0" ]; then + echo "Commit aborted. Fix linter issues found by ruff before committing." + exit 1 +fi + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +echo "Pre-commit checks completed successfully." +exit 0 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1eadb8e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,62 @@ +name: Python package + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + + build: + name: Build for (${{ matrix.python-version }}, ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Python info + shell: bash -e {0} + run: | + which python + python --version + - name: Upgrade pip and install dependencies + run: | + python -m pip install --upgrade pip setuptools + python -m pip install .[dev,publishing] + - name: Run unit tests + run: python -m pytest -v + - name: Verify that we can build the package + run: python -m build + + lint: + name: Linting build + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.9 + uses: actions/setup-python@v3 + with: + python-version: 3.9 + - name: Python info + shell: bash -e {0} + run: | + which python + python --version + - name: Upgrade pip and install dependencies + run: | + python -m pip install --upgrade pip setuptools + python -m pip install .[dev,publishing] + - name: Check style against standards using ruff + run: ruff . diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..2bccfd1 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,36 @@ +name: documentation + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build-documentation: + name: Build documentation + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v3 + with: + python-version: 3.9 + - name: Python info + shell: bash -e {0} + run: | + which python + python --version + - name: Upgrade pip and install dependencies + run: | + python -m pip install --upgrade pip setuptools + python -m pip install .[dev,publishing] + - name: Install pandoc using apt + run: sudo apt install pandoc + - name: Build documentation + run: make coverage doctest html + working-directory: docs \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ebc9836 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.0.1] - 1900-12-31 + +### Added + +### Removed + +### Changed + +[Unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/v1.0.0...HEAD +[0.0.1]: https://github.com/olivierlacan/keep-a-changelog/releases/tag/v0.0.1 diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..5f6307d --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,18 @@ +# YAML 1.2 +--- +cff-version: "1.2.0" +title: "pads" +authors: + - + family-names: Renaud + given-names: Nicolas + orcid: "https://orcid.org/0000-0000-0000-0000" +date-released: 20??-MM-DD +doi: +version: "0.1.0" +repository-code: "https://github.com/QuantumApplicationLab/pads_pck" +keywords: + - data + - algorithms +message: "If you use this software, please cite it using these metadata." +license: MIT diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d57974d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,71 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at n.renaud@esciencecenter.nl. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..44a1248 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,68 @@ +# Contributing guidelines + +We welcome any kind of contribution to our software, from simple comment or question to a full fledged [pull request](https://help.github.com/articles/about-pull-requests/). Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md). + +A contribution can be one of the following cases: + +1. you have a question; +1. you think you may have found a bug (including unexpected behavior); +1. you want to make some kind of change to the code base (e.g. to fix a bug, to add a new feature, to update documentation); +1. you want to make a new release of the code base. + +The sections below outline the steps in each case. + +## You have a question + +1. use the search functionality [here](https://github.com/QuantumApplicationLab/pads_pck/issues) to see if someone already filed the same issue; +2. if your issue search did not yield any relevant results, make a new issue; +3. apply the "Question" label; apply other labels when relevant. + +## You think you may have found a bug + +1. use the search functionality [here](https://github.com/QuantumApplicationLab/pads_pck/issues) to see if someone already filed the same issue; +1. if your issue search did not yield any relevant results, make a new issue, making sure to provide enough information to the rest of the community to understand the cause and context of the problem. Depending on the issue, you may want to include: + - the [SHA hashcode](https://help.github.com/articles/autolinked-references-and-urls/#commit-shas) of the commit that is causing your problem; + - some identifying information (name and version number) for dependencies you're using; + - information about the operating system; +1. apply relevant labels to the newly created issue. + +## You want to make some kind of change to the code base + +1. (**important**) announce your plan to the rest of the community *before you start working*. This announcement should be in the form of a (new) issue; +1. (**important**) wait until some kind of consensus is reached about your idea being a good idea; +1. if needed, fork the repository to your own Github profile and create your own feature branch off of the latest main commit. While working on your feature branch, make sure to stay up to date with the main branch by pulling in changes, possibly from the 'upstream' repository (follow the instructions [here](https://help.github.com/articles/configuring-a-remote-for-a-fork/) and [here](https://help.github.com/articles/syncing-a-fork/)); +1. install dependencies (see the [development documentation](README.dev.md#development_install)); +1. make sure the existing tests still work by running ``pytest``; +1. add your own tests (if necessary); +1. update or expand the documentation; +1. update the `CHANGELOG.md` file with your change; +1. [push](http://rogerdudler.github.io/git-guide/) your feature branch to (your fork of) the pads repository on GitHub; +1. create the pull request, e.g. following the instructions [here](https://help.github.com/articles/creating-a-pull-request/). + +In case you feel like you've made a valuable contribution, but you don't know how to write or run tests for it, or how to generate the documentation: don't let this discourage you from making the pull request; we can help you! Just go ahead and submit the pull request, but keep in mind that you might be asked to append additional commits to your pull request. + +## You want to make a new release of the code base + +To create a release you need write permission on the repository. + +1. Check the author list in [`CITATION.cff`](CITATION.cff) +1. Bump the version using `bump-my-version bump `. For example, `bump-my-version bump major` will increase major version numbers everywhere it's needed (code, meta, etc.) in the repo. Alternatively the version can be manually changed in pads/__init__.py, pyproject.toml, CITATION.cff and docs/conf.py (and other places it was possibly added). +1. Update the `CHANGELOG.md` to include changes made +1. Go to the [GitHub release page](https://github.com/QuantumApplicationLab/pads_pck/releases) +1. Press draft a new release button +1. Fill version, title and description field +1. Press the Publish Release button + + + +Also a Zenodo entry will be made for the release with its own DOI. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f128825 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include CITATION.cff +include LICENSE +include NOTICE +include README.md diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..e609da6 --- /dev/null +++ b/NOTICE @@ -0,0 +1,2 @@ +This product includes pads, software developed by +Eppstein David. diff --git a/README.md b/README.md index 9e3aaee..cf21191 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,50 @@ -# PADS: Python Algorithms and Data Structures +## Badges -This repository is a fork of PADS, as implemented by David Eppstein and available at http://www.ics.uci.edu/~eppstein/PADS/. +(Customize these badges with your own links, and check https://shields.io/ or https://badgen.net/ to see which other badges are available.) -# Tests +| fair-software.eu recommendations | | +| :-- | :-- | +| (1/5) code repository | [![github repo badge](https://img.shields.io/badge/github-repo-000.svg?logo=github&labelColor=gray&color=blue)](https://github.com/QuantumApplicationLab/pads_pck) | +| (2/5) license | [![github license badge](https://img.shields.io/github/license/QuantumApplicationLab/pads_pck)](https://github.com/QuantumApplicationLab/pads_pck) | +| (3/5) community registry | [![RSD](https://img.shields.io/badge/rsd-pads-00a3e3.svg)](https://www.research-software.nl/software/pads) [![workflow pypi badge](https://img.shields.io/pypi/v/pads.svg?colorB=blue)](https://pypi.python.org/project/pads/) | +| (4/5) citation | [![DOI](https://zenodo.org/badge/DOI/.svg)](https://doi.org/) | +| (5/5) checklist | [![workflow cii badge](https://bestpractices.coreinfrastructure.org/projects//badge)](https://bestpractices.coreinfrastructure.org/projects/) | +| howfairis | [![fair-software badge](https://img.shields.io/badge/fair--software.eu-%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8B-yellow)](https://fair-software.eu) | +| **Other best practices** |   | +| Static analysis | [![workflow scq badge](https://sonarcloud.io/api/project_badges/measure?project=QuantumApplicationLab_pads_pck&metric=alert_status)](https://sonarcloud.io/dashboard?id=QuantumApplicationLab_pads_pck) | +| Coverage | [![workflow scc badge](https://sonarcloud.io/api/project_badges/measure?project=QuantumApplicationLab_pads_pck&metric=coverage)](https://sonarcloud.io/dashboard?id=QuantumApplicationLab_pads_pck) | +| Documentation | [![Documentation Status](https://readthedocs.org/projects/pads_pck/badge/?version=latest)](https://pads_pck.readthedocs.io/en/latest/?badge=latest) | +| **GitHub Actions** |   | +| Build | [![build](https://github.com/QuantumApplicationLab/pads_pck/actions/workflows/build.yml/badge.svg)](https://github.com/QuantumApplicationLab/pads_pck/actions/workflows/build.yml) | +| Citation data consistency | [![cffconvert](https://github.com/QuantumApplicationLab/pads_pck/actions/workflows/cffconvert.yml/badge.svg)](https://github.com/QuantumApplicationLab/pads_pck/actions/workflows/cffconvert.yml) | +| SonarCloud | [![sonarcloud](https://github.com/QuantumApplicationLab/pads_pck/actions/workflows/sonarcloud.yml/badge.svg)](https://github.com/QuantumApplicationLab/pads_pck/actions/workflows/sonarcloud.yml) | +| MarkDown link checker | [![markdown-link-check](https://github.com/QuantumApplicationLab/pads_pck/actions/workflows/markdown-link-check.yml/badge.svg)](https://github.com/QuantumApplicationLab/pads_pck/actions/workflows/markdown-link-check.yml) | -Run -```bash -python -m unittest -v pads/*.py +## How to use pads + +Python Algorithms and Data Structure + +The project setup is documented in [project_setup.md](project_setup.md). Feel free to remove this document (and/or the link to this document) if you don't need it. + +## Installation + +To install pads from GitHub repository, do: + +```console +git clone git@github.com:QuantumApplicationLab/pads_pck.git +cd pads_pck +python -m pip install . ``` + +## Documentation + +Include a link to your project's full documentation here. + +## Contributing + +If you want to contribute to the development of pads, +have a look at the [contribution guidelines](CONTRIBUTING.md). + +## Credits + +This package was created with [Cookiecutter](https://github.com/audreyr/cookiecutter) and the [NLeSC/python-template](https://github.com/NLeSC/python-template). diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..6d69782 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = pads +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_templates/.gitignore b/docs/_templates/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..b6633b5 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,89 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html +# + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +# -- Project information ----------------------------------------------------- + +project = "pads" +copyright = "2024, Eppstein David" +author = "Nicolas Renaud" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = "0.1.0" +# The full version, including alpha/beta/rc tags. +release = version + +# -- General configuration ------------------------------------------------ + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named "sphinx.ext.*") or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "autoapi.extension", + "myst_parser", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + +# -- Use autoapi.extension to run sphinx-apidoc ------- + +autoapi_dirs = ["../pads"] + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# -- Options for Intersphinx + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + # Commonly used libraries, uncomment when used in package + # 'numpy': ('http://docs.scipy.org/doc/numpy/', None), + # 'scipy': ('http://docs.scipy.org/doc/scipy/reference/', None), + # 'scikit-learn': ('https://scikit-learn.org/stable/', None), + # 'matplotlib': ('https://matplotlib.org/stable/', None), + # 'pandas': ('http://pandas.pydata.org/docs/', None), +} diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..82a61ec --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,18 @@ +.. pads documentation master file, created by + sphinx-quickstart on Wed May 5 22:45:36 2021. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to pads's documentation! +========================================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..1942987 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=pads + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/pads/AcyclicReachability.py b/pads/AcyclicReachability.py index dbb9b99..6b2cc52 100644 --- a/pads/AcyclicReachability.py +++ b/pads/AcyclicReachability.py @@ -1,4 +1,4 @@ -"""AcyclicReachability.py +"""AcyclicReachability.py. Bit-parallel algorithm for testing which vertices can reach which other vertices in a DAG. @@ -17,8 +17,9 @@ import unittest from .PartialOrder import TopologicalOrder + class Reachability: - def __init__(self,G): + def __init__(self, G): """Initialize a reachability data structure for the given DAG.""" self.key = {} self.canReach = [] @@ -26,23 +27,26 @@ def __init__(self,G): L.reverse() for v in L: k = self.key[v] = len(self.canReach) - bits = 1<= other and self != other def __invert__(self): """Complement (with respect to alphabet) of language.""" return Language(self.recognizer.complement()) - def __and__(self,other): + def __and__(self, other): """Intersection of two languages with the same alphabet.""" - if not isinstance(other,RegularLanguage): + if not isinstance(other, RegularLanguage): raise LanguageError("Unable to intersect nonregular language") return Language(self.recognizer.intersection(other.recognizer)) - def __or__(self,other): + def __or__(self, other): """Union of two languages with the same alphabet.""" - if not isinstance(other,RegularLanguage): + if not isinstance(other, RegularLanguage): raise LanguageError("Unable to intersect nonregular language") return Language(self.recognizer.union(other.recognizer)) - def __xor__(self,other): + def __xor__(self, other): """Symmetric difference of two languages with the same alphabet.""" - if not isinstance(other,RegularLanguage): + if not isinstance(other, RegularLanguage): raise LanguageError("Unable to intersect nonregular language") return Language(self.recognizer.symmetricDifference(other.recognizer)) @@ -95,6 +100,7 @@ def __nonzero__(self): return True return False + class FiniteAutomaton: """Base class for DFA and NFA. This class should not be instantiated on its own, but dispatches methods that are appropriate to both types @@ -116,7 +122,7 @@ def __len__(self): """How many states does this automaton have?""" return len(list(self.states())) - def __call__(self,symbols): + def __call__(self, symbols): """Test whether sequence of symbols is accepted by the DFA.""" return self.asDFA()(symbols) @@ -128,7 +134,7 @@ def states(self): """Generate all states reachable from initial state.""" return self.asNFA().states() - def pprint(self,output=sys.stdout): + def pprint(self, output=sys.stdout): """Pretty-print this automaton to an output stream.""" return self.asNFA().pprint(output) @@ -140,9 +146,9 @@ def reverse(self): """Construct NFA for reversal of original NFA's language.""" return _ReverseNFA(self.asNFA()) - def renumber(self,offset=0): + def renumber(self, offset=0): """Replace complicated state objects by small integers.""" - return _RenumberNFA(self.asNFA(),offset=offset) + return _RenumberNFA(self.asNFA(), offset=offset) def RegExp(self): """Return equivalent regular expression.""" @@ -152,52 +158,52 @@ def complement(self): """Make automaton recognizing complement of given automaton's language.""" return _ComplementDFA(self.asDFA()) - def union(self,other): + def union(self, other): """Make automaton recognizing union of two automata's languages.""" - return _ProductDFA(self.asDFA(),other.asDFA(),operator.or_) + return _ProductDFA(self.asDFA(), other.asDFA(), operator.or_) - def intersection(self,other): + def intersection(self, other): """Make automaton recognizing union of two automata's languages.""" - return _ProductDFA(self.asDFA(),other.asDFA(),operator.and_) + return _ProductDFA(self.asDFA(), other.asDFA(), operator.and_) - def symmetricDifference(self,other): + def symmetricDifference(self, other): """Make automaton recognizing union of two automata's languages.""" - return _ProductDFA(self.asDFA(),other.asDFA(),operator.xor) + return _ProductDFA(self.asDFA(), other.asDFA(), operator.xor) + class DFA(FiniteAutomaton): """Base class for deterministic finite automaton. Subclasses are responsible for filling out the details of the initial state, alphabet, and transition function. """ + def asDFA(self): return self def asNFA(self): return _NFAfromDFA(self) - def __call__(self,symbols): + def __call__(self, symbols): """Test whether sequence of symbols is accepted by the DFA.""" state = self.initial for symbol in symbols: if symbol not in self.alphabet: - raise LanguageError("Symbol " + repr(symbol) + - " not in input alphabet") - state = self.transition(state,symbol) + raise LanguageError("Symbol " + repr(symbol) + " not in input alphabet") + state = self.transition(state, symbol) return self.isfinal(state) - def __eq__(self,other): + def __eq__(self, other): """Report whether these two DFAs have equivalent states.""" - if not isinstance(other,DFA) or len(self) != len(other) \ - or self.alphabet != other.alphabet: + if not isinstance(other, DFA) or len(self) != len(other) or self.alphabet != other.alphabet: return False - equivalences = {self.initial:other.initial} + equivalences = {self.initial: other.initial} unprocessed = [self.initial] while unprocessed: x = unprocessed.pop() y = equivalences[x] for c in self.alphabet: - xc = self.transition(x,c) - yc = other.transition(y,c) + xc = self.transition(x, c) + yc = other.transition(y, c) if xc not in equivalences: equivalences[xc] = yc unprocessed.append(xc) @@ -205,10 +211,11 @@ def __eq__(self,other): return False return True - def __ne__(self,other): + def __ne__(self, other): """Report whether these two DFAs have equivalent states.""" return not (self == other) + class NFA(FiniteAutomaton): """Base class for nondeterministic finite automaton. Subclasses are responsible for filling out the details of the initial state, alphabet, @@ -216,6 +223,7 @@ class NFA(FiniteAutomaton): epsilon-transitions. Results of self.initial and self.transition are assumed to be represented as frozenset instances. """ + def asNFA(self): return self @@ -231,9 +239,9 @@ def states(self): unvisited.remove(state) visited.add(state) for symbol in self.alphabet: - unvisited |= self.transition(state,symbol) - visited + unvisited |= self.transition(state, symbol) - visited - def pprint(self,output=sys.stdout): + def pprint(self, output=sys.stdout): """Pretty-print this NFA to an output stream.""" for state in self.states(): adjectives = [] @@ -241,15 +249,15 @@ def pprint(self,output=sys.stdout): adjectives.append("initial") if self.isfinal(state): adjectives.append("accepting") - if not [c for c in self.alphabet if self.transition(state,c)]: + if not [c for c in self.alphabet if self.transition(state, c)]: adjectives.append("terminal") if not adjectives: - print >>output, state + print >> output, state else: - print >>output, state, "(" + ", ".join(adjectives) + ")" + print >> output, state, "(" + ", ".join(adjectives) + ")" for c in self.alphabet: - for neighbor in self.transition(state,c): - print >>output, " --[" + str(c) + "]-->", neighbor + for neighbor in self.transition(state, c): + print >> output, " --[" + str(c) + "]-->", neighbor def RegExp(self): """Convert to regular expression and return as a string. @@ -258,57 +266,58 @@ def RegExp(self): # create artificial initial and final states initial = object() final = object() - states = {initial,final} | set(self.states()) + states = {initial, final} | set(self.states()) # 2d matrix of expressions connecting each pair of states expr = {} for x in states: for y in states: - expr[x,y] = None + expr[x, y] = None for x in self.states(): if x in self.initial: - expr[initial,x] = '' + expr[initial, x] = "" if self.isfinal(x): - expr[x,final] = '' - expr[x,x] = '' + expr[x, final] = "" + expr[x, x] = "" for x in self.states(): for c in self.alphabet: - for y in self.transition(x,c): - if expr[x,y]: - expr[x,y] += '+' + str(c) + for y in self.transition(x, c): + if expr[x, y]: + expr[x, y] += "+" + str(c) else: - expr[x,y] = str(c) + expr[x, y] = str(c) # eliminate states one at a time for s in self.states(): states.remove(s) for x in states: for y in states: - if expr[x,s] is not None and expr[s,y] is not None: + if expr[x, s] is not None and expr[s, y] is not None: xsy = [] - if expr[x,s]: - xsy += self._parenthesize(expr[x,s]) - if expr[s,s]: - xsy += self._parenthesize(expr[s,s],True) + ['*'] - if expr[s,y]: - xsy += self._parenthesize(expr[s,y]) - if expr[x,y] is not None: - xsy += ['+',expr[x,y] or '()'] - expr[x,y] = ''.join(xsy) - return expr[initial,final] - - def _parenthesize(self,expr,starring=False): + if expr[x, s]: + xsy += self._parenthesize(expr[x, s]) + if expr[s, s]: + xsy += self._parenthesize(expr[s, s], True) + ["*"] + if expr[s, y]: + xsy += self._parenthesize(expr[s, y]) + if expr[x, y] is not None: + xsy += ["+", expr[x, y] or "()"] + expr[x, y] = "".join(xsy) + return expr[initial, final] + + def _parenthesize(self, expr, starring=False): """Return list of strings with or without parens for use in RegExp. This is only for the purpose of simplifying the expressions returned, by omitting parentheses or other expression features when unnecessary; it would always be correct simply to return ['(',expr,')']. """ - if len(expr) == 1 or (not starring and '+' not in expr): + if len(expr) == 1 or (not starring and "+" not in expr): return [expr] - elif starring and expr.endswith('+()'): - return ['(',expr[:-3],')'] # +epsilon redundant when starring + elif starring and expr.endswith("+()"): + return ["(", expr[:-3], ")"] # +epsilon redundant when starring else: - return ['(',expr,')'] + return ["(", expr, ")"] + class _DFAfromNFA(DFA): """Conversion of NFA to DFA. We create a DFA state for each set @@ -316,49 +325,53 @@ class _DFAfromNFA(DFA): final NFA set, and the transition function for a DFA state is the union of the transition functions of the NFA states it contains. """ - def __init__(self,N): + + def __init__(self, N): self.initial = frozenset(N.initial) self.alphabet = N.alphabet self.NFA = N - def transition(self,stateset,symbol): + def transition(self, stateset, symbol): result = set() for state in stateset: - result |= self.NFA.transition(state,symbol) + result |= self.NFA.transition(state, symbol) return frozenset(result) - def isfinal(self,stateset): + def isfinal(self, stateset): for state in stateset: if self.NFA.isfinal(state): return True return False + class _NFAfromDFA(NFA): """Conversion of DFA to NFA. We convert the initial state and the results of each transition function into single-element sets. """ - def __init__(self,D): + + def __init__(self, D): self.initial = frozenset([D.initial]) self.alphabet = D.alphabet self.DFA = D - def transition(self,state,symbol): - return frozenset([self.DFA.transition(state,symbol)]) + def transition(self, state, symbol): + return frozenset([self.DFA.transition(state, symbol)]) - def isfinal(self,state): + def isfinal(self, state): return self.DFA.isfinal(state) + class RegExp(NFA): """Convert regular expression to NFA.""" - def __init__(self,expr): + def __init__(self, expr): self.expr = expr self.pos = 0 self.nstates = 0 self.expect = {} self.successor = {} self.alphabet = set() - self.initial,penultimate,epsilon = self.expression() + self.initial, penultimate, epsilon = self.expression() final = self.newstate(None) for state in penultimate: self.successor[state].add(final) @@ -366,14 +379,14 @@ def __init__(self,expr): if epsilon: self.final = self.final | self.initial - def transition(self,state,c): + def transition(self, state, c): """Implement NFA transition function.""" if c != self.expect[state]: return frozenset() else: return self.successor[state] - def isfinal(self,state): + def isfinal(self, state): """Implement NFA acceptance test.""" return state in self.final @@ -387,9 +400,9 @@ def isfinal(self,state): def epsilon(self): """Parse an empty string and return an empty automaton.""" - return frozenset(),frozenset(),True + return frozenset(), frozenset(), True - def newstate(self,expect): + def newstate(self, expect): """Allocate a new state in which we expect to see the given letter.""" state = self.nstates self.successor[state] = set() @@ -399,16 +412,16 @@ def newstate(self,expect): def base(self): """Parse a subexpression that can be starred: single letter or group.""" - if self.pos == len(self.expr) or self.expr[self.pos] == ')': + if self.pos == len(self.expr) or self.expr[self.pos] == ")": return self.epsilon() - if self.expr[self.pos] == '(': + if self.expr[self.pos] == "(": self.pos += 1 ret = self.expression() - if self.pos == len(self.expr) or self.expr[self.pos] != ')': + if self.pos == len(self.expr) or self.expr[self.pos] != ")": raise LanguageError("Close paren expected at char " + str(self.pos)) self.pos += 1 return ret - if self.expr[self.pos] == '\\': + if self.expr[self.pos] == "\\": self.pos += 1 if self.pos == len(self.expr): raise RegExpError("Character expected after backslash") @@ -416,23 +429,23 @@ def base(self): state = self.newstate(self.expr[self.pos]) self.pos += 1 state = frozenset([state]) - return state,state,False + return state, state, False def factor(self): """Parse a catenable expression: base or starred base.""" - initial,penultimate,epsilon = self.base() - while self.pos < len(self.expr) and self.expr[self.pos] == '*': + initial, penultimate, epsilon = self.base() + while self.pos < len(self.expr) and self.expr[self.pos] == "*": self.pos += 1 for state in penultimate: self.successor[state] |= initial epsilon = True - return initial,penultimate,epsilon + return initial, penultimate, epsilon def term(self): """Parse a summable expression: factor or concatenation.""" - initial,penultimate,epsilon = self.factor() - while self.pos < len(self.expr) and self.expr[self.pos] not in ')+': - Fi,Fp,Fe = self.factor() + initial, penultimate, epsilon = self.factor() + while self.pos < len(self.expr) and self.expr[self.pos] not in ")+": + Fi, Fp, Fe = self.factor() for state in penultimate: self.successor[state] |= Fi if epsilon: @@ -442,34 +455,37 @@ def term(self): else: penultimate = Fp epsilon = epsilon and Fe - return initial,penultimate,epsilon + return initial, penultimate, epsilon def expression(self): """Parse a whole regular expression or grouped subexpression.""" - initial,penultimate,epsilon = self.term() - while self.pos < len(self.expr) and self.expr[self.pos] == '+': + initial, penultimate, epsilon = self.term() + while self.pos < len(self.expr) and self.expr[self.pos] == "+": self.pos += 1 - Ti,Tp,Te = self.term() + Ti, Tp, Te = self.term() initial = initial | Ti penultimate = penultimate | Tp epsilon = epsilon or Te - return initial,penultimate,epsilon + return initial, penultimate, epsilon + class LookupNFA(NFA): """Construct NFA with precomputed lookup table of transitions.""" - def __init__(self,alphabet,initial,ttable,final): + + def __init__(self, alphabet, initial, ttable, final): self.alphabet = alphabet self.initial = frozenset(initial) self.ttable = ttable self.final = frozenset(final) - def transition(self,state,symbol): - return frozenset(self.ttable[state,symbol]) + def transition(self, state, symbol): + return frozenset(self.ttable[state, symbol]) - def isfinal(self,state): + def isfinal(self, state): return state in self.final -def _RenumberNFA(N,offset=0): + +def _RenumberNFA(N, offset=0): """Replace NFA state objects with small integers.""" replacements = {} for x in N.states(): @@ -479,74 +495,79 @@ def _RenumberNFA(N,offset=0): ttable = {} for state in N.states(): for symbol in N.alphabet: - ttable[replacements[state],symbol] = [replacements[x] - for x in N.transition(state,symbol)] + ttable[replacements[state], symbol] = [replacements[x] for x in N.transition(state, symbol)] final = [replacements[x] for x in N.states() if N.isfinal(x)] - return LookupNFA(N.alphabet,initial,ttable,final) + return LookupNFA(N.alphabet, initial, ttable, final) + class _ProductDFA(DFA): """DFA that simulates D1 and D2 and combines their outputs with op.""" - def __init__(self,D1,D2,op): + + def __init__(self, D1, D2, op): if D1.alphabet != D2.alphabet: raise LanguageError("DFAs have incompatible alphabets") self.alphabet = D1.alphabet - self.initial = (D1.initial,D2.initial) + self.initial = (D1.initial, D2.initial) self.D1 = D1 self.D2 = D2 self.op = op - def transition(self,state,symbol): - s1,s2 = state - return self.D1.transition(s1,symbol), \ - self.D2.transition(s2,symbol) + def transition(self, state, symbol): + s1, s2 = state + return self.D1.transition(s1, symbol), self.D2.transition(s2, symbol) - def isfinal(self,state): - s1,s2 = state + def isfinal(self, state): + s1, s2 = state f1 = self.D1.isfinal(s1) and 1 or 0 f2 = self.D2.isfinal(s2) and 1 or 0 - return self.op(f1,f2) + return self.op(f1, f2) + def _ReverseNFA(N): """Construct NFA for reversal of original NFA's language.""" initial = [s for s in N.states() if N.isfinal(s)] - ttable = {(s,c):[] for s in N.states() for c in N.alphabet} + ttable = {(s, c): [] for s in N.states() for c in N.alphabet} for s in N.states(): for c in N.alphabet: - for t in N.transition(s,c): - ttable[t,c].append(s) - return LookupNFA(N.alphabet,initial,ttable,N.initial) + for t in N.transition(s, c): + ttable[t, c].append(s) + return LookupNFA(N.alphabet, initial, ttable, N.initial) + class _ComplementDFA(DFA): """DFA for complementary language.""" - def __init__(self,D): + + def __init__(self, D): self.DFA = D self.initial = D.initial self.alphabet = D.alphabet - def transition(self,state,symbol): - return self.DFA.transition(state,symbol) + def transition(self, state, symbol): + return self.DFA.transition(state, symbol) - def isfinal(self,state): + def isfinal(self, state): return not self.DFA.isfinal(state) + class _MinimumDFA(DFA): """Construct equivalent DFA with minimum number of states, using Hopcroft's O(ns log n) partition-refinement algorithm. """ - def __init__(self,D): + + def __init__(self, D): # refine partition of states by reversed neighborhoods N = D.reverse() P = PartitionRefinement(D.states()) P.refine([s for s in D.states() if D.isfinal(s)]) - unrefined = Sequence(P,key=id) + unrefined = Sequence(P, key=id) while unrefined: part = arbitrary_item(unrefined) unrefined.remove(part) for symbol in D.alphabet: neighbors = set() for state in part: - neighbors |= N.transition(state,symbol) - for new,old in P.refine(neighbors): + neighbors |= N.transition(state, symbol) + for new, old in P.refine(neighbors): if old in unrefined or len(new) < len(old): unrefined.append(new) else: @@ -559,27 +580,29 @@ def __init__(self,D): self.alphabet = D.alphabet self.DFA = D - def transition(self,state,symbol): + def transition(self, state, symbol): rep = arbitrary_item(state) - return self.partition[self.DFA.transition(rep,symbol)] + return self.partition[self.DFA.transition(rep, symbol)] - def isfinal(self,state): + def isfinal(self, state): rep = arbitrary_item(state) return self.DFA.isfinal(rep) + # If called as standalone routine, run some unit tests + class RegExpTest(unittest.TestCase): # tuples (L,[strings in L],[strings not in L]) languages = [ - (RegularLanguage("0"), ["0"], ["","00"]), - (RegularLanguage("(10+0)*"), ["","0","010"], ["1"]), + (RegularLanguage("0"), ["0"], ["", "00"]), + (RegularLanguage("(10+0)*"), ["", "0", "010"], ["1"]), (RegularLanguage("(0+1)*1(0+1)(0+1)"), ["000100"], ["0011"]), ] def testMembership(self): """membership tests for RegularLanguage(expression)""" - for L,Li,Lx in self.languages: + for L, Li, Lx in self.languages: for S in Li: self.assertTrue(S in L) for S in Lx: @@ -587,7 +610,7 @@ def testMembership(self): def testComplement(self): """membership tests for ~RegularLanguage""" - for L,Li,Lx in self.languages: + for L, Li, Lx in self.languages: L = ~L for S in Lx: self.assertTrue(S in L) @@ -596,17 +619,16 @@ def testComplement(self): def testEquivalent(self): """test that converting NFA->expr->NFA produces same language""" - for L1,Li,Lx in self.languages: + for L1, Li, Lx in self.languages: L2 = RegularLanguage(L1.recognizer.RegExp()) - self.assertEqual(L1,L2) + self.assertEqual(L1, L2) def testInequivalent(self): """test that different regular languages are recognized as different""" for i in range(len(self.languages)): for j in range(i): - self.assertNotEqual(self.languages[i][0], - self.languages[j][0]) + self.assertNotEqual(self.languages[i][0], self.languages[j][0]) -if __name__ == "__main__": - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/pads/BFS.py b/pads/BFS.py index 25961d9..255bf78 100644 --- a/pads/BFS.py +++ b/pads/BFS.py @@ -5,7 +5,8 @@ D. Eppstein, May 2007. """ -def BreadthFirstLevels(G,root): + +def BreadthFirstLevels(G, root): """ Generate a sequence of bipartite directed graphs, each consisting of the edges from level i to level i+1 of G. Edges that connect @@ -19,7 +20,7 @@ def BreadthFirstLevels(G,root): for v in currentLevel: visited.add(v) nextLevel = set() - levelGraph = {v:set() for v in currentLevel} + levelGraph = {v: set() for v in currentLevel} for v in currentLevel: for w in G[v]: if w not in visited: diff --git a/pads/Bicliques.py b/pads/Bicliques.py index 75b673a..d66c080 100644 --- a/pads/Bicliques.py +++ b/pads/Bicliques.py @@ -17,52 +17,55 @@ from .Subsets import subsets import unittest + def Bicliques(G): D = degeneracyOrientation(G) B = {} for v in G: - for N in subsets(D[v]): # all subsets of outgoing neighbors - if len(N) > 1: # of big enough size to be interesting + for N in subsets(D[v]): # all subsets of outgoing neighbors + if len(N) > 1: # of big enough size to be interesting F = frozenset(N) if F not in B: B[F] = {v} else: B[F].add(v) - def adjacent(v,w): + def adjacent(v, w): return v in D[w] or w in D[v] - - def adjacentToAll(v,S): + + def adjacentToAll(v, S): for w in S: - if not adjacent(v,w): + if not adjacent(v, w): return False return True - for F in B: # found incoming neighbors, now need outgoing - if len(F) > 0: # ignore empty and single-vertex sets + for F in B: # found incoming neighbors, now need outgoing + if len(F) > 0: # ignore empty and single-vertex sets done = set() - for v in F: # pick a vertex - for w in D[v]: # try outgoing neighbors - if w not in done and adjacentToAll(w,F): + for v in F: # pick a vertex + for w in D[v]: # try outgoing neighbors + if w not in done and adjacentToAll(w, F): B[F].add(w) done.add(w) - for F in list(B): # add backlinks from subsets to subsets + for F in list(B): # add backlinks from subsets to subsets G = B[F] = frozenset(B[F]) # but only to the biggest ones if G not in B or len(B[G]) < len(F): B[G] = F - output = set() # keep track of what we already output - for F in B: # so we only list one of (F,G) and (G,F) + output = set() # keep track of what we already output + for F in B: # so we only list one of (F,G) and (G,F) G = B[F] - if len(F) > 1 and len(G) > 1 and (G,F) not in output: - yield F,G - output.add((F,G)) + if len(F) > 1 and len(G) > 1 and (G, F) not in output: + yield F, G + output.add((F, G)) + # ============================================================ # If run from command line, perform unit tests # ============================================================ + class BicliqueTest(unittest.TestCase): def testComplete(self): K = {} @@ -72,24 +75,29 @@ def testComplete(self): if i != j: K[i].add(j) L = list(Bicliques(K)) - self.assertEqual(len(L),10) - for F,G in L: - self.assertEqual(frozenset((len(F),len(G))),frozenset((2,3))) + self.assertEqual(len(L), 10) + for F, G in L: + self.assertEqual(frozenset((len(F), len(G))), frozenset((2, 3))) def testGrid(self): G = {} for i in range(5): for j in range(5): - G[i,j] = [] - if i < 4: G[i,j].append((i+1,j)) - if i > 0: G[i,j].append((i-1,j)) - if j < 4: G[i,j].append((i,j+1)) - if j > 0: G[i,j].append((i,j-1)) + G[i, j] = [] + if i < 4: + G[i, j].append((i + 1, j)) + if i > 0: + G[i, j].append((i - 1, j)) + if j < 4: + G[i, j].append((i, j + 1)) + if j > 0: + G[i, j].append((i, j - 1)) L = list(Bicliques(G)) - self.assertEqual(len(L),16) - for F,G in L: - self.assertEqual(len(F),2) - self.assertEqual(len(G),2) + self.assertEqual(len(L), 16) + for F, G in L: + self.assertEqual(len(F), 2) + self.assertEqual(len(G), 2) + if __name__ == "__main__": - unittest.main() + unittest.main() diff --git a/pads/Biconnectivity.py b/pads/Biconnectivity.py index da27e5e..da8ab1c 100644 --- a/pads/Biconnectivity.py +++ b/pads/Biconnectivity.py @@ -12,7 +12,8 @@ from . import DFS -disconnected = object() # flag for BiconnectedComponents +disconnected = object() # flag for BiconnectedComponents + class BiconnectedComponents(DFS.Searcher): """ @@ -23,7 +24,7 @@ class BiconnectedComponents(DFS.Searcher): The result of BiconnectedComponents(G) is a sequence of subgraphs of G. """ - def __init__(self,G): + def __init__(self, G): """Search for biconnected components of graph G.""" if not isUndirected(G): raise ValueError("BiconnectedComponents: input not undirected graph") @@ -34,10 +35,10 @@ def __init__(self,G): self._activelen = {} self._active = [] self._low = {} - self._ancestors = {} # directed subgraph from nodes to DFS ancestors + self._ancestors = {} # directed subgraph from nodes to DFS ancestors # perform the Depth First Search - DFS.Searcher.__init__(self,G) + DFS.Searcher.__init__(self, G) # clean up now-useless data structures del self._dfsnumber, self._activelen, self._active @@ -47,7 +48,7 @@ def __iter__(self): """Return iterator for sequence of biconnected components.""" return iter(self._components) - def preorder(self,parent,child): + def preorder(self, parent, child): if parent == child: self._active = [child] else: @@ -56,22 +57,21 @@ def preorder(self,parent,child): self._ancestors[child] = set() self._activelen[child] = len(self._active) - def backedge(self,source,destination): + def backedge(self, source, destination): if self._dfsnumber[destination] < self._dfsnumber[source]: - self._low[source] = min(self._low[source], - self._dfsnumber[destination]) + self._low[source] = min(self._low[source], self._dfsnumber[destination]) self._ancestors[source].add(destination) - def postorder(self,parent,child): + def postorder(self, parent, child): if self._low[child] != self._dfsnumber[parent]: - self._low[parent] = min(self._low[parent],self._low[child]) + self._low[parent] = min(self._low[parent], self._low[child]) self._activelen[parent] = len(self._active) elif parent != child: - self._component(self._activelen[parent],parent) + self._component(self._activelen[parent], parent) elif not self._components or child not in self._components[-1]: self._component() - def _component(self,start=0, articulation_point=disconnected): + def _component(self, start=0, articulation_point=disconnected): """Make new component, removing active vertices from start onward.""" component = {} if articulation_point is not disconnected: @@ -85,7 +85,9 @@ def _component(self,start=0, articulation_point=disconnected): self._components.append(component) -class NotBiconnected(Exception): pass +class NotBiconnected(Exception): + pass + class BiconnectivityTester(DFS.Searcher): """ @@ -94,34 +96,34 @@ class BiconnectivityTester(DFS.Searcher): Otherwise does nothing. """ - def __init__(self,G): + def __init__(self, G): """Search for biconnected components of graph G.""" if not isUndirected(G): raise NotBiconnected self._dfsnumber = {} self._low = {} self._rootedge = None - DFS.Searcher.__init__(self,G) + DFS.Searcher.__init__(self, G) - def preorder(self,parent,child): + def preorder(self, parent, child): if parent == child and self._rootedge: - raise NotBiconnected # two roots, not even connected + raise NotBiconnected # two roots, not even connected elif not self._rootedge and parent != child: - self._rootedge = (parent,child) + self._rootedge = (parent, child) self._low[child] = self._dfsnumber[child] = len(self._dfsnumber) - def backedge(self,source,destination): - self._low[source] = min(self._low[source],self._dfsnumber[destination]) + def backedge(self, source, destination): + self._low[source] = min(self._low[source], self._dfsnumber[destination]) - def postorder(self,parent,child): + def postorder(self, parent, child): if self._low[child] != self._dfsnumber[parent]: - self._low[parent] = min(self._low[parent],self._low[child]) - elif (parent,child) == self._rootedge: - pass # end of first component, >1 vertices + self._low[parent] = min(self._low[parent], self._low[child]) + elif (parent, child) == self._rootedge: + pass # end of first component, >1 vertices elif parent != child: - raise NotBiconnected # articulation point + raise NotBiconnected # articulation point elif not self._rootedge: - self._rootedge = parent,child # end of first component, isolani + self._rootedge = parent, child # end of first component, isolani def isBiconnected(G): @@ -138,7 +140,7 @@ class stOrienter(DFS.Searcher): Subclass for st-orienting a biconnected graph. """ - def __init__(self,G): + def __init__(self, G): """Relate edges for st-orientation.""" if not isUndirected(G): raise ValueError("stOrienter: input not undirected graph") @@ -146,17 +148,17 @@ def __init__(self,G): # set up data structures for DFS self._dfsnumber = {} self._low = {} - self._down = {} # down[v] = child we're currently exploring from v - self._lowv = {} # lowv[n] = vertex with low number n - + self._down = {} # down[v] = child we're currently exploring from v + self._lowv = {} # lowv[n] = vertex with low number n + # The main data structure! # a dictionary mapping edges to lists of edges # each of which should be oriented the same as the key. self.orient = {} - self.roots = [] # edges with no predecessor + self.roots = [] # edges with no predecessor # perform the Depth First Search - DFS.Searcher.__init__(self,G) + DFS.Searcher.__init__(self, G) # clean up now-useless data structures del self._dfsnumber, self._low, self._down, self._lowv @@ -165,31 +167,31 @@ def __iter__(self): """Return iterator for sequence of biconnected components.""" return iter(self._components) - def preorder(self,parent,child): + def preorder(self, parent, child): self._low[child] = self._dfsnumber[child] = len(self._dfsnumber) self._lowv[self._low[child]] = self._down[parent] = child - def backedge(self,source,destination): + def backedge(self, source, destination): if self._dfsnumber[destination] < self._dfsnumber[source]: - self._low[source] = min(self._low[source], - self._dfsnumber[destination]) + self._low[source] = min(self._low[source], self._dfsnumber[destination]) if source != self._down[destination]: - self.addOrientation(destination,source,destination) + self.addOrientation(destination, source, destination) - def postorder(self,parent,child): + def postorder(self, parent, child): if self._low[child] != self._dfsnumber[parent]: - self._low[parent] = min(self._low[parent],self._low[child]) - self.addOrientation(child,parent,self._lowv[self._low[child]]) + self._low[parent] = min(self._low[parent], self._low[child]) + self.addOrientation(child, parent, self._lowv[self._low[child]]) elif parent != child: - self.roots.append((parent,child)) + self.roots.append((parent, child)) - def addOrientation(self,source,dest,anchor): + def addOrientation(self, source, dest, anchor): """Store orientation info for source->dest edge. It should be oriented the same as the edge from the anchor to the current child of the anchor.""" child = self._down[anchor] - L = self.orient.setdefault((anchor,child),[]) - L.append((source,dest)) + L = self.orient.setdefault((anchor, child), []) + L.append((source, dest)) + def stOrientation(G): """Find an acyclic orientation of G, with one source and one sink.""" @@ -197,45 +199,47 @@ def stOrientation(G): if len(stO.roots) != 1: raise NotBiconnected - source,dest = stO.roots[0] - G = {v:set() for v in G} + source, dest = stO.roots[0] + G = {v: set() for v in G} orientable = [] while True: G[source].add(dest) - for u,v in stO.orient.get((source,dest),[]): - orientable.append((u,v)) - for v,u in stO.orient.get((dest,source),[]): - orientable.append((u,v)) + for u, v in stO.orient.get((source, dest), []): + orientable.append((u, v)) + for v, u in stO.orient.get((dest, source), []): + orientable.append((u, v)) if not orientable: break - source,dest = orientable.pop() - + source, dest = orientable.pop() + return G + # If run as "python Biconnectivity.py", run tests on various small graphs # and check that the correct results are obtained. + class BiconnectivityTest(unittest.TestCase): G1 = { - 0: [1,2,5], - 1: [0,5], - 2: [0,3,4], - 3: [2,4,5,6], - 4: [2,3,5,6], - 5: [0,1,3,4], - 6: [3,4], + 0: [1, 2, 5], + 1: [0, 5], + 2: [0, 3, 4], + 3: [2, 4, 5, 6], + 4: [2, 3, 5, 6], + 5: [0, 1, 3, 4], + 6: [3, 4], } G2 = { - 0: [2,5], - 1: [3,8], - 2: [0,3,5], - 3: [1,2,6,8], + 0: [2, 5], + 1: [3, 8], + 2: [0, 3, 5], + 3: [1, 2, 6, 8], 4: [7], - 5: [0,2], - 6: [3,8], + 5: [0, 2], + 6: [3, 8], 7: [4], - 8: [1,3,6], + 8: [1, 3, 6], } def testIsBiconnected(self): @@ -247,19 +251,19 @@ def testBiconnectedComponents(self): """G2 has four biconnected components.""" C = BiconnectedComponents(self.G2) CV = sorted(sorted(component.keys()) for component in C) - self.assertEqual(CV,[[0,2,5],[1,3,6,8],[2,3],[4,7]]) - + self.assertEqual(CV, [[0, 2, 5], [1, 3, 6, 8], [2, 3], [4, 7]]) + def testSTOrientation(self): STO = stOrientation(self.G1) L = list(TopologicalOrder(STO)) - indegree = dict([(v,0) for v in self.G1]) + indegree = dict([(v, 0) for v in self.G1]) for v in L: for w in STO[v]: indegree[w] += 1 - outdegree = dict([(v,len(STO[v])) for v in self.G1]) + outdegree = dict([(v, len(STO[v])) for v in self.G1]) self.assertEqual(len([v for v in self.G1 if indegree[v] == 0]), 1) self.assertEqual(len([v for v in self.G1 if outdegree[v] == 0]), 1) -if __name__ == "__main__": - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/pads/Bipartite.py b/pads/Bipartite.py index f322292..d28e52c 100644 --- a/pads/Bipartite.py +++ b/pads/Bipartite.py @@ -9,9 +9,11 @@ from . import Graphs from . import DFS + class NonBipartite(Exception): pass + def TwoColor(G): """ Find a bipartition of G, if one exists. @@ -19,13 +21,14 @@ def TwoColor(G): to two colors (True and False). """ color = {} - for v,w,edgetype in DFS.search(G): + for v, w, edgetype in DFS.search(G): if edgetype is DFS.forward: - color[w] = not color.get(v,False) + color[w] = not color.get(v, False) elif edgetype is DFS.nontree and color[v] == color[w]: raise NonBipartite return color + def Bipartition(G): """ Find a bipartition of G, if one exists. @@ -37,6 +40,7 @@ def Bipartition(G): if color[v]: yield v + def isBipartite(G): """ Return True if G is bipartite, False otherwise. @@ -47,38 +51,41 @@ def isBipartite(G): except NonBipartite: return False -def BipartiteOrientation(G,adjacency_list_type=set): + +def BipartiteOrientation(G, adjacency_list_type=set): """ Given an undirected bipartite graph G, return a directed graph in which the edges are oriented from one side of the bipartition to the other. The second argument has the same meaning as in Graphs.copyGraph. """ B = Bipartition(G) - return {v:adjacency_list_type(iter(G[v])) for v in B} + return {v: adjacency_list_type(iter(G[v])) for v in B} + def OddCore(G): """ Subgraph of vertices and edges that participate in odd cycles. Aka, the union of nonbipartite biconnected components. """ - return Graphs.union(*[C for C in BiconnectedComponents(G) - if not isBipartite(C)]) + return Graphs.union(*[C for C in BiconnectedComponents(G) if not isBipartite(C)]) + # If run as "python Bipartite.py", run tests on various small graphs # and check that the correct results are obtained. + class BipartitenessTest(unittest.TestCase): - def cycle(self,n): - return {i:[(i-1)%n,(i+1)%n] for i in range(n)} + def cycle(self, n): + return {i: [(i - 1) % n, (i + 1) % n] for i in range(n)} def testEvenCycles(self): - for i in range(4,12,2): + for i in range(4, 12, 2): self.assertEqual(isBipartite(self.cycle(i)), True) def testOddCycles(self): - for i in range(3,12,2): + for i in range(3, 12, 2): self.assertEqual(isBipartite(self.cycle(i)), False) -if __name__ == "__main__": - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/pads/BipartiteMatching.py b/pads/BipartiteMatching.py index 5076a73..38bc601 100644 --- a/pads/BipartiteMatching.py +++ b/pads/BipartiteMatching.py @@ -8,6 +8,7 @@ from .StrongConnectivity import StronglyConnectedComponents + def matching(graph): """ Find maximum cardinality matching of a bipartite graph (U,V,E). @@ -35,7 +36,7 @@ def matching(graph): # and is also used as a flag value for pred[u] when u is in the first layer preds = {} unmatched = [] - pred = {u:unmatched for u in graph} + pred = {u: unmatched for u in graph} for v in matching: del pred[matching[v]] layer = list(pred) @@ -46,7 +47,7 @@ def matching(graph): for u in layer: for v in graph[u]: if v not in preds: - newLayer.setdefault(v,[]).append(u) + newLayer.setdefault(v, []).append(u) layer = [] for v in newLayer: preds[v] = newLayer[v] @@ -63,7 +64,7 @@ def matching(graph): for v in graph[u]: if v not in preds: unlayered[v] = None - return (matching,list(pred),list(unlayered)) + return (matching, list(pred), list(unlayered)) # recursively search backward through layers to find alternating paths # recursion returns true if found path, false otherwise @@ -80,45 +81,47 @@ def recurse(v): return True return False - for v in unmatched: recurse(v) + for v in unmatched: + recurse(v) + def imperfections(graph): """ Find edges that do not belong to any perfect matching of G. The input format is the same as for matching(), and the output is a subgraph of the input graph in the same format. - + For each edge v->w in the output subgraph, imperfections[v][w] is itself a subgraph of the input, induced by a set of vertices that must be matched to each other, including w but not including v. """ - M,A,B = matching(graph) + M, A, B = matching(graph) if len(M) != len(graph): - return graph # whole graph is imperfect + return graph # whole graph is imperfect orientation = {} for v in graph: - orientation[v,True]=[] + orientation[v, True] = [] for w in graph[v]: if M[w] == v: - orientation[w,False]=[(v,True)] + orientation[w, False] = [(v, True)] else: - orientation[v,True].append((w,False)) + orientation[v, True].append((w, False)) components = {} for C in StronglyConnectedComponents(orientation): - induced = {v:{w for w,bit2 in C[v,bit]} for v,bit in C if bit} - for v,bit in C: - if not bit: # don't forget the matched edges! - induced.setdefault(M[v],set()).add(v) + induced = {v: {w for w, bit2 in C[v, bit]} for v, bit in C if bit} + for v, bit in C: + if not bit: # don't forget the matched edges! + induced.setdefault(M[v], set()).add(v) for v in C: components[v] = induced imperfections = {} for v in graph: - imperfections[v] = {w:components[w,False] for w in graph[v] - if M[w] != v and - components[v,True] != components[w,False]} - + imperfections[v] = { + w: components[w, False] for w in graph[v] if M[w] != v and components[v, True] != components[w, False] + } + return imperfections diff --git a/pads/BucketQueue.py b/pads/BucketQueue.py index ba9e493..8629fe4 100644 --- a/pads/BucketQueue.py +++ b/pads/BucketQueue.py @@ -17,37 +17,38 @@ D. Eppstein, July 2016. """ + class BucketQueue: def __init__(self): """Create a new empty integer priority queue.""" - self._D = {} # map from items to priorities - self._Q = {} # map from priorities to buckets - self._N = None # lower bound on min priority + self._D = {} # map from items to priorities + self._Q = {} # map from priorities to buckets + self._N = None # lower bound on min priority - def __getitem__(self,item): + def __getitem__(self, item): """Look up the priority of an item.""" return self._D[item] - def __delitem__(self,item): + def __delitem__(self, item): """Remove an item from the priority queue.""" priority = self._D[item] - del self._D[item] # remove from map of items => priorities + del self._D[item] # remove from map of items => priorities self._Q[priority].remove(item) # remove from bucket if not self._Q[priority]: - del self._Q[priority] # remove empty bucket + del self._Q[priority] # remove empty bucket - def __setitem__(self,item,priority): + def __setitem__(self, item, priority): """Add an element to the priority queue with the given priority.""" - if not isinstance(priority,int): + if not isinstance(priority, int): raise TypeError("Priority must be an integer") if item in self._D: del self[item] - self._D[item] = priority # add to map of items => priorities + self._D[item] = priority # add to map of items => priorities if not self._Q or priority < self._N: - self._N = priority # update priority lower bound + self._N = priority # update priority lower bound if priority not in self._Q: - self._Q[priority] = set() # make new bucket if necessary - self._Q[priority].add(item) # and add to bucket + self._Q[priority] = set() # make new bucket if necessary + self._Q[priority].add(item) # and add to bucket def __iter__(self): """Repeatedly find and remove the min-priority item from the queue. @@ -55,7 +56,7 @@ def __iter__(self): while self._Q: while self._N not in self._Q: self._N += 1 - x = next(iter(self._Q[self._N])) # arbitrary item in 1st bucket + x = next(iter(self._Q[self._N])) # arbitrary item in 1st bucket del self[x] yield x @@ -64,9 +65,9 @@ def items(self): We rely on the fact that the usual __iter__ always leaves self._N equal to the priority.""" for x in iter(self): - yield x,self._N + yield x, self._N - def __contains__(self,item): + def __contains__(self, item): """Container class membership test.""" return item in self._D diff --git a/pads/CardinalityMatching.py b/pads/CardinalityMatching.py index 3964363..972e311 100644 --- a/pads/CardinalityMatching.py +++ b/pads/CardinalityMatching.py @@ -10,7 +10,8 @@ from .UnionFind import UnionFind from .Util import arbitrary_item -def matching(G, initialMatching = None): + +def matching(G, initialMatching=None): """Find a maximum cardinality matching in a graph G. G is represented in modified GvR form: iter(G) lists its vertices; iter(G[v]) lists the neighbors of v; w in G[v] tests adjacency. @@ -25,7 +26,7 @@ def matching(G, initialMatching = None): # Copy initial matching so we can use it nondestructively # and augment it greedily to reduce main loop iterations - matching = greedyMatching(G,initialMatching) + matching = greedyMatching(G, initialMatching) def augment(): """Search for a single augmenting path. @@ -63,12 +64,12 @@ def augment(): # Many of these are called only from one place, but are split out # as subroutines to improve modularization and readability. - def blossom(v,w,a): + def blossom(v, w, a): """Create a new blossom from edge v-w with common ancestor a.""" - def findSide(v,w): + def findSide(v, w): path = [leader[v]] - b = (v,w) # new base for all T nodes found on the path + b = (v, w) # new base for all T nodes found on the path while path[-1] != a: tnode = S[path[-1]] path.append(tnode) @@ -77,14 +78,15 @@ def findSide(v,w): path.append(leader[T[tnode]]) return path - a = leader[a] # sanity check - path1,path2 = findSide(v,w), findSide(w,v) + a = leader[a] # sanity check + path1, path2 = findSide(v, w), findSide(w, v) leader.union(*path1) leader.union(*path2) - S[leader[a]] = S[a] # update structure tree + S[leader[a]] = S[a] # update structure tree topless = object() # should be unequal to any graph vertex - def alternatingPath(start, goal = topless): + + def alternatingPath(start, goal=topless): """Return sequence of vertices on alternating path from start to goal. The goal must be a T node along the path from the start to the root of the structure tree. If goal is omitted, we find @@ -100,20 +102,20 @@ def alternatingPath(start, goal = topless): start = w path.append(start) if start not in matching: - return path # reached top of structure tree, done! + return path # reached top of structure tree, done! tnode = matching[start] path.append(tnode) if tnode == goal: - return path # finished recursive subpath + return path # finished recursive subpath start = T[tnode] def alternate(v): """Make v unmatched by alternating the path to the root of its structure tree.""" path = alternatingPath(v) path.reverse() - for i in range(0,len(path)-1,2): - matching[path[i]] = path[i+1] - matching[path[i+1]] = path[i] + for i in range(0, len(path) - 1, 2): + matching[path[i]] = path[i + 1] + matching[path[i + 1]] = path[i] def addMatch(v, w): """Here with an S-S edge vw connecting vertices in different structure trees. @@ -124,13 +126,13 @@ def addMatch(v, w): matching[v] = w matching[w] = v - def ss(v,w): + def ss(v, w): """Handle detection of an S-S edge in augmenting path search. Like augment(), returns true iff the matching size was increased. """ if leader[v] == leader[w]: - return False # self-loop within blossom, ignore + return False # self-loop within blossom, ignore # parallel search up two branches of structure tree # until we find a common ancestor of v and w @@ -141,7 +143,7 @@ def step(path, head): head = leader[head] parent = leader[S[head]] if parent == head: - return head # found root of structure tree + return head # found root of structure tree path[head] = parent path[parent] = leader[T[parent]] return path[parent] @@ -173,24 +175,24 @@ def step(path, head): S[v] = v unexplored.append(v) - current = 0 # index into unexplored, in FIFO order so we get short paths + current = 0 # index into unexplored, in FIFO order so we get short paths while current < len(unexplored): v = unexplored[current] current += 1 for w in G[v]: if leader[w] in S: # S-S edge: blossom or augmenting path - if ss(v,w): + if ss(v, w): return True - elif w not in T: # previously unexplored node, add as T-node + elif w not in T: # previously unexplored node, add as T-node T[w] = v u = matching[w] if leader[u] not in S: - S[u] = w # and add its match as an S-node + S[u] = w # and add its match as an S-node unexplored.append(u) - return False # ran out of graph without finding an augmenting path + return False # ran out of graph without finding an augmenting path # augment the matching until it is maximum while augment(): @@ -198,6 +200,7 @@ def step(path, head): return matching + def greedyMatching(G, initialMatching=None): """Near-linear-time greedy heuristic for creating high-cardinality matching. If there is any vertex with one unmatched neighbor, we match it. @@ -223,7 +226,7 @@ def greedyMatching(G, initialMatching=None): avail[v] = {} for w in G[v]: if w not in matching: - avail[v][w] = (v,w) + avail[v][w] = (v, w) has_edge = True if not avail[v]: del avail[v] @@ -234,6 +237,7 @@ def greedyMatching(G, initialMatching=None): deg1 = {v for v in avail if len(avail[v]) == 1} deg2 = {v for v in avail if len(avail[v]) == 2} d2edges = [] + def updateDegree(v): """Cluster degree changed, update sets.""" if v in deg1: @@ -247,9 +251,9 @@ def updateDegree(v): elif len(avail[v]) == 2: deg2.add(v) - def addMatch(v,w): + def addMatch(v, w): """Add edge connecting two given cluster reps, update avail.""" - p,q = avail[v][w] + p, q = avail[v][w] matching[p] = q matching[q] = p for x in avail[v].keys(): @@ -266,12 +270,12 @@ def addMatch(v,w): def contract(v): """Handle degree two vertex.""" - u,w = avail[v] # find reps for two neighbors - d2edges.extend([avail[v][u],avail[v][w]]) + u, w = avail[v] # find reps for two neighbors + d2edges.extend([avail[v][u], avail[v][w]]) del avail[u][v] del avail[w][v] if len(avail[u]) > len(avail[w]): - u,w = w,u # swap to preserve near-linear time bound + u, w = w, u # swap to preserve near-linear time bound for x in avail[u].keys(): del avail[x][u] if x in avail[w]: @@ -288,28 +292,28 @@ def contract(v): if deg1: v = arbitrary_item(deg1) w = arbitrary_item(avail[v]) - addMatch(v,w) + addMatch(v, w) elif deg2: v = arbitrary_item(deg2) contract(v) else: v = arbitrary_item(avail) w = arbitrary_item(avail[v]) - addMatch(v,w) + addMatch(v, w) # at this point the edges listed in d2edges form a matchable tree # repeat the degree one part of the algorithm only on those edges avail = {} - d2edges = [(u,v) for u,v in d2edges if u not in matching and v not in matching] - for u,v in d2edges: + d2edges = [(u, v) for u, v in d2edges if u not in matching and v not in matching] + for u, v in d2edges: avail[u] = {} avail[v] = {} - for u,v in d2edges: - avail[u][v] = avail[v][u] = (u,v) + for u, v in d2edges: + avail[u][v] = avail[v][u] = (u, v) deg1 = {v for v in avail if len(avail[v]) == 1} while deg1: v = arbitrary_item(deg1) w = arbitrary_item(avail[v]) - addMatch(v,w) + addMatch(v, w) return matching diff --git a/pads/Chordal.py b/pads/Chordal.py index d33af33..65fa76b 100644 --- a/pads/Chordal.py +++ b/pads/Chordal.py @@ -9,10 +9,10 @@ D. Eppstein, November 2003. """ -import unittest from .LexBFS import LexBFS from .Graphs import isUndirected + def PerfectEliminationOrdering(G): """Return a perfect elimination ordering, or raise an exception if not chordal. G should be represented in such a way that "for v in G" loops through @@ -22,7 +22,7 @@ def PerfectEliminationOrdering(G): """ alreadyProcessed = set() B = list(LexBFS(G)) - position = {B[i]:i for i in range(len(B))} + position = {B[i]: i for i in range(len(B))} leftNeighbors = {} parent = {} for v in B: @@ -35,6 +35,7 @@ def PerfectEliminationOrdering(G): B.reverse() return B + def Chordal(G): """Test if a given graph is chordal.""" if not isUndirected(G): @@ -44,30 +45,3 @@ def Chordal(G): except: return False return True - -class ChordalTest(unittest.TestCase): - claw = {0:[1,2,3],1:[0],2:[0],3:[0]} - butterfly = {0:[1,2,3,4],1:[0,2],2:[0,1],3:[0,4],4:[0,3]} - diamond = {0:[1,2],1:[0,2,3],2:[0,1,3],3:[1,2]} - quad = {0:[1,3],1:[0,2],2:[1,3],3:[0,2]} - graphs = [(claw,True), (butterfly,True), (diamond,True), (quad,False)] - - def testChordal(self): - """Check that Chordal() returns the correct answer on each test graph.""" - for G,isChordal in ChordalTest.graphs: - self.assertEqual(Chordal(G), isChordal) - - def testElimination(self): - """Check that PerfectEliminationOrdering generates an elimination ordering.""" - for G,isChordal in ChordalTest.graphs: - if isChordal: - eliminated = set() - for v in PerfectEliminationOrdering(G): - eliminated.add(v) - for w in G[v]: - for x in G[v]: - if w != x and w not in eliminated and x not in eliminated: - self.assertTrue(w in G[x] and x in G[w]) - -if __name__ == "__main__": - unittest.main() diff --git a/pads/CirclePack.py b/pads/CirclePack.py index 8be086b..133fdba 100644 --- a/pads/CirclePack.py +++ b/pads/CirclePack.py @@ -4,15 +4,16 @@ "A Circle Packing Algorithm", Comp. Geom. Theory and Appl. 2003. """ -from math import pi,acos,asin,sin,e +from math import pi, acos, asin, sin, e -tolerance = 1+10e-12 # how accurately to approximate things +tolerance = 1 + 10e-12 # how accurately to approximate things # ====================================================== # The main circle packing algorithm # ====================================================== -def CirclePack(internal,external): + +def CirclePack(internal, external): """Find a circle packing for the given data. The two arguments should be dictionaries with disjoint keys; the keys of the two arguments are identifiers for circles in the packing. @@ -20,7 +21,7 @@ def CirclePack(internal,external): surrounding circles; the external argument maps each external circle to its desired radius. The return function is a mapping from circle keys to pairs (center,radius) where center is a complex number.""" - + # Some sanity checks and preprocessing if min(external.values()) <= 0: raise ValueError("CirclePack: external radii must be positive") @@ -35,121 +36,128 @@ def CirclePack(internal,external): while lastChange > tolerance: lastChange = 1 for k in internal: - theta = flower(radii,k,internal[k]) - hat = radii[k]/(1/sin(theta/(2*len(internal[k])))-1) - newrad = hat * (1/(sin(pi/len(internal[k]))) - 1) - kc = max(newrad/radii[k],radii[k]/newrad) - lastChange = max(lastChange,kc) + theta = flower(radii, k, internal[k]) + hat = radii[k] / (1 / sin(theta / (2 * len(internal[k]))) - 1) + newrad = hat * (1 / (sin(pi / len(internal[k]))) - 1) + kc = max(newrad / radii[k], radii[k] / newrad) + lastChange = max(lastChange, kc) radii[k] = newrad # Recursively place all the circles placements = {} - k1 = next(iter(internal)) # pick one internal circle - placements[k1] = 0j # place it at the origin - k2 = internal[k1][0] # pick one of its neighbors - placements[k2] = radii[k1]+radii[k2] # place it on the real axis - place(placements,radii,internal,k1) # recursively place the rest - place(placements,radii,internal,k2) + k1 = next(iter(internal)) # pick one internal circle + placements[k1] = 0j # place it at the origin + k2 = internal[k1][0] # pick one of its neighbors + placements[k2] = radii[k1] + radii[k2] # place it on the real axis + place(placements, radii, internal, k1) # recursively place the rest + place(placements, radii, internal, k2) + + return dict((k, (placements[k], radii[k])) for k in radii) - return dict((k,(placements[k],radii[k])) for k in radii) # ====================================================== # Invert a collection of circles # ====================================================== -def InvertPacking(packing,center): + +def InvertPacking(packing, center): """Invert with specified center""" result = {} for k in packing: - z,r = packing[k] + z, r = packing[k] z -= center if z == 0: offset = 1j else: - offset = z/abs(z) - p,q = z-offset*r,z+offset*r - p,q = 1/p,1/q - z = (p+q)/2 - r = abs((p-q)/2) - result[k] = z,r + offset = z / abs(z) + p, q = z - offset * r, z + offset * r + p, q = 1 / p, 1 / q + z = (p + q) / 2 + r = abs((p - q) / 2) + result[k] = z, r return result -def NormalizePacking(packing,k=None,target=1.0): + +def NormalizePacking(packing, k=None, target=1.0): """Make the given circle have radius one (or the target if given). If no circle is given, the minimum radius circle is chosen instead.""" if k is None: - r = min(r for z,r in packing.values()) + r = min(r for z, r in packing.values()) else: - z,r = packing[k] - s = target/r - return dict((kk,(zz*s,rr*s)) for kk,(zz,rr) in packing.iteritems()) + z, r = packing[k] + s = target / r + return dict((kk, (zz * s, rr * s)) for kk, (zz, rr) in packing.iteritems()) + -def InvertAround(packing,k,smallCircles=None): +def InvertAround(packing, k, smallCircles=None): """Invert so that the specified circle surrounds all the others. Searches for the inversion center that maximizes the minimum radius. - + This can be expressed as a quasiconvex program, but in a related hyperbolic space, so rather than applying QCP methods it seems simpler to use a numerical hill-climbing approach, relying on the theory of QCP to tell us there are no local maxima to get stuck in. - + If the smallCircles argument is given, the optimization for the minimum radius circle will look only at these circles""" - z,r = packing[k] + z, r = packing[k] if smallCircles: - optpack = {k:packing[k] for k in smallCircles} + optpack = {k: packing[k] for k in smallCircles} else: optpack = packing - q,g = z,r*0.4 - oldrad,ratio = None,2 - while abs(g) > r*(tolerance-1) or ratio > tolerance: - rr,ignore1,ignore2,q = max(list(testgrid(optpack,k,z,r,q,g))) + q, g = z, r * 0.4 + oldrad, ratio = None, 2 + while abs(g) > r * (tolerance - 1) or ratio > tolerance: + rr, ignore1, ignore2, q = max(list(testgrid(optpack, k, z, r, q, g))) if oldrad: - ratio = rr/oldrad + ratio = rr / oldrad oldrad = rr - g *= 0.53+0.1j # rotate so not always axis-aligned - return InvertPacking(packing,q) + g *= 0.53 + 0.1j # rotate so not always axis-aligned + return InvertPacking(packing, q) # ====================================================== # Utility routines, not for outside callers # ====================================================== -def acxyz(x,y,z): + +def acxyz(x, y, z): """Angle at a circle of radius x given by two circles of radii y and z""" try: - return acos(((x+y)**2 + (x+z)**2 - (y+z)**2)/(2.0*(x+y)*(x+z))) + return acos(((x + y) ** 2 + (x + z) ** 2 - (y + z) ** 2) / (2.0 * (x + y) * (x + z))) except ValueError: - return pi/3 + return pi / 3 except ZeroDivisionError: return pi -def flower(radius,center,cycle): + +def flower(radius, center, cycle): """Compute the angle sum around a given internal circle""" - return sum(acxyz(radius[center],radius[cycle[i-1]],radius[cycle[i]]) - for i in range(len(cycle))) + return sum(acxyz(radius[center], radius[cycle[i - 1]], radius[cycle[i]]) for i in range(len(cycle))) -def place(placements,radii,internal,center): + +def place(placements, radii, internal, center): """Recursively find centers of all circles surrounding k""" if center not in internal: return cycle = internal[center] - for i in range(-len(cycle),len(cycle)-1): - if cycle[i] in placements and cycle[i+1] not in placements: - s,t = cycle[i],cycle[i+1] - theta = acxyz(radii[center],radii[s],radii[t]) - offset = (placements[s]-placements[center])/(radii[s]+radii[center]) - offset *= e**(-1j*theta) - placements[t] = placements[center] + offset*(radii[t]+radii[center]) - place(placements,radii,internal,t) - -def testgrid(packing,k,z,r,q,g): + for i in range(-len(cycle), len(cycle) - 1): + if cycle[i] in placements and cycle[i + 1] not in placements: + s, t = cycle[i], cycle[i + 1] + theta = acxyz(radii[center], radii[s], radii[t]) + offset = (placements[s] - placements[center]) / (radii[s] + radii[center]) + offset *= e ** (-1j * theta) + placements[t] = placements[center] + offset * (radii[t] + radii[center]) + place(placements, radii, internal, t) + + +def testgrid(packing, k, z, r, q, g): """Build grid of test points around q with grid size g""" - for i in (-2,-1,0,1,2): - for j in (-2,-1,0,1,2): - center = q + i*g + j*1j*g - if abs(center-z) < r: - newpack = InvertPacking(packing,center) - newpack = NormalizePacking(newpack,k) - minrad = min(r for z,r in newpack.values()) - yield minrad,i,j,center + for i in (-2, -1, 0, 1, 2): + for j in (-2, -1, 0, 1, 2): + center = q + i * g + j * 1j * g + if abs(center - z) < r: + newpack = InvertPacking(packing, center) + newpack = NormalizePacking(newpack, k) + minrad = min(r for z, r in newpack.values()) + yield minrad, i, j, center diff --git a/pads/CubicHam.py b/pads/CubicHam.py index 9de9606..5618199 100644 --- a/pads/CubicHam.py +++ b/pads/CubicHam.py @@ -10,6 +10,7 @@ from .CardinalityMatching import matching from .Util import arbitrary_item, map_to_constant + def HamiltonianCycles(G): """ Generate a sequence of all Hamiltonian cycles in graph G. @@ -28,13 +29,13 @@ def HamiltonianCycles(G): raise ValueError("HamiltonianCycles input must be undirected degree three graph") if minDegree(G) < 2: return - G = copyGraph(G,map_to_constant(True)) + G = copyGraph(G, map_to_constant(True)) # Subgraph of forced edges in the input - forced_in_input = {v:{} for v in G} + forced_in_input = {v: {} for v in G} # Subgraph of forced edges in current G - forced_in_current = {v:{} for v in G} + forced_in_current = {v: {} for v in G} # List of vertices with degree two degree_two = [v for v in G if len(G[v]) == 2] @@ -52,13 +53,13 @@ def HamiltonianCycles(G): # Whenever we modify the graph, we push an action undoing that modification. # Below are definitions of actions and action-related functions. - def remove(v,w): + def remove(v, w): """Remove edge v,w from edges of G.""" was_original = G[v][w] - del G[v][w],G[w][v] + del G[v][w], G[w][v] was_forced = w in forced_in_current[v] if was_forced: - del forced_in_current[v][w],forced_in_current[w][v] + del forced_in_current[v][w], forced_in_current[w][v] def unremove(): G[v][w] = G[w][v] = was_original @@ -70,12 +71,14 @@ def unremove(): def now_degree_two(v): """Discover that changing G has caused v's degree to become two.""" degree_two.append(v) + def not_degree_two(): top = degree_two.pop() assert v == top + actions.append(not_degree_two) - def safely_remove(v,w): + def safely_remove(v, w): """ Remove edge v,w and update degree two data structures. Returns True if successful, False if found a contradiction. @@ -83,7 +86,7 @@ def safely_remove(v,w): assert w in G[v] if w in forced_in_current[v] or len(G[v]) < 3 or len(G[w]) < 3: return False - remove(v,w) + remove(v, w) now_degree_two(v) now_degree_two(w) return True @@ -98,21 +101,21 @@ def remove_third_leg(v): w = [x for x in G[v] if x not in forced_in_current[v]][0] if len(G[w]) <= 2: return False - return safely_remove(v,w) + return safely_remove(v, w) - def force(v,w): + def force(v, w): """ Add edge v,w to forced edges. Returns True if successful, False if found a contradiction. """ if w in forced_in_current[v]: - return True # Already forced, nothing to do + return True # Already forced, nothing to do if len(forced_in_current[v]) > 2 or len(forced_in_current[w]) > 2: - return False # Three incident forced => no cycle exists + return False # Three incident forced => no cycle exists if w not in G[v] or v not in G[w]: - return False # Removed from G after we decided to force it? + return False # Removed from G after we decided to force it? forced_in_current[v][w] = forced_in_current[w][v] = True - not_previously_forced = [x for x in (v,w) if x not in forced_vertices] + not_previously_forced = [x for x in (v, w) if x not in forced_vertices] for x in not_previously_forced: forced_vertices[x] = True was_original = G[v][w] @@ -123,16 +126,20 @@ def unforce(): """Undo call to force.""" for x in not_previously_forced: del forced_vertices[x] - del forced_in_current[v][w],forced_in_current[w][v] + del forced_in_current[v][w], forced_in_current[w][v] if was_original: - del forced_in_input[v][w],forced_in_input[w][v] + del forced_in_input[v][w], forced_in_input[w][v] actions.append(unforce) - return remove_third_leg(v) and remove_third_leg(w) and \ - force_into_triangle(v,w) and force_into_triangle(w,v) and \ - force_from_triangle(v,w) - - def force_into_triangle(v,w): + return ( + remove_third_leg(v) + and remove_third_leg(w) + and force_into_triangle(v, w) + and force_into_triangle(w, v) + and force_from_triangle(v, w) + ) + + def force_into_triangle(v, w): """ After v,w has been added to forced edges, check if w belongs to a triangle, and if so force the opposite edge. @@ -140,23 +147,23 @@ def force_into_triangle(v,w): """ if len(G[w]) != 3: return True - x,y = [z for z in G[w] if z != v] + x, y = [z for z in G[w] if z != v] if y not in G[x]: return True - return force(x,y) + return force(x, y) - def force_from_triangle(v,w): + def force_from_triangle(v, w): """ After v,w has been added to forced edges, check whether it belongs to a triangle, and if so force the opposite edge. Returns True if successful, False if found a contradiction. """ - for u in list(G[v]): # Use list to avoid dict changes + for u in list(G[v]): # Use list to avoid dict changes if u in G[w]: if len(G[u]) < 3: return len(G) == 3 # deg=2 only ok if 3 verts left x = [y for y in G[u] if y != v and y != w][0] - if not force(u,x): + if not force(u, x): return False return True @@ -167,30 +174,30 @@ def contract(v): Appends recursive search of contracted graph to action stack. """ assert len(G[v]) == 2 - u,w = G[v] - if w in G[u]: # About to create parallel edge? - if len(G) == 3: # Graph is a triangle? - return force(u,v) and force(v,w) and force(u,w) - if not safely_remove(u,w): - return None # Unable to remove uw, no cycles exist - - if not force(u,v) or not force(v,w): - return None # Forcing the edges led to a contradiction - remove(u,v) - remove(v,w) + u, w = G[v] + if w in G[u]: # About to create parallel edge? + if len(G) == 3: # Graph is a triangle? + return force(u, v) and force(v, w) and force(u, w) + if not safely_remove(u, w): + return None # Unable to remove uw, no cycles exist + + if not force(u, v) or not force(v, w): + return None # Forcing the edges led to a contradiction + remove(u, v) + remove(v, w) G[u][w] = G[w][u] = False forced_in_current[u][w] = forced_in_current[w][u] = True - del G[v],forced_vertices[v] + del G[v], forced_vertices[v] def uncontract(): - del G[u][w],G[w][u] - del forced_in_current[u][w],forced_in_current[w][u] + del G[u][w], G[w][u] + del forced_in_current[u][w], forced_in_current[w][u] forced_vertices[v] = True G[v] = {} actions.append(uncontract) - if force_from_triangle(u,w): # Contraction may have made a triangle - actions.append(main) # Search contracted graph recursively + if force_from_triangle(u, w): # Contraction may have made a triangle + actions.append(main) # Search contracted graph recursively def handle_degree_two(): """ @@ -199,8 +206,10 @@ def handle_degree_two(): Appends recursive search of contracted graph to action stack. """ v = degree_two.pop() + def unpop(): degree_two.append(v) + actions.append(unpop) return contract(v) @@ -225,7 +234,7 @@ def main(): # We jump-start the matching algorithm with our previously computed # matching (or as much of it as fits the current graph) since that is # likely to be near-perfect. - unforced = {v:{} for v in G} + unforced = {v: {} for v in G} for v in G: for w in G[v]: if w not in forced_in_current[v]: @@ -251,11 +260,11 @@ def main(): def continuation(): """Here after searching first recursive subgraph.""" - if force(v,w): + if force(v, w): actions.append(main) actions.append(continuation) - if safely_remove(v,w): + if safely_remove(v, w): actions.append(main) # The main backtracking loop @@ -264,11 +273,13 @@ def continuation(): if actions.pop()(): yield forced_in_input + # If run as "python CubicHam.py", run tests on various small graphs # and check that the correct number of cycles is generated. + class CubicHamTest(unittest.TestCase): - def check(self,G,N): + def check(self, G, N): """Make sure G has N Hamiltonian cycles.""" count = 0 for C in HamiltonianCycles(G): @@ -277,58 +288,57 @@ def check(self,G,N): # Check that it's a degree-two undirected subgraph. for v in C: - self.assertEqual(len(C[v]),2) + self.assertEqual(len(C[v]), 2) for w in C[v]: assert v in G and w in G[v] and v in C[w] # Check that it connects all vertices. nreached = 0 x = arbitrary_item(G) - a,b = x,x + a, b = x, x while True: nreached += 1 - a,b = b,[z for z in C[b] if z != a][0] + a, b = b, [z for z in C[b] if z != a][0] if b == x: break - self.assertEqual(nreached,len(G)) + self.assertEqual(nreached, len(G)) # Did we find enough cycles? - self.assertEqual(count,N) + self.assertEqual(count, N) def testCube(self): """Cube has six Hamiltonian cycles.""" - cube = {i:(i^1,i^2,i^4) for i in range(8)} - self.check(cube,6) + cube = {i: (i ^ 1, i ^ 2, i ^ 4) for i in range(8)} + self.check(cube, 6) - def twistedLadder(self,n): + def twistedLadder(self, n): """Connect opposite vertices on an even length cycle.""" - return {i:((i+1)%n,(i-1)%n,(i+n//2)%n) for i in range(n)} + return {i: ((i + 1) % n, (i - 1) % n, (i + n // 2) % n) for i in range(n)} def testEvenTwistedLadders(self): """twistedLadder(4n) has 2n+1 Hamiltonian cycles.""" - for n in range(4,50,4): - self.check(self.twistedLadder(n),n//2+1) + for n in range(4, 50, 4): + self.check(self.twistedLadder(n), n // 2 + 1) def testOddTwistedLadders(self): """twistedLadder(4n+2) has 2n+4 Hamiltonian cycles.""" - for n in range(6,50,4): - self.check(self.twistedLadder(n),n//2+3) + for n in range(6, 50, 4): + self.check(self.twistedLadder(n), n // 2 + 3) - def truncate(self,G): + def truncate(self, G): """Replace each vertex of G by a triangle and return the result.""" - return {(v,w):{(v,u) for u in G[v] if u != w} | {(w,v)} - for v in G for w in G[v]} + return {(v, w): {(v, u) for u in G[v] if u != w} | {(w, v)} for v in G for w in G[v]} def testSierpinski(self): """ Sierpinski triangle like graphs formed by repeated truncation of K_4 should all have exactly three Hamiltonian cycles. """ - G = self.twistedLadder(4) # Complete graph on four vertices + G = self.twistedLadder(4) # Complete graph on four vertices for i in range(3): G = self.truncate(G) - self.check(G,3) + self.check(G, 3) -if __name__ == "__main__": - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/pads/DFS.py b/pads/DFS.py index 2485d84..ce4c026 100644 --- a/pads/DFS.py +++ b/pads/DFS.py @@ -9,13 +9,14 @@ # Types of edges in DFS traversal. # The numerical values are used in DepthFirstSearcher, change with care. -forward = 1 # traversing edge (v,w) from v to w -reverse = -1 # returning backwards on (v,w) from w to v -nontree = 0 # edge (v,w) is not part of the DFS tree +forward = 1 # traversing edge (v,w) from v to w +reverse = -1 # returning backwards on (v,w) from w to v +nontree = 0 # edge (v,w) is not part of the DFS tree whole_graph = object() # special flag object, do not use as a graph vertex -def search(G,initial_vertex = whole_graph): + +def search(G, initial_vertex=whole_graph): """ Generate sequence of triples (v,w,edgetype) for DFS of graph G. The subsequence for each root of each tree in the DFS forest starts @@ -30,40 +31,44 @@ def search(G,initial_vertex = whole_graph): initials = [initial_vertex] for v in initials: if v not in visited: - yield v,v,forward + yield v, v, forward visited.add(v) - stack = [(v,iter(G[v]))] + stack = [(v, iter(G[v]))] while stack: - parent,children = stack[-1] + parent, children = stack[-1] try: child = next(children) if child in visited: - yield parent,child,nontree + yield parent, child, nontree else: - yield parent,child,forward + yield parent, child, forward visited.add(child) - stack.append((child,iter(G[child]))) + stack.append((child, iter(G[child]))) except StopIteration: stack.pop() if stack: - yield stack[-1][0],parent,reverse - yield v,v,reverse + yield stack[-1][0], parent, reverse + yield v, v, reverse + -def preorder(G,initial_vertex = whole_graph): +def preorder(G, initial_vertex=whole_graph): """Generate all vertices of graph G in depth-first preorder.""" - for v,w,edgetype in search(G,initial_vertex): + for v, w, edgetype in search(G, initial_vertex): if edgetype is forward: yield w -def postorder(G,initial_vertex = whole_graph): + +def postorder(G, initial_vertex=whole_graph): """Generate all vertices of graph G in depth-first postorder.""" - for v,w,edgetype in search(G,initial_vertex): + for v, w, edgetype in search(G, initial_vertex): if edgetype is reverse: yield w -def reachable(G,v,w): + +def reachable(G, v, w): """Can we get from v to w in graph G?""" - return w in preorder(G,v) + return w in preorder(G, v) + class Searcher: """ @@ -72,26 +77,26 @@ class Searcher: should be shadowed in order to make the search do something useful. """ - def preorder(self,parent,child): + def preorder(self, parent, child): """ Called when DFS visits child, before visiting all grandchildren. Parent==child when child is the root of each DFS tree. """ pass - def postorder(self,parent,child): + def postorder(self, parent, child): """ Called when DFS visits child, after visiting all grandchildren. Parent==child when child is the root of each DFS tree. """ pass - def backedge(self,source,destination): + def backedge(self, source, destination): """Called when DFS discovers an edge to a non-child.""" pass - def __init__(self,G): + def __init__(self, G): """Perform a depth first search of graph G.""" - dispatch = [self.backedge,self.preorder,self.postorder] - for v,w,edgetype in search(G): - dispatch[edgetype](v,w) + dispatch = [self.backedge, self.preorder, self.postorder] + for v, w, edgetype in search(G): + dispatch[edgetype](v, w) diff --git a/pads/Eratosthenes.py b/pads/Eratosthenes.py index 11fbde5..d3e82f9 100644 --- a/pads/Eratosthenes.py +++ b/pads/Eratosthenes.py @@ -30,15 +30,16 @@ import unittest from collections import defaultdict + def primes(): - '''Yields the sequence of primes via the Sieve of Eratosthenes.''' - yield 2 # Only even prime. Sieve only odd numbers. + """Yields the sequence of primes via the Sieve of Eratosthenes.""" + yield 2 # Only even prime. Sieve only odd numbers. # Generate recursively the sequence of primes up to sqrt(n). # Each p from the sequence is used to initiate sieving at p*p. roots = primes() root = next(roots) - square = root*root + square = root * root # The main sieving loop. # We use a hash table D such that D[n]=2p for p a prime factor of n. @@ -47,21 +48,22 @@ def primes(): D = {} n = 3 while True: - if n >= square: # Time to include another square? - D[square] = root+root + if n >= square: # Time to include another square? + D[square] = root + root root = next(roots) - square = root*root + square = root * root - if n not in D: # Not witnessed, must be prime. + if n not in D: # Not witnessed, must be prime. yield n - else: # Move witness p to next free multiple. + else: # Move witness p to next free multiple. p = D[n] - q = n+p + q = n + p while q in D: q += p del D[n] D[q] = p - n += 2 # Move on to next odd number. + n += 2 # Move on to next odd number. + def FactoredIntegers(): """ @@ -69,42 +71,46 @@ def FactoredIntegers(): F is represented as a dictionary in which each prime factor of n is a key and the exponent of that prime is the corresponding value. """ - yield 1,{} + yield 1, {} i = 2 factorization = defaultdict(dict) while True: if i not in factorization: # prime - F = {i:1} - yield i,F - factorization[2*i] = F - elif len(factorization[i]) == 1: # prime power - p,x = next(iter(factorization[i].items())) - F = {p:x+1} - yield i,F - factorization[2*i] = F - factorization[i+p**x][p] = x + F = {i: 1} + yield i, F + factorization[2 * i] = F + elif len(factorization[i]) == 1: # prime power + p, x = next(iter(factorization[i].items())) + F = {p: x + 1} + yield i, F + factorization[2 * i] = F + factorization[i + p**x][p] = x del factorization[i] else: - yield i,factorization[i] - for p,x in factorization[i].items(): + yield i, factorization[i] + for p, x in factorization[i].items(): q = p**x - iq = i+q + iq = i + q if iq in factorization and p in factorization[iq]: iq += p**x # skip higher power of p factorization[iq][p] = x del factorization[i] i += 1 + def MoebiusSequence(): """The sequence of values of the Moebius function, OEIS A008683.""" - for n,F in FactoredIntegers(): + for n, F in FactoredIntegers(): if n > 1 and set(F.values()) != {1}: yield 0 else: - yield (-1)**len(F) + yield (-1) ** len(F) + MoebiusFunctionValues = [None] MoebiusFunctionIterator = MoebiusSequence() + + def MoebiusFunction(n): """A functional version of the Moebius sequence. Efficient only for small values of n.""" @@ -112,35 +118,39 @@ def MoebiusFunction(n): MoebiusFunctionValues.append(next(MoebiusFunctionIterator)) return MoebiusFunctionValues[n] + def isPracticalFactorization(f): """Test whether f is the factorization of a practical number.""" f = sorted(f.items()) sigma = 1 - for p,x in f: + for p, x in f: if sigma < p - 1: return False - sigma *= (p**(x+1)-1)//(p-1) + sigma *= (p ** (x + 1) - 1) // (p - 1) return True + def PracticalNumbers(): """Generate the sequence of practical (or panarithmic) numbers.""" - for x,f in FactoredIntegers(): + for x, f in FactoredIntegers(): if isPracticalFactorization(f): yield x + # If run standalone, perform unit tests -class SieveTest(unittest.TestCase): +class SieveTest(unittest.TestCase): def testPrime(self): """Test that the first few primes are generated correctly.""" G = primes() - for p in [2,3,5,7,11,13,17,19,23,29,31,37]: - self.assertEqual(p,next(G)) + for p in [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]: + self.assertEqual(p, next(G)) def testPractical(self): """Test that the first few practical nos are generated correctly.""" G = PracticalNumbers() - for p in [1,2,4,6,8,12,16,18,20,24,28,30,32,36]: - self.assertEqual(p,next(G)) + for p in [1, 2, 4, 6, 8, 12, 16, 18, 20, 24, 28, 30, 32, 36]: + self.assertEqual(p, next(G)) + if __name__ == "__main__": unittest.main() diff --git a/pads/FrequencyEstimator.py b/pads/FrequencyEstimator.py index 479e385..6332989 100644 --- a/pads/FrequencyEstimator.py +++ b/pads/FrequencyEstimator.py @@ -2,6 +2,7 @@ Estimate frequencies of items in a data stream. D. Eppstein, Feb 2016.""" + class FrequencyEstimator: """Estimate frequencies of a stream of items to a specified accuracy (e.g. accuracy=0.1 means within 10% of actual frequency) @@ -9,7 +10,7 @@ class FrequencyEstimator: def __init__(self, accuracy): self._counts = {} - self._howmany = int(1./accuracy) + self._howmany = int(1.0 / accuracy) self._total = 0 def __iadd__(self, key): @@ -18,10 +19,10 @@ def __iadd__(self, key): time may be O(1/accuracy).""" self._total += 1 if key in self._counts: - self._counts[key] += 1 # Already there, increment its counter. + self._counts[key] += 1 # Already there, increment its counter. elif len(self._counts) < self._howmany: - self._counts[key] = 1 # We have room to add it, so do. - else: + self._counts[key] = 1 # We have room to add it, so do. + else: # We need to make some room, by decrementing all the counters # and clearing out the keys that this reduces to zero. # This happens on at most 1/(howmany+1) of the calls to add(), @@ -37,7 +38,7 @@ def __iadd__(self, key): # structure less accurate by increasing the potential # rate of decrements from 1/(howmany+1) to 1/howmany. # - anchor = linkchain = object() # nonce sentinel + anchor = linkchain = object() # nonce sentinel for key in self._counts: self._counts[key] -= 1 if self._counts[key] == 0: @@ -51,9 +52,9 @@ def __iadd__(self, key): def __iter__(self): """iter(FrequencyEstimator) loops through the most frequent keys.""" return iter(self._counts) - + def __getitem__(self, key): """FrequencyEstimator[key] estimates the frequency of the key.""" if key not in self._counts: return 0 - return self._counts[key]*1.0/self._total + return self._counts[key] * 1.0 / self._total diff --git a/pads/GraphDegeneracy.py b/pads/GraphDegeneracy.py index c20d475..4da4f18 100644 --- a/pads/GraphDegeneracy.py +++ b/pads/GraphDegeneracy.py @@ -9,67 +9,74 @@ from .Graphs import isUndirected from .BucketQueue import BucketQueue + def degeneracySequence(G): """Generate pairs (vertex,number of later neighbors) in degeneracy order.""" if not isUndirected(G): raise TypeError("Graph must be undirected") Q = BucketQueue() for v in G: - Q[v] = len(G[v]) # prioritize vertices by degree - for v,d in Q.items(): - yield v,d # output vertices in priority order + Q[v] = len(G[v]) # prioritize vertices by degree + for v, d in Q.items(): + yield v, d # output vertices in priority order for w in G[v]: if w in Q: - Q[w] -= 1 # one fewer remaining neighbor + Q[w] -= 1 # one fewer remaining neighbor + def degeneracy(G): """Calculate the degeneracy of a given graph""" - return max(d for v,d in degeneracySequence(G)) + return max(d for v, d in degeneracySequence(G)) + def degeneracyOrientation(G): """Directed version of G with <= degeneracy out-neighbors per vertex.""" D = {} - for v,d in degeneracySequence(G): + for v, d in degeneracySequence(G): D[v] = {w for w in G[v] if w not in D} return D -def core(G,k=None): + +def core(G, k=None): """The k-core of G, or the deepest core if k is not given. The return value is a set of vertices; use Graphs.InducedSubgraph if the edges are also needed.""" level = 0 coreset = set() - for v,d in degeneracySequence(G): - if d > level: # new depth record? + for v, d in degeneracySequence(G): + if d > level: # new depth record? if k == None or level < k: # we care about new records? - coreset = set() # yes, restart core + coreset = set() # yes, restart core level = d coreset.add(v) return coreset + def triangles(G): """Use degeneracy to list all the triangles in G""" G = degeneracyOrientation(G) - return ((u,v,w) for u in G for v in G[u] for w in G[u] if w in G[v]) + return ((u, v, w) for u in G for v in G[u] for w in G[u] if w in G[v]) # ============================================================ # If run from command line, perform unit tests # ============================================================ + class DegeneracyTest(unittest.TestCase): - G = {1:[2,5],2:[1,3,5],3:[2,4],4:[3,5,6],5:[1,2,4],6:[4]} #File:6n-graf.svg + G = {1: [2, 5], 2: [1, 3, 5], 3: [2, 4], 4: [3, 5, 6], 5: [1, 2, 4], 6: [4]} # File:6n-graf.svg def testDegeneracy(self): - self.assertEqual(degeneracy(DegeneracyTest.G),2) - + self.assertEqual(degeneracy(DegeneracyTest.G), 2) + def testCore(self): - self.assertEqual(core(DegeneracyTest.G),{1,2,3,4,5}) + self.assertEqual(core(DegeneracyTest.G), {1, 2, 3, 4, 5}) def testTriangles(self): T = list(triangles(DegeneracyTest.G)) - self.assertEqual(len(T),1) - self.assertEqual(set(T[0]),{1,2,5}) + self.assertEqual(len(T), 1) + self.assertEqual(set(T[0]), {1, 2, 5}) + if __name__ == "__main__": - unittest.main() + unittest.main() diff --git a/pads/GraphExamples.py b/pads/GraphExamples.py index 67108f2..8c25e0c 100644 --- a/pads/GraphExamples.py +++ b/pads/GraphExamples.py @@ -5,42 +5,48 @@ D. Eppstein, September 2005. """ -def GeneralizedPetersenGraph(n,k): + +def GeneralizedPetersenGraph(n, k): G = {} for i in range(n): - G[i,True] = (i,False),((i-1)%n,True),((i+1)%n,True) - G[i,False] = (i,True),((i-k)%n,False),((i+k)%n,False) + G[i, True] = (i, False), ((i - 1) % n, True), ((i + 1) % n, True) + G[i, False] = (i, True), ((i - k) % n, False), ((i + k) % n, False) return G -PetersenGraph = GeneralizedPetersenGraph(5,2) -DesarguesGraph = GeneralizedPetersenGraph(10,3) -def GeneralizedCoxeterGraph(n,a,b): +PetersenGraph = GeneralizedPetersenGraph(5, 2) +DesarguesGraph = GeneralizedPetersenGraph(10, 3) + + +def GeneralizedCoxeterGraph(n, a, b): G = {} for i in range(n): - G[i,0] = (i,1),(i,2),(i,3) - G[i,1] = (i,0),((i+1)%n,1),((i-1)%n,1) - G[i,2] = (i,0),((i+a)%n,2),((i-a)%n,2) - G[i,3] = (i,0),((i+b)%n,1),((i-b)%n,1) + G[i, 0] = (i, 1), (i, 2), (i, 3) + G[i, 1] = (i, 0), ((i + 1) % n, 1), ((i - 1) % n, 1) + G[i, 2] = (i, 0), ((i + a) % n, 2), ((i - a) % n, 2) + G[i, 3] = (i, 0), ((i + b) % n, 1), ((i - b) % n, 1) return G -CoxeterGraph = GeneralizedCoxeterGraph(7,2,3) + +CoxeterGraph = GeneralizedCoxeterGraph(7, 2, 3) + def CubeConnectedCycles(n): - return {(x,y):[(x,(y+1)%n),(x,(y-1)%n),(x^(1< values - self._L = dict() # values -> sets of keys - self._Q = IntegerHeap(i) # queue of values w/nonempty lists + self._D = dict() # keys -> values + self._L = dict() # values -> sets of keys + self._Q = IntegerHeap(i) # queue of values w/nonempty lists def __getitem__(self, key): return self._D[key] @@ -77,7 +79,7 @@ def min(self): def IntegerHeap(i): """Return an integer heap for 2^i-bit integers. We use a BitVectorHeap for small i and a FlatHeap for large i. - + Timing tests indicate that the cutoff i <= 3 is slightly faster than the also-plausible cutoff i <= 2, and that both are much faster than the way-too-large cutoff i <= 4. @@ -87,62 +89,71 @@ def IntegerHeap(i): return BitVectorHeap() return FlatHeap(i) -Log2Table = {} # Table of powers of two, with their logs + +Log2Table = {} # Table of powers of two, with their logs + + def Log2(b): """Return log_2(b), where b must be a power of two.""" while b not in Log2Table: i = len(Log2Table) - Log2Table[1< self._max: raise ValueError("FlatHeap: %s out of range" % repr(x)) @@ -161,7 +172,7 @@ def min(self): raise ValueError("FlatHeap is empty") return self._min - def add(self,x): + def add(self, x): """Include x among the values in the heap.""" self._rangecheck(x) if self._min is None or self._min == x: @@ -171,14 +182,14 @@ def add(self,x): if x < self._min: # swap to make sure the value we're adding is non-minimal x, self._min = self._min, x - H = x >> self._shift # split into high and low halfwords + H = x >> self._shift # split into high and low halfwords L = x - (H << self._shift) if H not in self._LQ: self._HQ.add(H) - self._LQ[H] = IntegerHeap(self._order-1) + self._LQ[H] = IntegerHeap(self._order - 1) self._LQ[H].add(L) - def remove(self,x): + def remove(self, x): """Remove x from the values in the heap.""" self._rangecheck(x) if self._min == x: @@ -191,15 +202,16 @@ def remove(self,x): L = self._LQ[H].min() x = self._min = (H << self._shift) + L else: - H = x >> self._shift # split into high and low halfwords + H = x >> self._shift # split into high and low halfwords L = x - (H << self._shift) if H not in self._LQ: - return # ignore removal when not in heap + return # ignore removal when not in heap self._LQ[H].remove(L) if not self._LQ[H]: del self._LQ[H] self._HQ.remove(H) + # ====================================================================== # LinearHeap # ====================================================================== @@ -207,23 +219,24 @@ def remove(self,x): class LinearHeap: """Maintain the minimum of a set of integers using a set object.""" + def __init__(self): """Create a new BitVectorHeap.""" self._S = set() - + def __nonzero__(self): """True if this heap is nonempty, false if empty.""" return len(self._S) > 0 - + def __bool__(self): """True if this heap is nonempty, false if empty.""" return len(self._S) > 0 - def add(self,x): + def add(self, x): """Include x among the values in the heap.""" self._S.add(x) - def remove(self,x): + def remove(self, x): """Remove x from the values in the heap.""" self._S.remove(x) @@ -237,24 +250,25 @@ def min(self): # ====================================================================== if __name__ == "__main__": - import unittest,random + import unittest, random + random.seed(1234) class IntegerHeapTest(unittest.TestCase): def testHeaps(self): - o = 5 # do tests on 2^5-bit integers + o = 5 # do tests on 2^5-bit integers N = LinearHeap() I = IntegerHeap(o) for iteration in range(20000): - self.assertEqual(bool(N),bool(I)) # both have same emptiness + self.assertEqual(bool(N), bool(I)) # both have same emptiness if (not N) or random.randrange(2): # flip coin for add/remove - x = random.randrange(1<<(1< 1 and p[-1] < p[-2]): p[-1] += 1 yield p @@ -60,13 +62,14 @@ def revlex_partitions(n): yield p p.pop() + def lex_partitions(n): """Similar to revlex_partitions, but in lexicographic order.""" if n == 0: yield [] if n <= 0: return - for p in lex_partitions(n-1): + for p in lex_partitions(n - 1): p.append(1) yield p p.pop() @@ -75,7 +78,9 @@ def lex_partitions(n): yield p p[-1] -= 1 -partitions = revlex_partitions # default partition generating algorithm + +partitions = revlex_partitions # default partition generating algorithm + def binary_partitions(n): """ @@ -94,16 +99,16 @@ def binary_partitions(n): pow <<= 1 partition = [] while pow: - if sum+pow <= n: + if sum + pow <= n: partition.append(pow) sum += pow pow >>= 1 - + # Find all partitions of numbers up to n into powers of two > 1, # in revlex order, by repeatedly splitting the smallest nonunit power, # and replacing the following sequence of 1's by the first revlex # partition with maximum power less than the result of the split. - + # Time analysis: # # Each outer iteration increases len(partition) by at most one @@ -121,8 +126,8 @@ def binary_partitions(n): # of such inner iterations is <= sum_k k*X/2^{k-1} = O(X). # # Therefore the overall average time per output is constant. - - last_nonunit = len(partition) - 1 - (n&1) + + last_nonunit = len(partition) - 1 - (n & 1) while True: yield partition if last_nonunit < 0: @@ -133,24 +138,24 @@ def binary_partitions(n): last_nonunit -= 1 continue partition.append(1) - x = partition[last_nonunit] = partition[last_nonunit+1] = \ - partition[last_nonunit] >> 1 # make the split! + x = partition[last_nonunit] = partition[last_nonunit + 1] = partition[last_nonunit] >> 1 # make the split! last_nonunit += 1 while x > 1: if len(partition) - last_nonunit - 1 >= x: - del partition[-x+1:] + del partition[-x + 1 :] last_nonunit += 1 partition[last_nonunit] = x else: x >>= 1 -def fixed_length_partitions(n,L): + +def fixed_length_partitions(n, L): """ Integer partitions of n into L parts, in colex order. The algorithm follows Knuth v4 fasc3 p38 in rough outline; Knuth credits it to Hindenburg, 1779. """ - + # guard against special cases if L == 0: if n == 0: @@ -163,7 +168,7 @@ def fixed_length_partitions(n,L): if n < L: return - partition = [n - L + 1] + (L-1)*[1] + partition = [n - L + 1] + (L - 1) * [1] while True: yield partition if partition[0] - 1 > partition[1]: @@ -185,6 +190,7 @@ def fixed_length_partitions(n,L): j -= 1 partition[0] = s + def conjugate(p): """ Find the conjugate of a partition. @@ -196,48 +202,50 @@ def conjugate(p): return result while True: result.append(j) - while len(result) >= p[j-1]: + while len(result) >= p[j - 1]: j -= 1 if j == 0: return result - + + # If run standalone, perform unit tests + class PartitionTest(unittest.TestCase): - counts = [1,1,2,3,5,7,11,15,22,30,42,56,77,101,135] + counts = [1, 1, 2, 3, 5, 7, 11, 15, 22, 30, 42, 56, 77, 101, 135] def testCounts(self): """Check that each generator has the right number of outputs.""" for n in range(len(self.counts)): - self.assertEqual(self.counts[n],len(list(mckay(n)))) - self.assertEqual(self.counts[n],len(list(lex_partitions(n)))) - self.assertEqual(self.counts[n],len(list(revlex_partitions(n)))) + self.assertEqual(self.counts[n], len(list(mckay(n)))) + self.assertEqual(self.counts[n], len(list(lex_partitions(n)))) + self.assertEqual(self.counts[n], len(list(revlex_partitions(n)))) def testSums(self): """Check that all outputs are partitions of the input.""" for n in range(len(self.counts)): for p in mckay(n): - self.assertEqual(n,sum(p)) + self.assertEqual(n, sum(p)) for p in revlex_partitions(n): - self.assertEqual(n,sum(p)) + self.assertEqual(n, sum(p)) for p in lex_partitions(n): - self.assertEqual(n,sum(p)) - + self.assertEqual(n, sum(p)) + def testRevLex(self): """Check that the revlex generators' outputs are in revlex order.""" for n in range(len(self.counts)): - last = [n+1] + last = [n + 1] for p in mckay(n): self.assert_(last > p) last = list(p) # make less-mutable copy - last = [n+1] + last = [n + 1] for p in revlex_partitions(n): self.assert_(last > p) last = list(p) # make less-mutable copy def testLex(self): """Check that the lex generator's outputs are in lex order.""" - for n in range(1,len(self.counts)): + for n in range(1, len(self.counts)): last = [] for p in lex_partitions(n): self.assert_(last < p) @@ -255,20 +263,20 @@ def testRange(self): for p in revlex_partitions(n): for x in p: self.assert_(0 < x <= n) - + def testFixedLength(self): """Check that the fixed length partition outputs are correct.""" for n in range(len(self.counts)): pn = [list(p) for p in revlex_partitions(n)] pn.sort() np = 0 - for L in range(n+1): - pnL = [list(p) for p in fixed_length_partitions(n,L)] + for L in range(n + 1): + pnL = [list(p) for p in fixed_length_partitions(n, L)] pnL.sort() np += len(pnL) - self.assertEqual(pnL,[p for p in pn if len(p) == L]) - self.assertEqual(np,len(pn)) - + self.assertEqual(pnL, [p for p in pn if len(p) == L]) + self.assertEqual(np, len(pn)) + def testConjugatePartition(self): """Check that conjugating a partition forms another partition.""" for n in range(len(self.counts)): @@ -276,19 +284,19 @@ def testConjugatePartition(self): c = conjugate(p) for x in c: self.assert_(0 < x <= n) - self.assertEqual(sum(c),n) + self.assertEqual(sum(c), n) def testConjugateInvolution(self): """Check that double conjugation returns the same partition.""" for n in range(len(self.counts)): for p in partitions(n): - self.assertEqual(p,conjugate(conjugate(p))) + self.assertEqual(p, conjugate(conjugate(p))) def testConjugateMaxLen(self): """Check the max-length reversing property of conjugation.""" - for n in range(1,len(self.counts)): + for n in range(1, len(self.counts)): for p in partitions(n): - self.assertEqual(len(p),max(conjugate(p))) + self.assertEqual(len(p), max(conjugate(p))) def testBinary(self): """Test that the binary partitions are generated correctly.""" @@ -300,7 +308,8 @@ def testBinary(self): break else: binaries.append(list(p)) - self.assertEqual(binaries,[list(p) for p in binary_partitions(n)]) + self.assertEqual(binaries, [list(p) for p in binary_partitions(n)]) + if __name__ == "__main__": unittest.main() diff --git a/pads/IntegerPoints.py b/pads/IntegerPoints.py index 95967c6..586d066 100644 --- a/pads/IntegerPoints.py +++ b/pads/IntegerPoints.py @@ -11,14 +11,15 @@ from .BucketQueue import BucketQueue from .Sequence import Sequence + def IntegerPointsByDistance(): """All integer points in order by distance, regardless of sign. The space needed to generate the first N points is O(N^{1/3}). Each point is generated in constant amortized time.""" # Start out by generating the two initial points of the hull - yield (0,0) - yield (0,1) + yield (0, 0) + yield (0, 1) # Data structures: hull, a sequence of edges, and queue, a bucket queue # Each point is represented as a tuple (x,y) of integer coordinates @@ -29,18 +30,18 @@ def IntegerPointsByDistance(): # (which is not necessarily the other endpoint) # beyond -- the next point beyond the edge, in its rectangle class edge: - def __init__(self,corner,unit,beyond): + def __init__(self, corner, unit, beyond): self.corner = corner self.unit = unit self.beyond = beyond - e = edge((0,0),(0,1),(-1,0)) - f = edge((0,1),(0,-1),(1,0)) - hull = Sequence([e,f]) + e = edge((0, 0), (0, 1), (-1, 0)) + f = edge((0, 1), (0, -1), (1, 0)) + hull = Sequence([e, f]) def dist2(p): """Squared Euclidean distance of a point from the origin""" - return p[0]**2 + p[1]**2 + return p[0] ** 2 + p[1] ** 2 def prioritize(e): """Include edge e in the priority queue with its correct priority""" @@ -50,13 +51,13 @@ def prioritize(e): prioritize(e) prioritize(f) - def box(p,u,b): + def box(p, u, b): """Given an edge with corner p and unit u, find a boxed translate of b.""" - dot = (b[0]-p[0])*u[0]+(b[1]-p[1])*u[1] - shift = dot//dist2(u) - b = (b[0]-shift*u[0],b[1]-shift*u[1]) + dot = (b[0] - p[0]) * u[0] + (b[1] - p[1]) * u[1] + shift = dot // dist2(u) + b = (b[0] - shift * u[0], b[1] - shift * u[1]) if abs(u[0]) + abs(u[1]) == 1: # Unit square has 2 boxed xlates, pick best - c = (b[0]+u[0],b[1]+u[1]) + c = (b[0] + u[0], b[1] + u[1]) if dist2(c) < dist2(b): return c return b @@ -65,36 +66,36 @@ def nonconvex(e): """Are e and its successor not strictly convex?""" u = e.unit v = hull.successor(e).unit - return u[1]*v[0]-u[0]*v[1] <= 0 + return u[1] * v[0] - u[0] * v[1] <= 0 def pop(e): """Merge e into its successor (removing the successor from the hull)""" f = hull.successor(e) - if e.unit == f.unit: # Special case flat vertex + if e.unit == f.unit: # Special case flat vertex if dist2(f.beyond) < dist2(e.beyond): e.beyond = f.beyond - else: # Concave vertex, use opp vertex of parallelogram + else: # Concave vertex, use opp vertex of parallelogram p = e.corner q = f.corner r = hull.successor(f).corner - e.unit = (r[0]-p[0],r[1]-p[1]) - e.beyond = (p[0]-q[0]+r[0],p[1]-q[1]+r[1]) + e.unit = (r[0] - p[0], r[1] - p[1]) + e.beyond = (p[0] - q[0] + r[0], p[1] - q[1] + r[1]) hull.remove(f) del queue[f] prioritize(e) - + def split(e): """Update the hull after adding e's beyond point""" p = e.corner q = hull.successor(e).corner u = e.unit b = e.beyond - e.unit = (b[0]-p[0],b[1]-p[1]) - e.beyond = box(p,e.unit,(b[0]-u[0],b[1]-u[1])) - fu = (q[0]-b[0],q[1]-b[1]) - fb = box(b,fu,(q[0]+u[0],q[1]+u[1])) - f = edge(b,fu,fb) - hull.insertAfter(e,f) + e.unit = (b[0] - p[0], b[1] - p[1]) + e.beyond = box(p, e.unit, (b[0] - u[0], b[1] - u[1])) + fu = (q[0] - b[0], q[1] - b[1]) + fb = box(b, fu, (q[0] + u[0], q[1] + u[1])) + f = edge(b, fu, fb) + hull.insertAfter(e, f) prioritize(e) prioritize(f) while nonconvex(hull.predecessor(e)): @@ -107,19 +108,21 @@ def split(e): yield e.beyond split(e) + # ============================================================ # If run from command line, perform unit tests # ============================================================ + class IntegerPointsByDistanceTest(unittest.TestCase): radius = 100 threshold = radius**2 - trange = range(-radius,radius+1) + trange = range(-radius, radius + 1) def testOrdered(self): """Test whether point ordering is by distance.""" oldDistance = 0 - for x,y in IntegerPointsByDistance(): + for x, y in IntegerPointsByDistance(): newDistance = x**2 + y**2 self.assertTrue(newDistance >= oldDistance) oldDistance = newDistance @@ -129,15 +132,23 @@ def testOrdered(self): def testCircle(self): """Test whether each point in a disk is listed exactly once.""" points = [] - for x,y in IntegerPointsByDistance(): + for x, y in IntegerPointsByDistance(): if x**2 + y**2 > IntegerPointsByDistanceTest.threshold: break - points.append((x,y)) - self.assertEqual(len(points),len(set(points))) - self.assertEqual(len(points),len( - [None for x in IntegerPointsByDistanceTest.trange - for y in IntegerPointsByDistanceTest.trange - if x**2 + y**2 <= IntegerPointsByDistanceTest.threshold])) + points.append((x, y)) + self.assertEqual(len(points), len(set(points))) + self.assertEqual( + len(points), + len( + [ + None + for x in IntegerPointsByDistanceTest.trange + for y in IntegerPointsByDistanceTest.trange + if x**2 + y**2 <= IntegerPointsByDistanceTest.threshold + ] + ), + ) + if __name__ == "__main__": - unittest.main() + unittest.main() diff --git a/pads/LCA.py b/pads/LCA.py index 127ee54..a77fa5a 100644 --- a/pads/LCA.py +++ b/pads/LCA.py @@ -14,7 +14,7 @@ D. Eppstein, November 2003. """ -import unittest,random +import unittest, random from collections import defaultdict from .UnionFind import UnionFind @@ -24,14 +24,16 @@ except: xrange = range -def _decodeSlice(self,it): + +def _decodeSlice(self, it): """Work around removal of __getslice__ in Python 3""" if type(it) != slice: raise ValueError("Can only access LCA object by slice notation") - left,right,stride = it.indices(len(self)) + left, right, stride = it.indices(len(self)) if stride != 1: raise ValueError("Stride not permitted in LCA") - return left,right + return left, right + class RangeMin: """If X is any list, RangeMin(X)[i:j] == min(X[i:j]). @@ -39,27 +41,27 @@ class RangeMin: and querying the minimum of a range takes constant time per query. """ - def __init__(self,X): + def __init__(self, X): """Set up structure with sequence X as data. Uses an LCA structure on a Cartesian tree for the input.""" self._data = list(X) if len(X) > 1: big = list(map(max, self._ansv(False), self._ansv(True))) - parents = {i:big[i][1] for i in range(len(X)) if big[i]} + parents = {i: big[i][1] for i in range(len(X)) if big[i]} self._lca = LCA(parents) - def __getitem__(self,it): + def __getitem__(self, it): """When called by X[left:right], return min(X[left:right]).""" - left,right = _decodeSlice(self,it) + left, right = _decodeSlice(self, it) if right <= left: - return None # empty range has no minimum - return self._data[self._lca(left,right-1)] + return None # empty range has no minimum + return self._data[self._lca(left, right - 1)] def __len__(self): """How much data do we have? Needed for negative index in slice.""" return len(self._data) - def _ansv(self,reversed): + def _ansv(self, reversed): """All nearest smaller values. For each x in the data, find the value smaller than x in the closest position to the left of x (if not reversed) or to the right of x @@ -67,19 +69,20 @@ def _ansv(self,reversed): Due to our use of positions as a tie-breaker, values equal to x count as smaller on the left and larger on the right. """ - stack = [(min(self._data),-1)] # protect stack top with sentinel - output = [0]*len(self._data) - for xi in _pairs(self._data,reversed): + stack = [(min(self._data), -1)] # protect stack top with sentinel + output = [0] * len(self._data) + for xi in _pairs(self._data, reversed): while stack[-1] > xi: stack.pop() output[xi[1]] = stack[-1] stack.append(xi) return output - def _lca(self,first,last): + def _lca(self, first, last): """Function to replace LCA when we have too little data.""" return 0 + class RestrictedRangeMin: """Linear-space RangeMin for integer data obeying the constraint abs(X[i]-X[i-1])==1. @@ -89,27 +92,28 @@ class RestrictedRangeMin: all ranges are in the same positions as the minima of the integers in the first positions of each pair, so the data structure still works. """ - def __init__(self,X): + + def __init__(self, X): # Compute parameters for partition into blocks. # Position i in X becomes transformed into # position i&self._blockmask in block i>>self.blocklen - self._blocksize = _log2(len(X))//2 + self._blocksize = _log2(len(X)) // 2 self._blockmask = (1 << self._blocksize) - 1 blocklen = 1 << self._blocksize # Do partition into blocks, find minima within # each block, prefix minima in each block, # and suffix minima in each block - blocks = [] # map block to block id - ids = {} # map block id to PrecomputedRangeMin - blockmin = [] # map block to min value - self._prefix = [None] # map data index to prefix min of block - self._suffix = [] # map data index to suffix min of block - for i in range(0,len(X),blocklen): - XX = X[i:i+blocklen] + blocks = [] # map block to block id + ids = {} # map block id to PrecomputedRangeMin + blockmin = [] # map block to min value + self._prefix = [None] # map data index to prefix min of block + self._suffix = [] # map data index to suffix min of block + for i in range(0, len(X), blocklen): + XX = X[i : i + blocklen] blockmin.append(min(XX)) self._prefix += PrefixMinima(XX) - self._suffix += PrefixMinima(XX,reversed=True) + self._suffix += PrefixMinima(XX, reversed=True) blockid = len(XX) < blocklen and -1 or self._blockid(XX) blocks.append(blockid) if blockid not in ids: @@ -124,70 +128,74 @@ def __len__(self): """How much data do we have? Needed for negative index in slice.""" return len(self._data) - def __getitem__(self,it): + def __getitem__(self, it): """When called by X[left:right], return min(X[left:right]).""" - left,right = _decodeSlice(self,it) + left, right = _decodeSlice(self, it) firstblock = left >> self._blocksize lastblock = (right - 1) >> self._blocksize if firstblock == lastblock: i = left & self._blockmask - position = self._blocks[firstblock][i:i+right-left][1] + position = self._blocks[firstblock][i : i + right - left][1] return self._data[position + (firstblock << self._blocksize)] else: best = min(self._suffix[left], self._prefix[right]) if lastblock > firstblock + 1: - best = min(best, self._blockrange[firstblock+1:lastblock]) + best = min(best, self._blockrange[firstblock + 1 : lastblock]) return best - def _blockid(self,XX): + def _blockid(self, XX): """Return value such that all blocks with the same pattern of increments and decrements get the same id. """ blockid = 0 - for i in range(1,len(XX)): - blockid = blockid*2 + (XX[i] > XX[i-1]) + for i in range(1, len(XX)): + blockid = blockid * 2 + (XX[i] > XX[i - 1]) return blockid + class PrecomputedRangeMin: """RangeMin solved in quadratic space by precomputing all solutions.""" - def __init__(self,X): + def __init__(self, X): self._minima = [PrefixMinima(X[i:]) for i in range(len(X))] - def __getitem__(self,it): + def __getitem__(self, it): """When called by X[left:right], return min(X[left:right]).""" - left,right = _decodeSlice(self,it) - return self._minima[left][right-left-1] + left, right = _decodeSlice(self, it) + return self._minima[left][right - left - 1] def __len__(self): return len(self._minima) + class LogarithmicRangeMin: """RangeMin in O(n log n) space and constant query time.""" - def __init__(self,X): + def __init__(self, X): """Compute min(X[i:i+2**j]) for each possible i,j.""" self._minima = m = [list(X)] for j in range(_log2(len(X))): - m.append(list(map(min, m[-1][:-1<= len(head): head.append(x) if i > 0: - tail.append((head[i-1],tail[i-1])) + tail.append((head[i - 1], tail[i - 1])) elif head[i] > x: head[i] = x if i > 0: - tail[i] = head[i-1],tail[i-1] + tail[i] = head[i - 1], tail[i - 1] if not head: return [] @@ -40,24 +41,26 @@ def LongestIncreasingSubsequence(S): output = [head[-1]] pair = tail[-1] while pair: - x,pair = pair + x, pair = pair output.append(x) output.reverse() return output + # If run as "python LongestIncreasingSubsequence.py", run tests on various # small lists and check that the correct subsequences are generated. + class LISTest(unittest.TestCase): def testLIS(self): - self.assertEqual(LongestIncreasingSubsequence([]),[]) - self.assertEqual(LongestIncreasingSubsequence(range(10,0,-1)),[1]) - self.assertEqual(LongestIncreasingSubsequence(range(10)), - list(range(10))) - self.assertEqual(LongestIncreasingSubsequence([3,1,4,1,5,9,2,6,5,3,5,8,9,7,9]), - [1,2,3,5,8,9]) + self.assertEqual(LongestIncreasingSubsequence([]), []) + self.assertEqual(LongestIncreasingSubsequence(range(10, 0, -1)), [1]) + self.assertEqual(LongestIncreasingSubsequence(range(10)), list(range(10))) + self.assertEqual( + LongestIncreasingSubsequence([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]), [1, 2, 3, 5, 8, 9] + ) -if __name__ == "__main__": - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/pads/Lyndon.py b/pads/Lyndon.py index 1eaa628..1a0be4a 100644 --- a/pads/Lyndon.py +++ b/pads/Lyndon.py @@ -5,64 +5,70 @@ import unittest from .Eratosthenes import MoebiusFunction -def LengthLimitedLyndonWords(s,n): + +def LengthLimitedLyndonWords(s, n): """Generate nonempty Lyndon words of length <= n over an s-symbol alphabet. The words are generated in lexicographic order, using an algorithm from J.-P. Duval, Theor. Comput. Sci. 1988, doi:10.1016/0304-3975(88)90113-2. As shown by Berstel and Pocchiola, it takes constant average time per generated word.""" - w = [-1] # set up for first increment + w = [-1] # set up for first increment while w: - w[-1] += 1 # increment the last non-z symbol + w[-1] += 1 # increment the last non-z symbol yield w m = len(w) - while len(w) < n: # repeat word to fill exactly n syms + while len(w) < n: # repeat word to fill exactly n syms w.append(w[-m]) - while w and w[-1] == s - 1: # delete trailing z's + while w and w[-1] == s - 1: # delete trailing z's w.pop() -def LyndonWordsWithLength(s,n): + +def LyndonWordsWithLength(s, n): """Generate Lyndon words of length exactly n over an s-symbol alphabet. Since nearly half of the outputs of LengthLimitedLyndonWords(s,n) have the desired length, it again takes constant average time per word.""" if n == 0: - yield [] # the empty word is a special case not handled by main alg - for w in LengthLimitedLyndonWords(s,n): + yield [] # the empty word is a special case not handled by main alg + for w in LengthLimitedLyndonWords(s, n): if len(w) == n: yield w + def LyndonWords(s): """Generate all Lyndon words over an s-symbol alphabet. The generation order is by length, then lexicographic within each length.""" n = 0 while True: - for w in LyndonWordsWithLength(s,n): + for w in LyndonWordsWithLength(s, n): yield w n += 1 -def DeBruijnSequence(s,n): + +def DeBruijnSequence(s, n): """Generate a De Bruijn sequence for words of length n over s symbols by concatenating together in lexicographic order the Lyndon words whose lengths divide n. The output length will be s^n. Because nearly half of the generated sequences will have length exactly n, the algorithm will take O(s^n/n) steps, and the bulk of the time will be spent in sequence concatenation.""" - + output = [] - for w in LengthLimitedLyndonWords(s,n): + for w in LengthLimitedLyndonWords(s, n): if n % len(w) == 0: output += w return output -def CountLyndonWords(s,n): + +def CountLyndonWords(s, n): """The number of length-n Lyndon words over s symbols.""" if n == 0: return 1 total = 0 - for i in range(1,n+1): - if n%i == 0: - total += MoebiusFunction(n//i) * s**i - return total//n + for i in range(1, n + 1): + if n % i == 0: + total += MoebiusFunction(n // i) * s**i + return total // n + def ChenFoxLyndonBreakpoints(s): """Find starting positions of Chen-Fox-Lyndon decomposition of s. @@ -74,14 +80,15 @@ def ChenFoxLyndonBreakpoints(s): indexing rather than Duval's choice of 1-based indexing.""" k = 0 while k < len(s): - i,j = k,k+1 + i, j = k, k + 1 while j < len(s) and s[i] <= s[j]: - i = (s[i] == s[j]) and i+1 or k # Python cond?yes:no syntax + i = (s[i] == s[j]) and i + 1 or k # Python cond?yes:no syntax j += 1 - while k < i+1: - k += j-i + while k < i + 1: + k += j - i yield k + def ChenFoxLyndon(s): """Decompose s into Lyndon words according to the Chen-Fox-Lyndon theorem. The arguments are the same as for ChenFoxLyndonBreakpoints but the @@ -91,80 +98,84 @@ def ChenFoxLyndon(s): yield s[old:k] old = k + def SmallestSuffix(s): """Find the suffix of s that is smallest in lexicographic order.""" for w in ChenFoxLyndon(s): pass return w + def SmallestRotation(s): """Find the rotation of s that is smallest in lexicographic order. Duval 1983 describes how to modify his algorithm to do so but I think it's cleaner and more general to work from the ChenFoxLyndon output.""" - prev,rep = None,0 - for w in ChenFoxLyndon(s+s): + prev, rep = None, 0 + for w in ChenFoxLyndon(s + s): if w == prev: rep += 1 else: - prev,rep = w,1 - if len(w)*rep == len(s): - return w*rep + prev, rep = w, 1 + if len(w) * rep == len(s): + return w * rep raise Exception("Reached end of factorization with no shortest rotation") + def isLyndonWord(s): """Is the given sequence a Lyndon word?""" if len(s) == 0: return True return next(ChenFoxLyndonBreakpoints(s)) == len(s) + # If run standalone, perform unit tests class LyndonTest(unittest.TestCase): def testCount(self): """Test that we count Lyndon words correctly.""" - for s in range(2,7): - for n in range(1,6): - self.assertEqual(CountLyndonWords(s,n), - len(list(LyndonWordsWithLength(s,n)))) + for s in range(2, 7): + for n in range(1, 6): + self.assertEqual(CountLyndonWords(s, n), len(list(LyndonWordsWithLength(s, n)))) def testOrder(self): """Test that we generate Lyndon words in lexicographic order.""" - for s in range(2,7): - for n in range(1,6): + for s in range(2, 7): + for n in range(1, 6): prev = [] - for x in LengthLimitedLyndonWords(s,n): + for x in LengthLimitedLyndonWords(s, n): self.assertTrue(prev < x) prev = list(x) def testSubsequence(self): """Test that words of length n-1 are a subsequence of length n.""" - for s in range(2,7): - for n in range(2,6): - smaller = LengthLimitedLyndonWords(s,n-1) - for x in LengthLimitedLyndonWords(s,n): + for s in range(2, 7): + for n in range(2, 6): + smaller = LengthLimitedLyndonWords(s, n - 1) + for x in LengthLimitedLyndonWords(s, n): if len(x) < n: - self.assertEqual(x,next(smaller)) - + self.assertEqual(x, next(smaller)) + def testIsLyndon(self): """Test that the words we generate are Lyndon words.""" - for s in range(2,7): - for n in range(2,6): - for w in LengthLimitedLyndonWords(s,n): + for s in range(2, 7): + for n in range(2, 6): + for w in LengthLimitedLyndonWords(s, n): self.assertEqual(isLyndonWord(w), True) - + def testNotLyndon(self): """Test that words that are not Lyndon words aren't claimed to be.""" nl = sum(1 for i in range(8**4) if isLyndonWord("%04o" % i)) - self.assertEqual(nl,CountLyndonWords(8,4)) + self.assertEqual(nl, CountLyndonWords(8, 4)) def testDeBruijn(self): """Test that the De Bruijn sequence is correct.""" - for s in range(2,7): - for n in range(1,6): - db = DeBruijnSequence(s,n) + for s in range(2, 7): + for n in range(1, 6): + db = DeBruijnSequence(s, n) self.assertEqual(len(db), s**n) - db = db + db # duplicate so we can wrap easier - subs = set(tuple(db[i:i+n]) for i in range(s**n)) + db = db + db # duplicate so we can wrap easier + subs = set(tuple(db[i : i + n]) for i in range(s**n)) self.assertEqual(len(subs), s**n) + if __name__ == "__main__": unittest.main() diff --git a/pads/Medium.py b/pads/Medium.py index 46fb5e0..bf41570 100644 --- a/pads/Medium.py +++ b/pads/Medium.py @@ -21,24 +21,27 @@ D. Eppstein, May 2007. """ - -from . import BFS,DFS + +from . import BFS, DFS from .Graphs import isUndirected import unittest -class MediumError(ValueError): pass + +class MediumError(ValueError): + pass + class Medium: """ Base class for media. - + A medium is defined by four instance methods: - M.states() lists the states of M - M.tokens() lists the tokens of M - M.reverse(token) finds the token with opposite action to its argument - M.action(state,token) gives the result of applying that token These should be defined in subclasses; the base does not define them. - + In addition, we define methods (that may possibly be overridden): - iter(M) is a synonym for M.states() - len(M) is a synonym for len(M.states()) @@ -50,7 +53,7 @@ class Medium: def __iter__(self): """Generate sequence of medium states.""" return self.states() - + def __len__(self): """Return number of states in the medium.""" i = 0 @@ -58,13 +61,13 @@ def __len__(self): i += 1 return i - def __getitem__(self,S): + def __getitem__(self, S): """Construct dict mapping tokens to actions from state S.""" - return {t:self.action(S,t) for t in self.tokens()} + return {t: self.action(S, t) for t in self.tokens()} - def __call__(self,S,t): + def __call__(self, S, t): """Apply token t to state S.""" - return self.action(S,t) + return self.action(S, t) class ExplicitMedium(Medium): @@ -75,10 +78,10 @@ class ExplicitMedium(Medium): (# states) x (# tokens) but it makes all operations fast. """ - def __init__(self,M): + def __init__(self, M): """Form ExplicitMedium from any other kind of medium.""" - self._reverse = {t:M.reverse(t) for t in M.tokens()} - self._action = {S:M[S] for S in M} + self._reverse = {t: M.reverse(t) for t in M.tokens()} + self._action = {S: M[S] for S in M} # Basic classes needed to define any medium @@ -88,10 +91,10 @@ def states(self): def tokens(self): return iter(self._reverse) - def reverse(self,t): + def reverse(self, t): return self._reverse[t] - def action(self,S,t): + def action(self, S, t): return self._action[S][t] # Faster implementation of other medium functions @@ -99,55 +102,55 @@ def action(self,S,t): def __len__(self): return len(self._action) - def __getitem__(self,S): + def __getitem__(self, S): return self._action[S] class BitvectorMedium(Medium): """ Medium defined by a set of bitvectors. - + The tokens of the medium are pairs (i,b) where i is an index into a bitvector and b is a bit; the action of a token on a bitvector is to change the i'th bit to b, if the result is part of the set, and if not to leave the bitvector unchanged. - + We assume but do not verify that the bitvectors do form a medium; that is, that one can transform any bitvector in the set into any other via a sequence of actions of length equal to the Hamming distance between the vectors. """ - def __init__(self,states,L): + def __init__(self, states, L): """Initialize medium for set states and bitvector length L.""" self._states = set(states) self._veclen = L - + def states(self): return iter(self._states) def tokens(self): for i in range(self._veclen): - yield i,False - yield i,True + yield i, False + yield i, True - def reverse(self,t): - i,b = t - return i,not b + def reverse(self, t): + i, b = t + return i, not b - def action(self,S,t): + def action(self, S, t): """ Compute the action of token t on state S. We form the bitvector V that should correspond to St, then test whether V belongs to the given set of states. If so, we return it; otherwise, we return S itself. """ - i,b = t - mask = 1<= len(activeTokens): - raise MediumError("no active token from %s to %s" %(S,current)) - if activeTokens[i] != inactivated and M(S,activeTokens[i]) != S: + raise MediumError("no active token from %s to %s" % (S, current)) + if activeTokens[i] != inactivated and M(S, activeTokens[i]) != S: activeForState[S] = i statesForPos[i].append(S) return - + # set initial active states for S in M: if S != current: @@ -267,16 +271,16 @@ def scan(S): # traverse the graph, maintaining active tokens visited = set() routes = {} - for prev,current,edgetype in DFS.search(G,initialState): + for prev, current, edgetype in DFS.search(G, initialState): if prev != current and edgetype != DFS.nontree: if edgetype == DFS.reverse: - prev,current = current,prev - + prev, current = current, prev + # add token to end of list, point to it from old state activeTokens.append(G[prev][current]) activeForState[prev] = len(activeTokens) - 1 statesForPos.append([prev]) - + # inactivate reverse token, find new token for its states activeTokens[activeForState[current]] = inactivated for S in statesForPos[activeForState[current]]: @@ -287,7 +291,7 @@ def scan(S): if current not in visited: for S in M: if S != current: - routes[S,current] = activeTokens[activeForState[S]] + routes[S, current] = activeTokens[activeForState[S]] return routes @@ -298,11 +302,11 @@ def HypercubeEmbedding(M): tokmap = {} for t in M.tokens(): if t not in tokmap: - tokmap[t] = tokmap[M.reverse(t)] = 1<>i)&1 - self.assertEqual(x,M(x,(i,b))) - y = M(x,(i,not b)) + b = (x >> i) & 1 + self.assertEqual(x, M(x, (i, b))) + y = M(x, (i, not b)) if b == (x in MediumTest.twobits): - self.assertEqual(x,y) + self.assertEqual(x, y) else: - self.assertEqual(y,x^(1<>i)&1, not b) - - def testExplicit(self): + i, b = R[x, y] + self.assertEqual((x ^ y) & (1 << i), 1 << i) + self.assertEqual((x >> i) & 1, not b) + + def testExplicit(self): """Check that ExplicitMedium looks the same as its argument.""" M = MediumTest.M523 E = ExplicitMedium(M) - self.assertEqual(set(M),set(E)) - self.assertEqual(set(M.tokens()),set(E.tokens())) + self.assertEqual(set(M), set(E)) + self.assertEqual(set(M.tokens()), set(E.tokens())) for t in M.tokens(): - self.assertEqual(M.reverse(t),E.reverse(t)) + self.assertEqual(M.reverse(t), E.reverse(t)) for s in M: for t in M.tokens(): - self.assertEqual(M(s,t),E(s,t)) + self.assertEqual(M(s, t), E(s, t)) def testEmbed(self): """Check that HypercubeEmbedding finds appropriate coordinates.""" M = MediumTest.M523 E = HypercubeEmbedding(M) - def ham(x,y): - z = x^y + + def ham(x, y): + z = x ^ y d = 0 while z: d += 1 - z &= z-1 + z &= z - 1 return d + for x in M: for y in M: - self.assertEqual(ham(x,y),ham(E[x],E[y])) + self.assertEqual(ham(x, y), ham(E[x], E[y])) def testGraph(self): """Check that LabeledGraphMedium(StateTransitionGraph(M)) = M.""" M = MediumTest.M523 L = LabeledGraphMedium(StateTransitionGraph(M)) - self.assertEqual(set(M),set(L)) - self.assertEqual(set(M.tokens()),set(L.tokens())) + self.assertEqual(set(M), set(L)) + self.assertEqual(set(M.tokens()), set(L.tokens())) for t in M.tokens(): - self.assertEqual(M.reverse(t),L.reverse(t)) + self.assertEqual(M.reverse(t), L.reverse(t)) for s in M: for t in M.tokens(): - self.assertEqual(M(s,t),L(s,t)) + self.assertEqual(M(s, t), L(s, t)) + if __name__ == "__main__": unittest.main() - diff --git a/pads/MinimumSpanningTree.py b/pads/MinimumSpanningTree.py index 1de4a84..b3a8a7b 100644 --- a/pads/MinimumSpanningTree.py +++ b/pads/MinimumSpanningTree.py @@ -7,6 +7,7 @@ from .UnionFind import UnionFind from .Graphs import isUndirected + def MinimumSpanningTree(G): """ Return the minimum spanning tree of an undirected graph G. @@ -28,23 +29,25 @@ def MinimumSpanningTree(G): # part (the sort) is sped up by being built in to Python. subtrees = UnionFind() tree = [] - for W,u,v in sorted((G[u][v],u,v) for u in G for v in G[u]): + for W, u, v in sorted((G[u][v], u, v) for u in G for v in G[u]): if subtrees[u] != subtrees[v]: - tree.append((u,v)) - subtrees.union(u,v) - return tree + tree.append((u, v)) + subtrees.union(u, v) + return tree # If run standalone, perform unit tests + class MSTTest(unittest.TestCase): def testMST(self): """Check that MinimumSpanningTree returns the correct answer.""" - G = {0:{1:11,2:13,3:12},1:{0:11,3:14},2:{0:13,3:10},3:{0:12,1:14,2:10}} - T = [(2,3),(0,1),(0,3)] - for e,f in zip(MinimumSpanningTree(G),T): - self.assertEqual(min(e),min(f)) - self.assertEqual(max(e),max(f)) + G = {0: {1: 11, 2: 13, 3: 12}, 1: {0: 11, 3: 14}, 2: {0: 13, 3: 10}, 3: {0: 12, 1: 14, 2: 10}} + T = [(2, 3), (0, 1), (0, 3)] + for e, f in zip(MinimumSpanningTree(G), T): + self.assertEqual(min(e), min(f)) + self.assertEqual(max(e), max(f)) + if __name__ == "__main__": - unittest.main() + unittest.main() diff --git a/pads/Not.py b/pads/Not.py index 6dfd569..ceddc9b 100644 --- a/pads/Not.py +++ b/pads/Not.py @@ -18,47 +18,54 @@ import unittest -class DoubleNegationError(Exception): pass + +class DoubleNegationError(Exception): + pass + class SymbolicNegation: - def __init__(self,x): - if isinstance(x,SymbolicNegation): - raise DoubleNegationError( - "Use Not(x) rather than instantiating SymbolicNegation directly") + def __init__(self, x): + if isinstance(x, SymbolicNegation): + raise DoubleNegationError("Use Not(x) rather than instantiating SymbolicNegation directly") self.negation = x def negate(self): return self.negation - + def __repr__(self): return "Not(" + repr(self.negation) + ")" - def __eq__(self,other): - return isinstance(other,SymbolicNegation) and \ - self.negation == other.negation + def __eq__(self, other): + return isinstance(other, SymbolicNegation) and self.negation == other.negation def __hash__(self): return -hash(self.negation) + def Not(x): - if isinstance(x,SymbolicNegation): + if isinstance(x, SymbolicNegation): return x.negate() else: return SymbolicNegation(x) + class NotNotTest(unittest.TestCase): - things = [None,3,"ABC",Not(27)] + things = [None, 3, "ABC", Not(27)] + def testNot(self): for x in NotNotTest.things: - self.assertEqual(Not(Not(x)),x) + self.assertEqual(Not(Not(x)), x) + def testEq(self): for x in NotNotTest.things: for y in NotNotTest.things: - self.assertEqual(Not(x)==Not(y),x==y) + self.assertEqual(Not(x) == Not(y), x == y) + def testHash(self): - D = {Not(x):x for x in NotNotTest.things} + D = {Not(x): x for x in NotNotTest.things} for x in NotNotTest.things: - self.assertEqual(D[Not(x)],x) + self.assertEqual(D[Not(x)], x) + if __name__ == "__main__": - unittest.main() + unittest.main() diff --git a/pads/OrderedSequence.py b/pads/OrderedSequence.py index aac8e63..18ebfcd 100644 --- a/pads/OrderedSequence.py +++ b/pads/OrderedSequence.py @@ -9,6 +9,7 @@ import sys from .Sequence import Sequence + class SimpleOrderedSequence(Sequence): """Maintain a sequence of items subject to insertions, removals, and comparisons of the positions of pairs of items. In addition to @@ -21,29 +22,29 @@ class SimpleOrderedSequence(Sequence): to use this data structure only for sequences of very few items. """ - def __init__(self,iterable=[],key=None): + def __init__(self, iterable=[], key=None): """The only additional data we maintain over a vanilla Sequence is a dictionary self._tag mapping sequence items to integers, such that an item is earlier than another iff its tag is smaller. """ self._tag = {} - Sequence.__init__(self,iterable,key=key) + Sequence.__init__(self, iterable, key=key) - def cmp(self,x,y): + def cmp(self, x, y): """Compare the positions of x and y in the sequence.""" - return cmp(self._tag[self.key(x)],self._tag[self.key(y)]) + return cmp(self._tag[self.key(x)], self._tag[self.key(y)]) - def append(self,x): + def append(self, x): """Add x to the end of the sequence.""" if not self._next: # add to empty sequence - Sequence.append(self,x) - self._tag[self.key(x)] = sys.maxint//2 + Sequence.append(self, x) + self._tag[self.key(x)] = sys.maxint // 2 else: - self.insertAfter(self._prev[self._first],x) + self.insertAfter(self._prev[self._first], x) - def insertAfter(self,x,y): + def insertAfter(self, x, y): """Add y after x and compute a tag for it.""" - Sequence.insertAfter(self,x,y) + Sequence.insertAfter(self, x, y) x = self.key(x) y = self.key(y) next = self._next[y] @@ -52,28 +53,29 @@ def insertAfter(self,x,y): else: nexttag = self._tag[next] xtag = self._tag[x] - self._tag[y] = xtag + (nexttag - xtag + 1)//2 + self._tag[y] = xtag + (nexttag - xtag + 1) // 2 if self._tag[y] == nexttag: self.rebalance(y) - def insertBefore(self,x,y): + def insertBefore(self, x, y): """Add y before x in the sequence.""" - Sequence.insertBefore(self,x,y) + Sequence.insertBefore(self, x, y) x = self.key(x) y = self.key(y) if self._first == y: - self._tag[y] = self._tag[x]//2 + self._tag[y] = self._tag[x] // 2 if self._tag[y] == self._tag[x]: self.rebalance(y) - def rebalance(self,x): + def rebalance(self, x): """Clean up after x and its successor's tags collide.""" base = 0 - increment = sys.maxint//len(self) + increment = sys.maxint // len(self) for y in self: self._tag[y] = base base += increment + class LogarithmicOrderedSequence(SimpleOrderedSequence): """Maintain a sequence of items subject to insertions, removals, and comparisons of the positions of pairs of items. We use the @@ -84,7 +86,7 @@ class LogarithmicOrderedSequence(SimpleOrderedSequence): the amortized time per insertion in an n-item list is O(log n). """ - def rebalance(self,x): + def rebalance(self, x): """Clean up after x and its successor's tags collide. At each iteration of the rebalancing algorithm, we look at @@ -106,18 +108,16 @@ def rebalance(self,x): threshhold = 1.0 first = last = x nItems = 1 - multiplier = 2/(2*len(self))**(1/30.) + multiplier = 2 / (2 * len(self)) ** (1 / 30.0) while 1: - while first != self._first and \ - self._tag[self._prev[first]] &~ mask == base: + while first != self._first and self._tag[self._prev[first]] & ~mask == base: first = self._prev[first] nItems += 1 - while self._next[last] != self._first and \ - self._tag[self._next[last]] &~ mask == base: + while self._next[last] != self._first and self._tag[self._next[last]] & ~mask == base: last = self._next[last] nItems += 1 - increment = (mask+1)//nItems - if increment >= threshhold: # found rebalanceable range + increment = (mask + 1) // nItems + if increment >= threshhold: # found rebalanceable range item = first while item != last: self._tag[item] = base @@ -125,6 +125,6 @@ def rebalance(self,x): base += increment self._tag[last] = base return - mask = (mask << 1) + 1 # expand to next power of two - base = base &~ mask + mask = (mask << 1) + 1 # expand to next power of two + base = base & ~mask threshhold *= multiplier diff --git a/pads/PartialCube.py b/pads/PartialCube.py index 96ea2fc..88caec3 100644 --- a/pads/PartialCube.py +++ b/pads/PartialCube.py @@ -13,6 +13,7 @@ from .Graphs import isUndirected import unittest + def PartialCubeEdgeLabeling(G): """ Label edges of G by their equivalence classes in a partial cube structure. @@ -27,7 +28,7 @@ def PartialCubeEdgeLabeling(G): set representing edges in the original graph that have been contracted to the single edge v-w. """ - + # Some simple sanity checks if not isUndirected(G): raise Medium.MediumError("graph is not undirected") @@ -40,55 +41,55 @@ def PartialCubeEdgeLabeling(G): # - CG: contracted graph at current stage of algorithm # - LL: limit on number of remaining available labels UF = UnionFind() - CG = {v:{w:(v,w) for w in G[v]} for v in G} - NL = len(CG)-1 - + CG = {v: {w: (v, w) for w in G[v]} for v in G} + NL = len(CG) - 1 + # Initial sanity check: are there few enough edges? # Needed so that we don't try to use union-find on a dense # graph and incur superquadratic runtimes. n = len(CG) m = sum([len(CG[v]) for v in CG]) - if 1<<(m//n) > n: + if 1 << (m // n) > n: raise Medium.MediumError("graph has too many edges") # Main contraction loop in place of the original algorithm's recursion - while len(CG) > 1: + while len(CG) > 1: if not isBipartite(CG): raise Medium.MediumError("graph is not bipartite") # Find max degree vertex in G, and update label limit - deg,root = max([(len(CG[v]),v) for v in CG]) + deg, root = max([(len(CG[v]), v) for v in CG]) if deg > NL: raise Medium.MediumError("graph has too many equivalence classes") NL -= deg # Set up bitvectors on vertices - bitvec = {v:0 for v in CG} + bitvec = {v: 0 for v in CG} neighbors = {} i = 0 for neighbor in CG[root]: - bitvec[neighbor] = 1< i] for i in range(16)} - + cube = {i: [i ^ b for b in (1, 2, 4, 8) if i ^ b > i] for i in range(16)} + def testHypercubeAcyclic(self): self.assert_(isAcyclic(self.cube)) - + def testHypercubeClosure(self): TC = TransitiveClosure(self.cube) for i in range(16): - self.assertEqual(TC[i], - {j for j in range(16) if i & j == i and i != j}) + self.assertEqual(TC[i], {j for j in range(16) if i & j == i and i != j}) - def testHypercubeAntichain(self): + def testHypercubeAntichain(self): A = MaximumAntichain(self.cube) - self.assertEqual(A,{3,5,6,9,10,12}) - + self.assertEqual(A, {3, 5, 6, 9, 10, 12}) + def testHypercubeDilworth(self): CD = list(MinimumChainDecomposition(self.cube)) - self.assertEqual(len(CD),6) + self.assertEqual(len(CD), 6) + if __name__ == "__main__": - unittest.main() + unittest.main() diff --git a/pads/PartitionRefinement.py b/pads/PartitionRefinement.py index c6b5872..a2cc17a 100644 --- a/pads/PartitionRefinement.py +++ b/pads/PartitionRefinement.py @@ -7,7 +7,10 @@ D. Eppstein, November 2003. """ -class PartitionError(Exception): pass + +class PartitionError(Exception): + pass + class PartitionRefinement: """Maintain and refine a partition of a set of items into subsets. @@ -15,21 +18,21 @@ class PartitionRefinement: operation takes time proportional to the size of its argument. """ - def __init__(self,items): + def __init__(self, items): """Create a new partition refinement data structure for the given items. Initially, all items belong to the same subset. """ S = set(items) - self._sets = {id(S):S} - self._partition = {x:S for x in S} + self._sets = {id(S): S} + self._partition = {x: S for x in S} - def __getitem__(self,element): + def __getitem__(self, element): """Return the set that contains the given element.""" return self._partition[element] def __iter__(self): """Loop through the sets in the partition.""" - try: # Python 2/3 compatibility + try: # Python 2/3 compatibility return self._sets.itervalues() except AttributeError: return iter(self._sets.values()) @@ -38,7 +41,7 @@ def __len__(self): """Return the number of sets in the partition.""" return len(self._sets) - def add(self,element,theset): + def add(self, element, theset): """Add a new element to the given partition subset.""" if id(theset) not in self._sets: raise PartitionError("Set does not belong to the partition") @@ -47,12 +50,12 @@ def add(self,element,theset): theset.add(element) self._partition[element] = theset - def remove(self,element): + def remove(self, element): """Remove the given element from its partition subset.""" self._partition[element].remove(element) del self._partition[element] - def refine(self,S): + def refine(self, S): """Refine each set A in the partition to the two sets A & S, A - S. Return a list of pairs (A & S, A - S) for each changed set. Within each pair, A & S will be @@ -66,15 +69,15 @@ def refine(self,S): for x in S: if x in self._partition: Ax = self._partition[x] - hit.setdefault(id(Ax),set()).add(x) - for A,AS in hit.items(): + hit.setdefault(id(Ax), set()).add(x) + for A, AS in hit.items(): A = self._sets[A] if AS != A: self._sets[id(AS)] = AS for x in AS: self._partition[x] = AS A -= AS - output.append((AS,A)) + output.append((AS, A)) return output def freeze(self): diff --git a/pads/Permutations.py b/pads/Permutations.py index d4379c1..230b0b9 100644 --- a/pads/Permutations.py +++ b/pads/Permutations.py @@ -40,13 +40,14 @@ except: xrange = range + def PlainChanges(n): """Generate the swaps for the Steinhaus-Johnson-Trotter algorithm.""" if n < 1: return - up = xrange(n-1) - down = xrange(n-2,-1,-1) - recur = PlainChanges(n-1) + up = xrange(n - 1) + down = xrange(n - 2, -1, -1) + recur = PlainChanges(n - 1) try: while True: for x in down: @@ -58,6 +59,7 @@ def PlainChanges(n): except StopIteration: pass + def SteinhausJohnsonTrotter(x): """Generate all permutations of x. If x is a number rather than an iterable, we generate the permutations @@ -73,16 +75,17 @@ def SteinhausJohnsonTrotter(x): # run through the sequence of swaps yield perm for x in PlainChanges(n): - perm[x],perm[x+1] = perm[x+1],perm[x] + perm[x], perm[x + 1] = perm[x + 1], perm[x] yield perm + def DoublePlainChanges(n): """Generate the swaps for double permutations.""" if n < 1: return - up = xrange(1,2*n-1) - down = xrange(2*n-2,0,-1) - recur = DoublePlainChanges(n-1) + up = xrange(1, 2 * n - 1) + down = xrange(2 * n - 2, 0, -1) + recur = DoublePlainChanges(n - 1) try: while True: for x in up: @@ -94,18 +97,20 @@ def DoublePlainChanges(n): except StopIteration: pass + def DoubleSteinhausJohnsonTrotter(n): """Generate all double permutations of the range 0 through n-1""" perm = [] for i in range(n): - perm += [i,i] + perm += [i, i] # run through the sequence of swaps yield perm for x in DoublePlainChanges(n): - perm[x],perm[x+1] = perm[x+1],perm[x] + perm[x], perm[x + 1] = perm[x + 1], perm[x] yield perm + def StirlingChanges(n): """Variant Steinhaus-Johnson-Trotter for Stirling permutations. A Stirling permutation is a double permutation in which each @@ -117,9 +122,9 @@ def StirlingChanges(n): in swapping items two positions apart instead of adjacent items.""" if n <= 1: return - up = xrange(2*n-2) - down = xrange(2*n-3,-1,-1) - recur = StirlingChanges(n-1) + up = xrange(2 * n - 2) + down = xrange(2 * n - 3, -1, -1) + recur = StirlingChanges(n - 1) try: while True: for x in down: @@ -131,18 +136,20 @@ def StirlingChanges(n): except StopIteration: pass + def StirlingPermutations(n): """Generate all Stirling permutations of order n.""" perm = [] for i in range(n): - perm += [i,i] + perm += [i, i] # run through the sequence of swaps yield perm for x in StirlingChanges(n): - perm[x],perm[x+2] = perm[x+2],perm[x] + perm[x], perm[x + 2] = perm[x + 2], perm[x] yield perm + def InvolutionChanges(n): """Generate change sequence for involutions on n items. Uses a variation of the Steinhaus-Johnson-Trotter idea, @@ -151,17 +158,17 @@ def InvolutionChanges(n): for the last item back and forth over a recursively generated sequence for n-2.""" if n <= 3: - for c in [[],[],[0],[0,1,0]][n]: + for c in [[], [], [0], [0, 1, 0]][n]: yield c return - for c in InvolutionChanges(n-1): + for c in InvolutionChanges(n - 1): yield c - yield n-2 - for i in range(n-4,-1,-1): + yield n - 2 + for i in range(n - 4, -1, -1): yield i - ic = InvolutionChanges(n-2) - up = range(0,n-2) - down = range(n-3,-1,-1) + ic = InvolutionChanges(n - 2) + up = range(0, n - 2) + down = range(n - 3, -1, -1) try: while True: yield next(ic) + 1 @@ -171,7 +178,8 @@ def InvolutionChanges(n): for i in down: yield i except StopIteration: - yield n-4 + yield n - 4 + def Involutions(n): """Generate involutions on n items. @@ -184,65 +192,69 @@ def Involutions(n): p = list(range(n)) yield p for c in InvolutionChanges(n): - x,y = p[c],p[c+1] # current partners of c and c+1 - if x == c and y != c+1: x = c+1 - if x != c and y == c+1: y = c - p[x],p[y],p[c],p[c+1] = c+1, c, y, x # swap partners + x, y = p[c], p[c + 1] # current partners of c and c+1 + if x == c and y != c + 1: + x = c + 1 + if x != c and y == c + 1: + y = c + p[x], p[y], p[c], p[c + 1] = c + 1, c, y, x # swap partners yield p + # If run standalone, perform unit tests -class PermutationTest(unittest.TestCase): +class PermutationTest(unittest.TestCase): def testChanges(self): """Do we get the expected sequence of changes for n=3?""" - self.assertEqual(list(PlainChanges(3)),[1,0,1,0,1]) - + self.assertEqual(list(PlainChanges(3)), [1, 0, 1, 0, 1]) + def testLengths(self): """Are the lengths of the generated sequences factorial?""" f = 1 - for i in range(2,7): + for i in range(2, 7): f *= i - self.assertEqual(f,len(list(SteinhausJohnsonTrotter(i)))) - + self.assertEqual(f, len(list(SteinhausJohnsonTrotter(i)))) + def testDistinct(self): """Are all permutations in the sequence different from each other?""" - for i in range(2,7): + for i in range(2, 7): s = set() n = 0 for x in SteinhausJohnsonTrotter(i): s.add(tuple(x)) n += 1 - self.assertEqual(len(s),n) - + self.assertEqual(len(s), n) + def testAdjacent(self): """Do consecutive permutations in the sequence differ by a swap?""" - for i in range(2,7): + for i in range(2, 7): last = None for p in SteinhausJohnsonTrotter(i): if last: diffs = [j for j in range(i) if p[j] != last[j]] - self.assertEqual(len(diffs),2) - self.assertEqual(p[diffs[0]],last[diffs[1]]) - self.assertEqual(p[diffs[1]],last[diffs[0]]) + self.assertEqual(len(diffs), 2) + self.assertEqual(p[diffs[0]], last[diffs[1]]) + self.assertEqual(p[diffs[1]], last[diffs[0]]) last = list(p) - + def testListInput(self): """If given a list as input, is it the first output?""" - for L in ([1,3,5,7], list('zyx'), [], [[]], list(range(20))): - self.assertEqual(L,next(SteinhausJohnsonTrotter(L))) + for L in ([1, 3, 5, 7], list("zyx"), [], [[]], list(range(20))): + self.assertEqual(L, next(SteinhausJohnsonTrotter(L))) def testInvolutions(self): """Are these involutions and do we have the right number of them?""" - telephone = [1,1,2,4,10,26,76,232,764] + telephone = [1, 1, 2, 4, 10, 26, 76, 232, 764] for n in range(len(telephone)): count = 0 sorted = list(range(n)) invs = set() for p in Involutions(n): - self.assertEqual([p[i] for i in p],sorted) + self.assertEqual([p[i] for i in p], sorted) invs.add(tuple(p)) count += 1 - self.assertEqual(len(invs),count) - self.assertEqual(len(invs),telephone[n]) + self.assertEqual(len(invs), count) + self.assertEqual(len(invs), telephone[n]) + if __name__ == "__main__": unittest.main() diff --git a/pads/ReadUndirectedGraph.py b/pads/ReadUndirectedGraph.py index 44371e3..2eb0b42 100644 --- a/pads/ReadUndirectedGraph.py +++ b/pads/ReadUndirectedGraph.py @@ -27,373 +27,394 @@ D. Eppstein, UC Irvine, August 12, 2003. """ + class GraphFormatError(Exception): - pass + pass + def graphNum(s): - """Parse s as an integer, complain appropriately if it fails.""" - try: - return int(s) - except: - raise GraphFormatError('Number expected: "%s"' % s) + """Parse s as an integer, complain appropriately if it fails.""" + try: + return int(s) + except: + raise GraphFormatError('Number expected: "%s"' % s) + def graph(): - """Create a new empty graph.""" - return {} - + """Create a new empty graph.""" + return {} + + def vertex(G, v): - """Add new vertex v to graph G.""" - if v in G: - raise GraphFormatError('Duplicate vertex %s', str(v)) - G[v] = {} - -def edge(G,u,v,e): - """Add edge e connecting vertices u and v in graph G. Vertices must already be in G.""" - if u == v: - raise GraphFormatError('Self-loop at %s' % str(u)) - if u not in G: - raise GraphFormatError('Unexpected vertex %s in edge to %s' % (str(u),str(v))) - if v not in G: - raise GraphFormatError('Unexpected vertex %s in edge from %s' % (str(v),str(u))) - G[u][v] = G[v][u] = e + """Add new vertex v to graph G.""" + if v in G: + raise GraphFormatError("Duplicate vertex %s", str(v)) + G[v] = {} + + +def edge(G, u, v, e): + """Add edge e connecting vertices u and v in graph G. Vertices must already be in G.""" + if u == v: + raise GraphFormatError("Self-loop at %s" % str(u)) + if u not in G: + raise GraphFormatError("Unexpected vertex %s in edge to %s" % (str(u), str(v))) + if v not in G: + raise GraphFormatError("Unexpected vertex %s in edge from %s" % (str(v), str(u))) + G[u][v] = G[v][u] = e # ========================================================================== -# MALF format +# MALF format # ========================================================================== + def readMALF(lines): - """Read undirected graph in MALF format.""" - G = graph() - lines = iter(filter(None,lines)) - for line in lines: - if line == '#': - break - n = graphNum(line.split()[0]) - if n != len(G)+1: - raise GraphFormatError('Nonconsecutive vertices in MALF') - vertex(G,n) - - m = 0 - for line in lines: - nums = [graphNum(x) for x in line.split()] - m += 1 - if len(nums) != 4: - raise GraphFormatError("Other than four numbers per line in MALF edge list") - elif nums[0] != m: - raise GraphFormatError('Nonconsecutive edges in MALF') - elif nums[1] != 0: - raise GraphFormatError("Unrecognized edge type in MALF edge list") - edge(G,nums[2],nums[3],m) - - return G + """Read undirected graph in MALF format.""" + G = graph() + lines = iter(filter(None, lines)) + for line in lines: + if line == "#": + break + n = graphNum(line.split()[0]) + if n != len(G) + 1: + raise GraphFormatError("Nonconsecutive vertices in MALF") + vertex(G, n) + + m = 0 + for line in lines: + nums = [graphNum(x) for x in line.split()] + m += 1 + if len(nums) != 4: + raise GraphFormatError("Other than four numbers per line in MALF edge list") + elif nums[0] != m: + raise GraphFormatError("Nonconsecutive edges in MALF") + elif nums[1] != 0: + raise GraphFormatError("Unrecognized edge type in MALF edge list") + edge(G, nums[2], nums[3], m) + + return G # ========================================================================== -# Edge list format +# Edge list format # ========================================================================== - + + def readEdgeList(lines): - """Read undirected graph in edge list format.""" - G = graph() - m = 0 - for line in filter(None,lines): - words = line.split() - if len(words) < 2 or len(words) > 3: - raise GraphFormatError('Wrong number of words in edge list: "%s"' % line) - if len(words) == 3 and words[1] != '-': - raise GraphFormatError('Unrecognized edge type "%s" in edge list' % words[1]) - u, v = words[0], words[-1] - if u not in G: - vertex(G, u) - if v not in G: - vertex(G, v) - m = m + 1 - edge(G, u, v, m) - - return G + """Read undirected graph in edge list format.""" + G = graph() + m = 0 + for line in filter(None, lines): + words = line.split() + if len(words) < 2 or len(words) > 3: + raise GraphFormatError('Wrong number of words in edge list: "%s"' % line) + if len(words) == 3 and words[1] != "-": + raise GraphFormatError('Unrecognized edge type "%s" in edge list' % words[1]) + u, v = words[0], words[-1] + if u not in G: + vertex(G, u) + if v not in G: + vertex(G, v) + m = m + 1 + edge(G, u, v, m) + + return G # ========================================================================== -# Node edge list format +# Node edge list format # ========================================================================== - + + def readNodeEdgeList(lines): - """Read undirected graph in node edge list format.""" - G = graph() - EdgeNames = {} - lines = iter(filter(None,lines)) - numEdges = [0] - - def addVertex(line): - vertex(G, line) - - def addEdge(line, id): - u = line - v = next(lines) - if v.startswith('//'): - raise GraphFormatError('Missing edge endpoint in node edge list') - edge(G, u, v, id) - numEdges[0] += 1 - - def anonEdge(line): - return addEdge(line, numEdges[0]+1) - - def namedEdge(line): - id = line - if id in EdgeNames: - raise GraphFormatError('Edge name "%s" used twice in node edge list' % id) - addEdge(next(lines), id) - EdgeNames[id] = id - - def noActionYet(line): - raise GraphFormatError('No section yet in node edge list') - - actions = { - 'nodes': addVertex, - 'edges': anonEdge, - 'named edges': namedEdge, - } - action = noActionYet - - for line in lines: - if line.startswith('// '): - try: - action = actions[line[3:]] - except KeyError: - raise GraphFormatError ('Unrecognized section "%s" in node edge list' % line[3:]) - else: - action(line) - - return G + """Read undirected graph in node edge list format.""" + G = graph() + EdgeNames = {} + lines = iter(filter(None, lines)) + numEdges = [0] + + def addVertex(line): + vertex(G, line) + + def addEdge(line, id): + u = line + v = next(lines) + if v.startswith("//"): + raise GraphFormatError("Missing edge endpoint in node edge list") + edge(G, u, v, id) + numEdges[0] += 1 + + def anonEdge(line): + return addEdge(line, numEdges[0] + 1) + + def namedEdge(line): + id = line + if id in EdgeNames: + raise GraphFormatError('Edge name "%s" used twice in node edge list' % id) + addEdge(next(lines), id) + EdgeNames[id] = id + + def noActionYet(line): + raise GraphFormatError("No section yet in node edge list") + + actions = { + "nodes": addVertex, + "edges": anonEdge, + "named edges": namedEdge, + } + action = noActionYet + + for line in lines: + if line.startswith("// "): + try: + action = actions[line[3:]] + except KeyError: + raise GraphFormatError('Unrecognized section "%s" in node edge list' % line[3:]) + else: + action(line) + + return G # ========================================================================== -# GraphML format +# GraphML format # ========================================================================== - + + def readGraphML(lines): - """Read undirected graph in GraphML format.""" - context = [] - G = graph() - edgecounter = [0] - defaultDirectedness = ['true'] - - def start_element(name,attrs): - context.append(name) - if len(context) == 1: - if name != 'graphml': - raise GraphFormatError('Unrecognized outer tag "%s" in GraphML' % name) - elif len(context) == 2 and name == 'graph': - if 'edgedefault' not in attrs: - raise GraphFormatError('Required attribute edgedefault missing in GraphML') - if attrs['edgedefault'] == 'undirected': - defaultDirectedness[0] = 'false' - elif len(context) == 3 and context[1] == 'graph' and name == 'node': - if 'id' not in attrs: - raise GraphFormatError('Anonymous node in GraphML') - vertex(G, attrs['id']) - elif len(context) == 3 and context[1] == 'graph' and name == 'edge': - if 'source' not in attrs: - raise GraphFormatError('Edge without source in GraphML') - if 'target' not in attrs: - raise GraphFormatError('Edge without target in GraphML') - if attrs.get('directed', defaultDirectedness[0]) != 'false': - raise GraphFormatError('Directed edge in GraphML') - edge(G, attrs['source'], attrs['target'], edgecounter[0]) - edgecounter[0] += 1 - - def end_element(name): - context.pop() - - import xml.parsers.expat - p = xml.parsers.expat.ParserCreate() - p.StartElementHandler = start_element - p.EndElementHandler = end_element - for line in lines: - p.Parse(line) - p.Parse("", 1) - return G + """Read undirected graph in GraphML format.""" + context = [] + G = graph() + edgecounter = [0] + defaultDirectedness = ["true"] + + def start_element(name, attrs): + context.append(name) + if len(context) == 1: + if name != "graphml": + raise GraphFormatError('Unrecognized outer tag "%s" in GraphML' % name) + elif len(context) == 2 and name == "graph": + if "edgedefault" not in attrs: + raise GraphFormatError("Required attribute edgedefault missing in GraphML") + if attrs["edgedefault"] == "undirected": + defaultDirectedness[0] = "false" + elif len(context) == 3 and context[1] == "graph" and name == "node": + if "id" not in attrs: + raise GraphFormatError("Anonymous node in GraphML") + vertex(G, attrs["id"]) + elif len(context) == 3 and context[1] == "graph" and name == "edge": + if "source" not in attrs: + raise GraphFormatError("Edge without source in GraphML") + if "target" not in attrs: + raise GraphFormatError("Edge without target in GraphML") + if attrs.get("directed", defaultDirectedness[0]) != "false": + raise GraphFormatError("Directed edge in GraphML") + edge(G, attrs["source"], attrs["target"], edgecounter[0]) + edgecounter[0] += 1 + + def end_element(name): + context.pop() + + import xml.parsers.expat + + p = xml.parsers.expat.ParserCreate() + p.StartElementHandler = start_element + p.EndElementHandler = end_element + for line in lines: + p.Parse(line) + p.Parse("", 1) + return G # ========================================================================== -# Graph6 and Sparse6 format +# Graph6 and Sparse6 format # ========================================================================== - + + def graph6data(str): - """Convert graph6 character sequence to 6-bit integers.""" - v = [ord(c)-63 for c in str] - if min(v) < 0 or max(v) > 63: - return None - return v - + """Convert graph6 character sequence to 6-bit integers.""" + v = [ord(c) - 63 for c in str] + if min(v) < 0 or max(v) > 63: + return None + return v + + def graph6n(data): - """Read initial one or four-unit value from graph6 sequence. Return value, rest of seq.""" - if data[0] <= 62: - return data[0], data[1:] - return (data[1]<<12) + (data[2]<<6) + data[3], data[4:] + """Read initial one or four-unit value from graph6 sequence. Return value, rest of seq.""" + if data[0] <= 62: + return data[0], data[1:] + return (data[1] << 12) + (data[2] << 6) + data[3], data[4:] + def readGraph6(str): - """Read undirected graph in graph6 format.""" - if str.startswith('>>graph6<<'): - str = str[10:] - data = graph6data(str) - n, data = graph6n(data) - nd = (n*(n-1)//2 + 5) // 6 - if len(data) != nd: - raise GraphFormatError('Expected %d bits but got %d in graph6' % (n*(n-1)//2, len(data)*6)) - - def bits(): - """Return sequence of individual bits from 6-bit-per-value list of data values.""" - for d in data: - for i in [5,4,3,2,1,0]: - yield (d>>i)&1 - - nEdges = 0 - G = graph() - for i in range(n): - vertex(G, i) - - for (i,j),b in zip([(i,j) for j in range(1,n) for i in range(j)], bits()): - if b: - edge(G, i, j, nEdges) - nEdges += 1 - - return G - + """Read undirected graph in graph6 format.""" + if str.startswith(">>graph6<<"): + str = str[10:] + data = graph6data(str) + n, data = graph6n(data) + nd = (n * (n - 1) // 2 + 5) // 6 + if len(data) != nd: + raise GraphFormatError("Expected %d bits but got %d in graph6" % (n * (n - 1) // 2, len(data) * 6)) + + def bits(): + """Return sequence of individual bits from 6-bit-per-value list of data values.""" + for d in data: + for i in [5, 4, 3, 2, 1, 0]: + yield (d >> i) & 1 + + nEdges = 0 + G = graph() + for i in range(n): + vertex(G, i) + + for (i, j), b in zip([(i, j) for j in range(1, n) for i in range(j)], bits()): + if b: + edge(G, i, j, nEdges) + nEdges += 1 + + return G + + def readSparse6(str): - """Read undirected graph in sparse6 format.""" - if str.startswith('>>sparse6<<'): - str = str[10:] - if not str.startswith(':'): - raise GraphFormatError('Expected colon in sparse6') - n, data = graph6n(graph6data(str[1:])) - k = 1 - while 1<>dLen) & 1 # grab top remaining bit - - x = d & ((1<> (xLen - k)) # shift back the extra bits - dLen = xLen - k - yield b,x - - v = 0 - G = graph() - nEdges = 0 - for i in range(n): - vertex(G, i) - - for b,x in parseData(): - if b: v += 1 - if x >= n: break # padding with ones can cause overlarge number here - elif x > v: v = x - else: - edge(G, x, v, nEdges) - nEdges += 1 - - return G + """Read undirected graph in sparse6 format.""" + if str.startswith(">>sparse6<<"): + str = str[10:] + if not str.startswith(":"): + raise GraphFormatError("Expected colon in sparse6") + n, data = graph6n(graph6data(str[1:])) + k = 1 + while 1 << k < n: + k += 1 + + def parseData(): + """Return stream of pairs b[i], x[i] for sparse6 format.""" + chunks = iter(data) + d = None # partial data word + dLen = 0 # how many unparsed bits are left in d + + while 1: + if dLen < 1: + d = next(chunks) + dLen = 6 + dLen -= 1 + b = (d >> dLen) & 1 # grab top remaining bit + + x = d & ((1 << dLen) - 1) # partially built up value of x + xLen = dLen # how many bits included so far in x + while xLen < k: # now grab full chunks until we have enough + d = next(chunks) + dLen = 6 + x = (x << 6) + d + xLen += 6 + x = x >> (xLen - k) # shift back the extra bits + dLen = xLen - k + yield b, x + + v = 0 + G = graph() + nEdges = 0 + for i in range(n): + vertex(G, i) + + for b, x in parseData(): + if b: + v += 1 + if x >= n: + break # padding with ones can cause overlarge number here + elif x > v: + v = x + else: + edge(G, x, v, nEdges) + nEdges += 1 + + return G # ========================================================================== -# LEDA.GRAPH format +# LEDA.GRAPH format # ========================================================================== - + + def ledaLines(lines): - """Filter sequence of lines to keep only the relevant ones for LEDA.GRAPH""" - def relevant(line): - return line and not line.startswith('#') - return filter(relevant, lines) - + """Filter sequence of lines to keep only the relevant ones for LEDA.GRAPH""" + + def relevant(line): + return line and not line.startswith("#") + + return filter(relevant, lines) + + def readLeda(lines): - """Parse filtered LEDA.GRAPH format file.""" - lines = iter(lines) - for i in range(3): - next(lines) # skip header lines - G = graph() - - n = graphNum(next(lines)) # number of vertices - for i in range(n): - vertex(G, i+1) - next(lines) # skip LEDA data - - m = graphNum(next(lines)) # number of edges - for i in range(m): - words = next(lines).split() - if len(words) < 4: - raise GraphFormatError('Too few fields in LEDA.GRAPH edge %d' % (i+1)) - source = graphNum(words[0]) - target = graphNum(words[1]) - reversal = graphNum(words[2]) - if not reversal: - raise GraphFormatError('Edge %d is directed in LEDA.GRAPH' % (i+1)) - if source < target: - edge(G, source, target, i+1) - - return G + """Parse filtered LEDA.GRAPH format file.""" + lines = iter(lines) + for i in range(3): + next(lines) # skip header lines + G = graph() + + n = graphNum(next(lines)) # number of vertices + for i in range(n): + vertex(G, i + 1) + next(lines) # skip LEDA data + + m = graphNum(next(lines)) # number of edges + for i in range(m): + words = next(lines).split() + if len(words) < 4: + raise GraphFormatError("Too few fields in LEDA.GRAPH edge %d" % (i + 1)) + source = graphNum(words[0]) + target = graphNum(words[1]) + reversal = graphNum(words[2]) + if not reversal: + raise GraphFormatError("Edge %d is directed in LEDA.GRAPH" % (i + 1)) + if source < target: + edge(G, source, target, i + 1) + + return G # ========================================================================== -# Main entry +# Main entry # ========================================================================== + def readUndirectedGraph(arg): - """Parse graph and return in modified GvR format. - Argument may be a file object, a single string, or a sequence of lines. - """ - if isinstance(arg,file): - lines = [L.strip() for L in arg] - elif isinstance(arg,str) or isinstance(arg,unicode): - lines = [arg] - else: - lines = list(arg) - - # Test out different possible formats, - # ordered from more distinctive to more ambiguous. - - # Graph6 and Sparse6 - if len(lines) == 1: - line = lines[0] - if line.startswith('>>graph6<<') or graph6data(line): - return readGraph6(line) - elif line.startswith('>>sparse6<<') or \ - (line.startswith(':') and graph6data(line[1:])): - return readSparse6(line) - - # LEDA.GRAPH - leda = ledaLines(lines) - if leda and leda[0] == 'LEDA.GRAPH': - return readLeda(leda) - - # GraphML - if lines[0].startswith("<"): - return readGraphML(lines) - - # Node edge list - if lines[0].startswith("//"): - return readNodeEdgeList(lines) - - # MALF - if '#' in lines: - return readMALF(lines) - - # Edge list - return readEdgeList(lines) + """Parse graph and return in modified GvR format. + Argument may be a file object, a single string, or a sequence of lines. + """ + if isinstance(arg, file): + lines = [L.strip() for L in arg] + elif isinstance(arg, str) or isinstance(arg, unicode): + lines = [arg] + else: + lines = list(arg) + + # Test out different possible formats, + # ordered from more distinctive to more ambiguous. + + # Graph6 and Sparse6 + if len(lines) == 1: + line = lines[0] + if line.startswith(">>graph6<<") or graph6data(line): + return readGraph6(line) + elif line.startswith(">>sparse6<<") or (line.startswith(":") and graph6data(line[1:])): + return readSparse6(line) + + # LEDA.GRAPH + leda = ledaLines(lines) + if leda and leda[0] == "LEDA.GRAPH": + return readLeda(leda) + + # GraphML + if lines[0].startswith("<"): + return readGraphML(lines) + + # Node edge list + if lines[0].startswith("//"): + return readNodeEdgeList(lines) + + # MALF + if "#" in lines: + return readMALF(lines) + + # Edge list + return readEdgeList(lines) diff --git a/pads/Repetitivity.py b/pads/Repetitivity.py index 71b1e5b..05c3373 100644 --- a/pads/Repetitivity.py +++ b/pads/Repetitivity.py @@ -10,6 +10,7 @@ from .StrongConnectivity import StronglyConnectedComponents from . import DFS + class NonrepetitiveGraph: """ Data structure for finding nonrepetitive paths in graphs. @@ -17,7 +18,7 @@ class NonrepetitiveGraph: of the edge from v to w, then NonrepetitiveGraph(G) allows us to find paths in G, with a choice of label per edge of the path, such that no two consecutive labels are equal. - + If NR is a NonrepetitiveGraph instance, then - iter(NR) lists the vertices in NR - NR[v] lists the labels incident to vertex v @@ -29,15 +30,15 @@ class NonrepetitiveGraph: - shortest(v,label,w,label) finds a shortest path between the given vertex,label pairs. """ - - def __init__(self,G): + + def __init__(self, G): """ Initialize from a given graph instance. The graph G should have G[v][w] equal to a collection (list, set, etc) of the labels on edges from v to w; this allows us to represent multigraphs with differing labels on their multiedges. - + Data stored in fields of this instance: - self.nrg is a transformed unlabeled graph in which paths represent nonrepetitive paths in G @@ -55,17 +56,17 @@ def __init__(self,G): self.labels[w].update(G[v][w]) self.nrg = {} for v in self: - self._gadget(v,self.labels[v]) + self._gadget(v, self.labels[v]) for v in G: for w in G[v]: for L in G[v][w]: - self.nrg[v,L,False].add((w,L,True)) - - def __getitem__(self,v): + self.nrg[v, L, False].add((w, L, True)) + + def __getitem__(self, v): """x.__getitem__(y) <==> x[y]""" return self.labels[v] - def __contains__(self,v): + def __contains__(self, v): """x.__contains__(y) <==> y in x""" return v in self.labels @@ -73,12 +74,12 @@ def __iter__(self): """x.__iter__() <==> iter(x)""" return iter(self.labels) - def _gadget(self,v,labels): + def _gadget(self, v, labels): """Create nonrepetitivity gadget for vertex v and given label set.""" labels = list(labels) for L in labels: - self.nrg.setdefault((v,L,True),set()) - self.nrg.setdefault((v,L,False),set()) + self.nrg.setdefault((v, L, True), set()) + self.nrg.setdefault((v, L, False), set()) if len(labels) == 1: return groups = [] @@ -88,19 +89,19 @@ def _gadget(self,v,labels): grouplen = 3 else: grouplen = 2 - group = labels[n-grouplen:n] + group = labels[n - grouplen : n] for L1 in group: for L2 in group: if L1 != L2: - self.nrg[v,L1,True].add((v,L2,False)) + self.nrg[v, L1, True].add((v, L2, False)) if len(labels) > 3: groups.append(object()) - self.nrg[v,groups[-1],False] = {(v,L,False) for L in group} + self.nrg[v, groups[-1], False] = {(v, L, False) for L in group} for L in group: - self.nrg[v,L,True].add((v,groups[-1],True)) + self.nrg[v, L, True].add((v, groups[-1], True)) n -= grouplen if len(groups) > 1: - self._gadget(v,groups) + self._gadget(v, groups) def cyclic(self): """Yield triples (v,w,label) belonging to all nonrepetitive cycles.""" @@ -110,19 +111,19 @@ def cyclic(self): components[v] = C for v in self: for L in self[v]: - for w,LL,bit in self.nrg[v,L,False]: - if components[v,L,False] == components[w,L,True]: - yield v,w,L + for w, LL, bit in self.nrg[v, L, False]: + if components[v, L, False] == components[w, L, True]: + yield v, w, L - def reachable(self,v,L): + def reachable(self, v, L): """Yield pairs (w,label) on nonrepetitive paths from v,L.""" if v not in self or L not in self[v]: return - for w,LL,bit in DFS.preorder(self.nrg,(v,L,False)): + for w, LL, bit in DFS.preorder(self.nrg, (v, L, False)): if bit and LL in self[w]: - yield w,LL + yield w, LL - def _flattenpath(self,path): + def _flattenpath(self, path): """Helper routine for shortest: convert output from internal format.""" output = [] while path: @@ -131,14 +132,14 @@ def _flattenpath(self,path): output.reverse() return output - def shortest(self,v,L,w,LL): + def shortest(self, v, L, w, LL): """ Breadth first search for shortest path from (v,L) to (w,LL). The path is returned as a list of vertices. """ - start = (v,L,False) + start = (v, L, False) visited = {start} - thislevel = [(start,(v,None))] + thislevel = [(start, (v, None))] nextlevel = [] levelindex = 0 while levelindex < len(thislevel) or nextlevel: @@ -146,16 +147,16 @@ def shortest(self,v,L,w,LL): thislevel = nextlevel nextlevel = [] levelindex = 0 - current,path = thislevel[levelindex] + current, path = thislevel[levelindex] levelindex += 1 for nrgnode in self.nrg[current]: if nrgnode not in visited: - if nrgnode[2] and not current[2]: # non-gadget edge? - newpath = (nrgnode[0],path) - if nrgnode[:2] == (w,LL): + if nrgnode[2] and not current[2]: # non-gadget edge? + newpath = (nrgnode[0], path) + if nrgnode[:2] == (w, LL): return self._flattenpath(newpath) - nextlevel.append((nrgnode,newpath)) + nextlevel.append((nrgnode, newpath)) else: - thislevel.append((nrgnode,path)) + thislevel.append((nrgnode, path)) visited.add(nrgnode) - raise ValueError("No such path exists") \ No newline at end of file + raise ValueError("No such path exists") diff --git a/pads/SMAWK.py b/pads/SMAWK.py index 0a94e9e..b62741c 100644 --- a/pads/SMAWK.py +++ b/pads/SMAWK.py @@ -14,12 +14,13 @@ D. Eppstein, March 2002, significantly revised August 2005 """ -def ConcaveMinima(RowIndices,ColIndices,Matrix): + +def ConcaveMinima(RowIndices, ColIndices, Matrix): """ Search for the minimum value in each column of a matrix. The return value is a dictionary mapping ColIndices to pairs (value,rowindex). We break ties in favor of earlier rows. - + The matrix is defined implicitly as a function, passed as the third argument to this routine, where Matrix(i,j) gives the matrix value at row index i and column index j. @@ -28,66 +29,64 @@ def ConcaveMinima(RowIndices,ColIndices,Matrix): for every i= 1 and \ - Matrix(stack[-1], ColIndices[len(stack)-1]) \ - > Matrix(r, ColIndices[len(stack)-1]): + while len(stack) >= 1 and Matrix(stack[-1], ColIndices[len(stack) - 1]) > Matrix(r, ColIndices[len(stack) - 1]): stack.pop() if len(stack) != len(ColIndices): stack.append(r) RowIndices = stack - + # Recursive call to search for every odd column - minima = ConcaveMinima(RowIndices, - [ColIndices[i] for i in range(1,len(ColIndices),2)], - Matrix) + minima = ConcaveMinima(RowIndices, [ColIndices[i] for i in range(1, len(ColIndices), 2)], Matrix) # Go back and fill in the even rows r = 0 - for c in range(0,len(ColIndices),2): + for c in range(0, len(ColIndices), 2): col = ColIndices[c] row = RowIndices[r] if c == len(ColIndices) - 1: lastrow = RowIndices[-1] else: - lastrow = minima[ColIndices[c+1]][1] - pair = (Matrix(row,col),row) + lastrow = minima[ColIndices[c + 1]][1] + pair = (Matrix(row, col), row) while row != lastrow: r += 1 row = RowIndices[r] - pair = min(pair,(Matrix(row,col),row)) + pair = min(pair, (Matrix(row, col), row)) minima[col] = pair return minima + class OnlineConcaveMinima: """ Online concave minimization algorithm of Galil and Park. - + OnlineConcaveMinima(Matrix,initial) creates a sequence of pairs (self.value(j),self.index(j)), where self.value(0) = initial, self.value(j) = min { Matrix(i,j) | i < j } for j > 0, and where self.index(j) is the value of j that provides the minimum. Matrix(i,j) must be concave, in the same sense as for ConcaveMinima. - + We never call Matrix(i,j) until value(i) has already been computed, so that the Matrix function may examine previously computed values. Calling value(i) for an i that has not yet been computed forces the sequence to be continued until the desired index is reached. Calling iter(self) produces a sequence of (value,index) pairs. - + Matrix(i,j) should always return a value, rather than raising an exception, even for j larger than the range we expect to compute. If j is out of range, a suitable value to return that will not @@ -95,14 +94,14 @@ class OnlineConcaveMinima: to return a flag value such as None for large j, because the ties formed by the equalities among such flags may violate concavity. """ - - def __init__(self,Matrix,initial): + + def __init__(self, Matrix, initial): """Initialize a OnlineConcaveMinima object.""" # State used by self.value(), self.index(), and iter(self) - self._values = [initial] # tentative solution values... - self._indices = [None] # ...and their indices - self._finished = 0 # index of last non-tentative value + self._values = [initial] # tentative solution values... + self._indices = [None] # ...and their indices + self._finished = 0 # index of last non-tentative value # State used by the internal algorithm # @@ -124,16 +123,16 @@ def __iter__(self): """Loop through (value,index) pairs.""" i = 0 while True: - yield self.value(i),self.index(i) + yield self.value(i), self.index(i) i += 1 - def value(self,j): + def value(self, j): """Return min { Matrix(i,j) | i < j }.""" while self._finished < j: self._advance() return self._values[j] - def index(self,j): + def index(self, j): """Return argmin { Matrix(i,j) | i < j }.""" while self._finished < j: self._advance() @@ -146,44 +145,44 @@ def _advance(self): # to the largest square submatrix that fits under the base. i = self._finished + 1 if i > self._tentative: - rows = range(self._base,self._finished+1) - self._tentative = self._finished+len(rows) - cols = range(self._finished+1,self._tentative+1) - minima = ConcaveMinima(rows,cols,self._matrix) + rows = range(self._base, self._finished + 1) + self._tentative = self._finished + len(rows) + cols = range(self._finished + 1, self._tentative + 1) + minima = ConcaveMinima(rows, cols, self._matrix) for col in cols: if col >= len(self._values): self._values.append(minima[col][0]) self._indices.append(minima[col][1]) elif minima[col][0] < self._values[col]: - self._values[col],self._indices[col] = minima[col] + self._values[col], self._indices[col] = minima[col] self._finished = i return - + # Second case: the new column minimum is on the diagonal. # All subsequent ones will be at least as low, # so we can clear out all our work from higher rows. # As in the fourth case, the loss of tentative is # amortized against the increase in base. - diag = self._matrix(i-1,i) + diag = self._matrix(i - 1, i) if diag < self._values[i]: self._values[i] = diag - self._indices[i] = self._base = i-1 + self._indices[i] = self._base = i - 1 self._tentative = self._finished = i return - + # Third case: row i-1 does not supply a column minimum in # any column up to tentative. We simply advance finished # while maintaining the invariant. - if self._matrix(i-1,self._tentative) >= self._values[self._tentative]: + if self._matrix(i - 1, self._tentative) >= self._values[self._tentative]: self._finished = i return - + # Fourth and final case: a new column minimum at self._tentative. # This allows us to make progress by incorporating rows # prior to finished into the base. The base invariant holds # because these rows cannot supply any later column minima. # The work done when we last advanced tentative (and undone by # this step) can be amortized against the increase in base. - self._base = i-1 + self._base = i - 1 self._tentative = self._finished = i return diff --git a/pads/SVG.py b/pads/SVG.py index 4cc6ae3..270e132 100644 --- a/pads/SVG.py +++ b/pads/SVG.py @@ -5,13 +5,14 @@ D. Eppstein, November 2011. """ + def _coord(x): """String representation for coordinate""" return ("%.4f" % x).rstrip("0").rstrip(".") + class SVG: - def __init__(self, bbox, stream, - standalone=True, prefix=None, indentation=0): + def __init__(self, bbox, stream, standalone=True, prefix=None, indentation=0): """Create a new SVG object, to be written to the given stream. If standalone is True or omitted, the SVG object becomes a whole XML file; otherwise, it becomes an XML object within a larger XML @@ -19,7 +20,7 @@ def __init__(self, bbox, stream, from other tags; a reasonable choice for the prefix value would be "s" or "svg". If the indentation is nonzero, it gives a number of spaces by which every line of the file is indented. - + The bbox argument should be a complex number, the farthest visible point from the origin in the positive quadrant. The bounding box will become the rectangle between the origin and that point. @@ -31,16 +32,22 @@ def __init__(self, bbox, stream, else: self.prefix = "" if standalone: - self.stream.write(''' + self.stream.write( + """ -''') +""" + ) self.indentation = indentation self.nesting = 0 br = _coord(bbox.real) bi = _coord(bbox.imag) - self.element('''svg width="%s" height="%s" viewBox="0 0 %s %s" - xmlns="http://www.w3.org/2000/svg" version="1.1"''' % (br,bi,br,bi),+1) + self.element( + '''svg width="%s" height="%s" viewBox="0 0 %s %s" + xmlns="http://www.w3.org/2000/svg" version="1.1"''' + % (br, bi, br, bi), + +1, + ) def close(self): """Output the end of an SVG file.""" @@ -63,7 +70,7 @@ def element(self, e, delta=0, unspaced=False, style={}, **morestyle): if delta < 0: self.nesting += delta if delta >= 0 or not unspaced: - output = [" " * (self.indentation + 2*self.nesting), "<"] + output = [" " * (self.indentation + 2 * self.nesting), "<"] else: output = ["<"] if delta < 0: @@ -89,7 +96,7 @@ def element(self, e, delta=0, unspaced=False, style={}, **morestyle): output.append("\n") self.stream.write("".join(output)) - def group(self,style={},**morestyle): + def group(self, style={}, **morestyle): """Start a group of objects, all with the same style""" self.element("g", +1, style=style, **morestyle) @@ -99,37 +106,41 @@ def ungroup(self): def circle(self, center, radius, style={}, **morestyle): """Circle with given center and radius""" - self.element('circle cx="%s" cy="%s" r="%s"' % - (_coord(center.real), _coord(center.imag), _coord(radius)), - style=style, **morestyle) + self.element( + 'circle cx="%s" cy="%s" r="%s"' % (_coord(center.real), _coord(center.imag), _coord(radius)), + style=style, + **morestyle, + ) def rectangle(self, p, q, style={}, **morestyle): """Rectangle with corners at points p and q""" - x = min(p.real,q.real) - y = min(p.imag,q.imag) - width = abs((p-q).real) - height = abs((p-q).imag) - self.element('rect x="%s" y="%s" width="%s" height="%s"' % - (_coord(x), _coord(x), _coord(width), _coord(height)), - style=style, **morestyle) + x = min(p.real, q.real) + y = min(p.imag, q.imag) + width = abs((p - q).real) + height = abs((p - q).imag) + self.element( + 'rect x="%s" y="%s" width="%s" height="%s"' % (_coord(x), _coord(x), _coord(width), _coord(height)), + style=style, + **morestyle, + ) def polygon(self, points, style={}, **morestyle): """Polygon with corners at the given set of points""" - pointlist = " ".join(_coord(p.real)+","+_coord(p.imag) for p in points) - self.element('polygon points="%s"' % pointlist, - style=style, **morestyle) + pointlist = " ".join(_coord(p.real) + "," + _coord(p.imag) for p in points) + self.element('polygon points="%s"' % pointlist, style=style, **morestyle) def polyline(self, points, style={}, **morestyle): """Polyline with corners at the given set of points""" - pointlist = " ".join(_coord(p.real)+","+_coord(p.imag) for p in points) - self.element('polyline points="%s"' % pointlist, - style=style, **morestyle) + pointlist = " ".join(_coord(p.real) + "," + _coord(p.imag) for p in points) + self.element('polyline points="%s"' % pointlist, style=style, **morestyle) def segment(self, p, q, style={}, **morestyle): """Line segment from p to q""" - self.element('line x1="%s" y1="%s" x2="%s" y2="%s"' % - (_coord(p.real), _coord(p.imag), - _coord(q.real), _coord(q.imag)), style=style, **morestyle) + self.element( + 'line x1="%s" y1="%s" x2="%s" y2="%s"' % (_coord(p.real), _coord(p.imag), _coord(q.real), _coord(q.imag)), + style=style, + **morestyle, + ) def arc(self, p, q, r, large=False, style={}, **morestyle): """Circular arc from p to q with radius r. @@ -141,18 +152,26 @@ def arc(self, p, q, r, large=False, style={}, **morestyle): else: large = "0" r = _coord(abs(r)) - self.element('path d="M %s,%s A %s,%s 0 %s 0 %s,%s"' % - (_coord(p.real),_coord(p.imag),r,r,large, - _coord(q.real),_coord(q.imag)), style=style, **morestyle) + self.element( + 'path d="M %s,%s A %s,%s 0 %s 0 %s,%s"' + % (_coord(p.real), _coord(p.imag), r, r, large, _coord(q.real), _coord(q.imag)), + style=style, + **morestyle, + ) def text(self, label, location, style={}, **morestyle): """Text label at the given location. Caller is responsible for making the label xml-safe.""" - self.element('text x="%s" y="%s"' % - (_coord(location.real),_coord(location.imag)), - delta=1, unspaced=True, style=style, **morestyle) + self.element( + 'text x="%s" y="%s"' % (_coord(location.real), _coord(location.imag)), + delta=1, + unspaced=True, + style=style, + **morestyle, + ) self.stream.write(label) - self.element('text', delta=-1, unspaced=True) + self.element("text", delta=-1, unspaced=True) + # A small color palette chosen to have high contrast # even when viewed by color-blind readers diff --git a/pads/Sequence.py b/pads/Sequence.py index a900e6d..bb0294f 100644 --- a/pads/Sequence.py +++ b/pads/Sequence.py @@ -9,7 +9,10 @@ import math import sys -class SequenceError(Exception): pass + +class SequenceError(Exception): + pass + class Sequence: """Maintain a sequence of items subject to insertions and removals. @@ -38,12 +41,12 @@ def __iter__(self): """ item = self._first while self._next: - yield self._items.get(item,item) + yield self._items.get(item, item) item = self._next[item] if item == self._first: return - def __getitem__(self,i): + def __getitem__(self, i): """Return the ith item in the sequence.""" item = self._first while i: @@ -51,7 +54,7 @@ def __getitem__(self,i): if item == self._first: raise IndexError("Index out of range") i -= 1 - return self._items.get(item,item) + return self._items.get(item, item) def __len__(self): """Number of items in the sequence.""" @@ -62,9 +65,9 @@ def __repr__(self): output = [] for x in self: output.append(repr(x)) - return 'Sequence([' + ','.join(output) + '])' + return "Sequence([" + ",".join(output) + "])" - def key(self,x): + def key(self, x): """Apply supplied key function.""" if not self._key: return x @@ -72,25 +75,25 @@ def key(self,x): self._items[key] = x return key - def _insafter(self,x,y): + def _insafter(self, x, y): """Unkeyed version of insertAfter.""" if y in self._next: - raise SequenceError("Item already in sequence: "+repr(y)) + raise SequenceError("Item already in sequence: " + repr(y)) self._next[y] = z = self._next[x] self._next[x] = self._prev[z] = y self._prev[y] = x - def append(self,x): + def append(self, x): """Add x to the end of the sequence.""" x = self.key(x) if not self._next: # add to empty sequence - self._next = {x:x} - self._prev = {x:x} + self._next = {x: x} + self._prev = {x: x} self._first = x else: - self._insafter(self._prev[self._first],x) + self._insafter(self._prev[self._first], x) - def remove(self,x): + def remove(self, x): """Remove x from the sequence.""" x = self.key(x) prev = self._prev[x] @@ -100,28 +103,28 @@ def remove(self,x): self._first = next del self._next[x], self._prev[x] - def insertAfter(self,x,y): + def insertAfter(self, x, y): """Add y after x in the sequence.""" y = self.key(y) x = self.key(x) - self._insafter(x,y) + self._insafter(x, y) - def insertBefore(self,x,y): + def insertBefore(self, x, y): """Add y before x in the sequence.""" y = self.key(y) x = self.key(x) - self._insafter(self._prev[x],y) + self._insafter(self._prev[x], y) if self._first == x: self._first = y - def predecessor(self,x): + def predecessor(self, x): """Find the previous element in the sequence.""" x = self.key(x) prev = self._prev[x] - return self._items.get(prev,prev) + return self._items.get(prev, prev) - def successor(self,x): + def successor(self, x): """Find the next element in the sequence.""" x = self.key(x) next = self._next[x] - return self._items.get(next,next) + return self._items.get(next, next) diff --git a/pads/SortedSet.py b/pads/SortedSet.py index efb1d4d..5ca874f 100644 --- a/pads/SortedSet.py +++ b/pads/SortedSet.py @@ -17,13 +17,14 @@ from itertools import chain from functools import cmp_to_key + class SortedSet: """Maintain a set of items in such a way that iter(set) returns the items in sorted order. We also allow a custom comparison routine, and augment the usual add() and remove() methods with an update() method that tells the set that a single item's position in the order might have changed.""" - - def __init__(self,iterable=[],comparison=None): + + def __init__(self, iterable=[], comparison=None): """Create a new sorted set with the given comparison function.""" self._comparison = comparison self._set = set(iterable) @@ -33,20 +34,20 @@ def __len__(self): """How many items do we have?""" return len(self._set) - def add(self,item): + def add(self, item): """Add the given item to a sorted set.""" self._set.add(item) if self._previous: self._additions.add(item) - def remove(self,item): + def remove(self, item): """Remove the given item from a sorted set.""" self._set.remove(item) if self._previous: self._removals.add(item) self._additions.discard(item) - def update(self,item): + def update(self, item): """Flag the given item as needing re-comparison with its neighbors in the order.""" if self._previous: self._removals.add(item) @@ -56,17 +57,16 @@ def __iter__(self): if not self._previous: sortarg = self._set else: - sortarg = chain(self._additions, - (x for x in self._previous - if x not in self._removals)) + sortarg = chain(self._additions, (x for x in self._previous if x not in self._removals)) if self._comparison: - self._previous = sorted(sortarg,key=cmp_to_key(self._comparison)) + self._previous = sorted(sortarg, key=cmp_to_key(self._comparison)) else: self._previous = sorted(sortarg) self._removals = set() self._additions = set() return iter(self._previous) + # ====================================================================== # Unit tests # ====================================================================== @@ -78,14 +78,14 @@ class SortedSetTest(unittest.TestCase): def testSortedSet(self): """Test whether SortedSet works correctly.""" S = SortedSet() - self.assertEqual(len(S),0) + self.assertEqual(len(S), 0) S.add(1) S.add(4) S.add(2) S.add(9) S.add(3) - self.assertEqual(list(S),[1,2,3,4,9]) - self.assertEqual(len(S),5) + self.assertEqual(list(S), [1, 2, 3, 4, 9]) + self.assertEqual(len(S), 5) S.remove(4) S.add(6) S.add(5) @@ -94,7 +94,7 @@ def testSortedSet(self): S.remove(1) S.remove(2) S.add(1) - self.assertEqual(list(S),[1,3,5,7,9]) - self.assertEqual(list(SortedSet([1,3,6,7])),[1,3,6,7]) + self.assertEqual(list(S), [1, 3, 5, 7, 9]) + self.assertEqual(list(SortedSet([1, 3, 6, 7])), [1, 3, 6, 7]) - unittest.main() + unittest.main() diff --git a/pads/StrongConnectivity.py b/pads/StrongConnectivity.py index 5a5aef1..7251d08 100644 --- a/pads/StrongConnectivity.py +++ b/pads/StrongConnectivity.py @@ -16,6 +16,7 @@ import unittest from . import DFS + class StronglyConnectedComponents(DFS.Searcher): """ Generate the strongly connected components of G. G should be @@ -26,7 +27,7 @@ class StronglyConnectedComponents(DFS.Searcher): a sequence of subgraphs of G. """ - def __init__(self,G): + def __init__(self, G): """Search for strongly connected components of graph G.""" # set up data structures for DFS @@ -39,7 +40,7 @@ def __init__(self,G): self._graph = G # perform the Depth First Search - DFS.Searcher.__init__(self,G) + DFS.Searcher.__init__(self, G) # clean up now-useless data structures del self._dfsnumber, self._activelen, self._active, self._low @@ -52,13 +53,13 @@ def __len__(self): """How many components are there?""" return len(self._components) - def _component(self,vertices): + def _component(self, vertices): """Make a new SCC.""" vertices = set(vertices) - induced = {v:{w for w in self._graph[v] if w in vertices} for v in vertices} + induced = {v: {w for w in self._graph[v] if w in vertices} for v in vertices} self._components.append(induced) - def preorder(self,parent,child): + def preorder(self, parent, child): """Handle first visit to vertex in DFS search for components.""" if parent == child: self._active = [] @@ -66,19 +67,20 @@ def preorder(self,parent,child): self._active.append(child) self._low[child] = self._dfsnumber[child] = len(self._dfsnumber) - def backedge(self,source,destination): + def backedge(self, source, destination): """Handle non-tree edge in DFS search for components.""" - self._low[source] = min(self._low[source],self._low[destination]) + self._low[source] = min(self._low[source], self._low[destination]) - def postorder(self,parent,child): + def postorder(self, parent, child): """Handle last visit to vertex in DFS search for components.""" if self._low[child] == self._dfsnumber[child]: - self._component(self._active[self._activelen[child]:]) + self._component(self._active[self._activelen[child] :]) for v in self._components[-1]: self._low[v] = self._biglow - del self._active[self._activelen[child]:] + del self._active[self._activelen[child] :] else: - self._low[parent] = min(self._low[parent],self._low[child]) + self._low[parent] = min(self._low[parent], self._low[child]) + def Condensation(G): """Return a DAG with vertices equal to sets of vertices in SCCs of G.""" @@ -94,14 +96,15 @@ def Condensation(G): if GtoC[v] != GtoC[w]: components[GtoC[v]].add(GtoC[w]) return components - + + # If run as "python StrongConnectivity.py", run tests on various small graphs # and check that the correct results are obtained. -class StrongConnectivityTest(unittest.TestCase): - G1 = { 0:[1], 1:[2,3], 2:[4,5], 3:[4,5], 4:[6], 5:[], 6:[] } - C1 = [[0],[1],[2],[3],[4],[5],[6]] +class StrongConnectivityTest(unittest.TestCase): + G1 = {0: [1], 1: [2, 3], 2: [4, 5], 3: [4, 5], 4: [6], 5: [], 6: []} + C1 = [[0], [1], [2], [3], [4], [5], [6]] # Work around http://bugs.python.org/issue11796 by using a loop # instead of a dict/set comprehension in a class variable initializer @@ -110,27 +113,27 @@ class StrongConnectivityTest(unittest.TestCase): Con1 = {} for v in G1: Con1[frozenset([v])] = {frozenset([w]) for w in G1[v]} - - G2 = { 0:[1], 1:[2,3,4], 2:[0,3], 3:[4], 4:[3] } - C2 = [[0,1,2],[3,4]] - f012 = frozenset([0,1,2]) - f34 = frozenset([3,4]) - Con2 = {f012:{f34}, f34:set()} - - knownpairs = [(G1,C1),(G2,C2)] + + G2 = {0: [1], 1: [2, 3, 4], 2: [0, 3], 3: [4], 4: [3]} + C2 = [[0, 1, 2], [3, 4]] + f012 = frozenset([0, 1, 2]) + f34 = frozenset([3, 4]) + Con2 = {f012: {f34}, f34: set()} + + knownpairs = [(G1, C1), (G2, C2)] def testStronglyConnectedComponents(self): """Check known graph/component pairs.""" - for (graph,expectedoutput) in self.knownpairs: + for graph, expectedoutput in self.knownpairs: output = [list(C) for C in StronglyConnectedComponents(graph)] for component in output: component.sort() output.sort() - self.assertEqual(output,expectedoutput) + self.assertEqual(output, expectedoutput) def testSubgraph(self): """Check that each SCC is an induced subgraph.""" - for (graph,expectedoutput) in self.knownpairs: + for graph, expectedoutput in self.knownpairs: components = StronglyConnectedComponents(graph) for C in components: for v in C: @@ -139,9 +142,9 @@ def testSubgraph(self): def testCondensation(self): """Check that the condensations are what we expect.""" - self.assertEqual(Condensation(self.G1),self.Con1) - self.assertEqual(Condensation(self.G2),self.Con2) + self.assertEqual(Condensation(self.G1), self.Con1) + self.assertEqual(Condensation(self.G2), self.Con2) -if __name__ == "__main__": - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/pads/Subsets.py b/pads/Subsets.py index 9bf3518..92da5d9 100644 --- a/pads/Subsets.py +++ b/pads/Subsets.py @@ -9,6 +9,7 @@ D. Eppstein, September 2017.""" + def subsets(S): """All subsets of sequence S.""" S = iter(S) diff --git a/pads/Sudoku.py b/pads/Sudoku.py index 8f4c3ce..3114f34 100644 --- a/pads/Sudoku.py +++ b/pads/Sudoku.py @@ -27,62 +27,67 @@ from .TwoSatisfiability import Forced from .SVG import SVG -class BadSudoku(Exception): pass + +class BadSudoku(Exception): + pass # raised when we discover that a puzzle has no solutions + # ====================================================================== # Bitmaps and patterns # ====================================================================== -digits = range(1,10) +digits = range(1, 10) + class group: def __init__(self, i, j, x, y, name): mask = 0 - h,k = [q for q in range(4) if q != i and q != j] + h, k = [q for q in range(4) if q != i and q != j] for w in range(3): for z in range(3): - mask |= 1 << (x*3**i + y*3**j + w*3**h + z*3**k) + mask |= 1 << (x * 3**i + y * 3**j + w * 3**h + z * 3**k) self.mask = mask - self.pos = [None]*9 - self.name = "%s %d" % (name,x+3*y+1) + self.pos = [None] * 9 + self.name = "%s %d" % (name, x + 3 * y + 1) -cols = [group(0,1,x,y,"column") for x in range(3) for y in range(3)] -rows = [group(2,3,x,y,"row") for x in range(3) for y in range(3)] -sqrs = [group(1,3,x,y,"square") for x in range(3) for y in range(3)] -groups = sqrs+rows+cols -neighbors = [0]*81 +cols = [group(0, 1, x, y, "column") for x in range(3) for y in range(3)] +rows = [group(2, 3, x, y, "row") for x in range(3) for y in range(3)] +sqrs = [group(1, 3, x, y, "square") for x in range(3) for y in range(3)] +groups = sqrs + rows + cols + +neighbors = [0] * 81 for i in range(81): - b = 1< min(census): - continue # block already filled, can't use this row now - FullPlacementGraph[node][node|b] = 1<<(9*i+sum(census)) # found edge - census[i//3] += 1 - searchPlacements(node|b,census) # recurse - census[i//3] -= 1 + continue # row already used, can't use it again + if census[i // 3] > min(census): + continue # block already filled, can't use this row now + FullPlacementGraph[node][node | b] = 1 << (9 * i + sum(census)) # found edge + census[i // 3] += 1 + searchPlacements(node | b, census) # recurse + census[i // 3] -= 1 -searchPlacements(0,[0,0,0]) +searchPlacements(0, [0, 0, 0]) # ====================================================================== # Human-readable names for puzzle cells # ====================================================================== -cellnames = [None]*81 +cellnames = [None] * 81 for row in range(9): for col in range(9): - cellnames[row*9+col] = ''.join(['R',str(row+1),'C',str(col+1)]) + cellnames[row * 9 + col] = "".join(["R", str(row + 1), "C", str(col + 1)]) -def andlist(list,conjunction="and"): + +def andlist(list, conjunction="and"): """Turn list of strings into English text.""" if len(list) == 0: return "(empty list!)" if len(list) == 1: return list[0] elif len(list) == 2: - return (' '+conjunction+' ').join(list) + return (" " + conjunction + " ").join(list) else: - return ', '.join(list[:-1]+[conjunction+' '+list[-1]]) + return ", ".join(list[:-1] + [conjunction + " " + list[-1]]) + -def namecells(mask,conjunction="and"): +def namecells(mask, conjunction="and"): """English string describing a sequence of cells.""" names = [] while mask: - bit = mask &~ (mask - 1) + bit = mask & ~(mask - 1) names.append(cellnames[unmask[bit]]) - mask &=~ bit - return andlist(names,conjunction) + mask &= ~bit + return andlist(names, conjunction) + def pathname(cells): - return '-'.join([cellnames[c] for c in cells]) + return "-".join([cellnames[c] for c in cells]) + -def plural(howmany,objectname): +def plural(howmany, objectname): if howmany == 1: return objectname else: - return "%d %ss" % (howmany,objectname) + return "%d %ss" % (howmany, objectname) + # ====================================================================== # State for puzzle solver # ====================================================================== + class Sudoku: """ Data structure for storing and manipulating Sudoku puzzles. @@ -169,57 +181,57 @@ class Sudoku: separately from this class. """ - def __init__(self,initial_placements = None): + def __init__(self, initial_placements=None): """ Initialize a new Sudoku grid. - + If an argument is given, it should either be a sequence of 81 digits 0-9 (0 meaning a not-yet-filled cell), or a sequence of (digit,cell) pairs. - + The main state we use for the solver is an array contents[] of 81 cells containing digits 0-9 (0 for an unfilled cell) and an array locations[] indexed by the digits 1-9, containing bitmasks of the cells still available to each digit. - + We also store additional fields: - progress is a boolean, set whenever one of our methods changes the state of the puzzle, and used by step() to tell whether one of its rules fired. - + - rules_used is a set of the rule names that have made progress. - + - pairs is a dictionary mapping bitmasks of pairs of cells to lists of digits that must be located in that pair, as set up by the pair rule and used by other later rules. - + - bilocation is a NonrepetitiveGraph representing paths and cycles among bilocated digits, as constructed by the bilocal rule and used by the repeat and conflict rules. - + - bivalues is a NonrepetitiveGraph representing paths and cycles among bivalued cells, as constructed by the bivalue rule and used by the repeat and conflict rules. - + - otherbv maps pairs (cell,digit) in the bivalue graph to the other digit available at the same cell - + - logstream is a stream on which to log verbose descriptions of the steps made by the solver (typically sys.stderr), or None if verbose descriptions are not to be logged. - + - steps is used to count how many solver passes we've made so far. - + - original_cells is a bitmask of cells that were originally nonempty. - + - assume_unique should be set true to enable solution rules based on the assumption that there exists a unique solution - + - twosat should be set true to enable a 2SAT-based solution rule """ - self.contents = [0]*81 - self.locations = [None]+[(1<<81)-1]*9 + self.contents = [0] * 81 + self.locations = [None] + [(1 << 81) - 1] * 9 self.rules_used = set() self.progress = False self.pairs = None @@ -236,12 +248,12 @@ def __init__(self,initial_placements = None): try: digit = int(item) except TypeError: - digit,cell = item + digit, cell = item if digit: - self.place(digit,cell) + self.place(digit, cell) self.original_cells |= 1 << cell cell += 1 - + def __iter__(self): """ If we are asked to loop over the items in a grid @@ -250,13 +262,13 @@ def __iter__(self): known cell contents of the grid. """ return iter(self.contents) - + def mark_progress(self): """Set progress True and clear fields that depended on old state.""" self.progress = True self.pairs = None - - def log(self,items,explanation=None): + + def log(self, items, explanation=None): """ Send a message for verbose output. Items should be a string or list of strings in the message. @@ -265,50 +277,57 @@ def log(self,items,explanation=None): """ if not self.logstream: return - if isinstance(items,str): + if isinstance(items, str): items = [items] if explanation: - if isinstance(explanation,str) or isinstance(explanation,list): + if isinstance(explanation, str) or isinstance(explanation, list): x = explanation else: x = explanation() - if isinstance(x,str): + if isinstance(x, str): x = [x] else: x = [] - text = ' '.join([str(i) for i in items+x]) + text = " ".join([str(i) for i in items + x]) for line in wrap(text): - self.logstream.write(line+'\n') - self.logstream.write('\n') + self.logstream.write(line + "\n") + self.logstream.write("\n") self.logstream.flush() - def place(self,digit,cell,explanation=None): + def place(self, digit, cell, explanation=None): """Change the puzzle by filling the given cell with the given digit.""" if digit != int(digit) or not 1 <= digit <= 9: - raise ValueError("place(%d,%d): digit out of range" % (digit,cell)) + raise ValueError("place(%d,%d): digit out of range" % (digit, cell)) if self.contents[cell] == digit: return if self.contents[cell]: - self.log(["Unable to place",digit,"in",cellnames[cell], - "as it already contains",str(self.contents[cell])+"."]) - raise BadSudoku("place(%d,%d): cell already contains %d" % - (digit,cell,self.contents[cell])) - if (1< 1: - that = "would leave too few remaining cells" \ - " to place those digits." + that = "would leave too few remaining cells" " to place those digits." if expls: - expls[-1] += ',' + expls[-1] += "," if force == forces[-1]: - expls[-1] += ' and' + expls[-1] += " and" forcedigs = [str(x) for x in force] forcedigs.sort() forcemask = 0 for dig in force: for cell in force[dig]: - forcemask |= 1< 1: for d in grid.choices(cell): - T[(cell,d)] = [Not((cell,e)) - for e in grid.choices(cell) - if d != e] - T[Not((cell,d))] = [] - + T[(cell, d)] = [Not((cell, e)) for e in grid.choices(cell) if d != e] + T[Not((cell, d))] = [] + # If a cell has value d, its neighbors can't have the same value for cell in range(81): if len(grid.choices(cell)) > 1: for neighbor in range(81): - if cell != neighbor and (1<") for a in range(3): @@ -1398,45 +1659,51 @@ def html_format(grid): for c in range(3): print("") for d in range(3): - row = 3*a+c - col = 3*b+d - cell = 9*row+col + row = 3 * a + c + col = 3 * b + d + cell = 9 * row + col if grid.contents[cell]: - print('%d' % grid.contents[cell]) + print( + '%d' + % grid.contents[cell] + ) else: - print('') + print( + '' + ) print("") print("") print("") print("") return False + def svg_format(grid): - svg = SVG(274+274j,sys.stdout) - svg.group(style={"fill":"none", "stroke":"black", "stroke-width":"1.5"}) - svg.rectangle(2+2j,272+272j) - for i in [3,6]: - pos = 30*i+2 - svg.segment(2+pos*1j,272+pos*1j) - svg.segment(pos+2j,pos+272j) + svg = SVG(274 + 274j, sys.stdout) + svg.group(style={"fill": "none", "stroke": "black", "stroke-width": "1.5"}) + svg.rectangle(2 + 2j, 272 + 272j) + for i in [3, 6]: + pos = 30 * i + 2 + svg.segment(2 + pos * 1j, 272 + pos * 1j) + svg.segment(pos + 2j, pos + 272j) svg.ungroup() - svg.group(style={"fill":"none", "stroke":"black", "stroke-width":"0.5"}) - for i in [1,2,4,5,7,8]: - pos = 30*i+2 - svg.segment(2+pos*1j,272+pos*1j) - svg.segment(pos+2j,pos+272j) + svg.group(style={"fill": "none", "stroke": "black", "stroke-width": "0.5"}) + for i in [1, 2, 4, 5, 7, 8]: + pos = 30 * i + 2 + svg.segment(2 + pos * 1j, 272 + pos * 1j) + svg.segment(pos + 2j, pos + 272j) svg.ungroup() - svg.group(style={"font-family":"Times", "font-size":"24", "fill":"black", - "text-anchor":"middle"}) + svg.group(style={"font-family": "Times", "font-size": "24", "fill": "black", "text-anchor": "middle"}) for row in range(9): for col in range(9): - cell = row*9+col + cell = row * 9 + col if grid.contents[cell]: - svg.text(str(grid.contents[cell]),30*col+17+30j*row+25j) + svg.text(str(grid.contents[cell]), 30 * col + 17 + 30j * row + 25j) svg.ungroup() svg.close() return False + output_formats = { "text": text_format, "txt": text_format, @@ -1449,54 +1716,71 @@ def svg_format(grid): "svg": svg_format, "s": svg_format, } - + # ====================================================================== # Backtracking search for all solutions # ====================================================================== -def all_solutions(grid, fastrules = True): + +def all_solutions(grid, fastrules=True): """Generate sequence of completed Sudoku grids from initial puzzle.""" while True: # first try the usual non-backtracking rules try: - while step(grid,fastrules): pass + while step(grid, fastrules): + pass except BadSudoku: - grid.log("A contradiction was found," - " so this branch has no solutions.") + grid.log("A contradiction was found," " so this branch has no solutions.") return # no solutions - + # if they finished off the puzzle, there's only one solution if grid.complete(): grid.log("A solution to the puzzle has been found.") yield grid return - + # find a cell with few remaining possibilities def choices(c): ch = grid.choices(c) - if len(ch) < 2: return (10,0,0) - return (len(ch),c,ch[0]) - L,c,d = min([choices(c) for c in range(81)]) - + if len(ch) < 2: + return (10, 0, 0) + return (len(ch), c, ch[0]) + + L, c, d = min([choices(c) for c in range(81)]) + # try it both ways branch = Sudoku(grid) - grid.log("Failed to progress, " - "creating a new backtracking search branch.") + grid.log("Failed to progress, " "creating a new backtracking search branch.") branch.logstream = grid.logstream branch.steps = grid.steps branch.original_cells = grid.original_cells - branch.place(d,c,"The backtracking search will try this placement" - " first. Then, after returning from this branch," - " it will try preventing this placement.") - for sol in all_solutions(branch,fastrules): + branch.place( + d, + c, + "The backtracking search will try this placement" + " first. Then, after returning from this branch," + " it will try preventing this placement.", + ) + for sol in all_solutions(branch, fastrules): yield sol - grid.log(["Returned from backtracking branch; undoing placement of", - d,"in",cellnames[c],"and all subsequent decisions."]) + grid.log( + [ + "Returned from backtracking branch; undoing placement of", + d, + "in", + cellnames[c], + "and all subsequent decisions.", + ] + ) grid.rules_used.update(branch.rules_used) grid.rules_used.add("backtrack") grid.steps = branch.steps - grid.unplace(d,1<B, then it is also valid to conclude that ~B => ~A, and our 2SAT solver needs to have that second implication made explicit. @@ -49,15 +50,16 @@ def Symmetrize(G): """ H = copyGraph(G) for v in G: - H.setdefault(Not(v),set()) # make sure all negations are included + H.setdefault(Not(v), set()) # make sure all negations are included for w in G[v]: - H.setdefault(w,set()) # as well as all implicants - H.setdefault(Not(w),set()) # and negated implicants + H.setdefault(w, set()) # as well as all implicants + H.setdefault(Not(w), set()) # and negated implicants for v in G: for w in G[v]: H[Not(w)].add(Not(v)) return H + def Satisfiable(G): """Does this 2SAT instance have a satisfying assignment?""" G = Condensation(Symmetrize(G)) @@ -67,14 +69,15 @@ def Satisfiable(G): return False return True + def Forced(G): """Find forced values for variables in a 2SAT instance. - + A variable's value is forced to x if every satisfying assignment assigns the same value x to that variable. We return a dictionary (possibly empty) in which the keys are the forced variables and their values are the values they are forced to. - + If the given instance is unsatisfiable, we return None.""" Force = {} Sym = Symmetrize(G) @@ -85,9 +88,9 @@ def Forced(G): Map[v] = SCC Reach = Reachability(Con) for v in Sym: - if Reach.reachable(Map[v],Map[Not(v)]): # v implies not v? + if Reach.reachable(Map[v], Map[Not(v)]): # v implies not v? value = False - if isinstance(v,SymbolicNegation): + if isinstance(v, SymbolicNegation): v = Not(v) value = True if v in Force: # already added by negation? @@ -95,22 +98,25 @@ def Forced(G): Force[v] = value return Force + # Unit tests # Run python TwoSatisfiability.py to perform these tests. + class TwoSatTest(unittest.TestCase): - T1 = {1:[2,3], 2:[Not(1),3]} - T2 = {1:[2], 2:[Not(1)], Not(1):[3], 3:[4,2], 4:[1]} + T1 = {1: [2, 3], 2: [Not(1), 3]} + T2 = {1: [2], 2: [Not(1)], Not(1): [3], 3: [4, 2], 4: [1]} def testTwoSat(self): """Check that the correct problems are satisfiable.""" - self.assertEqual(Satisfiable(self.T1),True) - self.assertEqual(Satisfiable(self.T2),False) + self.assertEqual(Satisfiable(self.T1), True) + self.assertEqual(Satisfiable(self.T2), False) def testForced(self): """Check that we can correctly identify forced variables.""" - self.assertEqual(Forced(self.T1),{1:False}) - self.assertEqual(Forced(self.T2),None) + self.assertEqual(Forced(self.T1), {1: False}) + self.assertEqual(Forced(self.T2), None) + if __name__ == "__main__": - unittest.main() + unittest.main() diff --git a/pads/UnionFind.py b/pads/UnionFind.py index c5e0599..8debd41 100644 --- a/pads/UnionFind.py +++ b/pads/UnionFind.py @@ -5,6 +5,7 @@ with significant additional changes by D. Eppstein. """ + class UnionFind: """Union-find data structure. @@ -47,7 +48,7 @@ def __getitem__(self, object): for ancestor in path: self.parents[ancestor] = root return root - + def __iter__(self): """Iterate through all items ever found or unioned by this structure.""" return iter(self.parents) @@ -55,7 +56,7 @@ def __iter__(self): def union(self, *objects): """Find the sets containing the objects and merge them all.""" roots = [self[x] for x in objects] - heaviest = max([(self.weights[r],r) for r in roots])[1] + heaviest = max([(self.weights[r], r) for r in roots])[1] for r in roots: if r != heaviest: self.weights[heaviest] += self.weights[r] diff --git a/pads/Util.py b/pads/Util.py index 859470b..06269cc 100644 --- a/pads/Util.py +++ b/pads/Util.py @@ -4,6 +4,7 @@ D. Eppstein, April 2004. """ + def arbitrary_item(S): """ Select an arbitrary item from set or sequence S. @@ -15,12 +16,15 @@ def arbitrary_item(S): except StopIteration: raise IndexError("No items to select.") + def map_to_constant(constant): """ Return a factory that turns sequences into dictionaries, where the dictionary maps each item in the sequence into the given constant. Appropriate as the adjacency_list_type argument for Graphs.copyGraph. """ + def factory(seq): - return dict.fromkeys(seq,constant) + return dict.fromkeys(seq, constant) + return factory diff --git a/pads/Wrap.py b/pads/Wrap.py index 553fd6a..ba91c62 100644 --- a/pads/Wrap.py +++ b/pads/Wrap.py @@ -12,67 +12,70 @@ from .SMAWK import OnlineConcaveMinima -def wrap(text, # string or unicode to be wrapped - target = 76, # maximum length of a wrapped line - longlast = False, # True if last line should be as long as others - frenchspacing = False, # Single space instead of double after periods - measure = len, # how to measure the length of a word - overpenalty = 1000, # penalize long lines by overpen*(len-target) - nlinepenalty = 1000, # penalize more lines than optimal - onewordpenalty = 25, # penalize really short last line - hyphenpenalty = 25): # penalize breaking hyphenated words + +def wrap( + text, # string or unicode to be wrapped + target=76, # maximum length of a wrapped line + longlast=False, # True if last line should be as long as others + frenchspacing=False, # Single space instead of double after periods + measure=len, # how to measure the length of a word + overpenalty=1000, # penalize long lines by overpen*(len-target) + nlinepenalty=1000, # penalize more lines than optimal + onewordpenalty=25, # penalize really short last line + hyphenpenalty=25, +): # penalize breaking hyphenated words """Wrap the given text, returning a sequence of lines.""" # Make sequence of tuples (word, spacing if no break, cum.measure). words = [] total = 0 - spacings = [0, measure(' '), measure(' ')] + spacings = [0, measure(" "), measure(" ")] for hyphenword in text.split(): if words: total += spacings[words[-1][1]] - parts = hyphenword.split('-') + parts = hyphenword.split("-") for word in parts[:-1]: - word += '-' + word += "-" total += measure(word) - words.append((word,0,total)) + words.append((word, 0, total)) word = parts[-1] total += measure(word) spacing = 1 - if word.endswith('.') and (len(hyphenword) > 2 or - not hyphenword[0].isupper()): + if word.endswith(".") and (len(hyphenword) > 2 or not hyphenword[0].isupper()): spacing = 2 - frenchspacing - words.append((word,spacing,total)) + words.append((word, spacing, total)) # Define penalty function for breaking on line words[i:j] # Below this definition we will set up cost[i] to be the # total penalty of all lines up to a break prior to word i. - def penalty(i,j): - if j > len(words): return -i # concave flag for out of bounds + def penalty(i, j): + if j > len(words): + return -i # concave flag for out of bounds total = cost.value(i) + nlinepenalty - prevmeasure = i and (words[i-1][2] + spacings[words[i-1][1]]) - linemeasure = words[j-1][2] - prevmeasure + prevmeasure = i and (words[i - 1][2] + spacings[words[i - 1][1]]) + linemeasure = words[j - 1][2] - prevmeasure if linemeasure > target: total += overpenalty * (linemeasure - target) elif j < len(words) or longlast: - total += (target - linemeasure)**2 - elif i == j-1: + total += (target - linemeasure) ** 2 + elif i == j - 1: total += onewordpenalty - if not words[j-1][1]: + if not words[j - 1][1]: total += hyphenpenalty return total # Apply concave minima algorithm and backtrack to form lines - cost = OnlineConcaveMinima(penalty,0) + cost = OnlineConcaveMinima(penalty, 0) pos = len(words) lines = [] while pos: breakpoint = cost.index(pos) line = [] - for i in range(breakpoint,pos): + for i in range(breakpoint, pos): line.append(words[i][0]) - if i < pos-1 and words[i][1]: - line.append(' '*words[i][1]) - lines.append(''.join(line)) + if i < pos - 1 and words[i][1]: + line.append(" " * words[i][1]) + lines.append("".join(line)) pos = breakpoint lines.reverse() return lines diff --git a/pads/xyzGraph.py b/pads/xyzGraph.py index 64e3f31..f390c1a 100644 --- a/pads/xyzGraph.py +++ b/pads/xyzGraph.py @@ -26,13 +26,14 @@ except: xrange = range + def CubicMatchPartitions(G): """Partition a biconnected cubic graph G into three matchings. Each matching is represented as a graph, in which G[v] is a list of the three edges of G in the order of the three matchings. This function generates a sequence of such representations. """ - + if not isUndirected(G): raise ValueError("CubicMatchPartitions: graph is not undirected") for v in G: @@ -40,31 +41,31 @@ def CubicMatchPartitions(G): raise ValueError("CubicMatchPartitions: graph is not cubic") ST = stOrientation(G) L = TopologicalOrder(ST) - for B in xrange(1<<(len(L)//2 - 1)): + for B in xrange(1 << (len(L) // 2 - 1)): # Here with a bitstring representing the sequence of choices out = {} pos = 0 for v in L: source = [w for w in G[v] if w in out] sourcepos = {} - adjlist = [None,None,None] + adjlist = [None, None, None] for w in source: - sourcepos[w] = [i for i in (0,1,2) if out[w][i]==v][0] + sourcepos[w] = [i for i in (0, 1, 2) if out[w][i] == v][0] adjlist[sourcepos[w]] = w usedpos = [sourcepos[w] for w in source] if len(set(usedpos)) != len(usedpos): # two edges in with same index, doesn't form matching - break + break elif len(source) == 0: # start vertex, choose one orientation adjlist = list(ST[v]) elif len(source) == 1: # two outgoing vertices, one incoming - avail = [i for i in (0,1,2) if i != usedpos[0]] - if B & (1<= 65.0", "wheel"] +requires = ["setuptools>=64.0.0", "setuptools-scm", "wheel"] build-backend = "setuptools.build_meta" + +[project] +authors = [{ name = "TNO Quantum", email = "tnoquantum@tno.nl" }] +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [] +description = "Python Algorithms and Data Structure" +keywords = ["data", "algorithms"] +license = { file = "LICENSE" } +name = "pads" +readme = { file = "README.md", content-type = "text/markdown" } +requires-python = ">=3.8" +version = "0.1.0" + +[project.optional-dependencies] +dev = [ + "build", # build is not only used in publishing (below), but also in the template's test suite + "bump-my-version", + "coverage [toml]", + "pytest", + "pytest-cov", + "ruff", + "sphinx", + "sphinx_rtd_theme", + "sphinx-autoapi", + "tox", + "myst_parser", +] +publishing = ["build", "twine", "wheel"] + +[project.urls] +Repository = "https://github.com/QuantumApplicationLab/PADS" +Issues = "https://github.com/QuantumApplicationLab/PADS/issues" +Changelog = "https://github.com/QuantumApplicationLab/PADS/CHANGELOG.md" + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.coverage.run] +branch = true +source = ["pads"] +command_line = "-m pytest" + +[tool.isort] +lines_after_imports = 2 +force_single_line = 1 +no_lines_before = [ + "FUTURE", + "STDLIB", + "THIRDPARTY", + "FIRSTPARTY", + "LOCALFOLDER", +] +known_first_party = "pads" +src_paths = ["pads", "tests"] +line_length = 120 + +# For completeness, until we move to an src-based layout +[tool.setuptools.packages.find] +include = ["pads*"] +exclude = ["tests*"] + +[tool.tox] +legacy_tox_ini = """ +[tox] +envlist = py38,py39,py310,py311,py312 +skip_missing_interpreters = true +[testenv] +commands = pytest +extras = dev +""" + +[tool.ruff] +# Enable Pyflakes `E` and `F` codes by default. +select = [ + "F", # Pyflakes + "E", # pycodestyle (error) + "W", # pycodestyle (warning) + # "C90", # mccabe + "I", # isort + "D", # pydocstyle + # "PL", # Pylint + # "PLC", # Convention + # "PLE", # Error + # "PLR", # Refactor + # "PLW", # Warning + +] +ignore = [ + 'D100', # Missing module docstring + 'D104', # Missing public package docstring + # The following list excludes rules irrelevant to the Google style + 'D203', + 'D204', + 'D213', + 'D215', + 'D400', + 'D401', + 'D404', + 'D406', + 'D407', + 'D408', + 'D409', + 'D413', +] + +# Allow autofix for all enabled rules (when `--fix`) is provided. +fixable = ["A", "B", "C", "D", "E", "F", "I"] +unfixable = [] + +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + ".venv", + "scripts", +] +per-file-ignores = {} + + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +target-version = "py39" +line-length = 120 + +[tool.ruff.isort] +known-first-party = ["pads"] +force-single-line = true +no-lines-before = [ + "future", + "standard-library", + "third-party", + "first-party", + "local-folder", +] + +[tool.bumpversion] +current_version = "0.1.0" + +[[tool.bumpversion.files]] +filename = "pads/__init__.py" + +[[tool.bumpversion.files]] +filename = "pyproject.toml" + +[[tool.bumpversion.files]] +filename = "CITATION.cff" + +[[tool.bumpversion.files]] +filename = "docs/conf.py" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 04600c8..0000000 --- a/setup.cfg +++ /dev/null @@ -1,19 +0,0 @@ -[metadata] -name = PADS -version = 1.0 -author = TNO Quantum -author_email = tnoquantum@tno.nl -description = Python Algorithms and Data Structures -long_description = file: README.md -long_description_content_type = text/markdown -url = https://tno.nl -classifiers = - Programming Language :: Python :: 3 - Operating System :: OS Independent -license_files = LICENSE - -[options] -packages = find: - -[options.packages.find] -include = pads diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_chordal.py b/tests/test_chordal.py new file mode 100644 index 0000000..245952d --- /dev/null +++ b/tests/test_chordal.py @@ -0,0 +1,30 @@ +import unittest +from pads.Chordal import Chordal +from pads.Chordal import PerfectEliminationOrdering + + +class ChordalTest(unittest.TestCase): + """Test the chordal reduction.""" + + claw = {0: [1, 2, 3], 1: [0], 2: [0], 3: [0]} + butterfly = {0: [1, 2, 3, 4], 1: [0, 2], 2: [0, 1], 3: [0, 4], 4: [0, 3]} + diamond = {0: [1, 2], 1: [0, 2, 3], 2: [0, 1, 3], 3: [1, 2]} + quad = {0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2]} + graphs = [(claw, True), (butterfly, True), (diamond, True), (quad, False)] + + def testChordal(self): + """Check that Chordal() returns the correct answer on each test graph.""" + for G, isChordal in ChordalTest.graphs: + self.assertEqual(Chordal(G), isChordal) + + def testElimination(self): + """Check that PerfectEliminationOrdering generates an elimination ordering.""" + for G, isChordal in ChordalTest.graphs: + if isChordal: + eliminated = set() + for v in PerfectEliminationOrdering(G): + eliminated.add(v) + for w in G[v]: + for x in G[v]: + if w != x and w not in eliminated and x not in eliminated: + self.assertTrue(w in G[x] and x in G[w])