diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..4255a68 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,47 @@ +version: 2.1 + +orbs: + python: circleci/python@0.2.1 + aws-cli: circleci/aws-cli@2.0 + jq: circleci/jq@2.2 + +jobs: + + build: + executor: python/default + + steps: + - checkout + + publish_to_pypi: + executor: python/default + + steps: + - checkout + - jq/install + - aws-cli/install + - run: + name: Source PyPi creds + command: | + echo "export PYPI_PASSWORD=$(aws secretsmanager get-secret-value --secret-id admin/cicd/circleci/context/pypi | jq -r '.SecretString | fromjson.password')" >> $BASH_ENV + echo "export PYPI_USERNAME=$(aws secretsmanager get-secret-value --secret-id admin/cicd/circleci/context/pypi | jq -r '.SecretString | fromjson.username')" >> $BASH_ENV + - run: + name: "publish to pypi" + command: | + sudo pip3 install --upgrade pip + curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - + poetry build + poetry publish -u $PYPI_USERNAME -p $PYPI_PASSWORD + +workflows: + + on_commit: + jobs: + - build + - publish_to_pypi: + context: aws + filters: + branches: + ignore: /.*/ + tags: + only: /^v.*/ diff --git a/.gitignore b/.gitignore index cc368d7..ff12d46 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,127 @@ -.idea -__pycache__ -*.pyc \ No newline at end of file +# 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/ +pip-wheel-metadata/ +share/python-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/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +*venv* + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +.idea* diff --git a/.gitmodules b/.gitmodules index 6531a8c..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "platform_client_server_utils"] - path = platform_client_server_utils - url = https://github.com/qcware/platform_client_server_utils.git diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..3ddb5ca --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,18 @@ +version: 2 + +build: + image: testing + +python: + version: 3.9 + install: + - requirements: doc_requirements.txt + - requirements: docs/source/requirements.txt + - method: pip + path: . + +sphinx: + configuration: docs/source/conf.py + +submodules: + exclude: all diff --git a/LICENSE.txt b/LICENSE.txt index dada3a0..5412d2b 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1 +1 @@ -Copyright 2017 QC Ware Corp. All rights reserved. \ No newline at end of file +Copyright 2017 QC Ware Corp. All rights reserved. diff --git a/README.md b/README.md deleted file mode 100644 index c07ae55..0000000 --- a/README.md +++ /dev/null @@ -1,6 +0,0 @@ -![logo](https://qcware.com/img/qc-ware-logo.png) - -# QC Ware Platform Client Library (Python) - -This package contains functions for easily interfacing with the QC Ware -Platform from Python. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..9386896 --- /dev/null +++ b/README.rst @@ -0,0 +1,109 @@ + + +.. image:: http://qcwareco.wpengine.com/wp-content/uploads/2019/08/qc-ware-logo-11.png + :alt: logo + + +======================================== +Forge Client Library +======================================== + +This package contains functions for easily interfacing with Forge. + + +.. image:: https://badge.fury.io/py/qcware.svg + :target: https://badge.fury.io/py/qcware + :alt: PyPI version + +.. image:: https://pepy.tech/badge/qcware + :target: https://pepy.tech/project/qcware + :alt: Downloads + +.. image:: https://pepy.tech/badge/qcware/month + :target: https://pepy.tech/project/qcware/month + :alt: Downloads + +.. image:: https://circleci.com/gh/qcware/platform_client_library_python.svg?style=svg + :target: https://circleci.com/gh/qcware/platform_client_library_python + :alt: CircleCI + +.. image:: https://readthedocs.org/projects/qcware/badge/?version=latest + :target: https://qcware.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + +| + +Installation +============ + +To install with pip: + +.. code:: shell + + pip install qcware + +To install from source, you must first install `poetry `_. +Then, execute the following: + +.. code:: shell + + git clone https://github.com/qcware/platform_client_library_python.git + cd platform_client_library_python + poetry build + cd dist + pip install qcware-7.0.0-py3-none-any.whl + + +API Key +======= + +To use the client library, you will need an API key. You can sign up for one at `https://forge.qcware.com `__. + +To access your API key, log in to `Forge `_ and navigate to the API page. Your API key should be plainly visible there. + + +A Tiny Program +============== + +The following code snippet illustrates how you might run Forge client code locally. Please make sure that you have installed the client library and obtained an API key before running the Python code presented below. + +.. code:: python + + # configuration + from qcware.forge.config import set_api_key, set_host + set_api_key('YOUR-API-KEY-HERE') + set_host('https://api.forge.qcware.com') + + # specify the problem (for more details, see the "Getting Started" Jupyter notebook on Forge) + from qcware.forge import optimization + from qcware.types import PolynomialObjective, Constraints, BinaryProblem + + qubo = { + (0, 0): 1, + (0, 1): 1, + (1, 1): 1, + (1, 2): 1, + (2, 2): -1 + } + + qubo_objective = PolynomialObjective( + polynomial=qubo, + num_variables=3, + domain='boolean' + ) + + # run a CPU-powered brute force solution + results = optimization.brute_force_minimize( + objective=qubo_objective, + backend='qcware/cpu' + ) + print(results) + +If the client code has been properly installed and configured, the above code should display a result similar to the following: + +.. code:: shell + + Objective value: -1 + Solution: [0, 0, 1] + +For further guidance on running client code to solve machine learning problems, optimization problems, and more, please read through the documentation made available at `https://qcware.readthedocs.io `_ as well as the Jupyter notebooks made available on `Forge `__. diff --git a/SOURCE_ROOT b/SOURCE_ROOT new file mode 100644 index 0000000..e69de29 diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 8d6d64d..0000000 --- a/circle.yml +++ /dev/null @@ -1,16 +0,0 @@ -machine: - timezone: - America/Los_Angeles - -dependencies: - pre: - - sudo apt-get -y update - - python setup.py install - override: - - pip install -r requirements.txt - -test: - override: - - pep8 --config=pep8.cfg . --exclude=platform_client_server_utils,qcware/params_pb2.py,./build/lib/qcware/params_pb2.py - - mkdir -p $CIRCLE_TEST_REPORTS/unit/ - - pytest tests.py --junitxml=$CIRCLE_TEST_REPORTS/unit/pytest.xml diff --git a/doc_requirements.txt b/doc_requirements.txt new file mode 100644 index 0000000..6d60540 --- /dev/null +++ b/doc_requirements.txt @@ -0,0 +1,7 @@ +backoff==1.11.1 +icontract==2.5.4 +networkx==2.6.3 +numpy==1.21.2 +python-decouple==3.4 +qcware-quasar==1.0.6 +requests==2.26.0 diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..6247f7e --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +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% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..4b4fe5b --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,131 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +print(os.path.abspath("../..")) +sys.path.insert(0, os.path.abspath("../..")) +import qcware.forge + +# -- Project information ----------------------------------------------------- + +project = "qcware" +copyright = "2017, QC Ware Corp" +author = "Bryan E. Burr (bryan@qcware.com)" +master_doc = "index" +version = qcware.forge.__version__ + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# -- 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" + +# 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'] + +html_theme_options = {"collapse_navigation": False} + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = "qcwaredoc" + +# -- 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, "qcware.tex", "qcware Documentation", "QC Ware, Corp.", "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, "qcware", "qcware 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, + "qcware", + "qcware Documentation", + author, + "qcware", + "The Python client library for the QC Ware Platform.", + "Miscellaneous", + ), +] + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# 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 ------------------------------------------------- diff --git a/docs/source/copyright.rst b/docs/source/copyright.rst new file mode 100644 index 0000000..701f4b2 --- /dev/null +++ b/docs/source/copyright.rst @@ -0,0 +1,6 @@ +Copyright +========= + +support@qcware.com + +.. include:: ../../LICENSE.txt diff --git a/docs/source/genindex.rst b/docs/source/genindex.rst new file mode 100644 index 0000000..c07da40 --- /dev/null +++ b/docs/source/genindex.rst @@ -0,0 +1,4 @@ +.. This file is a placeholder and will be replaced + +Index +##### diff --git a/docs/source/get-started/quickstart.rst b/docs/source/get-started/quickstart.rst new file mode 100644 index 0000000..3625571 --- /dev/null +++ b/docs/source/get-started/quickstart.rst @@ -0,0 +1,87 @@ +Quickstart +========== + +The quickest way to get started with the Forge client library is to `register `_ as a Forge user and use our hosted `Jupyter `_ environment. Jupyter notebooks hosted on our service are pre-configured for Forge client library usage. **No additional setup required.** + +If you would like to work with the Forge client library locally, read on. + +.. _LocalInstall: + +Installation +------------ + +To install with pip: + +.. code:: shell + + pip install qcware + +To install from source, you must first install `poetry `_. +Then, execute the following: + +.. code:: shell + + git clone https://github.com/qcware/platform_client_library_python.git + cd platform_client_library_python + poetry build + cd dist + pip install qcware-7.0.0-py3-none-any.whl + + +API Key +------- + +To use the Forge client library, you will need an API key. You can sign up for one at `https://forge.qcware.com `__. + +To access your API key, log in to `Forge `_ and navigate to the API page. Your API key should be plainly visible there. + + +A Tiny Program +-------------- + +The following code snippet illustrates how you might run Forge client code locally. Please make sure that you have installed the client library and obtained an API key before running the Python code presented below. + +.. code:: python + + # configuration + from qcware.forge.config import set_api_key, set_host + set_api_key('YOUR-API-KEY-HERE') + set_host('https://api.forge.qcware.com') + + # specify the problem (for more details, see the "Getting Started" Jupyter notebook on Forge) + from qcware.forge import optimization + from qcware.types.optimization import PolynomialObjective, Constraints, BinaryProblem + + qubo = { + (0, 0): 1, + (0, 1): 1, + (1, 1): 1, + (1, 2): 1, + (2, 2): -1 + } + + qubo_objective = PolynomialObjective( + polynomial=qubo, + num_variables=3, + domain='boolean' + ) + + # run a CPU-powered brute force solution + results = optimization.brute_force_minimize( + objective=qubo_objective, + backend='qcware/cpu' + ) + print(results) + +If the client code has been properly installed and configured, the above code should display a result similar to the following: + +.. code:: shell + + Objective value: -1 + Solution: [0, 0, 1] + + +Next Steps +---------- + +For further guidance on running client code to solve machine learning problems, optimization problems, and more, we encourage you to read through the documentation on this site and the Jupyter notebooks made available on `Forge `__. diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..da6cb9c --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,85 @@ +.. qcware documentation master file, created by + sphinx-quickstart on Tue Aug 20 12:53:08 2019. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + + +.. image:: http://qcwareco.wpengine.com/wp-content/uploads/2019/08/qc-ware-logo-11.png + :target: https://qcware.com/ + :alt: logo + +=========================================== +Forge: Your Algorithms on Quantum Computers +=========================================== + +.. image:: https://badge.fury.io/py/qcware.svg + :target: https://badge.fury.io/py/qcware + :alt: PyPI version + +.. image:: https://pepy.tech/badge/qcware + :target: https://pepy.tech/project/qcware + :alt: Downloads + +.. image:: https://pepy.tech/badge/qcware/month + :target: https://pepy.tech/project/qcware/month + :alt: Downloads + +.. image:: https://circleci.com/gh/qcware/platform_client_library_python.svg?style=svg + :target: https://circleci.com/gh/qcware/platform_client_library_python + :alt: CircleCI + +.. image:: https://readthedocs.org/projects/qcware/badge/?version=latest + :target: https://qcware.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + +Welcome! The documentation on this site will help you understand and use the Forge client library. + +Forge is a service that lets you develop and run quantum software. An interface to this service is exposed to Python developers via the Forge client library. + + +.. toctree:: + :maxdepth: 2 + :caption: Get Started + :hidden: + + get-started/quickstart + +.. toctree:: + :maxdepth: 2 + :caption: Learn + :hidden: + +.. toctree:: + :maxdepth: 2 + :caption: Quantum Circuits + :hidden: + + Overview + I/O + Utils + +.. toctree:: + :maxdepth: 2 + :caption: Problem Classes + :hidden: + + Demos + Machine Learning + Monte Carlo + Optimization + + +.. toctree:: + :maxdepth: 2 + :caption: Reference + :hidden: + + Config + +.. toctree:: + :maxdepth: 2 + :caption: About + :hidden: + + copyright + genindex diff --git a/docs/source/reference/circuits.rst b/docs/source/reference/circuits.rst new file mode 100644 index 0000000..d6f2861 --- /dev/null +++ b/docs/source/reference/circuits.rst @@ -0,0 +1,49 @@ +Quantum Circuit Programming +=========================== + +Forge has a number of facilities for "traditional" circuit-model quantum machine +programming using our Quasar library for building and executing quantum circuits. + +The most current version of Quasar is present on our jupyter notebook server. +To use quasar to build a circuit, you can do something like the following:: + + >>> from quasar import Circuit + >>> bell_pair = Circuit() + >>> bell_pair.H(0).CX(0,1) + + >>> print(bell_pair) + T : |0|1| + q0 : -H-@- + | + q1 : ---X- + T : |0|1| + + +You can then run the circuit by using +the quasar forge backend. + +Using the Quasar backend: +``````````````````````````````` + +The `QuasarBackend` class provides a more integrated backend to Quasar using Forge +as a platform. You can import the class from `qcware.forge.circuits.quasar_backend` +and use it as shown below:: + + >>> from qcware.forge.circuits.quasar_backend import QuasarBackend + >>> backend = QuasarBackend("qcware/cpu_simulator") + >>> backend.has_run_statevector + True + >>> backend.run_statevector(circuit=bell_pair) + array([0.70710678+0.j, 0. +0.j, 0. +0.j, 0.70710678+0.j]) + >>> phist = backend.run_measurement(circuit=bell_pair, nmeasurement=1000) + >>> p + + >>> print(p) + nqubit : 2 + nmeasurement : None + |00> : 0.500000 + |11> : 0.500000 + + +The `QuasarBackend` class supports all the features of the Quasar backends +(please see the Quasar documentation `here `_ ) diff --git a/docs/source/reference/classification.rst b/docs/source/reference/classification.rst new file mode 100644 index 0000000..33468fe --- /dev/null +++ b/docs/source/reference/classification.rst @@ -0,0 +1,8 @@ +Quantum Classification for Machine Learning +=========================================== + +Forge provides the beginnings of quantum machine learning in the form of classification +algorithms for fitting data points to a classification model, and predicting the classification +of further data points in a unified function. + +.. autofunction:: qcware.forge.qml.fit_and_predict diff --git a/docs/source/reference/config.rst b/docs/source/reference/config.rst new file mode 100644 index 0000000..a532560 --- /dev/null +++ b/docs/source/reference/config.rst @@ -0,0 +1,53 @@ +Config +====== + +Reference document for the functions and classes users may use to configure Forge. + + +Functions +--------- + +.. autofunction:: qcware.forge.config.additional_config +.. autofunction:: qcware.forge.config.async_interval_between_tries +.. autofunction:: qcware.forge.config.client_api_incompatibility_message +.. autofunction:: qcware.forge.config.client_api_semver +.. autofunction:: qcware.forge.config.client_timeout +.. autofunction:: qcware.forge.config.do_client_api_compatibility_check +.. autofunction:: qcware.forge.config.do_client_api_compatibility_check_once +.. autofunction:: qcware.forge.config.host_api_semver +.. autofunction:: qcware.forge.config.ibmq_credentials +.. autofunction:: qcware.forge.config.is_valid_host_url +.. autofunction:: qcware.forge.config.ibmq_credentials_from_ibmq +.. autofunction:: qcware.forge.config.qcware_api_key +.. autofunction:: qcware.forge.config.qcware_host +.. autofunction:: qcware.forge.config.scheduling_mode +.. autofunction:: qcware.forge.config.server_timeout +.. autofunction:: qcware.forge.config.set_api_key +.. autofunction:: qcware.forge.config.set_async_interval_between_tries +.. autofunction:: qcware.forge.config.set_client_timeout +.. autofunction:: qcware.forge.config.set_environment_environment +.. autofunction:: qcware.forge.config.set_environment_source_file +.. autofunction:: qcware.forge.config.set_host +.. autofunction:: qcware.forge.config.set_ibmq_credentials +.. autofunction:: qcware.forge.config.set_ibmq_credentials_from_ibmq_provider +.. autofunction:: qcware.forge.config.set_server_timeout +.. autofunction:: qcware.forge.config.set_scheduling_mode + + +Classes +------- + +.. autoclass:: qcware.forge.config.ApiCallContext + :members: + +.. autoclass:: qcware.forge.config.ApiCredentials + :members: + +.. autoclass:: qcware.forge.config.Environment + :members: + +.. autoclass:: qcware.forge.config.IBMQCredentials + :members: + +.. autoclass:: qcware.forge.config.SchedulingMode + :members: diff --git a/docs/source/reference/demos.rst b/docs/source/reference/demos.rst new file mode 100644 index 0000000..036fb98 --- /dev/null +++ b/docs/source/reference/demos.rst @@ -0,0 +1,45 @@ +Demos +===== + +A preponderance of Forge client library functionality is demonstrated in example Jupyter notebooks. + +The quickest way to access the example Jupyter notebooks is to `register `_ as a Forge user and access the example notebooks through our hosted `Jupyter `_ environment. Jupyter notebooks hosted on our service are pre-configured for Forge client library usage. **No additional setup required.** + + +Installation +------------ + +If you would like to install the example Jupyter notebooks locally, first, :ref:`install the Forge client library ` and configure your local Jupyter environment to resolve Forge client library imports. This will allow you to run the example notebooks. Next, clone the `example notebook repository `_, and explore its contents as desired. + + +List of Example Notebooks +------------------------- + +* `Circuits | Composition | Introduction `_ +* `Circuits | Composition | Circuit Composition `_ +* `Circuits | Composition | Derivatives `_ +* `Circuits | Composition | Gate Library `_ +* `Circuits | Composition | Parameterized Circuits `_ +* `Circuits | Composition | Pauli Operators `_ +* `Circuits | Execution | IBMQ Through Forge `_ +* `Circuits | Execution | Measurement `_ +* `Circuits | Tools | Data Loaders `_ +* `Circuits | Tools | Qiskit Through Forge `_ +* `Linear Algebra | Quantum Distance Estimation `_ +* `Linear Algebra | Quantum Dot Product `_ +* `Machine Learning | Classification `_ +* `Machine Learning | Clustering `_ +* `Machine Learning | Regression `_ +* `Monte Carlo | NISQ Amplitude Estimation `_ +* `Monte Carlo | Option Pricing `_ +* `Optimization | Algorithms | Brute Force Constraint Satisfaction `_ +* `Optimization | Algorithms | Brute ForceOptimization `_ +* `Optimization | Examples | Anneal Offsets `_ +* `Optimization | Examples | Fault Tree Analysis `_ +* `Optimization | Examples | Job Shop Scheduling `_ +* `Optimization | Examples | Portfolio Optimization `_ +* `Optimization | Examples | QAOA Angles `_ +* `Optimization | Examples | QAOA Angle Runtime Comparison `_ +* `Optimization | Examples | Set Cover `_ +* `Optimization | Problem Composition | Constraints `_ +* `Optimization | Problem Composition | Objective Functions `_ diff --git a/docs/source/reference/montecarlo.rst b/docs/source/reference/montecarlo.rst new file mode 100644 index 0000000..54d05b9 --- /dev/null +++ b/docs/source/reference/montecarlo.rst @@ -0,0 +1,16 @@ +Monte Carlo Methods +=================== + +nisqAE +------ + +Functions +^^^^^^^^^ + +.. autofunction:: qcware.forge.montecarlo.nisqAE.make_schedule + +.. autofunction:: qcware.forge.montecarlo.nisqAE.run_schedule + +.. autofunction:: qcware.forge.montecarlo.nisqAE.run_unary + +.. autofunction:: qcware.forge.montecarlo.nisqAE.compute_mle diff --git a/docs/source/reference/optimization.rst b/docs/source/reference/optimization.rst new file mode 100644 index 0000000..1b05126 --- /dev/null +++ b/docs/source/reference/optimization.rst @@ -0,0 +1,52 @@ +Optimization +============ + + +Brute Force Minimization +------------------------ + +Types +^^^^^ + +The brute-force minimization functions in Forge rely on custom +types for problem specification, constraints, and results. + +.. autoclass:: qcware.types.optimization.PolynomialObjective + :members: + +.. autoclass:: qcware.types.optimization.Constraints + :members: + +.. autoclass:: qcware.types.optimization.BruteOptimizeResult + :members: + +Functions +^^^^^^^^^ + +.. autofunction:: qcware.forge.optimization.brute_force_minimize + + +Binary Optimization +------------------- + +Types +^^^^^ + +.. autoclass:: qcware.types.optimization.BinaryProblem + :members: + +.. autoclass:: qcware.types.optimization.BinaryResults + :members: + +Functions +^^^^^^^^^ + +.. autofunction:: qcware.forge.optimization.optimize_binary + + +Quantum Approximate Optimization Algorithm (QAOA) +------------------------------------------------- + +.. autofunction:: qcware.forge.optimization.find_optimal_qaoa_angles +.. autofunction:: qcware.forge.optimization.qaoa_expectation_value +.. autofunction:: qcware.forge.optimization.qaoa_sample diff --git a/docs/source/reference/qio.rst b/docs/source/reference/qio.rst new file mode 100644 index 0000000..f66a443 --- /dev/null +++ b/docs/source/reference/qio.rst @@ -0,0 +1,9 @@ +Quantum Input/Output +======================= + +A common problem in quantum circuits deals with how to get classical +data in and out of a quantum computer. Forge provides a method to +create a circuit from an array of numerical data which loads that +data into a quantum circuit. + +.. autofunction:: qcware.forge.qio.loader diff --git a/docs/source/reference/qutils.rst b/docs/source/reference/qutils.rst new file mode 100644 index 0000000..87e7884 --- /dev/null +++ b/docs/source/reference/qutils.rst @@ -0,0 +1,13 @@ +Quantum Utilities +================= + +As more and more low-level functions migrate to the quantum +space, such as dot products and distance estimation between +vectors, they comprise a category we (so far) simply refer to as +"utility" functions + +.. autofunction:: qcware.forge.qutils.qdot + +.. autofunction:: qcware.forge.qutils.qdist + +.. autofunction:: qcware.forge.qutils.create_qdot_circuit diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt new file mode 100644 index 0000000..f15f15e --- /dev/null +++ b/docs/source/requirements.txt @@ -0,0 +1,2 @@ +Sphinx==4.2.0 +sphinx_rtd_theme==1.0.0 diff --git a/pep8.cfg b/pep8.cfg deleted file mode 100644 index 52f7647..0000000 --- a/pep8.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[pep8] -# ignore = E226,E302,E41 -max-line-length = 300 diff --git a/platform_client_server_utils b/platform_client_server_utils deleted file mode 160000 index 469b2ce..0000000 --- a/platform_client_server_utils +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 469b2ce77e6082a3cf2761a5d8d1f836c07bf119 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7dbc116 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "qcware" +version = "7.4.3" +description = "The python client for QC Ware's Forge SaaS quantum computing product" +authors = ["Vic Putz ","Bryan Burr aiohttp.ClientSession: + """ + Singleton guardian for client session. This may need to be moved + to being a contextvar, and it could be that the whole python Client + needs to be made instantiable (for sessions). But since aiohttp is + single-threaded this should be OK for now. + """ + global _client_session + if _client_session is None: + _client_session = aiohttp.ClientSession() + return _client_session + + +def _fatal_code(e): + return 400 <= e.response.status_code < 500 + + +# By default, evidently aiohttp doesn't raise exceptions for non-200 +# statuses, so backoff has trouble unless you specifically ask +# using raise_for_status = True; see +# https://stackoverflow.com/questions/56152651/how-to-retry-async-aiohttp-requests-depending-on-the-status-code + + +@backoff.on_exception( + backoff.expo, requests.exceptions.RequestException, max_tries=3, giveup=_fatal_code +) +def post_request(url, data): + return client_session().post(url, json=data, raise_for_status=True) + + +@backoff.on_exception( + backoff.expo, requests.exceptions.RequestException, max_tries=3, giveup=_fatal_code +) +def get_request(url): + return client_session().get(url, raise_for_status=True) + + +async def post(url, data): + async with post_request(url, data) as response: + if response.status >= 400: + raise ApiCallFailedError(response.json()["message"]) + return await response.json() + + +async def get(url): + async with get_request(url) as response: + if response.status >= 400: + raise ApiCallResultUnavailableError( + "Unable to retrieve result, please try again later or contact support" + ) + return await response.text() diff --git a/qcware/forge/circuits/__init__.py b/qcware/forge/circuits/__init__.py new file mode 100644 index 0000000..dfa192f --- /dev/null +++ b/qcware/forge/circuits/__init__.py @@ -0,0 +1,9 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +# only the following line is autogenerated; further imports can be added below +from qcware.forge.circuits.api import * + +# add further imports below this line +from qcware.forge.circuits.quasar_backend import QuasarBackend diff --git a/qcware/forge/circuits/api/__init__.py b/qcware/forge/circuits/api/__init__.py new file mode 100644 index 0000000..14a284f --- /dev/null +++ b/qcware/forge/circuits/api/__init__.py @@ -0,0 +1,5 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +from .run_backend_method import run_backend_method diff --git a/qcware/forge/circuits/api/run_backend_method.py b/qcware/forge/circuits/api/run_backend_method.py new file mode 100644 index 0000000..fbb1bcb --- /dev/null +++ b/qcware/forge/circuits/api/run_backend_method.py @@ -0,0 +1,29 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call( + name="circuits.run_backend_method", endpoint="circuits/run_backend_method" +) +def run_backend_method(backend: str, method: str, kwargs: dict): + r"""Runs an arbitrary backend method. This API call is not intended to be used directly by users; rather, it is meant to be called by the QuasarBackend class to transparently delegate class method calls to Forge API endpoints. + + Arguments: + + :param backend: string representing the backend + :type backend: str + + :param method: name of the method to be called + :type method: str + + :param kwargs: Keyword args passed to the method. Positional args should be converted to kwargs + :type kwargs: dict + + + :return: variable; see Quasar documentation + :rtype: object""" + pass diff --git a/qcware/forge/circuits/quasar_backend.py b/qcware/forge/circuits/quasar_backend.py new file mode 100644 index 0000000..88d1fec --- /dev/null +++ b/qcware/forge/circuits/quasar_backend.py @@ -0,0 +1,98 @@ +from quasar.backend import Backend +from types import MethodType, FunctionType +from qcware.forge.api_calls.api_call_decorator import ApiCall +from qcware.serialization.transforms import client_args_to_wire +import inspect + + +class QuasarBackendApiCall(ApiCall): + + # you must manually assign backend and method + def data(self, *args, **kwargs): + if hasattr(self, "signature"): + new_bound_kwargs = self.signature.bind(*[[self] + [args]], **kwargs) + new_bound_kwargs.apply_defaults() + new_kwargs = new_bound_kwargs.arguments + # one problem here is that "kwargs" isn't a real argument, so + if "kwargs" in new_kwargs: + del new_kwargs["kwargs"] + else: + new_kwargs = {} + if "self" in new_kwargs: + del new_kwargs["self"] + all_kwargs = dict(backend=self.backend, method=self.method, kwargs=new_kwargs) + return client_args_to_wire("circuits.run_backend_method", **all_kwargs) + + +class QuasarBackend(object): + """ + A backend for Quasar which runs on the Forge SaaS service. + Forge must be configured with an api key prior to using this. + """ + + def __init__(self, forge_backend: str, backend_args={}): + """ + Creates the QuasarBackend. You must provide a Forge backend, and + provide Forge backend arguments if necessary. + + :param forge_backend: A backend string such as `qcware/cpu_simulator` + :type forge_backend: str + + :param backend_args: A dict of arguments for the Forge backend. Typically not necessary; defaults to `{}` + :type backend_args: dict + """ + self.forge_backend = forge_backend + self.backend_args = backend_args + + def __getattr__(self, name): + # we override this to return a BackendApiCall object instead of + # an actual wrapper + if ( + (name not in dir(Backend)) + or (name[0] == "_") + or ( + name + in ( + "linear_commuting_group", + "run_pauli_expectation_value_gradient_pauli_contraction", + "run_pauli_expectation_value_hessian", + ) + ) + ): + raise NotImplementedError + f = getattr(Backend, name) + + if isinstance(f, MethodType) or isinstance(f, FunctionType): + result = type( + name, + (QuasarBackendApiCall,), + dict( + { + "name": "circuits.run_backend_method", + "endpoint": "circuits/run_backend_method", + "backend": self.forge_backend, + "method": name, + "_decorated": True, + "__doc__": f.__doc__, + "__module__": f.__module__, + "__annotations__": f.__annotations__, + "signature": inspect.signature(f), + "__wrapper__": f, + } + ), + )() + + else: + result = type( + name, + (QuasarBackendApiCall,), + dict( + { + "name": "circuits.run_backend_method", + "endpoint": "circuits/run_backend_method", + "backend": self.forge_backend, + "method": name, + } + ), + )() + return result diff --git a/qcware/forge/config/__init__.py b/qcware/forge/config/__init__.py new file mode 100644 index 0000000..eda142d --- /dev/null +++ b/qcware/forge/config/__init__.py @@ -0,0 +1,2 @@ +"Configuration modules for the QCWare Python client" +from qcware.forge.config.config import * diff --git a/qcware/forge/config/api_semver.py b/qcware/forge/config/api_semver.py new file mode 100644 index 0000000..11e1f92 --- /dev/null +++ b/qcware/forge/config/api_semver.py @@ -0,0 +1,5 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +api_semver = "8.3.0" diff --git a/qcware/forge/config/config.py b/qcware/forge/config/config.py new file mode 100644 index 0000000..6050f81 --- /dev/null +++ b/qcware/forge/config/config.py @@ -0,0 +1,658 @@ +import contextvars +import os +import sys +from contextlib import contextmanager +from enum import Enum +from functools import reduce +from typing import Optional +from urllib.parse import urljoin, urlparse + +import colorama # type: ignore +import requests +from decouple import config # type: ignore +from packaging import version +from pydantic import BaseModel, ConstrainedStr, Field +from qcware.forge import __version__ as Qcware_client_version +from qcware.forge.config.api_semver import api_semver + + +class ConfigurationError(Exception): + pass + + +Client_semver_override = None + + +def override_client_api_semver(semver: str): + """ + Sets the API version reported by the client module. This + does NOT change any behaviour and is only for testing! + """ + global Client_semver_override + Client_semver_override = semver + + +def client_api_semver() -> str: + """ + Reports the semantic API version used by the client + """ + global Client_semver_override + return Client_semver_override if Client_semver_override is not None else api_semver + + +def qcware_api_key(override: Optional[str] = None) -> str: + """ + Returns the API key from environment variable QCWARE_API_KEY, config file, + or the provided override (if the override is provided, this function simply + returns the provided override) + """ + result = ( + override + if override is not None + else current_context().credentials.qcware_api_key # type:ignore + ) + if result is None: + raise ConfigurationError( + "You have not provided a QCWare API key. " + "Please set one with the argument api_key, " + "by calling qcware.set_api_key, or via " + "configuration variables or files." + ) + return result + + +def is_valid_host_url(url: Optional[str]) -> bool: + """ + Checks if a host url is valid. A valid host url is just a scheme + (http/https), a net location, and no path. + """ + if url is None: + result = False + else: + parse_result = urlparse(url) + result = ( + all([parse_result.scheme, parse_result.netloc]) and not parse_result.path + ) + return result + + +def qcware_host(override: Optional[str] = None) -> str: + """ + Returns the QCWare host url from the environment variable QCWARE_HOST, + a config file, or the provided override. The url should be in the form + 'scheme://netloc' where scheme is http or https and netloc is the + IP address or name of the host. No trailing slash is required + (or desired). + + If an override is provided, this function checks for validity and returns + the override. + """ + # get the host; default is https://api.forge.qcware.com; this should + # always work + result = override if override is not None else current_context().qcware_host + # check to make sure the host is a valid url + + if is_valid_host_url(result): + # type ignored below because if we get here, it's a valid string + return result # type:ignore + else: + raise ConfigurationError( + f"Configured QCWARE_HOST ({result}): does not seem to be" + "a valid URL. Please select a host url with scheme" + "(http or https) and no path, e.g." + "'http://api.forge.qcware.com'" + ) + + +def host_api_semver() -> str: + """ + Returns the semantic API version string reported by the host. + """ + result = None + try: + url = urljoin(qcware_host(), "about/about") + r = requests.get(url) + if r.status_code != 200: + raise ConfigurationError( + f'Unable to retrieve API version from host "{qcware_host()}"' + ) + result = r.json()["api_semver"] + except (AttributeError, requests.exceptions.InvalidSchema) as e: + raise ConfigurationError( + f'Error contacting configured host "{qcware_host()}": raised {e}' + ) + return result + + +def client_api_incompatibility_message( + client_version: version.Version, host_version: version.Version +) -> str: + """ + Returns an informative string based on the severity of incompatibility between + client and host API versions + """ + if client_version.major != host_version.major: + return ( + colorama.Fore.RED + + colorama.Style.BRIGHT + + f"\nMajor API version incompatibility (you: {str(client_version)}; host: {str(host_version)})\n" + + "Likely many calls will fail as the API has changed in incompatible ways.\n" + + "Please upgrade with 'pip install --upgrade qcware'" + + colorama.Style.RESET_ALL + ) + elif ( + client_version.major == host_version.major + and client_version.minor != host_version.minor + ): + return ( + colorama.Fore.YELLOW + + f"\nMinor API version incompatibility (you: {str(client_version)}; host: {str(host_version)})\n" + + "Some calls may act strangely, and you may be missing out on \n" + + "new functionality if using an older client.\n" + + "Consider upgrading with 'pip install --upgrade qcware'" + + colorama.Style.RESET_ALL + ) + elif ( + client_version.major == host_version.major + and client_version.minor == host_version.minor + and client_version.micro != host_version.minor + ): + return ( + colorama.Fore.GREEN + + f"\nMicro API version incompatibility (you: {str(client_version)}; host: {str(host_version)})\n" + + "You may be missing out on minor bugfixes if using an older client.\n" + + "consider upgrading with 'pip install --upgrade qcware'" + + colorama.Style.RESET_ALL + ) + else: + return "" + + +Client_api_compatibility_check_has_been_done = False + + +def do_client_api_compatibility_check( + client_version_string: str = None, host_version_string: str = None +): + """ + Checks the client and host API versions and prints an informative + string if the versions differ in a material way. + """ + client_version_string = ( + client_api_semver() if client_version_string is None else client_version_string + ) + host_version_string = ( + host_api_semver() if host_version_string is None else host_version_string + ) + client_version = version.Version(client_version_string) + host_version = version.Version(host_version_string) + if client_version != host_version: + print(client_api_incompatibility_message(client_version, host_version)) + global Client_api_compatibility_check_has_been_done + Client_api_compatibility_check_has_been_done = True + + +def do_client_api_compatibility_check_once( + client_version_string: str = None, host_version_string: str = None +): + """ + If an API compatibility check has not been done between client and the + selected host, do it now and disable further checks. + """ + global Client_api_compatibility_check_has_been_done + if not Client_api_compatibility_check_has_been_done: + client_version_string = ( + client_api_semver() + if client_version_string is None + else client_version_string + ) + host_version_string = ( + host_api_semver() if host_version_string is None else host_version_string + ) + do_client_api_compatibility_check(client_version_string, host_version_string) + + +def set_api_key(key: str): + """ + Set's the user's forge API key via environment variable. + Equivalent to os.environ['QCWARE_API_KEY']=key + """ + os.environ["QCWARE_API_KEY"] = key + + +def set_host(host_url: str): + if is_valid_host_url(host_url): + os.environ["QCWARE_HOST"] = host_url + global Client_api_compatibility_check_has_been_done + Client_api_compatibility_check_has_been_done = False + else: + raise ConfigurationError( + f"Requested QCWARE_HOST ({host_url}): does not" + " seem to be a valid URL. Please select a host url" + "with scheme (http or https) and no path, e.g." + "'http://api.forge.qcware.com'" + ) + + +def client_timeout(override: Optional[int] = None): + """ + Returns the maximum time the api should retry polling when running + in synchronous mode before returning the error state that the call + is not complete and allowing the user to poll manually. + + This is configurable by the environment variable QCWARE_CLIENT_TIMEOUT + + The default value is 60 seconds + """ + result = override if override is not None else current_context().client_timeout + return result + + +def set_client_timeout(new_wait: int): + """ + Sets the maximum time the API should retry polling the server before + returning the error state that the call is not complete. + + This may be set to any value greater than or equal to 0 seconds. + """ + if new_wait < 0: + print( + colorama.Fore.YELLOW + + "Client timeout must be >= 0 seconds; no action taken" + + colorama.Style.RESET_ALL + ) + else: + os.environ["QCWARE_CLIENT_TIMEOUT"] = str(new_wait) + + +def server_timeout(override: Optional[int] = None): + """ + Returns the maximum time the api should retry polling when running + in synchronous mode before returning the error state that the call + is not complete and allowing the user to poll manually. + + This is configurable by the environment variable QCWARE_CLIENT_TIMEOUT + + The default value is 60 seconds + """ + result = override if override is not None else current_context().server_timeout + return result + + +def set_server_timeout(new_wait: int): + """ + Sets the maximum time the API should retry polling the server before + returning the error state that the call is not complete. + + This may be set to any value greater than or equal to 0 seconds. + """ + if new_wait < 0 or new_wait > 50: + print( + colorama.Fore.YELLOW + + "Server timeout must be between 0 and 50 seconds; no action taken" + + colorama.Style.RESET_ALL + ) + else: + os.environ["QCWARE_SERVER_TIMEOUT"] = str(new_wait) + + +def async_interval_between_tries(override: Optional[float] = None): + """Return the maximum time the server should sit pinging the database + for a result before giving up. + + This is configurable by the environment variable QCWARE_SERVER_TIMEOUT + + The default value is 10 seconds; the maximum is 50 + + """ + result = ( + override + if override is not None + else current_context().async_interval_between_tries + ) + return result + + +def set_async_interval_between_tries(new_interval: float): + """Set the maximum server timeout. + + This sets how long the server will poll for a result before + returning to the client with a result or 'still waiting' message. + + Normally the user should not change this from the default value of 10s. + + """ + if new_interval < 0 or new_interval > 50: + print( + colorama.Fore.YELLOW + + "Time between async tries must be between 0 and 50 seconds; no action taken" + + colorama.Style.RESET_ALL + ) + else: + os.environ["QCWARE_ASYNC_INTERVAL_BETWEEN_TRIES"] = str(new_interval) + + +class SchedulingMode(str, Enum): + """Scheduling modes for API calls. + + 'immediate' means 'attempt this now; if it must be rescheduled + (such as for an unavailable backend), fail with an exception' + + 'next_available' means 'attempt this now, but if the backend is + unavailable, schedule during the next availability window.' + + """ + + immediate = "immediate" + next_available = "next_available" + + +def scheduling_mode(override: Optional[SchedulingMode] = None): + """ + Returns the scheduling mode, only relevant for backends that have availability + windows. "immediate" means a call should fail if called for outside its + availability window, while "next_available" means such calls should be automaticall + scheduled for the next availability window. + + A reschedule is not a guarantee that the job will be run within that window! If not, + it will stay in the queue until the next availability window. + """ + result = override if override is not None else current_context().scheduling_mode + return result + + +def set_scheduling_mode(new_mode: SchedulingMode): + """ + Sets the scheduling mode, only relevant for backends that have availability + windows. "immediate" means a call should fail if called for outside its + availability window, while "next_available" means such calls should be automaticall + scheduled for the next availability window. + + A reschedule is not a guarantee that the job will be run within that window! If not, + it will stay in the queue until the next availability window. + """ + new_value = SchedulingMode(new_mode) + os.environ["QCWARE_SCHEDULING_MODE"] = new_value.value + + +def set_ibmq_credentials( + token: Optional[str] = None, + hub: Optional[str] = None, + group: Optional[str] = None, + project: Optional[str] = None, +): + """Set the IBMQ credentials "by hand".""" + + def _set_if(envvar_name: str, value: Optional[str] = None): + if value is None and envvar_name in os.environ: + del os.environ[envvar_name] + elif value is not None: + os.environ[envvar_name] = value + + _set_if("QCWARE_CRED_IBMQ_TOKEN", token) + _set_if("QCWARE_CRED_IBMQ_HUB", hub) + _set_if("QCWARE_CRED_IBMQ_GROUP", group) + _set_if("QCWARE_CRED_IBMQ_PROJECT", project) + + +def set_ibmq_credentials_from_ibmq_provider( + provider: "qiskit.providers.ibmq.accountprovider.AccountProvider", +): + """Set the IBMQ credentials from an ibmq provider object. + + Called normally as set_ibmq_credentials_from_ibmq(IBMQ.providers()[0]). + The IBMQ "factory" can provide several providers, particularly if your + IBMQ token is associated with various hubs, groups, or projects. + + """ + set_ibmq_credentials( + provider.credentials.token, + provider.credentials.hub, + provider.credentials.group, + provider.credentials.project, + ) + + +class IBMQCredentials(BaseModel): + + token: Optional[str] + hub: Optional[str] + group: Optional[str] + project: Optional[str] + + @classmethod + def from_ibmq(cls, ibmq): + """Creates the IBMQ credentials from an existing initialized + IBMQFactory object. To be used as + + from qiskit import IBMQ + IBMQ.load_account() # or enable_account(...) + credentials=IBMQCredentials.from_ibmq(IBMQ) + """ + return cls( + token=ibmq._credentials.token, + hub=ibmq._credentials.hub, + group=ibmq._credentials.group, + project=ibmq._credentials.project, + ) + + class Config: + extra = "forbid" + + +class ApiCredentials(BaseModel): + qcware_api_key: Optional[str] = None + ibmq: Optional[IBMQCredentials] = None + + class Config: + extra = "forbid" + + +class Environment(BaseModel): + """This deserves a little explanation; it is greatly helpful to us + when diagnosing a problem to have recorded information about the + "environment" of the call. These are manually overloadable should + users wish to hide this information, but it is set by default in this + fashion: + + client, client_version: usually "qcware" and this library's version + + version environment: the sort of "global environment". Usually + this is set by environment variable + "QCWARE_ENVIRONMENT_ENVIRONMENT", which is "hosted_jupyter"on + hosted jupyter notebooks, or "local" for a local installation. + + source_file: this is empty by default so that we do not collect + unnecessary information from users. It is set in our hosted example + notebooks so that we can see what calls come from example notebooks. + This is set via the environment variable QCWARE_ENVIRONMENT_SOURCE_FILE. + """ + + client: str + client_version: str + python_version: str + environment: str + source_file: str + + +def set_environment_environment(new_environment: str): + """Set the Environment ... environment.""" + os.environ["QCWARE_ENVIRONMENT_ENVIRONMENT"] = new_environment + + +def set_environment_source_file(new_source_file: str): + """Set the source file recorded in the context environment.""" + os.environ["QCWARE_ENVIRONMENT_SOURCE_FILE"] = new_source_file + + +class ApiCallContext(BaseModel): + """The context sent over with every API call. + + A number of things are listed as "optional" which really are not; they are + created by default in the `root_context` function. By allowing them to be + optional, you can augment the current context with "temporary contexts" that + override only one field (or a subset). + """ + + qcware_host: Optional[str] = None + credentials: Optional[ApiCredentials] = None + environment: Optional[Environment] = None + server_timeout: Optional[int] = None + client_timeout: Optional[int] = None + async_interval_between_tries: Optional[float] = None + scheduling_mode: Optional[SchedulingMode] = None + + class Config: + extra = "forbid" + + +def root_context() -> ApiCallContext: + """ + Return a dictionary containing relevant information for API calls. + + Used internally + """ + return ApiCallContext( + qcware_host=config("QCWARE_HOST", "https://api.forge.qcware.com"), + credentials=ApiCredentials( + qcware_api_key=config("QCWARE_API_KEY", None), + ibmq=IBMQCredentials( + token=config("QCWARE_CRED_IBMQ_TOKEN", None), + hub=config("QCWARE_CRED_IBMQ_HUB", None), + group=config("QCWARE_CRED_IBMQ_GROUP", None), + project=config("QCWARE_CRED_IBMQ_PROJECT", None), + ), + ), + environment=Environment( + client="qcware (python)", + client_version=Qcware_client_version, + python_version=sys.version, + environment=config("QCWARE_ENVIRONMENT_ENVIRONMENT", default="local"), + source_file=config("QCWARE_ENVIRONMENT_SOURCE_FILE", default=""), + ), + server_timeout=config("QCWARE_SERVER_TIMEOUT", default=10, cast=int), + client_timeout=config("QCWARE_CLIENT_TIMEOUT", default=60, cast=int), + async_interval_between_tries=config( + "QCWARE_ASYNC_INTERVAL_BETWEEN_TRIES", 0.5, cast=float + ), + scheduling_mode=config( + "QCWARE_SCHEDULING_MODE", default=SchedulingMode.immediate + ), + ) + + +_contexts: contextvars.ContextVar[ApiCallContext] = contextvars.ContextVar( + "contexts", default=[] +) # type:ignore + + +def push_context(**kwargs): + """Manually pushes a configuration context onto the stack. + + Normally this is done with the `additional_config` context rather + than called directly by the user + + """ + next_context = ApiCallContext(**kwargs) + _contexts.set(_contexts.get() + [next_context]) + + +def pop_context(): + """Manually pops a configuration context from the stack. + + Normally this is done with the `additional_config` context rather + than called directly by the user + + """ + _contexts.set(_contexts.get()[:-1]) + + +# from https://github.com/pytoolz/toolz/issues/281 +# although as noted more efficient solutions exist. This is also +# modified to ignore k/v pairs in b for which the value is None +def deep_merge(a, b): + """Merge two dictionaries recursively.""" + + def merge_values(k, v1, v2): + if isinstance(v1, dict) and isinstance(v2, dict): + return k, deep_merge(v1, v2) + elif v2 is not None: + return k, v2 + else: + return k, v1 + + a_keys = set(a.keys()) + b_keys = set(b.keys()) + pairs = ( + [merge_values(k, a[k], b[k]) for k in a_keys & b_keys] + + [(k, a[k]) for k in a_keys - b_keys] + + [(k, b[k]) for k in b_keys - a_keys] + ) + return dict(pairs) + + +def merge_models(c1: BaseModel, c2: BaseModel) -> BaseModel: + d1 = c1.dict() + d2 = c2.dict() + result_dict = deep_merge(d1, d2) + return c1.copy(update=result_dict) + + +def current_context() -> ApiCallContext: + """Return the "current context" for an API call. + + This is the calculated root context plus any additional changes + through the stack. Normally not called by the user. + + """ + # known problem below with mypy and reduce, see https://github.com/python/mypy/issues/4150 + # among others + return reduce(merge_models, _contexts.get(), root_context()) # type:ignore + + +@contextmanager +def additional_config(**kwargs): + """ + This provides a context manager through which the qcware python client library + can be temporarily reconfigured, for example to allow a longer client timeout for a + call, or to make a call with different credentials. To use it, one must provide + a set of keywords which map to the arguments of an `ApiCallContext`, for example, to + make a single call with a client timout of five minutes: + + ``` + with additional_config(client_tiemout=5*60): + result = optimize_binary(...) + ``` + """ + push_context(**kwargs) + try: + yield + finally: + pop_context() + + +@contextmanager +def ibmq_credentials( + token: str, + hub: Optional[str] = None, + group: Optional[str] = None, + project: Optional[str] = None, +): + ibmq_creds = IBMQCredentials(token=token, hub=hub, group=group, project=project) + credentials = ApiCredentials(ibmq=ibmq_creds) + push_context(credentials=credentials) + try: + yield + finally: + pop_context() + + +@contextmanager +def ibmq_credentials_from_ibmq(ibmq): + ibmq_creds = IBMQCredentials.from_ibmq(ibmq) + credentials = ApiCredentials(ibmq=ibmq_creds) + push_context(credentials=credentials) + try: + yield + finally: + pop_context() diff --git a/qcware/forge/exceptions.py b/qcware/forge/exceptions.py new file mode 100644 index 0000000..34747ad --- /dev/null +++ b/qcware/forge/exceptions.py @@ -0,0 +1,32 @@ +class QCWareClientException(Exception): + pass + + +class ApiException(QCWareClientException): + pass + + +class ApiCallFailedError(ApiException): + pass + + +class ApiCallResultUnavailableError(ApiException): + pass + + +class ApiCallExecutionError(ApiException): + def __init__(self, message, traceback, api_call_info=dict()): + super().__init__(message) + self.traceback = traceback + self.api_call_info = api_call_info + + +class ApiTimeoutError(ApiException): + def __init__(self, api_call_info, message=None): + if message is None: + message = f"""API Call timed out. +You can retrieve with qcware.api_calls.retrieve_result(call_token='{api_call_info['uid']}') +or use the .submit or .call_async forms of the API call. +See the getting started notebook "Retrieving_long_task_results.ipynb" in Forge""" + super().__init__(message) + self.api_call_info = api_call_info diff --git a/qcware/forge/montecarlo/__init__.py b/qcware/forge/montecarlo/__init__.py new file mode 100644 index 0000000..eec4e99 --- /dev/null +++ b/qcware/forge/montecarlo/__init__.py @@ -0,0 +1,11 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + + +# only the following lines are autogenerated; further imports can be added below +from .api import * + +from .nisqAE import * + +# add further imports below this line diff --git a/qcware/forge/montecarlo/api/__init__.py b/qcware/forge/montecarlo/api/__init__.py new file mode 100644 index 0000000..136b287 --- /dev/null +++ b/qcware/forge/montecarlo/api/__init__.py @@ -0,0 +1,3 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved diff --git a/qcware/forge/montecarlo/nisqAE/__init__.py b/qcware/forge/montecarlo/nisqAE/__init__.py new file mode 100644 index 0000000..1926779 --- /dev/null +++ b/qcware/forge/montecarlo/nisqAE/__init__.py @@ -0,0 +1,9 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + + +# only the following line is autogenerated; further imports can be added below +from .api import * + +# add further imports below this line diff --git a/qcware/forge/montecarlo/nisqAE/api/__init__.py b/qcware/forge/montecarlo/nisqAE/api/__init__.py new file mode 100644 index 0000000..ca68e26 --- /dev/null +++ b/qcware/forge/montecarlo/nisqAE/api/__init__.py @@ -0,0 +1,11 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +from .make_schedule import make_schedule + +from .run_schedule import run_schedule + +from .run_unary import run_unary + +from .compute_mle import compute_mle diff --git a/qcware/forge/montecarlo/nisqAE/api/compute_mle.py b/qcware/forge/montecarlo/nisqAE/api/compute_mle.py new file mode 100644 index 0000000..61133ef --- /dev/null +++ b/qcware/forge/montecarlo/nisqAE/api/compute_mle.py @@ -0,0 +1,32 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +import quasar + +from typing import Sequence, Tuple + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call( + name="montecarlo.nisqAE.compute_mle", endpoint="montecarlo.nisqAE/compute_mle" +) +def compute_mle(target_counts: Sequence[Tuple[Tuple[int, int], int]], epsilon: float): + r"""Given the output of the run_nisqAE functions and estimates the parameter theta via MLE. + + Arguments: + + :param target_counts: For each element in the schedule, returns a tuple of (element, target_counts), where target_counts is the number of measurements whose outcome is in the set of targetStates. + :type target_counts: Sequence[Tuple[Tuple[int, int], int]] + + :param epsilon: The additive error within which we would like to calculate theta. + :type epsilon: float + + + :return: MLE estimate of theta. + :rtype: float""" + pass diff --git a/qcware/forge/montecarlo/nisqAE/api/make_schedule.py b/qcware/forge/montecarlo/nisqAE/api/make_schedule.py new file mode 100644 index 0000000..20f128a --- /dev/null +++ b/qcware/forge/montecarlo/nisqAE/api/make_schedule.py @@ -0,0 +1,49 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +import quasar + +from typing import Optional, Sequence, Tuple + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call( + name="montecarlo.nisqAE.make_schedule", endpoint="montecarlo.nisqAE/make_schedule" +) +def make_schedule( + epsilon: float, + schedule_type: str, + max_depth: Optional[int] = 20, + beta: Optional[float] = 0.5, + n_shots: Optional[float] = 20, +): + r"""Create a schedule for use in the run_nisqAE functions. + + Arguments: + + :param epsilon: The additive bound with which to approximate the amplitude + :type epsilon: float + + :param schedule_type: schedule_type in 'linear', 'exponential', 'powerlaw', 'classical' + :type schedule_type: str + + :param max_depth: The maximum number of times we should run the iteration circuit (does not affect 'powerlaw')., defaults to 20 + :type max_depth: Optional[int] + + :param beta: Beta parameter for powerlaw schedule (does not affect other schedule_types)., defaults to 0.5 + :type beta: Optional[float] + + :param n_shots: Number of measurements to take at each power, defaults to 20 + :type n_shots: Optional[float] + + + :return: A schedule for how many times to run the iteration_circuit, and how many shots to take. A List[Tuple[power, num_shots]], where: + - power is the number of times to run the iteration circuit + - num_shots is the number of shots to run at the given power + :rtype: Sequence[Tuple[int, int]]""" + pass diff --git a/qcware/forge/montecarlo/nisqAE/api/run_schedule.py b/qcware/forge/montecarlo/nisqAE/api/run_schedule.py new file mode 100644 index 0000000..6794fc5 --- /dev/null +++ b/qcware/forge/montecarlo/nisqAE/api/run_schedule.py @@ -0,0 +1,55 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +import quasar + +from typing import Sequence, Tuple + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call( + name="montecarlo.nisqAE.run_schedule", endpoint="montecarlo.nisqAE/run_schedule" +) +def run_schedule( + initial_circuit: quasar.Circuit, + iteration_circuit: quasar.Circuit, + target_qubits: Sequence[int], + target_states: Sequence[int], + schedule: Sequence[Tuple[int, int]], + backend: str = "qcware/cpu_simulator", +): + r"""Run a nisq variant of amplitude estimation and output circuit measurements for each circuit in the given schedule. + + Arguments: + + :param initial_circuit: The oracle circuit whose output we would like to estimate. + :type initial_circuit: quasar.Circuit + + :param iteration_circuit: The iteration circuit which we will run multiple times according to the schedule. + :type iteration_circuit: quasar.Circuit + + :param target_qubits: The qubits which will be measured after every shot and compared to the target_states below. + In the classic amplitude estimation problem, this is usually just [0]. + :type target_qubits: Sequence[int] + + :param target_states: The set of states states [in base-10 integer representation] which correspond to "successful" measurements of the target_qubits. If the target_qubits are measured as one of target_states at the end of a shot, target_counts will be incremented. + In the classic amplitude estimation problem, this is usually just [1]. + :type target_states: Sequence[int] + + :param schedule: A schedule for how many times to run the iteration_circuit, and how many shots to take. A List[Tuple[power, num_shots]], where: + - power is the number of times to run the iteration_circuit in a shot + - num_shots is the number of shots to run at the given power + :type schedule: Sequence[Tuple[int, int]] + + :param backend: String denoting the backend to use, defaults to qcware/cpu_simulator + :type backend: str + + + :return: For each element in the schedule, returns a tuple of (element, target_counts), where target_counts is the number of measurements whose outcome is in the set of target_states. + :rtype: Sequence[Tuple[Tuple[int, int], int]]""" + pass diff --git a/qcware/forge/montecarlo/nisqAE/api/run_unary.py b/qcware/forge/montecarlo/nisqAE/api/run_unary.py new file mode 100644 index 0000000..1c674c4 --- /dev/null +++ b/qcware/forge/montecarlo/nisqAE/api/run_unary.py @@ -0,0 +1,41 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +import quasar + +from typing import Sequence, Tuple + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call( + name="montecarlo.nisqAE.run_unary", endpoint="montecarlo.nisqAE/run_unary" +) +def run_unary( + circuit: quasar.Circuit, + schedule: Sequence[Tuple[int, int]], + backend: str = "qcware/cpu_simulator", +): + r"""Performs amplitude estimation routine for unary circuits, assuming the 0th qubit is the target and the target state is |1>, that is, we assume that the state of our system can be written as cos(theta)|0>|badStates> + sin(theta)|1>|0> where badStates is a set of unary states (i.e ones which only one qubit at a time is at state 1) and we're trying to estimate theta. + + Arguments: + + :param circuit: The oracle circuit whose output we would like to estimate + :type circuit: quasar.Circuit + + :param schedule: A schedule for how many times to run the iteration_circuit, and how many shots to take. A List[Tuple[power, num_shots]], where: + - power is the number of times to run the iteration circuit + - num_shots is the number of shots to run at the given power + :type schedule: Sequence[Tuple[int, int]] + + :param backend: String denoting the backend to use, defaults to qcware/cpu_simulator + :type backend: str + + + :return: For each element in the schedule, returns a tuple of (element, target_counts), where target_counts is the number of measurements whose outcome is in the set of targetStates. + :rtype: Sequence[Tuple[Tuple[int, int], int]]""" + pass diff --git a/qcware/forge/optimization/__init__.py b/qcware/forge/optimization/__init__.py new file mode 100644 index 0000000..2f87d51 --- /dev/null +++ b/qcware/forge/optimization/__init__.py @@ -0,0 +1,6 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +# only the following line is autogenerated; further imports can be added below +from .api import * diff --git a/qcware/forge/optimization/api/__init__.py b/qcware/forge/optimization/api/__init__.py new file mode 100644 index 0000000..d94e051 --- /dev/null +++ b/qcware/forge/optimization/api/__init__.py @@ -0,0 +1,13 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +from .brute_force_minimize import brute_force_minimize + +from .qaoa_expectation_value import qaoa_expectation_value + +from .qaoa_sample import qaoa_sample + +from .find_optimal_qaoa_angles import find_optimal_qaoa_angles + +from .optimize_binary import optimize_binary diff --git a/qcware/forge/optimization/api/brute_force_minimize.py b/qcware/forge/optimization/api/brute_force_minimize.py new file mode 100644 index 0000000..6202ede --- /dev/null +++ b/qcware/forge/optimization/api/brute_force_minimize.py @@ -0,0 +1,38 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import qcware.types.optimization as types + +from typing import Optional + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call( + name="optimization.brute_force_minimize", + endpoint="optimization/brute_force_minimize", +) +def brute_force_minimize( + objective: types.PolynomialObjective, + constraints: Optional[types.Constraints] = None, + backend: str = "qcware/cpu", +): + r"""Minimize given objective polynomial subject to constraints. + + Arguments: + + :param objective: The integer-coefficient polynomial to be evaluated should be specified by a PolynomialObjective. See documentation for PolynomialObjective for more information. Note that variables are boolean in the sense that their values are 0 and 1. + :type objective: types.PolynomialObjective + + :param constraints: Optional constraints are specified with an object of class Constraints. See its documentation for further information., defaults to None + :type constraints: Optional[types.Constraints] + + :param backend: String specifying the backend. Currently only [qcware/cpu] available, defaults to qcware/cpu + :type backend: str + + + :return: BruteOptimizeResult object specifying the minimum value of the objective function (that does not violate a constraint) as well as the variables that attain this value. + :rtype: types.BruteOptimizeResult""" + pass diff --git a/qcware/forge/optimization/api/find_optimal_qaoa_angles.py b/qcware/forge/optimization/api/find_optimal_qaoa_angles.py new file mode 100644 index 0000000..fd9c45b --- /dev/null +++ b/qcware/forge/optimization/api/find_optimal_qaoa_angles.py @@ -0,0 +1,51 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call( + name="optimization.find_optimal_qaoa_angles", + endpoint="optimization/find_optimal_qaoa_angles", +) +def find_optimal_qaoa_angles( + Q: dict = {}, + num_evals: int = 100, + num_min_vals: int = 10, + fastmath_flag_in: bool = True, + precision: int = 30, +): + r"""Finds the optimal expectation values for a given cost function, to be used in QAOA. + + Arguments: + + :param Q: The objective function matrix. As :math:`Q` is usually sparse, it should be specified as a Python dictionary with integer pairs :math:`(i,j)` as keys (representing the :math:`(i,j)`th entry of :math:`Q`) and integer or float values., defaults to {} + :type Q: dict + + :param num_evals: The number of evaluations used for :math:`\beta`/:math:`\gamma`, defaults to 100 + :type num_evals: int + + :param num_min_vals: The number of returned minima, defaults to 10 + :type num_min_vals: int + + :param fastmath_flag_in: The "fastmath" flag in Numba, defaults to True + :type fastmath_flag_in: bool + + :param precision: Inverse proportional to the minimum distance between peaks (nx/precision), defaults to 30 + :type precision: int + + + :return: A tuple of three values min_val, min_beta_gamma, Z where: + * min_val is a list of the best `num_min_vals` expectation values found, sorted from minimum to maximum. + * min_beta_gamma is a list of [:math:`\beta`, :math:`\gamma`] pairs representing the best + `num_min_vals` expectation values found, in the same order as the expectation values + + + * Z is a numpy.ndarray of shape (num_evals, num_evals) representing the expectation value for + the beta/gamma pair. Each row represents a choice of :math:`\gamma` and each column represents + a choice of :math:`\beta`, so `Z[1,2]` represents the expectation value from the :math:`\gamma` value `Y[1]` + and the :math:`\beta` value `X[2]` + :rtype: tuple""" + pass diff --git a/qcware/forge/optimization/api/optimize_binary.py b/qcware/forge/optimization/api/optimize_binary.py new file mode 100644 index 0000000..20f494d --- /dev/null +++ b/qcware/forge/optimization/api/optimize_binary.py @@ -0,0 +1,323 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import warnings + +from qcware.forge.api_calls import declare_api_call +from qcware.types.optimization import BinaryProblem, BinaryResults + + +@declare_api_call( + name="optimization.optimize_binary", endpoint="optimization/optimize_binary" +) +def optimize_binary( + instance: BinaryProblem, + backend: str, + constraints_linear_A: list = [], + constraints_linear_b: list = [], + constraints_sat_max_runs: int = 3100, + constraints_hard: bool = False, + constraints_penalty_scaling_factor: int = 1, + constraints_equality_R: list = [], + constraints_equality_c: list = [], + constraints_inequality_S: list = [], + constraints_inequality_d: list = [], + return_all_solutions: bool = False, + num_runs: int = 50, + dwave_algorithm: str = None, + dwave_embedding: dict = None, + dwave_solver_limit: str = None, + dwave_target_energy: str = None, + dwave_find_max: str = None, + dwave_reduce_intersample_correlation: str = None, + dwave_num_spin_reversal_transforms: str = None, + dwave_programming_thermalization: str = None, + dwave_reinitialize_state: str = None, + dwave_anneal_offsets: str = None, + dwave_anneal_offsets_delta: str = None, + dwave_num_reads: int = 1, + dwave_max_answers: str = None, + dwave_flux_biases: str = None, + dwave_beta: str = None, + dwave_answer_mode: str = None, + dwave_auto_scale: str = None, + dwave_postprocess: str = None, + dwave_annealing_time: str = None, + dwave_anneal_schedule: str = None, + dwave_initial_state: str = None, + dwave_chains: str = None, + dwave_flux_drift_compensation: bool = None, + dwave_beta_range: str = None, + dwave_num_sweeps: str = None, + dwave_precision_ancillas: str = None, + dwave_precision_ancillas_tuples: str = None, + constraints_hard_num: int = 4, + sa_num_sweeps: int = 200, + use_sample_persistence: bool = False, + sample_persistence_solution_threshold: float = 0.5, + sample_persistence_persistence_threshold: float = 0.5, + sample_persistence_persistence_iterations: int = 0, + google_num_steps: int = 1, + google_n_samples: int = 1000, + google_arguments_optimizer: dict = {}, + google_step_sampling: bool = True, + google_n_samples_step_sampling: int = 1000, + number_of_blocks: int = 1, + iterations: int = 50, + initial_solution: list = None, + always_update_with_best: bool = True, + update_q_each_block_solution: bool = True, + qaoa_nmeasurement: int = None, + qaoa_optimizer: str = "COBYLA", + qaoa_beta: float = None, + qaoa_gamma: float = None, + qaoa_p_val: int = 1, +): + r"""Solve a binary optimization problem using one of the solvers provided by the platform. +This function solves a binary optimization problem that is either + * Unconstrained (quadratic or higher order) + * Linearly and/or quadratically Constrained (quadratic) + +Constraints may be linear or quadratic. Specifically, the function is capable of solving a function of the form + +.. math:: + \min_x x^T& Q x \\ + + \text{such that} \hspace{4em} Ax &= b \\ + + x^T R_i x &= c_i \\ + + x^T S_i x &\geq d_i \\ + +Here, :math:`x` is a length-:math:`n` vector of binary values, i.e., :math:`\{0,1\}^n` (this is what the solver finds). :math:`Q` is a :math:`(n\times n)` matrix of reals. :math:`A` is a :math:`(m \times n)` matrix of reals (partially specifying :math:`m` different linear constraints). :math:`b` is a length-:math:`m` vector of reals (specifying the other component of `m` different linear constraints). Every :math:`R_i` and :math:`S_i` is a :math:`(n \times n)` matrix of reals, and every :math:`c_i` and :math:`d_i` is a real constant. The :math:`(R_i, c_i)` pairs specify quadratic equality constraints, and the :math:`(S_i, d_i)` pairs specify quadratic inequality constraints. + +In the simplest case, the only variables required to be passed to this function are a valid access key for the platform and a dictionary representing a QUBO. Additional options are available to: + + * Specify constraints for a problem + * Select different solvers (note: different accounts have different solvers available) + * Specify solver-specific parameters + + +Error handling is provided by the platform, and warnings and errors that are detected while attempting to run a problem are returned in the JSON object returned by this function. + +Possible warnings include: + ++--------------+--------------------------------------------------------------------+ +| Warning Code | Warning Message | ++==============+====================================================================+ +| 7 | Precision required not supported by the machine, still proceeding. | ++--------------+--------------------------------------------------------------------+ +| 10 | Hardware solver failed, solving in software solver. | ++--------------+--------------------------------------------------------------------+ +| 22 | Automatic parameter setting failed; solving using default values. | ++--------------+--------------------------------------------------------------------+ + +Possible errors include: + ++------------+----------------------------------------+ +| Error Code | Error Message | ++============+========================================+ +| 6 | Integer formulation for HFS not found. | ++------------+----------------------------------------+ +| 11 | D-Wave hardware solver returned error. | ++------------+----------------------------------------+ +| 100 | Invalid solver selected. | ++------------+----------------------------------------+ + +It is strongly recommended to wrap a call to :obj:`optimize_binary` in a try/catch block since it is possible for the platform or the client library to raise an exception. + +Arguments: + +:param instance: The objective function matrix in the optimization problem described above. In the case of a quadratic problem, this is a 2D matrix; generally, in the case of higher-order problems, this is an :math:`n`-dimensional matrix (a tensor). Since :math:`Q` is usually sparse, :math:`Q` should be specified as a Python dictionary with integer or string pairs :math:`(i,j)` as keys (representing the :math:`(i,j)`th entry of :math:`Q`) and integer or float values. In the case of a cubic function, for example, some dictionary keys will be 3-tuples of integers, rather than pairs. Alternatively, :math:`Q` may be specified as a numpy array or list, in which case :obj:`mat_to_dict` is called on :math:`Q` before sending it to the platform. Note that that helper function assumes :math:`Q` is symmetric, which may not be true in general. It is strongly encouraged to format :math:`Q` is a dictionary. +:type instance: BinaryProblem + +:param backend: The name of the backend to use for the given problem. Currently valid values are: + + * "qcware/cpu": Run on a classical computing backend using a brute-force solver + * "qcware/cpu_simulator": Run on a classical computing simulation of a quantum computer, using QAOA + * "qcware/gpu_simulator": Run on a gpu-accelerated simulation of a quantum computer, using QAOA +:type backend: str + +:param constraints_linear_A: The :math:`A` matrix for specifying linear constraints. :math:`A` should be formatted as a two-dimensional Python list. Default value :obj:`[]`., defaults to [] +:type constraints_linear_A: list + +:param constraints_linear_b: The :math:`b` vector for specifying linear constraints. :math:`b` should be formatted as a one-dimensional Python list. Default value :obj:`[]`., defaults to [] +:type constraints_linear_b: list + +:param constraints_sat_max_runs: The maximum number of iterations the platform should run in order to find a formulation where all constraints are satisfied. Default value :obj:`3100`., defaults to 3100 +:type constraints_sat_max_runs: int + +:param constraints_hard: Whether to strictly enforce all constraints; if :obj:`False`, constraint penalties may be low enough such that constraints are violated, with the benefit of an improved energy landscape. Default value :obj:`False`., defaults to False +:type constraints_hard: bool + +:param constraints_penalty_scaling_factor: An extra constant scaling factor for the Lagrange multipliers associated with the penalty terms for the constraints. This may be helpful if constraints are being violated too much or too often. Default value 1., defaults to 1 +:type constraints_penalty_scaling_factor: int + +:param constraints_equality_R: The :math:`R` matrices for specifying quadratic equality constraints. :math:`R` should be formatted as a list of two-dimensional lists (i.e., a list of matrices). Default value :obj:`[]`., defaults to [] +:type constraints_equality_R: list + +:param constraints_equality_c: The :math:`c` vectors for specifying quadratic equality constraints. :math:`c` should be formatted as a list of one-dimensional Python lists (i.e., a list of vectors). Default value :obj:`[]`., defaults to [] +:type constraints_equality_c: list + +:param constraints_inequality_S: The :math:`S` matrices for specifying quadratic inequality constraints. :math:`S` should be formatted as a list of two-dimensional lists (i.e., a list of matrices). Default value :obj:`[]`., defaults to [] +:type constraints_inequality_S: list + +:param constraints_inequality_d: The :math:`d` vectors for specifying quadratic inequality constraints. :math:`d` should be formatted as a list of one-dimensional Python lists (i.e., a list of vectors). Default value :obj:`[]`., defaults to [] +:type constraints_inequality_d: list + +:param return_all_solutions: Whether to return all the candidate solutions found for a problem; if :obj:`False`, the platform will only return the solution corresponding to the lowest energy found. Default value :obj:`False`., defaults to False +:type return_all_solutions: bool + +:param num_runs: The number of iterations to run with the selected solver. Default value :obj:`50`., defaults to 50 +:type num_runs: int + +:param dwave_algorithm: , defaults to None +:type dwave_algorithm: str + +:param dwave_embedding: A manual minor embedding. This may not be compatible with anneal offsets., defaults to None +:type dwave_embedding: dict + +:param dwave_solver_limit: , defaults to None +:type dwave_solver_limit: str + +:param dwave_target_energy: , defaults to None +:type dwave_target_energy: str + +:param dwave_find_max: , defaults to None +:type dwave_find_max: str + +:param dwave_reduce_intersample_correlation: D-Wave hardware system parameter. See `reduce_intersample_correlation `_., defaults to None +:type dwave_reduce_intersample_correlation: str + +:param dwave_num_spin_reversal_transforms: D-Wave hardware system parameter. See `num_spin_reversal_transforms `_., defaults to None +:type dwave_num_spin_reversal_transforms: str + +:param dwave_programming_thermalization: D-Wave hardware system parameter. See `programming_thermalization `_., defaults to None +:type dwave_programming_thermalization: str + +:param dwave_reinitialize_state: D-Wave hardware system parameter. See `reinitialize_state `_., defaults to None +:type dwave_reinitialize_state: str + +:param dwave_anneal_offsets: D-Wave hardware system parameter. See `anneal_offsets `_., defaults to None +:type dwave_anneal_offsets: str + +:param dwave_anneal_offsets_delta: Parameter greater or equal to 0 that is used to generate anneal offsets, cannot be specified if dwave_anneal_offsets is also specified. We recommend the value to be in [0, 0.05]. See ``_., defaults to None +:type dwave_anneal_offsets_delta: str + +:param dwave_num_reads: D-Wave hardware system parameter. See `num_reads `_., defaults to 1 +:type dwave_num_reads: int + +:param dwave_max_answers: D-Wave hardware system parameter. See `max_answers `_., defaults to None +:type dwave_max_answers: str + +:param dwave_flux_biases: D-Wave hardware system parameter. See `flux_biases `_., defaults to None +:type dwave_flux_biases: str + +:param dwave_beta: D-Wave hardware system parameter. See `beta `_., defaults to None +:type dwave_beta: str + +:param dwave_answer_mode: D-Wave hardware system parameter. See `answer_mode `_., defaults to None +:type dwave_answer_mode: str + +:param dwave_auto_scale: D-Wave hardware system parameter. See `auto_scale `_., defaults to None +:type dwave_auto_scale: str + +:param dwave_postprocess: D-Wave hardware system parameter. See `postprocess `_., defaults to None +:type dwave_postprocess: str + +:param dwave_annealing_time: D-Wave hardware system parameter. See `annealing_time `_., defaults to None +:type dwave_annealing_time: str + +:param dwave_anneal_schedule: D-Wave hardware system parameter. See `anneal_schedule `_., defaults to None +:type dwave_anneal_schedule: str + +:param dwave_initial_state: D-Wave hardware system parameter. See `initial_state `_., defaults to None +:type dwave_initial_state: str + +:param dwave_chains: D-Wave hardware system parameter. See `chains `_., defaults to None +:type dwave_chains: str + +:param dwave_flux_drift_compensation: D-Wave hardware system parameter. See `flux_drift_compensation `_., defaults to None +:type dwave_flux_drift_compensation: bool + +:param dwave_beta_range: D-Wave software system parameter. See `beta_range `_., defaults to None +:type dwave_beta_range: str + +:param dwave_num_sweeps: D-Wave software system parameter. See `num_sweeps `_., defaults to None +:type dwave_num_sweeps: str + +:param dwave_precision_ancillas: , defaults to None +:type dwave_precision_ancillas: str + +:param dwave_precision_ancillas_tuples: , defaults to None +:type dwave_precision_ancillas_tuples: str + +:param constraints_hard_num: , defaults to 4 +:type constraints_hard_num: int + +:param sa_num_sweeps: If using a simulated annealing solver, how many sweeps to perform per run of the algorithm. Default value :obj:`200`., defaults to 200 +:type sa_num_sweeps: int + +:param use_sample_persistence: Whether to use the sample persistence method of https://arxiv.org/abs/1606.07797 , which aims to improve the probability of a quantum annealer to obtain an optimal solution., defaults to False +:type use_sample_persistence: bool + +:param sample_persistence_solution_threshold: A threshold that is used to filter out higher-energy candidate solutions from the sample persistence method. A percentage that ranges from 0 to 1., defaults to 0.5 +:type sample_persistence_solution_threshold: float + +:param sample_persistence_persistence_threshold: A threshold between 0 and 1 such that a variable is fixed if its mean absolute value across the filtered sample is larger than the value of the threshold. Called fixing_threshold in the original paper., defaults to 0.5 +:type sample_persistence_persistence_threshold: float + +:param sample_persistence_persistence_iterations: The number of iterations to run the sample persistence algorithm. Generally speaking, more iterations will make the algorithm more successful, at the cost of increased computation time., defaults to 0 +:type sample_persistence_persistence_iterations: int + +:param google_num_steps: The number of QAOA steps implemented by the algorithm. Default value :obj:`1`., defaults to 1 +:type google_num_steps: int + +:param google_n_samples: The number of runs corresponding to the final sampling step. Default value :obj:`1000`., defaults to 1000 +:type google_n_samples: int + +:param google_arguments_optimizer: The dictionary that contains the parameters of the bayesian-optimization optimizer. Default value :obj:`{'init_point': 10, 'number_iter': 20, 'kappa': 2}`., defaults to {} +:type google_arguments_optimizer: dict + +:param google_step_sampling: Whether to sample the circuit with the current parameters at every step of the optimization (True) or just at the final one. Default value :obj:`True`., defaults to True +:type google_step_sampling: bool + +:param google_n_samples_step_sampling: The number of runs corresponding to sampling at every step of the optimization. Default value :obj:`1000`., defaults to 1000 +:type google_n_samples_step_sampling: int + +:param number_of_blocks: number of blocks to decompose problem into using random decomposition. Default value :obj: `1` meaning no decomposition., defaults to 1 +:type number_of_blocks: int + +:param iterations: number of iterations to cycle through when using random decomposition. Only valid if :obj:`number_of_blocks` is greater than 1. Each iterations corresponds to solving all blocks of the decomposition once. Default value :obj:`50`., defaults to 50 +:type iterations: int + +:param initial_solution: initial solution seed for constructing the blocks using random decomposition. If none is provided, a random solution is initialized. Default value :obj: `None`. :obj:`initial_solution` should be the same type as :obj:`Q`../, defaults to None +:type initial_solution: list + +:param always_update_with_best: solutions found using decomposition do not monotonically get better with each iterations. The best solution is always returned, but this flag determines whether or not to construct new decomposition using best solution. Default value :obj: `True`., defaults to True +:type always_update_with_best: bool + +:param update_q_each_block_solution: each blocks decomposed Q matrix can be constructed at the onset of block composition, or updated every time a block is solved. Default value :obj: `True`., defaults to True +:type update_q_each_block_solution: bool + +:param qaoa_nmeasurement: The number of measurements to use for the QAOA algorithm if a simulator is chosen. Leave at null to attempt an ideal Pauli measurement. Ideal Pauli measurements can only be used on backends which support statevectors and will raise an exception otherwise., defaults to None +:type qaoa_nmeasurement: int + +:param qaoa_optimizer: The optimizer to use for the QAOA algorithm if a simulator backend is chosen. Valid options are `COBYLA`, `bounded_Powell`, and `analytical`, or `None` if qaoa_beta and qaoa_gamma are provided., defaults to COBYLA +:type qaoa_optimizer: str + +:param qaoa_beta: A :math:`\beta` angle(s) to provide to the QAOA algorithm if a simulator backend is chosen. This can either be a float or a list of floats of length `qaoa_p_val`. Invalid unless qaoa_gamma is also provided and has the same length., defaults to None +:type qaoa_beta: float + +:param qaoa_gamma: A :math:`\gamma` angle(s) to provide to the QAOA algorithm if a simulator backend is chosen. This can either be a float or a list of floats of length `qaoa_p_val`. Invalid unless qaoa_beta is also provided and has the same length., defaults to None +:type qaoa_gamma: float + +:param qaoa_p_val: A p_val to provide the qaoa algorithm if a simulator backend is chosen. Must be equal to the number of :math:`\beta` and :math:`\gamma` angles provided in `qaoa_beta` and `qaoa_gamma`., defaults to 1 +:type qaoa_p_val: int + + +:return: A BinaryResults object +:rtype: BinaryResults +""" + pass diff --git a/qcware/forge/optimization/api/qaoa_expectation_value.py b/qcware/forge/optimization/api/qaoa_expectation_value.py new file mode 100644 index 0000000..4105443 --- /dev/null +++ b/qcware/forge/optimization/api/qaoa_expectation_value.py @@ -0,0 +1,51 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +from qcware.types.optimization import BinaryProblem, BinaryResults + +import numpy + +from typing import Optional + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call( + name="optimization.qaoa_expectation_value", + endpoint="optimization/qaoa_expectation_value", +) +def qaoa_expectation_value( + problem_instance: BinaryProblem, + beta: numpy.ndarray, + gamma: numpy.ndarray, + num_samples: Optional[int] = None, + backend: str = "qcware/cpu", +): + r"""Get the QAOA expectation value for a BinaryProblem instance. + This function sets up the conventional quantum state for the quantum approximate optimization algorithm (QAOA) based on the objective function in the BinaryProblem `problem_instance`. + Because this is conventional QAOA, `problem_instance` must be unconstrained: the user can add terms to their objective function to account for constraints. + This function can be used in a QAOA workflow to optimize or study angles. + + Arguments: + + :param problem_instance: Unconstrained BinaryProblem instance specifying the objective function. + :type problem_instance: BinaryProblem + + :param beta: NumPy array with shape (p,) giving beta angles as typically defined in the QAOA). + :type beta: numpy.ndarray + + :param gamma: NumPy array with shape (p,) giving gamma angles as typically defined in the QAOA). + :type gamma: numpy.ndarray + + :param num_samples: The number of measurements to use to estimate expectation value. When set to None (the default value), simulation is used (if the backend allows it) to get an exact expectation value. This can be much faster than using samples., defaults to None + :type num_samples: Optional[int] + + :param backend: String specifying the backend. Currently only [qcware/cpu] available, defaults to qcware/cpu + :type backend: str + + + :return: The expectation value for the QAOA state. + :rtype: ndarray""" + pass diff --git a/qcware/forge/optimization/api/qaoa_sample.py b/qcware/forge/optimization/api/qaoa_sample.py new file mode 100644 index 0000000..74267c1 --- /dev/null +++ b/qcware/forge/optimization/api/qaoa_sample.py @@ -0,0 +1,45 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +from qcware.types.optimization import BinaryProblem, BinaryResults + +import numpy + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call(name="optimization.qaoa_sample", endpoint="optimization/qaoa_sample") +def qaoa_sample( + problem_instance: BinaryProblem, + beta: numpy.ndarray, + gamma: numpy.ndarray, + num_samples: int, + backend: str = "qcware/cpu", +): + r"""Build a QAOA circuit for given (objective, angles) and sample. + This function sets up the conventional quantum state for the quantum approximate optimization algorithm (QAOA) based on the objective function in the BinaryProblem `problem_instance`. That state is then samples and a histogram of results is returned. + Because this is conventional QAOA, `problem_instance` must be unconstrained: the user can add terms to their objective function to account for constraints. + + Arguments: + + :param problem_instance: Unconstrained BinaryProblem instance specifying the objective function. + :type problem_instance: BinaryProblem + + :param beta: NumPy array with shape (p,) giving beta angles as typically defined in the QAOA). + :type beta: numpy.ndarray + + :param gamma: NumPy array with shape (p,) giving gamma angles as typically defined in the QAOA). + :type gamma: numpy.ndarray + + :param num_samples: The number of samples to take from the QAOA state. + :type num_samples: int + + :param backend: String specifying the backend. Currently only [qcware/cpu] available, defaults to qcware/cpu + :type backend: str + + + :return: BinaryResults providing a histogram of bit samples. + :rtype: BinaryResults""" + pass diff --git a/qcware/forge/py.typed b/qcware/forge/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/qcware/forge/qio/__init__.py b/qcware/forge/qio/__init__.py new file mode 100644 index 0000000..a40a3ca --- /dev/null +++ b/qcware/forge/qio/__init__.py @@ -0,0 +1,8 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +# only the following line is autogenerated; further imports can be added below +from .api import * + +# add further imports below this line diff --git a/qcware/forge/qio/api/__init__.py b/qcware/forge/qio/api/__init__.py new file mode 100644 index 0000000..ce0d4d1 --- /dev/null +++ b/qcware/forge/qio/api/__init__.py @@ -0,0 +1,5 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +from .loader import loader diff --git a/qcware/forge/qio/api/loader.py b/qcware/forge/qio/api/loader.py new file mode 100644 index 0000000..34581e1 --- /dev/null +++ b/qcware/forge/qio/api/loader.py @@ -0,0 +1,46 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +from typing import Optional, Tuple + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call(name="qio.loader", endpoint="qio/loader") +def loader( + data: numpy.ndarray, + mode: str = "optimized", + opt_shape: Optional[Tuple[int, ...]] = None, + initial: bool = True, + return_statevector_indices: bool = False, +): + r"""Creates a circuit which loads an array of classical data into the state space of a quantum computer or simulator. This is useful in order to act on known data or to simulator quantum RAM. + + Arguments: + + :param data: A 1-d array representing the classical data to be represented in the circuit + :type data: numpy.ndarray + + :param mode: Whether to used the "optimized" loader (using approximately :math:`~sqrt(d)` depth and :math:`~sqrt(d)` qubits) or the "parallel" loader (using approximately :math:`log(d)` depth and `d` qubits., defaults to optimized + :type mode: str + + :param opt_shape: If the loader is an optimized loader, this corresponds to the new shape of the input matrix, defaults to None + :type opt_shape: Optional[Tuple[int,...]] + + :param initial: Whether the loader is at the beginning of the circuit (in which it performs an initial X gate on the first qubit), defaults to True + :type initial: bool + + :param return_statevector_indices: True; return a tuple (circ, inds) of circuit and statevector indices False; return circuit circ, defaults to False + :type return_statevector_indices: bool + + + :return: circ: if return_statevector_indices == False (circ, inds): if return_statevector_indices == True + circ: A Quasar circuit suitable for execution on any quasar backend supporting the required + gates which loads the classical vector into a quantum state. + inds: Indices of the resulting statevector into which the classical data was loaded. + :rtype: quasar.Circuit: if return_statevector_indices == False tuple(quasar.Circuit, numpy.ndarray): if return_statevector_indices == True""" + pass diff --git a/qcware/forge/qml/__init__.py b/qcware/forge/qml/__init__.py new file mode 100644 index 0000000..32c9d3c --- /dev/null +++ b/qcware/forge/qml/__init__.py @@ -0,0 +1,9 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +# only the following line is autogenerated; further imports can be added below +from .api import * + +# add further imports below this line +from .types import * diff --git a/qcware/forge/qml/api/__init__.py b/qcware/forge/qml/api/__init__.py new file mode 100644 index 0000000..52827c9 --- /dev/null +++ b/qcware/forge/qml/api/__init__.py @@ -0,0 +1,9 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +from .fit_and_predict import fit_and_predict + +from .fit import fit + +from .predict import predict diff --git a/qcware/forge/qml/api/fit.py b/qcware/forge/qml/api/fit.py new file mode 100644 index 0000000..3d66280 --- /dev/null +++ b/qcware/forge/qml/api/fit.py @@ -0,0 +1,46 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +import numpy.typing + +from qcware.types.qml import FitData + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call(name="qml.fit", endpoint="qml/fit") +def fit( + X: numpy.typing.ArrayLike, + model: str, + y: numpy.typing.ArrayLike = None, + parameters: dict = {"num_measurements": 100}, + backend: str = "qcware/cpu_simulator", +): + r"""This function fits data to a quantum model for purposes of classification. + Four clustering models are implemented at this time (see parameter `model`) + + Arguments: + + :param X: Training data: :math:`(N\times d)` array containing training data + :type X: numpy.typing.ArrayLike + + :param model: String for the clustering model; one of ['QNearestCentroid', 'QNeighborsClassifier', 'QNeighborsRegressor', 'QMeans'] + :type model: str + + :param y: Label vector: length :math:`d` array containing respective labels of each data, defaults to None + :type y: numpy.typing.ArrayLike + + :param parameters: Dictionary containing parameters for the model, defaults to {'num_measurements': 100} + :type parameters: dict + + :param backend: String describing the backend to use, defaults to qcware/cpu_simulator + :type backend: str + + + :return: A FitData structure holding information to be passed back for the prediction step. + :rtype: FitData""" + pass diff --git a/qcware/forge/qml/api/fit_and_predict.py b/qcware/forge/qml/api/fit_and_predict.py new file mode 100644 index 0000000..918f5cb --- /dev/null +++ b/qcware/forge/qml/api/fit_and_predict.py @@ -0,0 +1,49 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +import numpy.typing + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call(name="qml.fit_and_predict", endpoint="qml/fit_and_predict") +def fit_and_predict( + X: numpy.typing.ArrayLike, + model: str, + y: numpy.typing.ArrayLike = None, + T: numpy.typing.ArrayLike = None, + parameters: dict = {"num_measurements": 100}, + backend: str = "qcware/cpu_simulator", +): + r"""This function combines both the fitting of data to a quantum model for the purposes of classification and also the use of that trained model for classifying new data. + The interface and use are similar to scikit-learn's fit and predict functions. At the present time, since the fit data comprises (in many cases) both classical and quantum data difficult to serialize, the fitting and prediction are done in a single step. We are looking to separate them into separate fit and predict steps in the future. + Four clustering models are implemented at this time (see parameter `model`) + + Arguments: + + :param X: Training data: :math:`(N\times d)` array containing training data + :type X: numpy.typing.ArrayLike + + :param model: String for the clustering model; one of ['QNearestCentroid', 'QNeighborsClassifier', 'QNeighborsRegressor', 'QMeans'] + :type model: str + + :param y: Label vector: length :math:`d` array containing respective labels of each data, defaults to None + :type y: numpy.typing.ArrayLike + + :param T: Test data: :math:`(M\times d)` array containing test data, defaults to None + :type T: numpy.typing.ArrayLike + + :param parameters: Dictionary containing parameters for the model, defaults to {'num_measurements': 100} + :type parameters: dict + + :param backend: String describing the backend to use, defaults to qcware/cpu_simulator + :type backend: str + + + :return: A numpy array the length of the test data `T` containing fit labels + :rtype: numpy.array""" + pass diff --git a/qcware/forge/qml/api/predict.py b/qcware/forge/qml/api/predict.py new file mode 100644 index 0000000..f478068 --- /dev/null +++ b/qcware/forge/qml/api/predict.py @@ -0,0 +1,35 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +import numpy.typing + +from qcware.types.qml import FitData + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call(name="qml.predict", endpoint="qml/predict") +def predict( + X: numpy.typing.ArrayLike, fit_data: FitData, backend: str = "qcware/cpu_simulator" +): + r"""Predicts classification labels for fitted data. + + Arguments: + + :param X: Test data: :math:`(M\times d)` array containing test data + :type X: numpy.typing.ArrayLike + + :param fit_data: A FitData instance representing a previous fitting operation + :type fit_data: FitData + + :param backend: String describing the backend to use, defaults to qcware/cpu_simulator + :type backend: str + + + :return: A numpy array the length of the test data `X` containing fit labels + :rtype:""" + pass diff --git a/qcware/forge/qml/types/__init__.py b/qcware/forge/qml/types/__init__.py new file mode 100644 index 0000000..3f9e618 --- /dev/null +++ b/qcware/forge/qml/types/__init__.py @@ -0,0 +1,102 @@ +from abc import ABC, abstractmethod +from typing import Optional, Tuple + +from qcware.forge.qml import fit, predict + + +class Classifier(ABC): + def fit(self, X, y=None): + self.fit_data = fit(X, type(self).__name__, y, self.parameters, self.backend) + + def predict(self, X): + result = predict(X, self.fit_data, self.backend) + return result + + +class QNearestCentroid(Classifier): + def __init__( + self, + loader_mode: str = "parallel", + backend: str = "qcware/cpu_simulator", + num_measurements: int = 100, + absolute: bool = False, + opt_shape: Optional[Tuple[int, int]] = None, + ): + self.parameters = dict( + loader_mode=loader_mode, + num_measurements=num_measurements, + absolute=absolute, + opt_shape=opt_shape, + ) + # we keep backend separate as it is passed separately to the API call + self.backend = backend + + +class QNeighborsRegressor(Classifier): + def __init__( + self, + n_neighbors: int = 3, + loader_mode: str = "parallel", + backend: str = "qcware/cpu_simulator", + num_measurements: int = 100, + absolute: bool = False, + opt_shape: Optional[Tuple[int, int]] = None, + ): + self.parameters = dict( + n_neighbors=n_neighbors, + loader_mode=loader_mode, + num_measurements=num_measurements, + absolute=absolute, + opt_shape=opt_shape, + ) + self.backend = backend + + +class QNeighborsClassifier(Classifier): + def __init__( + self, + n_neighbors: int = 3, + loader_mode: str = "parallel", + backend: str = "qcware/cpu_simulator", + num_measurements: int = 100, + absolute: bool = False, + opt_shape: Optional[Tuple[int, int]] = None, + ): + self.parameters = dict( + n_neighbors=n_neighbors, + loader_mode=loader_mode, + num_measurements=num_measurements, + absolute=absolute, + opt_shape=opt_shape, + ) + self.backend = backend + + +class QMeans(Classifier): + def __init__( + self, + n_clusters: int = 3, + init: str = "random", + n_init: int = 1, + max_iter: int = 10, + tol: float = 1e-4, + analysis: bool = False, + loader_mode: str = "parallel", + backend: str = "qcware/cpu_simulator", + num_measurements: int = 100, + absolute: bool = False, + opt_shape: Optional[Tuple[int, int]] = None, + ): + self.parameters = dict( + n_clusters=n_clusters, + init=init, + n_init=n_init, + max_iter=max_iter, + tol=tol, + analysis=analysis, + loader_mode=loader_mode, + num_measurements=num_measurements, + absolute=absolute, + opt_shape=opt_shape, + ) + self.backend = backend diff --git a/qcware/forge/qutils/__init__.py b/qcware/forge/qutils/__init__.py new file mode 100644 index 0000000..a40a3ca --- /dev/null +++ b/qcware/forge/qutils/__init__.py @@ -0,0 +1,8 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +# only the following line is autogenerated; further imports can be added below +from .api import * + +# add further imports below this line diff --git a/qcware/forge/qutils/api/__init__.py b/qcware/forge/qutils/api/__init__.py new file mode 100644 index 0000000..f344b06 --- /dev/null +++ b/qcware/forge/qutils/api/__init__.py @@ -0,0 +1,9 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +from .qdot import qdot + +from .create_qdot_circuit import create_qdot_circuit + +from .qdist import qdist diff --git a/qcware/forge/qutils/api/calculate_nisqAE_MLE.py b/qcware/forge/qutils/api/calculate_nisqAE_MLE.py new file mode 100644 index 0000000..cd1d439 --- /dev/null +++ b/qcware/forge/qutils/api/calculate_nisqAE_MLE.py @@ -0,0 +1,34 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +import quasar + +from typing import Sequence, Tuple + +import warnings +from ...api_calls import declare_api_call + + +@declare_api_call( + name="qutils.calculate_nisqAE_MLE", endpoint="qutils/calculate_nisqAE_MLE" +) +def calculate_nisqAE_MLE( + target_counts: Sequence[Tuple[Tuple[int, int], int]], epsilon: float +): + r"""Given the output of the run_nisqAE functions and estimates the parameter theta via MLE. + + Arguments: + + :param target_counts: For each element in the schedule, returns a tuple of (element, target_counts), where target_counts is the number of measurements whose outcome is in the set of targetStates. + :type target_counts: Sequence[Tuple[Tuple[int, int], int]] + + :param epsilon: The additive error within which we would like to calculate theta. + :type epsilon: float + + + :return: MLE estimate of theta. + :rtype: float""" + pass diff --git a/qcware/forge/qutils/api/create_qdot_circuit.py b/qcware/forge/qutils/api/create_qdot_circuit.py new file mode 100644 index 0000000..85d2d81 --- /dev/null +++ b/qcware/forge/qutils/api/create_qdot_circuit.py @@ -0,0 +1,40 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call( + name="qutils.create_qdot_circuit", endpoint="qutils/create_qdot_circuit" +) +def create_qdot_circuit( + x: numpy.ndarray, + y: numpy.ndarray, + loader_mode: str = "parallel", + absolute: bool = False, +): + r"""Creates a circuit which, when run, outputs the dot product of two 1d arrays; quantum analogue of:: + numpy.dot + + Arguments: + + :param x: 1d array + :type x: numpy.ndarray + + :param y: 1d array + :type y: numpy.ndarray + + :param loader_mode: Type of loader to use, one of parallel, diagonal, semi-diagonal, or optimized, defaults to parallel + :type loader_mode: str + + :param absolute: Whether to return the absolute value of output, defaults to False + :type absolute: bool + + + :return: A Quasar circuit suitable for execution on any quasar backend supporting the required gates which returns the dot product. + :rtype: quasar.Circuit""" + pass diff --git a/qcware/forge/qutils/api/make_nisqAE_schedule.py b/qcware/forge/qutils/api/make_nisqAE_schedule.py new file mode 100644 index 0000000..b3eec6f --- /dev/null +++ b/qcware/forge/qutils/api/make_nisqAE_schedule.py @@ -0,0 +1,49 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +import quasar + +from typing import Optional, Sequence, Tuple + +import warnings +from ...api_calls import declare_api_call + + +@declare_api_call( + name="qutils.make_nisqAE_schedule", endpoint="qutils/make_nisqAE_schedule" +) +def make_nisqAE_schedule( + epsilon: float, + schedule_type: str, + max_depth: Optional[int] = 20, + beta: Optional[float] = 0.5, + n_shots: Optional[float] = 20, +): + r"""Create a schedule for use in the run_nisqAE functions. + + Arguments: + + :param epsilon: The additive bound with which to approximate the amplitude + :type epsilon: float + + :param schedule_type: schedule_type in 'linear', 'exponential', 'powerlaw', 'classical' + :type schedule_type: str + + :param max_depth: The maximum number of times we should run the iteration circuit (does not affect 'powerlaw')., defaults to 20 + :type max_depth: Optional[int] + + :param beta: Beta parameter for powerlaw schedule (does not affect other schedule_types)., defaults to 0.5 + :type beta: Optional[float] + + :param n_shots: Number of measurements to take at each power, defaults to 20 + :type n_shots: Optional[float] + + + :return: A schedule for how many times to run the iteration_circuit, and how many shots to take. A List[Tuple[power, num_shots]], where: + - power is the number of times to run the iteration circuit + - num_shots is the number of shots to run at the given power + :rtype: Sequence[Tuple[int, int]]""" + pass diff --git a/qcware/forge/qutils/api/qdist.py b/qcware/forge/qutils/api/qdist.py new file mode 100644 index 0000000..68718db --- /dev/null +++ b/qcware/forge/qutils/api/qdist.py @@ -0,0 +1,64 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +import quasar + +from typing import Union, Optional, Tuple + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call(name="qutils.qdist", endpoint="qutils/qdist") +def qdist( + x: Union[float, numpy.ndarray], + y: Union[float, numpy.ndarray], + loader_mode: str = "parallel", + circuit: quasar.Circuit = None, + backend: str = "qcware/cpu_simulator", + num_measurements: int = 1000, + absolute: bool = False, + opt_shape: Optional[Tuple[int, ...]] = None, +): + r"""Outputs the distance between input vectors; quantum analogue of:: + numpy.linalg.norm(X - Y)**2 + + Cases (following numpy.dot): + x is 1d, y is 1d; performs vector - vector multiplication. Returns float. + x is 2d, y is 1d; performs matrix - vector multiplication. Returns 1d array. + x is 1d, y is 2d; performs vector - matrix multiplication. Returns 1d array. + x is 2d, y is 2d; performs matrix - matrix multiplication. Returns 2d array. + + Arguments: + + :param x: 1d or 2d array + :type x: Union[float, numpy.ndarray] + + :param y: 1d or 2d array + :type y: Union[float, numpy.ndarray] + + :param loader_mode: Type of loader to use, one of parallel, diagonal, semi-diagonal, or optimized, defaults to parallel + :type loader_mode: str + + :param circuit: Circuit to use for evaluation (None to implicitly create circuit), defaults to None + :type circuit: quasar.Circuit + + :param backend: String denoting the backend to use, defaults to qcware/cpu_simulator + :type backend: str + + :param num_measurements: Number of measurements; required, defaults to 1000 + :type num_measurements: int + + :param absolute: Whether to return the absolute value of the result, defaults to False + :type absolute: bool + + :param opt_shape: shape of the optimized loader's input (N1, N2), defaults to None + :type opt_shape: Optional[Tuple[int,...]] + + + :return: float, 1d array, or 2d array: distance estimation + :rtype: Union[float, numpy.ndarray]""" + pass diff --git a/qcware/forge/qutils/api/qdot.py b/qcware/forge/qutils/api/qdot.py new file mode 100644 index 0000000..a802719 --- /dev/null +++ b/qcware/forge/qutils/api/qdot.py @@ -0,0 +1,64 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +import quasar + +from typing import Union, Optional, Tuple + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call(name="qutils.qdot", endpoint="qutils/qdot") +def qdot( + x: Union[float, numpy.ndarray], + y: Union[float, numpy.ndarray], + loader_mode: str = "parallel", + circuit: quasar.Circuit = None, + backend: str = "qcware/cpu_simulator", + num_measurements: int = 1000, + absolute: bool = False, + opt_shape: Optional[Tuple[int, ...]] = None, +): + r"""Outputs the dot product of two arrays; quantum analogue of:: + numpy.dot + + Cases (following numpy.dot): + x is 1d, y is 1d; performs vector - vector multiplication. Returns float. + x is 2d, y is 1d; performs matrix - vector multiplication. Returns 1d array. + x is 1d, y is 2d; performs vector - matrix multiplication. Returns 1d array. + x is 2d, y is 2d; performs matrix - matrix multiplication. Returns 2d array. + + Arguments: + + :param x: 1d or 2d array + :type x: Union[float, numpy.ndarray] + + :param y: 1d or 2d array + :type y: Union[float, numpy.ndarray] + + :param loader_mode: Type of loader to use, one of parallel, diagonal, semi-diagonal, or optimized, defaults to parallel + :type loader_mode: str + + :param circuit: Circuit to use for evaluation (None to implicitly create circuit), defaults to None + :type circuit: quasar.Circuit + + :param backend: string describing the desired backend, defaults to qcware/cpu_simulator + :type backend: str + + :param num_measurements: Number of measurements (necessary for all backends), defaults to 1000 + :type num_measurements: int + + :param absolute: Whether to return the absolute value of output, defaults to False + :type absolute: bool + + :param opt_shape: Shape of optimal loader (N1, N2), defaults to None + :type opt_shape: Optional[Tuple[int,...]] + + + :return: float, 1d array, or 2d array: dot product + :rtype: Union[float, numpy.ndarray]""" + pass diff --git a/qcware/forge/qutils/api/run_nisqAE_schedule.py b/qcware/forge/qutils/api/run_nisqAE_schedule.py new file mode 100644 index 0000000..1bfdfbf --- /dev/null +++ b/qcware/forge/qutils/api/run_nisqAE_schedule.py @@ -0,0 +1,55 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +import quasar + +from typing import Sequence, Tuple + +import warnings +from ...api_calls import declare_api_call + + +@declare_api_call( + name="qutils.run_nisqAE_schedule", endpoint="qutils/run_nisqAE_schedule" +) +def run_nisqAE_schedule( + initial_circuit: quasar.Circuit, + iteration_circuit: quasar.Circuit, + target_qubits: Sequence[int], + target_states: Sequence[int], + schedule: Sequence[Tuple[int, int]], + backend: str = "qcware/cpu_simulator", +): + r"""Run a nisq variant of amplitude estimation and output circuit measurements for each circuit in the given schedule. + + Arguments: + + :param initial_circuit: The oracle circuit whose output we would like to estimate. + :type initial_circuit: quasar.Circuit + + :param iteration_circuit: The iteration circuit which we will run multiple times according to the schedule. + :type iteration_circuit: quasar.Circuit + + :param target_qubits: The qubits which will be measured after every shot and compared to the target_states below. + In the classic amplitude estimation problem, this is usually just [0]. + :type target_qubits: Sequence[int] + + :param target_states: The set of states states [in base-10 integer representation] which correspond to "successful" measurements of the target_qubits. If the target_qubits are measured as one of target_states at the end of a shot, target_counts will be incremented. + In the classic amplitude estimation problem, this is usually just [1]. + :type target_states: Sequence[int] + + :param schedule: A schedule for how many times to run the iteration_circuit, and how many shots to take. A List[Tuple[power, num_shots]], where: + - power is the number of times to run the iteration_circuit in a shot + - num_shots is the number of shots to run at the given power + :type schedule: Sequence[Tuple[int, int]] + + :param backend: String denoting the backend to use, defaults to qcware/cpu_simulator + :type backend: str + + + :return: For each element in the schedule, returns a tuple of (element, target_counts), where target_counts is the number of measurements whose outcome is in the set of target_states. + :rtype: Sequence[Tuple[Tuple[int, int], int]]""" + pass diff --git a/qcware/forge/qutils/api/run_nisqAE_unary.py b/qcware/forge/qutils/api/run_nisqAE_unary.py new file mode 100644 index 0000000..25d13b6 --- /dev/null +++ b/qcware/forge/qutils/api/run_nisqAE_unary.py @@ -0,0 +1,39 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import numpy + +import quasar + +from typing import Sequence, Tuple + +import warnings +from ...api_calls import declare_api_call + + +@declare_api_call(name="qutils.run_nisqAE_unary", endpoint="qutils/run_nisqAE_unary") +def run_nisqAE_unary( + circuit: quasar.Circuit, + schedule: Sequence[Tuple[int, int]], + backend: str = "qcware/cpu_simulator", +): + r"""Performs amplitude estimation routine for unary circuits, assuming the 0th qubit is the target and the target state is |1>, that is, we assume that the state of our system can be written as cos(theta)|0>|badStates> + sin(theta)|1>|0> where badStates is a set of unary states (i.e ones which only one qubit at a time is at state 1) and we're trying to estimate theta. + + Arguments: + + :param circuit: The oracle circuit whose output we would like to estimate + :type circuit: quasar.Circuit + + :param schedule: A schedule for how many times to run the iteration_circuit, and how many shots to take. A List[Tuple[power, num_shots]], where: + - power is the number of times to run the iteration circuit + - num_shots is the number of shots to run at the given power + :type schedule: Sequence[Tuple[int, int]] + + :param backend: String denoting the backend to use, defaults to qcware/cpu_simulator + :type backend: str + + + :return: For each element in the schedule, returns a tuple of (element, target_counts), where target_counts is the number of measurements whose outcome is in the set of targetStates. + :rtype: Sequence[Tuple[Tuple[int, int], int]]""" + pass diff --git a/qcware/forge/request.py b/qcware/forge/request.py new file mode 100644 index 0000000..5496e74 --- /dev/null +++ b/qcware/forge/request.py @@ -0,0 +1,50 @@ +import backoff +import requests +from qcware.forge.exceptions import ApiCallFailedError, ApiCallResultUnavailableError + +_client_session = None + + +def client_session() -> requests.Session: + """ + Singleton guardian for client session + """ + global _client_session + if _client_session is None: + _client_session = requests.Session() + return _client_session + + +def _fatal_code(e): + return 400 <= e.response.status_code < 500 + + +@backoff.on_exception( + backoff.expo, requests.exceptions.RequestException, max_tries=3, giveup=_fatal_code +) +def post_request(url, data): + return client_session().post(url, json=data) + + +@backoff.on_exception( + backoff.expo, requests.exceptions.RequestException, max_tries=3, giveup=_fatal_code +) +def get_request(url): + return client_session().get(url) + + +def post(url, data): + response = post_request(url, data) + if response.status_code >= 400: + print(response) + raise ApiCallFailedError(response.json().get("message", "No message")) + return response.json() + + +def get(url): + response = get_request(url) + if response.status_code >= 400: + raise ApiCallResultUnavailableError( + "Unable to retrieve result, please try again later or contact support" + ) + return response.text diff --git a/qcware/forge/test/__init__.py b/qcware/forge/test/__init__.py new file mode 100644 index 0000000..a40a3ca --- /dev/null +++ b/qcware/forge/test/__init__.py @@ -0,0 +1,8 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +# only the following line is autogenerated; further imports can be added below +from .api import * + +# add further imports below this line diff --git a/qcware/forge/test/api/__init__.py b/qcware/forge/test/api/__init__.py new file mode 100644 index 0000000..7bfa300 --- /dev/null +++ b/qcware/forge/test/api/__init__.py @@ -0,0 +1,5 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +from .echo import echo diff --git a/qcware/forge/test/api/echo.py b/qcware/forge/test/api/echo.py new file mode 100644 index 0000000..18e2122 --- /dev/null +++ b/qcware/forge/test/api/echo.py @@ -0,0 +1,21 @@ +# AUTO-GENERATED FILE - MODIFY AT OWN RISK +# Project: qcware +# Copyright (c) 2019 QC Ware Corp - All Rights Reserved + +import warnings +from qcware.forge.api_calls import declare_api_call + + +@declare_api_call(name="test.echo", endpoint="test/echo") +def echo(text: str = "hello world."): + r""" + + Arguments: + + :param text: The text to return, defaults to hello world. + :type text: str + + + :return: + :rtype:""" + pass diff --git a/qcware/optimization.py b/qcware/optimization.py deleted file mode 100644 index 34c260e..0000000 --- a/qcware/optimization.py +++ /dev/null @@ -1,96 +0,0 @@ -from . import request - - -def mat_to_dict(mat): - the_dict = {} - for i in range(mat.shape[0]): - for j in range(mat.shape[1]): - the_dict[(i, j)] = mat[i, j] - Q_new = {} - for it in the_dict.keys(): - val_loop = the_dict[it] - if (it[1], it[0]) in the_dict.keys() and it[1] != it[0] and it[1] > it[0]: - val_loop += the_dict[(it[1], it[0])] - Q_new[it] = val_loop - elif it[1] == it[0]: - Q_new[it] = the_dict[it] - return Q_new - - -# Note: this is good for both HOBOs and QUBOs -def solve_binary(key, - Q, - higher_order=False, - solver="dwave_software", - constraints_linear_A=[], - constraints_linear_b=[], - constraints_sat_max_runs=3100, - constraints_hard=False, - constraints_penalty_scaling_factor=1, - constraints_equality_R=[], - constraints_equality_c=[], - constraints_inequality_S=[], - constraints_inequality_d=[], - return_all_solutions=False, - num_runs=1000, - dwave_chain_coupling=-1.5, - dwave_optimize_chain_coupling=False, - dwave_num_runs_chain_coupling=1000, - dwave_use_dwave_embedder=False, - dwave_use_full_embedding=False, - dwave_use_gauges=False, - dwave_num_gauges=3, - dwave_num_runs_gauge_selection=500, - dwave_chain_coupling_pi_fraction=0.1, - dwave_embedding="", - sa_num_sweeps=200, - use_sample_persistence=False, - sample_persistence_solution_threshold=0.5, - sample_persistence_persistence_threshold=0.5, - sample_persistence_persistence_iterations=0, - cirq_num_steps=1, - cirq_n_samples=1000, - cirq_arguments_optimizer={}, - cirq_step_sampling=True, - cirq_n_samples_step_sampling=1000, - host="https://platform.qcware.com", - ): - params = { - "key": key, - "Q": mat_to_dict(Q) if not isinstance(Q, dict) else Q, - "higher_order": higher_order, - "solver": solver, - "constraints_linear_A": constraints_linear_A, - "constraints_linear_b": constraints_linear_b, - "constraints_sat_max_runs": constraints_sat_max_runs, - "constraints_hard": constraints_hard, - "constraints_penalty_scaling_factor": constraints_penalty_scaling_factor, - "constraints_equality_R": constraints_equality_R, - "constraints_equality_c": constraints_equality_c, - "constraints_inequality_S": constraints_inequality_S, - "constraints_inequality_d": constraints_inequality_d, - "return_all_solutions": return_all_solutions, - "num_runs": num_runs, - "dwave_chain_coupling": dwave_chain_coupling, - "dwave_optimize_chain_coupling": dwave_optimize_chain_coupling, - "dwave_num_runs_chain_coupling": dwave_num_runs_chain_coupling, - "dwave_use_dwave_embedder": dwave_use_dwave_embedder, - "dwave_use_full_embedding": dwave_use_full_embedding, - "dwave_use_gauges": dwave_use_gauges, - "dwave_num_gauges": dwave_num_gauges, - "dwave_num_runs_gauge_selection": dwave_num_runs_gauge_selection, - "dwave_chain_coupling_pi_fraction": dwave_chain_coupling_pi_fraction, - "dwave_embedding": dwave_embedding, - "sa_num_sweeps": sa_num_sweeps, - "use_sample_persistence": use_sample_persistence, - "sample_persistence_solution_threshold": sample_persistence_solution_threshold, - "sample_persistence_persistence_threshold": sample_persistence_persistence_threshold, - "sample_persistence_persistence_iterations": sample_persistence_persistence_iterations, - "cirq_num_steps": cirq_num_steps, - "cirq_n_samples": cirq_n_samples, - "cirq_arguments_optimizer": cirq_arguments_optimizer, - "cirq_step_sampling": cirq_step_sampling, - "cirq_n_samples_step_sampling": cirq_n_samples_step_sampling, - } - - return request.post(host + "/api/v2/solve_binary", params, "solve_binary") diff --git a/qcware/param_utils.py b/qcware/param_utils.py deleted file mode 100644 index 4592edd..0000000 --- a/qcware/param_utils.py +++ /dev/null @@ -1,140 +0,0 @@ -import params_pb2 -from google.protobuf import descriptor -import numpy as np - - -def convert(params, endpoint_type): - param_dict = params_pb2.params() - if endpoint_type != "solve_binary": - print('here') - param_dict = params_pb2.params_vqe() -# print(param_dict) - valid_keys = [f.name for f in param_dict.DESCRIPTOR.fields] - for k, v in params.items(): - if k in valid_keys: - python_to_proto(param_dict, k, v) - return param_dict - - -def isInt(a): - return isinstance(a, (int, np.integer)) - - -def python_to_proto(param_dict, k, v): - if k == "Q": - getattr(param_dict, k).CopyFrom(dict_to_protodict(v, isTensor=True)) - elif k == "constraints_linear_A": - getattr(param_dict, k).CopyFrom(dict_to_protodict(mat_to_dict(v))) - elif k == "constraints_linear_b": - getattr(param_dict, k).CopyFrom(vec_to_protovec(v)) - elif k == "constraints_equality_R" or k == "constraints_inequality_S": - getattr(param_dict, k).extend(mat_array_to_protodict_array(v)) - elif k == "constraints_equality_c" or k == "constraints_inequality_d": - getattr(param_dict, k).extend(vec_array_to_protovec_array(v)) - elif k == "cirq_arguments_optimizer": - getattr(param_dict, k).CopyFrom(dict_to_cirq_arguments_optimizer(v)) - elif k == "molecule": - getattr(param_dict, k).CopyFrom(array_to_molecule_vqe(v)) - elif k == "guess_amplitudes": - getattr(param_dict, k).CopyFrom(array_to_amplitudes_vqe(v)) - else: - # Must be a 'primitive' of some type - setattr(param_dict, k, v) - - -def mat_to_dict(mat, symmetrize=False): - the_dict = {} - for i in range(len(mat)): - for j in range(len(mat[i])): - the_dict[(i, j)] = mat[i][j] - if symmetrize: - new_dict = {} - for it in the_dict.keys(): - val = the_dict[it] - if (it[1], it[0]) in the_dict.keys() and it[1] != it[0] and it[1] > it[0]: - val += the_dict[(it[1], it[0])] - new_dict[it] = val - elif it[1] == it[0]: - new_dict[it] = the_dict[it] - return new_dict - else: - return the_dict - - -def dict_to_protodict(pydict, isTensor=False): - pb_obj = params_pb2.params.Tensor() if isTensor else params_pb2.params.Matrix() - for k, v in pydict.items(): - entry = pb_obj.entries.add() - if isTensor: - entry.indices.extend(k) - else: - entry.i = k[0] - entry.j = k[1] - if isInt(v): - entry.int_val = v - else: - entry.float_val = v - return pb_obj - - -def array_to_molecule_vqe(arr): - pb_obj = params_pb2.params_vqe.Molecule() - for atom in arr: - entry = pb_obj.entries.add() - entry.atom = atom[0] - for pos in atom[1]: - coord = entry.coord.add() - if isInt(pos): - coord.x_int = pos - else: - coord.x_float = pos - return pb_obj - - -def array_to_amplitudes_vqe(arr): - pb_obj = params_pb2.params_vqe.Vector2() - for amplitude in arr: - entry = pb_obj.entries.add() - if isInt(amplitude): - entry.int_val = amplitude - else: - entry.float_val = amplitude - return pb_obj - - -def mat_array_to_protodict_array(mat_array): - pb_matrices = [] - for mat in mat_array: - pbmat = dict_to_protodict(mat_to_dict(mat)) - pb_matrices.append(pbmat) - return pb_matrices - - -def vec_to_protovec(vec): - pb_vec = params_pb2.params.Vector() - for el in vec: - entry = pb_vec.entries.add() - if isInt(el): - entry.int_val = el - else: - entry.float_val = el - return pb_vec - - -def vec_array_to_protovec_array(vec_array): - pb_vecs = [] - for vec in vec_array: - pb_vec = vec_to_protovec(vec) - pb_vecs.append(pb_vec) - return pb_vecs - - -def dict_to_cirq_arguments_optimizer(pydict): - pb_obj = params_pb2.params.CirqArgumentsOptimizer() - if 'init_point' in pydict: - pb_obj.init_point = pydict['init_point'] - if 'number_iter' in pydict: - pb_obj.number_iter = pydict['number_iter'] - if 'kappa' in pydict: - pb_obj.kappa = pydict['kappa'] - return pb_obj diff --git a/qcware/params_pb2.py b/qcware/params_pb2.py deleted file mode 100755 index 5d58fcd..0000000 --- a/qcware/params_pb2.py +++ /dev/null @@ -1,1022 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: params.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -from google.protobuf import descriptor_pb2 -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='params.proto', - package='com.qcware', - syntax='proto2', - serialized_pb=_b('\n\x0cparams.proto\x12\ncom.qcware\"\xad\x0e\n\x06params\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x01Q\x18\x02 \x01(\x0b\x32\x19.com.qcware.params.Tensor\x12\x14\n\x0chigher_order\x18\x03 \x01(\x08\x12\x0e\n\x06solver\x18\x04 \x01(\t\x12\x37\n\x14\x63onstraints_linear_A\x18\x05 \x01(\x0b\x32\x19.com.qcware.params.Matrix\x12\x37\n\x14\x63onstraints_linear_b\x18\x06 \x01(\x0b\x32\x19.com.qcware.params.Vector\x12 \n\x18\x63onstraints_sat_max_runs\x18\x07 \x01(\x05\x12\x18\n\x10\x63onstraints_hard\x18\x08 \x01(\x08\x12*\n\"constraints_penalty_scaling_factor\x18\t \x01(\x01\x12\x39\n\x16\x63onstraints_equality_R\x18\n \x03(\x0b\x32\x19.com.qcware.params.Matrix\x12\x39\n\x16\x63onstraints_equality_c\x18\x0b \x03(\x0b\x32\x19.com.qcware.params.Vector\x12;\n\x18\x63onstraints_inequality_S\x18\x0c \x03(\x0b\x32\x19.com.qcware.params.Matrix\x12;\n\x18\x63onstraints_inequality_d\x18\r \x03(\x0b\x32\x19.com.qcware.params.Vector\x12\x1c\n\x14return_all_solutions\x18\x0e \x01(\x08\x12\x10\n\x08num_runs\x18\x0f \x01(\x05\x12\x1c\n\x14\x64wave_chain_coupling\x18\x10 \x01(\x01\x12%\n\x1d\x64wave_optimize_chain_coupling\x18\x11 \x01(\x08\x12%\n\x1d\x64wave_num_runs_chain_coupling\x18\x12 \x01(\x05\x12 \n\x18\x64wave_use_dwave_embedder\x18\x13 \x01(\x08\x12\x18\n\x10\x64wave_use_gauges\x18\x14 \x01(\x08\x12\x18\n\x10\x64wave_num_gauges\x18\x15 \x01(\x05\x12&\n\x1e\x64wave_num_runs_gauge_selection\x18\x16 \x01(\x05\x12(\n dwave_chain_coupling_pi_fraction\x18\x17 \x01(\x01\x12\x17\n\x0f\x64wave_embedding\x18\x18 \x01(\t\x12 \n\x18\x64wave_use_full_embedding\x18\x19 \x01(\x08\x12\x15\n\rsa_num_sweeps\x18\x1a \x01(\x05\x12\x1e\n\x16use_sample_persistence\x18\x1b \x01(\x08\x12-\n%sample_persistence_solution_threshold\x18\x1c \x01(\x01\x12\x30\n(sample_persistence_persistence_threshold\x18\x1d \x01(\x01\x12\x31\n)sample_persistence_persistence_iterations\x18\x1e \x01(\x05\x12\x16\n\x0e\x63irq_num_steps\x18\x1f \x01(\x05\x12\x16\n\x0e\x63irq_n_samples\x18 \x01(\x05\x12K\n\x18\x63irq_arguments_optimizer\x18! \x01(\x0b\x32).com.qcware.params.CirqArgumentsOptimizer\x12\x1a\n\x12\x63irq_step_sampling\x18\" \x01(\x08\x12$\n\x1c\x63irq_n_samples_step_sampling\x18# \x01(\x05\x1aT\n\x0bTensorEntry\x12\x0f\n\x07indices\x18\x01 \x03(\x05\x12\x11\n\x07int_val\x18\x02 \x01(\x05H\x00\x12\x13\n\tfloat_val\x18\x03 \x01(\x02H\x00\x42\x0c\n\nintOrFloat\x1a\x39\n\x06Tensor\x12/\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x1e.com.qcware.params.TensorEntry\x1aY\n\x0bMatrixEntry\x12\t\n\x01i\x18\x01 \x01(\x05\x12\t\n\x01j\x18\x02 \x01(\x05\x12\x11\n\x07int_val\x18\x03 \x01(\x05H\x00\x12\x13\n\tfloat_val\x18\x04 \x01(\x02H\x00\x42\x0c\n\nintOrFloat\x1a\x43\n\x0bVectorEntry\x12\x11\n\x07int_val\x18\x01 \x01(\x05H\x00\x12\x13\n\tfloat_val\x18\x02 \x01(\x02H\x00\x42\x0c\n\nintOrFloat\x1a\x39\n\x06Vector\x12/\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x1e.com.qcware.params.VectorEntry\x1a\x39\n\x06Matrix\x12/\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x1e.com.qcware.params.MatrixEntry\x1aP\n\x16\x43irqArgumentsOptimizer\x12\x12\n\ninit_point\x18\x01 \x01(\x05\x12\x13\n\x0bnumber_iter\x18\x02 \x01(\x05\x12\r\n\x05kappa\x18\x03 \x01(\x05\"\xf3\x04\n\nparams_vqe\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x31\n\x08molecule\x18\x02 \x02(\x0b\x32\x1f.com.qcware.params_vqe.Molecule\x12\r\n\x05\x62\x61sis\x18\x03 \x01(\t\x12\x0e\n\x06solver\x18\x04 \x01(\t\x12\x14\n\x0cmultiplicity\x18\x05 \x01(\x05\x12\x0e\n\x06\x63harge\x18\x06 \x01(\x05\x12\x10\n\x08sampling\x18\x07 \x01(\x08\x12\x17\n\x0fsampling_trials\x18\x08 \x01(\x05\x12\x38\n\x10guess_amplitudes\x18\t \x01(\x0b\x32\x1e.com.qcware.params_vqe.Vector2\x12\x15\n\rinitial_state\x18\n \x01(\t\x12\x11\n\tminimizer\x18\x0b \x01(\t\x1a\x44\n\x0cVector2Entry\x12\x11\n\x07int_val\x18\x01 \x01(\x05H\x00\x12\x13\n\tfloat_val\x18\x02 \x01(\x02H\x00\x42\x0c\n\nintOrFloat\x1a?\n\x07Vector2\x12\x34\n\x07\x65ntries\x18\x01 \x03(\x0b\x32#.com.qcware.params_vqe.Vector2Entry\x1a>\n\nCoordinate\x12\x0f\n\x05x_int\x18\x01 \x01(\x05H\x00\x12\x11\n\x07x_float\x18\x02 \x01(\x02H\x00\x42\x0c\n\nintOrFloat\x1aK\n\tAtomEntry\x12\x0c\n\x04\x61tom\x18\x01 \x02(\t\x12\x30\n\x05\x63oord\x18\x02 \x03(\x0b\x32!.com.qcware.params_vqe.Coordinate\x1a=\n\x08Molecule\x12\x31\n\x07\x65ntries\x18\x01 \x03(\x0b\x32 .com.qcware.params_vqe.AtomEntry') -) - - - - -_PARAMS_TENSORENTRY = _descriptor.Descriptor( - name='TensorEntry', - full_name='com.qcware.params.TensorEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='indices', full_name='com.qcware.params.TensorEntry.indices', index=0, - number=1, type=5, cpp_type=1, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='int_val', full_name='com.qcware.params.TensorEntry.int_val', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='float_val', full_name='com.qcware.params.TensorEntry.float_val', index=2, - number=3, type=2, cpp_type=6, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='intOrFloat', full_name='com.qcware.params.TensorEntry.intOrFloat', - index=0, containing_type=None, fields=[]), - ], - serialized_start=1363, - serialized_end=1447, -) - -_PARAMS_TENSOR = _descriptor.Descriptor( - name='Tensor', - full_name='com.qcware.params.Tensor', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='entries', full_name='com.qcware.params.Tensor.entries', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1449, - serialized_end=1506, -) - -_PARAMS_MATRIXENTRY = _descriptor.Descriptor( - name='MatrixEntry', - full_name='com.qcware.params.MatrixEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='i', full_name='com.qcware.params.MatrixEntry.i', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='j', full_name='com.qcware.params.MatrixEntry.j', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='int_val', full_name='com.qcware.params.MatrixEntry.int_val', index=2, - number=3, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='float_val', full_name='com.qcware.params.MatrixEntry.float_val', index=3, - number=4, type=2, cpp_type=6, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='intOrFloat', full_name='com.qcware.params.MatrixEntry.intOrFloat', - index=0, containing_type=None, fields=[]), - ], - serialized_start=1508, - serialized_end=1597, -) - -_PARAMS_VECTORENTRY = _descriptor.Descriptor( - name='VectorEntry', - full_name='com.qcware.params.VectorEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='int_val', full_name='com.qcware.params.VectorEntry.int_val', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='float_val', full_name='com.qcware.params.VectorEntry.float_val', index=1, - number=2, type=2, cpp_type=6, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='intOrFloat', full_name='com.qcware.params.VectorEntry.intOrFloat', - index=0, containing_type=None, fields=[]), - ], - serialized_start=1599, - serialized_end=1666, -) - -_PARAMS_VECTOR = _descriptor.Descriptor( - name='Vector', - full_name='com.qcware.params.Vector', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='entries', full_name='com.qcware.params.Vector.entries', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1668, - serialized_end=1725, -) - -_PARAMS_MATRIX = _descriptor.Descriptor( - name='Matrix', - full_name='com.qcware.params.Matrix', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='entries', full_name='com.qcware.params.Matrix.entries', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1727, - serialized_end=1784, -) - -_PARAMS_CIRQARGUMENTSOPTIMIZER = _descriptor.Descriptor( - name='CirqArgumentsOptimizer', - full_name='com.qcware.params.CirqArgumentsOptimizer', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='init_point', full_name='com.qcware.params.CirqArgumentsOptimizer.init_point', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='number_iter', full_name='com.qcware.params.CirqArgumentsOptimizer.number_iter', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='kappa', full_name='com.qcware.params.CirqArgumentsOptimizer.kappa', index=2, - number=3, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1786, - serialized_end=1866, -) - -_PARAMS = _descriptor.Descriptor( - name='params', - full_name='com.qcware.params', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='com.qcware.params.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='Q', full_name='com.qcware.params.Q', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='higher_order', full_name='com.qcware.params.higher_order', index=2, - number=3, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='solver', full_name='com.qcware.params.solver', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='constraints_linear_A', full_name='com.qcware.params.constraints_linear_A', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='constraints_linear_b', full_name='com.qcware.params.constraints_linear_b', index=5, - number=6, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='constraints_sat_max_runs', full_name='com.qcware.params.constraints_sat_max_runs', index=6, - number=7, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='constraints_hard', full_name='com.qcware.params.constraints_hard', index=7, - number=8, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='constraints_penalty_scaling_factor', full_name='com.qcware.params.constraints_penalty_scaling_factor', index=8, - number=9, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='constraints_equality_R', full_name='com.qcware.params.constraints_equality_R', index=9, - number=10, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='constraints_equality_c', full_name='com.qcware.params.constraints_equality_c', index=10, - number=11, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='constraints_inequality_S', full_name='com.qcware.params.constraints_inequality_S', index=11, - number=12, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='constraints_inequality_d', full_name='com.qcware.params.constraints_inequality_d', index=12, - number=13, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='return_all_solutions', full_name='com.qcware.params.return_all_solutions', index=13, - number=14, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='num_runs', full_name='com.qcware.params.num_runs', index=14, - number=15, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='dwave_chain_coupling', full_name='com.qcware.params.dwave_chain_coupling', index=15, - number=16, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='dwave_optimize_chain_coupling', full_name='com.qcware.params.dwave_optimize_chain_coupling', index=16, - number=17, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='dwave_num_runs_chain_coupling', full_name='com.qcware.params.dwave_num_runs_chain_coupling', index=17, - number=18, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='dwave_use_dwave_embedder', full_name='com.qcware.params.dwave_use_dwave_embedder', index=18, - number=19, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='dwave_use_gauges', full_name='com.qcware.params.dwave_use_gauges', index=19, - number=20, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='dwave_num_gauges', full_name='com.qcware.params.dwave_num_gauges', index=20, - number=21, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='dwave_num_runs_gauge_selection', full_name='com.qcware.params.dwave_num_runs_gauge_selection', index=21, - number=22, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='dwave_chain_coupling_pi_fraction', full_name='com.qcware.params.dwave_chain_coupling_pi_fraction', index=22, - number=23, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='dwave_embedding', full_name='com.qcware.params.dwave_embedding', index=23, - number=24, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='dwave_use_full_embedding', full_name='com.qcware.params.dwave_use_full_embedding', index=24, - number=25, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='sa_num_sweeps', full_name='com.qcware.params.sa_num_sweeps', index=25, - number=26, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='use_sample_persistence', full_name='com.qcware.params.use_sample_persistence', index=26, - number=27, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='sample_persistence_solution_threshold', full_name='com.qcware.params.sample_persistence_solution_threshold', index=27, - number=28, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='sample_persistence_persistence_threshold', full_name='com.qcware.params.sample_persistence_persistence_threshold', index=28, - number=29, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='sample_persistence_persistence_iterations', full_name='com.qcware.params.sample_persistence_persistence_iterations', index=29, - number=30, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='cirq_num_steps', full_name='com.qcware.params.cirq_num_steps', index=30, - number=31, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='cirq_n_samples', full_name='com.qcware.params.cirq_n_samples', index=31, - number=32, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='cirq_arguments_optimizer', full_name='com.qcware.params.cirq_arguments_optimizer', index=32, - number=33, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='cirq_step_sampling', full_name='com.qcware.params.cirq_step_sampling', index=33, - number=34, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='cirq_n_samples_step_sampling', full_name='com.qcware.params.cirq_n_samples_step_sampling', index=34, - number=35, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[_PARAMS_TENSORENTRY, _PARAMS_TENSOR, _PARAMS_MATRIXENTRY, _PARAMS_VECTORENTRY, _PARAMS_VECTOR, _PARAMS_MATRIX, _PARAMS_CIRQARGUMENTSOPTIMIZER, ], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=29, - serialized_end=1866, -) - - -_PARAMS_VQE_VECTOR2ENTRY = _descriptor.Descriptor( - name='Vector2Entry', - full_name='com.qcware.params_vqe.Vector2Entry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='int_val', full_name='com.qcware.params_vqe.Vector2Entry.int_val', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='float_val', full_name='com.qcware.params_vqe.Vector2Entry.float_val', index=1, - number=2, type=2, cpp_type=6, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='intOrFloat', full_name='com.qcware.params_vqe.Vector2Entry.intOrFloat', - index=0, containing_type=None, fields=[]), - ], - serialized_start=2159, - serialized_end=2227, -) - -_PARAMS_VQE_VECTOR2 = _descriptor.Descriptor( - name='Vector2', - full_name='com.qcware.params_vqe.Vector2', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='entries', full_name='com.qcware.params_vqe.Vector2.entries', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2229, - serialized_end=2292, -) - -_PARAMS_VQE_COORDINATE = _descriptor.Descriptor( - name='Coordinate', - full_name='com.qcware.params_vqe.Coordinate', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='x_int', full_name='com.qcware.params_vqe.Coordinate.x_int', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='x_float', full_name='com.qcware.params_vqe.Coordinate.x_float', index=1, - number=2, type=2, cpp_type=6, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='intOrFloat', full_name='com.qcware.params_vqe.Coordinate.intOrFloat', - index=0, containing_type=None, fields=[]), - ], - serialized_start=2294, - serialized_end=2356, -) - -_PARAMS_VQE_ATOMENTRY = _descriptor.Descriptor( - name='AtomEntry', - full_name='com.qcware.params_vqe.AtomEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='atom', full_name='com.qcware.params_vqe.AtomEntry.atom', index=0, - number=1, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='coord', full_name='com.qcware.params_vqe.AtomEntry.coord', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2358, - serialized_end=2433, -) - -_PARAMS_VQE_MOLECULE = _descriptor.Descriptor( - name='Molecule', - full_name='com.qcware.params_vqe.Molecule', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='entries', full_name='com.qcware.params_vqe.Molecule.entries', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2435, - serialized_end=2496, -) - -_PARAMS_VQE = _descriptor.Descriptor( - name='params_vqe', - full_name='com.qcware.params_vqe', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='com.qcware.params_vqe.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='molecule', full_name='com.qcware.params_vqe.molecule', index=1, - number=2, type=11, cpp_type=10, label=2, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='basis', full_name='com.qcware.params_vqe.basis', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='solver', full_name='com.qcware.params_vqe.solver', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='multiplicity', full_name='com.qcware.params_vqe.multiplicity', index=4, - number=5, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='charge', full_name='com.qcware.params_vqe.charge', index=5, - number=6, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='sampling', full_name='com.qcware.params_vqe.sampling', index=6, - number=7, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='sampling_trials', full_name='com.qcware.params_vqe.sampling_trials', index=7, - number=8, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='guess_amplitudes', full_name='com.qcware.params_vqe.guess_amplitudes', index=8, - number=9, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='initial_state', full_name='com.qcware.params_vqe.initial_state', index=9, - number=10, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='minimizer', full_name='com.qcware.params_vqe.minimizer', index=10, - number=11, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[_PARAMS_VQE_VECTOR2ENTRY, _PARAMS_VQE_VECTOR2, _PARAMS_VQE_COORDINATE, _PARAMS_VQE_ATOMENTRY, _PARAMS_VQE_MOLECULE, ], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1869, - serialized_end=2496, -) - -_PARAMS_TENSORENTRY.containing_type = _PARAMS -_PARAMS_TENSORENTRY.oneofs_by_name['intOrFloat'].fields.append( - _PARAMS_TENSORENTRY.fields_by_name['int_val']) -_PARAMS_TENSORENTRY.fields_by_name['int_val'].containing_oneof = _PARAMS_TENSORENTRY.oneofs_by_name['intOrFloat'] -_PARAMS_TENSORENTRY.oneofs_by_name['intOrFloat'].fields.append( - _PARAMS_TENSORENTRY.fields_by_name['float_val']) -_PARAMS_TENSORENTRY.fields_by_name['float_val'].containing_oneof = _PARAMS_TENSORENTRY.oneofs_by_name['intOrFloat'] -_PARAMS_TENSOR.fields_by_name['entries'].message_type = _PARAMS_TENSORENTRY -_PARAMS_TENSOR.containing_type = _PARAMS -_PARAMS_MATRIXENTRY.containing_type = _PARAMS -_PARAMS_MATRIXENTRY.oneofs_by_name['intOrFloat'].fields.append( - _PARAMS_MATRIXENTRY.fields_by_name['int_val']) -_PARAMS_MATRIXENTRY.fields_by_name['int_val'].containing_oneof = _PARAMS_MATRIXENTRY.oneofs_by_name['intOrFloat'] -_PARAMS_MATRIXENTRY.oneofs_by_name['intOrFloat'].fields.append( - _PARAMS_MATRIXENTRY.fields_by_name['float_val']) -_PARAMS_MATRIXENTRY.fields_by_name['float_val'].containing_oneof = _PARAMS_MATRIXENTRY.oneofs_by_name['intOrFloat'] -_PARAMS_VECTORENTRY.containing_type = _PARAMS -_PARAMS_VECTORENTRY.oneofs_by_name['intOrFloat'].fields.append( - _PARAMS_VECTORENTRY.fields_by_name['int_val']) -_PARAMS_VECTORENTRY.fields_by_name['int_val'].containing_oneof = _PARAMS_VECTORENTRY.oneofs_by_name['intOrFloat'] -_PARAMS_VECTORENTRY.oneofs_by_name['intOrFloat'].fields.append( - _PARAMS_VECTORENTRY.fields_by_name['float_val']) -_PARAMS_VECTORENTRY.fields_by_name['float_val'].containing_oneof = _PARAMS_VECTORENTRY.oneofs_by_name['intOrFloat'] -_PARAMS_VECTOR.fields_by_name['entries'].message_type = _PARAMS_VECTORENTRY -_PARAMS_VECTOR.containing_type = _PARAMS -_PARAMS_MATRIX.fields_by_name['entries'].message_type = _PARAMS_MATRIXENTRY -_PARAMS_MATRIX.containing_type = _PARAMS -_PARAMS_CIRQARGUMENTSOPTIMIZER.containing_type = _PARAMS -_PARAMS.fields_by_name['Q'].message_type = _PARAMS_TENSOR -_PARAMS.fields_by_name['constraints_linear_A'].message_type = _PARAMS_MATRIX -_PARAMS.fields_by_name['constraints_linear_b'].message_type = _PARAMS_VECTOR -_PARAMS.fields_by_name['constraints_equality_R'].message_type = _PARAMS_MATRIX -_PARAMS.fields_by_name['constraints_equality_c'].message_type = _PARAMS_VECTOR -_PARAMS.fields_by_name['constraints_inequality_S'].message_type = _PARAMS_MATRIX -_PARAMS.fields_by_name['constraints_inequality_d'].message_type = _PARAMS_VECTOR -_PARAMS.fields_by_name['cirq_arguments_optimizer'].message_type = _PARAMS_CIRQARGUMENTSOPTIMIZER -_PARAMS_VQE_VECTOR2ENTRY.containing_type = _PARAMS_VQE -_PARAMS_VQE_VECTOR2ENTRY.oneofs_by_name['intOrFloat'].fields.append( - _PARAMS_VQE_VECTOR2ENTRY.fields_by_name['int_val']) -_PARAMS_VQE_VECTOR2ENTRY.fields_by_name['int_val'].containing_oneof = _PARAMS_VQE_VECTOR2ENTRY.oneofs_by_name['intOrFloat'] -_PARAMS_VQE_VECTOR2ENTRY.oneofs_by_name['intOrFloat'].fields.append( - _PARAMS_VQE_VECTOR2ENTRY.fields_by_name['float_val']) -_PARAMS_VQE_VECTOR2ENTRY.fields_by_name['float_val'].containing_oneof = _PARAMS_VQE_VECTOR2ENTRY.oneofs_by_name['intOrFloat'] -_PARAMS_VQE_VECTOR2.fields_by_name['entries'].message_type = _PARAMS_VQE_VECTOR2ENTRY -_PARAMS_VQE_VECTOR2.containing_type = _PARAMS_VQE -_PARAMS_VQE_COORDINATE.containing_type = _PARAMS_VQE -_PARAMS_VQE_COORDINATE.oneofs_by_name['intOrFloat'].fields.append( - _PARAMS_VQE_COORDINATE.fields_by_name['x_int']) -_PARAMS_VQE_COORDINATE.fields_by_name['x_int'].containing_oneof = _PARAMS_VQE_COORDINATE.oneofs_by_name['intOrFloat'] -_PARAMS_VQE_COORDINATE.oneofs_by_name['intOrFloat'].fields.append( - _PARAMS_VQE_COORDINATE.fields_by_name['x_float']) -_PARAMS_VQE_COORDINATE.fields_by_name['x_float'].containing_oneof = _PARAMS_VQE_COORDINATE.oneofs_by_name['intOrFloat'] -_PARAMS_VQE_ATOMENTRY.fields_by_name['coord'].message_type = _PARAMS_VQE_COORDINATE -_PARAMS_VQE_ATOMENTRY.containing_type = _PARAMS_VQE -_PARAMS_VQE_MOLECULE.fields_by_name['entries'].message_type = _PARAMS_VQE_ATOMENTRY -_PARAMS_VQE_MOLECULE.containing_type = _PARAMS_VQE -_PARAMS_VQE.fields_by_name['molecule'].message_type = _PARAMS_VQE_MOLECULE -_PARAMS_VQE.fields_by_name['guess_amplitudes'].message_type = _PARAMS_VQE_VECTOR2 -DESCRIPTOR.message_types_by_name['params'] = _PARAMS -DESCRIPTOR.message_types_by_name['params_vqe'] = _PARAMS_VQE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -params = _reflection.GeneratedProtocolMessageType('params', (_message.Message,), dict( - - TensorEntry = _reflection.GeneratedProtocolMessageType('TensorEntry', (_message.Message,), dict( - DESCRIPTOR = _PARAMS_TENSORENTRY, - __module__ = 'params_pb2' - # @@protoc_insertion_point(class_scope:com.qcware.params.TensorEntry) - )) - , - - Tensor = _reflection.GeneratedProtocolMessageType('Tensor', (_message.Message,), dict( - DESCRIPTOR = _PARAMS_TENSOR, - __module__ = 'params_pb2' - # @@protoc_insertion_point(class_scope:com.qcware.params.Tensor) - )) - , - - MatrixEntry = _reflection.GeneratedProtocolMessageType('MatrixEntry', (_message.Message,), dict( - DESCRIPTOR = _PARAMS_MATRIXENTRY, - __module__ = 'params_pb2' - # @@protoc_insertion_point(class_scope:com.qcware.params.MatrixEntry) - )) - , - - VectorEntry = _reflection.GeneratedProtocolMessageType('VectorEntry', (_message.Message,), dict( - DESCRIPTOR = _PARAMS_VECTORENTRY, - __module__ = 'params_pb2' - # @@protoc_insertion_point(class_scope:com.qcware.params.VectorEntry) - )) - , - - Vector = _reflection.GeneratedProtocolMessageType('Vector', (_message.Message,), dict( - DESCRIPTOR = _PARAMS_VECTOR, - __module__ = 'params_pb2' - # @@protoc_insertion_point(class_scope:com.qcware.params.Vector) - )) - , - - Matrix = _reflection.GeneratedProtocolMessageType('Matrix', (_message.Message,), dict( - DESCRIPTOR = _PARAMS_MATRIX, - __module__ = 'params_pb2' - # @@protoc_insertion_point(class_scope:com.qcware.params.Matrix) - )) - , - - CirqArgumentsOptimizer = _reflection.GeneratedProtocolMessageType('CirqArgumentsOptimizer', (_message.Message,), dict( - DESCRIPTOR = _PARAMS_CIRQARGUMENTSOPTIMIZER, - __module__ = 'params_pb2' - # @@protoc_insertion_point(class_scope:com.qcware.params.CirqArgumentsOptimizer) - )) - , - DESCRIPTOR = _PARAMS, - __module__ = 'params_pb2' - # @@protoc_insertion_point(class_scope:com.qcware.params) - )) -_sym_db.RegisterMessage(params) -_sym_db.RegisterMessage(params.TensorEntry) -_sym_db.RegisterMessage(params.Tensor) -_sym_db.RegisterMessage(params.MatrixEntry) -_sym_db.RegisterMessage(params.VectorEntry) -_sym_db.RegisterMessage(params.Vector) -_sym_db.RegisterMessage(params.Matrix) -_sym_db.RegisterMessage(params.CirqArgumentsOptimizer) - -params_vqe = _reflection.GeneratedProtocolMessageType('params_vqe', (_message.Message,), dict( - - Vector2Entry = _reflection.GeneratedProtocolMessageType('Vector2Entry', (_message.Message,), dict( - DESCRIPTOR = _PARAMS_VQE_VECTOR2ENTRY, - __module__ = 'params_pb2' - # @@protoc_insertion_point(class_scope:com.qcware.params_vqe.Vector2Entry) - )) - , - - Vector2 = _reflection.GeneratedProtocolMessageType('Vector2', (_message.Message,), dict( - DESCRIPTOR = _PARAMS_VQE_VECTOR2, - __module__ = 'params_pb2' - # @@protoc_insertion_point(class_scope:com.qcware.params_vqe.Vector2) - )) - , - - Coordinate = _reflection.GeneratedProtocolMessageType('Coordinate', (_message.Message,), dict( - DESCRIPTOR = _PARAMS_VQE_COORDINATE, - __module__ = 'params_pb2' - # @@protoc_insertion_point(class_scope:com.qcware.params_vqe.Coordinate) - )) - , - - AtomEntry = _reflection.GeneratedProtocolMessageType('AtomEntry', (_message.Message,), dict( - DESCRIPTOR = _PARAMS_VQE_ATOMENTRY, - __module__ = 'params_pb2' - # @@protoc_insertion_point(class_scope:com.qcware.params_vqe.AtomEntry) - )) - , - - Molecule = _reflection.GeneratedProtocolMessageType('Molecule', (_message.Message,), dict( - DESCRIPTOR = _PARAMS_VQE_MOLECULE, - __module__ = 'params_pb2' - # @@protoc_insertion_point(class_scope:com.qcware.params_vqe.Molecule) - )) - , - DESCRIPTOR = _PARAMS_VQE, - __module__ = 'params_pb2' - # @@protoc_insertion_point(class_scope:com.qcware.params_vqe) - )) -_sym_db.RegisterMessage(params_vqe) -_sym_db.RegisterMessage(params_vqe.Vector2Entry) -_sym_db.RegisterMessage(params_vqe.Vector2) -_sym_db.RegisterMessage(params_vqe.Coordinate) -_sym_db.RegisterMessage(params_vqe.AtomEntry) -_sym_db.RegisterMessage(params_vqe.Molecule) - - -# @@protoc_insertion_point(module_scope) diff --git a/qcware/physics.py b/qcware/physics.py deleted file mode 100644 index d7be76b..0000000 --- a/qcware/physics.py +++ /dev/null @@ -1,51 +0,0 @@ -from . import request - - -def mat_to_dict(mat): - the_dict = {} - for i in range(mat.shape[0]): - for j in range(mat.shape[1]): - the_dict[(i, j)] = mat[i, j] - Q_new = {} - for it in the_dict.keys(): - val_loop = the_dict[it] - if (it[1], it[0]) in the_dict.keys() and it[1] != it[0] and it[1] > it[0]: - val_loop += the_dict[(it[1], it[0])] - Q_new[it] = val_loop - elif it[1] == it[0]: - Q_new[it] = the_dict[it] - return Q_new - - -# VQE call - -def find_ground_state_energy(key, - molecule, - basis='sto-3g', - solver='projectq', - multiplicity=1, - charge=0, - sampling=False, - sampling_trials=1000, - guess_amplitudes=None, - initial_state='UCCSD', - minimizer='swarm', - - host="https://platform.qcware.com", - ): - - params = { - "key": key, - "molecule": molecule, - "basis": basis, - "solver": solver, - "multiplicity": multiplicity, - "charge": charge, - "sampling": sampling, - "sampling_trials": sampling_trials, - 'guess_amplitudes': guess_amplitudes, - 'initial_state': initial_state, - 'minimizer': minimizer - } - - return request.post(host + "/api/v2/find_ground_state_energy", params, 'VQE') diff --git a/qcware/request.py b/qcware/request.py deleted file mode 100644 index c402d21..0000000 --- a/qcware/request.py +++ /dev/null @@ -1,32 +0,0 @@ -import ast -import json -import pickle -import requests -import param_utils - - -def pickle_json(json_object): - if isinstance(json_object, dict): - r = {} - for k, v in json_object.items(): - if k != 'Q': - r[k] = pickle_json(v) - else: - r['Q'] = pickle.dumps(v, protocol=0) - return r - elif isinstance(json_object, list): - return [pickle_json(elem) for elem in json_object] - else: - return pickle.dumps(json_object, protocol=0) - - -def post(api_endpoint_url, param_dictionary, endpoint_type): - pbuffed_params = param_utils.convert(param_dictionary, endpoint_type) - r = requests.post(api_endpoint_url, - data=pbuffed_params.SerializeToString()) - - r = json.loads(r.text) - if r.get('solution') and endpoint_type == 'solve_binary': - r['solution'] = ast.literal_eval(r['solution']) - - return r diff --git a/qcware/serialization/__init__.py b/qcware/serialization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qcware/serialization/py.typed b/qcware/serialization/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/qcware/serialization/serialize_quasar.py b/qcware/serialization/serialize_quasar.py new file mode 100644 index 0000000..cb56d29 --- /dev/null +++ b/qcware/serialization/serialize_quasar.py @@ -0,0 +1,324 @@ +# helper routines to serialize to/from quasar circuits +import re +from quasar.circuit import Circuit, CompositeGate, ControlledGate, Gate +from quasar.pauli import PauliString, Pauli +from quasar.measurement import ProbabilityHistogram +from qcware.serialization.transforms.helpers import ( + ndarray_to_dict, + dict_to_ndarray, + scalar_to_dict, + dict_to_scalar, +) +import numpy as np +from collections.abc import Iterable +from typing import List, Tuple, Dict, Mapping +import json +import lz4 +import base64 +from sortedcontainers import SortedSet, SortedDict + + +def q_instruction_to_s(k, v): + """ + Trivial serialization: taking a gate from a quasar + circuit, gets the name, a dict of the parameters for + instantiating the gate, and the list of bits + to apply the gate + (gatename, parameters, bits); returns a dict + of the form { "gate": str, + "parameters": (dict(parmname: parmval)), + "bits": tuple(int), + "times": tuple(int) } + """ + if isinstance(v, CompositeGate): + return dict( + gate="CompositeGate", + parameters=dict( + name=v.name, + circuit=list(quasar_to_sequence(v.circuit)), + ascii_symbols=v.ascii_symbols, + ), + bits=k[1], + times=k[0], + ) + elif isinstance(v, ControlledGate): + return dict( + gate="ControlledGate", + parameters=dict( + gate=q_instruction_to_s([None, None], v.gate), controls=v.controls + ), + bits=k[1], + times=k[0], + ) + # for the U1 and U2 unitary gates, Quasar doesn't store the parameter U, + # so we must call the operator function to extract it + elif v.name in ["U1", "U2"]: + newparms = dict(U=ndarray_to_dict(v.operator_function(None))) + return dict(gate=v.name, parameters=newparms, bits=k[1], times=k[0]) + elif base_gate_name(v.name) not in Canonical_gate_names: + raise GateSerializationNotImplementedError(v.name) + else: + return dict(gate=v.name, parameters=dict(v.parameters), bits=k[1], times=k[0]) + + +def wrap_gate(fn): + """ + Gets a gate creation function and passes arguments if they + exist + """ + return lambda parms: fn(**parms) if len(parms) > 0 else fn + + +def wrap_composite_gate(parms): + cg_circuit = sequence_to_quasar(parms["circuit"]) + gate = CompositeGate(cg_circuit, parms["name"], parms["ascii_symbols"]) + # make the bit list into a tuple since this is needed in hashland + # see quasar add_gate method + return gate + + +def wrap_controlled_gate(parms): + gate = make_gate(parms["gate"]["gate"], parms["gate"]["parameters"]) + result = ControlledGate(gate, parms["controls"]) + return result + + +# this is just copied/pasted from 'dir (Gate)' in Python and taking +# the list of gate names from that. It will need to be updated if + +adjoint_re = re.compile(r"\^\+") +gate_re = re.compile(r"([\w]*)(\^\+)*") +adjoint_str = "^+" + + +def base_gate_name(gate_name: str) -> str: + "Return the base gate name (without adjoint markers)" + # return gate_re.search(gate_name).group(1) + adjoint_index = gate_name.find(adjoint_str) + return gate_name if adjoint_index == -1 else gate_name[0:adjoint_index] + + +def num_adjoints(gate_name: str) -> int: + "the number of adjoint markers in this gate name" + # return len(adjoint_re.findall(gate_name)) + return gate_name.count(adjoint_str) + + +# more gates are added to the base Gate class and there's not a way +# to programatically get it (eg from a SimpleNamespace) +Canonical_gate_names = [ + "CCX", + "CF", + "CS", + "CST", + "CSWAP", + "CX", + "CY", + "CZ", + "H", + "I", + "R_ion", + "Rx", + "Rx2", + "Rx2T", + "Rx_ion", + "Ry", + "Ry_ion", + "Rz", + "Rz_ion", + "S", + "SO4", + "SO42", + "ST", + "SWAP", + "T", + "TT", + "U1", + "U2", + "X", + "XX_ion", + "Y", + "Z", + "u1", + "u2", + "u3", + "RBS", +] + +Name_to_gatefn = { + "CCX": wrap_gate(Gate.CCX), + "CF": wrap_gate(Gate.CF), + "CS": wrap_gate(Gate.CS), + "CST": wrap_gate(Gate.CST), + "CSWAP": wrap_gate(Gate.CSWAP), + "CX": wrap_gate(Gate.CX), + "CY": wrap_gate(Gate.CY), + "CZ": wrap_gate(Gate.CZ), + "H": wrap_gate(Gate.H), + "I": wrap_gate(Gate.I), + "R_ion": wrap_gate(Gate.R_ion), + "Rx": wrap_gate(Gate.Rx), + "Rx2": wrap_gate(Gate.Rx2), + "Rx2T": wrap_gate(Gate.Rx2T), + "Rx_ion": wrap_gate(Gate.Rx_ion), + "Ry": wrap_gate(Gate.Ry), + "Ry_ion": wrap_gate(Gate.Ry_ion), + "Rz": wrap_gate(Gate.Rz), + "Rz_ion": wrap_gate(Gate.Rz_ion), + "RBS": wrap_gate(Gate.RBS), + "S": wrap_gate(Gate.S), + "SO4": wrap_gate(Gate.SO4), + "SO42": wrap_gate(Gate.SO42), + "ST": wrap_gate(Gate.ST), + "SWAP": wrap_gate(Gate.SWAP), + "T": wrap_gate(Gate.T), + "U1": wrap_gate(Gate.U1), + "U2": wrap_gate(Gate.U2), + "X": wrap_gate(Gate.X), + "XX_ion": wrap_gate(Gate.XX_ion), + "Y": wrap_gate(Gate.Y), + "Z": wrap_gate(Gate.Z), + "CompositeGate": wrap_composite_gate, + "ControlledGate": wrap_controlled_gate, + "u1": wrap_gate(Gate.u1), + "u2": wrap_gate(Gate.u2), + "u3": wrap_gate(Gate.u3), +} + + +class GateSerializationNotImplementedError(NotImplementedError): + pass + + +def quasar_to_dict(q: Circuit) -> Dict: + """ + Returns a serializable dict object suitable for conversion to JSON + or other simple format, with format + { "instructions": sequence of dicts in form q_instruction_to_s, + "qubits": sequence of ints from circuit.qubits, + "times": sequence of ints from circuit.times } + This is for the intent of faster serialization by constructing the + base objects of Circuit (SortedDict, SortedSet) more directly, + providing them sorted sequences which should be quickly iterable + by timsort on construction + """ + return dict( + instructions=quasar_to_list(q), + qubits=list(q.qubits), + times=list(q.times), + times_and_qubits=list(q.times_and_qubits), + ) + + +def dict_to_quasar(d: Mapping) -> Circuit: + """ + Takes a serialized mapping dict as in quasar_to_dict and returns + a rebuilt circuit from the elements therein + """ + gate_generator = ( + ( + (tuple(instruction["times"]), tuple(instruction["bits"])), + make_gate(instruction["gate"], instruction["parameters"]), + ) + for instruction in d["instructions"] + ) + gates = SortedDict(gate_generator) + qubits = SortedSet(d["qubits"]) + times = SortedSet(d["times"]) + times_and_qubits = SortedSet([tuple(x) for x in d["times_and_qubits"]]) + result = Circuit.__new__(Circuit) + result.gates = gates + result.qubits = qubits + result.times = times + result.times_and_qubits = times_and_qubits + return result + + +def quasar_to_sequence(q: Circuit) -> Iterable: + return (q_instruction_to_s(k, v) for k, v in q.gates.items()) + + +def quasar_to_list(q: Circuit) -> List: + return list(quasar_to_sequence(q)) + + +def quasar_to_string(q: Circuit) -> str: + b = json.dumps(quasar_to_dict(q)).encode("utf-8") + cb = lz4.frame.compress(b) + return base64.b64encode(cb).decode("utf-8") + + +def make_gate(gate_name: str, original_parameters: dict): + # U1 and U2 have translated ndarrays, so we must convert them + parameters = original_parameters.copy() + if gate_name in ["U1", "U2"]: + parameters["U"] = dict_to_ndarray(parameters["U"]) + base = base_gate_name(gate_name) + n_adj = num_adjoints(gate_name) + f = Name_to_gatefn.get(base, None) + if f is None: + raise GateSerializationNotImplementedError(gate_name) + else: + gate = f(parameters) + for i in range(n_adj): + gate = gate.adjoint() + return gate + + +def sequence_to_quasar(s: Iterable) -> Circuit: + # make a sequence of tuples (gate, qubits, times) and manually + # add to circuit, no need for copy + result = Circuit() + for instruction in s: + gate = make_gate(instruction["gate"], instruction["parameters"]) + # add_gate is quite fussy about taking tuples + result.add_gate( + gate, tuple(instruction["bits"]), times=tuple(instruction["times"]) + ) + return result + + +def string_to_quasar(s: str) -> Circuit: + cb = base64.b64decode(s) + b = lz4.frame.decompress(cb) + qdict = json.loads(b.decode("utf-8")) + return dict_to_quasar(qdict) + + +def probability_histogram_to_dict(hist: ProbabilityHistogram): + return dict( + nqubit=hist.nqubit, histogram=hist.histogram, nmeasurement=hist.nmeasurement + ) + + +def dict_to_probability_histogram(d: dict): + d2 = d.copy() + if "histogram" in d2: + d2["histogram"] = {int(k): v for k, v in d2["histogram"].items()} + return ProbabilityHistogram(d2["nqubit"], d2["histogram"], d2["nmeasurement"]) + + +def pauli_item_to_tuple(k: PauliString, v: object) -> Tuple[str, Dict]: + return (str(k), scalar_to_dict(v)) + + +def tuple_to_pauli_item(t: Tuple[str, Dict]) -> Tuple: + return tuple((PauliString.from_string(t[0]), dict_to_scalar(t[1]))) + + +def pauli_to_list(p: Pauli) -> List[Tuple[str, Dict]]: + """ + Transforms a pauli object into a list of tuples + of the form str, float float, representing + (PauliString, Dict) where the Dict is an encoded + numpy array + """ + return [pauli_item_to_tuple(k, v) for k, v in p.items()] + + +def list_to_pauli(pl: List) -> Pauli: + """ + Transforms a list of pauli tuples (see above) to + a pauli object + """ + return Pauli([tuple_to_pauli_item(t) for t in pl]) diff --git a/qcware/serialization/transforms/__init__.py b/qcware/serialization/transforms/__init__.py new file mode 100644 index 0000000..e559469 --- /dev/null +++ b/qcware/serialization/transforms/__init__.py @@ -0,0 +1,10 @@ +from qcware.serialization.transforms.transform_params import ( + client_args_to_wire, + server_args_from_wire, + replace_server_args_from_wire, +) +from qcware.serialization.transforms.transform_results import ( + server_result_to_wire, + client_result_from_wire, +) +from qcware.serialization.transforms.helpers import ndarray_to_dict, dict_to_ndarray diff --git a/qcware/serialization/transforms/helpers.py b/qcware/serialization/transforms/helpers.py new file mode 100644 index 0000000..f4cdefe --- /dev/null +++ b/qcware/serialization/transforms/helpers.py @@ -0,0 +1,118 @@ +import base64 +from typing import Dict + +import lz4.frame +import numpy as np +from icontract import require + + +def ndarray_to_dict(x: np.ndarray): + # from https://stackoverflow.com/questions/30698004/how-can-i-serialize-a-numpy-array-while-preserving-matrix-dimensions + if x is None: + return None + else: + if isinstance(x, list) or isinstance(x, tuple): + x = np.array(x) + b = x.tobytes() + Compression_threshold = 1024 + if len(b) > Compression_threshold: + b = lz4.frame.compress(b) + compression = "lz4" + else: + compression = "none" + return dict( + ndarray=base64.b64encode(b).decode("utf-8"), + compression=compression, + dtype=x.dtype.str, + shape=x.shape, + ) + + +def dict_to_ndarray(d: dict): + if d is None: + return None + else: + b = base64.b64decode(d["ndarray"]) + if d["compression"] == "lz4": + b = lz4.frame.decompress(b) + return np.frombuffer( + b, + dtype=np.dtype(d["dtype"]), + ).reshape(d["shape"]) + + +@require(lambda v: np.isscalar(v)) +def scalar_to_dict(v, dtype=None) -> Dict: + """ + Hack for individual numerical scalars to serializable form. + This is done by casting them to complex128, which is byte-wasteful + in some ways, and into an array, which is byte-wasteful in other + ways, but at least preserves accuracy to a degree + """ + if dtype is None: + dtype = np.complex128 if np.iscomplex(v) else np.float64 + result = ndarray_to_dict(np.array([v], dtype=dtype)) + result["is_scalar"] = True + return result + + +@require(lambda d: d.get("is_scalar", False) is True) +def dict_to_scalar(d: Dict): + return dict_to_ndarray(d)[0] + + +@require(lambda x: isinstance(x, np.ndarray) or np.isscalar(x)) +def numeric_to_dict(x): + """ + A more generic transformation in the case that x represents either + an array or a scalar + """ + return scalar_to_dict(x) if np.isscalar(x) else ndarray_to_dict(x) + + +@require(lambda x: isinstance(x, dict) and "ndarray" in x) +def dict_to_numeric(x): + """See numeric_to_dict""" + if x.get("is_scalar", False) is True: + return dict_to_scalar(x) + else: + return dict_to_ndarray(x) + + +def string_to_int_tuple(s: str): + term_strings = s.split(",") + if term_strings[-1] == "": + term_strings = term_strings[:-1] + return tuple(map(int, term_strings)) + + +def remap_q_indices_from_strings(q_old: dict) -> dict: + q_new = {string_to_int_tuple(k[1:-1].strip(", ")): v for k, v in q_old.items()} + return q_new + + +def remap_q_indices_to_strings(Q: dict) -> dict: + return {str(k): v for k, v in Q.items()} + + +def complex_or_real_dtype_to_string(t: type): + if t is None: + result = None + else: + if np.isreal(t()) or np.iscomplex(t()): + result = t.__name__ + else: + raise NotImplementedError("dtypes must be complex or real") + return result + + +def string_to_complex_or_real_dtype(s: str): + if s is None: + result = None + else: + t = np.dtype(s).type + if np.isreal(t()) or np.iscomplex(t()): + result = t + else: + raise NotImplementedError("dtypes must be complex or real") + return result diff --git a/qcware/serialization/transforms/to_wire.py b/qcware/serialization/transforms/to_wire.py new file mode 100644 index 0000000..c90b9a9 --- /dev/null +++ b/qcware/serialization/transforms/to_wire.py @@ -0,0 +1,193 @@ +from functools import singledispatch + +from qcware.serialization.transforms.helpers import ( + dict_to_ndarray, + ndarray_to_dict, + remap_q_indices_from_strings, + remap_q_indices_to_strings, +) +from qcware.types.optimization import ( + BinaryProblem, + BruteOptimizeResult, + Constraints, + PolynomialObjective, +) +from qcware.types.optimization.results.results_types import BinaryResults, Sample +from qcware.types.qml import ( + FitData, + QMeansFitData, + QNearestCentroidFitData, + QNeighborsClassifierFitData, + QNeighborsRegressorFitData, +) + + +@singledispatch +def to_wire(x): + """For complex types, this dispatches to create a JSON-compatible dict""" + raise NotImplementedError(f"Unsupported Type: {type(x)}") + + +@to_wire.register(PolynomialObjective) +def polynomial_objective_to_wire(x): + result = x.dict() + result["polynomial"] = remap_q_indices_to_strings(result["polynomial"]) + result["variable_name_mapping"] = { + str(k): v for k, v in result["variable_name_mapping"].items() + } + return result + + +def polynomial_objective_from_wire(d: dict): + remapped_dict = d.copy() + + remapped_dict["polynomial"] = remap_q_indices_from_strings(d["polynomial"]) + remapped_dict["variable_name_mapping"] = { + int(k): v for k, v in remapped_dict["variable_name_mapping"].items() + } + return PolynomialObjective(**remapped_dict) + + +@to_wire.register(Constraints) +def constraints_to_wire(x): + result = x.dict() + result["constraints"] = { + k: [to_wire(x) for x in v] for k, v in x.dict()["constraints"].items() + } + return result + + +def constraints_from_wire(d: dict): + remapped_dict = d.copy() + remapped_dict["constraints"] = { + k: [polynomial_objective_from_wire(x) for x in v] + for k, v in d["constraints"].items() + } + + return Constraints(**remapped_dict) + + +@to_wire.register(BinaryProblem) +def binary_problem_to_wire(x): + result = x.dict() + result["objective"] = to_wire(result["objective"]) + result["constraints"] = ( + to_wire(result["constraints"]) if result["constraints"] is not None else None + ) + return result + + +def binary_problem_from_wire(d: dict): + remapped_dict = d.copy() + remapped_dict["objective"] = polynomial_objective_from_wire(d["objective"]) + remapped_dict["constraints"] = ( + constraints_from_wire(remapped_dict["constraints"]) + if remapped_dict["constraints"] is not None + else None + ) + return BinaryProblem(**remapped_dict) + + +@to_wire.register(BinaryResults) +def _(x): + result = x.dict() + result["original_problem"] = to_wire(x.original_problem) + result["task_metadata"] = { + k: v + for k, v in result["task_metadata"].items() + if k not in ("Q", "Q_array", "split_to_full_map_array", "instance") + } + return result + + +def binary_results_from_wire(d: dict): + remapped_dict = d.copy() + remapped_dict["sample_ordered_dict"] = { + k: Sample(**v) for k, v in remapped_dict["sample_ordered_dict"].items() + } + remapped_dict["original_problem"] = binary_problem_from_wire(d["original_problem"]) + return BinaryResults(**remapped_dict) + + +def brute_optimize_result_from_wire(d: dict): + return BruteOptimizeResult(**d) + + +@to_wire.register(QNearestCentroidFitData) +def q_nearest_centroid_fit_data_to_wire(x): + result = x.dict() + result["centroids"] = ndarray_to_dict(result["centroids"]) + result["classes"] = ndarray_to_dict(result["classes"]) + return result + + +def q_nearest_centroid_fit_data_from_wire(d: dict): + d["centroids"] = dict_to_ndarray(d["centroids"]) + d["classes"] = dict_to_ndarray(d["classes"]) + return QNearestCentroidFitData(**d) + + +@to_wire.register(QNeighborsRegressorFitData) +def q_neighbors_regressor_fit_data_to_wire(x): + result = x.dict() + result["regressor_data"] = ndarray_to_dict(result["regressor_data"]) + result["regressor_labels"] = ndarray_to_dict(result["regressor_labels"]) + return result + + +def q_neighbors_regressor_fit_data_from_wire(d: dict): + d["regressor_data"] = dict_to_ndarray(d["regressor_data"]) + d["regressor_labels"] = dict_to_ndarray(d["regressor_labels"]) + return QNeighborsRegressorFitData(**d) + + +@to_wire.register(QNeighborsClassifierFitData) +def q_neighbors_classifier_fit_data_to_wire(x): + result = x.dict() + result["classifier_data"] = ndarray_to_dict(result["classifier_data"]) + result["classifier_labels"] = ndarray_to_dict(result["classifier_labels"]) + return result + + +def q_neighbors_classifier_fit_data_from_wire(d: dict): + d["classifier_data"] = dict_to_ndarray(d["classifier_data"]) + d["classifier_labels"] = dict_to_ndarray(d["classifier_labels"]) + return QNeighborsClassifierFitData(**d) + + +@to_wire.register(QMeansFitData) +def q_means_fit_data_to_wire(x): + result = x.dict() + result["data"] = ndarray_to_dict(result["data"]) + result["labels"] = ndarray_to_dict(result["labels"]) + result["cluster_centers"] = ndarray_to_dict(result["cluster_centers"]) + result["history"] = ndarray_to_dict(result["history"]) + return result + + +def q_means_fit_data_from_wire(d: dict): + d["data"] = dict_to_ndarray(d["data"]) + d["labels"] = dict_to_ndarray(d["labels"]) + d["cluster_centers"] = dict_to_ndarray(d["cluster_centers"]) + d["history"] = dict_to_ndarray(d["history"]) + + return QMeansFitData(**d) + + +@to_wire.register(FitData) +def fit_data_to_wire(x): + result = x.dict() + result["fit_data"] = to_wire(x.fit_data) + return result + + +def fit_data_from_wire(d: dict): + if d["model_name"] == "QNearestCentroid": + d["fit_data"] == q_nearest_centroid_fit_data_from_wire(d["fit_data"]) + elif d["model_name"] == "QNeighborsRegressor": + d["fit_data"] = q_neighbors_regressor_fit_data_from_wire(d["fit_data"]) + elif d["model_name"] == "QNeighborsClassifier": + d["fit_data"] = q_neighbors_classifier_fit_data_from_wire(d["fit_data"]) + elif d["model_name"] == "QMeans": + d["fit_data"] = q_means_fit_data_from_wire(d["fit_data"]) + return FitData(**d) diff --git a/qcware/serialization/transforms/transform_params.py b/qcware/serialization/transforms/transform_params.py new file mode 100644 index 0000000..b6b318b --- /dev/null +++ b/qcware/serialization/transforms/transform_params.py @@ -0,0 +1,416 @@ +""" +Methods to transform FROM native types used by the backends +TO serializable types for the api to send to the client. + +This file should primarily contain the marshaling for argument +transformations and functions, not so much the transformation functions +themselves for particular types (those go in the helpers file). +""" +from functools import wraps +from typing import Any, Callable, Mapping, Optional, Dict + +from qcware.serialization.serialize_quasar import ( + list_to_pauli, + pauli_to_list, + quasar_to_string, + string_to_quasar, +) +from qcware.serialization.transforms.helpers import ( + complex_or_real_dtype_to_string, + dict_to_ndarray, + dict_to_numeric, + dict_to_scalar, + ndarray_to_dict, + numeric_to_dict, + remap_q_indices_from_strings, + remap_q_indices_to_strings, + scalar_to_dict, + string_to_complex_or_real_dtype, +) +from qcware.serialization.transforms.to_wire import ( + binary_problem_from_wire, + binary_results_from_wire, + constraints_from_wire, + fit_data_from_wire, + fit_data_to_wire, + polynomial_objective_from_wire, + to_wire, +) +from qcware.types.optimization import BinaryProblem +from toolz.dicttoolz import update_in + + +def update_with_replacers(d: Dict[str, Any], replacers: Mapping[str, Callable]): + """for all (k,f) in replacers, updates the dict entry marked by k by calling the + function f on the value""" + result = d.copy() + for k, f in replacers.items(): + if k in result: + result[k] = f(result[k]) + return result + + +_to_wire_arg_replacers: Dict[str, Dict[str, Callable]] = {} + + +def client_args_to_wire(method_name: str, **kwargs): + # grab the dict of + # key replacers and apply them + if method_name == "circuits.run_backend_method": + method_name = "_shadowed." + kwargs.get("method", "") + inner_kwargs = client_args_to_wire(method_name, **kwargs.get("kwargs", {})) + return {**kwargs, **{"kwargs": inner_kwargs}} + else: + return update_with_replacers( + kwargs, _to_wire_arg_replacers.get(method_name, {}) + ) + + +_from_wire_arg_replacers: Dict[str, Dict[str, Callable]] = {} + + +def replace_server_args_from_wire(method_name: str): + """Decorates a function with a method name for serialization. + + Uses this to transform the keyword parameters (all parameters + must be keyword parameters) and call the original function + with the transformed parameters. + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + kwargs = server_args_from_wire(method_name, **kwargs) + return func(*args, **kwargs) + + return wrapper + + return decorator + + +def server_args_from_wire(method_name: str, **kwargs): + # grab the dict of + # key replacers and apply them + if method_name == "circuits.run_backend_method": + method_name = "_shadowed." + kwargs.get("method", "") + inner_kwargs = server_args_from_wire(method_name, **kwargs.get("kwargs", {})) + return {**kwargs, **{"kwargs": inner_kwargs}} + else: + return update_with_replacers( + kwargs, _from_wire_arg_replacers.get(method_name, {}) + ) + + +def register_argument_transform( + method_name: str, to_wire: Optional[dict] = None, from_wire: Optional[dict] = None +): + if to_wire is None: + to_wire = dict() + if from_wire is None: + from_wire = dict() + + _to_wire_arg_replacers[method_name] = to_wire + _from_wire_arg_replacers[method_name] = from_wire + + +register_argument_transform( + "optimization.optimize_binary", + to_wire={ + "instance": to_wire, + }, + from_wire={ + "instance": binary_problem_from_wire, + "dwave_embedding": lambda y: {int(k): v for k, v in y.items()} + if y is not None + else None, + }, +) + +register_argument_transform( + "optimization.find_optimal_qaoa_angles", + to_wire={"Q": remap_q_indices_to_strings}, + from_wire={"Q": remap_q_indices_from_strings}, +) + +register_argument_transform( + "optimization.qaoa_expectation_value", + to_wire={ + "problem_instance": to_wire, + "beta": ndarray_to_dict, + "gamma": ndarray_to_dict, + }, + from_wire={ + "problem_instance": binary_problem_from_wire, + "beta": dict_to_ndarray, + "gamma": dict_to_ndarray, + }, +) + +register_argument_transform( + "optimization.qaoa_sample", + to_wire={ + "problem_instance": to_wire, + "beta": ndarray_to_dict, + "gamma": ndarray_to_dict, + }, + from_wire={ + "problem_instance": binary_problem_from_wire, + "beta": dict_to_ndarray, + "gamma": dict_to_ndarray, + }, +) + +register_argument_transform( + "optimization.brute_force_minimize", + to_wire={ + "objective": lambda x: to_wire(x), + "constraints": lambda x: to_wire(x) if x is not None else None, + }, + from_wire={ + "objective": polynomial_objective_from_wire, + "constraints": lambda x: constraints_from_wire(x) if x is not None else None, + }, +) + +register_argument_transform( + "qio.loader", to_wire={"data": ndarray_to_dict}, from_wire={"data": dict_to_ndarray} +) + +register_argument_transform( + "qml.fit_and_predict", + to_wire={"X": ndarray_to_dict, "y": ndarray_to_dict, "T": ndarray_to_dict}, + from_wire={"X": dict_to_ndarray, "y": dict_to_ndarray, "T": dict_to_ndarray}, +) + +register_argument_transform( + "qml.fit", + to_wire={"X": ndarray_to_dict, "y": ndarray_to_dict}, + from_wire={"X": dict_to_ndarray, "y": dict_to_ndarray}, +) + +register_argument_transform( + "qml.predict", + to_wire={"X": ndarray_to_dict, "fit_data": fit_data_to_wire}, + from_wire={"X": dict_to_ndarray, "fit_data": fit_data_from_wire}, +) + +register_argument_transform( + "montecarlo.nisqAE.run_schedule", + to_wire={ + "initial_circuit": quasar_to_string, + "iteration_circuit": quasar_to_string, + }, + from_wire={ + # "initial_circuit": string_to_quasar, + # "iteration_circuit": string_to_quasar, + }, +) + +register_argument_transform( + "montecarlo.nisqAE.run_unary", + to_wire={ + "circuit": quasar_to_string, + }, + from_wire={ + # "circuit": string_to_quasar, + }, +) + +register_argument_transform( + "qutils.qdot", + to_wire={ + "x": numeric_to_dict, + "y": numeric_to_dict, + "circuit": lambda x: quasar_to_string(x) if x is not None else None, + }, + from_wire={ + "x": dict_to_numeric, + "y": dict_to_numeric, + "circuit": lambda x: string_to_quasar(x) if x is not None else None, + }, +) + +register_argument_transform( + "qutils.create_qdot_circuit", + to_wire={"x": numeric_to_dict, "y": numeric_to_dict}, + from_wire={"x": dict_to_numeric, "y": dict_to_numeric}, +) + +register_argument_transform( + "qutils.qdist", + to_wire={ + "x": numeric_to_dict, + "y": numeric_to_dict, + "circuit": lambda x: quasar_to_string(x) if x is not None else None, + }, + from_wire={ + "x": dict_to_numeric, + "y": dict_to_numeric, + "circuit": lambda x: string_to_quasar(x) if x is not None else None, + }, +) + +register_argument_transform( + "_shadowed.run_measurement", + to_wire={ + "circuit": quasar_to_string, + "statevector": ndarray_to_dict, + "dtype": complex_or_real_dtype_to_string, + }, + from_wire={ + "statevector": dict_to_ndarray, + "dtype": string_to_complex_or_real_dtype, + }, +) + +register_argument_transform( + "_shadowed.run_measurements", + to_wire={ + "circuits": lambda x: [quasar_to_string(y) for y in x], + }, + from_wire={ + # "circuits": lambda x: [string_to_quasar(y) for y in x], + # we don't actually deserialize here because this happens in + # the dispatcher and pickle can't handle the lambdas used in + # circuit gate definitions so we have to do final deserialization + # in the workers themselves; ugh + }, +) + + +register_argument_transform( + "_shadowed.run_statevector", + to_wire={ + "circuit": quasar_to_string, + "statevector": ndarray_to_dict, + "dtype": complex_or_real_dtype_to_string, + }, + from_wire={ + "statevector": dict_to_ndarray, + "dtype": string_to_complex_or_real_dtype, + }, +) + +register_argument_transform( + "_shadowed.circuit_in_basis", + to_wire={ + "circuit": quasar_to_string, + }, + from_wire={}, +) +register_argument_transform( + "_shadowed.run_density_matrix", + to_wire=dict( + circuit=quasar_to_string, + statevector=ndarray_to_dict, + dtype=complex_or_real_dtype_to_string, + ), + from_wire=dict(statevector=dict_to_ndarray, dtype=string_to_complex_or_real_dtype), +) +register_argument_transform( + "_shadowed.run_pauli_diagonal", + to_wire=dict(pauli=pauli_to_list, dtype=complex_or_real_dtype_to_string), + from_wire=dict(pauli=list_to_pauli, dtype=string_to_complex_or_real_dtype), +) +register_argument_transform( + "_shadowed.run_pauli_expectation", + to_wire=dict( + circuit=quasar_to_string, + pauli=pauli_to_list, + statevector=ndarray_to_dict, + dtype=complex_or_real_dtype_to_string, + ), + from_wire=dict( + pauli=list_to_pauli, + statevector=dict_to_ndarray, + dtype=string_to_complex_or_real_dtype, + ), +) +register_argument_transform( + "_shadowed.run_pauli_expectation_ideal", + to_wire=dict( + circuit=quasar_to_string, + pauli=pauli_to_list, + statevector=ndarray_to_dict, + dtype=complex_or_real_dtype_to_string, + ), + from_wire=dict( + pauli=list_to_pauli, + statevector=dict_to_ndarray, + dtype=string_to_complex_or_real_dtype, + ), +) +register_argument_transform( + "_shadowed.run_pauli_expectation_measurement", + to_wire=dict( + circuit=quasar_to_string, + pauli=pauli_to_list, + statevector=ndarray_to_dict, + dtype=complex_or_real_dtype_to_string, + ), + from_wire=dict( + pauli=list_to_pauli, + statevector=dict_to_ndarray, + dtype=string_to_complex_or_real_dtype, + ), +) +register_argument_transform( + "_shadowed.run_pauli_expectation_value", + to_wire=dict( + circuit=quasar_to_string, + pauli=pauli_to_list, + statevector=ndarray_to_dict, + dtype=complex_or_real_dtype_to_string, + ), + from_wire=dict( + pauli=list_to_pauli, + statevector=dict_to_ndarray, + dtype=string_to_complex_or_real_dtype, + ), +) +register_argument_transform( + "_shadowed.run_pauli_expectation_value_gradient", + to_wire=dict( + circuit=quasar_to_string, + pauli=pauli_to_list, + statevector=ndarray_to_dict, + dtype=complex_or_real_dtype_to_string, + ), + from_wire=dict( + pauli=list_to_pauli, + statevector=dict_to_ndarray, + dtype=string_to_complex_or_real_dtype, + ), +) +register_argument_transform( + "_shadowed.run_pauli_expectation_value_ideal", + to_wire=dict( + circuit=quasar_to_string, + pauli=pauli_to_list, + statevector=ndarray_to_dict, + dtype=complex_or_real_dtype_to_string, + ), + from_wire=dict( + pauli=list_to_pauli, + statevector=dict_to_ndarray, + dtype=string_to_complex_or_real_dtype, + ), +) +register_argument_transform( + "_shadowed.run_pauli_sigma", + to_wire=dict( + pauli=pauli_to_list, + statevector=ndarray_to_dict, + dtype=complex_or_real_dtype_to_string, + ), + from_wire=dict( + pauli=list_to_pauli, + statevector=dict_to_ndarray, + dtype=string_to_complex_or_real_dtype, + ), +) +register_argument_transform( + "_shadowed.run_unitary", + to_wire=dict(circuit=quasar_to_string, dtype=complex_or_real_dtype_to_string), + from_wire=dict(dtype=string_to_complex_or_real_dtype), +) diff --git a/qcware/serialization/transforms/transform_results.py b/qcware/serialization/transforms/transform_results.py new file mode 100644 index 0000000..eab60b6 --- /dev/null +++ b/qcware/serialization/transforms/transform_results.py @@ -0,0 +1,279 @@ +import os +from typing import Callable, Optional, cast, Dict + +import numpy +from decouple import config + +from ..serialize_quasar import ( + dict_to_probability_histogram, + list_to_pauli, + pauli_to_list, + probability_histogram_to_dict, + quasar_to_list, + sequence_to_quasar, +) +from .helpers import ( + dict_to_ndarray, + dict_to_numeric, + dict_to_scalar, + ndarray_to_dict, + numeric_to_dict, + scalar_to_dict, +) +from .to_wire import ( + binary_results_from_wire, + brute_optimize_result_from_wire, + fit_data_from_wire, + fit_data_to_wire, + to_wire, +) + +_to_wire_result_replacers: Dict[str, Callable] = {} + + +def debug_is_set() -> bool: + return config("QCWARE_CLIENT_DEBUG", default=False, cast=bool) + + +def result_represents_error(worker_result: object): + """ + Defines whether the result object returned by a backend function represents an + error. This was done by backend functions returning a dict with "error" as a key + which conflicted with some results which inherited from dict but magically parsed + key requests and would throw exceptions. + """ + result = isinstance(worker_result, dict) and "error" in worker_result + return result + + +def strip_traceback_if_debug_set(error_result: dict) -> dict: + result = error_result.copy() + if not debug_is_set() and "traceback" in result: + result.pop("traceback") + return result + + +def server_result_to_wire(method_name: str, worker_result: object): + if result_represents_error(worker_result): + return strip_traceback_if_debug_set(cast(dict, worker_result)) + else: + f = _to_wire_result_replacers.get(method_name, lambda x: x) + return f(worker_result) + + +_from_wire_result_replacers: Dict[str, Callable] = {} + + +def client_result_from_wire(method_name: str, worker_result: object): + if result_represents_error(worker_result): + return strip_traceback_if_debug_set(cast(dict, worker_result)) + else: + f = _from_wire_result_replacers.get(method_name, lambda x: x) + return f(worker_result) + + +def register_result_transform( + method_name: str, + to_wire: Optional[Callable] = None, + from_wire: Optional[Callable] = None, +): + if to_wire is not None: + _to_wire_result_replacers[method_name] = to_wire + if from_wire is not None: + _from_wire_result_replacers[method_name] = from_wire + + +def transform_optimization_find_optimal_qaoa_angles_to_wire(t): + # this function requires a little special-casing since it + # returns a number of arrays + return (t[0], t[1], ndarray_to_dict(t[2])) + + +def transform_optimization_find_optimal_qaoa_angles_from_wire(t): + return (t[0], t[1], dict_to_ndarray(t[2])) + + +register_result_transform( + "qio.loader", + to_wire=lambda x: (quasar_to_list(x[0]), ndarray_to_dict(x[1])) + if type(x) is tuple + else tuple([quasar_to_list(x)]), + from_wire=lambda x: (sequence_to_quasar(x[0]), dict_to_ndarray(x[1])) + if len(x) == 2 + else sequence_to_quasar(x[0]), +) +register_result_transform( + "qutils.qdot", to_wire=numeric_to_dict, from_wire=dict_to_numeric +) + +register_result_transform( + "qutils.create_qdot_circuit", to_wire=quasar_to_list, from_wire=sequence_to_quasar +) + +register_result_transform( + "qutils.qdist", to_wire=numeric_to_dict, from_wire=dict_to_numeric +) + +register_result_transform( + "circuits.run_measurement", + to_wire=probability_histogram_to_dict, + from_wire=dict_to_probability_histogram, +) +register_result_transform( + "circuits.run_measurements", + to_wire=lambda x: [probability_histogram_to_dict(y) for y in x], + from_wire=lambda x: [dict_to_probability_histogram(y) for y in x], +) +register_result_transform( + "circuits.run_statevector", to_wire=ndarray_to_dict, from_wire=dict_to_ndarray +) +register_result_transform( + "optimization.find_optimal_qaoa_angles", + to_wire=transform_optimization_find_optimal_qaoa_angles_to_wire, + from_wire=transform_optimization_find_optimal_qaoa_angles_from_wire, +) +register_result_transform( + "qml.fit_and_predict", to_wire=ndarray_to_dict, from_wire=dict_to_ndarray +) + +register_result_transform( + "qml.fit", to_wire=fit_data_to_wire, from_wire=fit_data_from_wire +) + +register_result_transform( + "qml.predict", to_wire=ndarray_to_dict, from_wire=dict_to_ndarray +) + + +def run_backend_method_to_wire(backend_method_result: dict): + result = server_result_to_wire( + "_shadowed." + backend_method_result["method"], backend_method_result["result"] + ) + return dict(method=backend_method_result["method"], result=result) + + +def run_backend_method_from_wire(backend_method_result: dict): + return client_result_from_wire( + "_shadowed." + backend_method_result["method"], backend_method_result["result"] + ) + + +# it may seem odd to have multiple registrations for solve_binary, but +# it was named multiple things in different solvers since it could be dispatched +# to different tasks based on the back ends. + + +def old_binary_result_from_new(x: dict): + br = binary_results_from_wire(x) + result = dict( + solution=br.lowest_value_bitstring, + extra_info=br.result_metadata["extra_info"], + ) + if "qubo_energy_list" in br.result_metadata: + result["qubo_energy_list"] = qubo_energy_list = ( + br.result_metadata["qubo_energy_list"], + ) + + return result + + +register_result_transform( + "optimization.optimize_binary", to_wire=to_wire, from_wire=binary_results_from_wire +) + +register_result_transform( + "optimization.qaoa_expectation_value", + to_wire=numeric_to_dict, + from_wire=dict_to_numeric, +) +register_result_transform( + "optimization.qaoa_sample", to_wire=to_wire, from_wire=binary_results_from_wire +) +register_result_transform( + "solve_qubo_with_brute_force_task", + to_wire=to_wire, + from_wire=lambda x: binary_results_from_wire(x), +) +register_result_transform( + "solve_qubo_with_quasar_qaoa_simulator_task", + to_wire=to_wire, + from_wire=lambda x: binary_results_from_wire(x), +) +register_result_transform( + "solve_qubo_with_dwave_task", + to_wire=to_wire, + from_wire=lambda x: binary_results_from_wire(x), +) +register_result_transform( + "solve_qubo_with_quasar_qaoa_vulcan_task", + to_wire=to_wire, + from_wire=lambda x: binary_results_from_wire(x), +) + +register_result_transform( + "circuits.run_backend_method", + to_wire=run_backend_method_to_wire, + from_wire=run_backend_method_from_wire, +) +register_result_transform( + "optimization.brute_force_minimize", + to_wire=lambda x: x.dict(), + from_wire=brute_optimize_result_from_wire, +) +register_result_transform( + "_shadowed.run_measurement", + to_wire=probability_histogram_to_dict, + from_wire=dict_to_probability_histogram, +) +register_result_transform( + "_shadowed.run_measurements", + to_wire=lambda x: [probability_histogram_to_dict(y) for y in x], + from_wire=lambda x: [dict_to_probability_histogram(y) for y in x], +) + +register_result_transform( + "_shadowed.run_statevector", to_wire=ndarray_to_dict, from_wire=dict_to_ndarray +) +register_result_transform( + "_shadowed.circuit_in_basis", to_wire=quasar_to_list, from_wire=sequence_to_quasar +) +register_result_transform( + "_shadowed.run_density_matrix", to_wire=ndarray_to_dict, from_wire=dict_to_ndarray +) +register_result_transform( + "_shadowed.run_pauli_diagonal", to_wire=ndarray_to_dict, from_wire=dict_to_ndarray +) +register_result_transform( + "_shadowed.run_pauli_expectation", to_wire=pauli_to_list, from_wire=list_to_pauli +) +register_result_transform( + "_shadowed.run_pauli_expectation_ideal", + to_wire=pauli_to_list, + from_wire=list_to_pauli, +) +register_result_transform( + "_shadowed.run_pauli_expectation_measurement", + to_wire=pauli_to_list, + from_wire=list_to_pauli, +) +register_result_transform( + "_shadowed.run_pauli_expectation_value", + to_wire=lambda x: scalar_to_dict(x, dtype=numpy.float64), + from_wire=dict_to_scalar, +) +register_result_transform( + "_shadowed.run_pauli_expectation_value_gradient", + to_wire=ndarray_to_dict, + from_wire=dict_to_ndarray, +) +register_result_transform( + "_shadowed.run_pauli_expectation_value_ideal", + to_wire=lambda x: scalar_to_dict(x, dtype=numpy.float64), + from_wire=dict_to_scalar, +) +register_result_transform( + "_shadowed.run_pauli_sigma", to_wire=ndarray_to_dict, from_wire=dict_to_ndarray +) +register_result_transform( + "_shadowed.run_unitary", to_wire=ndarray_to_dict, from_wire=dict_to_ndarray +) diff --git a/qcware/types/__init__.py b/qcware/types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qcware/types/ndarray.py b/qcware/types/ndarray.py new file mode 100644 index 0000000..87d07ff --- /dev/null +++ b/qcware/types/ndarray.py @@ -0,0 +1,61 @@ +import base64 +from typing import Any + +import lz4 +import numpy as np + + +def dict_to_ndarray(d: dict): + if d is None: + return None + else: + b = base64.b64decode(d["ndarray"]) + if d["compression"] == "lz4": + b = lz4.frame.decompress(b) + return np.frombuffer( + b, + dtype=np.dtype(d["dtype"]), + ).reshape(d["shape"]) + + +class NDArray(np.ndarray): + @classmethod + def __get_validators__(cls): + yield cls.validate + + @classmethod + def __modify_schema__(cls, field_schema): + field_schema.update( + title="NDArray", + oneOf=[ + { + "type": "object", + "properties": { + "ndarray": {"type": "string"}, + "compression": {"type": "string"}, + "dtype": {"type": "string"}, + "shape": {"type": "array", "items": {"type": "number"}}, + }, + }, + {"type": "array", "items": {"type": "number"}}, + { + "type": "array", + "items": {"type": "array", "items": {"type": "number"}}, + }, + ], + ) + + @classmethod + def validate(cls, v: Any): + if isinstance(v, np.ndarray): + return v + elif isinstance(v, dict): + return cls.from_dict(v) + elif isinstance(v, list): + return np.array(v) + else: + raise ValueError("invalid type") + + @classmethod + def from_dict(cls, d): + return dict_to_ndarray(d) diff --git a/qcware/types/optimization/__init__.py b/qcware/types/optimization/__init__.py new file mode 100644 index 0000000..ca3583d --- /dev/null +++ b/qcware/types/optimization/__init__.py @@ -0,0 +1,6 @@ +from .predicate import Predicate +from .variable_types import Domain +from .problem_spec import PolynomialObjective, Constraints +from .problem_spec import BinaryProblem +from .results import BruteOptimizeResult, BinaryResults, Sample +from . import utils diff --git a/qcware/types/optimization/predicate.py b/qcware/types/optimization/predicate.py new file mode 100644 index 0000000..19a2a65 --- /dev/null +++ b/qcware/types/optimization/predicate.py @@ -0,0 +1,15 @@ +import enum + + +class Predicate(str, enum.Enum): + """Relations for constraint specification.""" + + NONNEGATIVE = "nonnegative" + POSITIVE = "positive" + NONPOSITIVE = "nonpositive" + NEGATIVE = "negative" + ZERO = "zero" + NONZERO = "nonzero" + + def __repr__(self): + return str(self).__str__() diff --git a/qcware/types/optimization/problem_spec/__init__.py b/qcware/types/optimization/problem_spec/__init__.py new file mode 100644 index 0000000..0a6ae50 --- /dev/null +++ b/qcware/types/optimization/problem_spec/__init__.py @@ -0,0 +1,3 @@ +from .objective import PolynomialObjective +from .constraints import Constraints +from .binary_problem import BinaryProblem diff --git a/qcware/types/optimization/problem_spec/binary_problem.py b/qcware/types/optimization/problem_spec/binary_problem.py new file mode 100644 index 0000000..7bcc151 --- /dev/null +++ b/qcware/types/optimization/problem_spec/binary_problem.py @@ -0,0 +1,94 @@ +from pydantic import BaseModel +from typing import Dict, Tuple, Optional + +from .. import Predicate, Domain + +from . import PolynomialObjective +from . import Constraints + + +class BinaryProblem(BaseModel): + objective: PolynomialObjective + constraints: Optional[Constraints] = None + name: str = "my_qcware_binary_problem" + + class Config: + validate_assignment = True + allow_mutation = False + arbitrary_types_allowed = True + + def __str__(self) -> str: + out = "Objective:\n" + out += " " + self.objective.__str__() + "\n\n" + + if self.constraints is None: + out += "Unconstrained" + elif self.constraints.num_constraints() == 0: + out += "Unconstrained" + else: + out += self.constraints.__str__() + return out + + @classmethod + def from_dict( + cls, objective: Dict[Tuple[int, ...], int], domain: Domain = Domain.BOOLEAN + ): + """ + Creates the BinaryProblem from a dict specifying a boolean polynomial. + """ + + def count_variables(polynomial: dict): + var_names = set() + for k in polynomial.keys(): + var_names.update(k) + return len(var_names) + + objective = PolynomialObjective( + polynomial=objective, + num_variables=count_variables(objective), + domain=domain, + ) + + return cls(objective=objective) + + def dwave_dict(self): + """Returns a dict valid for D-Wave problem specification.""" + q_start = self.objective.polynomial + q_final = {} + for elm in q_start.keys(): + if elm == (): + pass + elif len(elm) == 1: + q_final[(elm[0], elm[0])] = q_start[(elm[0],)] + else: + q_final[elm] = q_start[elm] + + return q_final + + @property + def num_variables(self): + """The number of variables for the objective function.""" + return self.objective.num_variables + + @property + def domain(self): + """The domain (boolean or spin) for the problem instance.""" + return self.objective.domain + + @property + def constraint_dict(self): + """Constraints in a dict format.""" + return self.constraints.constraint_dict + + def num_constraints(self, predicate: Optional[Predicate] = None): + """Return the number of constraints. + + If a predicate is specified, only return the number of constraints + for that predicate. + """ + return self.constraints.num_constraints(predicate) + + @property + def constrained(self): + """True if this problem instance is constrained.""" + return self.constraints is not None diff --git a/qcware/types/optimization/problem_spec/constraints.py b/qcware/types/optimization/problem_spec/constraints.py new file mode 100644 index 0000000..e6c95a7 --- /dev/null +++ b/qcware/types/optimization/problem_spec/constraints.py @@ -0,0 +1,363 @@ +import itertools +import textwrap +from typing import Dict, List, Union, Iterable, Optional + +import tabulate +from qcware.types.optimization.predicate import Predicate +from qcware.types.optimization.variable_types import Domain +from qcware.types.optimization import utils +from qcware.types.optimization.problem_spec import PolynomialObjective +from qcware.types.optimization.problem_spec.utils import ( + constraint_validation as validator, +) + + +class Constraints: + """Specification of constraints on binary variables. + + An object of class Constraints does not have information about the + objective function on which the constraints are applied. It is simply a + collection of constraints which can then be imposed on something. + + To specify constraints, we use a "constraint dict". The format + of this dict is:: + + {type of constraint: list of constraints of that type}. + + The "type of constraint" means a `Predicate` object. The list of + constraints of a given `Predicate` are a list of `PolynomialObjective` s. + + This can be understood fairly easily by example. Suppose that f is an + PolynomialObjective with 3 boolean variables. We take:: + + f(x, y, z) = (x + y + z - 1)^2 + + which is minimized when exactly one of the three variables is 1. + + If we want to impose the condition that either x or z is zero, + we can roughly use the constraint dict:: + + {Predicate.ZERO: [p]} + + where p is the PolynomialObjective representing the function:: + + p(x, y, z) = OR(x, z) = x + z - x z + + (To build this, see the documentation for PolynomialObjective.) If we + additionally want to add the constraint that the sum of the three variables + is at least 2, we can make our dict:: + + {Predicate.ZERO: [p], Predicate.POSITIVE: [q]} + + where q is a PolynomialObjective representing `q(x, y, z) = x + y + z - 1`. The + reason that we are using [p] and [q] instead of just p and q is that we can + add additional constraints of those types in this fashion by adding + more entries to the lists. + """ + + constraint_dict: Dict[Predicate, List[PolynomialObjective]] + domain: Domain + + def __init__( + self, + constraints: Dict[Predicate, List[PolynomialObjective]], + num_variables: int, + domain: Optional[Union[Domain, str]] = None, + variable_name_mapping: Optional[dict] = None, + validate_types: bool = True, + ): + """ + Args: + constraints: Constraints are specified with a dict from + Predicate objects to lists of PolynomialObjective objects. All + PolynomialObjective objects in the lists must have the same + number of variables as this Constraints object has. + + Keys to the dict are Predicate objects that specify the + type of constraint. The values of the dict are lists of + PolynomialObjective objects. Here's an example: + { + Predicate.NEGATIVE: [p, q], + Predicate.ZERO: [r] + } + is a specification for three constraints. p, q, and r are + all objects of type PolynomialObjective. The constraints are + + p(x) < 0, q(x) < 0, r(x) = 0 <==> x is feasible. + + The meaning of different Predicates are: + NONNEGATIVE -- polynomial >= 0 + POSITIVE -- polynomial > 0 + NONPOSITIVE -- polynomial <= 0 + NEGATIVE -- polynomial < 0 + ZERO -- polynomial = 0 + NONZERO -- polynomial != 0 + + num_variables: The number of binary variables that the + constraints constrain. + """ + parsed_constraints = validator.constraint_validation( + constraints=constraints, + num_variables=num_variables, + validate_types=validate_types, + domain=domain, + variable_name_mapping=variable_name_mapping, + ) + del constraints + self.constraint_dict = parsed_constraints.constraint_dict + self.num_variables = num_variables + self.predicates = set(self.constraint_dict) + self.degree_dict = {rel: [] for rel in self.predicates} + self.degree_set = set() + self._total_num_constraints = 0 + self._num_constraints_dict = {rel: 0 for rel in self.predicates} + + selected_domain = None + if domain is not None: + selected_domain = Domain(domain.lower()) + for predicate in self.predicates: + for c in self.constraint_dict[predicate]: + self.degree_dict[predicate].append(c.degree) + self.degree_set.add(c.degree) + self._total_num_constraints += 1 + self._num_constraints_dict[predicate] += 1 + if selected_domain is None: + selected_domain = c.domain + elif selected_domain is not c.domain: + raise ValueError( + "Inconsistent specification of spin versus boolean " + "constraints." + ) + + self.domain = selected_domain + self.max_degree_dict = { + predicate: max(degs) for predicate, degs in self.degree_dict.items() + } + if self.constraint_dict == {}: + self.max_degree = None + else: + self.max_degree = max(self.max_degree_dict.values()) + + def get_constraint_group( + self, predicate: Predicate, order: Union[int, Iterable[int], None] = None + ): + """Iterate over constraints with specified predicate and order. + + If order is not specified, all orders are generated. + """ + if order is None: + order = range(self.max_degree_dict[predicate] + 1) + elif isinstance(order, int): + order = range(order, order + 1) + if not utils.iterable(order): + raise TypeError( + f"Expected `order` to be int or iterable " + f"of int but got type {type(order)}." + ) + + for i, constraint in enumerate(self[predicate]): + if constraint.degree in order: + yield i, constraint + + def constraint_exists( + self, + order: Union[int, Iterable[int], None] = None, + predicate: Optional[Predicate] = None, + ): + """Return True iff a constraint exists with given order or predicate. + + `order` can be an int or an iterable of ints. `predicate` is a + Predicate object. + + If order and predicate are both None, this function returns True iff + any constraint exists. + """ + if order is None and predicate is None: + return self.num_constraints() > 0 + if order is not None: + if isinstance(order, int): + if order < 0: + raise ValueError("Order must be non-negative.") + order = range(order, order + 1) + + if order is None: + # True iff constraint with given predicate exists. + return self.num_constraints(predicate) > 0 + else: + if predicate is None: + degree_collection = self.degree_set + else: + try: + degree_collection = self.degree_dict[predicate] + except KeyError: + return False + for deg in degree_collection: + if deg in order: + return True + else: + return False + + def num_constraints(self, predicate: Optional[Predicate] = None): + """Return the number of constraints. + + If a predicate is specified, only return the number of constraints + for that predicate. + """ + if predicate is None: + return self._total_num_constraints + try: + return self._num_constraints_dict[predicate] + except KeyError: + if isinstance(predicate, Predicate): + return 0 + else: + raise TypeError(f"Expected Predicate, found {type(predicate)}") + + def __len__(self): + """Get the total number of constraints""" + return self.num_constraints() + + def __iter__(self): + return self.constraint_dict.__iter__() + + def __getitem__(self, item): + return self.constraint_dict.__getitem__(item) + + def __repr__(self): + out = "Constraints(\n" + out += f" constraints={self.constraint_dict},\n" + out += f" num_variables={self.num_variables}\n" + out += f")" + return out + + def constraint_string(self, max_shown: int = 10): + predicate_meaning = { + Predicate.ZERO: " = 0", + Predicate.NONZERO: " ≠ 0", + Predicate.POSITIVE: " > 0", + Predicate.NEGATIVE: " < 0", + Predicate.NONNEGATIVE: " ≥ 0", + Predicate.NONPOSITIVE: " ≤ 0", + } + constraint_string_list = [] + for pred in self.predicates: + pred_string = predicate_meaning[pred] + polynomials = self.constraint_dict[pred] + for p in itertools.islice(polynomials, max_shown): + constraint_string_list.append( + p.pretty_str(include_domain=False) + pred_string + ) + if self.num_constraints(predicate=pred) > max_shown: + num_hidden = self.num_constraints(predicate=pred) - max_shown + constraint_string_list += [f"({num_hidden} not shown)"] + constraint_string_list += ["\n"] + + # remove final '\n' + constraint_string_list = constraint_string_list[:-1] + return "\n".join(constraint_string_list) + + def __str__(self): + out = f"Number of variables: {self.num_variables}\n" + out += f"Total number of constraints: {self.num_constraints()}\n" + out += f"Variable domain: {self.domain.value}\n\n" + header = ["Predicate", "Number of Constraints", "Highest Degree"] + data = [ + [rel.upper(), self.num_constraints(rel), self.max_degree_dict[rel]] + for rel in self.predicates + ] + + out += tabulate.tabulate(data, header) + + out = textwrap.indent(out, " ", predicate=None) + out = "Constraints:\n" + out + + out += "\n\n" + textwrap.indent(self.constraint_string(max_shown=5), " ") + return out + + def dict(self): + return { + "constraints": self.constraint_dict, + "num_variables": self.num_variables, + } + + +if __name__ == "__main__": + + def std_constraints_3vars(feasible): + """Generate a standard constraint-specifying dict for three variables. + + If feasible is True, then this function generates four constraints + for which there are two feasible points. If false, there is one + more constraint that eliminates the feasible points. + + + Summary of constraints: + + infeasible case: + Number of binary variables: 3 + Total number of constraints: 6 + Predicate Number of Constraints Highest Degree + ---------- ----------------------- ---------------- + NONZERO 1 3 + ZERO 2 2 + NEGATIVE 2 1 + POSITIVE 1 0 + + feasible case: + Number of binary variables: 3 + Total number of constraints: 5 + Predicate Number of Constraints Highest Degree + ---------- ----------------------- ---------------- + EQ 2 2 + LT 2 1 + GT 1 0 + + """ + neg_constraints = [ + # a + b + c < 3 (violated iff a=b=c=1) + PolynomialObjective( + polynomial={(): -3, (0,): 1, (1,): 1, (2,): 1}, num_variables=3 + ), + # Always true + PolynomialObjective(polynomial={(): -1}, num_variables=3), + ] + zero_constraints = [ + # (a+b+c-1)^2 == 0 (true iff exactly one variable is 1.) + PolynomialObjective( + polynomial={ + (0, 1): 2, + (0, 2): 2, + (1, 2): 2, + (0,): -1, + (1,): -1, + (2,): -1, + (): 1, + }, + num_variables=3, + ), + # a + c = 1 (true iff a XOR c) + PolynomialObjective(polynomial={(0,): 1, (2,): 1, (): -1}, num_variables=3), + ] + pos_constraints = [ + # Always true + PolynomialObjective(polynomial={(): 7}, num_variables=3), + ] + + constraints = { + Predicate.NEGATIVE: neg_constraints, + Predicate.ZERO: zero_constraints, + Predicate.POSITIVE: pos_constraints, + } + + if not feasible: + constraints.update( + { + Predicate.NONZERO: [ + PolynomialObjective(polynomial={(0, 1, 2): 1}, num_variables=3) + ] + } + ) + + return Constraints(constraints, 3) + + con = std_constraints_3vars(True) diff --git a/qcware/types/optimization/problem_spec/objective.py b/qcware/types/optimization/problem_spec/objective.py new file mode 100644 index 0000000..f22d09b --- /dev/null +++ b/qcware/types/optimization/problem_spec/objective.py @@ -0,0 +1,377 @@ +from typing import Dict, Tuple, Set, Union, Optional +import qubovert as qv +from icontract import require +from qcware.types.optimization.problem_spec.utils import ( + polynomial_validation as validator, +) +from qcware.types.optimization.variable_types import Domain + + +class PolynomialObjective: + """Integer-valued polynomial of binary variables with int coefficients. + + Objects of this class specify polynomials of some number of + binary variables with integer coefficients. "Binary variables" can either + mean variables taking on the values 0 and 1 or variables taking on the + values 1 and -1. The former a referred to as boolean variables and the + latter are referred to as spin variables + + Objects of this class are meant to be treated as objective functions + for optimization. They do not know about constraints. Constraints + can be specified separately using a Constraints object. + + Polynomials are specified with a dict that specifies the coefficients + for the polynomial. For example, suppose that we are interested + in the polynomial of three boolean variables defined by:: + + p(x, y, z) = 12 x - 2 y z + x y z - 50 + + The three variables should be associated with the integers 0, 1, and 2. + We choose the association x ~ 0, y ~ 1, and z ~ 2. + + There are four terms in p. Consider the term -2yz. We can specify this + term by matching the tuple (1, 2), which represents yz, with the + coefficient -2. This can be encoded with an entry in a dict (1, 2): -2. + Overall, p can be defined by:: + + { + (): -50, + (0,): 12, + (1, 2): -2, + (0, 1, 2): -50 + } + + The number of variables must be specified explicitly, even if it seems + obvious how many variables there are. The reason for this is to allow + for the possibility that there are more variables than appear explicitly + in the polynomial. For example, the polynomial p(a, b) = 12 b might be + mistaken for q(b) = 12 b. + + Attributes: + polynomial: The polynomial is specified by a dict as described above. + We only use tuples of int as keys and the range of ints must + be from 0 to `num_variables - 1`. Values for the dict must be + type int. This is because we are only treating integer-coefficient + polynomials. + + variables: Set of ints representing the variables. + + num_variables: The number of variables for the polynomial. This number + can be larger than the actual number of variables that appear + in `polynomial` but it cannot be smaller. + + degree: The degree of the polynomial. This is not the mathematically + correct degree. It is instead the length of the longest key in + the polynomial dict or -inf in the case when the dict is {}. + For example, the boolean PolynomialObjective {(1,): 12, (0, 1): 0} + has mathematical degree 1 but the attribute `degree` is 2. + + domain: Specifies if variables take on boolean (0, 1) or spin (1, -1) + values. + """ + + polynomial: Dict[Tuple[int, ...], int] + variables: Set[int] + num_variables: int + degree: Union[int, float] + domain: Domain + variable_name_mapping: Dict[int, str] + + # @require(lambda polynomial: len(polynomial) > 0) + def __init__( + self, + polynomial: Dict[Tuple[int, ...], int], + num_variables: int, + domain: Union[Domain, str] = Domain.BOOLEAN, + variable_name_mapping: Optional[Dict[int, str]] = None, + validate_types: bool = True, + ): + # TODO: introduce mapping structure to guarantee consistent variable + # reduction. This is also critical because we don't want + # reduction to annoyingly re-order variables when it's not necessary. + self.num_variables = num_variables + self.domain = Domain(domain.lower()) + polynomial = simplify_polynomial(polynomial, self.domain) + + parsed_polynomial = validator.polynomial_validation( + polynomial=polynomial, + num_variables=num_variables, + validate_types=validate_types, + ) + + self.polynomial = parsed_polynomial.poly + self.active_variables = parsed_polynomial.variables + self.num_active_variables = len(self.active_variables) + self.degree = parsed_polynomial.deg + if self.degree < 0: + self.degree = float("-inf") + + self.variable_name_mapping = variable_name_mapping + + def default_symbol(variable_type: Domain): + if variable_type is Domain.BOOLEAN: + return "x" + elif variable_type is Domain.SPIN: + return "z" + + if variable_name_mapping is None: + symbol = default_symbol(self.domain) + self.variable_name_mapping = { + i: f"{symbol}_{i}" for i in range(num_variables) + } + + # We use qubovert to compute function values. Since we don't want + # to reconstruct a qubovert object every time we use it, we keep + # these private attributes around to use as a cache. + self._qv_polynomial = None + self._qv_polynomial_named = None + + def keys(self): + return self.polynomial.keys() + + def values(self): + return self.polynomial.values() + + def items(self): + return self.polynomial.items() + + def __iter__(self): + return self.polynomial.__iter__() + + def __repr__(self): + out = "PolynomialObjective(\n" + out += " polynomial=" + self.polynomial.__repr__() + "\n" + out += " num_variables=" + str(self.num_variables) + "\n" + out += " domain=" + repr(self.domain) + "\n" + out += " variable_mapping=" + repr(self.variable_name_mapping) + out += "\n)" + return out + + def __str__(self): + return self.pretty_str() + + def __getitem__(self, item): + return self.polynomial.__getitem__(item) + + def clone(self): + """Make a copy of this PolynomialObjective.""" + return PolynomialObjective( + polynomial=self.polynomial, + num_variables=self.num_variables, + domain=self.domain, + variable_name_mapping=self.variable_name_mapping.copy(), + validate_types=False, + ) + + def qubovert(self, use_variable_names: bool = False): + """Get a qubovert model describing this polynomial. + + This method will return a qubovert PUBO or PUSO depending on + the domain of the variables. + + This method creates a cached qubovert object once it is called. + TODO: This fact makes it particularly important that + PolynomialObjective is immutable. We should try to enforce this. + + Args: + use_variable_names: When True, the variables in the qubovert + object will use the same string names as appear in + the attribute variable_name_mapping. + """ + if not use_variable_names: + if self._qv_polynomial is None: + self._qv_polynomial = self._qubovert(use_variable_names=False) + return self._qv_polynomial + + else: + if self._qv_polynomial_named is None: + self._qv_polynomial_named = self._qubovert(use_variable_names=True) + return self._qv_polynomial_named + + def _qubovert(self, use_variable_names: bool = False): + """Get a qubovert model describing this polynomial. + + This method will return a qubovert PUBO or PUSO depending on + the domain of the variables. + + Args: + use_variable_names: When True, the variables in the qubovert + object will use the same string names as appear in + the attribute variable_name_mapping. + """ + if use_variable_names: + polynomial = dict() + for k, v in self.polynomial.items(): + new_key = tuple(self.variable_name_mapping[i] for i in k) + polynomial[new_key] = v + else: + polynomial = self.polynomial + + if self.domain is Domain.BOOLEAN: + model = qv.PUBO(polynomial) + elif self.domain is Domain.SPIN: + model = qv.PUSO(polynomial) + else: + raise RuntimeError("Domain is not valid.") + if use_variable_names: + model.set_reverse_mapping(self.variable_name_mapping) + return model + + def qubovert_boolean(self): + """Get a boolean qubovert model equivalent to this polynomial. + + This produces a PUBOMatrix which is qubovert's enumerated form of a + PUBO (polynomial unconstrained boolean optimization). This + transformation may change the variable ordering and for that + reason we also return a mapping that can be used to recover + the original variable identifiers. + """ + qv_model = self.qubovert(use_variable_names=False) + return {"polynomial": qv_model.to_pubo(), "mapping": qv_model.mapping} + + def qubovert_spin(self): + """Get a spin qubovert model equivalent to this polynomial. + + This produces a PUSOMatrix which is qubovert's enumerated form of a + PUSO (polynomial unconstrained spin optimization). This transformation + may change the variable ordering and for that + reason we also return a mapping that can be used to recover + the original variable identifiers. + """ + qv_model = self.qubovert(use_variable_names=False) + return {"polynomial": qv_model.to_puso(), "mapping": qv_model.mapping} + + def reduce_variables(self): + """Return a PolynomialObjective with trivial variables removed. + + As an example, suppose that we have a PolynomialObjective defined by + + PolynomialObjective( + polynomial={(1,): 1}, + num_variables=3 + ) + + which essentially means the polynomial p defined by p(x, y, z) = y. + This has two variables that p doesn't actually depend on. In this + case, the method `reduce_variables` will return a polynomial of one + variable along with a mapping to identify variables appropriately. + Specifically, in this case the return would be + + { + 'polynomial': PolynomialObjective(polynomial={(0,): 1}, + num_variables=1 + ) + 'mapping': {1: 0} + }. + """ + qv_form = self.qubovert(use_variable_names=False) + reduced_qv = qv_form.to_enumerated() + mapping = qv_form.mapping + num_vars = reduced_qv.num_binary_variables + + return { + "polynomial": PolynomialObjective( + polynomial=reduced_qv, + num_variables=num_vars, + domain=self.domain, + variable_name_mapping=None, + validate_types=False, + ), + "mapping": mapping, + } + + def compute_value(self, variable_values: dict, use_variable_names: bool = False): + """Compute the value of this polynomial at a specified input.""" + qv_polynomial = self.qubovert(use_variable_names=use_variable_names) + return qv_polynomial.value(variable_values) + + @classmethod + def __get_validators__(cls): + yield cls.validate_type + + @classmethod + def validate_type(cls, v): + """ + This validator only confirms that we are dealing with an object + of the expected type. This does not validate internal consistency + of attributes for the Objective function because that is done + by __init__. + + """ + if not isinstance(v, PolynomialObjective): + raise TypeError( + "Expected an object of class PolynomialObjective, but found " + f"{type(v)}." + ) + return v + + def pretty_str(self, include_domain: bool = True): + """Make a string that presents the polynomial algebraically. + + The names of variables can be controlled with the attribute + variable_name_mapping. + + Adapted from qubovert--thanks to Joseph T. Iosue. + """ + if self.domain is Domain.BOOLEAN: + domain_label = f"({self.num_variables} boolean variables)" + elif self.domain is Domain.SPIN: + domain_label = f"({self.num_variables} spin variables)" + else: + raise RuntimeError("Variable domain type seems to be invalid.") + + if self.polynomial == {}: + if include_domain: + return "0 " + domain_label + else: + return "0" + + res = "" + first = True + for term, coef in self.items(): + if coef >= 0 and (coef != 1 or not term): + res += f"{coef} " + elif coef < 0: + if coef == -1: + if first: + res += "-" if term else "-1 " + else: + res = res[:-2] + ("- " if term else "- 1 ") + else: + if first: + res += f"{coef} " + else: + res = res[:-2] + f"- {abs(coef)} " + + for x in term: + + res += self.variable_name_mapping[x] + " " + res += "+ " + first = False + res = res[:-2].strip() + + if include_domain: + return res + " " + domain_label + else: + return res + + def dict(self): + return { + "polynomial": self.polynomial, + "num_variables": self.num_variables, + "domain": self.domain.lower(), + "variable_name_mapping": self.variable_name_mapping, + } + + +def simplify_polynomial(polynomial: dict, domain: Domain) -> dict: + """Simplify given polynomial dict.""" + domain = Domain(domain.lower()) + if domain is Domain.BOOLEAN: + simplified_qv = qv.utils.PUBOMatrix(polynomial) + elif domain is Domain.SPIN: + simplified_qv = qv.utils.PUSOMatrix(polynomial) + else: + raise TypeError(f"Expected a Domain but found {type(domain)}.") + + return dict(simplified_qv) diff --git a/qcware/types/optimization/problem_spec/utils/__init__.py b/qcware/types/optimization/problem_spec/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qcware/types/optimization/problem_spec/utils/constraint_validation.py b/qcware/types/optimization/problem_spec/utils/constraint_validation.py new file mode 100644 index 0000000..859f6ca --- /dev/null +++ b/qcware/types/optimization/problem_spec/utils/constraint_validation.py @@ -0,0 +1,74 @@ +import pydantic +import dataclasses +from typing import Dict, List, Optional, Union + +from qcware.types.utils import pydantic_model_abridge_validation_errors + +from qcware.types.optimization.predicate import Predicate +from qcware.types.optimization.variable_types import Domain +from qcware.types.optimization.problem_spec import PolynomialObjective + + +def constraint_validation( + constraints: dict, + num_variables: int, + validate_types: bool = True, + domain: Optional[Union[Domain, str]] = None, + variable_name_mapping: Optional[Dict[int, str]] = None, +): + if domain is not None: + domain = Domain(domain.lower()) + for p_list in constraints.values(): + for i, p in enumerate(p_list): + if not isinstance(p, PolynomialObjective): + if domain is None: + domain = Domain.BOOLEAN + p_list[i] = PolynomialObjective( + polynomial=p, + num_variables=num_variables, + domain=domain, + variable_name_mapping=variable_name_mapping, + validate_types=validate_types, + ) + + # By changing from a pydantic.dataclass to a vanilla dataclass, + # we are able to turn off type checking while still using __post_init__. + if validate_types: + dataclass_selection = pydantic.dataclasses.dataclass + int_type = pydantic.StrictInt + else: + dataclass_selection = dataclasses.dataclass + int_type = int + + @dataclass_selection + class _ConstraintValidation: + constraint_dict: Dict[Predicate, List[PolynomialObjective]] + num_vars: int_type + + def __post_init__(self): + fixed_key_dict = {} + for k, v in self.constraint_dict.items(): + fixed_key_dict.update({Predicate(k.lower()): v}) + self.constraint_dict = fixed_key_dict + + removals = set() + for predicate, pubo_list in self.constraint_dict.items(): + if len(pubo_list) == 0: + removals.add(predicate) + for p in pubo_list: + if p.num_variables != self.num_vars: + raise RuntimeError( + f"Found a constraint for {p.num_variables} " + f"variables, but expected {self.num_vars} " + f"variables." + ) + + for pred in removals: + self.constraint_dict.pop(pred) + + return pydantic_model_abridge_validation_errors( + model=_ConstraintValidation, + max_num_errors=10, + constraint_dict=constraints, + num_vars=num_variables, + ) diff --git a/qcware/types/optimization/problem_spec/utils/polynomial_validation.py b/qcware/types/optimization/problem_spec/utils/polynomial_validation.py new file mode 100644 index 0000000..ba47e58 --- /dev/null +++ b/qcware/types/optimization/problem_spec/utils/polynomial_validation.py @@ -0,0 +1,82 @@ +import dataclasses +import pydantic +import itertools +from qcware.types.utils import pydantic_model_abridge_validation_errors + +from typing import Dict, Tuple, Set, Union + + +def compute_degree(polynomial: dict): + """Compute the largest length of a term in given polynomial dict. + + If there are no terms, return -1. This choice of -1 is corrected to + -inf after validation is complete. + + The degree here is not always the mathematically correct degree, even + for nontrivial polynomials. For + example, the polynomial specified by {(1,): 12, (0, 1): 0} has + mathematical degree 1 but this function will return 2. + """ + if polynomial == {}: + return -1 + return max(len(term) for term in polynomial) + + +def polynomial_validation( + polynomial: dict, num_variables: int, validate_types: bool = True +): + """ + + Args: + polynomial: + + num_variables: + + validate_types: If the polynomial has many terms, validating + all types can take a while. There is no reason to repeat + such a validation if it has already been done elsewhere. + By setting validate_types to False, the system will trust + the user to provide a polynomial with the correct structure. + + Returns: + + """ + # By sneakily changing from a pydantic.dataclass to a vanilla dataclass, + # we are able to turn off type checking while still using __post_init__. + # This is admittedly somewhat hacky. + if validate_types: + dataclass_selection = pydantic.dataclasses.dataclass + # int_type = pydantic.StrictInt + int_type = int # I disabled strict validation due to numpy issues. + else: + dataclass_selection = dataclasses.dataclass + int_type = int + + @dataclass_selection + class _PolynomialValidation: + poly: Dict[Tuple[int_type, ...], int_type] + num_vars: int_type + deg: int = None + variables: Set[int] = None + + def __post_init__(self): + # These are computed fields so they should start as None. + assert self.deg is None + assert self.variables is None + self.deg = compute_degree(self.poly) + self.variables = set(itertools.chain.from_iterable(self.poly.keys())) + if not self.variables.issubset(range(self.num_vars)): + raise ValueError( + f"Specified number of variables {self.num_vars} is inconsistent with " + f"the variables in the polynomial.\nExpected variables " + f"to be ints in the range {{0,...,{self.num_vars - 1}}} " + f"but found variables between {min(self.variables)} and " + f"{max(self.variables)} inclusive." + ) + + return pydantic_model_abridge_validation_errors( + model=_PolynomialValidation, + max_num_errors=10, + poly=polynomial, + num_vars=num_variables, + ) diff --git a/qcware/types/optimization/results/__init__.py b/qcware/types/optimization/results/__init__.py new file mode 100644 index 0000000..77e8f8c --- /dev/null +++ b/qcware/types/optimization/results/__init__.py @@ -0,0 +1,2 @@ +from .results_types import BruteOptimizeResult +from .results_types import Sample, BinaryResults diff --git a/qcware/types/optimization/results/results_types.py b/qcware/types/optimization/results/results_types.py new file mode 100644 index 0000000..67304cb --- /dev/null +++ b/qcware/types/optimization/results/results_types.py @@ -0,0 +1,417 @@ +from collections import OrderedDict +from itertools import tee, takewhile +from typing import Iterator, List, Optional, Tuple, Union, Iterable + +import numpy as np +import pydantic +from qcware.types.optimization import Domain +from qcware.types.optimization.problem_spec import BinaryProblem +from qcware.types.optimization.results import utils +from qcware.types.optimization.variable_types import domain_bit_values +from typing import Dict + + +class Sample(pydantic.BaseModel): + bitstring: Tuple[int, ...] + value: int + occurrences: int = 1 + + @pydantic.validator("occurrences", allow_reuse=True) + def positive_occurrences(cls, v): + if v <= 0: + raise ValueError( + "Sample occurrences must be positive. If there are no samples " + "for this bitstring, then the sample should not be included " + "in a BinaryResults construction." + "" + ) + return v + + def str_bitstring(self, domain: Domain): + """Write the sample bitstring in a format like '011' or '+--'.""" + return binary_ints_to_binstring(bl=self.bitstring, domain=domain) + + def convert(self, mapping: Dict[int, int]) -> "Sample": + return Sample( + bitstring=tuple(mapping[i] for i in self.bitstring), + value=self.value, + occurrences=self.occurrences, + ) + + def __add__(self, other: "Sample"): + if other.bitstring != self.bitstring: + raise ValueError("Cannot combine samples with different bitstring.") + if other.value != self.value: + raise ValueError("Cannot combine samples with different value.") + return Sample( + bitstring=self.bitstring, + occurrences=self.occurrences + other.occurrences, + value=self.value, + ) + + +def bool_sample_to_spin_sample(s: Sample): + """Convert a Sample with boolean variables to spin variables. + + Does not change sample occurrences or value. + """ + conversion = {0: 1, 1: -1} + try: + return s.convert(conversion) + except KeyError: + raise ValueError(f"Expected sample to have boolean domain.\nFound: {s}") + + +def spin_sample_to_bool_sample(s: Sample): + """Convert a Sample with spin variables to boolean variables. + + Does not change sample occurrences or value. + """ + conversion = {1: 0, -1: 1} + try: + return s.convert(conversion) + except KeyError: + raise ValueError(f"Expected sample to have spin domain.\nFound: {s}") + + +class BinaryResults(pydantic.BaseModel): + sample_ordered_dict: OrderedDict + original_problem: BinaryProblem + task_metadata: Optional[dict] = None + result_metadata: Optional[dict] = None + _sample_list: Optional[List[Sample]] = pydantic.PrivateAttr(None) + + @classmethod + def from_unsorted_samples( + cls, + samples: Iterator[Sample], + original_problem: BinaryProblem, + task_metadata: Optional[dict] = None, + result_metadata: Optional[dict] = None, + ): + # accumulator indexed by bitstring: + accumulator = {} + + for s in samples: + bitstring = s.str_bitstring(domain=original_problem.domain) + if bitstring in accumulator: + accumulator[bitstring].occurrences += s.occurrences + if accumulator[bitstring].value != s.value: + raise ValueError( + "Encountered samples with identical bitstring but " + "distinct objective values." + ) + else: + accumulator[bitstring] = s + if len(bitstring) != original_problem.num_variables: + raise ValueError( + f"Encountered bitstring with {len(s.bitstring)} " + f"variables. Expected " + f"{original_problem.num_variables}." + ) + + sample_ordered_dict = ( + (k, v) for (k, v) in sorted(accumulator.items(), key=lambda x: x[1].value) + ) + sample_ordered_dict = OrderedDict(sample_ordered_dict) + return cls( + sample_ordered_dict=sample_ordered_dict, + original_problem=original_problem, + task_metadata=task_metadata, + result_metadata=result_metadata, + ) + + def __eq__(self, value): + return ( + self.sample_ordered_dict == value.sample_ordered_dict + and self.original_problem == value.original_problem + and self.task_metadata == value.task_metadata + and self.result_metadata == value.result_metadata + ) + + def __getitem__(self, bitstring: Union[str, Iterable[int]]) -> Sample: + """Get sample data for given bitstring. + + Bitstrings can be specified in a few ways: + - Iterable[int] as in [0, 1, 0, 1] + - str as in '0101' (or '+-+-' in the spin case) + + Returns: + Sample with occurrences set to the total number of occurrences + for this particular bitstring. + """ + if not isinstance(bitstring, str): + bitstring = binary_ints_to_binstring(bitstring, domain=self.domain) + + try: + return self.sample_ordered_dict[bitstring] + except KeyError: + raise KeyError(f"There is no sample matching {bitstring}.") + + def num_occurrences(self, bitstring: Union[str, Iterable[int]]) -> int: + """Get the number of occurrences for a given binary string. + + If the specified bitstring does has no samples, 0 is returned. + """ + try: + return self[bitstring].occurrences + except KeyError: + return 0 + + def keys(self): + """Iterate through str representations of samples. + + The keys are binary strings like '0101'. The order of the iteration + goes from lowest values of the objective function to highest. + """ + return self.sample_ordered_dict.keys() + + def items(self): + """Iterate through the sample data. + + The order of iteration goes from lowest values of the objective + function to the highest. + """ + return self.sample_ordered_dict.items() + + @property + def domain(self) -> Domain: + """The domain (boolean or spin) of the variables.""" + return self.original_problem.domain + + @property + def num_variables(self) -> int: + """The number of binary variables for the objective function.""" + return self.original_problem.num_variables + + @property + def samples(self): + return self.sample_ordered_dict.values() + + @property + def sample_list(self) -> List[Sample]: + """List of all samples ordered by objective value.""" + if self._sample_list is None: + self._sample_list = list(self.sample_ordered_dict.values()) + return self._sample_list + + @property + def num_distinct_bitstrings(self) -> int: + return len(self.sample_ordered_dict) + + @property + def total_num_occurrences(self) -> int: + """The total number of occurrences of any bitstring. + + Includes in the count multiple occurrences of the same strings. + """ + return sum(s.occurrences for s in self.sample_list) + + @property + def lowest_value(self) -> int: + """Lowest observed value of the objective function.""" + first_sample = next(iter(self.samples)) + return first_sample.value + + @property + def lowest_value_bitstring(self) -> Tuple[int, ...]: + """A Bitstring with the lowest value of the objective function. + + This property only provides one example of a bitstring with the + lowest value. To get all bitstrings with lowest value, use + lowest_value_sample_list. + """ + first_sample = next(iter(self.samples)) + return first_sample.bitstring + + @property + def lowest_value_sample_list(self) -> List[Sample]: + """List of samples with lowest observed objective value.""" + lowest_value = self.lowest_value + samples = takewhile(lambda s: s.value == lowest_value, self.samples) + return list(samples) + + @property + def num_occurrences_lowest_value(self) -> int: + """The number of observed occurrences of the lowest value.""" + return self.num_occurrences_under(val=self.lowest_value + 1) + + def num_occurrences_under(self, val: int) -> int: + """The number of occurrences below a given objective value.""" + samples = takewhile(lambda s: s.value < val, self.samples) + return sum(s.occurrences for s in samples) + + @property + def num_bitstrings_lowest_value(self) -> int: + """The number of observed bitstrings of the lowest value.""" + return self.num_bitstrings_under(val=self.lowest_value + 1) + + def num_bitstrings_under(self, val: int) -> int: + """The number of distinct bitstrings below a given objective value.""" + samples = takewhile(lambda s: s.value < val, self.samples) + return sum(1 for _ in samples) + + def __eq__(self, other: "BinaryResults"): + conditions = ( + self.sample_ordered_dict == other.sample_ordered_dict, + self.original_problem == other.original_problem, + self.task_metadata == other.task_metadata, + self.result_metadata == other.result_metadata, + ) + return all(conditions) + + def __str__(self) -> str: + if self.num_distinct_bitstrings == 0: + return "No solutions sampled." + out = "Objective value: " + + out += str(self.lowest_value) + "\n" + out += "Solution: " + out += str(self.lowest_value_bitstring) + + if self.num_occurrences_lowest_value > 1: + out += ( + f" (and {self.num_bitstrings_lowest_value-1} " + f"other equally good solution" + ) + if self.num_bitstrings_lowest_value == 2: + out += ")" + else: + out += "s)" + return out + + +def samples_to_array(samples: Iterator[Sample], domain: Domain, num_variables: int): + """Convert a sequence of Samples to a NumPy array. + + Performs optional validation if domain and num_variables are specified. + """ + try: + bitstrings = np.array(list(s.bitstring for s in samples), dtype=int) + except ValueError: + raise ValueError("Samples provided appear to have inconsistent string length.") + + if bitstrings.shape[-1] != num_variables: + raise ValueError( + f"Sample bitstrings have {bitstrings.shape[-1]} variables, " + f"but the original problem instance has {num_variables}." + ) + + valid_bits = domain_bit_values(domain) + if not np.isin(bitstrings, valid_bits).all(): + raise ValueError( + f"Bitstrings are expected to have domain {domain} but " + f"found entries outside of {valid_bits}." + ) + return bitstrings + + +class BruteOptimizeResult(pydantic.BaseModel): + """Return type for brute force maximization and minimization. + + When solution_exists == False, we must have value is None and + argmin == []. + + Arguments are specified with a list of strings that describe solutions. + For the boolean case, this means something like ['00101', '11100'] and + for the spin case, this means something like ['++-+-', '---++']. + + The method int_argmin can be used to obtain minima in the format + Boolean case: [[0, 0, 1, 0, 1], [1, 1, 1, 0, 0]] + or + Spin case: [[1, 1, -1, 1, -1], [-1, -1, -1, 1, 1]]. + """ + + domain: Domain + value: Optional[int] = None + argmin: List[str] = [] + solution_exists: bool = True + + @pydantic.validator("solution_exists", always=True) + def no_solution_check(cls, sol_exists, values): + if not sol_exists: + if not values["value"] is None: + raise ValueError("Value given but solution_exists=False.") + if not values["argmin"] == []: + raise ValueError("argmin given but solution_exists=False.") + + else: + if values["value"] is None or values["argmin"] == []: + raise ValueError("solution_exists=True, but no solution was specified.") + return sol_exists + + def int_argmin(self) -> List[List[int]]: + """Convert argmin to a list of list of ints.""" + + def to_int(x: str): + if self.domain is Domain.BOOLEAN: + return int(x) + else: + if x == "+": + return 1 + elif x == "-": + return -1 + else: + raise ValueError(f"Unrecognized symbol {x}. Expected '+' or '-'.") + + return [[to_int(x) for x in s] for s in self.argmin] + + @property + def num_variables(self): + if not self.solution_exists: + return + return len(self.argmin[0]) + + @property + def num_minima(self): + return len(self.argmin) + + def __repr__(self): + if self.solution_exists: + out = "forge.types.BruteOptimizeResult(\n" + out += f"value={self.value}\n" + char_estimate = self.num_variables * len(self.argmin) + out += utils.short_list_str(self.argmin, char_estimate, "argmin") + return out + "\n)" + else: + return "forge.types.BruteOptimizeResult(solution_exists=False)" + + def __str__(self): + if not self.solution_exists: + return "No bit string satisfies constraints." + + out = "Objective value: " + str(self.value) + "\n" + int_argmin = self.int_argmin() + out += f"Solution: {int_argmin[0]}" + if self.num_minima > 1: + out += f" (and {self.num_minima-1} other equally good solution" + if self.num_minima == 2: + out += ")" + else: + out += "s)" + return out + + +def binary_ints_to_binstring(bl: Iterable[int], domain: Domain = Domain.BOOLEAN): + """Convert a binary object like [0, 1, 1] to a string like '011'.""" + if domain is Domain.BOOLEAN: + + def conv(bit): + if bit not in {0, 1}: + raise ValueError(f"Expected 0 or 1, found {bit}") + return str(bit) + + return "".join(map(conv, bl)) + elif domain is Domain.SPIN: + + def conv(bit): + if bit == 1: + return "+" + elif bit == -1: + return "-" + else: + raise ValueError(f"Expected 1 or -1, found {bit}") + + return "".join(map(conv, bl)) + else: + raise TypeError(f"Expected Domain, found {type(domain)}") diff --git a/qcware/types/optimization/results/utils/__init__.py b/qcware/types/optimization/results/utils/__init__.py new file mode 100644 index 0000000..66132f1 --- /dev/null +++ b/qcware/types/optimization/results/utils/__init__.py @@ -0,0 +1,3 @@ +from .utils import short_list_str +from .utils import short_string +from .utils import format_feasible_set diff --git a/qcware/types/optimization/results/utils/utils.py b/qcware/types/optimization/results/utils/utils.py new file mode 100644 index 0000000..1b0bece --- /dev/null +++ b/qcware/types/optimization/results/utils/utils.py @@ -0,0 +1,50 @@ +import math +from qcware.types.optimization import utils + + +def short_list_str(lst, expected_characters, name=None): + if name is None: + out = "" + else: + out = name + "=" + if expected_characters <= 200 or len(lst) < 6: + out += str(lst) + else: + out += str(lst[:3])[:-1] + out += " ..., " + str(lst[-3:])[1:] + + return out + + +def short_string(string: str, name=None): + if len(string) <= 80: + out = string + else: + out = string[:10] + out += f"... [{len(string)-20} characters hidden] ..." + out += string[-10:] + + if name is None: + return out + else: + return name + "=" + out + + +def format_feasible_set(feasible_set: str, num_feasible: int = None): + """Get the feasible set as a list of binary strings. + + This format is less compressed than the feasible_set attribute + which is a string of length 2^n. + """ + if feasible_set is None: + if num_feasible == 0: + return [] + raise RuntimeError("No feasible set is stored.") + + num_values = len(feasible_set) + num_variables = round(math.log2(num_values)) + if 2 ** num_variables != num_values: + raise ValueError("feasible_set must be a string of length 2^n.") + + index_list = [i for i in range(len(feasible_set)) if feasible_set[i] == "1"] + return utils.intlist_to_binlist(index_list, num_variables) diff --git a/qcware/types/optimization/utils/__init__.py b/qcware/types/optimization/utils/__init__.py new file mode 100644 index 0000000..6e06eaf --- /dev/null +++ b/qcware/types/optimization/utils/__init__.py @@ -0,0 +1,3 @@ +from .misc import iterable +from .misc import int_to_bin +from .misc import intlist_to_binlist diff --git a/qcware/types/optimization/utils/misc.py b/qcware/types/optimization/utils/misc.py new file mode 100644 index 0000000..1642ff2 --- /dev/null +++ b/qcware/types/optimization/utils/misc.py @@ -0,0 +1,153 @@ +from typing import List, Optional, Tuple, Union +from qcware.types.optimization import Domain +import numpy as np + + +def iterable(obj): + """Return True iff iter(obj) works.""" + try: + iter(obj) + except TypeError: + return False + else: + return True + + +def int_to_bin( + x: int, + num_bits: int, + int_return=False, +): + if x >= 2 ** num_bits: + raise ValueError(f"Insufficient bits to store integer {x}.") + if x < 0: + raise ValueError(f"Integer should be nonnegative but found {x}.") + + bin_format = str(bin(x))[2:] + padding_size = num_bits - len(bin_format) + bin_format = padding_size * "0" + bin_format + if not int_return: + return bin_format + else: + return [int(x) for x in bin_format] + + +def intlist_to_binlist( + integers: List[int], + num_bits: int, + symbols: Union[Tuple[str, str], Domain, None] = None, +): + """Convert list of integers to list of binary strings. + + Example: if there are three bits, [3, 1] will be + converted to ['011', '001']. + + If `symbols` is given then strings other than '0' or '1' can be used. + For example, if symbols=('a', 'b'), then [3, 1] will be + converted to ['abb', 'aab'] assuming that num_bits=3. + + Since the most common symbol choices are "boolean" and "spin" where we use + ('0', '1') and ('+', '-') respectively, symbols can also be specified + by a Domain. + """ + if symbols is Domain.BOOLEAN: + symbols = ("0", "1") + if symbols is Domain.SPIN: + symbols = ("+", "-") + + bin_list = [int_to_bin(x, num_bits) for x in integers] + if symbols is None: + return bin_list + else: + + def bin_to_symbols(x): + if x == "0": + return symbols[0] + elif x == "1": + return symbols[1] + else: + raise RuntimeError(f"Encountered {x} but expected '0' or '1'.") + + return ["".join(map(bin_to_symbols, s)) for s in bin_list] + + +def bitarray_to_int(bitstrings: np.ndarray, domain: Domain): + """Convert an array of binary strings to corresponding ints. + + For example, if `bitstrings` is + np.array( + [[0, 1, 0, 0], + [1, 1, 1, 1], + [0, 0, 0, 0]] + ) + then this function returns [] + """ + if domain is Domain.SPIN: + bitstrings = (1 - bitstrings) // 2 + + num_variables = bitstrings.shape[-1] + powers = np.array([2 ** n for n in reversed(range(num_variables))]) + return (powers * bitstrings).sum(axis=-1) + + +def bitstring_to_intlist(binstring, domain: Domain): + if domain is Domain.BOOLEAN: + return [int(i) for i in binstring] + elif domain is Domain.SPIN: + + def conversion(i): + if i == "+": + return 1 + elif i == "-": + return -1 + else: + raise ValueError( + "For spin domain, if binstring is an str, bits must be" + f"'+' or '-'. Encountered {i}." + ) + + return [conversion(i) for i in binstring] + else: + raise TypeError(f"Expected Domain, encountered {type(domain)}.") + + +def bin_to_int(bin_spec, domain: Domain, num_variables: Optional[int] = None): + """Convert a binary specification of a number to an int. + + If `bin_spec` is already an int, it is returned as is. + + Args: + bin_spec: Binary specification of a number. Valid specifications are: + - List[int] as in [0, 1, 0, 1] + - str as in '0101' (or '+-+-' in the spin case) + - int as in 5. In this case, 5 is returned. + domain: Domain for the variables (boolean or spin). + num_variables: When specified, checks that the number of variables is + consistent with `spec`. Note that if `spec` is an `int`, then + this validation can only confirm that `spec` is not too large. + + Returns: Integer corresponding to the given binary string. + """ + if isinstance(bin_spec, int): + if bin_spec < 0: + raise ValueError(f"Integer bin_spec must be positive, found {bin_spec}.") + if num_variables is not None: + if bin_spec >= 2 ** num_variables: + raise ValueError( + f"{bin_spec} is too large for {num_variables} " f"binary variables." + ) + return bin_spec + if num_variables is not None: + if len(bin_spec) != num_variables: + raise ValueError( + f"Expected a binary string with {num_variables} variables, " + f"but got {bin_spec} which has {len(bin_spec)} variables." + ) + if isinstance(bin_spec, str): + return bin_to_int( + bin_spec=bitstring_to_intlist(binstring=bin_spec, domain=domain), + domain=domain, + ) + else: + bin_spec = bitarray_to_int(np.array(bin_spec), domain=domain) + return int(bin_spec) diff --git a/qcware/types/optimization/variable_types.py b/qcware/types/optimization/variable_types.py new file mode 100644 index 0000000..2661b57 --- /dev/null +++ b/qcware/types/optimization/variable_types.py @@ -0,0 +1,21 @@ +import enum + + +class Domain(str, enum.Enum): + """Possible types of variables for binary polynomials.""" + + BOOLEAN = "boolean" + SPIN = "spin" + + def __repr__(self): + return str(self).__str__() + + +def domain_bit_values(domain: Domain): + """Get values of bits that are intended to be associated with a Domain.""" + if domain is Domain.BOOLEAN: + return [0, 1] + elif domain is Domain.SPIN: + return [1, -1] + else: + raise TypeError(f"Expected Domain, found {type(domain)}.") diff --git a/qcware/types/py.typed b/qcware/types/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/qcware/types/qml/__init__.py b/qcware/types/qml/__init__.py new file mode 100644 index 0000000..facde8d --- /dev/null +++ b/qcware/types/qml/__init__.py @@ -0,0 +1,9 @@ +from qcware.types.qml.fit_data import ( + model_dict, + FitData, + FitDataBase, + QMeansFitData, + QNearestCentroidFitData, + QNeighborsClassifierFitData, + QNeighborsRegressorFitData, +) diff --git a/qcware/types/qml/fit_data.py b/qcware/types/qml/fit_data.py new file mode 100644 index 0000000..ebf1592 --- /dev/null +++ b/qcware/types/qml/fit_data.py @@ -0,0 +1,100 @@ +"""Fit data, data structures only. + +This is intended to ease the separation of fit_and_predict into +the obvious layers of fitting and prediction, but to do that we +need to be prepared to serialize the sklearn fitting and prediction +classes which are the basis of the quantum algorithms, as well +as additional internal data. +""" + +from typing import Optional, Union, Tuple + +import numpy.typing +import quasar +from pydantic import BaseModel +from qcware.types.ndarray import NDArray + + +def model_dict(m, keys): + return {k: getattr(m, k) for k in keys} + + +class FitDataBase(BaseModel): + loader_mode: str + num_measurements: int + absolute: bool + opt_shape: Optional[Tuple[int, int]] + + +class QNearestCentroidFitData(FitDataBase): + """Fit data for QNearestCentroid. + + Note that it is not intended that users create this + data structure themselves! It is used for serialization. + + As such, fit data is independent of loader circuit and backend. + """ + + # The test_distances member doesn't exist for the fitting + # step so is omitted here + # test_distances: list + # following from NearestCentroid + centroids: NDArray + classes: NDArray + n_features_in: int + metric: str + shrink_threshold: None + + +# note: the peculiar extra regressor_ and classifier_ +# prefixes seem to be necessary to distinguish the two classes +# for pydantic +class QNeighborsRegressorFitData(FitDataBase): + """Fit data for QNeighborsRegressor. + + Note that it is not intended that users create this + data structure themselves! It is used for serialization. + + As such, fit data is independent of loader circuit and backend. + """ + + # set after fit + regressor_data: NDArray + regressor_labels: NDArray + # from kneighborsregressor + n_neighbors: int + + +class QNeighborsClassifierFitData(FitDataBase): + # set after fit + classifier_data: NDArray + classifier_labels: NDArray + # from kneighborsregressor + n_neighbors: int + + +class QMeansFitData(FitDataBase): + data: NDArray + labels: NDArray + cluster_centers: NDArray + history: NDArray + n_iter: int + inertia: float + analysis: bool + # from KMeans + n_clusters: int + init: str + n_init: int + max_iter: int + tol: float + + +class FitData(BaseModel): + + model_name: str + fit_data: Union[ + QNearestCentroidFitData, + QNeighborsRegressorFitData, + QNeighborsClassifierFitData, + QMeansFitData, + ] diff --git a/qcware/types/test_strategies/optimization.py b/qcware/types/test_strategies/optimization.py new file mode 100644 index 0000000..eee765f --- /dev/null +++ b/qcware/types/test_strategies/optimization.py @@ -0,0 +1,113 @@ +from hypothesis.strategies import ( + lists, + composite, + integers, + dictionaries, + sampled_from, + none, + just, + text, + iterables, +) +import itertools +import string +import cProfile + +from qcware.types.optimization import ( + BinaryProblem, + Constraints, + Domain, + PolynomialObjective, + Predicate, +) + +from qcware.types.optimization.results.results_types import Sample, BinaryResults + +# the following are some strategies meant solely to create data to test serialization; +# they make no bones about being reasonable problems +def keys(min_var: int = 0, max_var: int = 4): + return lists( + integers(min_var, max_var), min_size=1, max_size=(max_var - min_var) + ).map(tuple) + + +def qdicts(min_var: int = 0, max_var: int = 4, min_size: int = 1, max_size: int = 3): + return dictionaries(keys(), integers(1, 5), min_size=1, max_size=3) + + +@composite +def polynomial_objectives(draw, num_vars): + objective = draw(qdicts(max_var=num_vars - 1)) + variables = set(itertools.chain.from_iterable(objective.keys())) + num_variables = num_vars + domain = draw(sampled_from(Domain)) + varmap = draw(none() | just({k: str(k) for k in variables})) + return PolynomialObjective( + objective, + num_variables=num_variables, + domain=domain, + variable_name_mapping=varmap, + ) + + +@composite +def constraints(draw, num_vars): + domain = draw(sampled_from(Domain)) + constraint_dict = draw( + dictionaries( + sampled_from(Predicate), + lists(polynomial_objectives(num_vars), min_size=1), + min_size=1, + ) + ) + # we force all constraints here to have the same number of variables + for constraint, polys in constraint_dict.items(): + for p in polys: + p.domain = domain + p.num_variables = num_vars + return Constraints(constraint_dict, num_vars) + + +@composite +def binary_problems(draw, num_vars): + objective = draw(polynomial_objectives(num_vars)) + _constraints = draw(constraints(num_vars=objective.num_variables)) + name = draw(text(string.ascii_lowercase + string.ascii_uppercase)) + return BinaryProblem(objective=objective, constraints=_constraints, name=name) + + +@composite +def samples(draw, problem): + if problem.domain == Domain.BOOLEAN: + bitstring = draw( + lists( + sampled_from([0, 1]), + min_size=problem.num_variables, + max_size=problem.num_variables, + ) + ) + elif problem.domain == Domain.SPIN: + bitstring = draw( + lists( + sampled_from([-1, 1]), + min_size=problem.num_variables, + max_size=problem.num_variables, + ) + ) + value = problem.objective.compute_value( + {index: v for index, v in enumerate(bitstring)} + ) + # could change this too + occurrences = 1 + return Sample(bitstring=bitstring, value=value, occurrences=occurrences) + + +@composite +def sample_sequences(draw, problem, num_samples): + return draw(iterables(samples(problem), min_size=num_samples, max_size=num_samples)) + + +@composite +def binary_results(draw, problem, num_samples): + these_samples = draw(sample_sequences(problem, num_samples)) + return BinaryResults.from_unsorted_samples(samples, original_problem) diff --git a/qcware/types/test_strategies/qml.py b/qcware/types/test_strategies/qml.py new file mode 100644 index 0000000..f05cd06 --- /dev/null +++ b/qcware/types/test_strategies/qml.py @@ -0,0 +1,64 @@ +from hypothesis import strategies as st +from hypothesis.extra import numpy as hnp +from qcware.types.qml import FitData, QNearestCentroidFitData +from quasar import Circuit + +sample_backends = sorted(["qcware/cpu", "qcware/cpu_simulator", "qcware/gpu_simulator"]) + +sample_loader_modes = sorted(["parallel", "diagonal", "optimized"]) + +model_names = sorted(["QNearestCentroid"]) + + +@st.composite +def q_nearest_centroid_fit_datas(draw): + """Generates bogus data for testing serialization. + Of particular note: the circuit is just a bell pair. + """ + backend = draw(st.sampled_from(sample_backends)) + loader_mode = draw(st.sampled_from(sample_loader_modes)) + num_measurements = draw(st.integers()) + absolute = draw(st.booleans()) + opt_shape = draw(st.one_of(st.tuples(st.integers(), st.integers()), st.none())) + _n_classes = draw(st.integers(min_value=1, max_value=5)) + _n_features = draw(st.integers(min_value=1, max_value=5)) + + centroids = draw( + hnp.arrays( + "float64", + (_n_classes, _n_features), + elements={"allow_nan": False, "allow_infinity": False}, + ) + ) + classes = draw( + hnp.arrays( + "float64", + (_n_classes,), + elements={"allow_nan": False, "allow_infinity": False}, + ) + ) + n_features_in = _n_features + feature_names_in = None + metric = "euclidean" + shrink_threshold = None + return QNearestCentroidFitData( + backend=backend, + loader_mode=loader_mode, + num_measurements=num_measurements, + absolute=absolute, + opt_shape=opt_shape, + centroids=centroids, + classes=classes, + n_features_in=n_features_in, + feature_names_in=feature_names_in, + metric=metric, + shrink_threshold=shrink_threshold, + ) + + +@st.composite +def fit_datas(draw): + model_name = draw(st.sampled_from(model_names)) + if model_name == "QNearestCentroid": + fit_data = draw(q_nearest_centroid_fit_datas()) + return FitData(model_name=model_name, fit_data=fit_data) diff --git a/qcware/types/utils.py b/qcware/types/utils.py new file mode 100644 index 0000000..8692872 --- /dev/null +++ b/qcware/types/utils.py @@ -0,0 +1,15 @@ +from pydantic import ValidationError + + +def pydantic_model_abridge_validation_errors( + model, max_num_errors: int, *args, **kwargs +): + try: + return model(*args, **kwargs) + + except ValidationError as error: + # noinspection PyTypeChecker + abridged_error = ValidationError( + errors=error.raw_errors[:max_num_errors], model=model + ) + raise abridged_error from None diff --git a/requirements.txt b/requirements.txt index b623015..1a6df48 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,23 @@ -numpy -scipy +aiohttp +backoff +colorama +hypothesis[numpy] +icontract +ipython_genutils +lz4 +matplotlib +nbmake networkx +numpy +packaging +pandas +pydantic +python-decouple +qcware-quasar +qcware-transpile[qcware-quasar,qiskit] +qiskit +qubovert requests -pytest -protobuf==3.3.0 +rich +tabulate +wheel diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 224a779..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description-file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 989f98a..0000000 --- a/setup.py +++ /dev/null @@ -1,21 +0,0 @@ -from distutils.core import setup -import setuptools - - -setup( - name='qcware', - packages=['qcware'], - version='0.2.12', - description='Functions for easily interfacing with the QC Ware Platform from Python', - author='QC Ware Corp.', - author_email='info@qcware.com', - url='https://github.com/qcware/platform_client_library_python', - download_url='https://github.com/qcware/platform_client_library_python/tarball/0.2.12', - keywords=['quantum', 'computing', 'cloud', 'API'], - classifiers=[], - install_requires=[ - 'numpy', - 'protobuf', - 'requests', - ], -) diff --git a/tests.py b/tests.py deleted file mode 100644 index 1622fa5..0000000 --- a/tests.py +++ /dev/null @@ -1,15 +0,0 @@ -import numpy as np -import networkx as nx -import qcware - - -def test_invalid_1(): - my_graph = nx.complete_graph(4) - pos = nx.spring_layout(my_graph) - adj = nx.adjacency_matrix(my_graph).todense() - adj = -(np.diag(np.squeeze((np.matrix(adj) * np.ones([adj.shape[0], 1])).A)) - np.matrix(adj)).astype(int) - r = qcware.optimization.solve_binary(Q=adj, key="BAD_KEY") - - assert(r.get('error')) - - return diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..37a8140 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,49 @@ +""" +Test configuration for pytest in an attempt to make some +regimes (such as connected IBMQ tests, or Vulcan tests) +selectable but not run as default. +""" + +# see https://docs.pytest.org/en/stable/example/parametrize.html + + +def pytest_addoption(parser): + parser.addoption( + "--vulcan", action="store_true", help="run test for vulcan backends" + ) + parser.addoption("--ibmq", action="store_true", help="run test for ibmq backends") + parser.addoption( + "--dwavedirect", action="store_true", help="run test for dwave_direct backends" + ) + parser.addoption( + "--awsslow", + action="store_true", + help="run tests for braket schedule-window backends (ionq, rigetti)", + ) + + +# from some ideas in https://superorbit.al/journal/focusing-on-pytest/ +def pytest_collection_modifyitems(session, config, items): + deselected_items = [] + selected_items = [] + for item in items: + if ( + "qcware/gpu_simulator" in item.nodeid or "qcware/gpu" in item.nodeid + ) and not config.getoption("vulcan"): + deselected_items.append(item) + elif "ibmq" in item.nodeid and not config.getoption("ibmq"): + deselected_items.append(item) + elif "dwave_direct" in item.nodeid and not config.getoption("dwavedirect"): + deselected_items.append(item) + elif ( + "awsbraket/ionq" in item.nodeid + or "awsbraket/rigetti_aspen_11" in item.nodeid + or "awsbraket/rigetti_aspen_m_1" in item.nodeid + or "awsbraket/tn1" in item.nodeid + ) and not config.getoption("awsslow"): + deselected_items.append(item) + else: + selected_items.append(item) + + config.hook.pytest_deselected(items=deselected_items) + items[:] = selected_items diff --git a/tests/unit/test_backend_exception.py b/tests/unit/test_backend_exception.py new file mode 100644 index 0000000..8a8b842 --- /dev/null +++ b/tests/unit/test_backend_exception.py @@ -0,0 +1,33 @@ +import os + +import pytest +import quasar +from qcware import forge +from qcware.forge.circuits.quasar_backend import QuasarBackend +from qcware.types.optimization import BinaryProblem, PolynomialObjective + +QCWARE_API_KEY = os.environ["QCWARE_API_KEY"] +QCWARE_HOST = os.environ["QCWARE_HOST"] + +# test temporarily removed since the BinaryProblem class should check this +# def test_solve_binary_with_brute_force(): +# Q = {(0, 0): "POTATO"} + + +def test_solve_binary_with_brute_force(): + Q = {(0, 0): 1, (1, 1): 1, (0, 1): -2, (2, 2): -2, (3, 3): -4, (3, 2): -6} + # Q = {(0, 0): "POTATO"} + qubo = PolynomialObjective(polynomial=Q, num_variables=4, domain="boolean") + problem = BinaryProblem(objective=qubo) + + # run_backend_method is a sort of special case because it nests kwargs, so let's + # just make sure + with pytest.raises(forge.exceptions.ApiCallExecutionError): + backend = QuasarBackend("qcware/cpu_simulator") + q = quasar.Circuit() + q.H(0).CX(0, 1) + backend.run_measurement(circuit=q, nqubit=-42) + + # if there's a problem in the dispatcher (no backend), task_failure should handle that too + with pytest.raises(forge.exceptions.ApiCallFailedError): + result = forge.optimization.optimize_binary(instance=problem, backend="POTATO") diff --git a/tests/unit/test_brute_force.py b/tests/unit/test_brute_force.py new file mode 100644 index 0000000..2b106c2 --- /dev/null +++ b/tests/unit/test_brute_force.py @@ -0,0 +1,249 @@ +import itertools + +import pytest +from qcware.forge.optimization import brute_force_minimize +from qcware.serialization.transforms.to_wire import ( + constraints_from_wire, + polynomial_objective_from_wire, + to_wire, +) +from qcware.types.optimization.predicate import Predicate +from qcware.types.optimization.problem_spec import Constraints, PolynomialObjective + + +def pubo_example_1(constrained: bool): + out = {} + p = { + (0,): -3, + (0, 1): 2, + (0, 2): 2, + (0, 3): 2, + (1,): -3, + (1, 2): 2, + (1, 3): 2, + (2,): -3, + (2, 3): 2, + (3,): -2, + (0, 1, 3): -2, + (): 7, + } + p = PolynomialObjective( + polynomial=p, + num_variables=4, + ) + if not constrained: + expected_minima = {"0110", "1010", "1100", "1101"} + expected_value = 3 + else: + nonpositive_constraint_1 = PolynomialObjective( + {(): -1, (0,): 1, (1,): 1, (2,): 1, (3,): 1}, num_variables=4 + ) + constraints = {Predicate.NONPOSITIVE: [nonpositive_constraint_1]} + constraints = Constraints(constraints=constraints, num_variables=4) + out.update({"constraints": constraints}) + expected_minima = {"1000", "0100", "0010"} + expected_value = 4 + + out.update( + { + "pubo": p, + "expected_value": expected_value, + "expected_minima": expected_minima, + "solution_exists": True, + } + ) + return out + + +def pubo_example_2(constrained: bool): + out = {} + p = { + (): 123, + (0,): -368, + (1,): 138, + (2,): 376, + (0, 1): -305, + (0, 2): 99, + (1, 2): -397, + } + p = PolynomialObjective(polynomial=p, num_variables=3) + + if not constrained: + expected_minima = {"110"} + expected_value = -412 + else: + constraints = { + Predicate.NEGATIVE: [ + # a + b + c < 3 (violated iff a=b=c=1) + PolynomialObjective( + {(): -3, (0,): 1, (1,): 1, (2,): 1}, num_variables=3 + ), + # Always true + PolynomialObjective({(): -1}, num_variables=3), + ], + Predicate.ZERO: [ + # (a+b+c-1)^2 == 0 (true iff exactly one variable is 1.) + PolynomialObjective( + { + (0, 1): 2, + (0, 2): 2, + (1, 2): 2, + (0,): -1, + (1,): -1, + (2,): -1, + (): 1, + }, + num_variables=3, + ), + # a + c = 1 (true iff a XOR c) + PolynomialObjective({(0,): 1, (2,): 1, (): -1}, num_variables=3), + ], + } + constraints = Constraints(constraints, num_variables=3) + + out.update({"constraints": constraints}) + expected_minima = {"100"} + expected_value = -245 + + out.update( + { + "pubo": p, + "expected_value": expected_value, + "expected_minima": expected_minima, + "solution_exists": True, + } + ) + return out + + +def pubo_example_3(): + """ + p(x) = 3 x_0 x_1 _x2 x3 + """ + out = {} + p = PolynomialObjective( + polynomial={ + ( + 0, + 1, + 2, + 3, + ): 3 + }, + num_variables=4, + ) + out.update( + { + "pubo": p, + "expected_value": 0, + "expected_minima": { + "0000", + "0001", + "0010", + "0011", + "0100", + "0101", + "0110", + "0111", + "1000", + "1001", + "1010", + "1011", + "1100", + "1101", + "1110", + }, + "solution_exists": True, + } + ) + return out + + +def impossible_example(): + out = {} + p = {(0,): -3, (0, 1): 2, (0, 2): 2, (): -3} + p = PolynomialObjective( + polynomial=p, + num_variables=3, + ) + + constraints = { + Predicate.NONZERO: [PolynomialObjective({(0, 1): -3}, num_variables=3)], + Predicate.ZERO: [PolynomialObjective({(1,): 1}, num_variables=3)], + } + constraints = Constraints(constraints=constraints, num_variables=3) + out.update( + { + "pubo": p, + "expected_value": None, + "expected_minima": set(), + "solution_exists": False, + "constraints": constraints, + } + ) + return out + + +unconstrained_examples = ( + pubo_example_1(False), + pubo_example_2(False), + pubo_example_3(), +) + +constrained_examples = ( + pubo_example_1(True), + pubo_example_2(True), + impossible_example(), +) + + +def test_serialize_objective(): + p = pubo_example_1(False)["pubo"] + p2 = polynomial_objective_from_wire(to_wire(p)) + assert p.dict() == p2.dict() + + +def test_serialize_constraints(): + c = pubo_example_1(True)["constraints"] + c2 = constraints_from_wire(to_wire(c)) + assert to_wire(c) == to_wire(c2) + + +@pytest.mark.parametrize( + "example,backend", + itertools.product(unconstrained_examples, ("qcware/cpu", "qcware/gpu")), +) +def test_brute_force_minimize_unconstrained(example, backend): + print("EXAMPLE: ", example) + p = example["pubo"] + expected_value = example["expected_value"] + expected_minima = example["expected_minima"] + expected_solution_exists = example["solution_exists"] + + out = brute_force_minimize(p, backend=backend) + actual_minima = set(out.argmin) + actual_value = out.value + + assert actual_value == expected_value + assert actual_minima == expected_minima + assert out.solution_exists == expected_solution_exists + + +@pytest.mark.parametrize( + "example,backend", + itertools.product(constrained_examples, ("qcware/cpu", "qcware/gpu")), +) +def test_brute_force_minimize_constrained(example, backend): + p = example["pubo"] + expected_value = example["expected_value"] + expected_minima = example["expected_minima"] + constraints = example["constraints"] + expected_solution_exists = example["solution_exists"] + + out = brute_force_minimize(p, constraints) + actual_minima = set(out.argmin) + actual_value = out.value + + assert actual_value == expected_value + assert actual_minima == expected_minima + assert out.solution_exists == expected_solution_exists diff --git a/tests/unit/test_circuits.py b/tests/unit/test_circuits.py new file mode 100644 index 0000000..8d987a6 --- /dev/null +++ b/tests/unit/test_circuits.py @@ -0,0 +1,128 @@ +import os +from pprint import pprint + +import numpy as np +import pytest +import qcware +import quasar +from qcware.forge.circuits.quasar_backend import QuasarBackend +from qcware.forge.exceptions import ApiCallExecutionError + + +@pytest.mark.parametrize( + "backend,expected", + [ + ("qcware/cpu_simulator", True), + ("qcware/gpu_simulator", True), + ("ibm/simulator", True), + ("awsbraket/sv1", False), + ("awsbraket/tn1", False), + ], +) +def test_has_run_statevector(backend: str, expected: bool): + b = QuasarBackend(backend) + assert b.has_run_statevector() is expected + + +@pytest.mark.parametrize( + "backend,expected", + [ + ("qcware/cpu_simulator", True), + ("qcware/gpu_simulator", True), + ("ibm/simulator", True), + ("awsbraket/sv1", False), + ("awsbraket/tn1", False), + ], +) +def test_has_statevector_input(backend: str, expected: bool): + b = QuasarBackend(backend) + assert b.has_statevector_input() is expected + + +@pytest.mark.parametrize( + "backend", + [ + ("qcware/cpu_simulator"), + ("qcware/gpu_simulator"), + ("ibm/simulator"), + ("ibmq:ibmq_qasm_simulator"), + ("awsbraket/sv1"), + ("awsbraket/tn1"), + # ("awsbraket/rigetti_aspen_11") + # ("awsbraket/rigetti_aspen_m_1") + ], +) +def test_run_measurement(backend): + q = quasar.Circuit() + # We'll use a bell-pair with an additional NOT to try and + # flush out bit-ordering issues + q.H(0).CX(0, 1).X(2) + b = QuasarBackend(backend) + result = b.run_measurement(circuit=q, nmeasurement=100) + assert isinstance(result, quasar.ProbabilityHistogram) + assert isinstance(result.histogram, dict) + assert 1 in result.histogram + assert 7 in result.histogram + # yeah, pretty fuzzy but I'll take it; this is more or less a smoke test + assert abs(result.histogram[1] - 0.5) < 0.2 + assert abs(result.histogram[7] - 0.5) < 0.2 + + +@pytest.mark.parametrize( + "backend", + ( + ("awsbraket/ionq"), + ("awsbraket/rigetti_aspen_11"), + ("awsbraket/rigetti_aspen_m_1"), + ), +) +def test_smoke_backend_exception(backend): + """This is a 'smoke test' for having a NotImplementedError from a + backend. Accuracy doesn't matter here so long as the call gives a + NotImplementedError (since we call run_statevector on a backend without it) + """ + q = quasar.Circuit() + q.H(0).CX(0, 1) + b = QuasarBackend(backend) + try: + result = b.run_statevector(circuit=q) + except ApiCallExecutionError as e: + assert str(e) == "NotImplementedError: " + return + assert False + + +@pytest.mark.parametrize( + "backend", + ( + ("awsbraket/ionq"), + ("awsbraket/rigetti_aspen_11"), + ("awsbraket/rigetti_aspen_m_1"), + ), +) +def test_smoke_rescheduled_backends(backend): + """This is another 'smoke test' for the backends that can be rescheduled; they + need to either raise a rescheduled exception or run + """ + q = quasar.Circuit() + q.H(0).CX(0, 1) + b = QuasarBackend(backend) + result = b.run_measurement(circuit=q, nmeasurement=10) + + +@pytest.mark.parametrize( + "backend", + [ + ("qcware/cpu_simulator"), + ("qcware/gpu_simulator"), + # ("awsbraket/rigetti_aspen_11") + # ("awsbraket/rigetti_aspen_m_1") + ], +) +def test_run_statevector(backend): + q = quasar.Circuit() + q.H(0).CX(0, 1) + b = QuasarBackend(backend) + result = b.run_statevector(circuit=q) + val = complex(np.sqrt(2) / 2, 0) + assert np.allclose(result, [val, 0, 0, val]) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py new file mode 100644 index 0000000..505ec1e --- /dev/null +++ b/tests/unit/test_config.py @@ -0,0 +1,111 @@ +import os + +import pytest +from decouple import UndefinedValueError, config +from qcware.forge.config import ( + ConfigurationError, + SchedulingMode, + additional_config, + current_context, + pop_context, + push_context, + qcware_api_key, + qcware_host, + scheduling_mode, + set_api_key, + set_host, + set_scheduling_mode, + set_server_timeout, +) + + +@pytest.fixture(autouse=True) +def wrap_tests(): + old_key = os.environ.pop("QCWARE_API_KEY", None) + old_host = os.environ.pop("QCWARE_HOST", None) + + yield + + if old_key is not None: + os.environ["QCWARE_API_KEY"] = old_key + if old_host is not None: + os.environ["QCWARE_HOST"] = old_host + + +# these tests should be run with no configuration; this doesn't check +# for a config file at the moment +def test_undefined_config(): + with pytest.raises(UndefinedValueError): + config("QCWARE_API_KEY") + config("QCWARE_HOST") + + +def test_qcware_host(): + assert qcware_host() == "https://api.forge.qcware.com" + assert ( + qcware_host("https://api.hammer.qcware.com") == "https://api.hammer.qcware.com" + ) + + # test setting host via environment variable + os.environ["QCWARE_HOST"] = "https://api.anvil.qcware.com" + assert qcware_host() == "https://api.anvil.qcware.com" + + # test host resets to default when environment variable cleared + del os.environ["QCWARE_HOST"] + assert qcware_host() == "https://api.forge.qcware.com" + + # test for configuration errors on invalid urls + with pytest.raises(ConfigurationError): + qcware_host("api.forge.qcware.com") + with pytest.raises(ConfigurationError): + qcware_host("https://api.forge.qcware.com/") + + +def test_qcware_api_key(): + with pytest.raises(ConfigurationError): + assert qcware_api_key() is None + assert qcware_api_key("test") == "test" + + # test setting host via environment variable + os.environ["QCWARE_API_KEY"] = "test_key" + assert qcware_api_key() == "test_key" + + # test host resets to default (empty) when environment variable cleared + del os.environ["QCWARE_API_KEY"] + with pytest.raises(ConfigurationError): + assert qcware_api_key() == "bob" + + +def test_scheduling(): + with pytest.raises(ValueError): + assert set_scheduling_mode("potato") + + assert scheduling_mode() == SchedulingMode.immediate + + with additional_config(scheduling_mode="next_available"): + assert scheduling_mode() == SchedulingMode.next_available + + +def test_contexts(): + set_api_key("key") + set_server_timeout(42) + + assert current_context().server_timeout == 42 + + push_context(server_timeout=120) + assert current_context().server_timeout == 120 + + pop_context() + assert current_context().server_timeout == 42 + + +def test_additional_config(): + set_api_key("key") + set_server_timeout(42) + + assert current_context().server_timeout == 42 + + with additional_config(server_timeout=120): + assert current_context().server_timeout == 120 + + assert current_context().server_timeout == 42 diff --git a/tests/unit/test_distance_estimation.py b/tests/unit/test_distance_estimation.py new file mode 100644 index 0000000..f5a9727 --- /dev/null +++ b/tests/unit/test_distance_estimation.py @@ -0,0 +1,26 @@ +from qcware.forge.qutils import qdist +import numpy as np +import pytest +from qcware.forge.config import additional_config + +backends = ( + ("qcware/cpu_simulator", 100), + ("awsbraket/sv1", 100), + ("awsbraket/tn1", 100), + ("ibm/simulator", 100), + ("qcware/gpu_simulator", 100), +) + + +@pytest.mark.parametrize("backend, num_measurements", backends) +def test_qdist(backend, num_measurements): + # purely smoke test + x = np.random.rand(4) + y = np.random.rand(4) + x = x / np.linalg.norm(x) + y = y / np.linalg.norm(y) + with additional_config(client_timeout=5 * 60): + result = qdist(x, y, backend=backend, num_measurements=num_measurements) + distance = np.linalg.norm(x - y) ** 2 + # huge atol since this is mostly a smoke test + assert np.allclose(result, distance, atol=2) diff --git a/tests/unit/test_find_optimal_qaoa_angles.py b/tests/unit/test_find_optimal_qaoa_angles.py new file mode 100644 index 0000000..ca9e5f9 --- /dev/null +++ b/tests/unit/test_find_optimal_qaoa_angles.py @@ -0,0 +1,52 @@ +from qcware import forge +import networkx as nx +import numpy + + +def generate_rand_reg_p5(d, n): + """Generate a random d regular graph with n nodes with a P5 hamiltonian""" + G = nx.random_regular_graph(d, n, seed=999) + cost_dictionary = {} + for elm in list(G.nodes()): + cost_dictionary[ + elm, + ] = 1 + edges = [sorted(elm) for elm in list(G.edges())] + for elm in edges: + cost_dictionary[elm[0], elm[1]] = 1 + return cost_dictionary, G + + +def test_analytical_angle_determination(): + cost_dictionary, graph = generate_rand_reg_p5(2, 5) + n_linear = 100 + # Q = {(0, 0): 1, (1, 1): 1, (0, 1): -2, (2, 2): -2, (3, 3): -4, (3, 2): -5} + sol = forge.optimization.find_optimal_qaoa_angles( + cost_dictionary, + num_evals=n_linear, + num_min_vals=10, + fastmath_flag_in=True, + precision=30, + ) + assert ( + sol[0].sort() + == [ + -2.7542642560338755, + -2.754264256033875, + -2.7495290205139753, + -2.7495290205139744, + -0.7480763200529221, + -0.47430595681568066, + -0.47430595681568033, + -0.4193709671367115, + 0, + 0, + ].sort() + ) + assert sol[1][:4] == [ + [2.729060284936588, 0.28559933214452665], + [0.4125323686532052, 2.8559933214452666], + [2.729060284936588, 1.3010636242139548], + [0.4125323686532052, 1.8405290293758385], + ] + assert sol[2].shape == (n_linear, n_linear) diff --git a/tests/unit/test_fit_and_also_predict.py b/tests/unit/test_fit_and_also_predict.py new file mode 100644 index 0000000..7fa6f1f --- /dev/null +++ b/tests/unit/test_fit_and_also_predict.py @@ -0,0 +1,83 @@ +import pprint + +import numpy as np +import pytest +import qcware.forge +from qcware.forge.exceptions import ApiCallExecutionError +from qcware.forge.qml import ( + Classifier, + QMeans, + QNearestCentroid, + QNeighborsClassifier, + QNeighborsRegressor, + fit, + predict, +) + + +@pytest.mark.parametrize( + "backend", + [ + "qcware/cpu_simulator", + "qcware/gpu_simulator", + "ibm/simulator", + "ibmq:ibmq_qasm_simulator", + "awsbraket/sv1", + # "awsbraket/tn1", + ], +) +@pytest.mark.parametrize( + "classifier_class, params", + [ + (QNearestCentroid, dict(num_measurements=100)), + (QNeighborsRegressor, dict(num_measurements=100, n_neighbors=2)), + (QNeighborsClassifier, dict(num_measurements=100, n_neighbors=2)), + (QMeans, dict(n_clusters=2, num_measurements=100)), + ], +) +def test_fit_and_predict(backend: str, classifier_class, params): + X = np.array([[-1, -2], [-1, -1], [2, 1], [1, 2]]) + y = np.array([0, 0, 1, 1]) + try: + with qcware.forge.config.additional_config(client_timeout=8 * 60): + fit_data = fit( + X=X, + y=y, + model=classifier_class.__name__, + backend=backend, + parameters=params, + ) + + result = predict(X=X, fit_data=fit_data, backend=backend) + # instantiate the classifier with default args for now + classifier = classifier_class(**params) + classifier.fit(X=X, y=y) + print(classifier) + result2 = classifier.predict(X) + + except ApiCallExecutionError as e: + print(e) + print(type(e.traceback)) + raise (e) + # smoke test here in the client + assert set(result).issubset({0, 1}) + assert len(result) == 4 + # extra faff below from the fact that q-means is unsupervised so it could + # go either way on the labels + assert ( + np.array_equal(result, [0, 0, 1, 1]) + if classifier_class.__name__ != "QMeans" + else ( + np.array_equal(result, [0, 0, 1, 1]) or np.array_equal(result, [1, 1, 0, 0]) + ) + ) + + assert set(result2).issubset({0, 1}) + assert len(result2) == 4 + assert ( + np.array_equal(result2, [0, 0, 1, 1]) + if classifier_class.__name__ != "QMeans" + else ( + np.array_equal(result, [0, 0, 1, 1]) or np.array_equal(result, [1, 1, 0, 0]) + ) + ) diff --git a/tests/unit/test_fit_and_predict.py b/tests/unit/test_fit_and_predict.py new file mode 100644 index 0000000..676df8a --- /dev/null +++ b/tests/unit/test_fit_and_predict.py @@ -0,0 +1,40 @@ +import pprint + +import numpy as np +import pytest +import qcware.forge +from qcware.forge.qml import fit_and_predict +from qcware.forge.exceptions import ApiCallExecutionError + + +@pytest.mark.parametrize( + "backend", + [ + "qcware/cpu_simulator", + "qcware/gpu_simulator", + "ibm/simulator", + "ibmq:ibmq_qasm_simulator", + "awsbraket/sv1", + "awsbraket/tn1", + ], +) +def test_fit_and_predict(backend: str): + X = np.array([[-1, -2], [-1, -1], [2, 1], [1, 2]]) + y = np.array([0, 0, 1, 1]) + try: + with qcware.forge.config.additional_config(client_timeout=5 * 60): + result = fit_and_predict( + X=X, + y=y, + model="QNearestCentroid", + backend=backend, + parameters={"num_measurements": 1}, + ) + except ApiCallExecutionError as e: + print(e) + print(type(e.traceback)) + raise (e) + # smoke test here in the client + assert set(result).issubset({0, 1}) + assert len(result) == 4 + # assert (result == [0, 0, 1, 1]).all() diff --git a/tests/unit/test_loader.py b/tests/unit/test_loader.py new file mode 100644 index 0000000..7c42d2b --- /dev/null +++ b/tests/unit/test_loader.py @@ -0,0 +1,40 @@ +from qcware.forge.qio import loader +from qcware.forge.circuits.quasar_backend import QuasarBackend +import numpy as np +import pytest + + +def test_loader(): + x = np.random.rand(4) + x = x / np.linalg.norm(x) + + circ = loader(data=x, mode="optimized") + backend = QuasarBackend("qcware/cpu_simulator") + state = np.real(backend.run_statevector(circuit=circ)) + indices = [10, 9, 6, 5] + reduced_vec = state[indices] + eps = np.linalg.norm(np.abs(np.array(x) - np.abs(reduced_vec))) + assert eps <= 1e-2 + + +@pytest.mark.parametrize( + "kwparams", + [ + {"mode": "parallel"}, + {"mode": "optimized"}, + {"mode": "optimized", "opt_shape": (1, 4)}, + {"mode": "diagonal"}, + {"mode": "semi-diagonal"}, + {"mode": "semi-diagonal-middle"}, + ], +) +def test_loader_with_indices(kwparams): + x = np.random.rand(4) + x = x / np.linalg.norm(x) + backend = QuasarBackend("qcware/cpu_simulator") + + circ, indices = loader(data=x, **kwparams, return_statevector_indices=True) + state = np.real(backend.run_statevector(circuit=circ)) + reduced_vec = state[indices] + eps = np.linalg.norm(np.abs(np.array(x) - np.abs(reduced_vec))) + assert eps <= 1e-2 diff --git a/tests/unit/test_monte_carlo.py b/tests/unit/test_monte_carlo.py new file mode 100644 index 0000000..c2a19a5 --- /dev/null +++ b/tests/unit/test_monte_carlo.py @@ -0,0 +1,55 @@ +from qcware.forge.montecarlo.nisqAE import ( + make_schedule, + run_schedule, + run_unary, + compute_mle, +) +import numpy as np +import pytest +import quasar + +# these are currently the very barest of smoke tests! + + +def test_make_schedule(): + result = make_schedule(epsilon=0.1, schedule_type="powerlaw") + assert isinstance(result, list) + + +@pytest.mark.parametrize( + "backend", + ["qcware/cpu_simulator", "awsbraket/sv1", "ibm/simulator", "qcware/gpu_simulator"], +) +def test_run_schedule(backend): + circuit = quasar.Circuit().H(0).CX(0, 1) + schedule = make_schedule(epsilon=0.1, schedule_type="powerlaw") + result = run_schedule( + initial_circuit=circuit, + iteration_circuit=circuit, + target_qubits=[0], + target_states=[1], + schedule=schedule, + backend=backend, + ) + assert isinstance(result, list) + + +@pytest.mark.parametrize( + "backend", + ["qcware/cpu_simulator", "awsbraket/sv1", "ibm/simulator", "qcware/gpu_simulator"], +) +def test_run_unary(backend): + circuit = quasar.Circuit().H(0).CX(0, 1) + schedule = make_schedule(epsilon=0.1, schedule_type="powerlaw") + result = run_unary(circuit=circuit, schedule=schedule, backend=backend) + assert isinstance(result, list) + + +def test_compute_mle(): + circuit = quasar.Circuit().H(0).CX(0, 1) + schedule = make_schedule(epsilon=0.1, schedule_type="powerlaw") + counts = run_unary( + circuit=circuit, schedule=schedule, backend="qcware/cpu_simulator" + ) + result = compute_mle(counts, epsilon=0.1) + assert isinstance(result, float) diff --git a/tests/unit/test_pauli.py b/tests/unit/test_pauli.py new file mode 100644 index 0000000..2387439 --- /dev/null +++ b/tests/unit/test_pauli.py @@ -0,0 +1,56 @@ +import quasar +import time +import numpy as np +import qcware +from qcware.forge.circuits.quasar_backend import QuasarBackend +import pytest + + +def generate_circuit(N: int) -> quasar.Circuit: + gadget = quasar.Circuit().Ry(1).CZ(0, 1).Ry(1).CX(1, 0) + circuit = quasar.Circuit().X(0) + for I in range(N): + circuit.add_gates(circuit=gadget, qubits=(I, I + 1)) + + parameter_values = [] + for I in range(N): + value = 1.0 - I / 17.0 + parameter_values.append(+value) + parameter_values.append(-value) + circuit.set_parameter_values(parameter_values) + return circuit + + +def generate_pauli(N: int) -> quasar.Pauli: + I, X, Y, Z = quasar.Pauli.IXYZ() + pauli = quasar.Pauli.zero() + for k in range(N + 1): + pauli += (k + 1) / 10.0 * Z[k] + return pauli + + +@pytest.mark.parametrize( + "backend", + ( + "qcware/cpu_simulator", + "qcware/gpu_simulator" + # 'awsbraket/qs1') + ), +) +def test_pauli(backend): + N = 5 + circuit = generate_circuit(N) + pauli = generate_pauli(N) + vulcan_backend = QuasarBackend(backend) + data = vulcan_backend.run_pauli_expectation_value_gradient.data( + circuit=circuit, pauli=pauli, parameter_indices=[0, 1, 2, 3] + ) + result = vulcan_backend.run_pauli_expectation_value_gradient( + circuit=circuit, pauli=pauli, parameter_indices=[0, 1, 2, 3] + ) + assert np.isclose( + result, + np.array( + [0.68287656, -0.68287656, 0.37401749, -0.37401749], dtype=np.complex128 + ), + ).all() diff --git a/tests/unit/test_qaoa_expectation_value.py b/tests/unit/test_qaoa_expectation_value.py new file mode 100644 index 0000000..0bb0806 --- /dev/null +++ b/tests/unit/test_qaoa_expectation_value.py @@ -0,0 +1,104 @@ +from qcware.types.optimization import PolynomialObjective, BinaryProblem + +from qcware.forge.optimization import qaoa_expectation_value +from qcware.forge.api_calls import status, retrieve_result +import time +import pytest +import numpy as np + +Num_samples = 512 + +simulation_backends = ( + ("qcware/cpu", None), + ("qcware/gpu", None), + ("qcware/cpu_simulator", None), + ("qcware/gpu_simulator", None), + ("awsbraket/sv1", Num_samples), + ("ibm/simulator", Num_samples), +) + + +def pubo_example(num_variables: int = 4): + poly = { + (0,): -3, + (0, 1): 2, + (0, 2): np.random.randint(-5, 5), + (0, 3): 2, + (1,): -3, + (1, 2): 2, + (1, 3): 2, + (2,): np.random.randint(-5, 5), + (2, 3): 2, + (3,): -2, + (0, 1, 3): np.random.randint(-1, 1), + (): 7, + } + poly = PolynomialObjective( + polynomial=poly, + num_variables=num_variables, + domain=np.random.choice(["spin", "boolean"]), + ) + return BinaryProblem(objective=poly) + + +# This test is more thorough than it looks since the qcware/cpu algorithm +# is very different from the simulator backends. +@pytest.mark.parametrize(("backend", "num_samples"), simulation_backends) +def test_qaoa_expectation_value_against_cpu_emulate(backend, num_samples): + + instance = pubo_example() + qaoa_p = np.random.choice([1, 2]) + beta = np.random.random(qaoa_p) + gamma = np.random.random(qaoa_p) + + qcware_cpu_value = qaoa_expectation_value( + problem_instance=instance, beta=beta, gamma=gamma, backend="qcware/cpu" + ) + other_value = qaoa_expectation_value( + problem_instance=instance, + beta=beta, + gamma=gamma, + num_samples=num_samples, + backend=backend, + ) + assert np.isclose(other_value, qcware_cpu_value, rtol=0.2) + + +# This just checks to see if we can run with samples. +@pytest.mark.skip( + "Calling a backend with num_samples which can't handle them can result in an error (qcware/cpu)" +) +@pytest.mark.parametrize(("backend", "num_samples"), simulation_backends) +def test_qaoa_expectation_value_sampled(backend, num_samples): + instance = pubo_example() + qaoa_p = np.random.choice([1, 2]) + beta = np.random.random(qaoa_p) + gamma = np.random.random(qaoa_p) + + qaoa_expectation_value( + problem_instance=instance, + beta=beta, + gamma=gamma, + num_samples=128, + backend=backend, + ) + + +@pytest.mark.parametrize("backend", ["ibmq:ibmq_qasm_simulator"]) +def test_qaoa_expectation_value_ibmq(backend): + instance = pubo_example() + beta = np.array([5.284]) + gamma = np.array([-1.38]) + job_id = qaoa_expectation_value.submit( + problem_instance=instance, + beta=beta, + gamma=gamma, + num_samples=128, + backend=backend, + ) + job_status = status(job_id) + while job_status["status"] == "open": + time.sleep(0.5) + job_status = status(job_id) + + retrieve_result(job_id) diff --git a/tests/unit/test_qaoa_sample.py b/tests/unit/test_qaoa_sample.py new file mode 100644 index 0000000..b778e50 --- /dev/null +++ b/tests/unit/test_qaoa_sample.py @@ -0,0 +1 @@ +from qcware.forge.optimization import qaoa_sample diff --git a/tests/unit/test_qdot.py b/tests/unit/test_qdot.py new file mode 100644 index 0000000..ff06b1e --- /dev/null +++ b/tests/unit/test_qdot.py @@ -0,0 +1,85 @@ +import itertools +import time + +import numpy as np +import pytest +from qcware.forge.api_calls import retrieve_result, status +from qcware.forge.qutils import qdot + +# the tricky thing here for serialization is to make +# sure that the types come out right. For dot, +# we have +# scalar -> scalar -> scalar +# [m]->[m]->scalar +# m rows, n columns +# [mxn]->[n]->[m] +# [m]->[mxn]->[n] +# otherwise throw an exception + +# I really wanted to use hypothesis here, but the fact is it +# takes too long to send it over the wire, so we'll do case studies +# for each +backends = ( + ("qcware/cpu_simulator", 100), + ("awsbraket/sv1", 100), + ("awsbraket/tn1", 100), + ("ibm/simulator", 100), + ("qcware/gpu_simulator", 100), +) + +loader_modes = (("parallel",), ("optimized",)) + + +def flatten(x): + return list(itertools.chain.from_iterable(x)) + + +@pytest.mark.parametrize( + "x, y, backend, num_measurements, loader_mode", + ( + flatten(x) + for x in itertools.product( + ( + (np.array([5]), np.array([5])), + (np.array([[5, 4, 3], [2, 1, 0]]), np.array([8, 7, 6])), + ), + backends, + loader_modes, + ) + ), +) +def test_qdot(x, y, backend, num_measurements, loader_mode): + result = qdot( + x, + y, + backend=backend, + num_measurements=num_measurements, + loader_mode=loader_mode, + ) + numpy_result = np.dot(x, y) + if np.isscalar(numpy_result): + assert np.isscalar(result) + elif isinstance(numpy_result, np.ndarray): + assert isinstance(result, np.ndarray) and result.shape == numpy_result.shape + # big tolerance here since this is more or less a smoke test for the client + assert np.allclose(result, numpy_result, rtol=0.2) + + +@pytest.mark.parametrize("backend", ["ibmq:ibmq_qasm_simulator", "ibm/simulator"]) +def test_qdot_ibmq(backend): + """This is primarily a smoke test, and uses the .submit forms + because of the often long IBM queue times + """ + x = np.array([5, 4]) + y = np.array([3, 1]) + job_id = qdot.submit(x, y, backend=backend, num_measurements=1000) + + job_status = status(job_id) + while job_status["status"] == "open": + time.sleep(0.5) + job_status = status(job_id) + + result = retrieve_result(job_id) + numpy_result = np.dot(x, y) + + assert np.allclose(result, numpy_result, rtol=0.2) diff --git a/tests/unit/test_solve_binary.py b/tests/unit/test_solve_binary.py new file mode 100644 index 0000000..106d7cf --- /dev/null +++ b/tests/unit/test_solve_binary.py @@ -0,0 +1,89 @@ +import itertools +from itertools import product + +import pytest +import qcware.forge.optimization +from qcware.types.optimization import BinaryProblem + + +def sample_q(): + return {(0, 0): 1, (1, 1): 1, (0, 1): -2, (2, 2): -2, (3, 3): -4, (3, 2): -6} + + +def is_plausible_bitstring(bs, length): + return set(bs).issubset({0, 1}) and len(bs) == length + + +@pytest.mark.parametrize("backend", product(("qcware/cpu",), range(5))) # , +def test_optimize_binary(backend): + Q = sample_q() + problem_instance = BinaryProblem.from_dict(Q) + result = qcware.forge.optimization.optimize_binary( + instance=problem_instance, backend=backend[0] + ) + assert result.original_problem.objective.dict() == problem_instance.objective.dict() + result_bitstrings = {x.bitstring for x in result.samples} + # this is just a smoke test now + assert all([is_plausible_bitstring(bs, 4) for bs in result_bitstrings]) + # assert ((0, 0, 1, 1) in result_bitstrings) or ((1, 1, 1, 1) in result_bitstrings) + + +@pytest.mark.parametrize( + "backend,nmeasurement", + [ + ("qcware/cpu_simulator", None), + ("qcware/gpu_simulator", None), + ("awsbraket/sv1", 1000), + ], +) +def test_optimize_binary_qaoa(backend: str, nmeasurement: int): + Q = sample_q() + + result = qcware.forge.optimization.optimize_binary( + instance=BinaryProblem.from_dict(Q), + backend=backend, + qaoa_nmeasurement=nmeasurement, + qaoa_optimizer="analytical", + ) + result_bitstrings = {x.bitstring for x in result.samples} + assert all([is_plausible_bitstring(bs, 4) for bs in result_bitstrings]) + # assert ((0, 0, 1, 1) in result_bitstrings) or ((1, 1, 1, 1) in result_bitstrings) + + +@pytest.mark.parametrize( + "optimizer, backend", + itertools.product( + ("COBYLA", "bounded_Powell", "analytical"), + ("qcware/cpu_simulator", "qcware/gpu_simulator"), + ), +) +def test_various_qaoa_optimizers(optimizer, backend): + Q = sample_q() + result = qcware.forge.optimization.optimize_binary( + instance=BinaryProblem.from_dict(Q), backend=backend, qaoa_optimizer=optimizer + ) + result_bitstrings = {x.bitstring for x in result.samples} + assert all([is_plausible_bitstring(bs, 4) for bs in result_bitstrings]) + # assert ((0, 0, 1, 1) in result_bitstrings) or ((1, 1, 1, 1) in result_bitstrings) + + +@pytest.mark.parametrize("backend", ("qcware/cpu_simulator", "qcware/gpu_simulator")) +def test_analytical_angles_with_qaoa(backend): + Q = sample_q() + + exvals, angles, Z = qcware.forge.optimization.find_optimal_qaoa_angles( + Q, num_evals=100, num_min_vals=10 + ) + # print("EXVALS: ", exvals) + # print("ANGLES: ", angles) + + result = qcware.forge.optimization.optimize_binary( + instance=BinaryProblem.from_dict(Q), + backend="qcware/cpu_simulator", + qaoa_beta=angles[1][0], + qaoa_gamma=angles[1][1], + qaoa_p_val=1, + ) + result_bitstrings = {x.bitstring for x in result.samples} + assert all([is_plausible_bitstring(bs, 4) for bs in result_bitstrings]) + # assert ((0, 0, 1, 1) in result_bitstrings) or ((1, 1, 1, 1) in result_bitstrings) diff --git a/tests/unit/test_timeouts.py b/tests/unit/test_timeouts.py new file mode 100644 index 0000000..bff80dd --- /dev/null +++ b/tests/unit/test_timeouts.py @@ -0,0 +1,80 @@ +import time +import pytest +import qubovert as qv +from qcware.types.optimization import BinaryProblem +from qcware.types.optimization import PolynomialObjective +from qcware import forge + + +def generate_problem(): + Q = {(0, 0): 1, (1, 1): 1, (0, 1): -2, (2, 2): -2, (3, 3): -4, (3, 2): -6} + + qubo = PolynomialObjective(polynomial=Q, num_variables=4, domain="boolean") + problem = BinaryProblem(objective=qubo) + return problem + + +def test_timeout_with_solve_binary(): + old_timeout = forge.config.client_timeout() + forge.config.set_client_timeout(0) + old_server_timeout = forge.config.server_timeout() + forge.config.set_server_timeout(0) + + with pytest.raises(forge.exceptions.ApiTimeoutError): + sol = forge.optimization.find_optimal_qaoa_angles( + generate_problem().objective, + num_evals=100, + num_min_vals=10, + fastmath_flag_in=True, + precision=30, + ) + print(sol) + + forge.config.set_client_timeout(old_timeout) + forge.config.set_server_timeout(old_server_timeout) + + +def test_retrieve_result_with_timeout(): + old_timeout = forge.config.client_timeout() + forge.config.set_client_timeout(0) + old_server_timeout = forge.config.server_timeout() + forge.config.set_server_timeout(0) + + try: + result = forge.optimization.optimize_binary( + instance=generate_problem(), backend="qcware/cpu" + ) + except forge.exceptions.ApiTimeoutError as e: + call_id = e.api_call_info["uid"] + start = time.perf_counter() + # 30s timeouts + timeout = 60 + while ((time.perf_counter() - start) < timeout) and forge.api_calls.status( + call_id + )["status"] == "open": + time.sleep(1) + assert forge.api_calls.status(call_id)["status"] == "success" + result = forge.api_calls.retrieve_result(call_id) + result_vectors = {x.bitstring for x in result.samples} + assert (0, 0, 1, 1) in result_vectors + assert (1, 1, 1, 1) in result_vectors + forge.config.set_client_timeout(old_timeout) + forge.config.set_server_timeout(old_server_timeout) + + +@pytest.mark.asyncio +async def test_async(): + old_timeout = forge.config.client_timeout() + forge.config.set_client_timeout(0) + old_server_timeout = forge.config.server_timeout() + forge.config.set_server_timeout(0) + + result = await forge.optimization.optimize_binary.call_async( + instance=generate_problem(), backend="qcware/cpu" + ) + result_vectors = {x.bitstring for x in result.samples} + assert (0, 0, 1, 1) in result_vectors + assert (1, 1, 1, 1) in result_vectors + + forge.config.set_client_timeout(old_timeout) + forge.config.set_server_timeout(old_server_timeout) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..102e6ef --- /dev/null +++ b/tox.ini @@ -0,0 +1,12 @@ +[tox] +skipsdist = true +envlist = py37, py38, py39, py310 + +[testenv] +setenv = + QCWARE_HOST=http://localhost:5454 + QCWARE_API_KEY=QCWARE +whitelist_externals = poetry +commands = + poetry install -v + poetry run pytest -k cpu