diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..a3ce161f --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,107 @@ +version: 2 +jobs: + test-2.7: &test-template + docker: + - image: circleci/python:2.7-jessie + + working_directory: ~/repo + + steps: + - checkout + + - run: + name: create virtualenv + command: | + python -m virtualenv env + + - run: + name: install python dependencies + command: | + . env/bin/activate + python --version + pip install -r requirements.txt + + - run: + name: run unittests + command: | + . env/bin/activate + python --version + coverage run -m unittest discover + + - run: + name: codecov + command: | + . env/bin/activate + # todo: uncomment below when the repository becomes public + #codecov + + test-3.4: + <<: *test-template + docker: + - image: circleci/python:3.4-jessie + + test-3.5: + <<: *test-template + docker: + - image: circleci/python:3.5-jessie + + test-3.6: + <<: *test-template + docker: + - image: circleci/python:3.6-jessie + + test-3.7: + <<: *test-template + docker: + - image: circleci/python:3.7-stretch + + test-doc: + docker: + - image: circleci/python:3.6-jessie + + working_directory: ~/repo + + steps: + - run: + name: install graphviz and pandoc + command: | + sudo apt-get install graphviz + sudo apt-get install pandoc + + - checkout + + - run: + name: create virtualenv + command: | + python -m virtualenv env + + - run: + name: install sphinx and dependencies + command: | + . env/bin/activate + pip install -r requirements.txt + pip install sphinx + pip install sphinx_rtd_theme + + - run: + name: test doc build + command: | + . env/bin/activate + sphinx-build -W -b html docs docs/_build/html + + - run: + name: run doctest + command: | + . env/bin/activate + make doctest + +workflows: + version: 2 + tests: + jobs: + - test-2.7 + - test-3.4 + - test-3.5 + - test-3.6 + - test-3.7 + - test-doc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..9ec90b1e --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[run] +# omit virtualenv +omit = venv/* +source=pyqubo diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5941bd46 --- /dev/null +++ b/.gitignore @@ -0,0 +1,126 @@ +# PyCharm +.idea/ + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +docs/.doctrees/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + + +### Python.VirtualEnv Stack ### +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +pip-selfcheck.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..25c0b419 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Recruit Communications Co., Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..e8da3bf6 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = pyqubo +SOURCEDIR = docs +BUILDDIR = docs/_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) \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..c48815c8 --- /dev/null +++ b/README.rst @@ -0,0 +1,102 @@ +.. index-start-marker1 + +PyQUBO +====== + +PyQUBO allows you to create QUBOs or Ising models from flexible mathematical expressions easily. +Some of the features of PyQUBO are + +* **Python-based** (the entire code is written in Python). +* **QUBO generation (compile) is fast** (due to the JIT like compile mechanism). +* **Comes with plenty of useful expressions** such as binary integer variables, logical constraints, or matrix/vector variables. + +Example Usage +------------- + +This example constructs a simple expression and compile it to ``model``. +By calling ``model.to_qubo()``, we get the resulting QUBO. +(This example solves a number partitioning problem with a set S = {4, 2, 7, 1}) + +>>> from pyqubo import Spin +>>> s1, s2, s3, s4 = Spin("s1"), Spin("s2"), Spin("s3"), Spin("s4") +>>> H = (4*s1 + 2*s2 + 7*s3 + s4)**2 +>>> model = H.compile() +>>> qubo, offset = model.to_qubo() +>>> pprint(qubo) +{('s1', 's1'): -160.0, + ('s1', 's2'): 64.0, + ('s1', 's3'): 224.0, + ('s1', 's4'): 32.0, + ('s2', 's2'): -96.0, + ('s2', 's3'): 112.0, + ('s2', 's4'): 16.0, + ('s3', 's3'): -196.0, + ('s3', 's4'): 56.0, + ('s4', 's4'): -52.0} + + + +More examples can be found in `readthedocs_example_page (not published yet) `_ + +Installation +------------ + +.. code-block:: shell + + pip install pyqubo + +or + +.. code-block:: shell + + python setup.py install + +Supported Python Versions +------------------------- + +Python 2.7, 3.4, 3.5, 3.6 and 3.7 are supported. + +.. index-end-marker1 + +Test +---- + +Run all tests. + +.. code-block:: shell + + python -m unittest discover test + +Show coverage report. + +.. code-block:: shell + + coverage run -m unittest discover + coverage html + +Run test with circleci CLI. + +.. code-block:: shell + + circleci build --job $JOBNAME + +Run doctest. + +.. code-block:: shell + + make doctest + +Organization +------------ + +Recruit Communications Co., Ltd. + +Licence +------- + +Released under the Apache License 2.0. + +Contribution +------------ + +We welcome contributions to this project. \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..3a8f5b94 --- /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 = pyqubo +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) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..8d03c5c2 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- 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 +import doctest +sys.path.insert(0, os.path.abspath('../')) + + +# -- Project information ----------------------------------------------------- + +project = u'pyqubo' +copyright = u'Recruit Communications Co., Ltd' +author = u'Author' + +# The short X.Y version +version = u'0.1' +# The full version, including alpha/beta/rc tags +release = u'0.0.1' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# 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.viewcode', +# 'sphinx.ext.todo' +#] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '**.ipynb_checkpoints'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# -- 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 = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'pyqubodoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'pyqubo.tex', u'pyqubo Documentation', + u'Author', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'pyqubo', u'pyqubo Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'pyqubo', u'pyqubo Documentation', + author, 'pyqubo', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project +epub_author = author +epub_publisher = author +epub_copyright = copyright + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for todo extension ---------------------------------------------- + + +# enable google style docstring +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'sphinx.ext.mathjax', + 'sphinx.ext.inheritance_diagram', + 'sphinx.ext.graphviz', + 'sphinx.ext.doctest', + 'sphinx.ext.autosummary', + 'sphinx.ext.viewcode', + 'nbsphinx' +] + +# configure doctest +doctest_default_flags = doctest.NORMALIZE_WHITESPACE +doctest_global_setup = "from pprint import pprint" + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + +add_module_names = False diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..e55c7852 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,32 @@ + + +.. include:: ../README.rst + :start-after: index-start-marker1 + :end-before: index-end-marker1 + +.. toctree:: + :maxdepth: 1 + :caption: Manual: + + notebooks/getting_started.ipynb + +.. toctree:: + :maxdepth: 1 + :caption: Reference Document: + + reference/express + reference/model + reference/tensor + reference/constraint + reference/logic + reference/func + reference/utils + reference/internal/index + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..11a84537 --- /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=pyqubo + +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/docs/notebooks/getting_started.ipynb b/docs/notebooks/getting_started.ipynb new file mode 100644 index 00000000..66a1c4c8 --- /dev/null +++ b/docs/notebooks/getting_started.ipynb @@ -0,0 +1,77 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Getting Started\n", + "\n", + "With PyQUBO, you can construct QUBOs with 3 steps:\n", + "\n", + "1. Define the hamiltonian with expressions\n", + "2. Compile the expression to get the model\n", + "3. Call 'to_qubo()' method of model." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "({('a', 'a'): 2.0, ('c', 'c'): 1.0, ('b', 'b'): 0.0, ('a', 'b'): 1.0}, 0.0)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from pyqubo import Qbit\n", + "\n", + "# 1. Define the hamiltonian with expressions\n", + "a, b, c = Qbit('a'), Qbit('b'), Qbit('c')\n", + "H = a*b + 2*a + c\n", + "\n", + "# 2. Compile the expression to get the model\n", + "model = H.compile()\n", + "\n", + "# 3. Call 'to_qubo()' method of model\n", + "model.to_qubo()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3.0 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.1" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/reference/constraint.rst b/docs/reference/constraint.rst new file mode 100644 index 00000000..44f59f74 --- /dev/null +++ b/docs/reference/constraint.rst @@ -0,0 +1,30 @@ +Logical Constraint +================== + + +.. automodule:: pyqubo +.. currentmodule:: pyqubo + +NOT Constraint +-------------- + +.. autoclass:: NotConst + :members: + +AND Constraint +-------------- + +.. autoclass:: AndConst + :members: + +OR Constraint +------------- + +.. autoclass:: OrConst + :members: + +XOR Constraint +-------------- + +.. autoclass:: XorConst + :members: \ No newline at end of file diff --git a/docs/reference/express.rst b/docs/reference/express.rst new file mode 100644 index 00000000..cc9440f5 --- /dev/null +++ b/docs/reference/express.rst @@ -0,0 +1,51 @@ +Expression +========== + +.. automodule:: pyqubo +.. currentmodule:: pyqubo + +.. autoclass:: Express + :members: + +Qbit +---- +.. autoclass:: Qbit + +Spin +---- +.. autoclass:: Spin + +Param +----- +.. autoclass:: Param + :members: + +Constraint +---------- +.. autoclass:: Constraint + :members: + +UserDefinedExpress +------------------ +.. autoclass:: UserDefinedExpress + :members: + +Add +--- +.. autoclass:: Add + :members: + +AddList +------- +.. autoclass:: AddList + :members: + +Mul +--- +.. autoclass:: Mul + :members: + +Num +--- +.. autoclass:: Num + :members: \ No newline at end of file diff --git a/docs/reference/func.rst b/docs/reference/func.rst new file mode 100644 index 00000000..3721ce05 --- /dev/null +++ b/docs/reference/func.rst @@ -0,0 +1,11 @@ +Functions +========= + + +.. automodule:: pyqubo +.. currentmodule:: pyqubo + +Sum over indices +---------------- +.. autoclass:: Sum + :members: diff --git a/docs/reference/internal/binaryprod.rst b/docs/reference/internal/binaryprod.rst new file mode 100644 index 00000000..f57d4831 --- /dev/null +++ b/docs/reference/internal/binaryprod.rst @@ -0,0 +1,9 @@ +BinaryProd +========== + + +.. automodule:: pyqubo +.. currentmodule:: pyqubo + +.. autoclass:: BinaryProd + :members: \ No newline at end of file diff --git a/docs/reference/internal/coefficient.rst b/docs/reference/internal/coefficient.rst new file mode 100644 index 00000000..548b9969 --- /dev/null +++ b/docs/reference/internal/coefficient.rst @@ -0,0 +1,9 @@ +Coefficient +=========== + + +.. automodule:: pyqubo +.. currentmodule:: pyqubo + +.. autoclass:: Coefficient + :members: diff --git a/docs/reference/internal/compiledqubo.rst b/docs/reference/internal/compiledqubo.rst new file mode 100644 index 00000000..3857c151 --- /dev/null +++ b/docs/reference/internal/compiledqubo.rst @@ -0,0 +1,9 @@ +CompiledQubo +============ + + +.. automodule:: pyqubo +.. currentmodule:: pyqubo + +.. autoclass:: CompiledQubo + :members: \ No newline at end of file diff --git a/docs/reference/internal/index.rst b/docs/reference/internal/index.rst new file mode 100644 index 00000000..c69e7360 --- /dev/null +++ b/docs/reference/internal/index.rst @@ -0,0 +1,13 @@ +.. _internal: + +Internal Class +************** + +.. toctree:: + :maxdepth: 2 + + binaryprod + paramprod + compiledqubo + coefficient + diff --git a/docs/reference/internal/paramprod.rst b/docs/reference/internal/paramprod.rst new file mode 100644 index 00000000..164fe3d8 --- /dev/null +++ b/docs/reference/internal/paramprod.rst @@ -0,0 +1,9 @@ +ParamProd +========= + + +.. automodule:: pyqubo +.. currentmodule:: pyqubo + +.. autoclass:: ParamProd + :members: diff --git a/docs/reference/logic.rst b/docs/reference/logic.rst new file mode 100644 index 00000000..a5c0016d --- /dev/null +++ b/docs/reference/logic.rst @@ -0,0 +1,21 @@ +Logical Gate +============ + + +.. automodule:: pyqubo +.. currentmodule:: pyqubo + +Not +--- +.. autoclass:: Not + :members: + +And +--- +.. autoclass:: And + :members: + +Or +--- +.. autoclass:: Or + :members: \ No newline at end of file diff --git a/docs/reference/model.rst b/docs/reference/model.rst new file mode 100644 index 00000000..7eefce46 --- /dev/null +++ b/docs/reference/model.rst @@ -0,0 +1,9 @@ +Model +======= + + +.. automodule:: pyqubo +.. currentmodule:: pyqubo + +.. autoclass:: Model + :members: diff --git a/docs/reference/tensor.rst b/docs/reference/tensor.rst new file mode 100644 index 00000000..dbf97675 --- /dev/null +++ b/docs/reference/tensor.rst @@ -0,0 +1,18 @@ +Tensor +======= + + +.. automodule:: pyqubo +.. currentmodule:: pyqubo + +Vector +------ + +.. autoclass:: Vector + :members: + +Matrix +------ + +.. autoclass:: Matrix + :members: \ No newline at end of file diff --git a/docs/reference/utils.rst b/docs/reference/utils.rst new file mode 100644 index 00000000..0814312b --- /dev/null +++ b/docs/reference/utils.rst @@ -0,0 +1,19 @@ +Utils +===== + +.. automodule:: pyqubo +.. currentmodule:: pyqubo + + +Solvers +------- + +.. automodule:: pyqubo.utils.solver + :members: + + +Asserts +------- + +.. automodule:: pyqubo.utils.asserts + :members: \ No newline at end of file diff --git a/notebooks/japanese/TSP.ipynb b/notebooks/japanese/TSP.ipynb new file mode 100644 index 00000000..d896fd31 --- /dev/null +++ b/notebooks/japanese/TSP.ipynb @@ -0,0 +1,247 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "from pyqubo import Spin, Matrix, Param, solve_qubo, Const, Sum\n", + "import matplotlib.pyplot as plt\n", + "import networkx as nx\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Traveling Salesman Problem (TSP)\n", + "\n", + "全ての都市を一度だけ訪問し、元の都市に戻ってくる最短の経路を見つける。" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# utilメソッドを定義しておく\n", + "def plot_city(cities, sol = {}):\n", + " n_city = len(cities)\n", + " cities_dict = dict(cities)\n", + " G = nx.Graph()\n", + " for city in cities_dict:\n", + " G.add_node(city)\n", + " \n", + " # draw path\n", + " if sol:\n", + " city_order = []\n", + " for i, v in sol.items():\n", + " for j, v2 in v.items():\n", + " if v2 == 1:\n", + " city_order.append(j)\n", + " for i in range(n_city):\n", + " city_index1 = city_order[i]\n", + " city_index2 = city_order[(i+1) % n_city]\n", + " G.add_edge(cities[city_index1][0], cities[city_index2][0])\n", + "\n", + " plt.figure(figsize=(3,3))\n", + " pos = nx.spring_layout(G)\n", + " nx.draw_networkx(G, cities_dict)\n", + " plt.axis(\"off\")\n", + " plt.show()\n", + "\n", + "def dist(i, j, cities):\n", + " pos_i = cities[i][1]\n", + " pos_j = cities[j][1]\n", + " return np.sqrt((pos_i[0] - pos_j[0])**2 + (pos_i[1] - pos_j[1])**2)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAANAAAADFCAYAAAAlv3xcAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMi4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvIxREBQAACIpJREFUeJzt3U+IXVcdwPHvSbRMCoWxmkirabTBTRZRJCkUFXfCDEqlsUREiYKFLLoQZuOg6KYSRQpSELIzI4h2YSwoEwoWBBEkIyhjV/4Bk9gIrZTpoklQM9fFuS8z07k3eff+3rz73n3fz6rMe+/kMaffuffdOXdOKooCSe3s6/oNSNPMgKQAA5ICDEgKMCApwICkAAOSAgxICjAgKcCApIB3dP0GeiulQ8AZ4DgwD2wA68AFiuL1Lt+aRie5Fm7EUjoJLAMLQAEc2PboTSABl4BzFMXa+N+gRsmARimls8BzwBx3Pz3eBG4BSxTF+XG8Ne0NPwONylY891N+Xz8A/Lr62fvK5z1Xvk5TyoBGIZ+2DeJpYhDRidG/KY2DAY3GMvm0rY258vWaQgYUla+2LVDzvVwDjgHvAr5C/uDzNvuARVI6uGfvUXvGgOLOkK+2VfoJ8BLwd+AvwLPVTyvKcTRlDCjuODsvVe/wDHAYeBD4BvDT6qcdKMfRlDGguPm7PXh4238fAa63HEeTyYDiNu724LVt/30VeLjlOJpMBhS3Tl5hUOmHwD+BN4DvAKern3azHEdTxoDiVsjLcyp9AfgU8ChwFPhm9dNSOY6mjEt5RiGli8ATtPuBtAm8SFGcGu2b0jh4BBqNc1T+imcot8rXawoZ0CjkVdVLwI2Gr7xBXlD6h9G/KY2D9wONSlGcJyVwNfZM8TPQqOWFocvAIvX3A62S7wfyyDPlDGiv5LVtVXekrnhHan8YkBTgRQQpwICkAAOSAgxICjAgKcCApAADkgIMSAowICnAgKQAA5ICDEgKMCApwICkAAOSAgxICjAgKcCApAADkgIMSAowICnAgKQAA5ICDEgKMCApwICkAAOSAtzeRP2W0iGq/8j/hVH8kX//uLz6KaWT5G1mFqjfZuYSeZuZtdb/jAGpd1I6y5g2OjMg9ctWPPc3eNVgq83GERmQ+iOftv2GZvEM3AA+2XTXQK/CqU+WyadtbcyVr2/EI5D6IV9tu0L7gCB/HnqkydU5j0DqizPkq22VrgFPAgeBdwPPVD+tKMcZmgGpL46z81L1HbeBTwNHgH8ArwKfrx7jQDnO0PxFqvpivu6By8B14Pts/Q//8RbjVPEIpL7YqHvgGvnoM+TRonacKgakvlgnrzDY5TBwFfjfvce4WY4zNANSX6yQl+fs8hjwEPB14C3ypbbfVY+RynGGZkDqh6J4jby2bfPtD+0Hfgn8DXgEeD/wwu4RNoHVpgtM/T2Q+sOVCFJAXlW9RI6hicFauEbxgJex1TdFcZ6UwNXYUkBKJ8hr2xapvx9olXw/UOMjz51/xoDUaykdpPqO1BXvSJU65kUEKcCApAADkgIMSAowICnAgKQAA5ICDEgKMCApwICkAAOSAgxICjAgKcCApAADkgIMSAowICnAgKQAA5ICDEgKMCApwICkAAOSAgxICjAgKcCApAADkgIMSAqYzP2BUjpE9V/UvzCKv6ivITkP9zRZuzPkLfqWgQXq93S5RN7TZW38b3BGOA9Dm5yAUjrLmHYV0104D41MRkBbk9Zkc9jBvpYzO3kj5zw01n1AHeysrApDzsOXydvEP7vzyzM7D5NwFW6ZfLrQxlz5esU5Dy10G1C+yrMQeB/7gMVyH0y15Ty01vUR6Az5Ks8u14FTwEHgg8Dz9WMU5Thqr3Ye/gh8FHgAOE2+alBjJueh64COs/MSKZAv73wG+DDwKvAy8APgpeoxDpTjqL3KefgP8FngS8AbwFPAz+vHmMl56Dqg+aovrgGvA98C7gMeBZ4GftZwHA2t8vv3e+C/wNeAdwKfA062GKfPul6JsFH1xSvkU7jts3Eb+ETDcTS0yu/fdeB95N+aDhxpMU6fdR3QOvk32ztOHw6TP/f8dbgxbpbjqL3KeXiIfApdsBXRVeBo9RgzOQ9dn8KtsPMHHACPkT+0fo88K7eBV8indhVSOY7aq5yHx8k/YZ8nn8pdBC7XjzGT89BtQEXxGnlN1eb2L+8HfgX8iXwkeg/wVeDN3SNsAqsubAyqmYf7yNFcAB4EXgCerB5hZufBlQjKnIdWuj6Fo1zNu0SehCYGa7BmbtL2hPPQSvdHoAFXAU8G56GRyQkIIKUT5DVVi9Tfh7JKvg9lJn/ijYXzMLTJCmggr6mquhNyZRY/qHbGebinyQxImhLdX0SQppgBSQEGJAUYkBRgQFKAAUkBBiQFGJAUYEBSgAFJAQYkBRiQFGBAUoABSQEGJAUYkBRgQFKAAUkBBiQFGJAUYEBSgAFJAQYkBRiQFGBAUoABSQEGJAUYkBTQ9SbD1VI6RPWuABfcFUCTZLJ2Z8jbDC4DC9TvS3OJvC9NzZ7D0vhMTkDujKYpNBkBbcXTZIPbwd6cRqTOdB+Qu0Nrik3CVbhl8mlbG3Pl66VOdBtQvtq2UPU+vgscBR4AjgG/qB5hH7BY7uUpjV3XR6Az5KttuxwFfgu8CXwb+CLwr+oxinIcaey6Dug4Oy9V3/EU8DD5DZ4GPgRcrh7jQDmONHZdBzRf98CPgY+UT5gHXgH+3WIcaS91vRJho+qLV4CngZeBx4H95Jjucr2wchxpr3V9BFonrzDY4S3ykoPBlYEfkY9ANW6W40hj13VAK+RWdjgGLJGPPu8F/gx8rH6MVI4jjd0k/CL1IvAE7WLeBF6kKE6N9k1Jw+n6CARwjry2rY1b5eulTnQfUF5VvUReltPEYC2cy3jUma6vwmVFcZ6UwNXYmjLdfwbaLqUT5LVti9TfD7RKvh/II486N1kBDeS1bVV3pK54R6omyWQGJE2J7i8iSFPMgKQAA5ICDEgKMCApwICkAAOSAgxICjAgKcCApAADkgIMSAowICnAgKQAA5ICDEgKMCApwICkAAOSAgxICjAgKcCApAADkgL+D4iPU4rJiS9TAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# 都市の名前と座標のデータを用意 list[(\"name\", (x, y))]\n", + "cities = [\n", + " (\"a\", (0, 0)),\n", + " (\"b\", (1, 3)),\n", + " (\"c\", (3, 2)),\n", + " (\"d\", (2, 1)),\n", + " (\"e\", (0, 1))\n", + "]\n", + "plot_city(cities)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "バイナリベクトル$x$を用意。$x[i, j]=1$は時刻$i$に都市$j$にいることを表現する。" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "n_city = len(cities)\n", + "x = Matrix('c', n_city, n_city)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# ある時刻iに一つの都市にのみ存在できる制約を記述\n", + "time_const = 0.0\n", + "for i in range(n_city):\n", + " # Const(...)で数式を囲むと、その部分が制約として認識される。\n", + " time_const += Const((Sum(0, n_city, lambda j: x[i, j]) - 1)**2, label=\"time{}\".format(i))\n", + "\n", + "# 一つの都市を一度しか訪れない制約を記述\n", + "city_const = 0.0\n", + "for j in range(n_city):\n", + " city_const += Const((Sum(0, n_city, lambda i: x[i, j]) - 1)**2, label=\"city{}\".format(i))" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# 経路の総距離を記述\n", + "distance = 0.0\n", + "for i in range(n_city):\n", + " for j in range(n_city):\n", + " for k in range(n_city):\n", + " # 時刻kに都市i, 時刻k+1に都市jにいた場合の都市i,j間の距離\n", + " d_ij = dist(i, j, cities)\n", + " distance += d_ij * x[k, i] * x[(k+1)%n_city, j]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# ハミルトニアンを構築\n", + "A = Param(\"A\")\n", + "H = distance + A * (time_const + city_const)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# モデルをコンパイル\n", + "model = H.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# QUBOを作成\n", + "qubo, offset = model.to_qubo(params={'A': 4.0})" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "sol = solve_qubo(qubo)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "number of broken constarint = 0\n" + ] + } + ], + "source": [ + "solution, broken = model.decode_solution(sol, var_type=\"binary\")\n", + "print(\"number of broken constarint = {}\".format(len(broken)))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "if len(broken) == 0:\n", + " plot_city(cities, solution[\"c\"])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/japanese/graph_partition.ipynb b/notebooks/japanese/graph_partition.ipynb new file mode 100644 index 00000000..d736289f --- /dev/null +++ b/notebooks/japanese/graph_partition.ipynb @@ -0,0 +1,190 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "from pyqubo import Spin, Vector, Param, solve_ising, Const\n", + "import matplotlib.pyplot as plt\n", + "import networkx as nx" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## グラフ分割問題\n", + "\n", + "偶数の個数の頂点を持つグラフを2つに分割する。分割されるエッジが最小となる分割方法を見つけたい。\n", + "この問題はIsingモデルにより次のように定式化される。\n", + "\n", + "$$H(s) = H_{A}(s) + H_{B}(s)\\\\\n", + "H_{A}(s) = A \\left( \\sum_{i \\in V} s_{i}\\right )^2\\\\\n", + "H_{B}(s) = B \\sum_{(i, j) \\in E} \\frac{1-s_{i}s_{j}}{2}\n", + "$$\n", + "\n", + "$H_{A}(s)$は2つの集合の頂点数が同じになる制約。$H_{B}(s)$は切断されるエッジの個数である。" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_graph(E, colors=None):\n", + " G = nx.Graph()\n", + " for (i, j) in E:\n", + " G.add_edge(i, j)\n", + " plt.figure(figsize=(4,4))\n", + " pos = nx.spring_layout(G)\n", + " nx.draw_networkx(G, pos, node_color=colors)\n", + " plt.axis(\"off\")\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# エッジが以下のように与えられる\n", + "E = {(0, 6), (2, 4), (7, 5), (0, 4), (2, 0),\n", + " (5, 3), (2, 3), (2, 6), (4, 6), (1, 3),\n", + " (1, 5), (7, 1), (7, 3), (2, 5)}\n", + "plot_graph(E)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "ノード数と同じである$8$次元のスピンのスピンベクトル$s$を用意する。各スピンは対応するノードがどちらの集合に属するかを表している。" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# スピンベクトルの宣言\n", + "s = Vector(\"s\", 8, spin=True)\n", + "\n", + "# パラメータA, Bの宣言\n", + "A, B = Param(\"A\"), Param(\"B\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# ハミルトニアン H_{A}を定義\n", + "HA = A * Const(sum(s) ** 2, \"num_nodes\")\n", + "\n", + "# ハミルトニアン H_{B}を定義\n", + "HB = B * sum((1.0 - s[i]*s[j]) / 2.0 for (i, j) in E)\n", + "\n", + "H = HA + HB" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# モデルのコンパイル\n", + "model = H.compile()\n", + "\n", + "# A=1.0, B=1.0としてIsingモデルを得る\n", + "linear, quad, offset = model.to_ising(params={'A': 0.1, 'B':1.0})" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "#broken constraints: 1\n" + ] + } + ], + "source": [ + "# Isingモデルを解く\n", + "solution = solve_ising(linear, quad)\n", + "\n", + "# 解をデコードする\n", + "decoded_sol, broken = model.decode_solution(solution, var_type=\"binary\")\n", + "print(\"#broken constraints: {}\".format(len(broken)))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# グラフを色分けしてみる\n", + "plot_graph(E, [solution[k] for k in sorted(solution.keys())])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/japanese/integer_partition.ipynb b/notebooks/japanese/integer_partition.ipynb new file mode 100644 index 00000000..e2d3a3a6 --- /dev/null +++ b/notebooks/japanese/integer_partition.ipynb @@ -0,0 +1,89 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from pyqubo import Spin, solve_ising" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "整数集合が以下のように与えられる。値の和の差が0になるような二つの集合$A, B$に分割したい\n", + "\n", + "$S = \\{2, 4, 6\\}$\n", + "\n", + "スピン$s1,s2,s3 \\in \\{-1, 1\\}$を用意し、それぞれ、整数$2,4,6$がどちらの集合に属するかを表す。例えば、$s=1$のとき、その数は集合$A$に属し、$s=-1$のとき集合$B$に属する、と設定できる。" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# ハミルトニアンを記述\n", + "s1, s2, s3 = Spin(\"s1\"), Spin(\"s2\"), Spin(\"s3\")\n", + "H = (2*s1 + 4*s2 + 6*s3)**2" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# Isingモデルを得る\n", + "model = H.compile()\n", + "linear, quad, offset = model.to_ising()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'s1': -1, 's2': -1, 's3': 1}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Isingモデルを解く\n", + "solution = solve_ising(linear, quad)\n", + "solution" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pyqubo/__init__.py b/pyqubo/__init__.py new file mode 100644 index 00000000..d4fea46d --- /dev/null +++ b/pyqubo/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +import pyqubo.constraint +import pyqubo.core +import pyqubo.func +import pyqubo.logic +import pyqubo.tensor +import pyqubo.utils +from pyqubo.constraint import * +from pyqubo.core import * +from pyqubo.func import * +from pyqubo.logic import * +from pyqubo.tensor import * +from pyqubo.utils import * diff --git a/pyqubo/constraint.py b/pyqubo/constraint.py new file mode 100644 index 00000000..2380e1fd --- /dev/null +++ b/pyqubo/constraint.py @@ -0,0 +1,142 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .core import Constraint, UserDefinedExpress, Qbit + + +class NotConst(UserDefinedExpress): + """Constraint: Not(a) = b. + + Args: + a (:class:`Express`): expression to be binary + + b (:class:`Express`): expression to be binary + + label (str): label to identify the constraint + + Examples: + In this example, when the binary variables satisfy the constraint, + the energy is 0.0. On the other hand, when they break the constraint, + the energy is 1.0 > 0.0. + + >>> from pyqubo import NotConst, Qbit + >>> a, b = Qbit('a'), Qbit('b') + >>> exp = NotConst(a, b, 'not') + >>> model = exp.compile() + >>> model.energy({'a': 1, 'b': 0}, var_type='binary') + 0.0 + >>> model.energy({'a': 1, 'b': 1}, var_type='binary') + 1.0 + """ + + def __init__(self, a, b, label): + express = Constraint(2 * a * b - a - b + 1, label=label) + super(NotConst, self).__init__(express) + + +class AndConst(UserDefinedExpress): + """Constraint: AND(a, b) = c. + + Args: + a (:class:`Express`): expression to be binary + + b (:class:`Express`): expression to be binary + + c (:class:`Express`): expression to be binary + + label (str): label to identify the constraint + + Examples: + In this example, when the binary variables satisfy the constraint, + the energy is 0.0. On the other hand, when they break the constraint, + the energy is 1.0 > 0.0. + + >>> from pyqubo import AndConst, Qbit + >>> a, b, c = Qbit('a'), Qbit('b'), Qbit('c') + >>> exp = AndConst(a, b, c, 'and') + >>> model = exp.compile() + >>> model.energy({'a': 1, 'b': 0, 'c': 0}, var_type='binary') + 0.0 + >>> model.energy({'a': 0, 'b': 1, 'c': 1}, var_type='binary') + 1.0 + """ + + def __init__(self, a, b, c, label): + express = Constraint(a * b - 2 * (a + b) * c + 3 * c, label=label) + super(AndConst, self).__init__(express) + + +class OrConst(UserDefinedExpress): + """Constraint: OR(a, b) = c. + + Args: + a (:class:`Express`): expression to be binary + + b (:class:`Express`): expression to be binary + + c (:class:`Express`): expression to be binary + + label (str): label to identify the constraint + + Examples: + In this example, when the binary variables satisfy the constraint, + the energy is 0.0. On the other hand, when they break the constraint, + the energy is 1.0 > 0.0. + + >>> from pyqubo import OrConst, Qbit + >>> a, b, c = Qbit('a'), Qbit('b'), Qbit('c') + >>> exp = OrConst(a, b, c, 'or') + >>> model = exp.compile() + >>> model.energy({'a': 1, 'b': 0, 'c': 1}, var_type='binary') + 0.0 + >>> model.energy({'a': 0, 'b': 1, 'c': 0}, var_type='binary') + 1.0 + """ + + def __init__(self, a, b, c, label): + express = Constraint(a * b + (a + b) * (1 - 2 * c) + c, label=label) + super(OrConst, self).__init__(express) + + +class XorConst(UserDefinedExpress): + """Constraint: OR(a, b) = c. + + Args: + a (:class:`Express`): expression to be binary + + b (:class:`Express`): expression to be binary + + c (:class:`Express`): expression to be binary + + label (str): label to identify the constraint + + Examples: + In this example, when the binary variables satisfy the constraint, + the energy is 0.0. On the other hand, when they break the constraint, + the energy is 1.0 > 0.0. + + >>> from pyqubo import XorConst, Qbit + >>> a, b, c = Qbit('a'), Qbit('b'), Qbit('c') + >>> exp = XorConst(a, b, c, 'xor') + >>> model = exp.compile() + >>> model.energy({'a': 1, 'b': 0, 'c': 1, 'aux_xor': 0}, var_type='binary') + 0.0 + >>> model.energy({'a': 0, 'b': 1, 'c': 0, 'aux_xor': 0}, var_type='binary') + 1.0 + """ + + def __init__(self, a, b, c, label): + aux = Qbit("aux_"+label) + express = Constraint(2 * a * b - 2 * (a + b) * c - 4 * (a + b) * aux + 4 * aux * c + a + b + c + 4 * aux, label=label) + super(XorConst, self).__init__(express) diff --git a/pyqubo/core/__init__.py b/pyqubo/core/__init__.py new file mode 100644 index 00000000..61209a9a --- /dev/null +++ b/pyqubo/core/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pyqubo.core.express import * +from pyqubo.core.coefficient import * +from pyqubo.core.model import * +from pyqubo.core.binaryprod import * +from pyqubo.core.paramprod import * diff --git a/pyqubo/core/binaryprod.py b/pyqubo/core/binaryprod.py new file mode 100644 index 00000000..afad632d --- /dev/null +++ b/pyqubo/core/binaryprod.py @@ -0,0 +1,99 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from operator import mul, xor +from six.moves import reduce + + +class BinaryProd(object): + """A product of binary variables. + This class is used as a key of dictionary when you represent a polynomial as a dictionary. + + For example, a polynomial :math:`2ab + b + 2` is represented as + + .. code-block:: python + + {BinaryProd({'a', 'b'}): 2.0, BinaryProd({'b'}): 1.0, BinaryProd(set()): 2.0} + + This class represents product of binary variables. + Since :math:`a=a^2=a^3=...` where :math:`a` is binary, a product of binary variables + can be represented by a set of unique variables. + For example, :math:`aabbc` can be simplified as :math:`abc`. + For this reason, this class contains unique binary variables as a set. + + Note: + BinaryProd initialized with empty key corresponds to constant. + + Args: + keys (set[label]): set of variable labels. + """ + + JOINT_SYMBOL = "*" + CONST_STRING = "const" + + def __init__(self, keys): + assert isinstance(keys, set) + self.keys = keys + if keys: + self.cached_hash = reduce(xor, [hash(key) for key in self.keys]) + self.string = self.JOINT_SYMBOL.join(sorted(self.keys)) + + # When :obj:`keys` is empty set, this object represents constant. + else: + self.cached_hash = 0 + self.string = self.CONST_STRING + + def __hash__(self): + return self.cached_hash + + def __eq__(self, other): + if not isinstance(other, BinaryProd): + return False + else: + return self.keys == other.keys + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return self.string + + def is_constant(self): + """Returns whether this is constant or not. + + Returns: + bool + """ + if self.string == self.CONST_STRING: + return True + else: + return False + + def calc_product(self, dict_values): + """Returns the value of the product of binary variables. + + Args: + dict_values (dict[label, float]): value of binary variable. + + Returns: + float + """ + if self.is_constant(): + return 1.0 + else: + return reduce(mul, [dict_values[key] for key in self.keys]) + + @staticmethod + def merge_term_key(term_key1, term_key2): + return BinaryProd(term_key1.keys | term_key2.keys) diff --git a/pyqubo/core/coefficient.py b/pyqubo/core/coefficient.py new file mode 100644 index 00000000..4ff99690 --- /dev/null +++ b/pyqubo/core/coefficient.py @@ -0,0 +1,68 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class Coefficient: + """The value of QUBO as a function of :class:`Param`. + + Energy of QUBO is defined as :math:`E(\\mathbf{x}) = \\sum_{ij} a_{ij}x_{i}x_{j}`. + + If the expression contains :class:`Param`, QUBO in :class:`Model` is `half-compiled`. + `Half-compiled` means you need to specify the value of parameters to get the final QUBO. + Each coefficient :math:`a_{ij}` of half-compiled QUBO is defined as :class:`Coefficient`. + If you want to get the final value of :math:`a_{ij}`, you need to evaluate with `params`. + + Args: + terms (dict[:class:`ParamProd`, float]): polynomial function of :class:`Param`. + The labels in :class:`ParamProd` corresponds to labels of :class:`Param`. + + Example: + + For example, a polynomial :math:`2ab+2` is represented as + + >>> from pyqubo import Coefficient, ParamProd + >>> coeff = Coefficient({ParamProd({'a': 1, 'b': 1}): 2.0, ParamProd({}): 2.0}) + + If we specify the params as :math:`a=2, b=3`, then the evaluated value will be 14.0. + + >>> coeff.eval(params={'a': 2, 'b': 3}) + 14.0 + + """ + + def __init__(self, terms): + self.terms = terms + + def eval(self, params): + """Returns evaluated value with `params`. + + Args: + params (dict[str, float]): Parameters. + + Returns: + float + """ + if not params: + raise ValueError("No parameters are given. Specify the parameter.") + result = 0.0 + for term, value in self.terms.items(): + prod = value + for key, p in term.keys.items(): + if key in params: + prod *= params[key] ** p + else: + raise ValueError("{key} is not specified in params. " + "Set the value of {key}".format(key=key)) + result += prod + return result diff --git a/pyqubo/core/express.py b/pyqubo/core/express.py new file mode 100644 index 00000000..7ccbc0ef --- /dev/null +++ b/pyqubo/core/express.py @@ -0,0 +1,818 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import copy +from collections import defaultdict +from operator import or_, xor +from six.moves import reduce +import dimod +import six + +from .coefficient import Coefficient +from .model import Model, CompiledQubo +from .binaryprod import BinaryProd +from .paramprod import ParamProd + + +@six.add_metaclass(abc.ABCMeta) +class Express: + """Abstract class of pyqubo expression. + + All basic component class such as :class:`.Qbit`, :class:`.Spin` or :class:`.Add` + inherits :class:`.Express`. + + .. graphviz:: + + digraph { + graph [size="2.5, 2.5"] + node [shape=rl] + add [label=AddList] + qbit_a [label="Qbit(a)"] + qbit_b [label="Qbit(b)"] + mul_1 [label="Mul"] + mul_2 [label="Mul"] + num_1 [label="Num(1)"] + num_2 [label="Num(2)"] + add -> num_1 + add -> mul_1 + mul_1 -> mul_2 + mul_1 -> qbit_a + mul_2 -> qbit_b + mul_2 -> num_2 + } + + For example, an expression :math:`2ab+1` (where :math:`a, b` is :class:`Qbit` variable) is + represented by the binary tree above. + + Note: + This class is an abstract class of all component of expressions. + + Example: + We write mathematical expressions with objects such as :class:`Qbit` or :class:`Spin` + which inherit :class:`.Express`. + + >>> from pyqubo import Qbit + >>> a, b = Qbit("a"), Qbit("b") + >>> 2*a*b + 1 + (((Qbit(a)*Num(2))*Qbit(b))+Num(1)) + + """ + + CONST_TERM_KEY = BinaryProd(set()) + PROD_SYM = '*' + + def __init__(self): + pass + + def __rmul__(self, other): + """It is called when `other(number) - self`""" + return self.__mul__(other) + + def __mul__(self, other): + """It is called when `self * other(any object)`""" + return Mul(self, other) + + def __rsub__(self, other): + """It is called when `other(number) - self`""" + return AddList([Mul(self, -1), other]) + + def __sub__(self, other): + """It is called when `self - other(any object)`""" + if other == 0.0: + return self + else: + if isinstance(other, Express): + return self.__add__(Mul(other, -1)) + else: + return self.__add__(-other) + + def __radd__(self, other): + """It is called when `other(number) + self`""" + return self.__add__(other) + + def __add__(self, other): + """It is called when `self + other(any object)`""" + if other == 0.0: + return self + else: + return AddList([self, other]) + + def __pow__(self, order): + if int(order) == order and order >= 1: + return reduce(lambda a, b: Mul(a, b), order * [self]) + else: + raise ValueError("Power of {} th order cannot be done.".format(order)) + + def __div__(self, other): + """It is called when `self / other(any object)`""" + if not isinstance(other, Express): + return Mul(self, other ** -1) + else: + raise ValueError("Expression cannot be divided by Expression.") + + def __rdiv__(self, other): + """It is called when `other(number) / self`""" + raise ValueError("Expression cannot be divided by Expression.") + + def __truediv__(self, other): # pragma: no cover + """division in Python3""" + return self.__div__(other) + + def __rtruediv__(self, other): # pragma: no cover + """It is called when `other(number) / self`""" + return self.__rdiv__(other) + + def __neg__(self): + """Negative value of expression.""" + return Mul(self, Num(-1)) + + def __ne__(self, other): + return not self.__eq__(other) + + @abc.abstractmethod + def __repr__(self): # pragma: no cover + pass + + @abc.abstractmethod + def __eq__(self, other): # pragma: no cover + pass + + @abc.abstractmethod + def __hash__(self): # pragma: no cover + pass + + def compile(self, strength=5.0): + """Returns the compiled :class:`Model`. + + This method reduces the degree of the expression if the degree is higher than 2, + and convert it into :class:`.Model` which has information about QUBO. + + Args: + strength (float): The strength of the reduction constraint. + Insufficient strength can result in the binary quadratic model + not having the same minimizations as the polynomial. + + Returns: + :class:`Model`: The model compiled from the :class:`.Express`. + + Examples: + In this example, there is a higher order term :math:`abcd`. It is decomposed as + [[``a*d``, ``c``], ``b``] hierarchically and converted into QUBO. + By calling :func:`to_qubo()` of the :obj:`model`, we get the resulting QUBO. + + >>> from pyqubo import Qbit + >>> a, b, c, d = Qbit("a"), Qbit("b"), Qbit("c"), Qbit("d") + >>> model = (a*b*c + a*b*d).compile() + >>> pprint(model.to_qubo()) + ({('a', 'a'): 0.0, + ('a', 'a*b'): -10.0, + ('a', 'b'): 5.0, + ('a*b', 'a*b'): 15.0, + ('a*b', 'b'): -10.0, + ('a*b', 'c'): 1.0, + ('a*b', 'd'): 1.0, + ('b', 'b'): 0.0, + ('c', 'c'): 0.0, + ('d', 'd'): 0.0}, + 0.0) + """ + def compile_param_if_express(val): + if isinstance(val, Express): + return val._compile_param() + else: + return val + + # Constraint for AND(multiplier, multiplicand) = product + def binary_product(multiplier, multiplicand, product, weight): + return { + BinaryProd({product}): 3.0 * weight, + BinaryProd({multiplicand, product}): -2.0 * weight, + BinaryProd({multiplier, product}): -2.0 * weight, + BinaryProd({multiplier, multiplicand}): weight + } + + # When the label contains PROD_SYM, elements of products should be sorted + # such that the resulting label is uniquely determined. + def normalize_label(label): + s = Express.PROD_SYM + if s in label: + return s.join(sorted(label.split(s))) + else: + return label + + # Expand the expression to polynomial + expanded, const = Express._expand(self) + + # Make polynomials quadratic + offset = 0.0 + pubo = {} + for term_key, value in expanded.items(): + if term_key.is_constant(): + offset = value + else: + pubo[tuple(term_key.keys)] = value + bqm = dimod.make_quadratic(pubo, strength, dimod.BINARY) + bqm_qubo, bqm_offset = bqm.to_qubo() + + # Extracts product constrains + product_consts = {} + for (a, b), v in bqm.info['reduction'].items(): + prod = normalize_label(v['product']) + product_consts["AND({},{})={}".format(a, b, prod)]\ + = binary_product(a, b, prod, strength) + + # Normalize labels and compile values of the QUBO + compiled_qubo = {} + for (label1, label2), value in bqm_qubo.items(): + norm_label1 = normalize_label(label1) + norm_label2 = normalize_label(label2) + + # Sort the tuple of labels such that the created key is uniquely determined. + if norm_label2 > norm_label1: + label_key = (norm_label1, norm_label2) + else: + label_key = (norm_label2, norm_label1) + # Compile values of the QUBO + compiled_qubo[label_key] = compile_param_if_express(value) + compiled_qubo = CompiledQubo(compiled_qubo, compile_param_if_express(offset + bqm_offset)) + + # Merge structures + uniq_variables = Express._unique_vars(self) + structure = reduce(Express._merge_dict, [var.structure for var in uniq_variables]) + + return Model(compiled_qubo, structure, Express._merge_dict(const, product_consts)) + + def _compile_param(self): + expanded, _ = Express._expand_param(self) + return Coefficient(expanded) + + @staticmethod + def _merge_dict(dict1, dict2): + dict1_copy = copy.copy(dict1) + dict1_copy.update(dict2) + return dict1_copy + + @staticmethod + def _merge_dict_update(const1, const2): + const1.update(const2) + return const1 + + @staticmethod + def _unique_vars(exp): + if isinstance(exp, AddList): + return reduce(or_, [Express._unique_vars(term) for term in exp.terms]) + elif isinstance(exp, Mul): + return Express._unique_vars(exp.left) | Express._unique_vars(exp.right) + elif isinstance(exp, Add): + return Express._unique_vars(exp.left) | Express._unique_vars(exp.right) + elif isinstance(exp, Param): + return set() + elif isinstance(exp, Num): + return set() + elif isinstance(exp, Constraint): + return Express._unique_vars(exp.child) + elif isinstance(exp, Qbit): + return {exp} + elif isinstance(exp, Spin): + return {exp} + elif isinstance(exp, UserDefinedExpress): + return Express._unique_vars(exp.express) + else: + raise TypeError("Unexpected input type {}.".format(type(exp))) # pragma: no cover + + @staticmethod + def _merge_term(term1, term2): + if len(term1) < len(term2): + for k, v in term1.items(): + term2[k] += v + r = term2 + else: + for k, v in term2.items(): + term1[k] += v + r = term1 + return r + + @staticmethod + def _expand_param(exp): + """Expand the parameter expression hierarchically into dict format.""" + + if isinstance(exp, AddList): + expanded, const = reduce( + lambda arg1, arg2: + (Express._merge_term(arg1[0], arg2[0]), Express._merge_dict_update(arg1[1], arg2[1])), + [Express._expand_param(term) for term in exp.terms]) + return expanded, const + + elif isinstance(exp, Mul): + left, left_const = Express._expand_param(exp.left) + right, right_const = Express._expand_param(exp.right) + expanded_terms = defaultdict(float) + for k1, v1 in left.items(): + for k2, v2 in right.items(): + merged_key = ParamProd.merge_term_key(k1, k2) + expanded_terms[merged_key] += v1 * v2 + return expanded_terms, Express._merge_dict_update(left_const, right_const) + + elif isinstance(exp, Param): + expanded_terms = defaultdict(float) + expanded_terms[ParamProd({exp.label: 1.0})] = 1.0 + return expanded_terms, {} + + elif isinstance(exp, Num): + terms = defaultdict(float) + terms[ParamProd({})] = exp.value + return terms, {} + + else: + raise TypeError("Unexpected input type {}.".format(type(exp))) # pragma: no cover + + @staticmethod + def _expand(exp): + """Expand the expression hierarchically into dict format. + + For example, ``2*Qbit(a)*Qbit(b) + 1`` is represented as + dict format ``{BinaryProd(ab): 2.0, CONST_TERM_KEY: 1.0}``. + + Let's see how this dict is created step by step. + First, focus on `2*Qbit(a)*Qbit(b)` in which each expression is expanded as + _expand(Num(2)) # => {CONST_TERM_KEY: 2.0} + _expand(Qbit("a")) # => {BinaryProd(a): 1.0} + _expand(Qbit("b")) # => {BinaryProd(b): 1.0} + respectively. + + :class:`Mul` combines the expression in the following way + _expand(Mul(Num(2), Qbit("a"))) # => {BinaryProd(a): 2.0} + _expand(Mul(Qbit("b"), Mul(Num(2), Qbit("a")))) # => {BinaryProd(ab): 2.0} + + Finally, :class:`Add` combines the ``{BinaryProd(ab): 2.0}`` and `{CONST_TERM_KEY: 1.0}`, + and we get the final form {BinaryProd(ab): 2.0, CONST_TERM_KEY: 1.0} + + Args: + exp (:class:`Express`): Input expression. + + Returns: + tuple(expanded_terms, constraints): + tuple of expanded_terms and constrains. Expanded expression takes the form + ``dict[:class:`BinaryProd`, value]``. + Constraints takes the form of ``dict[label, expanded_terms]``. + + """ + + if isinstance(exp, Add): + left, left_const = Express._expand(exp.left) + right, right_const = Express._expand(exp.right) + result = Express._merge_term(right, left) + return result, Express._merge_dict_update(left_const, right_const) + + elif isinstance(exp, AddList): + expanded, const = reduce( + lambda arg1, arg2: + (Express._merge_term(arg1[0], arg2[0]), Express._merge_dict_update(arg1[1], arg2[1])), + [Express._expand(term) for term in exp.terms]) + return expanded, const + + elif isinstance(exp, Mul): + left, left_const = Express._expand(exp.left) + right, right_const = Express._expand(exp.right) + expanded_terms = defaultdict(float) + for k1, v1 in left.items(): + for k2, v2 in right.items(): + merged_key = BinaryProd.merge_term_key(k1, k2) + expanded_terms[merged_key] += v1 * v2 + return expanded_terms, Express._merge_dict_update(left_const, right_const) + + elif isinstance(exp, Param): + expanded_terms = defaultdict(float) + expanded_terms[Express.CONST_TERM_KEY] = exp + return expanded_terms, {} + + elif isinstance(exp, Constraint): + child, child_const = Express._expand(exp.child) + child_const[exp.label] = copy.copy(child) + return child, child_const + + elif isinstance(exp, Num): + terms = defaultdict(float) + terms[Express.CONST_TERM_KEY] = exp.value + return terms, {} + + elif isinstance(exp, Qbit): + terms = defaultdict(float) + terms[BinaryProd({exp.label})] = 1.0 + return terms, {} + + elif isinstance(exp, Spin): + terms = defaultdict(float) + terms[BinaryProd({exp.label})] = 2.0 + terms[Express.CONST_TERM_KEY] = -1.0 + return terms, {} + + elif isinstance(exp, UserDefinedExpress): + return Express._expand(exp.express) + + else: + raise TypeError("Unexpected input type {}.".format(type(exp))) # pragma: no cover + + +class UserDefinedExpress(Express): + """User Defined Express. + + User can define her/his own expression by inheriting :class:`UserDefinedExpress`. + + Attributes: + express (Express): User can define an original expression by defining this member. + + Example: + Define the :class:`LogicalAnd` class by inheriting :class:`UserDefinedExpress`. + + >>> from pyqubo import UserDefinedExpress + >>> class LogicalAnd(UserDefinedExpress): + ... def __init__(self, bit_a, bit_b): + ... express = bit_a * bit_b + ... super(LogicalAnd, self).__init__(express) + """ + + def __init__(self, express): + assert isinstance(express, Express) + super(UserDefinedExpress, self).__init__() + self.express = express + + def __hash__(self): + return hash(self.express) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + else: + return self.express == other.express + + def __repr__(self): + return "{}({})".format(self.__class__.__name__, self.express) + + +class Param(Express): + """Parameter expression. + + You can specify the value of the :class:`Param` when creating the QUBO. + By using :class:`Param`, you can change the value without compiling again. + This is useful when you need to update the strength of constraint gradually. + + Args: + label (str): The label of the parameter. + + Example: + The value of the parameter is specified when you call :func:`to_qubo`. + + >>> from pyqubo import Qbit, Param + >>> x, y, a = Qbit('x'), Qbit('y'), Param('a') + >>> exp = a*x*y + 2*x + >>> pprint(exp.compile().to_qubo(params={'a': 3})) + ({('x', 'x'): 2.0, ('x', 'y'): 3.0, ('y', 'y'): 0.0}, 0.0) + >>> pprint(exp.compile().to_qubo(params={'a': 5})) + ({('x', 'x'): 2.0, ('x', 'y'): 5.0, ('y', 'y'): 0.0}, 0.0) + """ + + def __init__(self, label): + super(Param, self).__init__() + self.label = label + + def __hash__(self): + return hash(self.label) + + def __eq__(self, other): + if not isinstance(other, Param): + return False + else: + return self.label == other.label + + def __repr__(self): + return "Param({})".format(self.label) + + +class Constraint(Express): + """Constraint expression. + + You can specify the constraint part in your expression. + + Args: + child (:class:`Express`): The expression you want to specify as a constraint. + + label (str): The label of the constraint. You can identify constraints by the label. + + Example: + When the solution is broken, `decode_solution` can detect it. + In this example, we introduce a constraint :math:`a+b=1`. + + >>> from pyqubo import Qbit, Constraint + >>> a, b = Qbit('a'), Qbit('b') + >>> exp = a + b + Constraint((a+b-1)**2, label="one_hot") + >>> model = exp.compile() + >>> sol, broken = model.decode_solution({'a': 1, 'b': 1}, var_type='binary') + >>> pprint(broken) + {'one_hot': {'penalty': 1.0, 'result': {'a': 1, 'b': 1}}} + >>> sol, broken = model.decode_solution({'a': 1, 'b': 0}, var_type='binary') + >>> pprint(broken) + {} + """ + + def __init__(self, child, label): + assert isinstance(label, str), "label should be string." + assert isinstance(child, Express), "child should be an Express instance." + super(Constraint, self).__init__() + self.child = child + self.label = label + + def __hash__(self): + return hash(self.label) ^ hash(self.child) + + def __eq__(self, other): + if not isinstance(other, Constraint): + return False + else: + return self.label == other.label and self.child == other.child + + def __repr__(self): + return "Const({}, {})".format(self.label, repr(self.child)) + + +class Spin(Express): + """Spin variable i.e. {-1, 1}. + + Args: + label (str): The label of a variable. A variable is identified by this label. + + structure (dict/optional): Variable structure. + + Example: + >>> from pyqubo import Spin + >>> a, b = Spin('a'), Spin('b') + >>> exp = 2*a*b + 3*a + >>> pprint(exp.compile().to_qubo()) + ({('a', 'a'): 2.0, ('a', 'b'): 8.0, ('b', 'b'): -4.0}, -1.0) + """ + + def __init__(self, label, structure=None): + assert isinstance(label, str), "label should be string." + assert Express.PROD_SYM not in label, "label should not contain {}".format(Express.PROD_SYM) + super(Spin, self).__init__() + if not structure: + # if no structure is given + self.structure = {label: (label,)} + else: + self.structure = structure + self.label = label + + def __repr__(self): + return "Spin({})".format(self.label) + + def __hash__(self): + return hash(self.label) + + def __eq__(self, other): + """Returns whether the label is same or not.""" + if not isinstance(other, Spin): + return False + else: + return self.label == other.label + + +class Qbit(Express): + """Binary variable i.e. {0, 1}. + + Args: + label (str): The label of a variable. A variable is identified by this label. + + structure (dict/optional): Variable structure. + + Example: + >>> from pyqubo import Qbit + >>> a, b = Qbit('a'), Qbit('b') + >>> exp = 2*a*b + 3*a + >>> pprint(exp.compile().to_qubo()) + ({('a', 'a'): 3.0, ('a', 'b'): 2.0, ('b', 'b'): 0.0}, 0.0) + """ + + def __init__(self, label, structure=None): + assert isinstance(label, str), "Label should be string." + assert Express.PROD_SYM not in label, "label should not contain {}".format(Express.PROD_SYM) + super(Qbit, self).__init__() + if not structure: + # if no structure is given + self.structure = {label: (label,)} + else: + self.structure = structure + self.label = label + + def __repr__(self): + return "Qbit({})".format(self.label) + + def __hash__(self): + return hash(self.label) + + def __eq__(self, other): + """Returns whether the label is same or not.""" + if not isinstance(other, Qbit): + return False + else: + return self.label == other.label + + +class Mul(Express): + """Product of expressions. + + Args: + left (:class:`Express`): An expression + + right (:class:`Express`): An expression + + Example: + You can multiply expressions with either the built-in operator or :class:`Mul`. + + >>> from pyqubo import Qbit, Mul + >>> a, b = Qbit('a'), Qbit('b') + >>> a * b + (Qbit(a)*Qbit(b)) + >>> Mul(a, b) + (Qbit(a)*Qbit(b)) + """ + + def __init__(self, left, right): + super(Mul, self).__init__() + # When right is a number (non-Express). (Only right can be a number.) + if isinstance(left, Express) and not isinstance(right, Express): + self.left = left + self.right = Num(right) + + # When both arguments are Express. + elif isinstance(left, Express) and isinstance(right, Express): + self.left = left + self.right = right + else: + raise ValueError("left should be an Express instance.") + + def __repr__(self): + return "({}*{})".format(repr(self.left), repr(self.right)) + + def __hash__(self): + return hash(self.left) ^ hash(self.right) + + def __eq__(self, other): + if not isinstance(other, Mul): + return False + elif self.left == other.left and self.right == other.right: + return True + elif self.right == other.left and self.left == other.right: + return True + else: + return False + + +class Add(Express): + """Addition of expressions (deprecated). + + Args: + left (:class:`Express`): An expression + + right (:class:`Express`): An expression + + Example: + You can add expressions with either the built-in operator or :class:`Add`. + + >>> from pyqubo import Qbit, Add + >>> a, b = Qbit('a'), Qbit('b') + >>> a + b + (Qbit(a)+Qbit(b)) + >>> Add(a, b) + (Qbit(a)+Qbit(b)) + + """ + + def __init__(self, left, right): + super(Add, self).__init__() + # When right is a number (non-Express). (Only right can be a number.) + if isinstance(left, Express) and not isinstance(right, Express): + self.left = left + self.right = Num(right) + + # When both arguments are Express. + elif isinstance(left, Express) and isinstance(right, Express): + self.left = left + self.right = right + else: + raise ValueError("left should be an Express instance.") + + def __hash__(self): + return hash(self.left) ^ hash(self.right) + + def __repr__(self): + return "({}+{})".format(repr(self.left), repr(self.right)) + + def __eq__(self, other): + if not isinstance(other, Add): + return False + elif self.left == other.left and self.right == other.right: + return True + elif self.right == other.left and self.left == other.right: + return True + else: + return False + + +class AddList(Express): + """Addition of a list of expressions. + + Args: + terms (list[:class:`Express`]): a list of expressions + + Example: + You can add expressions with either the built-in operator or :class:`AddList`. + + >>> from pyqubo import Qbit, AddList + >>> a, b = Qbit('a'), Qbit('b') + >>> a + b + (Qbit(a)+Qbit(b)) + >>> AddList([a, b]) + (Qbit(a)+Qbit(b)) + """ + + def __init__(self, terms): + super(AddList, self).__init__() + new_terms = [] + for term in terms: + if isinstance(term, Express): + new_terms.append(term) + else: + new_terms.append(Num(term)) + self.terms = new_terms + + def __add__(self, other): + """ Override __add__(). + + To prevent a deep nested expression, __add__() returns AddList object + where the added expression object is appended to :obj:`terms`. + + Returns: + :class:`AddList` + """ + if other == 0.0: + return self + else: + if not isinstance(other, Express): + other = Num(other) + return AddList(self.terms + [other]) + + def __hash__(self): + return reduce(xor, [hash(term) for term in self.terms]) + + def __repr__(self): + return "({})".format("+".join([repr(e) for e in self.terms])) + + def __eq__(self, other): + if not isinstance(other, AddList): + return False + else: + return set(self.terms) == set(other.terms) + + +class Num(Express): + """Expression of number + + Args: + value (float): the value of the number. + + Example: + >>> from pyqubo import Qbit, Num + >>> a = Qbit('a') + >>> a + 1 + (Qbit(a)+Num(1)) + >>> a + Num(1) + (Qbit(a)+Num(1)) + """ + def __init__(self, value): + super(Num, self).__init__() + self.value = value + + def __hash__(self): + return hash(self.value) + + def __repr__(self): + return "Num({})".format(str(self.value)) + + def __eq__(self, other): + if not isinstance(other, Num): + return False + else: + return self.value == other.value + diff --git a/pyqubo/core/model.py b/pyqubo/core/model.py new file mode 100644 index 00000000..a753b68f --- /dev/null +++ b/pyqubo/core/model.py @@ -0,0 +1,380 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from operator import or_ +import numpy as np +import dimod +from six.moves import reduce + + +class Model: + """Model represents binary quadratic optimization problem. + + By compiling :class:`Express` object, you get a :class:`Model` object. + It contains the information about QUBO (or equivalent Ising Model), + and it also has the function to decode the solution + into the original variable structure. + + Note: + We do not need to create this object directly. Instead, + we get this by compiling `Express` objects. + + Args: + compiled_qubo (:class:`.CompiledQubo`): + Half-compiled QUBO. If we want to get the final QUBO, we need to evaluate this QUBO + by passing :obj:`params`. See :func:`CompiledQubo.eval()`. + + structure (`dict[label, Tuple(key1, key2, key3, ...)]`): + It defines the mapping of the variable used in :func:`decode_solution`. + A solution of `label` is mapped to + :obj:`decoded_solution[key1][key2][key3][...]`. + For more details, see :func:`decode_solution()`. + + constraints (dict[label, polynomial_term]): + It contains constraints of the problem. `label` is each constraint name and + `polynomial_term` is corresponding polynomial which should be zero when the + constraint is satisfied. + + Attributes: + variable_order (list): + The list of labels. The order is corresponds to the index of QUBO or Ising model. + + index2label (dict[int, label]): + The dictionary which maps an index to a label. + + label2index (dict[label, index]): + The dictionary which maps a label to an index. + """ + + def __init__(self, compiled_qubo, structure, constraints): + self.compiled_qubo = compiled_qubo + self.structure = structure + self.constraints = constraints + self.variable_order = sorted(self.compiled_qubo.variables) + self.index2label = dict(enumerate(self.variable_order)) + self.label2index = {v: k for k, v in self.index2label.items()} + + def __repr__(self): + from pprint import pformat + return "Model({}, structure={})".format(repr(self.compiled_qubo), str(pformat(self.structure))) + + def _parse_solution(self, solution, var_type): + """Parse solutions. + + Args: + solution (list[bit]/dict[label, bit]/dict[index, bit]): + The solution returned from solvers. + + var_type (str): + Specify the variable type. "binary" or "spin". + + Returns: + dict[label, bit]: dictionary of label and binary bit. + """ + assert var_type in ("binary", "spin"), "var_type should be either 'binary' or 'spin'." + + if isinstance(solution, list) or isinstance(solution, np.ndarray): + if len(self.variable_order) != len(solution): + raise ValueError("Illegal solution. Length of the solution is different from" + "that of self.variable_order.") + dict_solution = dict(zip(self.variable_order, solution)) + elif isinstance(solution, dict): + + if set(solution.keys()) == set(self.variable_order): + dict_solution = solution + + elif set(solution.keys()) == set(range(len(self.variable_order))): + dict_solution = {self.index2label[index]: v for index, v in solution.items()} + + else: + raise ValueError("Illegal solution. The keys of the solution" + " should be same as self.variable_order") + else: + raise TypeError("Unexpected type of solution.") + + if var_type == "spin": + dict_solution = {k: (v + 1) / 2 for k, v in dict_solution.items()} + + return dict_solution + + def energy(self, solution, var_type, params=None): + """Returns energy of the solution. + + Args: + solution (list[bit]/dict[label, bit]/dict[index, bit]): + The solution returned from solvers. + + var_type (str): + Specify the variable type. "binary" or "spin". + + params (dict[str, float]): + Specify the parameter values. + + Returns: + float: energy of the solution. + """ + dict_solution = self._parse_solution(solution, var_type) + qubo, offset = self.to_qubo(params=params) + s = 0.0 + for (label1, label2), value in qubo.items(): + s += dict_solution[label1] * dict_solution[label2] * value + return s + offset + + def decode_solution(self, solution, var_type): + """Returns decoded solution. + + Args: + solution (list[bit]/dict[label, bit]/dict[index, bit]): + The solution returned from solvers. + + var_type (str): + Specify the input and output variable type. "binary" or "spin". + + Returns: + tuple(dict, dict): Tuple of the decoded solution and broken constraints. + Structure of this dict is defined by :obj:`structure`. + """ + + def put_value_with_keys(dict_body, keys, value): + for key in keys[:-1]: + if key not in dict_body: + dict_body[key] = {} + dict_body = dict_body[key] + dict_body[keys[-1]] = value + + def evaluate_constraint(constraint, dict_value): + e = 0.0 + for term_key, value in constraint.items(): + e += term_key.calc_product(dict_value) * value + return e + + decoded_solution = {} + + dict_bin_solution = self._parse_solution(solution, var_type) + + for label, bit in dict_bin_solution.items(): + if label in self.structure: + if var_type == "spin": + out_value = 2 * bit - 1 + elif var_type == "binary": + out_value = bit + else: # pragma: no cover + raise ValueError("var_type should be either 'binary' or 'spin'.") + put_value_with_keys(decoded_solution, self.structure[label], out_value) + + # Check satisfaction of constraints + broken_const = {} + for label, const in self.constraints.items(): + energy = evaluate_constraint(const, dict_bin_solution) + if energy > 0.0: + result_value = {var: dict_bin_solution[var] for var in + reduce(or_, [k.keys for k in const.keys()])} + broken_const[label] = {"result": result_value, "penalty": energy} + elif energy < 0.0: + raise ValueError("The energy of the constraint \"{label}\" is {energy}." + "But an energy of constraints should not be negative." + .format(label=label, energy=energy)) + + return decoded_solution, broken_const + + def to_qubo(self, index_label=False, params=None): + """Returns QUBO and energy offset. + + Args: + index_label (bool): + If true, the keys of returned QUBO are indexed with a positive integer number. + + params (dict[str, float]): + If the expression contains :class:`Param` objects, + you have to specify the value of them by :obj:`params`. + + Returns: + tuple(QUBO, float): Tuple of QUBO and energy offset. + QUBO takes the form of ``dict[(label, label), value]``. + + Examples: + This example creates the :obj:`model` from the expression, and + we get the resulting QUBO by calling :func:`model.to_qubo()`. + + >>> from pyqubo import Qbit + >>> x, y, z = Qbit("x"), Qbit("y"), Qbit("z") + >>> model = (x*y + y*z + 3*z).compile() + >>> pprint(model.to_qubo()) + ({('x', 'x'): 0.0, + ('x', 'y'): 1.0, + ('y', 'y'): 0.0, + ('y', 'z'): 1.0, + ('z', 'z'): 3.0}, + 0.0) + + If you want a QUBO which has index labels, specify the argument ``index_label=True``. + The mapping of the indices and the corresponding labels is + stored in :obj:`model.variable_order`. + + >>> pprint(model.to_qubo(index_label=True)) + ({(0, 0): 0.0, (0, 1): 1.0, (1, 1): 0.0, (1, 2): 1.0, (2, 2): 3.0}, 0.0) + >>> model.variable_order + ['x', 'y', 'z'] + + """ + + bqm = self.compiled_qubo.eval(params) + q, offset = bqm.to_qubo() + + # Evaluate values of QUBO + qubo = {} + for (label1, label2), v in q.items(): + if index_label: + i = self.label2index[label1] + j = self.label2index[label2] + else: + i = label1 + j = label2 + qubo[(i, j)] = v + + return qubo, offset + + def to_ising(self, index_label=False, params=None): + """Returns Ising Model and energy offset. + + Args: + index_label (bool): + If true, the keys of returned Ising model are + indexed with a positive integer number. + + params (dict[str, float]): + If the expression contains :class:`Param` objects, + you have to specify the value of them by :obj:`params`. + + Returns: + tuple(linear, quadratic, float): + Tuple of Ising Model and energy offset. Where `linear` takes the form of + ``(dict[label, value])``, and `quadratic` takes the form of + ``dict[(label, label), value]``. + + Examples: + This example creates the :obj:`model` from the expression, and + we get the resulting Ising model by calling :func:`model.to_ising()`. + + >>> from pyqubo import Qbit + >>> x, y, z = Qbit("x"), Qbit("y"), Qbit("z") + >>> model = (x*y + y*z + 3*z).compile() + >>> pprint(model.to_ising()) + ({'x': 0.25, 'y': 0.5, 'z': 1.75}, {('x', 'y'): 0.25, ('y', 'z'): 0.25}, 2.0) + + If you want a Ising model which has index labels, + specify the argument ``index_label=True``. + The mapping of the indices and the corresponding labels is + stored in :obj:`model.variable_order`. + + >>> pprint(model.to_ising(index_label=True)) + ({0: 0.25, 1: 0.5, 2: 1.75}, {(0, 1): 0.25, (1, 2): 0.25}, 2.0) + >>> model.variable_order + ['x', 'y', 'z'] + + """ + + bqm = self.compiled_qubo.eval(params) + linear, quadratic, offset = bqm.to_ising() + + # Construct linear + new_linear = {} + for label, v in linear.items(): + if index_label: + i = self.label2index[label] + else: + i = label + new_linear[i] = v + + # Construct quadratic + new_quadratic = {} + for (label1, label2), v in quadratic.items(): + if index_label: + i = self.label2index[label1] + j = self.label2index[label2] + else: + i = label1 + j = label2 + new_quadratic[(i, j)] = v + + return new_linear, new_quadratic, offset + + +class CompiledQubo: + """Half-compiled QUBO. + + Args: + qubo (dict[label, :class:`Coefficient`/float]): QUBO + + offset (:class:`Coefficient`/float): Offset of QUBO + + Attributes: + qubo (dict[label, :class:`Coefficient`/float]): QUBO + + offset (:class:`Coefficient`/float): Offset of QUBO + + This contains QUBO and the offset, but the value of the QUBO + has not been evaluated yet. To get the final QUBO, you need to + evaluate this QUBO by calling :func:`CompiledQubo.eval`. + """ + + def __init__(self, qubo, offset): + self.qubo = qubo + self.offset = offset + + @property + def variables(self): + """Unique labels contained in keys of QUBO.""" + return [i for (i, j) in self.qubo.keys() if i == j] + + def eval(self, params): + """Returns evaluated QUBO. + + Args: + params (dict[str, float]): Pass the value of the parameter. + + Returns: + :class:`BinaryQuadraticModel` + """ + evaluated_qubo = {} + for k, v in self.qubo.items(): + evaluated_qubo[k] = CompiledQubo._eval_if_not_float(v, params) + + evaluated_offset = CompiledQubo._eval_if_not_float(self.offset, params) + + return dimod.BinaryQuadraticModel.from_qubo( + evaluated_qubo, evaluated_offset) + + def __repr__(self): + from pprint import pformat + return "CompiledQubo({}, offset={})".format(pformat(self.qubo), self.offset) + + @staticmethod + def _eval_if_not_float(v, params): + """ If v is not float (i.e. v is :class:`Express`), returns an evaluated value. + + Args: + v (float/:class:`Coefficient`): + The value to be evaluated. + + params (dict[str, float]): + Parameters for evaluation. + + Returns: + float: Evaluated value of the input :obj:`v`: + """ + if isinstance(v, float): + return v + else: + return v.eval(params) diff --git a/pyqubo/core/paramprod.py b/pyqubo/core/paramprod.py new file mode 100644 index 00000000..be984418 --- /dev/null +++ b/pyqubo/core/paramprod.py @@ -0,0 +1,102 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from operator import mul, xor +from six.moves import reduce +import copy + + +class ParamProd(object): + """A product of parameter variables. + This class is used as a key of dictionary when you represent a polynomial as a dictionary. + + For example, a polynomial :math:`2a^2 b + 2` is represented as + + .. code-block:: python + + {ParamProd({'a': 2, 'b': 1}): 2.0, ParamProd({}): 2.0} + + Note: + ParamProd initialized with empty key corresponds to constant. + + Args: + keys (dict[label, int]): + dictionary with a key being label, a int value being order of power. + """ + + JOINT_SYMBOL = "*" + CONST_STRING = "const" + + def __init__(self, keys): + assert isinstance(keys, dict) + self.keys = keys + if keys: + self.cached_hash = reduce(xor, [hash(k) ^ hash(v) for k, v in self.keys.items()]) + self.is_const = False + + # When :obj:`keys` is empty dict, this object represents constant. + else: + self.cached_hash = 0 + self.is_const = True + + def __hash__(self): + return self.cached_hash + + def __eq__(self, other): + if not isinstance(other, ParamProd): + return False + else: + return self.keys == other.keys + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + if self.is_constant(): + return self.CONST_STRING + else: + return self.JOINT_SYMBOL.join(sorted(["{}^{}".format(k, v) + for k, v in self.keys.items()])) + + def is_constant(self): + """Returns whether this is constant or not. + + Returns: + bool + """ + return self.is_const + + def calc_product(self, dict_values): + """Returns the value of the product of binary variables. + + Args: + dict_values (dict[label, float]): value of binary variable. + + Returns: + float + """ + if self.is_constant(): + return 1.0 + else: + return reduce(mul, [dict_values[k] ** v for k, v in self.keys.items()]) + + @staticmethod + def merge_term_key(term_key1, term_key2): + term_key1_copy = copy.copy(term_key1.keys) + for k, v in term_key2.keys.items(): + if k in term_key1_copy: + term_key1_copy[k] += v + else: + term_key1_copy[k] = v + return ParamProd(term_key1_copy) diff --git a/pyqubo/func.py b/pyqubo/func.py new file mode 100644 index 00000000..964e3339 --- /dev/null +++ b/pyqubo/func.py @@ -0,0 +1,47 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .core import UserDefinedExpress + + +class Sum(UserDefinedExpress): + """Define sum of the expressions over sequent indices. + + Note: + Indices run from :obj:`start_index` to :obj:`end_index-1`. + + Args: + start_index (int): index to start with. + + end_index (int): index ends with end_index-1. + + func (function): function which takes integer as an argument and returns :class:`Express`. + + Example: + >>> from pyqubo import Sum, Vector + >>> x = Vector('x', n_dim=3) + >>> exp = (Sum(0, 3, lambda i: x[i]) - 1.0)**2 + >>> pprint(exp.compile().to_qubo()) + ({('x[0]', 'x[0]'): -1.0, + ('x[0]', 'x[1]'): 2.0, + ('x[0]', 'x[2]'): 2.0, + ('x[1]', 'x[1]'): -1.0, + ('x[1]', 'x[2]'): 2.0, + ('x[2]', 'x[2]'): -1.0}, + 1.0) + """ + + def __init__(self, start_index, end_index, func): + express = sum(func(i) for i in range(start_index, end_index)) + super(Sum, self).__init__(express) diff --git a/pyqubo/logic.py b/pyqubo/logic.py new file mode 100644 index 00000000..1542bbed --- /dev/null +++ b/pyqubo/logic.py @@ -0,0 +1,91 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .core import UserDefinedExpress + + +class Not(UserDefinedExpress): + """Logical NOT of input. + + Args: + bit (:class:`Express`): expression to be binary + + Examples: + >>> from pyqubo import Qbit, Not + >>> a = Qbit('a') + >>> exp = Not(a) + >>> model = exp.compile() + >>> for a in (0, 1): + ... print(a, int(model.energy({'a': a}, var_type='binary'))) + 0 1 + 1 0 + """ + + def __init__(self, bit): + express = 1 - bit + super(Not, self).__init__(express) + + +class And(UserDefinedExpress): + """Logical AND of inputs. + + Args: + bit_a (:class:`Express`): expression to be binary + + bit_b (:class:`Express`): expression to be binary + + Examples: + >>> from pyqubo import Qbit, And + >>> import itertools + >>> a, b = Qbit('a'), Qbit('b') + >>> exp = And(a, b) + >>> model = exp.compile() + >>> for a, b in itertools.product(*[(0, 1)] * 2): + ... print(a, b, int(model.energy({'a': a, 'b': b}, var_type='binary'))) + 0 0 0 + 0 1 0 + 1 0 0 + 1 1 1 + """ + + def __init__(self, bit_a, bit_b): + express = bit_a * bit_b + super(And, self).__init__(express) + + +class Or(UserDefinedExpress): + """Logical OR of inputs. + + Args: + bit_a (:class:`Express`): expression to be binary + bit_b (:class:`Express`): expression to be binary + + Examples: + + >>> from pyqubo import Qbit, Or + >>> import itertools + >>> a, b = Qbit('a'), Qbit('b') + >>> exp = Or(a, b) + >>> model = exp.compile() + >>> for a, b in itertools.product(*[(0, 1)] * 2): + ... print(a, b, int(model.energy({'a': a, 'b': b}, var_type='binary'))) + 0 0 0 + 0 1 1 + 1 0 1 + 1 1 1 + """ + + def __init__(self, bit_a, bit_b): + express = Not(And(Not(bit_a), Not(bit_b))) + super(Or, self).__init__(express) diff --git a/pyqubo/tensor.py b/pyqubo/tensor.py new file mode 100644 index 00000000..115da207 --- /dev/null +++ b/pyqubo/tensor.py @@ -0,0 +1,114 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .core import Spin, Qbit + + +class Matrix: + """Matrix of variables. + + Args: + name (str): Name of the matrix. It is used as a part of the label of variables. + For example, if the matrix name is 'x', + the label of `(i, j)` th variable will be ``x[i][j]``. + + n_row (int): Size of rows. + + n_col (int): Size of columns. + + spin (bool): If True, the element of the matrix is defined as :class:`Spin`. + + Examples: + Create a binary matrix with a size of 2 * 3. + + >>> from pyqubo import Matrix + >>> x = Matrix('x', 2, 3) + >>> x[0, 1] + x[1, 2] + (Qbit(x[0][1])+Qbit(x[1][2])) + """ + + def __init__(self, name, n_row, n_col, spin=False): + self.n_row = n_row + self.n_col = n_col + self.name = name + self.mat = {} + structure = self._create_structure() + for i in range(n_row): + for j in range(n_col): + if spin: + self.mat[(i, j)] = Spin(self._var_name(i, j), structure=structure) + else: + self.mat[(i, j)] = Qbit(self._var_name(i, j), structure=structure) + + def __getitem__(self, key): + i, j = key + if i < 0 or j < 0 or i >= self.n_row or j >= self.n_col: + raise IndexError + return self.mat[(i, j)] + + def _var_name(self, i, j): + return "{name}[{i}][{j}]".format(name=self.name, i=i, j=j) + + def _create_structure(self): + structure = dict() + for i in range(self.n_row): + for j in range(self.n_col): + structure[self._var_name(i, j)] = (self.name, i, j) + return structure + + +class Vector: + """Vector of variables. + + Args: + name (str): Name of the vector. It is used as a part of the label of variables. + For example, if the vector name is 'x', the label of `i` th variable will be ``x[i]``. + + n_dim (int): Size of the vector. + + spin (bool): If True, the element of the vector is defined as :class:`Spin`. + + Examples: + Create a binary vector with a size of 3. + + >>> from pyqubo import Vector + >>> x = Vector('x', 3) + >>> x[0] + x[2] + (Qbit(x[0])+Qbit(x[2])) + """ + + def __init__(self, name, n_dim, spin=False): + self.n_dim = n_dim + self.vec = {} + self.name = name + structure = self._create_structure() + for i in range(n_dim): + if spin: + self.vec[i] = Spin(self._var_name(i), structure=structure) + else: + self.vec[i] = Qbit(self._var_name(i), structure=structure) + + def __getitem__(self, i): + if i < 0 or i >= self.n_dim: + raise IndexError + return self.vec[i] + + def _var_name(self, i): + return "{name}[{i}]".format(name=self.name, i=i) + + def _create_structure(self): + structure = dict() + for i in range(self.n_dim): + structure[self._var_name(i)] = (self.name, i) + return structure diff --git a/pyqubo/utils/__init__.py b/pyqubo/utils/__init__.py new file mode 100644 index 00000000..d8c9f4be --- /dev/null +++ b/pyqubo/utils/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pyqubo.utils.solver import * +from pyqubo.utils.asserts import * diff --git a/pyqubo/utils/asserts.py b/pyqubo/utils/asserts.py new file mode 100644 index 00000000..cefeb068 --- /dev/null +++ b/pyqubo/utils/asserts.py @@ -0,0 +1,42 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def assert_qubo_equal(qubo1, qubo2): + """ Assert the given QUBOs are identical. + + Args: + qubo1 (dict[(label, label), float]): QUBO to be compared. + + qubo2 (dict[(label, label), float]): QUBO to be compared. + """ + + msg = "QUBO should be an dict instance." + assert isinstance(qubo1, dict), msg + assert isinstance(qubo2, dict), msg + + assert len(qubo1) == len(qubo2), "Number of elements in QUBO doesn't match." + + for (label1, label2), value in qubo1.items(): + if (label1, label2) in qubo2: + if qubo2[label1, label2] != value: + assert qubo2[label1, label2] == value,\ + "Value of {key} doesn't match".format(key=(label1, label2)) + elif (label2, label1) in qubo2: + if qubo2[label2, label1] != value: + assert qubo2[label2, label1] == value, \ + "Value of {key} doesn't match".format(key=(label2, label1)) + else: + raise AssertionError("Key: {key} of qubo1 isn't contained in qubo2" + .format(key=(label1, label2))) diff --git a/pyqubo/utils/solver.py b/pyqubo/utils/solver.py new file mode 100644 index 00000000..08e2c313 --- /dev/null +++ b/pyqubo/utils/solver.py @@ -0,0 +1,69 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dimod +import numpy as np + + +def solve_qubo(qubo, num_reads=10, num_sweeps=1000, beta_range=(1.0, 50.0)): + """Solve QUBO with Simulated Annealing (SA) provided by dimod. + + Args: + qubo (dict[(label, label), float]): The QUBO to be solved. + + num_reads (int, default=10): Number of run repetitions of SA. + + num_sweeps (int, default=1000): Number of iterations in each run of SA. + + beta_range (tuple(float, float), default=(1.0, 50.0)): Tuple of start beta and end beta. + + Returns: + dict[label, bit]: The solution of SA. + """ + max_abs_value = float(max(abs(v) for v in qubo.values())) + scale_qubo = {k: float(v) / max_abs_value for k, v in qubo.items()} + sa = dimod.reference.SimulatedAnnealingSampler() + sa_computation = sa.sample_qubo(scale_qubo, num_reads=num_reads, + num_sweeps=num_sweeps, beta_range=beta_range) + best = np.argmin(sa_computation.record.energy) + best_solution = list(sa_computation.record.sample[best]) + return dict(zip(sa_computation.variable_labels, best_solution)) + + +def solve_ising(linear, quad, num_reads=10, num_sweeps=1000, beta_range=(1.0, 50.0)): + """Solve Ising model with Simulated Annealing (SA) provided by dimod. + + Args: + linear (dict[label, float]): The linear parameter of the Ising model. + + quad (dict[(label, label), float]): The quadratic parameter of the Ising model. + + num_reads (int, default=10): Number of run repetitions of SA. + + num_sweeps (int, default=1000): Number of iterations in each run of SA. + + beta_range (tuple(float, float), default=(1.0, 50.0)): Tuple of start beta and end beta. + + Returns: + dict[label, bit]: The solution of SA. + """ + max_abs_value = float(max(abs(v) for v in (list(quad.values()) + list(linear.values())))) + scale_linear = {k: float(v) / max_abs_value for k, v in linear.items()} + scale_quad = {k: float(v) / max_abs_value for k, v in quad.items()} + sa = dimod.reference.SimulatedAnnealingSampler() + sa_computation = sa.sample_ising(scale_linear, scale_quad, num_reads=num_reads, + num_sweeps=num_sweeps, beta_range=beta_range) + best = np.argmin(sa_computation.record.energy) + best_solution = list(sa_computation.record.sample[best]) + return dict(zip(sa_computation.variable_labels, best_solution)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..45ed2ddd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +dimod==0.7.4 +numpy==1.15.1 +six==1.11.0 +coverage==4.5.1 +codecov==2.0.15 +nbsphinx==0.3.5 +ipykernel==4.9.0 diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..054f6ec0 --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +from __future__ import absolute_import +from setuptools import setup + +install_requires = ['dimod>=0.7.4', + 'numpy>=1.14.0,<2.0.0', + 'six>=1.10.0,<2.0.0'] + +packages = ['pyqubo', 'pyqubo.core', 'pyqubo.utils'] + +python_requires = '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' + +setup( + name='PyQUBO', + version='0.0.1', + author='Recruit Communications Co., Ltd.', + description='PyQUBO allows you to create QUBOs or Ising models' + 'from mathematical expressions.', + download_url='https://github.com/recruit-communications/pyqubo', + license='Apache 2.0', + packages=packages, + install_requires=install_requires, + include_package_data=True, + python_requires=python_requires +) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..ba399266 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/test/test_asserts.py b/test/test_asserts.py new file mode 100644 index 00000000..b619cfd4 --- /dev/null +++ b/test/test_asserts.py @@ -0,0 +1,31 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from pyqubo import assert_qubo_equal + + +class TestAsserts(unittest.TestCase): + + def test_qubo_equal(self): + qubo1 = {('a', 'b'): 1.0} + qubo2 = {('b', 'a'): 1.0} + qubo3 = {('a', 'b'): 2.0} + qubo4 = {('b', 'a'): 2.0} + qubo5 = {('c', 'a'): 1.0} + + assert_qubo_equal(qubo1, qubo2) + self.assertRaises(AssertionError, lambda: assert_qubo_equal(qubo1, qubo3)) + self.assertRaises(AssertionError, lambda: assert_qubo_equal(qubo1, qubo4)) + self.assertRaises(AssertionError, lambda: assert_qubo_equal(qubo1, qubo5)) diff --git a/test/test_binaryprod.py b/test/test_binaryprod.py new file mode 100644 index 00000000..0d4a53f3 --- /dev/null +++ b/test/test_binaryprod.py @@ -0,0 +1,59 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from pyqubo import BinaryProd + + +class TestBinaryProd(unittest.TestCase): + + def test_equality(self): + term_key1 = BinaryProd({"a", "b"}) + term_key2 = BinaryProd({"a", "b"}) + term_key3 = BinaryProd({"a", "b", "c"}) + self.assertTrue(term_key1 == term_key2) + self.assertTrue(term_key1 != term_key3) + self.assertTrue(term_key1 != 1) + + def test_equality_const(self): + term_key1 = BinaryProd(set()) + term_key2 = BinaryProd(set()) + term_key3 = BinaryProd({"a"}) + self.assertTrue(term_key1.is_constant()) + self.assertEqual(term_key1, term_key2) + self.assertNotEqual(term_key1, term_key3) + + def test_merge(self): + term_key1 = BinaryProd({"a", "b"}) + term_key2 = BinaryProd({"a", "b"}) + term_key3 = BinaryProd({"b", "c"}) + term_key4 = BinaryProd({"a", "b", "c"}) + self.assertEqual(term_key1, BinaryProd.merge_term_key(term_key1, term_key2)) + self.assertEqual(term_key4, BinaryProd.merge_term_key(term_key2, term_key3)) + + def test_evaluate(self): + term_key1 = BinaryProd({"a", "b"}) + empty_key = BinaryProd(set()) + dict_value = {"a": 3.0, "b": 5.0} + prod = term_key1.calc_product(dict_value) + expected_prod = 15 + self.assertEqual(expected_prod, prod) + self.assertEqual(1, empty_key.calc_product({})) + + def test_repr(self): + term_key1 = BinaryProd({"a", "b"}) + empty_key = BinaryProd(set()) + self.assertIn(repr(term_key1), "a*b") + self.assertEqual(repr(empty_key), "const") diff --git a/test/test_coefficient.py b/test/test_coefficient.py new file mode 100644 index 00000000..59e77741 --- /dev/null +++ b/test/test_coefficient.py @@ -0,0 +1,24 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from pyqubo import Coefficient, ParamProd + + +class TestCoefficient(unittest.TestCase): + + def test_coefficient_exception(self): + coeff = Coefficient({ParamProd({'a': 1, 'b': 1}): 2.0, ParamProd({}): 2.0}) + self.assertRaises(ValueError, lambda: coeff.eval({})) + self.assertRaises(ValueError, lambda: coeff.eval({'a': 1.0})) diff --git a/test/test_constraint.py b/test/test_constraint.py new file mode 100644 index 00000000..c408b402 --- /dev/null +++ b/test/test_constraint.py @@ -0,0 +1,76 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from pyqubo import Qbit, AndConst, OrConst, XorConst, NotConst + + +class TestConstraint(unittest.TestCase): + + def test_not(self): + a, b = Qbit("a"), Qbit("b") + exp = NotConst(a, b, label="not") + model = exp.compile() + self.assertTrue(model.energy({"a": 1, "b": 0}, var_type="binary") == 0) + self.assertTrue(model.energy({"a": 0, "b": 1}, var_type="binary") == 0) + self.assertTrue(model.energy({"a": 1, "b": 1}, var_type="binary") > 0) + self.assertTrue(model.energy({"a": 0, "b": 0}, var_type="binary") > 0) + + def test_and(self): + a, b, c = Qbit("a"), Qbit("b"), Qbit("c") + exp = AndConst(a, b, c, label="and") + model = exp.compile() + self.assertTrue(model.energy({"a": 1, "b": 1, "c": 1}, var_type="binary") == 0) + self.assertTrue(model.energy({"a": 1, "b": 0, "c": 0}, var_type="binary") == 0) + self.assertTrue(model.energy({"a": 0, "b": 1, "c": 0}, var_type="binary") == 0) + self.assertTrue(model.energy({"a": 0, "b": 0, "c": 0}, var_type="binary") == 0) + self.assertTrue(model.energy({"a": 1, "b": 1, "c": 0}, var_type="binary") > 0) + self.assertTrue(model.energy({"a": 0, "b": 0, "c": 1}, var_type="binary") > 0) + self.assertTrue(model.energy({"a": 1, "b": 0, "c": 1}, var_type="binary") > 0) + self.assertTrue(model.energy({"a": 0, "b": 1, "c": 1}, var_type="binary") > 0) + + def test_or(self): + a, b, c = Qbit("a"), Qbit("b"), Qbit("c") + exp = OrConst(a, b, c, label="or") + model = exp.compile() + self.assertTrue(model.energy({"a": 1, "b": 1, "c": 1}, var_type="binary") == 0) + self.assertTrue(model.energy({"a": 1, "b": 0, "c": 1}, var_type="binary") == 0) + self.assertTrue(model.energy({"a": 0, "b": 1, "c": 1}, var_type="binary") == 0) + self.assertTrue(model.energy({"a": 0, "b": 0, "c": 0}, var_type="binary") == 0) + self.assertTrue(model.energy({"a": 1, "b": 1, "c": 0}, var_type="binary") > 0) + self.assertTrue(model.energy({"a": 1, "b": 0, "c": 0}, var_type="binary") > 0) + self.assertTrue(model.energy({"a": 0, "b": 1, "c": 0}, var_type="binary") > 0) + self.assertTrue(model.energy({"a": 0, "b": 0, "c": 1}, var_type="binary") > 0) + + def test_xor(self): + a, b, c = Qbit("a"), Qbit("b"), Qbit("c") + exp = XorConst(a, b, c, label="xor") + model = exp.compile() + self.assertTrue(model.energy({"a": 1, "b": 1, "c": 0, "aux_xor": 1}, var_type="binary") == 0) + self.assertTrue(model.energy({"a": 1, "b": 0, "c": 1, "aux_xor": 0}, var_type="binary") == 0) + self.assertTrue(model.energy({"a": 0, "b": 1, "c": 1, "aux_xor": 0}, var_type="binary") == 0) + self.assertTrue(model.energy({"a": 1, "b": 1, "c": 0, "aux_xor": 1}, var_type="binary") == 0) + self.assertTrue(model.energy({"a": 0, "b": 0, "c": 1, "aux_xor": 1}, var_type="binary") > 0) + self.assertTrue(model.energy({"a": 1, "b": 1, "c": 1, "aux_xor": 1}, var_type="binary") > 0) + + def test_equality(self): + xor1 = XorConst(Qbit("a"), Qbit("b"), Qbit("c"), label="xor") + xor2 = XorConst(Qbit("a"), Qbit("b"), Qbit("c"), label="xor") + xor3 = XorConst(Qbit("b"), Qbit("c"), Qbit("a"), label="xor") + or1 = OrConst(Qbit("a"), Qbit("b"), Qbit("c"), label="xor") + self.assertTrue(xor1 + 1 == xor2 + 1) + self.assertTrue(xor1 == xor2) + self.assertFalse(xor1 == or1) + self.assertFalse(xor1 == xor3) diff --git a/test/test_express_compile.py b/test/test_express_compile.py new file mode 100644 index 00000000..2e67453f --- /dev/null +++ b/test/test_express_compile.py @@ -0,0 +1,214 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from pyqubo import Qbit, Spin, Param, Constraint, Vector, Matrix, Add, Sum +from pyqubo import assert_qubo_equal + + +class TestExpressCompile(unittest.TestCase): + + def compile_check(self, exp, expected_qubo, expected_offset, expected_structure, + params=None): + model = exp.compile(strength=5) + qubo, offset = model.to_qubo(params=params) + assert_qubo_equal(qubo, expected_qubo) + self.assertEqual(qubo, expected_qubo) + self.assertEqual(offset, expected_offset) + self.assertEqual(model.structure, expected_structure) + + def test_compile(self): + a, b = Qbit("a"), Qbit("b") + exp = 1 + a*b + a - 2 + expected_qubo = {('a', 'a'): 1.0, ('a', 'b'): 1.0, ('b', 'b'): 0.0} + expected_offset = -1 + expected_structure = {'a': ('a',), 'b': ('b',)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure) + + def test_compile_expand(self): + a, b = Qbit("a"), Qbit("b") + exp = (a+b)*(a-b) + expected_qubo = {('a', 'a'): 1.0, ('a', 'b'): 0.0, ('b', 'b'): -1.0} + expected_offset = 0.0 + expected_structure = {'a': ('a',), 'b': ('b',)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure) + + def test_compile_spin(self): + a, b = Qbit("a"), Spin("b") + exp = a * b + expected_qubo = {('a', 'a'): -1.0, ('a', 'b'): 2.0, ('b', 'b'): 0.0} + expected_offset = 0.0 + expected_structure = {'a': ('a',), 'b': ('b',)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure) + + def test_compile_2nd_order(self): + a, b = Qbit("a"), Qbit("b") + exp = (Add(a, b)-3)**2 + expected_qubo = {('a', 'a'): -5.0, ('a', 'b'): 2.0, ('b', 'b'): -5.0} + expected_offset = 9.0 + expected_structure = {'a': ('a',), 'b': ('b',)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure) + + def test_compile_3rd_order(self): + a, b = Qbit("a"), Qbit("b") + exp = (a+b-2)**3 + expected_qubo = {('a', 'a'): 7.0, ('a', 'b'): -6.0, ('b', 'b'): 7.0} + expected_offset = -8.0 + expected_structure = {'a': ('a',), 'b': ('b',)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure) + + def test_compile_reduce_degree(self): + a, b, c, d = Qbit("a"), Qbit("b"), Qbit("c"), Qbit("d") + exp = a * b * c + b * c * d + expected_qubo = { + ('a', 'a'): 0.0, + ('a', 'b*c'): 1.0, + ('b', 'b'): 0.0, + ('b', 'b*c'): -10.0, + ('b', 'c'): 5.0, + ('c', 'c'): 0.0, + ('b*c', 'c'): -10.0, + ('b*c', 'b*c'): 15.0, + ('b*c', 'd'): 1.0, + ('d', 'd'): 0.0} + expected_offset = 0.0 + expected_structure = {'a': ('a',), 'b': ('b',), 'c': ('c',), 'd': ('d',)} + + self.compile_check(exp, expected_qubo, expected_offset, expected_structure) + + def test_compile_div(self): + a, b = Qbit("a"), Qbit("b") + exp = (a+b-2)/2 + expected_qubo = {('a', 'a'): 0.5, ('b', 'b'): 0.5} + expected_offset = -1 + expected_structure = {'a': ('a',), 'b': ('b',)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure) + + def test_compile_param(self): + a, b, w, v = Qbit("a"), Qbit("b"), Param("w"), Param("v") + exp = w*(a+b-2) + v + expected_qubo = {('a', 'a'): 3.0, ('b', 'b'): 3.0} + expected_offset = -1 + expected_structure = {'a': ('a',), 'b': ('b',)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure, + params={"w": 3.0, "v": 5.0}) + + def test_compile_param2(self): + a, b, w, v = Qbit("a"), Qbit("b"), Param("w"), Param("v") + exp = v*w*(a+b-2) + v + expected_qubo = {('a', 'a'): 15.0, ('b', 'b'): 15.0} + expected_offset = -25 + expected_structure = {'a': ('a',), 'b': ('b',)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure, + params={"w": 3.0, "v": 5.0}) + + def test_compile_param3(self): + a, v = Qbit("a"), Param("v") + exp = v*v*a + v + expected_qubo = {('a', 'a'): 25.0} + expected_offset = 5 + expected_structure = {'a': ('a',)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure, + params={"v": 5.0}) + + def test_compile_const(self): + a, b, w = Qbit("a"), Qbit("b"), Param("w") + exp = Constraint(Constraint(w * (a + b - 1), label="const1") + Constraint((a + b - 1) ** 2, label="const2"), + label="const_all") + expected_qubo = {('a', 'a'): 2.0, ('a', 'b'): 2.0, ('b', 'b'): 2.0} + expected_offset = -2.0 + expected_structure = {'a': ('a',), 'b': ('b',)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure, + params={"w": 3.0}) + + def test_compile_vector(self): + x = Vector("x", n_dim=5) + a = Qbit("a") + exp = x[1] * (x[0] + 2*a + 1) + expected_qubo = { + ('a', 'a'): 0.0, + ('a', 'x[1]'): 2.0, + ('x[0]', 'x[0]'): 0.0, + ('x[0]', 'x[1]'): 1.0, + ('x[1]', 'x[1]'): 1.0} + expected_offset = 0.0 + expected_structure = { + 'a': ('a',), + 'x[0]': ('x', 0), + 'x[1]': ('x', 1), + 'x[2]': ('x', 2), + 'x[3]': ('x', 3), + 'x[4]': ('x', 4)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure) + + def test_compile_vector_spin(self): + x = Vector("x", n_dim=2, spin=True) + exp = x[1] * x[0] + x[0] + expected_qubo = {('x[1]', 'x[1]'): -2.0, ('x[0]', 'x[0]'): 0.0, ('x[0]', 'x[1]'): 4.0} + expected_offset = 0.0 + expected_structure = { + 'x[0]': ('x', 0), + 'x[1]': ('x', 1)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure) + + def test_compile_matrix(self): + x = Matrix("x", n_row=2, n_col=2) + a = Qbit("a") + exp = x[0, 1] * (x[1, 1] + 2*a + 1) + expected_qubo = { + ('a', 'a'): 0.0, + ('a', 'x[0][1]'): 2.0, + ('x[0][1]', 'x[0][1]'): 1.0, + ('x[0][1]', 'x[1][1]'): 1.0, + ('x[1][1]', 'x[1][1]'): 0.0} + expected_offset = 0.0 + expected_structure = { + 'a': ('a',), + 'x[0][0]': ('x', 0, 0), + 'x[0][1]': ('x', 0, 1), + 'x[1][0]': ('x', 1, 0), + 'x[1][1]': ('x', 1, 1)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure) + + def test_compile_matrix_spin(self): + x = Matrix("x", 2, 2, spin=True) + exp = x[1, 1] * x[0, 0] + x[0, 0] + expected_qubo = {('x[1][1]', 'x[1][1]'): -2.0, ('x[0][0]', 'x[0][0]'): 0.0, + ('x[0][0]', 'x[1][1]'): 4.0} + expected_offset = 0.0 + expected_structure = { + 'x[0][0]': ('x', 0, 0), + 'x[0][1]': ('x', 0, 1), + 'x[1][0]': ('x', 1, 0), + 'x[1][1]': ('x', 1, 1)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure) + + def test_index_error_vector(self): + x = Vector("x", n_dim=2) + self.assertRaises(IndexError, lambda: x[2]) + + x = Matrix("x", 2, 1) + self.assertRaises(IndexError, lambda: x[2, 2]) + + def test_sum(self): + x = Vector('x', 2) + exp = (Sum(0, 2, lambda i: x[i]) - 1) ** 2 + + expected_qubo = {('x[0]', 'x[0]'): -1.0, ('x[1]', 'x[1]'): -1.0, ('x[0]', 'x[1]'): 2.0} + expected_offset = 1.0 + expected_structure = { + 'x[0]': ('x', 0), + 'x[1]': ('x', 1)} + self.compile_check(exp, expected_qubo, expected_offset, expected_structure) diff --git a/test/test_express_equality.py b/test/test_express_equality.py new file mode 100644 index 00000000..786ba38e --- /dev/null +++ b/test/test_express_equality.py @@ -0,0 +1,140 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from pyqubo import Qbit, Spin, AddList, Mul, Add, Num, Param, Constraint + + +class TestExpressEquality(unittest.TestCase): + + def test_equality_of_add_list(self): + exp1 = AddList([Qbit("a"), Qbit("b")]) + exp2 = AddList([Qbit("b"), Qbit("a")]) + exp3 = AddList([Qbit("a"), Qbit("a")]) + self.assertTrue(exp1 == exp2) + self.assertTrue(hash(exp1) == hash(exp2)) + self.assertFalse(exp1 == exp3) + self.assertFalse(exp1 == Qbit("a")) + + def test_equality_of_add(self): + exp1 = Add(Qbit("a"), Qbit("b")) + exp2 = Add(Qbit("b"), Qbit("a")) + exp3 = Add(Qbit("a"), Qbit("a")) + exp4 = Add(Qbit("a"), Qbit("b")) + exp5 = Add(Qbit("a"), 1) + self.assertTrue(exp1 == exp2) + self.assertTrue(exp1 == exp4) + self.assertTrue(hash(exp1) == hash(exp2)) + self.assertTrue(exp1 != exp3) + self.assertFalse(exp1 == exp5) + self.assertFalse(exp1 == Qbit("a")) + self.assertFalse(exp1 == exp3) + self.assertEqual(repr(exp1), "(Qbit(a)+Qbit(b))") + + def test_equality_of_mul(self): + exp1 = Mul(Qbit("a"), Qbit("b")) + exp2 = Mul(Qbit("b"), Qbit("a")) + exp3 = Mul(Qbit("a"), Qbit("a")) + self.assertTrue(exp1 == exp2) + self.assertTrue(hash(exp1) == hash(exp2)) + self.assertFalse(exp1 == exp3) + self.assertTrue(exp1 != Qbit("a")) + + def test_equality_of_num(self): + self.assertTrue(Num(1) == Num(1)) + self.assertFalse(Num(1) == Num(2)) + self.assertFalse(Num(1) == Qbit("a")) + + def test_equality_of_param(self): + p1 = Param("p1") + p2 = Param("p2") + p3 = Param("p1") + self.assertTrue(p1 == p3) + self.assertTrue(hash(p1) == hash(p3)) + self.assertTrue(p1 != p2) + self.assertTrue(p1 != Qbit("a")) + + def test_equality_of_const(self): + c1 = Constraint(Qbit("a"), label="c1") + c2 = Constraint(Qbit("b"), label="c1") + c3 = Constraint(Qbit("a"), label="c3") + c4 = Constraint(Qbit("a"), label="c1") + self.assertTrue(c1 == c4) + self.assertFalse(c1 != c4) + self.assertTrue(hash(c1) == hash(c4)) + self.assertTrue(c1 != c2) + self.assertTrue(c1 != c3) + self.assertTrue(c1 != Qbit("a")) + + def test_equality_of_spin(self): + a, b, c = Spin("a"), Spin("b"), Spin("a") + self.assertTrue(a == c) + self.assertFalse(a != c) + self.assertTrue(hash(a) == hash(c)) + self.assertTrue(a != b) + self.assertTrue(a != Qbit("a")) + self.assertEqual(repr(a), "Spin(a)") + + def test_equality_of_qbit(self): + a, b, c = Qbit("a"), Qbit("b"), Qbit("a") + self.assertTrue(a == c) + self.assertFalse(a != c) + self.assertTrue(hash(a) == hash(c)) + self.assertTrue(a != b) + self.assertTrue(a != Spin("a")) + + def test_equality_of_express(self): + a, b = Qbit("a"), Qbit("b") + exp = a * b + 2*a - 1 + expected_exp = AddList([Mul(a, b), Num(-1.0), Mul(a, 2)]) + self.assertTrue(exp == expected_exp) + + def test_equality_sub(self): + a, b = Qbit("a"), Qbit("b") + exp = 1-a-b + expected_exp = AddList([Mul(a, -1), Num(1.0), Mul(b, -1)]) + self.assertTrue(exp == expected_exp) + self.assertTrue(exp - 0.0 == expected_exp) + + def test_equality_sub2(self): + a, b = Qbit("a"), Qbit("b") + exp = a-b-1 + expected_exp = AddList([a, Num(-1.0), Mul(b, -1)]) + self.assertTrue(exp == expected_exp) + + def test_equality_of_express_with_param(self): + a, b, p = Qbit("a"), Qbit("b"), Param("p") + exp = a + b - 1 + a * p + expected_exp = AddList([a, Num(-1.0), b, Mul(p, a)]) + self.assertTrue(exp == expected_exp) + + def test_equality_of_express_with_const(self): + a, b = Qbit("a"), Spin("b") + exp = a + b - 1 + Constraint(a * b, label="const") + expected_exp = AddList([a, Num(-1.0), b, Constraint(Mul(a, b), label="const")]) + self.assertTrue(exp == expected_exp) + + def test_repr(self): + a, b, p = Qbit("a"), Qbit("b"), Param("p") + exp = a + p - 1 + Constraint(a * b, label="const") + expected = "(Qbit(a)+Param(p)+Num(-1)+Const(const, (Qbit(a)*Qbit(b))))" + self.assertTrue(repr(exp) == expected) + + def test_express_error(self): + self.assertRaises(ValueError, lambda: 2 / Qbit("a")) + self.assertRaises(ValueError, lambda: Qbit("a") / Qbit("a")) + self.assertRaises(ValueError, lambda: Qbit("a") ** 1.5) + self.assertRaises(ValueError, lambda: Mul(1, Qbit("b"))) + self.assertRaises(ValueError, lambda: Add(1, Qbit("b"))) diff --git a/test/test_logic.py b/test/test_logic.py new file mode 100644 index 00000000..061d84d2 --- /dev/null +++ b/test/test_logic.py @@ -0,0 +1,46 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +import itertools +from pyqubo import Qbit, And, Or, Not + + +class TestLogic(unittest.TestCase): + + def test_and(self): + a, b = Qbit('a'), Qbit('b') + exp = And(a, b) + model = exp.compile() + for a, b in itertools.product(*[(0, 1)] * 2): + e = int(model.energy({'a': a, 'b': b}, var_type='binary')) + self.assertEqual(a*b, e) + + def test_or(self): + a, b = Qbit('a'), Qbit('b') + exp = Or(a, b) + model = exp.compile() + for a, b in itertools.product(*[(0, 1)] * 2): + e = int(model.energy({'a': a, 'b': b}, var_type='binary')) + self.assertEqual(int(a+b > 0), e) + + def test_not(self): + a = Qbit('a') + exp = Not(a) + model = exp.compile() + for a in [0, 1]: + e = int(model.energy({'a': a}, var_type='binary')) + self.assertEqual(1-a, e) + + self.assertEqual(repr(exp), "Not(((Qbit(a)*Num(-1))+Num(1)))") diff --git a/test/test_model.py b/test/test_model.py new file mode 100644 index 00000000..07b25541 --- /dev/null +++ b/test/test_model.py @@ -0,0 +1,139 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from pyqubo import Qbit, Matrix, Constraint, Param +from pyqubo import assert_qubo_equal + + +class TestModel(unittest.TestCase): + + def test_to_qubo(self): + a, b = Qbit("a"), Qbit("b") + exp = 1 + a * b + a - 2 + model = exp.compile() + qubo, offset = model.to_qubo() + assert_qubo_equal(qubo, {("a", "a"): 1.0, ("a", "b"): 1.0, ("b", "b"): 0.0}) + self.assertTrue(offset == -1) + + def test_to_qubo_with_index(self): + a, b = Qbit("a"), Qbit("b") + exp = 1 + a * b + a - 2 + model = exp.compile() + qubo, offset = model.to_qubo(index_label=True) + assert_qubo_equal(qubo, {(0, 0): 1.0, (0, 1): 1.0, (1, 1): 0.0}) + self.assertTrue(offset == -1) + + def test_to_ising(self): + a, b = Qbit("a"), Qbit("b") + exp = 1 + a * b + a - 2 + model = exp.compile() + linear, quad, offset = model.to_ising() + self.assertTrue(linear == {'a': 0.75, 'b': 0.25}) + assert_qubo_equal(quad, {('a', 'b'): 0.25}) + self.assertTrue(offset == -0.25) + + def test_to_ising_with_index(self): + a, b = Qbit("a"), Qbit("b") + exp = 1 + a * b + a - 2 + model = exp.compile() + linear, quad, offset = model.to_ising(index_label=True) + self.assertTrue(linear == {0: 0.75, 1: 0.25}) + assert_qubo_equal(quad, {(0, 1): 0.25}) + self.assertTrue(offset == -0.25) + + def test_decode(self): + x = Matrix("x", n_row=2, n_col=2) + exp = Constraint((x[1, 1] - x[0, 1]) ** 2, label="const") + model = exp.compile() + + # check __repr__ of CompiledQubo + expected_repr = "CompiledQubo({('x[0][1]', 'x[0][1]'): 1.0,\n" \ + " ('x[0][1]', 'x[1][1]'): -2.0,\n" \ + " ('x[1][1]', 'x[1][1]'): 1.0}, offset=0.0)" + self.assertEqual(repr(model.compiled_qubo), expected_repr) + + # check __repr__ of Model + expected_repr = "Model(CompiledQubo({('x[0][1]', 'x[0][1]'): 1.0,\n"\ + " ('x[0][1]', 'x[1][1]'): -2.0,\n"\ + " ('x[1][1]', 'x[1][1]'): 1.0}, offset=0.0), "\ + "structure={'x[0][0]': ('x', 0, 0),\n"\ + " 'x[0][1]': ('x', 0, 1),\n"\ + " 'x[1][0]': ('x', 1, 0),\n"\ + " 'x[1][1]': ('x', 1, 1)})" + self.assertEqual(repr(model), expected_repr) + + # when the constraint is not broken + + # type of solution is dict[label, bit] + decoded_sol, broken = model.decode_solution({'x[0][1]': 1.0, 'x[1][1]': 1.0}, + var_type="binary") + self.assertTrue(decoded_sol == {'x': {0: {1: 1}, 1: {1: 1}}}) + self.assertTrue(len(broken) == 0) + + # type of solution is list[bit] + decoded_sol, broken = model.decode_solution([1, 1], var_type="binary") + self.assertTrue(decoded_sol == {'x': {0: {1: 1}, 1: {1: 1}}}) + self.assertTrue(len(broken) == 0) + + # type of solution is dict[index_label(int), bit] + decoded_sol, broken = model.decode_solution({0: 1.0, 1: 1.0}, + var_type="binary") + self.assertTrue(decoded_sol == {'x': {0: {1: 1}, 1: {1: 1}}}) + self.assertTrue(len(broken) == 0) + + # when the constraint is broken + + # type of solution is dict[label, bit] + decoded_sol, broken = model.decode_solution({'x[0][1]': 0.0, 'x[1][1]': 1.0}, + var_type="binary") + self.assertTrue(decoded_sol == {'x': {0: {1: 0}, 1: {1: 1}}}) + self.assertTrue(len(broken) == 1) + + # type of solution is dict[label, spin] + decoded_sol, broken = model.decode_solution({'x[0][1]': 1.0, 'x[1][1]': -1.0}, + var_type="spin") + self.assertTrue(decoded_sol == {'x': {0: {1: 1}, 1: {1: -1}}}) + self.assertTrue(len(broken) == 1) + + # invalid solution + self.assertRaises(ValueError, lambda: model.decode_solution([1, 1, 1], var_type="binary")) + self.assertRaises(ValueError, lambda: model.decode_solution( + {'x[0][2]': 1.0, 'x[1][1]': 0.0}, var_type="binary")) + self.assertRaises(TypeError, lambda: model.decode_solution((1, 1), var_type="binary")) + + # invalid var_type + self.assertRaises(AssertionError, lambda: model.decode_solution([1, 1], var_type="sp")) + + def test_decode2(self): + x = Matrix("x", n_row=2, n_col=2) + exp = Constraint(-(x[1, 1] - x[0, 1]) ** 2, label="const") + model = exp.compile() + self.assertRaises(ValueError, lambda: model.decode_solution([1, 0], var_type="binary")) + + def test_params(self): + a, b, p = Qbit("a"), Qbit("b"), Param("p") + params = {'p': 2.0} + exp = p * (1 + a * b + a) + model = exp.compile() + qubo, offset = model.to_qubo(index_label=False, params=params) + dict_solution = {'a': 1, 'b': 0} + dict_energy = model.energy(dict_solution, var_type="binary", params=params) + list_solution = [1, 0] + list_energy = model.energy(list_solution, var_type="binary", params=params) + assert_qubo_equal(qubo, {('a', 'b'): 2.0, ('a', 'a'): 2.0, ('b', 'b'): 0.0}) + self.assertEqual(offset, 2.0) + self.assertTrue(dict_energy, 2.0) + self.assertTrue(list_energy, 2.0) diff --git a/test/test_paramprod.py b/test/test_paramprod.py new file mode 100644 index 00000000..216bb2c8 --- /dev/null +++ b/test/test_paramprod.py @@ -0,0 +1,59 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from pyqubo import ParamProd + + +class TestParamProd(unittest.TestCase): + + def test_equality(self): + term_key1 = ParamProd({"a": 1, "b": 2}) + term_key2 = ParamProd({"a": 1, "b": 2}) + term_key3 = ParamProd({"a": 2, "b": 1}) + self.assertTrue(term_key1 == term_key2) + self.assertTrue(term_key1 != term_key3) + self.assertTrue(term_key1 != 1) + + def test_equality_const(self): + term_key1 = ParamProd({}) + term_key2 = ParamProd({}) + term_key3 = ParamProd({'a': 1}) + self.assertTrue(term_key1.is_constant()) + self.assertEqual(term_key1, term_key2) + self.assertNotEqual(term_key1, term_key3) + + def test_merge(self): + term_key1 = ParamProd({"a": 1, "b": 2}) + term_key2 = ParamProd({"a": 1, "b": 1}) + term_key3 = ParamProd({"a": 2, "b": 3}) + term_key4 = ParamProd({}) + self.assertEqual(term_key3, ParamProd.merge_term_key(term_key1, term_key2)) + self.assertEqual(term_key1, ParamProd.merge_term_key(term_key1, term_key4)) + + def test_evaluate(self): + term_key1 = ParamProd({"a": 1, "b": 2}) + empty_key = ParamProd({}) + dict_value = {"a": 3.0, "b": 5.0} + prod = term_key1.calc_product(dict_value) + expected_prod = 75 + self.assertEqual(expected_prod, prod) + self.assertEqual(1, empty_key.calc_product({})) + + def test_repr(self): + term_key1 = ParamProd({"a": 2, "b": 3}) + empty_key = ParamProd({}) + self.assertIn(repr(term_key1), "a^2*b^3") + self.assertEqual(repr(empty_key), "const") diff --git a/test/test_solver.py b/test/test_solver.py new file mode 100644 index 00000000..ae29a7c6 --- /dev/null +++ b/test/test_solver.py @@ -0,0 +1,38 @@ +# Copyright 2018 Recruit Communications Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from pyqubo import solve_qubo, solve_ising, Spin + + +class TestSolver(unittest.TestCase): + + @staticmethod + def create_number_partition_model(): + s1, s2, s3 = Spin("s1"), Spin("s2"), Spin("s3") + H = (2 * s1 + 4 * s2 + 6 * s3) ** 2 + return H.compile() + + def test_solve_qubo(self): + model = TestSolver.create_number_partition_model() + qubo, offset = model.to_qubo() + solution = solve_qubo(qubo, num_reads=1, num_sweeps=10) + self.assertTrue(solution == {'s1': 0, 's2': 0, 's3': 1} or {'s1': 1, 's2': 1, 's3': 0}) + + def test_solve_ising(self): + model = TestSolver.create_number_partition_model() + linear, quad, offset = model.to_ising() + solution = solve_ising(linear, quad, num_reads=1, num_sweeps=10) + self.assertTrue(solution == {'s1': -1, 's2': -1, 's3': 1} or {'s1': 1, 's2': 1, 's3': -1})