From f3b1e06b13d8d2e57c9533f88ab0af887c98061e Mon Sep 17 00:00:00 2001 From: Daniel Strano Date: Sat, 18 Apr 2020 22:09:59 -0400 Subject: [PATCH] Merge from upstream (#15) * Update to newer RevKit version. (#271) * Add VQE example (#274) * Update docs of decompositions. (#281) * Add FlipBits gate (#289) * Fix strings with invalid escape sequences. (#300) * Avoid 502 error when waiting for IBM Q results, resolves #291 (#294) * Expose num_retries and interval in IBMBackend (#295) * Don't accept controlled single-qubit gates in restricted gate set. (#301) * Implement MatrixGate and simplify __eq__ in BasicGate to improve performance (#288) * Bumped version number to 0.4.2 * Phase Estimation as a Gate in ops (#260) * First testing version of Phase Estimation with a lot of hardcoding * Adapt to All(Measure) * Adding operators for more than 1 quibit, first version * Adding operators for more than 1 quibit, first versioni: testing * Work in progress: create a PhaseX gate to tests via class * Work in progress: create a PhaseX gate to tests via class. Clean garbaje files * Work in progress: create a PhaseX gate to tests via class. Some enhanement * Work in progress: create a PhaseX gate to tests via class. PhaseX testing * Work in progress: Debugging algorithm * Work in progress: Debugging algorithm * Adding 2qubit example * adding 2 qubit Gate * Initial version * Create Phase Estimation as a new Gate in operations * Solving travis checks * python 2 compatibility + error in StatePreparation normalization * test coverage includes now the string * Improve the check test for no eigenvector test * Start modifying to decomposition * QPE as decomposition and gate * QPE as decomposition and gate: correct a detail in the test * try to get the travis-ci freeze solved * Solve a name not defined error in the phaseestimation tests * Solve coverage in tests * Address comments in review + change how to assert the tests * Enhance statistis in the tests bi more executions * Correct bad calculation in tests * Refine test * Address Andi comments: add detail in the examples and atributes and removing code in the test that is never executed * Correct statistics in qpe test (#328) * Amplitude Amplification algorithm as a Gate in ops * Amplitude Amplification algorithm as a Gate in ops, correct test_string_functions * Amplitude Amplification algorithm as a Gate in ops, correct coverage for _qaagate_test * Amplitude Amplification algorithm as a Gate in ops, correct test estimation statistics in phaseestimation_test * Try to triger Travis test because an apt get failure in the travis test * resend docs/projectq.ops.rst file * resend file versions previous to AA * Try to triger Travis test because it never ran * Try to triger Travis test again to try to get the tests ran * Amplitude Amplification algorithm as a Gate in ops (#327) * Amplitude Amplification algorithm as a Gate in ops * Amplitude Amplification algorithm as a Gate in ops, correct test_string_functions * Amplitude Amplification algorithm as a Gate in ops, correct coverage for _qaagate_test * Amplitude Amplification algorithm as a Gate in ops, correct test estimation statistics in phaseestimation_test * Try to triger Travis test because an apt get failure in the travis test * Address changes proposed by Damien * Address changes proposed by Damien, missed one * Address comments by Damien including eliminate the usage of algorith_inverse and eliminate QPE files form the PR * Address comments by Damien including eliminate the usage of algorith_inverse and eliminate QPE files form the PR, second try * Address comments by Damien forgot _qaagate_test * Update amplitudeamplification_test.py Wrap lines to 80 characters * Update amplitudeamplification.py Wrap lines to 80 characters * Update _qaagate.py Minor style correction * Update amplitudeamplification_test.py Cleanup imports * 3 additional 2-qubit gates (#330) * Modified _gates.py: Documentation, 2-qubit rotations, 1qubit-rotation string attributes. * Strings of rotation gates fixed. * Added two-qubit rotation gate tests. * Resource Counter import Rzz added. * Added Rzz test and fixed expected outcome. * removed wrongfully pushed dev gates. * Update _gates.py Remove unneeded import * Removed hardcoded "Phase" name for Ph(angle) gate * C++ simulator performance improvements (#329) * C++ simulator performance: make the swap-gate run in native C++ It was defined as a BasicMathGate before which made it run as python code through the emulate_math_wrapper. The new variant just uses its matrix representation to run it in native code. * C++ simulator performance: add dedicated C++ code for common math gates The BasicMathGate uses a C++ python wrapper (emulate_math_wrapper) to allow generic calculations which makes it very slow. This detects some math gates and provides a native C++ implementation for it. * C++ simulator performance: use larger memory alignment * C++ simulator performance: recycle large StateVector memory buffers This avoids costly std::vector copying/reallocations by using some static std::vector to reuse the allocated buffer (just by std::swap'ing a vector into a buffer for later use when it would be deallocated otherwise). * C++ simulator performance: improve compiler flags * Add test coverage for constant math emulation * Revert "Add test coverage for constant math emulation" This reverts commit 3bb8a2cc7fd595db48b0f4d260124ccfe60d7fcf. * Add test coverage for constant math emulation * Update constant math documentation to include list of pre-conditions (#331) * Allow selection of drawing order in CircuitDrawer (solves #333) (#334) * included the possibility to draw the gates in the order they were added to the circuit * edited param names tests should pass now * solved comments * solved comments * passes tests * Test for unordered and ordered circuit drawing * Reindent files in _circuits * Minor adjustments to _drawer.py * added test_body_with_drawing_order * fixed tests and how draw_gates_in_parallel is interpreted * Fix failing tests with Python 2.7 One test of _to_latex_test.py was failing due to precision issues. * Trapped ion decomposer setup and rotation gates improvement (#346) * reduced Ry decomposition : modification of restrictedgateset setup to include compiler chooser, extension of available decompositions, new setup based on restrictedgateset but adapted for trapped ion configurations * Addition of test file for the rz2rx decomposition Addition of test file for the rz2rx decomposition and edit to comments in the rz2rx file * Revert "Addition of test file for the rz2rx decomposition" This reverts commit 5aab56b7384d60d5ac09e5b08bebf14135b5a006. * Create rz2rx_test file Addition of test file for the rz2rx decomposition * Update rz2rx.py Update to comments * Update rz2rx_test.py Update to comments * Minor update: remove accidental file * Minor update rz2rx.py Corrected an angle. * Minor update rz2rx_test.py Edited comments. * Create h2rx_test.py Updated method for test_decomposition which tests that decomposition produces identical wave-state up to a global phase. * Improvement of the decomposition chooser; Note that basic restricted gate set will fail decomposing into Rxx because of the default chooser * Updates to h2rx and cnot2rxx * Create cnot2rxx_test.py Testing file for cnot2rxx decomposition. Includes updated method for test_decomposition which tests that decomposition produces identical wave-state up to a global phase. * basic rotation gates at an angle of 0 or 2pi are removed by the optimizer. basic rotation gates ignore now the global phase and are defined over 0:2pi * Update and create trapped_ion_decomposer_test.py * Minor update Documentation and comments. * Update on comments regarding Identity gates * Changes 1/2 : command can be printed with unicode symbols only via new to_String method; syntax correction; * Work in progress, is commutable * Update to decomposition test files rz2rx_test now tests each decomposition defined in rz2rx.all_defined_decomposition_rules Similar for cnot2rxx_test and h2rx_test * Revert "Work in progress, is commutable" This reverts commit 27f820cac337c81dbc037b5e3e0381b7e31db5b3. * minor fixes; revert rotation gates to [0;4pi) * fix comments * Fix a few issues with the trapped-ion setup - Store the sign of the last Ry gate on an engine-by-engine basis - Cleanup of some remaining print statements - Some stylistic issues fixed * Mostly fixing stylistic issues and some code cleanup * h2rx decomposition with correct global phase * cnot2rxx decomposition with correct global phase * Fix non-ASCII character in cnot2rxx.py * Fix some more files for non-ASCII characters * Specify encoding for files with non-ASCII characters * Fix test errors * Fix tests for Python 2.7 * Complete code coverage for trapped-ion setup Co-authored-by: Nguyen Damien * Matplotlib drawer backend (#352) * Adding tutorials directory * test * BV Algorithm * add Matplotlib circuit drawer backend, this version works for H, CNOT, and Multi-CNOT. * Delete the added unnecessary attributes in Command object * Create the CircuitDrawerMatplotlib Class to handles drawing with matplotlib * Deleted tutorials/.gitkeep * update * fix measurement gate * Delete unrelated files. * fix Toffoli gate position issue and change the qubit position from 'str' to 'int' * Pytest for drawer_mpl * Tests for _plot function * Fix the R(angle) gate drawing * added test for is_available and QFT gate * fix drawing distance between gates when gate_length >2 * new test png for pytest mpl * added Swap gates and CSwap gate with multi-control and multi-target. * update test and comments * Address comments in _drawer.py * Reindent and reformat parts of _drawer.py * Address comments in _plot.py - Minor tweaks, code cleanup, rewrites, etc. * Reindent and reformat _plot.py * update tests * Move matplotlib drawer into its own file + add test coverage * Use regular expressions to rewrite and shorten gate names * Change internal storage format for CircuitDrawerMatplotlib * Better graphics and adapt plot functions to new internal format - Support for new internal format - Resulting quantum circuit figure whould work better with scaling - Large quantum circuits will now result in wider figure instead of squeezing everything into the default matplotlib size - Some support for multi-target qubit gates - General code cleanup - Dropped support for double lines when qubit is in classical state * Complete test coverage + add some checks for to_draw() inputs * Compatibility with matplotlib 2.2.3 * Remove compatibility code for MacOSX. Use local matplotlibrc if necessary instead. * Add matplotlib dependency to requirements.txt * Fix non-UTF8 character in file * Fix .travis.yml * Remove unnecessary PNG files * Add CircuitDrawerMatplotlib to documentation and minor code fix * Fix docstring for CircuitDrawerMatplotlib Co-authored-by: Nguyen Damien * Ibm backend v2 (solves #318 and #347) (#349) * reduced Ry decomposition : modification of restrictedgateset setup to include compiler chooser, extension of available decompositions, new setup based on restrictedgateset but adapted for trapped ion configurations * Addition of test file for the rz2rx decomposition Addition of test file for the rz2rx decomposition and edit to comments in the rz2rx file * Revert "Addition of test file for the rz2rx decomposition" This reverts commit 5aab56b7384d60d5ac09e5b08bebf14135b5a006. * Create rz2rx_test file Addition of test file for the rz2rx decomposition * Update rz2rx.py Update to comments * Update rz2rx_test.py Update to comments * Minor update: remove accidental file * Minor update rz2rx.py Corrected an angle. * Minor update rz2rx_test.py Edited comments. * Create h2rx_test.py Updated method for test_decomposition which tests that decomposition produces identical wave-state up to a global phase. * Improvement of the decomposition chooser; Note that basic restricted gate set will fail decomposing into Rxx because of the default chooser * Updates to h2rx and cnot2rxx * Create cnot2rxx_test.py Testing file for cnot2rxx decomposition. Includes updated method for test_decomposition which tests that decomposition produces identical wave-state up to a global phase. * basic rotation gates at an angle of 0 or 2pi are removed by the optimizer. basic rotation gates ignore now the global phase and are defined over 0:2pi * Update and create trapped_ion_decomposer_test.py * Minor update Documentation and comments. * Update on comments regarding Identity gates * Changes 1/2 : command can be printed with unicode symbols only via new to_String method; syntax correction; * Work in progress, is commutable * Update to decomposition test files rz2rx_test now tests each decomposition defined in rz2rx.all_defined_decomposition_rules Similar for cnot2rxx_test and h2rx_test * Revert "Work in progress, is commutable" This reverts commit 27f820cac337c81dbc037b5e3e0381b7e31db5b3. * ibmq fix: projectq uses new ibmq API (no doc available, just look at qiskit). Connection fixed for 5qb devices, the 15qb melbourne device and the online simulator coupling map obtained from ibm server instead of being manually written one setup can be used for the 3 different backend * minor fixes * update on the ibm example * minor fixes; revert rotation gates to [0;4pi) * fix comments * fix mapper choice for simulator. added comments * minor fixes with results, mapper and testing files. Improvement of get_probabilities * Revert "Merge branch 'develop' into ibm_V2" This reverts commit cd0452a5f56d8d7fc95dc17f6dc5d4970f3ad130, reversing changes made to 03daf7915ce663f8dc79975ba87dabb4534272e6. * minor fixes * bug fix in client test file * fixing bug and python2.7 compatibility for testing files * fix errors * major fix on testing files * minor fix on comments and python 2.7 compatibility * fix 'super()' call for python 2.7 * additional tests * python2.7 fix * increase coverage, fix a print statement * Some minor stylistic adjustments * Reindent files and fix some linting warnings/errors * Improve test coverage * Improve code readability Co-authored-by: Nguyen Damien * Automatically generate documentation ReST files (#339) * Automate generation of documentation ReST files * Fix error in conf.py * Adjust .gitignore * Update setup.py (#337) * Rewrite setup.py to fix some issues on Mac OSX and some code cleanup - On Mac OSX with Homebrew, properly find the path to the OpenMP library if compiling with clang from Homebrew - Code cleanup and some reformating * Remove use of deprecated setuptools.Feature Now try to compile the C++ simulator and if it fails, simply install a pure Python package with some warning messages for the user. * Update documentation to reflect the latest change to setup.py * Fix error with deleted method of BuildExt * Remove global COPYING file and move text to setup.py file itself * Fix compilation issues on MacOS X * Update .gitignore * Fix setup.py for Python2 and MacPorts (Mac OS) * Fix setup.py on Windows * Some more fixes * Attempt to fix failing installation on Travis CI * Fix installation under Linux * Undo changes in .travis.yml * Update setup related tutorials * Fix a few remaining typos. * ProjectQ v0.5.0 (#356) * Bumped version number to 0.5.0 * Remove unneeded test * Fix error in examples/ibm.py * Add documentation for **kwargs for CircuitDrawerMatplotlib * Update setup.py license header Co-authored-by: Damian Steiger * Fix generated documentation (#360) * Fix some issues with builtin modules and fix some warnings * Move generated documentation files into dedicated folder Co-authored-by: Damien Nguyen * Fix bugs with matplotlib drawer (#361) * Accept keywords arguments for Matplotlib drawing * fix circ drawer when depth == 1 Co-authored-by: Damien Nguyen Co-authored-by: Damian Steiger Co-authored-by: Mathias Soeken Co-authored-by: Christian Gogolin Co-authored-by: Thomas Haener Co-authored-by: Damian S. Steiger Co-authored-by: Fernando Co-authored-by: David Wierichs Co-authored-by: Melven Roehrig-Zoellner Co-authored-by: Nguyen Damien Co-authored-by: alexandrupaler Co-authored-by: David Bretaud <40793394+dbretaud@users.noreply.github.com> Co-authored-by: Cheng Li Co-authored-by: Nguyen Damien Co-authored-by: Ari Jordan <56979766+AriJordan@users.noreply.github.com> --- .gitignore | 182 ++++- .travis.yml | 3 + docs/conf.py | 170 ++++- docs/package_description.py | 162 +++++ docs/projectq.backends.rst | 20 - docs/projectq.cengines.rst | 33 - docs/projectq.libs.math.rst | 21 - docs/projectq.libs.revkit.rst | 34 - docs/projectq.libs.rst | 20 - docs/projectq.meta.rst | 32 - docs/projectq.ops.rst | 63 -- docs/projectq.rst | 14 +- docs/projectq.setups.decompositions.rst | 142 ---- docs/projectq.setups.rst | 94 --- docs/projectq.types.rst | 18 - docs/tutorials.rst | 119 ++-- examples/ibm.py | 20 +- .../variational_quantum_eigensolver.ipynb | 197 ++++++ projectq/_version.py | 2 +- projectq/backends/__init__.py | 2 +- projectq/backends/_circuits/__init__.py | 4 + projectq/backends/_circuits/_drawer.py | 40 +- .../backends/_circuits/_drawer_matplotlib.py | 232 +++++++ .../_circuits/_drawer_matplotlib_test.py | 148 ++++ projectq/backends/_circuits/_drawer_test.py | 15 +- projectq/backends/_circuits/_plot.py | 630 ++++++++++++++++++ projectq/backends/_circuits/_plot_test.py | 289 ++++++++ projectq/backends/_circuits/_to_latex.py | 299 ++++++--- projectq/backends/_circuits/_to_latex_test.py | 186 +++++- projectq/backends/_ibm/_ibm.py | 128 ++-- projectq/backends/_ibm/_ibm_http_client.py | 415 +++++++++--- .../backends/_ibm/_ibm_http_client_test.py | 533 ++++++++++----- projectq/backends/_ibm/_ibm_test.py | 306 ++++++--- projectq/backends/_resource_test.py | 5 +- .../backends/_sim/_cppkernels/simulator.hpp | 154 +++-- projectq/backends/_sim/_cppsim.cpp | 3 + projectq/backends/_sim/_simulator.py | 28 +- projectq/backends/_sim/_simulator_test.py | 105 ++- projectq/cengines/_basicmapper.py | 4 + projectq/cengines/_ibm5qubitmapper.py | 59 +- projectq/cengines/_ibm5qubitmapper_test.py | 51 +- projectq/cengines/_optimize.py | 16 +- projectq/cengines/_optimize_test.py | 20 + projectq/cengines/_replacer/_replacer.py | 1 - projectq/libs/math/_gates.py | 25 + projectq/libs/revkit/_control_function.py | 2 +- projectq/libs/revkit/_permutation.py | 2 +- projectq/libs/revkit/_phase.py | 2 +- projectq/meta/_loop.py | 1 + projectq/ops/__init__.py | 4 + projectq/ops/_basics.py | 157 +++-- projectq/ops/_basics_test.py | 107 ++- projectq/ops/_command.py | 70 +- projectq/ops/_command_test.py | 50 +- projectq/ops/_gates.py | 106 ++- projectq/ops/_gates_test.py | 118 +++- projectq/ops/_metagates.py | 20 +- projectq/ops/_metagates_test.py | 16 +- projectq/ops/_qaagate.py | 81 +++ projectq/ops/_qaagate_test.py | 27 + projectq/ops/_qpegate.py | 29 + projectq/ops/_qpegate_test.py | 23 + projectq/ops/_qubit_operator.py | 4 +- projectq/ops/_state_prep.py | 6 +- projectq/setups/decompositions/__init__.py | 14 +- .../decompositions/amplitudeamplification.py | 111 +++ .../amplitudeamplification_test.py | 182 +++++ .../decompositions/arb1qubit2rzandry_test.py | 10 +- .../carb1qubit2cnotrzandry_test.py | 8 +- projectq/setups/decompositions/cnot2rxx.py | 61 ++ .../setups/decompositions/cnot2rxx_test.py | 124 ++++ projectq/setups/decompositions/h2rx.py | 57 ++ projectq/setups/decompositions/h2rx_test.py | 117 ++++ .../setups/decompositions/phaseestimation.py | 129 ++++ .../decompositions/phaseestimation_test.py | 162 +++++ .../setups/decompositions/qubitop2onequbit.py | 4 + projectq/setups/decompositions/rz2rx.py | 67 ++ projectq/setups/decompositions/rz2rx_test.py | 125 ++++ projectq/setups/ibm.py | 142 +++- projectq/setups/ibm_test.py | 65 +- projectq/setups/restrictedgateset.py | 124 ++-- projectq/setups/restrictedgateset_test.py | 45 +- projectq/setups/trapped_ion_decomposer.py | 148 ++++ .../setups/trapped_ion_decomposer_test.py | 150 +++++ requirements.txt | 1 + setup.py | 548 +++++++++++---- 86 files changed, 6668 insertions(+), 1595 deletions(-) create mode 100644 docs/package_description.py delete mode 100755 docs/projectq.backends.rst delete mode 100755 docs/projectq.cengines.rst delete mode 100755 docs/projectq.libs.math.rst delete mode 100644 docs/projectq.libs.revkit.rst delete mode 100755 docs/projectq.libs.rst delete mode 100755 docs/projectq.meta.rst delete mode 100755 docs/projectq.ops.rst delete mode 100755 docs/projectq.setups.decompositions.rst delete mode 100755 docs/projectq.setups.rst delete mode 100755 docs/projectq.types.rst create mode 100644 examples/variational_quantum_eigensolver.ipynb create mode 100644 projectq/backends/_circuits/_drawer_matplotlib.py create mode 100644 projectq/backends/_circuits/_drawer_matplotlib_test.py create mode 100644 projectq/backends/_circuits/_plot.py create mode 100644 projectq/backends/_circuits/_plot_test.py mode change 100755 => 100644 projectq/ops/_metagates.py create mode 100755 projectq/ops/_qaagate.py create mode 100755 projectq/ops/_qaagate_test.py create mode 100755 projectq/ops/_qpegate.py create mode 100755 projectq/ops/_qpegate_test.py create mode 100644 projectq/setups/decompositions/amplitudeamplification.py create mode 100644 projectq/setups/decompositions/amplitudeamplification_test.py create mode 100644 projectq/setups/decompositions/cnot2rxx.py create mode 100644 projectq/setups/decompositions/cnot2rxx_test.py create mode 100644 projectq/setups/decompositions/h2rx.py create mode 100644 projectq/setups/decompositions/h2rx_test.py create mode 100644 projectq/setups/decompositions/phaseestimation.py create mode 100644 projectq/setups/decompositions/phaseestimation_test.py create mode 100644 projectq/setups/decompositions/rz2rx.py create mode 100644 projectq/setups/decompositions/rz2rx_test.py create mode 100644 projectq/setups/trapped_ion_decomposer.py create mode 100644 projectq/setups/trapped_ion_decomposer_test.py diff --git a/.gitignore b/.gitignore index c3957d2e0..24d243a5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,180 @@ -# python artifacts -*.pyc +# Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +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 +*.py,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/ +docs/_doc_gen/ +docs/doxygen + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .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 + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# ============================================================================== +# Prerequisites +*.d + +# C++ +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# ============================================================================== + +# Windows artifacts +thumbs.db + +# Mac OSX artifacts +*.DS_Store \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index fbe009436..47f727e2a 100755 --- a/.travis.yml +++ b/.travis.yml @@ -42,6 +42,9 @@ install: - if [ "${PYTHON:0:1}" = "3" ]; then pip$PY install dormouse; fi - pip$PY install -e . +before_script: + - "echo 'backend: Agg' > matplotlibrc" + # command to run tests script: export OMP_NUM_THREADS=1 && pytest projectq --cov projectq diff --git a/docs/conf.py b/docs/conf.py index 971cefd8d..169414e6c 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,6 +21,18 @@ import sys sys.path.insert(0, os.path.abspath('..')) +import projectq +# Also import all the modules that are not automatically imported +import projectq.libs.math +import projectq.libs.revkit +import projectq.setups.default +import projectq.setups.grid +import projectq.setups.ibm +import projectq.setups.ibm16 +import projectq.setups.linear +import projectq.setups.restrictedgateset +import projectq.setups.decompositions + # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. @@ -33,8 +45,11 @@ import sphinx_rtd_theme extensions = [ - 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.mathjax', - 'sphinx.ext.autosummary', 'sphinx.ext.linkcode', + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'sphinx.ext.mathjax', + 'sphinx.ext.autosummary', + 'sphinx.ext.linkcode', ] autosummary_generate = True @@ -125,7 +140,6 @@ # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False - # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for @@ -271,8 +285,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'projectq.tex', 'projectq Documentation', - 'a', 'manual'), + (master_doc, 'projectq.tex', 'projectq Documentation', 'a', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -307,30 +320,24 @@ # # latex_domain_indices = True - # -- 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, 'projectq', 'projectq Documentation', - [author], 1) -] +man_pages = [(master_doc, 'projectq', 'projectq Documentation', [author], 1)] # If true, show URL addresses after external links. # # man_show_urls = False - # -- 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, 'projectq', 'projectq Documentation', - author, 'projectq', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'projectq', 'projectq Documentation', author, 'projectq', + 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. @@ -351,7 +358,6 @@ # -- Options for sphinx.ext.linkcode -------------------------------------- import inspect -import projectq def linkcode_resolve(domain, info): @@ -381,7 +387,11 @@ def linkcode_resolve(domain, info): return None else: try: - obj = eval(info['module'] + '.' + info['fullname']) + if ('module' in info and 'fullname' in info + and info['module'] and info['fullname']): + obj = eval(info['module'] + '.' + info['fullname']) + else: + return None except AttributeError: # Object might be a non-static attribute of a class, e.g., # self.num_qubits, which would only exist after init was called. @@ -400,8 +410,8 @@ def linkcode_resolve(domain, info): if len(new_higher_name) <= 1: obj = eval(info['module']) else: - obj = eval(info['module'] + '.' + - '.'.join(new_higher_name[:-1])) + obj = eval(info['module'] + '.' + + '.'.join(new_higher_name[:-1])) filepath = inspect.getsourcefile(obj) line_number = inspect.getsourcelines(obj)[1] except: @@ -409,6 +419,126 @@ def linkcode_resolve(domain, info): # Only require relative path projectq/relative_path projectq_path = inspect.getsourcefile(projectq)[:-11] relative_path = os.path.relpath(filepath, projectq_path) - url = (github_url + github_tag + "/projectq/" + relative_path + "#L" + - str(line_number)) + url = (github_url + github_tag + "/projectq/" + relative_path + "#L" + + str(line_number)) return url + + +# ------------------------------------------------------------------------------ + +import importlib +sys.path.append(os.path.abspath('.')) +desc = importlib.import_module('package_description') + +PackageDescription = desc.PackageDescription + +# ------------------------------------------------------------------------------ +# Define the description of ProjectQ packages and their submodules below. +# +# In order for the automatic package recognition to work properly, it is +# important that PackageDescription of sub-packages appear earlier in the list +# than their parent package (see for example libs.math and libs.revkit +# compared to libs). +# +# It is also possible to customize the presentation of submodules (see for +# example the setups and setups.decompositions) or even to have private +# sub-modules listed in the documentation page of a parent packages (see for +# example the cengines package) + +descriptions = [ + PackageDescription('backends'), + PackageDescription('cengines', + desc=''' +The ProjectQ compiler engines package. +'''), + PackageDescription('libs.math', + desc=''' +A tiny math library which will be extended thoughout the next weeks. Right now, it only contains the math functions necessary to run Beauregard's implementation of Shor's algorithm. +'''), + PackageDescription('libs.revkit', + desc=''' +This library integrates `RevKit `_ into +ProjectQ to allow some automatic synthesis routines for reversible logic. The +library adds the following operations that can be used to construct quantum +circuits: + +- :class:`~projectq.libs.revkit.ControlFunctionOracle`: Synthesizes a reversible circuit from Boolean control function +- :class:`~projectq.libs.revkit.PermutationOracle`: Synthesizes a reversible circuit for a permutation +- :class:`~projectq.libs.revkit.PhaseOracle`: Synthesizes phase circuit from an arbitrary Boolean function + +RevKit can be installed from PyPi with `pip install revkit`. + +.. note:: + + The RevKit Python module must be installed in order to use this ProjectQ library. + + There exist precompiled binaries in PyPi, as well as a source distribution. + Note that a C++ compiler with C++17 support is required to build the RevKit + python module from source. Examples for compatible compilers are Clang + 6.0, GCC 7.3, and GCC 8.1. + +The integration of RevKit into ProjectQ and other quantum programming languages is described in the paper + + * Mathias Soeken, Thomas Haener, and Martin Roetteler "Programming Quantum Computers Using Design Automation," in: Design Automation and Test in Europe (2018) [`arXiv:1803.01022 `_] +''', + module_special_members='__init__,__or__'), + PackageDescription('libs', + desc=''' +The library collection of ProjectQ which, for now, consists of a tiny math library and an interface library to RevKit. Soon, more libraries will be added. +'''), + PackageDescription('meta', + desc=''' +Contains meta statements which allow more optimal code while making it easier for users to write their code. +Examples are `with Compute`, followed by an automatic uncompute or `with Control`, which allows the user to condition an entire code block upon the state of a qubit. +'''), + PackageDescription('ops', + desc=''' +The operations collection consists of various default gates and is a work-in-progress, as users start to work with ProjectQ. +''', + module_special_members='__init__,__or__'), + PackageDescription('setups.decompositions', + desc=''' +The decomposition package is a collection of gate decomposition / replacement rules which can be used by, e.g., the AutoReplacer engine. +'''), + PackageDescription('setups', + desc=''' +The setups package contains a collection of setups which can be loaded by the `MainEngine`. Each setup contains a `get_engine_list` function which returns a list of compiler engines: + +Example: + .. code-block:: python + + import projectq.setups.ibm as ibm_setup + from projectq import MainEngine + eng = MainEngine(engine_list=ibm_setup.get_engine_list()) + # eng uses the default Simulator backend + +The subpackage decompositions contains all the individual decomposition rules +which can be given to, e.g., an `AutoReplacer`. +''', + submodules_desc=''' +Each of the submodules contains a setup which can be used to specify the +`engine_list` used by the `MainEngine` :''', + submodule_special_members='__init__'), + PackageDescription( + 'types', ''' +The types package contains quantum types such as Qubit, Qureg, and WeakQubitRef. With further development of the math library, also quantum integers, quantum fixed point numbers etc. will be added. +'''), +] +# ------------------------------------------------------------------------------ +# Automatically generate ReST files for each package of ProjectQ + +docgen_path = os.path.join(os.path.dirname(os.path.abspath('__file__')), + '_doc_gen') +os.mkdir(docgen_path) +for desc in descriptions: + fname = os.path.join(docgen_path, 'projectq.{}.rst'.format(desc.name)) + lines = None + if os.path.exists(fname): + with open(fname, 'r') as fd: + lines = [line[:-1] for line in fd.readlines()] + + new_lines = desc.get_ReST() + + if new_lines != lines: + with open(fname, 'w') as fd: + fd.write('\n'.join(desc.get_ReST())) diff --git a/docs/package_description.py b/docs/package_description.py new file mode 100644 index 000000000..9980e4235 --- /dev/null +++ b/docs/package_description.py @@ -0,0 +1,162 @@ +import inspect +import sys +import os + + +class PackageDescription(object): + package_list = [] + + def __init__(self, + pkg_name, + desc='', + module_special_members='__init__', + submodule_special_members='', + submodules_desc='', + helper_submodules=None): + """ + Args: + name (str): Name of ProjectQ module + desc (str): (optional) Description of module + module_special_members (str): (optional) Special members to include + in the documentation of the module + submodule_special_members (str): (optional) Special members to + include in the documentation of submodules + submodules_desc (str): (optional) Description to print out before + the list of submodules + helper_submodules (list): (optional) List of tuples for helper + sub-modules to include in the documentation. + Tuples are (section_title, submodukle_name, + automodule_properties) + """ + + self.name = pkg_name + self.desc = desc + if pkg_name not in PackageDescription.package_list: + PackageDescription.package_list.append(pkg_name) + + self.module = sys.modules['projectq.{}'.format(self.name)] + self.module_special_members = module_special_members + + self.submodule_special_members = submodule_special_members + self.submodules_desc = submodules_desc + + self.helper_submodules = helper_submodules + + module_root = os.path.dirname(self.module.__file__) + sub = [(name, obj) for name, obj in inspect.getmembers( + self.module, lambda obj: inspect.ismodule(obj) and hasattr( + obj, '__file__') and module_root in obj.__file__) + if pkg_name[0] != '_'] + + self.subpackages = [] + self.submodules = [] + for name, obj in sub: + if '{}.{}'.format(self.name, + name) in PackageDescription.package_list: + self.subpackages.append((name, obj)) + else: + self.submodules.append((name, obj)) + + self.subpackages.sort(key=lambda x: x[0].lower()) + self.submodules.sort(key=lambda x: x[0].lower()) + + self.members = [(name, obj) for name, obj in inspect.getmembers( + self.module, lambda obj: + (inspect.isclass(obj) or inspect.isfunction(obj) or isinstance( + obj, (int, float, tuple, list, dict, set, frozenset, str)))) + if name[0] != '_'] + self.members.sort(key=lambda x: x[0].lower()) + + def get_ReST(self): + new_lines = [] + new_lines.append(self.name) + new_lines.append('=' * len(self.name)) + new_lines.append('') + + if self.desc: + new_lines.append(self.desc.strip()) + new_lines.append('') + + submodule_has_index = False + + if self.subpackages: + new_lines.append('Subpackages') + new_lines.append('-' * len(new_lines[-1])) + new_lines.append('') + new_lines.append('.. toctree::') + new_lines.append(' :maxdepth: 1') + new_lines.append('') + for name, _ in self.subpackages: + new_lines.append(' projectq.{}.{}'.format(self.name, name)) + new_lines.append('') + else: + submodule_has_index = True + new_lines.append('.. autosummary::') + new_lines.append('') + if self.submodules: + for name, _ in self.submodules: + new_lines.append('\tprojectq.{}.{}'.format( + self.name, name)) + new_lines.append('') + if self.members: + for name, _ in self.members: + new_lines.append('\tprojectq.{}.{}'.format( + self.name, name)) + new_lines.append('') + + if self.submodules: + new_lines.append('Submodules') + new_lines.append('-' * len(new_lines[-1])) + new_lines.append('') + if self.submodules_desc: + new_lines.append(self.submodules_desc.strip()) + new_lines.append('') + + if not submodule_has_index: + new_lines.append('.. autosummary::') + new_lines.append('') + for name, _ in self.submodules: + new_lines.append(' projectq.{}.{}'.format( + self.name, name)) + new_lines.append('') + + for name, _ in self.submodules: + new_lines.append(name) + new_lines.append('^' * len(new_lines[-1])) + new_lines.append('') + new_lines.append('.. automodule:: projectq.{}.{}'.format( + self.name, name)) + new_lines.append(' :members:') + if self.submodule_special_members: + new_lines.append(' :special-members: {}'.format( + self.submodule_special_members)) + new_lines.append(' :undoc-members:') + new_lines.append('') + + new_lines.append('Module contents') + new_lines.append('-' * len(new_lines[-1])) + new_lines.append('') + new_lines.append('.. automodule:: projectq.{}'.format(self.name)) + new_lines.append(' :members:') + new_lines.append(' :undoc-members:') + new_lines.append(' :special-members: {}'.format( + self.module_special_members)) + new_lines.append(' :imported-members:') + new_lines.append('') + + if self.helper_submodules: + new_lines.append('Helper sub-modules') + new_lines.append('-' * len(new_lines[-1])) + new_lines.append('') + for title, name, params in self.helper_submodules: + new_lines.append(title) + new_lines.append('^' * len(title)) + new_lines.append('') + new_lines.append('.. automodule:: projectq.{}.{}'.format( + self.name, name)) + for param in params: + new_lines.append(' {}'.format(param)) + new_lines.append('') + + assert not new_lines[-1] + return new_lines[:-1] diff --git a/docs/projectq.backends.rst b/docs/projectq.backends.rst deleted file mode 100755 index 621f7ce86..000000000 --- a/docs/projectq.backends.rst +++ /dev/null @@ -1,20 +0,0 @@ -backends -======== - -.. autosummary:: - - projectq.backends.CommandPrinter - projectq.backends.CircuitDrawer - projectq.backends.Simulator - projectq.backends.ClassicalSimulator - projectq.backends.ResourceCounter - projectq.backends.IBMBackend - - -Module contents ---------------- - -.. automodule:: projectq.backends - :members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.cengines.rst b/docs/projectq.cengines.rst deleted file mode 100755 index 5a3c963a6..000000000 --- a/docs/projectq.cengines.rst +++ /dev/null @@ -1,33 +0,0 @@ -cengines -======== - -The ProjectQ compiler engines package. - -.. autosummary:: - projectq.cengines.AutoReplacer - projectq.cengines.BasicEngine - projectq.cengines.BasicMapper - projectq.cengines.CommandModifier - projectq.cengines.CompareEngine - projectq.cengines.DecompositionRule - projectq.cengines.DecompositionRuleSet - projectq.cengines.DummyEngine - projectq.cengines.ForwarderEngine - projectq.cengines.GridMapper - projectq.cengines.InstructionFilter - projectq.cengines.IBM5QubitMapper - projectq.cengines.LinearMapper - projectq.cengines.LocalOptimizer - projectq.cengines.ManualMapper - projectq.cengines.MainEngine - projectq.cengines.SwapAndCNOTFlipper - projectq.cengines.TagRemover - - -Module contents ---------------- - -.. automodule:: projectq.cengines - :members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.libs.math.rst b/docs/projectq.libs.math.rst deleted file mode 100755 index 1567978b5..000000000 --- a/docs/projectq.libs.math.rst +++ /dev/null @@ -1,21 +0,0 @@ -math -==== - -A tiny math library which will be extended thoughout the next weeks. Right now, it only contains the math functions necessary to run Beauregard's implementation of Shor's algorithm. - -.. autosummary:: - - projectq.libs.math.all_defined_decomposition_rules - projectq.libs.math.AddConstant - projectq.libs.math.SubConstant - projectq.libs.math.AddConstantModN - projectq.libs.math.SubConstantModN - projectq.libs.math.MultiplyByConstantModN - -Module contents ---------------- - -.. automodule:: projectq.libs.math - :members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.libs.revkit.rst b/docs/projectq.libs.revkit.rst deleted file mode 100644 index 90a2dbb18..000000000 --- a/docs/projectq.libs.revkit.rst +++ /dev/null @@ -1,34 +0,0 @@ -revkit -====== - -This library integrates `RevKit `_ into -ProjectQ to allow some automatic synthesis routines for reversible logic. The -library adds the following operations that can be used to construct quantum -circuits: - -- :class:`~projectq.libs.revkit.ControlFunctionOracle`: Synthesizes a reversible circuit from Boolean control function -- :class:`~projectq.libs.revkit.PermutationOracle`: Synthesizes a reversible circuit for a permutation -- :class:`~projectq.libs.revkit.PhaseOracle`: Synthesizes phase circuit from an arbitrary Boolean function - -RevKit can be installed from PyPi with `pip install revkit`. - -.. note:: - - The RevKit Python module must be installed in order to use this ProjectQ library. - - There exist precompiled binaries in PyPi, as well as a source distribution. - Note that a C++ compiler with C++17 support is required to build the RevKit - python module from source. Examples for compatible compilers are Clang - 6.0, GCC 7.3, and GCC 8.1. - -The integration of RevKit into ProjectQ and other quantum programming languages is described in the paper - - * Mathias Soeken, Thomas Haener, and Martin Roetteler "Programming Quantum Computers Using Design Automation," in: Design Automation and Test in Europe (2018) [`arXiv:1803.01022 `_] - -Module contents ---------------- - -.. automodule:: projectq.libs.revkit - :members: - :special-members: __init__,__or__ - :imported-members: diff --git a/docs/projectq.libs.rst b/docs/projectq.libs.rst deleted file mode 100755 index 9f2c8cd4b..000000000 --- a/docs/projectq.libs.rst +++ /dev/null @@ -1,20 +0,0 @@ -libs -==== - -The library collection of ProjectQ which, for now, consists of a tiny math library and an interface library to RevKit. Soon, more libraries will be added. - -Subpackages ------------ - -.. toctree:: - - projectq.libs.math - projectq.libs.revkit - -Module contents ---------------- - -.. automodule:: projectq.libs - :members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.meta.rst b/docs/projectq.meta.rst deleted file mode 100755 index 14c3d9eea..000000000 --- a/docs/projectq.meta.rst +++ /dev/null @@ -1,32 +0,0 @@ -meta -==== - -Contains meta statements which allow more optimal code while making it easier for users to write their code. -Examples are `with Compute`, followed by an automatic uncompute or `with Control`, which allows the user to condition an entire code block upon the state of a qubit. - - -.. autosummary:: - - projectq.meta.DirtyQubitTag - projectq.meta.LogicalQubitIDTag - projectq.meta.LoopTag - projectq.meta.Loop - projectq.meta.Compute - projectq.meta.Uncompute - projectq.meta.CustomUncompute - projectq.meta.ComputeTag - projectq.meta.UncomputeTag - projectq.meta.Control - projectq.meta.get_control_count - projectq.meta.Dagger - projectq.meta.insert_engine - projectq.meta.drop_engine_after - -Module contents ---------------- - -.. automodule:: projectq.meta - :members: - :undoc-members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.ops.rst b/docs/projectq.ops.rst deleted file mode 100755 index 8f8d4cceb..000000000 --- a/docs/projectq.ops.rst +++ /dev/null @@ -1,63 +0,0 @@ -ops -=== - -The operations collection consists of various default gates and is a work-in-progress, as users start to work with ProjectQ. - -.. autosummary:: - - projectq.ops.BasicGate - projectq.ops.SelfInverseGate - projectq.ops.BasicRotationGate - projectq.ops.BasicPhaseGate - projectq.ops.ClassicalInstructionGate - projectq.ops.FastForwardingGate - projectq.ops.BasicMathGate - projectq.ops.apply_command - projectq.ops.Command - projectq.ops.H - projectq.ops.X - projectq.ops.Y - projectq.ops.Z - projectq.ops.S - projectq.ops.Sdag - projectq.ops.T - projectq.ops.Tdag - projectq.ops.SqrtX - projectq.ops.Swap - projectq.ops.SqrtSwap - projectq.ops.Entangle - projectq.ops.Ph - projectq.ops.Rx - projectq.ops.Ry - projectq.ops.Rz - projectq.ops.R - projectq.ops.FlushGate - projectq.ops.MeasureGate - projectq.ops.Allocate - projectq.ops.Deallocate - projectq.ops.AllocateDirty - projectq.ops.Barrier - projectq.ops.DaggeredGate - projectq.ops.ControlledGate - projectq.ops.C - projectq.ops.All - projectq.ops.Tensor - projectq.ops.QFT - projectq.ops.QubitOperator - projectq.ops.CRz - projectq.ops.CNOT - projectq.ops.CZ - projectq.ops.Toffoli - projectq.ops.TimeEvolution - projectq.ops.UniformlyControlledRy - projectq.ops.UniformlyControlledRz - projectq.ops.StatePreparation - - -Module contents ---------------- - -.. automodule:: projectq.ops - :members: - :special-members: __init__,__or__ - :imported-members: diff --git a/docs/projectq.rst b/docs/projectq.rst index cf69c7ab8..16a948655 100755 --- a/docs/projectq.rst +++ b/docs/projectq.rst @@ -11,12 +11,12 @@ For a detailed documentation of a subpackage or module, click on its name below: :maxdepth: 1 :titlesonly: - projectq.backends - projectq.cengines - projectq.libs - projectq.meta - projectq.ops - projectq.setups - projectq.types + _doc_gen/projectq.backends + _doc_gen/projectq.cengines + _doc_gen/projectq.libs + _doc_gen/projectq.meta + _doc_gen/projectq.ops + _doc_gen/projectq.setups + _doc_gen/projectq.types diff --git a/docs/projectq.setups.decompositions.rst b/docs/projectq.setups.decompositions.rst deleted file mode 100755 index a17e3d1bd..000000000 --- a/docs/projectq.setups.decompositions.rst +++ /dev/null @@ -1,142 +0,0 @@ -decompositions -============== - -The decomposition package is a collection of gate decomposition / replacement rules which can be used by, e.g., the AutoReplacer engine. - - -.. autosummary:: - - projectq.setups.decompositions.arb1qubit2rzandry - projectq.setups.decompositions.barrier - projectq.setups.decompositions.carb1qubit2cnotrzandry - projectq.setups.decompositions.cnu2toffoliandcu - projectq.setups.decompositions.crz2cxandrz - projectq.setups.decompositions.entangle - projectq.setups.decompositions.globalphase - projectq.setups.decompositions.ph2r - projectq.setups.decompositions.qft2crandhadamard - projectq.setups.decompositions.r2rzandph - projectq.setups.decompositions.rx2rz - projectq.setups.decompositions.ry2rz - projectq.setups.decompositions.swap2cnot - projectq.setups.decompositions.time_evolution - projectq.setups.decompositions.toffoli2cnotandtgate - - -Submodules ----------- - -projectq.setups.decompositions.arb1qubit2rzandry module -------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.arb1qubit2rzandry - :members: - :undoc-members: - - -projectq.setups.decompositions.barrier module ---------------------------------------------- - -.. automodule:: projectq.setups.decompositions.barrier - :members: - :undoc-members: - -projectq.setups.decompositions.carb1qubit2cnotrzandry module ------------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.carb1qubit2cnotrzandry - :members: - :undoc-members: - -projectq.setups.decompositions.cnu2toffoliandcu module ------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.cnu2toffoliandcu - :members: - :undoc-members: - -projectq.setups.decompositions.crz2cxandrz module -------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.crz2cxandrz - :members: - :undoc-members: - -projectq.setups.decompositions.entangle module ----------------------------------------------- - -.. automodule:: projectq.setups.decompositions.entangle - :members: - :undoc-members: - -projectq.setups.decompositions.globalphase module -------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.globalphase - :members: - :undoc-members: - -projectq.setups.decompositions.ph2r module ------------------------------------------- - -.. automodule:: projectq.setups.decompositions.ph2r - :members: - :undoc-members: - -projectq.setups.decompositions.qft2crandhadamard module -------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.qft2crandhadamard - :members: - :undoc-members: - -projectq.setups.decompositions.r2rzandph module ------------------------------------------------ - -.. automodule:: projectq.setups.decompositions.r2rzandph - :members: - :undoc-members: - -projectq.setups.decompositions.rx2rz module -------------------------------------------- - -.. automodule:: projectq.setups.decompositions.rx2rz - :members: - :undoc-members: - -projectq.setups.decompositions.ry2rz module -------------------------------------------- - -.. automodule:: projectq.setups.decompositions.ry2rz - :members: - :undoc-members: - -projectq.setups.decompositions.swap2cnot module ------------------------------------------------ - -.. automodule:: projectq.setups.decompositions.swap2cnot - :members: - :undoc-members: - -projectq.setups.decompositions.time_evolution module ----------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.time_evolution - :members: - :undoc-members: - -projectq.setups.decompositions.toffoli2cnotandtgate module ----------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.toffoli2cnotandtgate - :members: - :undoc-members: - - -Module contents ---------------- - -.. automodule:: projectq.setups.decompositions - :members: - :undoc-members: - :imported-members: diff --git a/docs/projectq.setups.rst b/docs/projectq.setups.rst deleted file mode 100755 index 058469f07..000000000 --- a/docs/projectq.setups.rst +++ /dev/null @@ -1,94 +0,0 @@ -setups -====== - -The setups package contains a collection of setups which can be loaded by the `MainEngine`. Each setup contains a `get_engine_list` function which returns a list of compiler engines: - -Example: - .. code-block:: python - - import projectq.setups.ibm as ibm_setup - from projectq import MainEngine - eng = MainEngine(engine_list=ibm_setup.get_engine_list()) - # eng uses the default Simulator backend - -The subpackage decompositions contains all the individual decomposition rules -which can be given to, e.g., an `AutoReplacer`. - - -Subpackages ------------ - -.. toctree:: - :maxdepth: 1 - - projectq.setups.decompositions - -Submodules ----------- - -Each of the submodules contains a setup which can be used to specify the -`engine_list` used by the `MainEngine` : - -.. autosummary:: - - projectq.setups.default - projectq.setups.grid - projectq.setups.ibm - projectq.setups.ibm16 - projectq.setups.linear - projectq.setups.restrictedgateset - -default -------- - -.. automodule:: projectq.setups.default - :members: - :special-members: __init__ - :undoc-members: - -grid ----- - -.. automodule:: projectq.setups.grid - :members: - :special-members: __init__ - :undoc-members: - -ibm ---- - -.. automodule:: projectq.setups.ibm - :members: - :special-members: __init__ - :undoc-members: - -ibm16 ------ - -.. automodule:: projectq.setups.ibm16 - :members: - :special-members: __init__ - :undoc-members: - -linear ------- - -.. automodule:: projectq.setups.linear - :members: - :special-members: __init__ - :undoc-members: - -restrictedgateset ------------------ - -.. automodule:: projectq.setups.restrictedgateset - :members: - :special-members: __init__ - :undoc-members: - -Module contents ---------------- - -.. automodule:: projectq.setups - :members: - :special-members: __init__ diff --git a/docs/projectq.types.rst b/docs/projectq.types.rst deleted file mode 100755 index 4f26edc9d..000000000 --- a/docs/projectq.types.rst +++ /dev/null @@ -1,18 +0,0 @@ -types -===== - -The types package contains quantum types such as Qubit, Qureg, and WeakQubitRef. With further development of the math library, also quantum integers, quantum fixed point numbers etc. will be added. - -.. autosummary:: - projectq.types.BasicQubit - projectq.types.Qubit - projectq.types.Qureg - projectq.types.WeakQubitRef - -Module contents ---------------- - -.. automodule:: projectq.types - :members: - :special-members: - :imported-members: diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 289af1aef..cec2e75e7 100755 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -25,13 +25,9 @@ or, alternatively, `clone/download `_ thi ProjectQ comes with a high-performance quantum simulator written in C++. Please see the detailed OS specific installation instructions below to make sure that you are installing the fastest version. .. note:: - The setup will try to build a C++-Simulator, which is much faster than the Python implementation. If it fails, you may use the `--without-cppsimulator` parameter, i.e., - - .. code-block:: bash - - python -m pip install --user --global-option=--without-cppsimulator . - - and the framework will use the **slow Python simulator instead**. Note that this only works if the installation has been tried once without the `--without-cppsimulator` parameter and hence all requirements are now installed. See the instructions below if you want to run larger simulations. The Python simulator works perfectly fine for the small examples (e.g., running Shor's algorithm for factoring 15 or 21). + The setup will try to build a C++-Simulator, which is much faster than the Python implementation. If the C++ compilation were to fail, the setup will install a pure Python implementation of the simulator instead. The Python simulator should work fine for small examples (e.g., running Shor's algorithm for factoring 15 or 21). + + If you want to skip the installation of the C++-Simulator altogether, you can define the ``DISABLE_PROJECTQ_CEXT`` environment variable to avoid any compilation steps. .. note:: If building the C++-Simulator does not work out of the box, consider specifying a different compiler. For example: @@ -40,13 +36,13 @@ ProjectQ comes with a high-performance quantum simulator written in C++. Please env CC=g++-5 python -m pip install --user projectq - Please note that the compiler you specify must support **C++11**! + Please note that the compiler you specify must support at leaste **C++11**! .. note:: Please use pip version v6.1.0 or higher as this ensures that dependencies are installed in the `correct order `_. .. note:: - ProjectQ should be installed on each computer individually as the C++ simulator compilation creates binaries which are optimized for the specific hardware on which it is being installed (potentially using our AVX version and `-march=native`). Therefore, sharing the same ProjectQ installation across different hardware can cause problems. + ProjectQ should be installed on each computer individually as the C++ simulator compilation creates binaries which are optimized for the specific hardware on which it is being installed (potentially using our AVX version and `-march=native`). Therefore, sharing the same ProjectQ installation across different hardware may cause some problems. Detailed instructions and OS-specific hints @@ -70,38 +66,75 @@ Detailed instructions and OS-specific hints .. code-block:: bash - sudo pip3 install --user projectq + sudo python3 -m pip install --user projectq + + all dependencies (such as numpy and pybind11) should be installed automatically. + + +**ArchLinux/Manjaro**: + + Make sure that you have a C/C++ compiler installed: + + .. code-block:: bash + + sudo pacman -Syu gcc + + You only need to install Python (and the package manager). For version 3, run + + .. code-block:: bash + + sudo pacman -Syu python python-pip + + When you then run + + .. code-block:: bash + + sudo python3 -m pip install --user projectq all dependencies (such as numpy and pybind11) should be installed automatically. **Windows**: - It is easiest to install a pre-compiled version of Python, including numpy and many more useful packages. One way to do so is using, e.g., the Python3.5 installers from `python.org `_ or `ANACONDA `_. Installing ProjectQ right away will succeed for the (slow) Python simulator (i.e., with the `--without-cppsimulator` flag). For a compiled version of the simulator, install the Visual C++ Build Tools and the Microsoft Windows SDK prior to doing a pip install. The built simulator will not support multi-threading due to the limited OpenMP support of msvc. + It is easiest to install a pre-compiled version of Python, including numpy and many more useful packages. One way to do so is using, e.g., the Python 3.7 installers from `python.org `_ or `ANACONDA `_. Installing ProjectQ right away will succeed for the (slow) Python simulator. For a compiled version of the simulator, install the Visual C++ Build Tools and the Microsoft Windows SDK prior to doing a pip install. The built simulator will not support multi-threading due to the limited OpenMP support of the Visual Studio compiler. + + If the Python executable is added to your PATH (option normally suggested at the end of the Python installation procedure), you can then open a cmdline window (WIN + R, type "cmd" and click *OK*) and enter the following in order to install ProjectQ: + + .. code-block:: batch + + python -m pip install --user projectq + Should you want to run multi-threaded simulations, you can install a compiler which supports newer OpenMP versions, such as MinGW GCC and then manually build the C++ simulator with OpenMP enabled. **macOS**: - These are the steps to install ProjectQ on a new Mac: + Similarly to the other platforms, installing ProjectQ without the C++ simulator is really easy: - In order to install the fast C++ simulator, we require that your system has a C++ compiler (see option 3 below on how to only install the slower Python simulator via the `--without-cppsimulator` parameter) + .. code-block:: bash - Below you will find two options to install the fast C++ simulator. The first one is the easiest and requires only the standard compiler which Apple distributes with XCode. The second option uses macports to install the simulator with additional support for multi-threading by using OpenMP, which makes it slightly faster. We show how to install the required C++ compiler (clang) which supports OpenMP and additionally, we show how to install a newer python version. + python3 -m pip install --user projectq -.. note:: - Depending on your system you might need to use `sudo` for the installation. + + In order to install the fast C++ simulator, we require that a C++ compiler is installed on your system. There are essentially three options you can choose from: + + 1. Using the compiler provided by Apple through the XCode command line tools. + 2. Using Homebrew + 3. Using MacPorts + + For both options 2 and 3, you will be required to first install the XCode command line tools -1. Installation using XCode and the default python: - Install XCode by opening a terminal and running the following command: + **Apple XCode command line tool** + + Install the XCode command line tools by opening a terminal window and running the following command: .. code-block:: bash xcode-select --install - - Next, you will need to install Python and pip. See option 2 for information on how to install a newer python version with macports. Here, we are using the standard python which is preinstalled with macOS. Pip can be installed by: + + Next, you will need to install Python and pip. See options 2 and 3 for information on how to install a newer python version with either Homebrew or MacPorts. Here, we are using the standard python which is preinstalled with macOS. Pip can be installed by: .. code-block:: bash @@ -111,56 +144,64 @@ Detailed instructions and OS-specific hints .. code-block:: bash - python -m pip install --user projectq + python3 -m pip install --user projectq + Note that the compiler provided by Apple is currently not able to compile ProjectQ's multi-threaded code. -2. Installation using macports: + **Homebrew** - Either use the standard python and install pip as shown in option 1 or better use macports to install a newer python version, e.g., Python 3.5 and the corresponding pip. Visit `macports.org `_ and install the latest version (afterwards open a new terminal). Then, use macports to install Python 3.5 by + First install the XCode command line tools. Then install Homebrew with the following command: .. code-block:: bash - sudo port install python35 + /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" - It might show a warning that if you intend to use python from the terminal, you should also install + Then proceed to install Python as well as a C/C++ compiler (note: gcc installed via Homebrew may lead to some issues): .. code-block:: bash - sudo port install py35-readline - - Install pip by + brew install python llvm + + You should now be able to install ProjectQ with the C++ simulator using the following command: .. code-block:: bash - sudo port install py35-pip + env P=/usr/local/opt/llvm/bin CC=$P/clang CXX=$P/clang++ python3 -m pip install --user projectq + + + **MacPorts** + + Visit `macports.org `_ and install the latest version that corresponds to your operating system's version. Afterwards, open a new terminal window. - Next, we can install ProjectQ with the high performance simulator written in C++. First, we will need to install a suitable compiler with support for **C++11**, OpenMP, and instrinsics. The best option is to install clang 3.9 also using macports (note: gcc installed via macports does not work) + Then, use macports to install Python 3.7 by entering the following command .. code-block:: bash - sudo port install clang-3.9 + sudo port install python37 - ProjectQ is now installed by: + It might show a warning that if you intend to use python from the terminal. In this case, you should also install .. code-block:: bash - env CC=clang-mp-3.9 env CXX=clang++-mp-3.9 python3.5 -m pip install --user projectq + sudo port install py37-gnureadline + + Install pip by -3. Installation with only the slow Python simulator: + .. code-block:: bash - While this simulator works fine for small examples, it is suggested to install the high performance simulator written in C++. + sudo port install py37-pip - If you just want to install ProjectQ with the (slow) Python simulator and no compiler, then first try to install ProjectQ with the default compiler + Next, we can install ProjectQ with the high performance simulator written in C++. First, we will need to install a suitable compiler with support for **C++11**, OpenMP, and instrinsics. The best option is to install clang 9.0 also using macports (note: gcc installed via macports does not work). .. code-block:: bash - python -m pip install --user projectq + sudo port install clang-9.0 - which most likely will fail. Then, try again with the flag ``--without-cppsimulator``: + ProjectQ is now installed by: .. code-block:: bash - python -m pip install --user --global-option=--without-cppsimulator projectq + env CC=clang-mp-9.0 env CXX=clang++-mp-9.0 /opt/local/bin/python3.7 -m pip install --user projectq The ProjectQ syntax diff --git a/examples/ibm.py b/examples/ibm.py index 05e042230..11a81a832 100755 --- a/examples/ibm.py +++ b/examples/ibm.py @@ -2,9 +2,10 @@ from projectq.backends import IBMBackend from projectq.ops import Measure, Entangle, All from projectq import MainEngine +import getpass -def run_entangle(eng, num_qubits=5): +def run_entangle(eng, num_qubits=3): """ Runs an entangling operation on the provided compiler engine. @@ -37,9 +38,20 @@ def run_entangle(eng, num_qubits=5): if __name__ == "__main__": + #devices commonly available : + #ibmq_16_melbourne (15 qubit) + #ibmq_essex (5 qubit) + #ibmq_qasm_simulator (32 qubits) + device = None #replace by the IBM device name you want to use + token = None #replace by the token given by IBMQ + if token is None: + token = getpass.getpass(prompt='IBM Q token > ') + if device is None: + token = getpass.getpass(prompt='IBM device > ') # create main compiler engine for the IBM back-end - eng = MainEngine(IBMBackend(use_hardware=True, num_runs=1024, - verbose=False, device='ibmqx4'), - engine_list=projectq.setups.ibm.get_engine_list()) + eng = MainEngine(IBMBackend(use_hardware=True, token=token, num_runs=1024, + verbose=False, device=device), + engine_list=projectq.setups.ibm.get_engine_list( + token=token, device=device)) # run the circuit and print the result print(run_entangle(eng)) diff --git a/examples/variational_quantum_eigensolver.ipynb b/examples/variational_quantum_eigensolver.ipynb new file mode 100644 index 000000000..b893a8698 --- /dev/null +++ b/examples/variational_quantum_eigensolver.ipynb @@ -0,0 +1,197 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Implementation of a Variational Quantum Eigensolver (VQE).\n", + "\n", + "The example shown here is from the paper \"Scalable Quantum Simulation of\n", + "Molecular Energies\" by P.J.J. O'Malley et al. [arXiv:1512.06860v2](https://arxiv.org/abs/1512.06860v2)\n", + "(Note that only the latest arXiv version contains the correct coefficients of\n", + " the Hamiltonian)\n", + "\n", + "Eq. 2 of the paper shows the functional which one needs to minimize and Eq. 3\n", + "shows the coupled cluster ansatz for the trial wavefunction (using the unitary\n", + "coupled cluster approach). The Hamiltonian is given in Eq. 1. The coefficients\n", + "can be found in Table 1. Note that both the ansatz and the Hamiltonian can be\n", + "calculated using FermiLib which is a library for simulating quantum systems\n", + "on top of ProjectQ." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import projectq\n", + "from projectq.ops import All, Measure, QubitOperator, TimeEvolution, X\n", + "\n", + "import matplotlib.pyplot as plt\n", + "from scipy.optimize import minimize_scalar\n", + "\n", + "# Data from paper (arXiv:1512.06860v2) table 1: R, I, Z0, Z1, Z0Z1, X0X1, Y0Y1\n", + "raw_data_table_1 = [\n", + " [0.20, 2.8489, 0.5678, -1.4508, 0.6799, 0.0791, 0.0791],\n", + " [0.25, 2.1868, 0.5449, -1.2870, 0.6719, 0.0798, 0.0798],\n", + " [0.30, 1.7252, 0.5215, -1.1458, 0.6631, 0.0806, 0.0806],\n", + " [0.35, 1.3827, 0.4982, -1.0226, 0.6537, 0.0815, 0.0815],\n", + " [0.40, 1.1182, 0.4754, -0.9145, 0.6438, 0.0825, 0.0825],\n", + " [0.45, 0.9083, 0.4534, -0.8194, 0.6336, 0.0835, 0.0835],\n", + " [0.50, 0.7381, 0.4325, -0.7355, 0.6233, 0.0846, 0.0846],\n", + " [0.55, 0.5979, 0.4125, -0.6612, 0.6129, 0.0858, 0.0858],\n", + " [0.60, 0.4808, 0.3937, -0.5950, 0.6025, 0.0870, 0.0870],\n", + " [0.65, 0.3819, 0.3760, -0.5358, 0.5921, 0.0883, 0.0883],\n", + " [0.70, 0.2976, 0.3593, -0.4826, 0.5818, 0.0896, 0.0896],\n", + " [0.75, 0.2252, 0.3435, -0.4347, 0.5716, 0.0910, 0.0910],\n", + " [0.80, 0.1626, 0.3288, -0.3915, 0.5616, 0.0925, 0.0925],\n", + " [0.85, 0.1083, 0.3149, -0.3523, 0.5518, 0.0939, 0.0939],\n", + " [0.90, 0.0609, 0.3018, -0.3168, 0.5421, 0.0954, 0.0954],\n", + " [0.95, 0.0193, 0.2895, -0.2845, 0.5327, 0.0970, 0.0970],\n", + " [1.00, -0.0172, 0.2779, -0.2550, 0.5235, 0.0986, 0.0986],\n", + " [1.05, -0.0493, 0.2669, -0.2282, 0.5146, 0.1002, 0.1002],\n", + " [1.10, -0.0778, 0.2565, -0.2036, 0.5059, 0.1018, 0.1018],\n", + " [1.15, -0.1029, 0.2467, -0.1810, 0.4974, 0.1034, 0.1034],\n", + " [1.20, -0.1253, 0.2374, -0.1603, 0.4892, 0.1050, 0.1050],\n", + " [1.25, -0.1452, 0.2286, -0.1413, 0.4812, 0.1067, 0.1067],\n", + " [1.30, -0.1629, 0.2203, -0.1238, 0.4735, 0.1083, 0.1083],\n", + " [1.35, -0.1786, 0.2123, -0.1077, 0.4660, 0.1100, 0.1100],\n", + " [1.40, -0.1927, 0.2048, -0.0929, 0.4588, 0.1116, 0.1116],\n", + " [1.45, -0.2053, 0.1976, -0.0792, 0.4518, 0.1133, 0.1133],\n", + " [1.50, -0.2165, 0.1908, -0.0666, 0.4451, 0.1149, 0.1149],\n", + " [1.55, -0.2265, 0.1843, -0.0549, 0.4386, 0.1165, 0.1165],\n", + " [1.60, -0.2355, 0.1782, -0.0442, 0.4323, 0.1181, 0.1181],\n", + " [1.65, -0.2436, 0.1723, -0.0342, 0.4262, 0.1196, 0.1196],\n", + " [1.70, -0.2508, 0.1667, -0.0251, 0.4204, 0.1211, 0.1211],\n", + " [1.75, -0.2573, 0.1615, -0.0166, 0.4148, 0.1226, 0.1226],\n", + " [1.80, -0.2632, 0.1565, -0.0088, 0.4094, 0.1241, 0.1241],\n", + " [1.85, -0.2684, 0.1517, -0.0015, 0.4042, 0.1256, 0.1256],\n", + " [1.90, -0.2731, 0.1472, 0.0052, 0.3992, 0.1270, 0.1270],\n", + " [1.95, -0.2774, 0.1430, 0.0114, 0.3944, 0.1284, 0.1284],\n", + " [2.00, -0.2812, 0.1390, 0.0171, 0.3898, 0.1297, 0.1297],\n", + " [2.05, -0.2847, 0.1352, 0.0223, 0.3853, 0.1310, 0.1310],\n", + " [2.10, -0.2879, 0.1316, 0.0272, 0.3811, 0.1323, 0.1323],\n", + " [2.15, -0.2908, 0.1282, 0.0317, 0.3769, 0.1335, 0.1335],\n", + " [2.20, -0.2934, 0.1251, 0.0359, 0.3730, 0.1347, 0.1347],\n", + " [2.25, -0.2958, 0.1221, 0.0397, 0.3692, 0.1359, 0.1359],\n", + " [2.30, -0.2980, 0.1193, 0.0432, 0.3655, 0.1370, 0.1370],\n", + " [2.35, -0.3000, 0.1167, 0.0465, 0.3620, 0.1381, 0.1381],\n", + " [2.40, -0.3018, 0.1142, 0.0495, 0.3586, 0.1392, 0.1392],\n", + " [2.45, -0.3035, 0.1119, 0.0523, 0.3553, 0.1402, 0.1402],\n", + " [2.50, -0.3051, 0.1098, 0.0549, 0.3521, 0.1412, 0.1412],\n", + " [2.55, -0.3066, 0.1078, 0.0572, 0.3491, 0.1422, 0.1422],\n", + " [2.60, -0.3079, 0.1059, 0.0594, 0.3461, 0.1432, 0.1432],\n", + " [2.65, -0.3092, 0.1042, 0.0614, 0.3433, 0.1441, 0.1441],\n", + " [2.70, -0.3104, 0.1026, 0.0632, 0.3406, 0.1450, 0.1450],\n", + " [2.75, -0.3115, 0.1011, 0.0649, 0.3379, 0.1458, 0.1458],\n", + " [2.80, -0.3125, 0.0997, 0.0665, 0.3354, 0.1467, 0.1467],\n", + " [2.85, -0.3135, 0.0984, 0.0679, 0.3329, 0.1475, 0.1475]]\n", + "\n", + "\n", + "def variational_quantum_eigensolver(theta, hamiltonian):\n", + " \"\"\"\n", + " Args:\n", + " theta (float): variational parameter for ansatz wavefunction\n", + " hamiltonian (QubitOperator): Hamiltonian of the system\n", + " Returns:\n", + " energy of the wavefunction for parameter theta\n", + " \"\"\"\n", + " # Create a ProjectQ compiler with a simulator as a backend\n", + " eng = projectq.MainEngine()\n", + " # Allocate 2 qubits in state |00>\n", + " wavefunction = eng.allocate_qureg(2)\n", + " # Initialize the Hartree Fock state |01>\n", + " X | wavefunction[0]\n", + " # build the operator for ansatz wavefunction\n", + " ansatz_op = QubitOperator('X0 Y1')\n", + " # Apply the unitary e^{-i * ansatz_op * t}\n", + " TimeEvolution(theta, ansatz_op) | wavefunction\n", + " # flush all gates\n", + " eng.flush()\n", + " # Calculate the energy.\n", + " # The simulator can directly return expectation values, while on a\n", + " # real quantum devices one would have to measure each term of the\n", + " # Hamiltonian.\n", + " energy = eng.backend.get_expectation_value(hamiltonian, wavefunction)\n", + " # Measure in order to return to return to a classical state\n", + " # (as otherwise the simulator will give an error)\n", + " All(Measure) | wavefunction\n", + " return energy" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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j0NV38Z1wR0876ag6X6VHlSC2LdvnyUNYxKMiNCgQQPKewFRlWgcryzZ2XDnne5vIrCkYUziqYj/exsB0LNgAfTsCfn5UKsWzJ4NWVmxjsoYY+KPlTg8H34IubkucRw4AHPnxjqi6m/jxo1ceOGFtG7dmvT0dLKysnj11crvhDUtLY0tW7aUviKwevVqnnuu+BAQB79eVTdt2jTatm1L7969K3S748eP5957763QbY4cOZLp06cf9OvLG1N59x9PLHF4evUCEfdISXHPTfSoKmeffTannnoqq1atYsmSJbzwwgusXbu2xLq5ubkxiDC0qpg4ovn+PfHEEzz22GPMmTOn9JWrmMr63uXl5VXKfiqSJQ5PVha0bQtHH23VVOFUZBvQBx98QEpKCldccUXBvJYtW3LNNdcA8PTTT3PWWWfRp08f+vbti6pyww030KFDBzp27MiLL7rB9ubOncugQYMKtnH11Vfz9NNPA64kcfPNN9OtWzc6duzI119/DcDWrVvp378/7du3Z9SoUYTrr+3DDz+kS5cudOnSha5du7Jr1y7GjRvHRx99RJcuXbjvvvtYvXo1PXr0oFu3bnTr1o0FC9yIt8XXy8vL44YbbqB79+506tSJRx55JOQ+n332WTIzM+nSpQuXX355wUnlsMMO48Ybb6Rz586ceOKJbNzohozfvHkz5557Lt27d6d79+58/PHHgPt1fMkll3DyySdzySWXsHfvXoYNG0a7du0YMmQIJ5xwAosXL+bJJ5/kuuuuK9j/Y489xh//+McScT3//PN07NiRDh06MHbsWABuvfVW5s+fz2WXXcYNN9xQZP25c+fSs2dPBg8eTOvWrRk3bhxTp04lMzOTjh078v333wMuwfbp04dOnTrRt29ffvrppxL7/v777xkwYADp6en06NGj4HPcuHEjQ4YMoXPnznTu3JkFCxawevVqOnToUPDae++9l/Hjx5fY5q233kr37t3p0KEDo0ePLvgO9OrVi+uuu46MjAweeOCBEq9bsWIFvXr1onXr1kycOBGAm266ifvvv79gnRtvvJEHHngAVeXqq6/m+OOPp1+/fmzaVHhHQVpaGmPHjqVbt25MmzaNpUuXcuKJJ9KpUyeGDBnC9u3bAVi0aBGdOnWiS5cuBd9/IOz3ae7cufTq1YvzzjuPNm3acNFFF4X9fpeLqla7R3p6uh6MgQNVu3Y9qJdWOStWrCiYvvZa1Z49Iz+6dFFNSlIF97dLl8jrX3tt5P0/8MADet1114Vd/tRTT+lRRx2lW7duVVXV6dOna79+/TQ3N1d//vlnbd68ua5fv17nzJmjAwcOLHjdVVddpU899ZSqqrZs2VInTpyoqqoPPvigXnbZZaqqes011+gtt9yiqqpvvvmmArp58+YSMQwaNEjnz5+vqqq7du3SnJycEvvbs2eP/vrrr6qqunLlSg1894qv98gjj+htt92mqqr79u3T9PR0XbVqVZH9rVixQgcNGqQHDhxQVdUrr7xSJ0+erKqqgM6YMUNVVW+44YaCbQ0fPlw/+ugjVVX98ccftU2bNqqqevPNN2u3bt107969qqo6YcIEHT16tKqqfv7555qcnKyLFi3SXbt2aevWrQv2mZWVpcuXLy8S17p167R58+a6adMmzcnJ0d69e+urr76qqqo9e/bURYsWlXjv5syZo/Xq1dP169frvn37tGnTpnrTTTepqur999+v13pfkEGDBunTTz+tqqpPPPGEDh48uCD+CRMmqKpqnz59dOXKlaqqunDhQu3du7eqqg4bNkzvu+8+VVXNzc3VHTt26A8//KDt27cviGPChAl68803q6rqiBEjdNq0aaqqBd8rVdWLL7644L3t2bOnXnnllSWOJxBTVlaW7tu3Tzdv3qxHHHGEHjhwQH/44Qft6p048vLytHXr1rplyxZ9+eWXC76z69at03r16hXsv2XLlnr33XcXbLtjx446d+5cVVX9xz/+UfD+tG/fXhcsWKCqqmPHji04tnDfpzlz5mjdunV1zZo1mpeXpyeeeGLB9yNY8P9/AO42CF/nWGscD5KaCsuXxzqK+LRzp2v/Afd3506o52eUaZ+uuuoq5s+fT0pKCosWLQLgtNNO44gjjgBg/vz5DB8+nOTkZBo3bkzPnj1ZtGgRdevWjbjdc845B4D09HReeeUVAObNm1cwPXDgQA4//PCQrz355JO5/vrrueiiizjnnHNo1qxZiXVycnK4+uqrWbp0KcnJyaxcuTLktt5//32WL19eUMe9c+dOvv322yLX0s+ePZslS5bQvbsbBvzXX3+lUaNGAKSkpBSUrNLT05k5cyYAs2bNYsWKgn5B+eWXX9i9ezcAZ511FoccckjB+3fttdcC0KFDBzp16gS4kkyfPn148803adu2LTk5OXTs2LFI7IsWLaJXr14EuvK56KKLmDdvHmeffXbIYw3o3r07TZq44b6PPvpo+vfvD0DHjh0Lqrays7MLPotLLrmEv/zlL0W2sXv3bhYsWMDQoUML5u3f7zqQ+OCDD5gyZQoAycnJ1KtXr+CXemnmzJnDPffcw969e9m2bRvt27fnzDPPBOD888N3wD1w4EBq1apFrVq1aNSoERs3biQtLY0GDRrw2WefsXHjRrp27UqDBg2YN29ewXe2adOm9OnTp8i2AvvZuXMnO3bsoGdPNzLviBEjGDp0KDt27GDXrl1kedUfF154IW+++SYQ/vuUkpJCZmZmwXe1S5curF69mlNOOcXX++KXJY4gDRvC5s2g6to6EkVQKTus7Gzo29ddOJCSAlOnlq86r3379rz88ssFzx988EG2bNlCRkZGwbxDDz201O3UqFGD/EBGgxJ3wteqVQtwJ5bS6qwffPBBHnvsMQDefvttxo0bx8CBA3n77bc5+eSTee+990q85r777qNx48YsW7aM/Px8ateuHXLbqsqkSZM4/fTTw+5fVRkxYgR33nlniWU1a9YsuIwy+Fjy8/NZuHBhyP36ef8ARo0axR133EGbNm249NJLfb3Gj8B7D5CUlFTwPCkpyXf7QX5+PvXr12fp0qW+1i/t+xCYN2bMGBYvXkzz5s0ZP358kfUivW/BxxT8OYwaNYqnn36an3/+md///ve+YvX7+YQS7vs0d+7csDFWJGvjCJKaCvv2wd69sY4k/mRlubaf226rmDagPn36sG/fPh566KGCeXsjvPE9evTgxRdfJC8vj82bNzNv3jwyMzNp2bIlK1asYP/+/ezYsYPZs2eXuu9TTz21oOH6nXfeKfiVetVVV7F06VKWLl1K06ZN+f777+nYsSNjx46le/fufP3119SpU4ddu3YVbGvnzp00adKEpKQknnnmmYI2ieLrnX766Tz00EPk5OQAsHLlSvbs2VMkrr59+zJ9+vSCuvBt27bx44+Re7ru378/kyZNKnge7gR78skn89JLLwGunv7zzz8vWHbCCSewZs0annvuOYYPH17itZmZmXz44Yds2bKFvLw8nn/++YJfx+V10kkn8cILLwAwdepUevToUWR53bp1adWqFdOmTQPcCXPZsmWAe78C35+8vDx27txJ48aN2bRpE1u3bmX//v0F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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "lowest_energies = []\n", + "bond_distances = []\n", + "for i in range(len(raw_data_table_1)):\n", + " # Use data of paper to construct the Hamiltonian\n", + " bond_distances.append(raw_data_table_1[i][0])\n", + " hamiltonian = raw_data_table_1[i][1] * QubitOperator(()) # == identity\n", + " hamiltonian += raw_data_table_1[i][2] * QubitOperator(\"Z0\")\n", + " hamiltonian += raw_data_table_1[i][3] * QubitOperator(\"Z1\")\n", + " hamiltonian += raw_data_table_1[i][4] * QubitOperator(\"Z0 Z1\")\n", + " hamiltonian += raw_data_table_1[i][5] * QubitOperator(\"X0 X1\")\n", + " hamiltonian += raw_data_table_1[i][6] * QubitOperator(\"Y0 Y1\")\n", + "\n", + " # Use Scipy to perform the classical outerloop of the variational\n", + " # eigensolver, i.e., the minimization of the parameter theta.\n", + " # See documentation of Scipy for different optimizers.\n", + " minimum = minimize_scalar(lambda theta: variational_quantum_eigensolver(theta, hamiltonian))\n", + " lowest_energies.append(minimum.fun)\n", + "\n", + "# print result\n", + "plt.xlabel(\"Bond length [Angstrom]\")\n", + "plt.ylabel(\"Total Energy [Hartree]\")\n", + "plt.title(\"Variational Quantum Eigensolver\")\n", + "plt.plot(bond_distances, lowest_energies, \"b.-\",\n", + " label=\"Ground-state energy of molecular hydrogen\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 2", + "language": "python", + "name": "python2" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.15" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/projectq/_version.py b/projectq/_version.py index 02ac3660d..6900d1135 100755 --- a/projectq/_version.py +++ b/projectq/_version.py @@ -13,4 +13,4 @@ # limitations under the License. """Define version number here and read it from setup.py automatically""" -__version__ = "0.4.1" +__version__ = "0.5.0" diff --git a/projectq/backends/__init__.py b/projectq/backends/__init__.py index 6a3319779..4813a52b4 100755 --- a/projectq/backends/__init__.py +++ b/projectq/backends/__init__.py @@ -26,7 +26,7 @@ * an interface to the IBM Quantum Experience chip (and simulator). """ from ._printer import CommandPrinter -from ._circuits import CircuitDrawer +from ._circuits import CircuitDrawer, CircuitDrawerMatplotlib from ._sim import Simulator, ClassicalSimulator from ._resource import ResourceCounter from ._ibm import IBMBackend diff --git a/projectq/backends/_circuits/__init__.py b/projectq/backends/_circuits/__init__.py index 1f22faec4..be22d24d2 100755 --- a/projectq/backends/_circuits/__init__.py +++ b/projectq/backends/_circuits/__init__.py @@ -13,4 +13,8 @@ # limitations under the License. from ._to_latex import to_latex +from ._plot import to_draw + from ._drawer import CircuitDrawer +from ._drawer_matplotlib import CircuitDrawerMatplotlib + diff --git a/projectq/backends/_circuits/_drawer.py b/projectq/backends/_circuits/_drawer.py index 269f592a2..2562a07dd 100755 --- a/projectq/backends/_circuits/_drawer.py +++ b/projectq/backends/_circuits/_drawer.py @@ -11,13 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Contains a compiler engine which generates TikZ Latex code describing the circuit. """ -import sys - from builtins import input from projectq.cengines import LastEngineException, BasicEngine @@ -42,9 +39,9 @@ def __init__(self, gate, lines, ctrl_lines): self.id = -1 def __eq__(self, other): - return (self.gate == other.gate and self.lines == other.lines and - self.ctrl_lines == other.ctrl_lines and - self.id == other.id) + return (self.gate == other.gate and self.lines == other.lines + and self.ctrl_lines == other.ctrl_lines + and self.id == other.id) def __ne__(self, other): return not self.__eq__(other) @@ -153,6 +150,9 @@ def __init__(self, accept_input=False, default_measure=0): self._free_lines = [] self._map = dict() + # Order in which qubit lines are drawn + self._drawing_order = [] + def is_available(self, cmd): """ Specialized implementation of is_available: Returns True if the @@ -190,7 +190,7 @@ def set_qubit_locations(self, id_to_loc): raise RuntimeError("set_qubit_locations() has to be called before" " applying gates!") - for k in range(min(id_to_loc), max(id_to_loc)+1): + for k in range(min(id_to_loc), max(id_to_loc) + 1): if k not in id_to_loc: raise RuntimeError("set_qubit_locations(): Invalid id_to_loc " "mapping provided. All ids in the provided" @@ -221,12 +221,13 @@ def _print_cmd(self, cmd): self._free_lines.append(qubit_id) if self.is_last_engine and cmd.gate == Measure: - assert(get_control_count(cmd) == 0) + assert get_control_count(cmd) == 0 + for qureg in cmd.qubits: for qubit in qureg: if self._accept_input: m = None - while m != '0' and m != '1' and m != 1 and m != 0: + while m not in ('0', '1', 1, 0): prompt = ("Input measurement result (0 or 1) for " "qubit " + str(qubit) + ": ") m = input(prompt) @@ -244,7 +245,9 @@ def _print_cmd(self, cmd): for l in all_lines: self._qubit_lines[l].append(item) - def get_latex(self): + self._drawing_order.append(all_lines[0]) + + def get_latex(self, ordered=False, draw_gates_in_parallel=True): """ Return the latex document string representing the circuit. @@ -256,6 +259,12 @@ def get_latex(self): python3 my_circuit.py | pdflatex where my_circuit.py calls this function and prints it to the terminal. + + Args: + ordered(bool): flag if the gates should be drawn in the order they + were added to the circuit + draw_gates_in_parallel(bool): flag if parallel gates should be drawn + parallel (True), or not (False) """ qubit_lines = dict() @@ -271,10 +280,13 @@ def get_latex(self): new_cmd.id = cmd.lines[0] qubit_lines[new_line].append(new_cmd) - circuit = [] - for lines in qubit_lines: - circuit.append(qubit_lines[lines]) - return to_latex(qubit_lines) + drawing_order = None + if ordered: + drawing_order = self._drawing_order + + return to_latex(qubit_lines, + drawing_order=drawing_order, + draw_gates_in_parallel=draw_gates_in_parallel) def receive(self, command_list): """ diff --git a/projectq/backends/_circuits/_drawer_matplotlib.py b/projectq/backends/_circuits/_drawer_matplotlib.py new file mode 100644 index 000000000..1fd61df1f --- /dev/null +++ b/projectq/backends/_circuits/_drawer_matplotlib.py @@ -0,0 +1,232 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Contains a compiler engine which generates matplotlib figures describing the +circuit. +""" + +from builtins import input +import re +import itertools + +from projectq.cengines import LastEngineException, BasicEngine +from projectq.ops import (FlushGate, Measure, Allocate, Deallocate) +from projectq.meta import get_control_count +from projectq.backends._circuits import to_draw + +# ============================================================================== + + +def _format_gate_str(cmd): + param_str = '' + gate_name = str(cmd.gate) + if '(' in gate_name: + (gate_name, param_str) = re.search(r'(.+)\((.*)\)', gate_name).groups() + params = re.findall(r'([^,]+)', param_str) + params_str_list = [] + for param in params: + try: + params_str_list.append('{0:.2f}'.format(float(param))) + except ValueError: + if len(param) < 8: + params_str_list.append(param) + else: + params_str_list.append(param[:5] + '...') + + gate_name += '(' + ','.join(params_str_list) + ')' + return gate_name + + +# ============================================================================== + + +class CircuitDrawerMatplotlib(BasicEngine): + """ + CircuitDrawerMatplotlib is a compiler engine which using Matplotlib library + for drawing quantum circuits + """ + def __init__(self, accept_input=False, default_measure=0): + """ + Initialize a circuit drawing engine(mpl) + Args: + accept_input (bool): If accept_input is true, the printer queries + the user to input measurement results if the CircuitDrawerMPL + is the last engine. Otherwise, all measurements yield the + result default_measure (0 or 1). + default_measure (bool): Default value to use as measurement + results if accept_input is False and there is no underlying + backend to register real measurement results. + """ + BasicEngine.__init__(self) + self._accept_input = accept_input + self._default_measure = default_measure + self._map = dict() + self._qubit_lines = {} + + def is_available(self, cmd): + """ + Specialized implementation of is_available: Returns True if the + CircuitDrawerMatplotlib is the last engine + (since it can print any command). + + Args: + cmd (Command): Command for which to check availability (all + Commands can be printed). + + Returns: + availability (bool): True, unless the next engine cannot handle + the Command (if there is a next engine). + """ + try: + # Multi-qubit gates may fail at drawing time if the target qubits + # are not right next to each other on the output graphic. + return BasicEngine.is_available(self, cmd) + except LastEngineException: + return True + + def _process(self, cmd): + """ + Process the command cmd and stores it in the internal storage + + Queries the user for measurement input if a measurement command + arrives if accept_input was set to True. Otherwise, it uses the + default_measure parameter to register the measurement outcome. + + Args: + cmd (Command): Command to add to the circuit diagram. + """ + if cmd.gate == Allocate: + qubit_id = cmd.qubits[0][0].id + if qubit_id not in self._map: + self._map[qubit_id] = qubit_id + self._qubit_lines[qubit_id] = [] + return + + if cmd.gate == Deallocate: + return + + if self.is_last_engine and cmd.gate == Measure: + assert get_control_count(cmd) == 0 + for qureg in cmd.qubits: + for qubit in qureg: + if self._accept_input: + measurement = None + while measurement not in ('0', '1', 1, 0): + prompt = ("Input measurement result (0 or 1) for " + "qubit " + str(qubit) + ": ") + measurement = input(prompt) + else: + measurement = self._default_measure + self.main_engine.set_measurement_result( + qubit, int(measurement)) + + targets = [qubit.id for qureg in cmd.qubits for qubit in qureg] + controls = [qubit.id for qubit in cmd.control_qubits] + + ref_qubit_id = targets[0] + gate_str = _format_gate_str(cmd) + + # First find out what is the maximum index that this command might + # have + max_depth = max( + len(self._qubit_lines[qubit_id]) + for qubit_id in itertools.chain(targets, controls)) + + # If we have a multi-qubit gate, make sure that all the qubit axes + # have the same depth. We do that by recalculating the maximum index + # over all the known qubit axes. + # This is to avoid the possibility of a multi-qubit gate overlapping + # with some other gates. This could potentially be improved by only + # considering the qubit axes that are between the topmost and + # bottommost qubit axes of the current command. + if len(targets) + len(controls) > 1: + max_depth = max( + len(self._qubit_lines[qubit_id]) + for qubit_id in self._qubit_lines) + + for qubit_id in itertools.chain(targets, controls): + depth = len(self._qubit_lines[qubit_id]) + self._qubit_lines[qubit_id] += [None] * (max_depth - depth) + + if qubit_id == ref_qubit_id: + self._qubit_lines[qubit_id].append( + (gate_str, targets, controls)) + else: + self._qubit_lines[qubit_id].append(None) + + def receive(self, command_list): + """ + Receive a list of commands from the previous engine, print the + commands, and then send them on to the next engine. + + Args: + command_list (list): List of Commands to print (and + potentially send on to the next engine). + """ + for cmd in command_list: + if not isinstance(cmd.gate, FlushGate): + self._process(cmd) + + if not self.is_last_engine: + self.send([cmd]) + + def draw(self, qubit_labels=None, drawing_order=None, **kwargs): + """ + Generates and returns the plot of the quantum circuit stored so far + + Args: + qubit_labels (dict): label for each wire in the output figure. + Keys: qubit IDs, Values: string to print out as label for + that particular qubit wire. + drawing_order (dict): position of each qubit in the output + graphic. Keys: qubit IDs, Values: position of qubit on the + qubit line in the graphic. + **kwargs (dict): additional parameters are used to update + the default plot parameters + + Returns: + A tuple containing the matplotlib figure and axes objects + + Note: + Additional keyword arguments can be passed to this + function in order to further customize the figure output + by matplotlib (default value in parentheses): + + - fontsize (14): Font size in pt + - column_spacing (.5): Vertical spacing between two + neighbouring gates (roughly in inches) + - control_radius (.015): Radius of the circle for controls + - labels_margin (1): Margin between labels and begin of + wire (roughly in inches) + - linewidth (1): Width of line + - not_radius (.03): Radius of the circle for X/NOT gates + - gate_offset (.05): Inner margins for gates with a text + representation + - mgate_width (.1): Width of the measurement gate + - swap_delta (.02): Half-size of the SWAP gate + - x_offset (.05): Absolute X-offset for drawing within the axes + - wire_height (1): Vertical spacing between two qubit + wires (roughly in inches) + """ + max_depth = max( + len(self._qubit_lines[qubit_id]) for qubit_id in self._qubit_lines) + for qubit_id in self._qubit_lines: + depth = len(self._qubit_lines[qubit_id]) + if depth < max_depth: + self._qubit_lines[qubit_id] += [None] * (max_depth - depth) + + return to_draw(self._qubit_lines, + qubit_labels=qubit_labels, + drawing_order=drawing_order, + **kwargs) diff --git a/projectq/backends/_circuits/_drawer_matplotlib_test.py b/projectq/backends/_circuits/_drawer_matplotlib_test.py new file mode 100644 index 000000000..a76fbc99b --- /dev/null +++ b/projectq/backends/_circuits/_drawer_matplotlib_test.py @@ -0,0 +1,148 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Tests for projectq.backends.circuits._drawer.py. +""" + +import pytest +from projectq import MainEngine +from projectq.cengines import DummyEngine +from projectq.ops import (H, X, Rx, CNOT, Swap, Measure, Command, BasicGate) +from projectq.types import WeakQubitRef + +from . import _drawer_matplotlib as _drawer +from ._drawer_matplotlib import CircuitDrawerMatplotlib + + +def test_drawer_measurement(): + drawer = CircuitDrawerMatplotlib(default_measure=0) + eng = MainEngine(drawer, []) + qubit = eng.allocate_qubit() + Measure | qubit + assert int(qubit) == 0 + + drawer = CircuitDrawerMatplotlib(default_measure=1) + eng = MainEngine(drawer, []) + qubit = eng.allocate_qubit() + Measure | qubit + assert int(qubit) == 1 + + drawer = CircuitDrawerMatplotlib(accept_input=True) + eng = MainEngine(drawer, []) + qubit = eng.allocate_qubit() + + old_input = _drawer.input + + _drawer.input = lambda x: '1' + Measure | qubit + assert int(qubit) == 1 + _drawer.input = old_input + + +class MockEngine(object): + def is_available(self, cmd): + self.cmd = cmd + self.called = True + return False + + +def test_drawer_isavailable(): + drawer = CircuitDrawerMatplotlib() + drawer.is_last_engine = True + + qb0 = WeakQubitRef(None, 0) + qb1 = WeakQubitRef(None, 1) + qb2 = WeakQubitRef(None, 2) + qb3 = WeakQubitRef(None, 3) + + for gate in (X, Rx(1.0)): + for qubits in (([qb0], ), ([qb0, qb1], ), ([qb0, qb1, qb2], )): + print(qubits) + cmd = Command(None, gate, qubits) + assert drawer.is_available(cmd) + + cmd0 = Command(None, X, ([qb0], )) + cmd1 = Command(None, Swap, ([qb0], [qb1])) + cmd2 = Command(None, Swap, ([qb0], [qb1]), [qb2]) + cmd3 = Command(None, Swap, ([qb0], [qb1]), [qb2, qb3]) + + assert drawer.is_available(cmd1) + assert drawer.is_available(cmd2) + assert drawer.is_available(cmd3) + + mock_engine = MockEngine() + mock_engine.called = False + drawer.is_last_engine = False + drawer.next_engine = mock_engine + + assert not drawer.is_available(cmd0) + assert mock_engine.called + assert mock_engine.cmd is cmd0 + + assert not drawer.is_available(cmd1) + assert mock_engine.called + assert mock_engine.cmd is cmd1 + + +def _draw_subst(qubit_lines, qubit_labels=None, drawing_order=None, **kwargs): + return qubit_lines + + +class MyGate(BasicGate): + def __init__(self, *args): + BasicGate.__init__(self) + self.params = args + + def __str__(self): + param_str = '{}'.format(self.params[0]) + for param in self.params[1:]: + param_str += ',{}'.format(param) + return str(self.__class__.__name__) + "(" + param_str + ")" + + +def test_drawer_draw(): + old_draw = _drawer.to_draw + _drawer.to_draw = _draw_subst + + backend = DummyEngine() + + drawer = CircuitDrawerMatplotlib() + + eng = MainEngine(backend, [drawer]) + qureg = eng.allocate_qureg(3) + H | qureg[1] + H | qureg[0] + X | qureg[0] + Rx(1) | qureg[1] + CNOT | (qureg[0], qureg[1]) + Swap | (qureg[0], qureg[1]) + MyGate(1.2) | qureg[2] + MyGate(1.23456789) | qureg[2] + MyGate(1.23456789, 2.3456789) | qureg[2] + MyGate(1.23456789, 'aaaaaaaa', 'bbb', 2.34) | qureg[2] + X | qureg[0] + + qubit_lines = drawer.draw() + + assert qubit_lines == { + 0: [('H', [0], []), ('X', [0], []), None, ('Swap', [0, 1], []), + ('X', [0], [])], + 1: [('H', [1], []), ('Rx(1.00)', [1], []), ('X', [1], [0]), None, + None], + 2: [('MyGate(1.20)', [2], []), ('MyGate(1.23)', [2], []), + ('MyGate(1.23,2.35)', [2], []), + ('MyGate(1.23,aaaaa...,bbb,2.34)', [2], []), None] + } + + _drawer.to_draw = old_draw diff --git a/projectq/backends/_circuits/_drawer_test.py b/projectq/backends/_circuits/_drawer_test.py index 7df4bd0ee..b73513b64 100755 --- a/projectq/backends/_circuits/_drawer_test.py +++ b/projectq/backends/_circuits/_drawer_test.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Tests for projectq.backends.circuits._drawer.py. """ @@ -20,19 +19,17 @@ from projectq import MainEngine from projectq.cengines import LastEngineException -from projectq.ops import (H, - X, - CNOT, - Measure) +from projectq.ops import (H, X, CNOT, Measure) from projectq.meta import Control import projectq.backends._circuits._drawer as _drawer from projectq.backends._circuits._drawer import CircuitItem, CircuitDrawer -def test_drawer_getlatex(): +@pytest.mark.parametrize("ordered", [False, True]) +def test_drawer_getlatex(ordered): old_latex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x drawer = CircuitDrawer() drawer.set_qubit_locations({0: 1, 1: 0}) @@ -46,13 +43,13 @@ def test_drawer_getlatex(): X | qureg[0] CNOT | (qureg[0], qureg[1]) - lines = drawer2.get_latex() + lines = drawer2.get_latex(ordered=ordered) assert len(lines) == 2 assert len(lines[0]) == 4 assert len(lines[1]) == 3 # check if it was sent on correctly: - lines = drawer.get_latex() + lines = drawer.get_latex(ordered=ordered) assert len(lines) == 2 assert len(lines[0]) == 3 assert len(lines[1]) == 4 diff --git a/projectq/backends/_circuits/_plot.py b/projectq/backends/_circuits/_plot.py new file mode 100644 index 000000000..f972f605b --- /dev/null +++ b/projectq/backends/_circuits/_plot.py @@ -0,0 +1,630 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module provides the basic functionality required to plot a quantum +circuit in a matplotlib figure. +It is mainly used by the CircuitDrawerMatplotlib compiler engine. + +Currently, it supports all single-qubit gates, including their controlled +versions to an arbitrary number of control qubits. It also supports +multi-target qubit gates under some restrictions. Namely that the target +qubits must be neighbours in the output figure (which cannot be determined +durinng compilation at this time). +""" + +from copy import deepcopy +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import PatchCollection, LineCollection +from matplotlib.lines import Line2D +from matplotlib.patches import Circle, Arc, Rectangle + +# Important note on units for the plot parameters. +# The following entries are in inches: +# - column_spacing +# - labels_margin +# - wire_height +# +# The following entries are in data units (matplotlib) +# - control_radius +# - gate_offset +# - mgate_width +# - not_radius +# - swap_delta +# - x_offset +# +# The rest have misc. units (as defined by matplotlib) +_DEFAULT_PLOT_PARAMS = dict(fontsize=14.0, + column_spacing=.5, + control_radius=0.015, + labels_margin=1, + linewidth=1.0, + not_radius=0.03, + gate_offset=.05, + mgate_width=0.1, + swap_delta=0.02, + x_offset=.05, + wire_height=1) + +# ============================================================================== + + +def to_draw(qubit_lines, qubit_labels=None, drawing_order=None, **kwargs): + """ + Translates a given circuit to a matplotlib figure. + + Args: + qubit_lines (dict): list of gates for each qubit axis + qubit_labels (dict): label to print in front of the qubit wire for + each qubit ID + drawing_order (dict): index of the wire for each qubit ID to be drawn. + **kwargs (dict): additional parameters are used to update the default + plot parameters + + Returns: + A tuple with (figure, axes) + + Note: + Numbering of qubit wires starts at 0 at the bottom and increases + vertically. + + Note: + Additional keyword arguments can be passed to this + function in order to further customize the figure output + by matplotlib (default value in parentheses): + + - fontsize (14): Font size in pt + - column_spacing (.5): Vertical spacing between two + neighbouring gates (roughly in inches) + - control_radius (.015): Radius of the circle for controls + - labels_margin (1): Margin between labels and begin of + wire (roughly in inches) + - linewidth (1): Width of line + - not_radius (.03): Radius of the circle for X/NOT gates + - gate_offset (.05): Inner margins for gates with a text + representation + - mgate_width (.1): Width of the measurement gate + - swap_delta (.02): Half-size of the SWAP gate + - x_offset (.05): Absolute X-offset for drawing within the axes + - wire_height (1): Vertical spacing between two qubit + wires (roughly in inches) + """ + if qubit_labels is None: + qubit_labels = {qubit_id: r'$|0\rangle$' for qubit_id in qubit_lines} + else: + if list(qubit_labels) != list(qubit_lines): + raise RuntimeError('Qubit IDs in qubit_labels do not match ' + + 'qubit IDs in qubit_lines!') + + if drawing_order is None: + n_qubits = len(qubit_lines) + drawing_order = { + qubit_id: n_qubits - qubit_id - 1 + for qubit_id in list(qubit_lines) + } + else: + if list(drawing_order) != list(qubit_lines): + raise RuntimeError('Qubit IDs in drawing_order do not match ' + + 'qubit IDs in qubit_lines!') + if (list(sorted(drawing_order.values())) != list( + range(len(drawing_order)))): + raise RuntimeError( + 'Indices of qubit wires in drawing_order ' + + 'must be between 0 and {}!'.format(len(drawing_order))) + + plot_params = deepcopy(_DEFAULT_PLOT_PARAMS) + plot_params.update(kwargs) + + n_labels = len(list(qubit_lines)) + + wire_height = plot_params['wire_height'] + # Grid in inches + wire_grid = np.arange(wire_height, (n_labels + 1) * wire_height, + wire_height, + dtype=float) + + fig, axes = create_figure(plot_params) + + # Grid in inches + gate_grid = calculate_gate_grid(axes, qubit_lines, plot_params) + + width = gate_grid[-1] + plot_params['column_spacing'] + height = wire_grid[-1] + wire_height + + resize_figure(fig, axes, width, height, plot_params) + + # Convert grids into data coordinates + units_per_inch = plot_params['units_per_inch'] + + gate_grid *= units_per_inch + gate_grid = gate_grid + plot_params['x_offset'] + wire_grid *= units_per_inch + plot_params['column_spacing'] *= units_per_inch + + draw_wires(axes, n_labels, gate_grid, wire_grid, plot_params) + + draw_labels(axes, qubit_labels, drawing_order, wire_grid, plot_params) + + draw_gates(axes, qubit_lines, drawing_order, gate_grid, wire_grid, + plot_params) + return fig, axes + + +# ============================================================================== +# Functions used to calculate the layout + + +def gate_width(axes, gate_str, plot_params): + """ + Calculate the width of a gate based on its string representation. + + Args: + axes (matplotlib.axes.Axes): axes object + gate_str (str): string representation of a gate + plot_params (dict): plot parameters + + Returns: + The width of a gate on the figure (in inches) + """ + if gate_str == 'X': + return 2 * plot_params['not_radius'] / plot_params['units_per_inch'] + if gate_str == 'Swap': + return 2 * plot_params['swap_delta'] / plot_params['units_per_inch'] + + if gate_str == 'Measure': + return plot_params['mgate_width'] + + obj = axes.text(0, + 0, + gate_str, + visible=True, + bbox=dict(edgecolor='k', facecolor='w', fill=True, lw=1.0), + fontsize=14) + obj.figure.canvas.draw() + width = (obj.get_window_extent(obj.figure.canvas.get_renderer()).width + / axes.figure.dpi) + obj.remove() + return width + 2 * plot_params['gate_offset'] + + +def calculate_gate_grid(axes, qubit_lines, plot_params): + """ + Calculate an optimal grid spacing for a list of quantum gates. + + Args: + axes (matplotlib.axes.Axes): axes object + qubit_lines (dict): list of gates for each qubit axis + plot_params (dict): plot parameters + + Returns: + An array (np.ndarray) with the gate x positions. + """ + # NB: column_spacing is still in inch when this function is called + column_spacing = plot_params['column_spacing'] + data = list(qubit_lines.values()) + depth = len(data[0]) + + width_list = [ + max( + gate_width(axes, line[idx][0], plot_params) if line[idx] else 0 + for line in data) for idx in range(depth) + ] + + gate_grid = np.array([0] * (depth + 1), dtype=float) + + gate_grid[0] = plot_params['labels_margin'] + if depth > 0: + gate_grid[0] += width_list[0] * 0.5 + for idx in range(1, depth): + gate_grid[idx] = gate_grid[idx - 1] + column_spacing + ( + width_list[idx] + width_list[idx - 1]) * 0.5 + gate_grid[-1] = gate_grid[-2] + column_spacing + width_list[-1] * 0.5 + return gate_grid + + +# ============================================================================== +# Basic helper functions + + +def text(axes, gate_pos, wire_pos, textstr, plot_params): + """ + Draws a text box on the figure. + + Args: + axes (matplotlib.axes.Axes): axes object + gate_pos (float): x coordinate of the gate [data units] + wire_pos (float): y coordinate of the qubit wire + textstr (str): text of the gate and box + plot_params (dict): plot parameters + box (bool): draw the rectangle box if box is True + """ + return axes.text(gate_pos, + wire_pos, + textstr, + color='k', + ha='center', + va='center', + clip_on=True, + size=plot_params['fontsize']) + + +# ============================================================================== + + +def create_figure(plot_params): + """ + Create a new figure as well as a new axes instance + + Args: + plot_params (dict): plot parameters + + Returns: + A tuple with (figure, axes) + """ + fig = plt.figure(facecolor='w', edgecolor='w') + axes = plt.axes() + axes.set_axis_off() + axes.set_aspect('equal') + plot_params['units_per_inch'] = fig.dpi / axes.get_window_extent().width + return fig, axes + + +def resize_figure(fig, axes, width, height, plot_params): + """ + Resizes a figure and adjust the limits of the axes instance to make sure + that the distances in data coordinates on the screen stay constant. + + Args: + fig (matplotlib.figure.Figure): figure object + axes (matplotlib.axes.Axes): axes object + width (float): new figure width + height (float): new figure height + plot_params (dict): plot parameters + + Returns: + A tuple with (figure, axes) + """ + fig.set_size_inches(width, height) + + new_limits = plot_params['units_per_inch'] * np.array([width, height]) + axes.set_xlim(0, new_limits[0]) + axes.set_ylim(0, new_limits[1]) + + +def draw_gates(axes, qubit_lines, drawing_order, gate_grid, wire_grid, + plot_params): + """ + Draws the gates. + + Args: + qubit_lines (dict): list of gates for each qubit axis + drawing_order (dict): index of the wire for each qubit ID to be drawn + gate_grid (np.ndarray): x positions of the gates + wire_grid (np.ndarray): y positions of the qubit wires + plot_params (dict): plot parameters + + Returns: + A tuple with (figure, axes) + """ + for qubit_line in qubit_lines.values(): + for idx, data in enumerate(qubit_line): + if data is not None: + (gate_str, targets, controls) = data + targets_order = [drawing_order[tgt] for tgt in targets] + draw_gate( + axes, gate_str, gate_grid[idx], + [wire_grid[tgt] for tgt in targets_order], targets_order, + [wire_grid[drawing_order[ctrl]] + for ctrl in controls], plot_params) + + +def draw_gate(axes, gate_str, gate_pos, target_wires, targets_order, + control_wires, plot_params): + """ + Draws a single gate at a given location. + + Args: + axes (AxesSubplot): axes object + gate_str (str): string representation of a gate + gate_pos (float): x coordinate of the gate [data units] + target_wires (list): y coordinates of the target qubits + targets_order (list): index of the wires corresponding to the target + qubit IDs + control_wires (list): y coordinates of the control qubits + plot_params (dict): plot parameters + + Returns: + A tuple with (figure, axes) + """ + # Special cases + if gate_str == 'Z' and len(control_wires) == 1: + draw_control_z_gate(axes, gate_pos, target_wires[0], control_wires[0], + plot_params) + elif gate_str == 'X': + draw_x_gate(axes, gate_pos, target_wires[0], plot_params) + elif gate_str == 'Swap': + draw_swap_gate(axes, gate_pos, target_wires[0], target_wires[1], + plot_params) + elif gate_str == 'Measure': + draw_measure_gate(axes, gate_pos, target_wires[0], plot_params) + else: + if len(target_wires) == 1: + draw_generic_gate(axes, gate_pos, target_wires[0], gate_str, + plot_params) + else: + if sorted(targets_order) != list( + range(min(targets_order), + max(targets_order) + 1)): + raise RuntimeError( + 'Multi-qubit gate with non-neighbouring qubits!\n' + + 'Gate: {} on wires {}'.format(gate_str, targets_order)) + + multi_qubit_gate(axes, gate_str, gate_pos, min(target_wires), + max(target_wires), plot_params) + + if not control_wires: + return + + for control_wire in control_wires: + axes.add_patch( + Circle((gate_pos, control_wire), + plot_params['control_radius'], + ec='k', + fc='k', + fill=True, + lw=plot_params['linewidth'])) + + all_wires = target_wires + control_wires + axes.add_line( + Line2D((gate_pos, gate_pos), (min(all_wires), max(all_wires)), + color='k', + lw=plot_params['linewidth'])) + + +def draw_generic_gate(axes, gate_pos, wire_pos, gate_str, plot_params): + """ + Draws a measurement gate. + + Args: + axes (AxesSubplot): axes object + gate_pos (float): x coordinate of the gate [data units] + wire_pos (float): y coordinate of the qubit wire + gate_str (str) : string representation of a gate + plot_params (dict): plot parameters + """ + obj = text(axes, gate_pos, wire_pos, gate_str, plot_params) + obj.set_zorder(7) + + factor = plot_params['units_per_inch'] / obj.figure.dpi + gate_offset = plot_params['gate_offset'] + + renderer = obj.figure.canvas.get_renderer() + width = obj.get_window_extent(renderer).width * factor + 2 * gate_offset + height = obj.get_window_extent(renderer).height * factor + 2 * gate_offset + + axes.add_patch( + Rectangle((gate_pos - width / 2, wire_pos - height / 2), + width, + height, + ec='k', + fc='w', + fill=True, + lw=plot_params['linewidth'], + zorder=6)) + + +def draw_measure_gate(axes, gate_pos, wire_pos, plot_params): + """ + Draws a measurement gate. + + Args: + axes (AxesSubplot): axes object + gate_pos (float): x coordinate of the gate [data units] + wire_pos (float): y coordinate of the qubit wire + plot_params (dict): plot parameters + """ + # pylint: disable=invalid-name + + width = plot_params['mgate_width'] + height = 0.9 * width + y_ref = wire_pos - 0.3 * height + + # Cannot use PatchCollection for the arc due to bug in matplotlib code... + arc = Arc((gate_pos, y_ref), + width * 0.7, + height * 0.8, + theta1=0, + theta2=180, + ec='k', + fc='w', + zorder=5) + axes.add_patch(arc) + + patches = [ + Rectangle((gate_pos - width / 2, wire_pos - height / 2), + width, + height, + fill=True), + Line2D((gate_pos, gate_pos + width * 0.35), + (y_ref, wire_pos + height * 0.35), + color='k', + linewidth=1) + ] + + gate = PatchCollection(patches, + edgecolors='k', + facecolors='w', + linewidths=plot_params['linewidth'], + zorder=5) + gate.set_label('Measure') + axes.add_collection(gate) + + +def multi_qubit_gate(axes, gate_str, gate_pos, wire_pos_min, wire_pos_max, + plot_params): + """ + Draws a multi-target qubit gate. + + Args: + axes (matplotlib.axes.Axes): axes object + gate_str (str): string representation of a gate + gate_pos (float): x coordinate of the gate [data units] + wire_pos_min (float): y coordinate of the lowest qubit wire + wire_pos_max (float): y coordinate of the highest qubit wire + plot_params (dict): plot parameters + """ + gate_offset = plot_params['gate_offset'] + y_center = (wire_pos_max - wire_pos_min) / 2 + wire_pos_min + obj = axes.text(gate_pos, + y_center, + gate_str, + color='k', + ha='center', + va='center', + size=plot_params['fontsize'], + zorder=7) + height = wire_pos_max - wire_pos_min + 2 * gate_offset + inv = axes.transData.inverted() + width = inv.transform_bbox( + obj.get_window_extent(obj.figure.canvas.get_renderer())).width + return axes.add_patch( + Rectangle((gate_pos - width / 2, wire_pos_min - gate_offset), + width, + height, + edgecolor='k', + facecolor='w', + fill=True, + lw=plot_params['linewidth'], + zorder=6)) + + +def draw_x_gate(axes, gate_pos, wire_pos, plot_params): + """ + Draws the symbol for a X/NOT gate. + + Args: + axes (matplotlib.axes.Axes): axes object + gate_pos (float): x coordinate of the gate [data units] + wire_pos (float): y coordinate of the qubit wire [data units] + plot_params (dict): plot parameters + """ + not_radius = plot_params['not_radius'] + + gate = PatchCollection([ + Circle((gate_pos, wire_pos), not_radius, fill=False), + Line2D((gate_pos, gate_pos), + (wire_pos - not_radius, wire_pos + not_radius)) + ], + edgecolors='k', + facecolors='w', + linewidths=plot_params['linewidth']) + gate.set_label('NOT') + axes.add_collection(gate) + + +def draw_control_z_gate(axes, gate_pos, wire_pos1, wire_pos2, plot_params): + """ + Draws the symbol for a controlled-Z gate. + + Args: + axes (matplotlib.axes.Axes): axes object + wire_pos (float): x coordinate of the gate [data units] + y1 (float): y coordinate of the 1st qubit wire + y2 (float): y coordinate of the 2nd qubit wire + plot_params (dict): plot parameters + """ + gate = PatchCollection([ + Circle( + (gate_pos, wire_pos1), plot_params['control_radius'], fill=True), + Circle( + (gate_pos, wire_pos2), plot_params['control_radius'], fill=True), + Line2D((gate_pos, gate_pos), (wire_pos1, wire_pos2)) + ], + edgecolors='k', + facecolors='k', + linewidths=plot_params['linewidth']) + gate.set_label('CZ') + axes.add_collection(gate) + + +def draw_swap_gate(axes, gate_pos, wire_pos1, wire_pos2, plot_params): + """ + Draws the symbol for a SWAP gate. + + Args: + axes (matplotlib.axes.Axes): axes object + x (float): x coordinate [data units] + y1 (float): y coordinate of the 1st qubit wire + y2 (float): y coordinate of the 2nd qubit wire + plot_params (dict): plot parameters + """ + delta = plot_params['swap_delta'] + + lines = [] + for wire_pos in (wire_pos1, wire_pos2): + lines.append([(gate_pos - delta, wire_pos - delta), + (gate_pos + delta, wire_pos + delta)]) + lines.append([(gate_pos - delta, wire_pos + delta), + (gate_pos + delta, wire_pos - delta)]) + lines.append([(gate_pos, wire_pos1), (gate_pos, wire_pos2)]) + + gate = LineCollection(lines, + colors='k', + linewidths=plot_params['linewidth']) + gate.set_label('SWAP') + axes.add_collection(gate) + + +def draw_wires(axes, n_labels, gate_grid, wire_grid, plot_params): + """ + Draws all the circuit qubit wires. + + Args: + axes (matplotlib.axes.Axes): axes object + n_labels (int): number of qubit + gate_grid (ndarray): array with the ref. x positions of the gates + wire_grid (ndarray): array with the ref. y positions of the qubit + wires + plot_params (dict): plot parameters + """ + # pylint: disable=invalid-name + + lines = [] + for i in range(n_labels): + lines.append(((gate_grid[0] - plot_params['column_spacing'], + wire_grid[i]), (gate_grid[-1], wire_grid[i]))) + all_lines = LineCollection(lines, + linewidths=plot_params['linewidth'], + edgecolor='k') + all_lines.set_label('qubit_wires') + axes.add_collection(all_lines) + + +def draw_labels(axes, qubit_labels, drawing_order, wire_grid, plot_params): + """ + Draws the labels at the start of each qubit wire + + Args: + axes (matplotlib.axes.Axes): axes object + qubit_labels (list): labels of the qubit to be drawn + drawing_order (dict): Mapping between wire indices and qubit IDs + gate_grid (ndarray): array with the ref. x positions of the gates + wire_grid (ndarray): array with the ref. y positions of the qubit + wires + plot_params (dict): plot parameters + """ + for qubit_id in qubit_labels: + wire_idx = drawing_order[qubit_id] + text(axes, plot_params['x_offset'], wire_grid[wire_idx], + qubit_labels[qubit_id], plot_params) diff --git a/projectq/backends/_circuits/_plot_test.py b/projectq/backends/_circuits/_plot_test.py new file mode 100644 index 000000000..cd5d3ab0f --- /dev/null +++ b/projectq/backends/_circuits/_plot_test.py @@ -0,0 +1,289 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + Tests for projectq.backends._circuits._plot.py. + + To generate the baseline images, + run the tests with '--mpl-generate-path=baseline' + + Then run the tests simply with '--mpl' +""" +import pytest +from copy import deepcopy +import projectq.backends._circuits._plot as _plot + +# ============================================================================== + + +class PseudoCanvas(object): + def __init__(self): + pass + + def draw(self): + pass + + def get_renderer(self): + return + + +class PseudoFigure(object): + def __init__(self): + self.canvas = PseudoCanvas() + self.dpi = 1 + + +class PseudoBBox(object): + def __init__(self, width, height): + self.width = width + self.height = height + + +class PseudoText(object): + def __init__(self, text): + self.text = text + self.figure = PseudoFigure() + + def get_window_extent(self, *args): + return PseudoBBox(len(self.text), 1) + + def remove(self): + pass + + +class PseudoTransform(object): + def __init__(self): + pass + + def inverted(self): + return self + + def transform_bbox(self, bbox): + return bbox + + +class PseudoAxes(object): + def __init__(self): + self.figure = PseudoFigure() + self.transData = PseudoTransform() + + def add_patch(self, x): + return x + + def text(self, x, y, text, *args, **kwargse): + return PseudoText(text) + + +# ============================================================================== + + +@pytest.fixture(scope="module") +def plot_params(): + params = deepcopy(_plot._DEFAULT_PLOT_PARAMS) + params.update([('units_per_inch', 1)]) + return params + + +@pytest.fixture +def axes(): + return PseudoAxes() + + +# ============================================================================== + + +@pytest.mark.parametrize('gate_str', ['X', 'Swap', 'Measure', 'Y', 'Rz(1.00)']) +def test_gate_width(axes, gate_str, plot_params): + width = _plot.gate_width(axes, gate_str, plot_params) + if gate_str == 'X': + assert width == 2 * plot_params['not_radius'] / plot_params[ + 'units_per_inch'] + elif gate_str == 'Swap': + assert width == 2 * plot_params['swap_delta'] / plot_params[ + 'units_per_inch'] + elif gate_str == 'Measure': + assert width == plot_params['mgate_width'] + else: + assert width == len(gate_str) + 2 * plot_params['gate_offset'] + + +def test_calculate_gate_grid(axes, plot_params): + qubit_lines = { + 0: [('X', [0], []), ('X', [0], []), ('X', [0], []), ('X', [0], [])] + } + + gate_grid = _plot.calculate_gate_grid(axes, qubit_lines, plot_params) + assert len(gate_grid) == 5 + assert gate_grid[0] > plot_params['labels_margin'] + width = [gate_grid[i + 1] - gate_grid[i] for i in range(4)] + + # Column grid is given by: + # |---*---|---*---|---*---|---*---| + # |-- w --|-- w --|-- w --|.5w| + + column_spacing = plot_params['column_spacing'] + ref_width = _plot.gate_width(axes, 'X', plot_params) + + for w in width[:-1]: + assert ref_width + column_spacing == pytest.approx(w) + assert 0.5 * ref_width + column_spacing == pytest.approx(width[-1]) + + +def test_create_figure(plot_params): + fig, axes = _plot.create_figure(plot_params) + + +def test_draw_single_gate(axes, plot_params): + with pytest.raises(RuntimeError): + _plot.draw_gate(axes, 'MyGate', 2, [0, 0, 0], [0, 1, 3], [], + plot_params) + _plot.draw_gate(axes, 'MyGate', 2, [0, 0, 0], [0, 1, 2], [], plot_params) + + +def test_draw_simple(plot_params): + qubit_lines = { + 0: [('X', [0], []), ('Z', [0], []), ('Z', [0], [1]), + ('Swap', [0, 1], []), ('Measure', [0], [])], + 1: [None, None, None, None, None] + } + fig, axes = _plot.to_draw(qubit_lines) + + units_per_inch = plot_params['units_per_inch'] + not_radius = plot_params['not_radius'] + control_radius = plot_params['control_radius'] + swap_delta = plot_params['swap_delta'] + wire_height = plot_params['wire_height'] * units_per_inch + mgate_width = plot_params['mgate_width'] + + labels = [] + text_gates = [] + measure_gates = [] + for text in axes.texts: + if text.get_text() == '$|0\\rangle$': + labels.append(text) + elif text.get_text() == ' ': + measure_gates.append(text) + else: + text_gates.append(text) + + assert all( + label.get_position()[0] == pytest.approx(plot_params['x_offset']) + for label in labels) + assert (abs(labels[1].get_position()[1] + - labels[0].get_position()[1]) == pytest.approx(wire_height)) + + # X gate + x_gate = [obj for obj in axes.collections if obj.get_label() == 'NOT'][0] + # find the filled circles + assert (x_gate.get_paths()[0].get_extents().width == pytest.approx( + 2 * not_radius)) + assert (x_gate.get_paths()[0].get_extents().height == pytest.approx( + 2 * not_radius)) + # find the vertical bar + x_vertical = x_gate.get_paths()[1] + assert len(x_vertical) == 2 + assert x_vertical.get_extents().width == 0. + assert (x_vertical.get_extents().height == pytest.approx( + 2 * plot_params['not_radius'])) + + # Z gate + assert len(text_gates) == 1 + assert text_gates[0].get_text() == 'Z' + assert text_gates[0].get_position()[1] == pytest.approx(2 * wire_height) + + # CZ gate + cz_gate = [obj for obj in axes.collections if obj.get_label() == 'CZ'][0] + # find the filled circles + for control in cz_gate.get_paths()[:-1]: + assert control.get_extents().width == pytest.approx(2 * control_radius) + assert control.get_extents().height == pytest.approx(2 + * control_radius) + # find the vertical bar + cz_vertical = cz_gate.get_paths()[-1] + assert len(cz_vertical) == 2 + assert cz_vertical.get_extents().width == 0. + assert (cz_vertical.get_extents().height == pytest.approx(wire_height)) + + # Swap gate + swap_gate = [obj for obj in axes.collections + if obj.get_label() == 'SWAP'][0] + # find the filled circles + for qubit in swap_gate.get_paths()[:-1]: + assert qubit.get_extents().width == pytest.approx(2 * swap_delta) + assert qubit.get_extents().height == pytest.approx(2 * swap_delta) + # find the vertical bar + swap_vertical = swap_gate.get_paths()[-1] + assert len(swap_vertical) == 2 + assert swap_vertical.get_extents().width == 0. + assert (swap_vertical.get_extents().height == pytest.approx(wire_height)) + + # Measure gate + measure_gate = [ + obj for obj in axes.collections if obj.get_label() == 'Measure' + ][0] + + assert (measure_gate.get_paths()[0].get_extents().width == pytest.approx( + mgate_width)) + assert (measure_gate.get_paths()[0].get_extents().height == pytest.approx( + 0.9 * mgate_width)) + + +def test_draw_advanced(plot_params): + qubit_lines = {0: [('X', [0], []), ('Measure', [0], [])], 1: [None, None]} + + with pytest.raises(RuntimeError): + _plot.to_draw(qubit_lines, qubit_labels={1: 'qb1', 2: 'qb2'}) + + with pytest.raises(RuntimeError): + _plot.to_draw(qubit_lines, drawing_order={0: 0, 1: 2}) + + with pytest.raises(RuntimeError): + _plot.to_draw(qubit_lines, drawing_order={1: 1, 2: 0}) + + # -------------------------------------------------------------------------- + + _, axes = _plot.to_draw(qubit_lines) + for text in axes.texts: + assert text.get_text() == r'$|0\rangle$' + + # NB numbering of wire starts from bottom. + _, axes = _plot.to_draw(qubit_lines, + qubit_labels={ + 0: 'qb0', + 1: 'qb1' + }, + drawing_order={ + 0: 0, + 1: 1 + }) + assert ([axes.texts[qubit_id].get_text() + for qubit_id in range(2)] == ['qb0', 'qb1']) + + positions = [axes.texts[qubit_id].get_position() for qubit_id in range(2)] + assert positions[1][1] > positions[0][1] + + _, axes = _plot.to_draw(qubit_lines, + qubit_labels={ + 0: 'qb2', + 1: 'qb3' + }, + drawing_order={ + 0: 1, + 1: 0 + }) + + assert ([axes.texts[qubit_id].get_text() + for qubit_id in range(2)] == ['qb2', 'qb3']) + + positions = [axes.texts[qubit_id].get_position() for qubit_id in range(2)] + assert positions[1][1] < positions[0][1] diff --git a/projectq/backends/_circuits/_to_latex.py b/projectq/backends/_circuits/_to_latex.py index 5cbe89cae..385f3d3f3 100755 --- a/projectq/backends/_circuits/_to_latex.py +++ b/projectq/backends/_circuits/_to_latex.py @@ -17,7 +17,7 @@ Measure, SqrtSwap, Swap, X, Z) -def to_latex(circuit): +def to_latex(circuit, drawing_order=None, draw_gates_in_parallel=True): """ Translates a given circuit to a TikZ picture in a Latex document. @@ -38,8 +38,12 @@ class name string as a key. Every gate can have its own width, height, pre function, and written using write_settings(). Args: - circuit (list>): Each qubit line is a list of + circuit (list): Each qubit line is a list of CircuitItem objects, i.e., in circuit[line]. + drawing_order (list): A list of qubit lines from which + the gates to be read from + draw_gates_in_parallel (bool): If gates should (False) + or not (True) be parallel in the circuit Returns: tex_doc_str (string): Latex document string which can be compiled @@ -57,7 +61,10 @@ class name string as a key. Every gate can have its own width, height, pre settings = write_settings(get_default_settings()) text = _header(settings) - text += _body(circuit, settings) + text += _body(circuit, + settings, + drawing_order, + draw_gates_in_parallel=draw_gates_in_parallel) text += _footer(settings) return text @@ -83,39 +90,88 @@ def get_default_settings(): """ settings = dict() settings['gate_shadow'] = True - settings['lines'] = ({'style': 'very thin', 'double_classical': True, - 'init_quantum': True, 'double_lines_sep': .04}) - settings['gates'] = ({'HGate': {'width': .5, 'offset': .3, - 'pre_offset': .1}, - 'XGate': {'width': .35, 'height': .35, - 'offset': .1}, - 'SqrtXGate': {'width': .7, 'offset': .3, - 'pre_offset': .1}, - 'SwapGate': {'width': .35, 'height': .35, - 'offset': .1}, - 'SqrtSwapGate': {'width': .35, 'height': .35, - 'offset': .1}, - 'Rx': {'width': 1., 'height': .8, 'pre_offset': .2, - 'offset': .3}, - 'Ry': {'width': 1., 'height': .8, 'pre_offset': .2, - 'offset': .3}, - 'Rz': {'width': 1., 'height': .8, 'pre_offset': .2, - 'offset': .3}, - 'Ph': {'width': 1., 'height': .8, 'pre_offset': .2, - 'offset': .3}, - 'EntangleGate': {'width': 1.8, 'offset': .2, - 'pre_offset': .2}, - 'DeallocateQubitGate': {'height': .15, 'offset': .2, - 'width': .2, - 'pre_offset': .1}, - 'AllocateQubitGate': {'height': .15, 'width': .2, - 'offset': .1, - 'pre_offset': .1, - 'draw_id': False, - 'allocate_at_zero': False}, - 'MeasureGate': {'width': 0.75, 'offset': .2, - 'height': .5, 'pre_offset': .2} - }) + settings['lines'] = ({ + 'style': 'very thin', + 'double_classical': True, + 'init_quantum': True, + 'double_lines_sep': .04 + }) + settings['gates'] = ({ + 'HGate': { + 'width': .5, + 'offset': .3, + 'pre_offset': .1 + }, + 'XGate': { + 'width': .35, + 'height': .35, + 'offset': .1 + }, + 'SqrtXGate': { + 'width': .7, + 'offset': .3, + 'pre_offset': .1 + }, + 'SwapGate': { + 'width': .35, + 'height': .35, + 'offset': .1 + }, + 'SqrtSwapGate': { + 'width': .35, + 'height': .35, + 'offset': .1 + }, + 'Rx': { + 'width': 1., + 'height': .8, + 'pre_offset': .2, + 'offset': .3 + }, + 'Ry': { + 'width': 1., + 'height': .8, + 'pre_offset': .2, + 'offset': .3 + }, + 'Rz': { + 'width': 1., + 'height': .8, + 'pre_offset': .2, + 'offset': .3 + }, + 'Ph': { + 'width': 1., + 'height': .8, + 'pre_offset': .2, + 'offset': .3 + }, + 'EntangleGate': { + 'width': 1.8, + 'offset': .2, + 'pre_offset': .2 + }, + 'DeallocateQubitGate': { + 'height': .15, + 'offset': .2, + 'width': .2, + 'pre_offset': .1 + }, + 'AllocateQubitGate': { + 'height': .15, + 'width': .2, + 'offset': .1, + 'pre_offset': .1, + 'draw_id': False, + 'allocate_at_zero': False + }, + 'MeasureGate': { + 'width': 0.75, + 'offset': .2, + 'height': .5, + 'pre_offset': .2 + } + }) settings['control'] = {'size': .1, 'shadow': False} return settings @@ -153,8 +209,7 @@ def _header(settings): gate_style += ("\\tikzstyle{operator}=[basic,minimum size=1.5em]\n" "\\tikzstyle{phase}=[fill=black,shape=circle," + "minimum size={}".format(settings['control']['size']) + - "cm,inner sep=0pt,outer sep=0pt,draw=black" - ) + "cm,inner sep=0pt,outer sep=0pt,draw=black") if settings['control']['shadow']: gate_style += ",basicshadow" gate_style += ("]\n\\tikzstyle{none}=[inner sep=0pt,outer sep=-.5pt," @@ -167,9 +222,9 @@ def _header(settings): x_gate_radius = min(settings['gates']['XGate']['height'], settings['gates']['XGate']['width']) gate_style += ("{x_rad}cm,minimum width={x_rad}cm,inner sep=-1pt," - "{linestyle}]\n" - ).format(x_rad=x_gate_radius, - linestyle=settings['lines']['style']) + "{linestyle}]\n").format( + x_rad=x_gate_radius, + linestyle=settings['lines']['style']) if settings['gate_shadow']: gate_style += ("\\tikzset{\nshadowed/.style={preaction={transform " "canvas={shift={(0.5pt,-0.5pt)}}, draw=gray, opacity=" @@ -182,13 +237,19 @@ def _header(settings): return packages + init + gate_style + edge_style -def _body(circuit, settings): +def _body(circuit, settings, drawing_order=None, draw_gates_in_parallel=True): """ Return the body of the Latex document, including the entire circuit in TikZ format. Args: circuit (list>): Circuit to draw. + settings: Dictionary of settings to use for the TikZ image. + drawing_order: A list of circuit wires from where to read + one gate command. + draw_gates_in_parallel: Are the gate/commands occupying a + single time step in the circuit diagram? For example, False means + that gates can be parallel in the circuit. Returns: tex_str (string): Latex string to draw the entire circuit. @@ -196,8 +257,19 @@ def _body(circuit, settings): code = [] conv = _Circ2Tikz(settings, len(circuit)) - for line in range(len(circuit)): - code.append(conv.to_tikz(line, circuit)) + + to_where = None + if drawing_order is None: + drawing_order = list(range(len(circuit))) + else: + to_where = 1 + + for line in drawing_order: + code.append( + conv.to_tikz(line, + circuit, + end=to_where, + draw_gates_in_parallel=draw_gates_in_parallel)) return "".join(code) @@ -209,7 +281,7 @@ def _footer(settings): Returns: tex_footer_str (string): Latex document footer. """ - return "\n\n\end{tikzpicture}\n\end{document}" + return "\n\n\\end{tikzpicture}\n\\end{document}" class _Circ2Tikz(object): @@ -219,7 +291,6 @@ class _Circ2Tikz(object): It uses the settings dictionary for gate offsets, sizes, spacing, ... """ - def __init__(self, settings, num_lines): """ Initialize a circuit to latex converter object. @@ -234,7 +305,7 @@ def __init__(self, settings, num_lines): self.op_count = [0] * num_lines self.is_quantum = [settings['lines']['init_quantum']] * num_lines - def to_tikz(self, line, circuit, end=None): + def to_tikz(self, line, circuit, end=None, draw_gates_in_parallel=True): """ Generate the TikZ code for one line of the circuit up to a certain gate. @@ -247,6 +318,7 @@ def to_tikz(self, line, circuit, end=None): line (int): Line to generate the TikZ code for. circuit (list>): The circuit to draw. end (int): Gate index to stop at (for recursion). + draw_gates_in_parallel (bool): True or False for how to place gates Returns: tikz_code (string): TikZ code representing the current qubit line @@ -272,19 +344,23 @@ def to_tikz(self, line, circuit, end=None): gate_idx += 1 tikz_code.append(self.to_tikz(l, circuit, gate_idx)) + # we are taking care of gate 0 (the current one) circuit[l] = circuit[l][1:] all_lines = lines + ctrl_lines - pos = max([self.pos[l] for l in range(min(all_lines), - max(all_lines) + 1)]) + pos = max([ + self.pos[l] for l in range(min(all_lines), + max(all_lines) + 1) + ]) for l in range(min(all_lines), max(all_lines) + 1): self.pos[l] = pos + self._gate_pre_offset(gate) connections = "" for l in all_lines: connections += self._line(self.op_count[l] - 1, - self.op_count[l], line=l) + self.op_count[l], + line=l) add_str = "" if gate == X: # draw NOT-gate with controls @@ -298,7 +374,8 @@ def to_tikz(self, line, circuit, end=None): elif gate == Swap: add_str = self._swap_gate(lines, ctrl_lines) elif gate == SqrtSwap: - add_str = self._sqrtswap_gate(lines, ctrl_lines, + add_str = self._sqrtswap_gate(lines, + ctrl_lines, daggered=False) elif gate == get_inverse(SqrtSwap): add_str = self._sqrtswap_gate(lines, ctrl_lines, daggered=True) @@ -319,32 +396,35 @@ def to_tikz(self, line, circuit, end=None): "cm,xshift=-{shift2}cm]{op}.east);\n" "\\draw[edgestyle] ([yshift=-{shift1}cm]{op}." "center) to ([yshift=-{shift2}cm,xshift=-" - "{shift1}cm]{op}.north east);" - ).format(op=op, pos=self.pos[l], line=l, - shift0=shift0, shift1=shift1, - shift2=shift2) + "{shift1}cm]{op}.north east);").format( + op=op, + pos=self.pos[l], + line=l, + shift0=shift0, + shift1=shift1, + shift2=shift2) self.op_count[l] += 1 self.pos[l] += (self._gate_width(gate) + self._gate_offset(gate)) self.is_quantum[l] = False elif gate == Allocate: # draw 'begin line' - add_str = "\n\\node[none] ({}) at ({},-{}) {{$\Ket{{0}}{}$}};" + add_str = "\n\\node[none] ({}) at ({},-{}) {{$\\Ket{{0}}{}$}};" id_str = "" if self.settings['gates']['AllocateQubitGate']['draw_id']: id_str = "^{{\\textcolor{{red}}{{{}}}}}".format(cmds[i].id) xpos = self.pos[line] try: if (self.settings['gates']['AllocateQubitGate'] - ['allocate_at_zero']): + ['allocate_at_zero']): self.pos[line] -= self._gate_pre_offset(gate) xpos = self._gate_pre_offset(gate) except KeyError: pass - self.pos[line] = max(xpos + self._gate_offset(gate) + - self._gate_width(gate), self.pos[line]) - add_str = add_str.format(self._op(line), xpos, line, - id_str) + self.pos[line] = max( + xpos + self._gate_offset(gate) + self._gate_width(gate), + self.pos[line]) + add_str = add_str.format(self._op(line), xpos, line, id_str) self.op_count[line] += 1 self.is_quantum[line] = self.settings['lines']['init_quantum'] elif gate == Deallocate: @@ -353,9 +433,10 @@ def to_tikz(self, line, circuit, end=None): add_str = "\n\\node[none] ({}) at ({},-{}) {{}};" add_str = add_str.format(op, self.pos[line], line) yshift = str(self._gate_height(gate)) + "cm]" - add_str += ("\n\\draw ([yshift={yshift}{op}.center) edge " - "[edgestyle] ([yshift=-{yshift}{op}.center);" - ).format(op=op, yshift=yshift) + add_str += ( + "\n\\draw ([yshift={yshift}{op}.center) edge " + "[edgestyle] ([yshift=-{yshift}{op}.center);").format( + op=op, yshift=yshift) self.op_count[line] += 1 self.pos[line] += (self._gate_width(gate) + self._gate_offset(gate)) @@ -370,6 +451,11 @@ def to_tikz(self, line, circuit, end=None): if not gate == Allocate: tikz_code.append(connections) + if not draw_gates_in_parallel: + for l in range(len(self.pos)): + if l != line: + self.pos[l] = self.pos[line] + circuit[line] = circuit[line][end:] return "".join(tikz_code) @@ -402,7 +488,7 @@ def _sqrtswap_gate(self, lines, ctrl_lines, daggered): ctrl_lines (list): List of qubit lines which act as controls. daggered (bool): Show the daggered one if True. """ - assert(len(lines) == 2) # sqrt swap gate acts on 2 qubits + assert (len(lines) == 2) # sqrt swap gate acts on 2 qubits delta_pos = self._gate_offset(SqrtSwap) gate_width = self._gate_width(SqrtSwap) lines.sort() @@ -420,20 +506,27 @@ def _sqrtswap_gate(self, lines, ctrl_lines, daggered): swap_style += ",shadowed" gate_str += ("\n\\node[swapstyle] ({op}) at ({pos},-{line}) {{}};" "\n\\draw[{swap_style}] ({s1})--({s2});\n" - "\\draw[{swap_style}] ({s3})--({s4});" - ).format(op=op, s1=s1, s2=s2, s3=s3, s4=s4, - line=line, pos=self.pos[line], - swap_style=swap_style) + "\\draw[{swap_style}] ({s3})--({s4});").format( + op=op, + s1=s1, + s2=s2, + s3=s3, + s4=s4, + line=line, + pos=self.pos[line], + swap_style=swap_style) # add a circled 1/2 midpoint = (lines[0] + lines[1]) / 2. pos = self.pos[lines[0]] - op_mid = "line{}_gate{}".format( - '{}-{}'.format(*lines), self.op_count[lines[0]]) + op_mid = "line{}_gate{}".format('{}-{}'.format(*lines), + self.op_count[lines[0]]) gate_str += ("\n\\node[xstyle] ({op}) at ({pos},-{line})\ - {{\\scriptsize $\\frac{{1}}{{2}}{dagger}$}};" - ).format(op=op_mid, line=midpoint, pos=pos, - dagger='^{{\dagger}}' if daggered else '') + {{\\scriptsize $\\frac{{1}}{{2}}{dagger}$}};").format( + op=op_mid, + line=midpoint, + pos=pos, + dagger='^{{\\dagger}}' if daggered else '') # add two vertical lines to connect circled 1/2 gate_str += "\n\\draw ({}) edge[edgestyle] ({});".format( @@ -468,7 +561,7 @@ def _swap_gate(self, lines, ctrl_lines): ctrl_lines (list): List of qubit lines which act as controls. """ - assert(len(lines) == 2) # swap gate acts on 2 qubits + assert (len(lines) == 2) # swap gate acts on 2 qubits delta_pos = self._gate_offset(Swap) gate_width = self._gate_width(Swap) lines.sort() @@ -486,10 +579,15 @@ def _swap_gate(self, lines, ctrl_lines): swap_style += ",shadowed" gate_str += ("\n\\node[swapstyle] ({op}) at ({pos},-{line}) {{}};" "\n\\draw[{swap_style}] ({s1})--({s2});\n" - "\\draw[{swap_style}] ({s3})--({s4});" - ).format(op=op, s1=s1, s2=s2, s3=s3, s4=s4, - line=line, pos=self.pos[line], - swap_style=swap_style) + "\\draw[{swap_style}] ({s3})--({s4});").format( + op=op, + s1=s1, + s2=s2, + s3=s3, + s4=s4, + line=line, + pos=self.pos[line], + swap_style=swap_style) gate_str += self._line(lines[0], lines[1]) if len(ctrl_lines) > 0: @@ -519,15 +617,15 @@ def _x_gate(self, lines, ctrl_lines): ctrl_lines (list): List of qubit lines which act as controls. """ - assert(len(lines) == 1) # NOT gate only acts on 1 qubit + assert (len(lines) == 1) # NOT gate only acts on 1 qubit line = lines[0] delta_pos = self._gate_offset(X) gate_width = self._gate_width(X) op = self._op(line) gate_str = ("\n\\node[xstyle] ({op}) at ({pos},-{line}) {{}};\n\\draw" "[edgestyle] ({op}.north)--({op}.south);\n\\draw" - "[edgestyle] ({op}.west)--({op}.east);" - ).format(op=op, line=line, pos=self.pos[line]) + "[edgestyle] ({op}.west)--({op}.east);").format( + op=op, line=line, pos=self.pos[line]) if len(ctrl_lines) > 0: for ctrl in ctrl_lines: @@ -705,10 +803,16 @@ def _line(self, p1, p2, double=False, line=None): line_sep = self.settings['lines']['double_lines_sep'] shift1 = shift.format(line_sep / 2.) shift2 = shift.format(-line_sep / 2.) - edges_str = edge_str.format(shift=shift1, op1=op1, op2=op2, - loc1=loc1, loc2=loc2) - edges_str += edge_str.format(shift=shift2, op1=op1, op2=op2, - loc1=loc1, loc2=loc2) + edges_str = edge_str.format(shift=shift1, + op1=op1, + op2=op2, + loc1=loc1, + loc2=loc2) + edges_str += edge_str.format(shift=shift2, + op1=op1, + op2=op2, + loc1=loc1, + loc2=loc2) return edges_str def _regular_gate(self, gate, lines, ctrl_lines): @@ -744,23 +848,24 @@ def _regular_gate(self, gate, lines, ctrl_lines): for l in lines: node1 = node_str.format(self._op(l), pos, l) node2 = ("\n\\node[none,minimum height={}cm,outer sep=0] ({}) at" - " ({},-{}) {{}};" - ).format(gate_height, self._op(l, offset=1), - pos + gate_width / 2., l) - node3 = node_str.format(self._op(l, offset=2), - pos + gate_width, l) + " ({},-{}) {{}};").format(gate_height, + self._op(l, offset=1), + pos + gate_width / 2., l) + node3 = node_str.format(self._op(l, offset=2), pos + gate_width, l) tex_str += node1 + node2 + node3 if l not in gate_lines: - tex_str += self._line(self.op_count[l] - 1, self.op_count[l], + tex_str += self._line(self.op_count[l] - 1, + self.op_count[l], line=l) tex_str += ("\n\\draw[operator,edgestyle,outer sep={width}cm] ([" "yshift={half_height}cm]{op1}) rectangle ([yshift=-" - "{half_height}cm]{op2}) node[pos=.5] {{{name}}};" - ).format(width=gate_width, op1=self._op(imin), - op2=self._op(imax, offset=2), - half_height=.5 * gate_height, - name=name) + "{half_height}cm]{op2}) node[pos=.5] {{{name}}};").format( + width=gate_width, + op1=self._op(imin), + op2=self._op(imax, offset=2), + half_height=.5 * gate_height, + name=name) for l in lines: self.pos[l] = pos + gate_width / 2. diff --git a/projectq/backends/_circuits/_to_latex_test.py b/projectq/backends/_circuits/_to_latex_test.py index 8d8ff81df..c993bcf2e 100755 --- a/projectq/backends/_circuits/_to_latex_test.py +++ b/projectq/backends/_circuits/_to_latex_test.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Tests for projectq.backends._circuits._to_latex.py. """ @@ -22,18 +21,19 @@ from projectq import MainEngine from projectq.cengines import LastEngineException -from projectq.ops import (BasicGate, - H, - X, - CNOT, - Measure, - Z, - Swap, - SqrtX, - SqrtSwap, - C, - get_inverse, - ) +from projectq.ops import ( + BasicGate, + H, + X, + CNOT, + Measure, + Z, + Swap, + SqrtX, + SqrtSwap, + C, + get_inverse, +) from projectq.meta import Control from projectq.backends import CircuitDrawer @@ -47,7 +47,7 @@ def test_tolatex(): old_footer = _to_latex._footer _to_latex._header = lambda x: "H" - _to_latex._body = lambda x, y: x + _to_latex._body = lambda x, settings, drawing_order, draw_gates_in_parallel: x _to_latex._footer = lambda x: "F" latex = _to_latex.to_latex("B") @@ -68,11 +68,26 @@ def test_default_settings(): def test_header(): - settings = {'gate_shadow': False, 'control': {'shadow': False, 'size': 0}, - 'gates': {'MeasureGate': {'height': 0, 'width': 0}, - 'XGate': {'height': 1, 'width': .5} - }, - 'lines': {'style': 'my_style'}} + settings = { + 'gate_shadow': False, + 'control': { + 'shadow': False, + 'size': 0 + }, + 'gates': { + 'MeasureGate': { + 'height': 0, + 'width': 0 + }, + 'XGate': { + 'height': 1, + 'width': .5 + } + }, + 'lines': { + 'style': 'my_style' + } + } header = _to_latex._header(settings) assert 'minimum' in header @@ -104,7 +119,7 @@ def test_large_gates(): drawer = _drawer.CircuitDrawer() eng = MainEngine(drawer, []) old_tolatex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x qubit1 = eng.allocate_qubit() qubit2 = eng.allocate_qubit() @@ -136,7 +151,7 @@ def test_body(): drawer = _drawer.CircuitDrawer() eng = MainEngine(drawer, []) old_tolatex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x qubit1 = eng.allocate_qubit() qubit2 = eng.allocate_qubit() @@ -174,19 +189,142 @@ def test_body(): # CZ is two phases plus 2 from CNOTs + 2 from cswap + 2 from csqrtswap assert code.count("phase") == 8 assert code.count("{{{}}}".format(str(H))) == 2 # 2 hadamard gates - assert code.count("{$\Ket{0}") == 3 # 3 qubits allocated + assert code.count("{$\\Ket{0}") == 3 # 3 qubits allocated # 1 cnot, 1 not gate, 3 SqrtSwap, 1 inv(SqrtSwap) assert code.count("xstyle") == 7 assert code.count("measure") == 1 # 1 measurement assert code.count("{{{}}}".format(str(Z))) == 1 # 1 Z gate assert code.count("{red}") == 3 +def test_body_with_drawing_order_and_gates_parallel(): + drawer = _drawer.CircuitDrawer() + eng = MainEngine(drawer, []) + old_tolatex = _drawer.to_latex + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x + + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qubit3 = eng.allocate_qubit() + + H | qubit1 + H | qubit2 + H | qubit3 + CNOT | (qubit1, qubit3) + + # replicates the above order + order = [0, 1, 2, # initializations + 0, 1, 2, # H1, H3, H2 + 0 # CNOT + ] + + del qubit1 + eng.flush() + + circuit_lines = drawer.get_latex() + _drawer.to_latex = old_tolatex + + settings = _to_latex.get_default_settings() + settings['gates']['AllocateQubitGate']['draw_id'] = True + code = _to_latex._body(circuit_lines, settings, + drawing_order=order, + draw_gates_in_parallel=True) + + # there are three Hadamards in parallel + assert code.count("node[pos=.5] {H}") == 3 + + # line1_gate0 is initialisation + # line1_gate1 is empty + # line1_gate2 is for Hadamard on line1 + # line1_gate3 is empty + # XOR of CNOT is node[xstyle] (line1_gate4) + assert code.count("node[xstyle] (line2_gate4)") == 1 + + # and the CNOT is at position 1.4, because of the offsets + assert code.count("node[phase] (line0_gate4) at (1.4") == 1 + assert code.count("node[xstyle] (line2_gate4) at (1.4") == 1 + + +def test_body_with_drawing_order_and_gates_not_parallel(): + drawer = _drawer.CircuitDrawer() + eng = MainEngine(drawer, []) + old_tolatex = _drawer.to_latex + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x + + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qubit3 = eng.allocate_qubit() + + H | qubit1 + H | qubit2 + H | qubit3 + CNOT | (qubit1, qubit3) + + # replicates the above order + order = [0, 1, 2, # initializations + 0, 1, 2, # H1, H3, H2 + 0 # CNOT + ] + + del qubit1 + eng.flush() + + circuit_lines = drawer.get_latex() + _drawer.to_latex = old_tolatex + + settings = _to_latex.get_default_settings() + settings['gates']['AllocateQubitGate']['draw_id'] = True + code = _to_latex._body(circuit_lines, settings, + drawing_order=order, + draw_gates_in_parallel=False) + + # and the CNOT is at position 4.0, because of the offsets + # which are 0.5 * 3 * 2 (due to three Hadamards) + the initialisations + assert code.count("node[phase] (line0_gate4) at (4.0,-0)") == 1 + assert code.count("node[xstyle] (line2_gate4) at (4.0,-2)") == 1 + +def test_body_without_drawing_order_and_gates_not_parallel(): + drawer = _drawer.CircuitDrawer() + eng = MainEngine(drawer, []) + old_tolatex = _drawer.to_latex + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x + + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qubit3 = eng.allocate_qubit() + + H | qubit1 + H | qubit2 + H | qubit3 + CNOT | (qubit1, qubit3) + + # replicates the above order + order = [0, 1, 2, # initializations + 0, 1, 2, # H1, H3, H2 + 0 # CNOT + ] + + del qubit1 + eng.flush() + + circuit_lines = drawer.get_latex() + _drawer.to_latex = old_tolatex + + settings = _to_latex.get_default_settings() + settings['gates']['AllocateQubitGate']['draw_id'] = True + code = _to_latex._body(circuit_lines, settings, + draw_gates_in_parallel=False) + + # line1_gate1 is after the cnot line2_gate_4 + idx1 = code.find("node[xstyle] (line2_gate4)") + idx2 = code.find("node[none] (line1_gate1)") + assert idx1 < idx2 + def test_qubit_allocations_at_zero(): drawer = _drawer.CircuitDrawer() eng = MainEngine(drawer, []) old_tolatex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x a = eng.allocate_qureg(4) @@ -219,7 +357,7 @@ def test_qubit_lines_classicalvsquantum1(): drawer = _drawer.CircuitDrawer() eng = MainEngine(drawer, []) old_tolatex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x qubit1 = eng.allocate_qubit() diff --git a/projectq/backends/_ibm/_ibm.py b/projectq/backends/_ibm/_ibm.py index d14dbc27f..6486ab4d0 100755 --- a/projectq/backends/_ibm/_ibm.py +++ b/projectq/backends/_ibm/_ibm.py @@ -13,7 +13,7 @@ # limitations under the License. """ Back-end to run quantum program on IBM's Quantum Experience.""" - +import math import random import json @@ -41,11 +41,12 @@ class IBMBackend(BasicEngine): """ - The IBM Backend class, which stores the circuit, transforms it to JSON - QASM, and sends the circuit through the IBM API. + The IBM Backend class, which stores the circuit, transforms it to JSON, + and sends the circuit through the IBM API. """ def __init__(self, use_hardware=False, num_runs=1024, verbose=False, - user=None, password=None, device='ibmqx4', + token='', device='ibmq_essex', + num_retries=3000, interval=1, retrieve_execution=None): """ Initialize the Backend object. @@ -58,10 +59,13 @@ def __init__(self, use_hardware=False, num_runs=1024, verbose=False, verbose (bool): If True, statistics are printed, in addition to the measurement result being registered (at the end of the circuit). - user (string): IBM Quantum Experience user name - password (string): IBM Quantum Experience password - device (string): Device to use ('ibmqx4', or 'ibmqx5') - if use_hardware is set to True. Default is ibmqx4. + token (str): IBM quantum experience user password. + device (str): name of the IBM device to use. ibmq_essex By default + num_retries (int): Number of times to retry to obtain + results from the IBM API. (default is 3000) + interval (float, int): Number of seconds between successive + attempts to obtain results from the IBM API. + (default is 1) retrieve_execution (int): Job ID to retrieve instead of re- running the circuit (e.g., if previous run timed out). """ @@ -70,13 +74,15 @@ def __init__(self, use_hardware=False, num_runs=1024, verbose=False, if use_hardware: self.device = device else: - self.device = 'simulator' + self.device = 'ibmq_qasm_simulator' self._num_runs = num_runs self._verbose = verbose - self._user = user - self._password = password + self._token=token + self._num_retries = num_retries + self._interval = interval self._probabilities = dict() self.qasm = "" + self._json=[] self._measured_ids = [] self._allocated_qubits = set() self._retrieve_execution = retrieve_execution @@ -85,17 +91,17 @@ def is_available(self, cmd): """ Return true if the command can be executed. - The IBM quantum chip can do X, Y, Z, T, Tdag, S, Sdag, - rotation gates, barriers, and CX / CNOT. + The IBM quantum chip can only do U1,U2,U3,barriers, and CX / CNOT. + Conversion implemented for Rotation gates and H gates. Args: cmd (Command): Command for which to check availability """ g = cmd.gate - if g == NOT and get_control_count(cmd) <= 1: + if g == NOT and get_control_count(cmd) == 1: return True if get_control_count(cmd) == 0: - if g in (T, Tdag, S, Sdag, H, Y, Z): + if g == H: return True if isinstance(g, (Rx, Ry, Rz)): return True @@ -103,6 +109,11 @@ def is_available(self, cmd): return True return False + def get_qasm(self): + """ Return the QASM representation of the circuit sent to the backend. + Should be called AFTER calling the ibm device """ + return self.qasm + def _reset(self): """ Reset all temporary variables (after flush gate). """ self._clear = True @@ -121,10 +132,10 @@ def _store(self, cmd): self._probabilities = dict() self._clear = False self.qasm = "" + self._json=[] self._allocated_qubits = set() gate = cmd.gate - if gate == Allocate: self._allocated_qubits.add(cmd.qubits[0][0].id) return @@ -146,6 +157,7 @@ def _store(self, cmd): ctrl_pos = cmd.control_qubits[0].id qb_pos = cmd.qubits[0][0].id self.qasm += "\ncx q[{}], q[{}];".format(ctrl_pos, qb_pos) + self._json.append({'qubits': [ctrl_pos, qb_pos], 'name': 'cx'}) elif gate == Barrier: qb_pos = [qb.id for qr in cmd.qubits for qb in qr] self.qasm += "\nbarrier " @@ -153,22 +165,28 @@ def _store(self, cmd): for pos in qb_pos: qb_str += "q[{}], ".format(pos) self.qasm += qb_str[:-2] + ";" + self._json.append({'qubits': qb_pos, 'name': 'barrier'}) elif isinstance(gate, (Rx, Ry, Rz)): assert get_control_count(cmd) == 0 qb_pos = cmd.qubits[0][0].id u_strs = {'Rx': 'u3({}, -pi/2, pi/2)', 'Ry': 'u3({}, 0, 0)', 'Rz': 'u1({})'} - gate = u_strs[str(gate)[0:2]].format(gate.angle) - self.qasm += "\n{} q[{}];".format(gate, qb_pos) - else: + u_name = {'Rx': 'u3', 'Ry': 'u3', + 'Rz': 'u1'} + u_angle = {'Rx': [gate.angle, -math.pi/2, math.pi/2], 'Ry': [gate.angle, 0, 0], + 'Rz': [gate.angle]} + gate_qasm = u_strs[str(gate)[0:2]].format(gate.angle) + gate_name=u_name[str(gate)[0:2]] + params= u_angle[str(gate)[0:2]] + self.qasm += "\n{} q[{}];".format(gate_qasm, qb_pos) + self._json.append({'qubits': [qb_pos], 'name': gate_name,'params': params}) + elif gate == H: assert get_control_count(cmd) == 0 - if str(gate) in self._gate_names: - gate_str = self._gate_names[str(gate)] - else: - gate_str = str(gate).lower() - qb_pos = cmd.qubits[0][0].id - self.qasm += "\n{} q[{}];".format(gate_str, qb_pos) + self.qasm += "\nu2(0,pi/2) q[{}];".format(qb_pos) + self._json.append({'qubits': [qb_pos], 'name': 'u2','params': [0, 3.141592653589793]}) + else: + raise Exception('Command not authorized. You should run the circuit with the appropriate ibm setup.') def _logical_to_physical(self, qb_id): """ @@ -190,6 +208,8 @@ def _logical_to_physical(self, qb_id): def get_probabilities(self, qureg): """ Return the list of basis states with corresponding probabilities. + If input qureg is a subset of the register used for the experiment, + then returns the projected probabilities over the other states. The measured bits are ordered according to the supplied quantum register, i.e., the left-most bit in the state-string corresponds to @@ -204,7 +224,7 @@ def get_probabilities(self, qureg): Returns: probability_dict (dict): Dictionary mapping n-bit strings to - probabilities. + probabilities. Raises: RuntimeError: If no data is available (i.e., if the circuit has @@ -215,62 +235,70 @@ def get_probabilities(self, qureg): raise RuntimeError("Please, run the circuit first!") probability_dict = dict() - for state in self._probabilities: mapped_state = ['0'] * len(qureg) for i in range(len(qureg)): mapped_state[i] = state[self._logical_to_physical(qureg[i].id)] probability = self._probabilities[state] - probability_dict["".join(mapped_state)] = probability - + mapped_state = "".join(mapped_state) + if mapped_state not in probability_dict: + probability_dict[mapped_state] = probability + else: + probability_dict[mapped_state] += probability return probability_dict def _run(self): """ Run the circuit. - Send the circuit via the IBM API (JSON QASM) using the provided user - data / ask for username & password. + Send the circuit via a non documented IBM API (using JSON written + circuits) using the provided user data / ask for the user token. """ # finally: add measurements (no intermediate measurements are allowed) for measured_id in self._measured_ids: qb_loc = self.main_engine.mapper.current_mapping[measured_id] self.qasm += "\nmeasure q[{}] -> c[{}];".format(qb_loc, qb_loc) - + self._json.append({'qubits': [qb_loc], 'name': 'measure','memory':[qb_loc]}) # return if no operations / measurements have been performed. if self.qasm == "": return - - max_qubit_id = max(self._allocated_qubits) + max_qubit_id = max(self._allocated_qubits) + 1 qasm = ("\ninclude \"qelib1.inc\";\nqreg q[{nq}];\ncreg c[{nq}];" + - self.qasm).format(nq=max_qubit_id + 1) + self.qasm).format(nq=max_qubit_id) info = {} - info['qasms'] = [{'qasm': qasm}] + info['json']=self._json + info['nq']=max_qubit_id + info['shots'] = self._num_runs - info['maxCredits'] = 5 + info['maxCredits'] = 10 info['backend'] = {'name': self.device} - info = json.dumps(info) - try: if self._retrieve_execution is None: res = send(info, device=self.device, - user=self._user, password=self._password, - shots=self._num_runs, verbose=self._verbose) + token=self._token, + num_retries=self._num_retries, + interval=self._interval, + verbose=self._verbose) else: - res = retrieve(device=self.device, user=self._user, - password=self._password, - jobid=self._retrieve_execution) - + res = retrieve(device=self.device, + token=self._token, + jobid=self._retrieve_execution, + num_retries=self._num_retries, + interval=self._interval, + verbose=self._verbose) counts = res['data']['counts'] # Determine random outcome P = random.random() p_sum = 0. measured = "" + length=len(self._measured_ids) for state in counts: probability = counts[state] * 1. / self._num_runs - state = list(reversed(state)) - state = "".join(state) + state="{0:b}".format(int(state,0)) + state=state.zfill(max_qubit_id) + #states in ibmq are right-ordered, so need to reverse state string + state=state[::-1] p_sum += probability star = "" if p_sum >= P and measured == "": @@ -308,9 +336,3 @@ def receive(self, command_list): else: self._run() self._reset() - - """ - Mapping of gate names from our gate objects to the IBM QASM representation. - """ - _gate_names = {str(Tdag): "tdg", - str(Sdag): "sdg"} diff --git a/projectq/backends/_ibm/_ibm_http_client.py b/projectq/backends/_ibm/_ibm_http_client.py index 83b054fcd..98751bf90 100755 --- a/projectq/backends/_ibm/_ibm_http_client.py +++ b/projectq/backends/_ibm/_ibm_http_client.py @@ -13,78 +13,350 @@ # limitations under the License. # helpers to run the jsonified gate sequence on ibm quantum experience server -# api documentation is at https://qcwi-staging.mybluemix.net/explorer/ -import requests +# api documentation does not exist and has to be deduced from the qiskit code source +# at: https://github.com/Qiskit/qiskit-ibmq-provider + import getpass -import json -import sys import time +import signal +import requests from requests.compat import urljoin +from requests import Session + +_AUTH_API_URL = 'https://auth.quantum-computing.ibm.com/api/users/loginWithToken' +_API_URL = 'https://api.quantum-computing.ibm.com/api/' + +# TODO: call to get the API version automatically +CLIENT_APPLICATION = 'ibmqprovider/0.4.4' + + +class IBMQ(Session): + """ + Manage a session between ProjectQ and the IBMQ web API. + """ + + def __init__(self, **kwargs): + super(IBMQ, self).__init__(**kwargs) # Python 2 compatibility + self.backends = dict() + self.timeout = 5.0 + + def get_list_devices(self, verbose=False): + """ + Get the list of available IBM backends with their properties + + Args: + verbose (bool): print the returned dictionnary if True + + Returns: + (dict) backends dictionary by name device, containing the qubit + size 'nq', the coupling map 'coupling_map' as well as the + device version 'version' + """ + list_device_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + argument = {'allow_redirects': True, 'timeout': (self.timeout, None)} + request = super(IBMQ, self).get(urljoin(_API_URL, list_device_url), + **argument) + request.raise_for_status() + r_json = request.json() + self.backends = dict() + for el in r_json: + self.backends[el['backend_name']] = { + 'nq': el['n_qubits'], + 'coupling_map': el['coupling_map'], + 'version': el['backend_version'] + } + + if verbose: + print('- List of IBMQ devices available:') + print(self.backends) + return self.backends + + def is_online(self, device): + """ + Check if the device is in the list of available IBM backends. + + Args: + device (str): name of the device to check + + Returns: + (bool) True if device is available, False otherwise + """ + return device in self.backends + + def can_run_experiment(self, info, device): + """ + Check if the device is big enough to run the code. + + Args: + info (dict): dictionary sent by the backend containing the code to + run + device (str): name of the ibm device to use + + Returns: + (tuple): (bool) True if device is big enough, False otherwise + (int) maximum number of qubit available on the device + (int) number of qubit needed for the circuit + """ + nb_qubit_max = self.backends[device]['nq'] + nb_qubit_needed = info['nq'] + return nb_qubit_needed <= nb_qubit_max, nb_qubit_max, nb_qubit_needed -_api_url = 'https://quantumexperience.ng.bluemix.net/api/' + def _authenticate(self, token=None): + """ + Args: + token (str): IBM quantum experience user API token. + """ + if token is None: + token = getpass.getpass(prompt="IBM QE token > ") + if len(token) == 0: + raise Exception('Error with the IBM QE token') + self.headers.update({'X-Qx-Client-Application': CLIENT_APPLICATION}) + args = { + 'data': None, + 'json': { + 'apiToken': token + }, + 'timeout': (self.timeout, None) + } + request = super(IBMQ, self).post(_AUTH_API_URL, **args) + request.raise_for_status() + r_json = request.json() + self.params.update({'access_token': r_json['id']}) + + def _run(self, info, device): + post_job_url = 'Network/ibm-q/Groups/open/Projects/main/Jobs' + shots = info['shots'] + n_classical_reg = info['nq'] + n_qubits = self.backends[device]['nq'] + version = self.backends[device]['version'] + instructions = info['json'] + maxcredit = info['maxCredits'] + c_label = [] + q_label = [] + for i in range(n_classical_reg): + c_label.append(['c', i]) + for i in range(n_qubits): + q_label.append(['q', i]) + experiment = [{ + 'header': { + 'qreg_sizes': [['q', n_qubits]], + 'n_qubits': n_qubits, + 'memory_slots': n_classical_reg, + 'creg_sizes': [['c', n_classical_reg]], + 'clbit_labels': c_label, + 'qubit_labels': q_label, + 'name': 'circuit0' + }, + 'config': { + 'n_qubits': n_qubits, + 'memory_slots': n_classical_reg + }, + 'instructions': instructions + }] + # Note: qobj_id is not necessary in projectQ, so fixed string for now + argument = { + 'data': None, + 'json': { + 'qObject': { + 'type': 'QASM', + 'schema_version': '1.1.0', + 'config': { + 'shots': shots, + 'max_credits': maxcredit, + 'n_qubits': n_qubits, + 'memory_slots': n_classical_reg, + 'memory': False, + 'parameter_binds': [] + }, + 'experiments': experiment, + 'header': { + 'backend_version': version, + 'backend_name': device + }, + 'qobj_id': 'e72443f5-7752-4e32-9ac8-156f1f3fee18' + }, + 'backend': { + 'name': device + }, + 'shots': shots + }, + 'timeout': (self.timeout, None) + } + request = super(IBMQ, self).post(urljoin(_API_URL, post_job_url), + **argument) + request.raise_for_status() + r_json = request.json() + execution_id = r_json["id"] + return execution_id + + def _get_result(self, + device, + execution_id, + num_retries=3000, + interval=1, + verbose=False): + + job_status_url = ('Network/ibm-q/Groups/open/Projects/main/Jobs/' + + execution_id) + + if verbose: + print("Waiting for results. [Job ID: {}]".format(execution_id)) + + original_sigint_handler = signal.getsignal(signal.SIGINT) + + def _handle_sigint_during_get_result(*_): + raise Exception( + "Interrupted. The ID of your submitted job is {}.".format( + execution_id)) + + try: + signal.signal(signal.SIGINT, _handle_sigint_during_get_result) + for retries in range(num_retries): + + argument = { + 'allow_redirects': True, + 'timeout': (self.timeout, None) + } + request = super(IBMQ, + self).get(urljoin(_API_URL, job_status_url), + **argument) + request.raise_for_status() + r_json = request.json() + if r_json['status'] == 'COMPLETED': + return r_json['qObjectResult']['results'][0] + if r_json['status'] != 'RUNNING': + raise Exception("Error while running the code: {}.".format( + r_json['status'])) + time.sleep(interval) + if self.is_online(device) and retries % 60 == 0: + self.get_list_devices() + if not self.is_online(device): + raise DeviceOfflineError( + "Device went offline. The ID of " + "your submitted job is {}.".format(execution_id)) + + finally: + if original_sigint_handler is not None: + signal.signal(signal.SIGINT, original_sigint_handler) + + raise Exception("Timeout. The ID of your submitted job is {}.".format( + execution_id)) + + +class DeviceTooSmall(Exception): + pass class DeviceOfflineError(Exception): pass -def is_online(device): - url = 'Backends/{}/queue/status'.format(device) - r = requests.get(urljoin(_api_url, url)) - return r.json()['state'] +def show_devices(token=None, verbose=False): + """ + Access the list of available devices and their properties (ex: for setup + configuration) + Args: + token (str): IBM quantum experience user API token. + verbose (bool): If True, additional information is printed -def retrieve(device, user, password, jobid): + Returns: + (list) list of available devices and their properties + """ + ibmq_session = IBMQ() + ibmq_session._authenticate(token=token) + return ibmq_session.get_list_devices(verbose=verbose) + + +def retrieve(device, + token, + jobid, + num_retries=3000, + interval=1, + verbose=False): """ Retrieves a previously run job by its ID. Args: device (str): Device on which the code was run / is running. - user (str): IBM quantum experience user (e-mail) - password (str): IBM quantum experience password + token (str): IBM quantum experience user API token. jobid (str): Id of the job to retrieve + + Returns: + (dict) result form the IBMQ server """ - user_id, access_token = _authenticate(user, password) - res = _get_result(device, jobid, access_token) + ibmq_session = IBMQ() + ibmq_session._authenticate(token) + ibmq_session.get_list_devices(verbose) + res = ibmq_session._get_result(device, + jobid, + num_retries=num_retries, + interval=interval, + verbose=verbose) return res -def send(info, device='sim_trivial_2', user=None, password=None, - shots=1, verbose=False): +def send(info, + device='ibmq_qasm_simulator', + token=None, + shots=None, + num_retries=3000, + interval=1, + verbose=False): """ Sends QASM through the IBM API and runs the quantum circuit. Args: - info: Contains QASM representation of the circuit to run. - device (str): Either 'simulator', 'ibmqx4', or 'ibmqx5'. - user (str): IBM quantum experience user. - password (str): IBM quantum experience user password. + info(dict): Contains representation of the circuit to run. + device (str): name of the ibm device. Simulator chosen by default + token (str): IBM quantum experience user API token. shots (int): Number of runs of the same circuit to collect statistics. verbose (bool): If True, additional information is printed, such as measurement statistics. Otherwise, the backend simply registers one measurement result (same behavior as the projectq Simulator). - """ - try: - # check if the device is online - if device in ['ibmqx4', 'ibmqx5']: - online = is_online(device) - if not online: - print("The device is offline (for maintenance?). Use the " - "simulator instead or try again later.") - raise DeviceOfflineError("Device is offline.") + Returns: + (dict) result form the IBMQ server + """ + try: + ibmq_session = IBMQ() + # Shots argument deprecated, as already + if shots is not None: + info['shots'] = shots if verbose: print("- Authenticating...") - user_id, access_token = _authenticate(user, password) + if token is not None: + print('user API token: ' + token) + ibmq_session._authenticate(token) + + # check if the device is online + ibmq_session.get_list_devices(verbose) + online = ibmq_session.is_online(device) + if not online: + print("The device is offline (for maintenance?). Use the " + "simulator instead or try again later.") + raise DeviceOfflineError("Device is offline.") + + # check if the device has enough qubit to run the code + runnable, qmax, qneeded = ibmq_session.can_run_experiment(info, device) + if not runnable: + print( + ("The device is too small ({} qubits available) for the code " + + "requested({} qubits needed) Try to look for another " + + "device with more qubits").format(qmax, qneeded)) + raise DeviceTooSmall("Device is too small.") if verbose: - print("- Running code: {}".format( - json.loads(info)['qasms'][0]['qasm'])) - execution_id = _run(info, device, user_id, access_token, shots) + print("- Running code: {}".format(info)) + execution_id = ibmq_session._run(info, device) if verbose: print("- Waiting for results...") - res = _get_result(device, execution_id, access_token) + res = ibmq_session._get_result(device, + execution_id, + num_retries=num_retries, + interval=interval, + verbose=verbose) if verbose: print("- Done.") return res @@ -97,78 +369,3 @@ def send(info, device='sim_trivial_2', user=None, password=None, except KeyError as err: print("- Failed to parse response:") print(err) - - -def _authenticate(email=None, password=None): - """ - :param email: - :param password: - :return: - """ - if email is None: - try: - input_fun = raw_input - except NameError: - input_fun = input - email = input_fun('IBM QE user (e-mail) > ') - if password is None: - password = getpass.getpass(prompt='IBM QE password > ') - - r = requests.post(urljoin(_api_url, 'users/login'), - data={"email": email, "password": password}) - r.raise_for_status() - - json_data = r.json() - user_id = json_data['userId'] - access_token = json_data['id'] - - return user_id, access_token - - -def _run(qasm, device, user_id, access_token, shots): - suffix = 'Jobs' - - r = requests.post(urljoin(_api_url, suffix), - data=qasm, - params={"access_token": access_token, - "deviceRunType": device, - "fromCache": "false", - "shots": shots}, - headers={"Content-Type": "application/json"}) - r.raise_for_status() - - r_json = r.json() - execution_id = r_json["id"] - return execution_id - - -def _get_result(device, execution_id, access_token, num_retries=3000, - interval=1): - suffix = 'Jobs/{execution_id}'.format(execution_id=execution_id) - status_url = urljoin(_api_url, 'Backends/{}/queue/status'.format(device)) - - print("Waiting for results. [Job ID: {}]".format(execution_id)) - - for retries in range(num_retries): - r = requests.get(urljoin(_api_url, suffix), - params={"access_token": access_token}) - r.raise_for_status() - - r_json = r.json() - if 'qasms' in r_json: - qasm = r_json['qasms'][0] - if 'result' in qasm: - return qasm['result'] - time.sleep(interval) - if device in ['ibmqx4', 'ibmqx5'] and retries % 60 == 0: - r = requests.get(status_url) - r_json = r.json() - if 'state' in r_json and not r_json['state']: - raise DeviceOfflineError("Device went offline. The ID of your " - "submitted job is {}." - .format(execution_id)) - if 'lengthQueue' in r_json: - print("Currently there are {} jobs queued for execution on {}." - .format(r_json['lengthQueue'], device)) - raise Exception("Timeout. The ID of your submitted job is {}." - .format(execution_id)) diff --git a/projectq/backends/_ibm/_ibm_http_client_test.py b/projectq/backends/_ibm/_ibm_http_client_test.py index 6162fa618..eb56b1ee4 100755 --- a/projectq/backends/_ibm/_ibm_http_client_test.py +++ b/projectq/backends/_ibm/_ibm_http_client_test.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.backends._ibm_http_client._ibm.py.""" import json @@ -28,24 +27,31 @@ def no_requests(monkeypatch): monkeypatch.delattr("requests.sessions.Session.request") -_api_url = 'https://quantumexperience.ng.bluemix.net/api/' -_api_url_status = 'https://quantumexperience.ng.bluemix.net/api/' +_API_URL = 'https://api.quantum-computing.ibm.com/api/' +_AUTH_API_URL = 'https://auth.quantum-computing.ibm.com/api/users/loginWithToken' def test_send_real_device_online_verbose(monkeypatch): - qasms = {'qasms': [{'qasm': 'my qasm'}]} - json_qasm = json.dumps(qasms) + json_qasm = { + 'qasms': [{ + 'qasm': 'my qasm' + }], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1 + } name = 'projectq_test' + token = '12345' access_token = "access" user_id = 2016 code_id = 11 name_item = '"name":"{name}", "jsonQASM":'.format(name=name) - json_body = ''.join([name_item, json_qasm]) + json_body = ''.join([name_item, json.dumps(json_qasm)]) json_data = ''.join(['{', json_body, '}']) shots = 1 device = "ibmqx4" - json_data_run = ''.join(['{"qasm":', json_qasm, '}']) - execution_id = 3 + execution_id = '3' result_ready = [False] result = "my_result" request_num = [0] # To assert correct order of calls @@ -70,24 +76,39 @@ def raise_for_status(self): pass # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if (args[0] == urljoin(_api_url_status, status_url) and - (request_num[0] == 0 or request_num[0] == 3)): + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if (args[1] == urljoin(_API_URL, status_url) + and (request_num[0] == 1 or request_num[0] == 4)): request_num[0] += 1 - return MockResponse({"state": True}, 200) + connections = set([(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), (2, 1), + (2, 3), (2, 4), (3, 1), (3, 4), (4, 3)]) + return MockResponse([{ + 'backend_name': 'ibmqx4', + 'coupling_map': connections, + 'backend_version': '0.1.547', + 'n_qubits': 32 + }], 200) # Getting result - elif (args[0] == urljoin(_api_url, - "Jobs/{execution_id}".format(execution_id=execution_id)) and - kwargs["params"]["access_token"] == access_token and not - result_ready[0] and request_num[0] == 3): + elif (args[1] == urljoin( + _API_URL, + "Network/ibm-q/Groups/open/Projects/main/Jobs/{execution_id}". + format(execution_id=execution_id)) and not result_ready[0] + and request_num[0] == 3): result_ready[0] = True - return MockResponse({"status": {"id": "NotDone"}}, 200) - elif (args[0] == urljoin(_api_url, - "Jobs/{execution_id}".format(execution_id=execution_id)) and - kwargs["params"]["access_token"] == access_token and - result_ready[0] and request_num[0] == 4): - print("state ok") - return MockResponse({"qasms": [{"result": result}]}, 200) + request_num[0] += 1 + return MockResponse({"status": "RUNNING"}, 200) + elif (args[1] == urljoin( + _API_URL, + "Network/ibm-q/Groups/open/Projects/main/Jobs/{execution_id}". + format(execution_id=execution_id)) and result_ready[0] + and request_num[0] == 5): + return MockResponse( + { + 'qObjectResult': { + "results": [result] + }, + "status": "COMPLETED" + }, 200) def mocked_requests_post(*args, **kwargs): class MockRequest: @@ -107,49 +128,69 @@ def json(self): def raise_for_status(self): pass + jobs_url = 'Network/ibm-q/Groups/open/Projects/main/Jobs' # Authentication - if (args[0] == urljoin(_api_url, "users/login") and - kwargs["data"]["email"] == email and - kwargs["data"]["password"] == password and - request_num[0] == 1): + if (args[1] == _AUTH_API_URL and kwargs["json"]["apiToken"] == token + and request_num[0] == 0): request_num[0] += 1 return MockPostResponse({"userId": user_id, "id": access_token}) # Run code - elif (args[0] == urljoin(_api_url, "Jobs") and - kwargs["data"] == json_qasm and - kwargs["params"]["access_token"] == access_token and - kwargs["params"]["deviceRunType"] == device and - kwargs["params"]["fromCache"] == "false" and - kwargs["params"]["shots"] == shots and - kwargs["headers"]["Content-Type"] == "application/json" and - request_num[0] == 2): + elif (args[1] == urljoin(_API_URL, jobs_url) and kwargs["data"] is None + and kwargs["json"]["backend"]["name"] == device + and kwargs["json"]["qObject"]['config']['shots'] == shots + and request_num[0] == 2): request_num[0] += 1 return MockPostResponse({"id": execution_id}) - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) - # Patch login data - password = 12345 - email = "test@projectq.ch" - monkeypatch.setitem(__builtins__, "input", lambda x: email) - monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) def user_password_input(prompt): - if prompt == "IBM QE password > ": - return password + if prompt == "IBM QE token > ": + return token monkeypatch.setattr("getpass.getpass", user_password_input) # Code to test: res = _ibm_http_client.send(json_qasm, device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) - print(res) + token=None, + shots=shots, + verbose=True) assert res == result + json_qasm['nq'] = 40 + request_num[0] = 0 + with pytest.raises(_ibm_http_client.DeviceTooSmall): + res = _ibm_http_client.send(json_qasm, + device="ibmqx4", + token=None, + shots=shots, + verbose=True) + + +def test_no_password_given(monkeypatch): + token = '' + json_qasm = '' + + def user_password_input(prompt): + if prompt == "IBM QE token > ": + return token + + monkeypatch.setattr("getpass.getpass", user_password_input) + + with pytest.raises(Exception): + res = _ibm_http_client.send(json_qasm, + device="ibmqx4", + token=None, + shots=1, + verbose=True) def test_send_real_device_offline(monkeypatch): + token = '12345' + access_token = "access" + user_id = 2016 + def mocked_requests_get(*args, **kwargs): class MockResponse: def __init__(self, json_data, status_code): @@ -159,22 +200,63 @@ def __init__(self, json_data, status_code): def json(self): return self.json_data - # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url_status, status_url): - return MockResponse({"state": False}, 200) - monkeypatch.setattr("requests.get", mocked_requests_get) + def raise_for_status(self): + pass + + # Accessing status of device. Return offline. + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url): + return MockResponse({}, 200) + + def mocked_requests_post(*args, **kwargs): + class MockRequest: + def __init__(self, body="", url=""): + self.body = body + self.url = url + + class MockPostResponse: + def __init__(self, json_data, text=" "): + self.json_data = json_data + self.text = text + self.request = MockRequest() + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + # Authentication + if (args[1] == _AUTH_API_URL and kwargs["json"]["apiToken"] == token): + return MockPostResponse({"userId": user_id, "id": access_token}) + + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + shots = 1 - json_qasm = "my_json_qasm" + token = '12345' + json_qasm = { + 'qasms': [{ + 'qasm': 'my qasm' + }], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1 + } name = 'projectq_test' with pytest.raises(_ibm_http_client.DeviceOfflineError): _ibm_http_client.send(json_qasm, device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) + token=token, + shots=shots, + verbose=True) -def test_send_that_errors_are_caught(monkeypatch): +def test_show_device(monkeypatch): + access_token = "access" + user_id = 2016 + class MockResponse: def __init__(self, json_data, status_code): self.json_data = json_data @@ -183,123 +265,191 @@ def __init__(self, json_data, status_code): def json(self): return self.json_data + def raise_for_status(self): + pass + def mocked_requests_get(*args, **kwargs): # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url_status, status_url): - return MockResponse({"state": True}, 200) + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url): + connections = set([(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), (2, 1), + (2, 3), (2, 4), (3, 1), (3, 4), (4, 3)]) + return MockResponse([{ + 'backend_name': 'ibmqx4', + 'coupling_map': connections, + 'backend_version': '0.1.547', + 'n_qubits': 32 + }], 200) + + def mocked_requests_post(*args, **kwargs): + class MockRequest: + def __init__(self, body="", url=""): + self.body = body + self.url = url + + class MockPostResponse: + def __init__(self, json_data, text=" "): + self.json_data = json_data + self.text = text + self.request = MockRequest() + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + # Authentication + if (args[1] == _AUTH_API_URL and kwargs["json"]["apiToken"] == token): + return MockPostResponse({"userId": user_id, "id": access_token}) + + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + # Patch login data + token = '12345' + + def user_password_input(prompt): + if prompt == "IBM QE token > ": + return token + + monkeypatch.setattr("getpass.getpass", user_password_input) + assert _ibm_http_client.show_devices() == { + 'ibmqx4': { + 'coupling_map': {(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), (2, 1), + (2, 3), (2, 4), (3, 1), (3, 4), (4, 3)}, + 'version': '0.1.547', + 'nq': 32 + } + } + + +def test_send_that_errors_are_caught(monkeypatch): + class MockResponse: + def __init__(self, json_data, status_code): + pass def mocked_requests_post(*args, **kwargs): # Test that this error gets caught raise requests.exceptions.HTTPError - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) # Patch login data - password = 12345 - email = "test@projectq.ch" - monkeypatch.setitem(__builtins__, "input", lambda x: email) - monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) + token = '12345' def user_password_input(prompt): - if prompt == "IBM QE password > ": - return password + if prompt == "IBM QE token > ": + return token monkeypatch.setattr("getpass.getpass", user_password_input) shots = 1 - json_qasm = "my_json_qasm" + json_qasm = { + 'qasms': [{ + 'qasm': 'my qasm' + }], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1 + } name = 'projectq_test' _ibm_http_client.send(json_qasm, device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) - + token=None, + shots=shots, + verbose=True) -def test_send_that_errors_are_caught2(monkeypatch): - def mocked_requests_get(*args, **kwargs): - class MockResponse: - def __init__(self, json_data, status_code): - self.json_data = json_data - self.status_code = status_code + token = '' + with pytest.raises(Exception): + _ibm_http_client.send(json_qasm, + device="ibmqx4", + token=None, + shots=shots, + verbose=True) - def json(self): - return self.json_data - # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url_status, status_url): - return MockResponse({"state": True}, 200) +def test_send_that_errors_are_caught2(monkeypatch): + class MockResponse: + def __init__(self, json_data, status_code): + pass def mocked_requests_post(*args, **kwargs): # Test that this error gets caught raise requests.exceptions.RequestException - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) # Patch login data - password = 12345 - email = "test@projectq.ch" - monkeypatch.setitem(__builtins__, "input", lambda x: email) - monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) + token = '12345' def user_password_input(prompt): - if prompt == "IBM QE password > ": - return password + if prompt == "IBM QE token > ": + return token monkeypatch.setattr("getpass.getpass", user_password_input) shots = 1 - json_qasm = "my_json_qasm" + json_qasm = { + 'qasms': [{ + 'qasm': 'my qasm' + }], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1 + } name = 'projectq_test' _ibm_http_client.send(json_qasm, device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) + token=None, + shots=shots, + verbose=True) def test_send_that_errors_are_caught3(monkeypatch): - def mocked_requests_get(*args, **kwargs): - class MockResponse: - def __init__(self, json_data, status_code): - self.json_data = json_data - self.status_code = status_code - - def json(self): - return self.json_data - - # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url_status, status_url): - return MockResponse({"state": True}, 200) + class MockResponse: + def __init__(self, json_data, status_code): + pass def mocked_requests_post(*args, **kwargs): # Test that this error gets caught raise KeyError - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) # Patch login data - password = 12345 - email = "test@projectq.ch" - monkeypatch.setitem(__builtins__, "input", lambda x: email) - monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) + token = '12345' def user_password_input(prompt): - if prompt == "IBM QE password > ": - return password + if prompt == "IBM QE token > ": + return token monkeypatch.setattr("getpass.getpass", user_password_input) shots = 1 - json_qasm = "my_json_qasm" + json_qasm = { + 'qasms': [{ + 'qasm': 'my qasm' + }], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1 + } name = 'projectq_test' _ibm_http_client.send(json_qasm, device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) + token=None, + shots=shots, + verbose=True) def test_timeout_exception(monkeypatch): - qasms = {'qasms': [{'qasm': 'my qasm'}]} - json_qasm = json.dumps(qasms) + qasms = { + 'qasms': [{ + 'qasm': 'my qasm' + }], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1 + } + json_qasm = qasms tries = [0] def mocked_requests_get(*args, **kwargs): @@ -314,14 +464,22 @@ def json(self): def raise_for_status(self): pass - # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url, status_url): - return MockResponse({"state": True}, 200) - job_url = 'Jobs/{}'.format("123e") - if args[0] == urljoin(_api_url, job_url): + # Accessing status of device. Return device info. + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url): + connections = set([(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), (2, 1), + (2, 3), (2, 4), (3, 1), (3, 4), (4, 3)]) + return MockResponse([{ + 'backend_name': 'ibmqx4', + 'coupling_map': connections, + 'backend_version': '0.1.547', + 'n_qubits': 32 + }], 200) + job_url = "Network/ibm-q/Groups/open/Projects/main/Jobs/{}".format( + "123e") + if args[1] == urljoin(_API_URL, job_url): tries[0] += 1 - return MockResponse({"noqasms": "not done"}, 200) + return MockResponse({"status": "RUNNING"}, 200) def mocked_requests_post(*args, **kwargs): class MockRequest: @@ -340,27 +498,28 @@ def json(self): def raise_for_status(self): pass - login_url = 'users/login' - if args[0] == urljoin(_api_url, login_url): + jobs_url = 'Network/ibm-q/Groups/open/Projects/main/Jobs' + if args[1] == _AUTH_API_URL: return MockPostResponse({"userId": "1", "id": "12"}) - if args[0] == urljoin(_api_url, 'Jobs'): + if args[1] == urljoin(_API_URL, jobs_url): return MockPostResponse({"id": "123e"}) - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + _ibm_http_client.time.sleep = lambda x: x with pytest.raises(Exception) as excinfo: _ibm_http_client.send(json_qasm, device="ibmqx4", - user="test", password="test", - shots=1, verbose=False) + token="test", + shots=1, + num_retries=10, + verbose=False) assert "123e" in str(excinfo.value) # check that job id is in exception assert tries[0] > 0 def test_retrieve_and_device_offline_exception(monkeypatch): - qasms = {'qasms': [{'qasm': 'my qasm'}]} - json_qasm = json.dumps(qasms) request_num = [0] def mocked_requests_get(*args, **kwargs): @@ -376,15 +535,41 @@ def raise_for_status(self): pass # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url, status_url) and request_num[0] < 2: - return MockResponse({"state": True, "lengthQueue": 10}, 200) - elif args[0] == urljoin(_api_url, status_url): - return MockResponse({"state": False}, 200) - job_url = 'Jobs/{}'.format("123e") - if args[0] == urljoin(_api_url, job_url): + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url) and request_num[0] < 2: + return MockResponse([{ + 'backend_name': 'ibmqx4', + 'coupling_map': None, + 'backend_version': '0.1.547', + 'n_qubits': 32 + }], 200) + elif args[1] == urljoin( + _API_URL, + status_url): # ibmqx4 gets disconnected, replaced by ibmqx5 + return MockResponse([{ + 'backend_name': 'ibmqx5', + 'coupling_map': None, + 'backend_version': '0.1.547', + 'n_qubits': 32 + }], 200) + job_url = "Network/ibm-q/Groups/open/Projects/main/Jobs/{}".format( + "123e") + err_url = "Network/ibm-q/Groups/open/Projects/main/Jobs/{}".format( + "123ee") + if args[1] == urljoin(_API_URL, job_url): + request_num[0] += 1 + return MockResponse( + { + "status": "RUNNING", + 'iteration': request_num[0] + }, 200) + if args[1] == urljoin(_API_URL, err_url): request_num[0] += 1 - return MockResponse({"noqasms": "not done"}, 200) + return MockResponse( + { + "status": "TERMINATED", + 'iteration': request_num[0] + }, 400) def mocked_requests_post(*args, **kwargs): class MockRequest: @@ -403,22 +588,26 @@ def json(self): def raise_for_status(self): pass - login_url = 'users/login' - if args[0] == urljoin(_api_url, login_url): + if args[1] == _AUTH_API_URL: return MockPostResponse({"userId": "1", "id": "12"}) - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + _ibm_http_client.time.sleep = lambda x: x with pytest.raises(_ibm_http_client.DeviceOfflineError): _ibm_http_client.retrieve(device="ibmqx4", - user="test", password="test", - jobid="123e") + token="test", + jobid="123e", + num_retries=200) + with pytest.raises(Exception): + _ibm_http_client.retrieve(device="ibmqx4", + token="test", + jobid="123ee", + num_retries=200) def test_retrieve(monkeypatch): - qasms = {'qasms': [{'qasm': 'my qasm'}]} - json_qasm = json.dumps(qasms) request_num = [0] def mocked_requests_get(*args, **kwargs): @@ -434,16 +623,28 @@ def raise_for_status(self): pass # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url, status_url): - return MockResponse({"state": True}, 200) - job_url = 'Jobs/{}'.format("123e") - if args[0] == urljoin(_api_url, job_url) and request_num[0] < 1: + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url): + return MockResponse([{ + 'backend_name': 'ibmqx4', + 'coupling_map': None, + 'backend_version': '0.1.547', + 'n_qubits': 32 + }], 200) + job_url = 'Network/ibm-q/Groups/open/Projects/main/Jobs/{}'.format( + "123e") + if args[1] == urljoin(_API_URL, job_url) and request_num[0] < 1: request_num[0] += 1 - return MockResponse({"noqasms": "not done"}, 200) - elif args[0] == urljoin(_api_url, job_url): - return MockResponse({"qasms": [{'qasm': 'qasm', - 'result': 'correct'}]}, 200) + return MockResponse({"status": "RUNNING"}, 200) + elif args[1] == urljoin(_API_URL, job_url): + return MockResponse( + { + "qObjectResult": { + 'qasm': 'qasm', + 'results': ['correct'] + }, + "status": "COMPLETED" + }, 200) def mocked_requests_post(*args, **kwargs): class MockRequest: @@ -462,14 +663,14 @@ def json(self): def raise_for_status(self): pass - login_url = 'users/login' - if args[0] == urljoin(_api_url, login_url): + if args[1] == _AUTH_API_URL: return MockPostResponse({"userId": "1", "id": "12"}) - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + _ibm_http_client.time.sleep = lambda x: x res = _ibm_http_client.retrieve(device="ibmqx4", - user="test", password="test", + token="test", jobid="123e") assert res == 'correct' diff --git a/projectq/backends/_ibm/_ibm_test.py b/projectq/backends/_ibm/_ibm_test.py index df1652b7a..f6890d34c 100755 --- a/projectq/backends/_ibm/_ibm_test.py +++ b/projectq/backends/_ibm/_ibm_test.py @@ -11,27 +11,18 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.backends._ibm._ibm.py.""" import pytest -import json - -import projectq.setups.decompositions +import math +from projectq.setups import restrictedgateset from projectq import MainEngine from projectq.backends._ibm import _ibm -from projectq.cengines import (TagRemover, - LocalOptimizer, - AutoReplacer, - IBM5QubitMapper, - SwapAndCNOTFlipper, - DummyEngine, - DecompositionRuleSet) +from projectq.cengines import (BasicMapperEngine, DummyEngine) + from projectq.ops import (All, Allocate, Barrier, Command, Deallocate, Entangle, Measure, NOT, Rx, Ry, Rz, S, Sdag, T, Tdag, - X, Y, Z) - -from projectq.setups.ibm import ibmqx4_connections + X, Y, Z, H, CNOT) # Insure that no HTTP request can be made in all tests in this module @@ -40,31 +31,29 @@ def no_requests(monkeypatch): monkeypatch.delattr("requests.sessions.Session.request") -_api_url = 'https://quantumexperience.ng.bluemix.net/api/' -_api_url_status = 'https://quantumexperience.ng.bluemix.net/api/' - - -@pytest.mark.parametrize("single_qubit_gate, is_available", [ - (X, True), (Y, True), (Z, True), (T, True), (Tdag, True), (S, True), - (Sdag, True), (Allocate, True), (Deallocate, True), (Measure, True), - (NOT, True), (Rx(0.5), True), (Ry(0.5), True), (Rz(0.5), True), - (Barrier, True), (Entangle, False)]) +@pytest.mark.parametrize("single_qubit_gate, is_available", + [(X, False), (Y, False), (Z, False), (H, True), + (T, False), (Tdag, False), (S, False), (Sdag, False), + (Allocate, True), (Deallocate, True), + (Measure, True), (NOT, False), (Rx(0.5), True), + (Ry(0.5), True), (Rz(0.5), True), (Barrier, True), + (Entangle, False)]) def test_ibm_backend_is_available(single_qubit_gate, is_available): eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) qubit1 = eng.allocate_qubit() ibm_backend = _ibm.IBMBackend() - cmd = Command(eng, single_qubit_gate, (qubit1,)) + cmd = Command(eng, single_qubit_gate, (qubit1, )) assert ibm_backend.is_available(cmd) == is_available -@pytest.mark.parametrize("num_ctrl_qubits, is_available", [ - (0, True), (1, True), (2, False), (3, False)]) +@pytest.mark.parametrize("num_ctrl_qubits, is_available", + [(0, False), (1, True), (2, False), (3, False)]) def test_ibm_backend_is_available_control_not(num_ctrl_qubits, is_available): eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) qubit1 = eng.allocate_qubit() qureg = eng.allocate_qureg(num_ctrl_qubits) ibm_backend = _ibm.IBMBackend() - cmd = Command(eng, NOT, (qubit1,), controls=qureg) + cmd = Command(eng, NOT, (qubit1, ), controls=qureg) assert ibm_backend.is_available(cmd) == is_available @@ -83,14 +72,17 @@ def test_ibm_sent_error(monkeypatch): # patch send def mock_send(*args, **kwargs): raise TypeError - monkeypatch.setattr(_ibm, "send", mock_send) + monkeypatch.setattr(_ibm, "send", mock_send) backend = _ibm.IBMBackend(verbose=True) - eng = MainEngine(backend=backend, - engine_list=[IBM5QubitMapper(), - SwapAndCNOTFlipper(set())]) + mapper = BasicMapperEngine() + res = dict() + for i in range(4): + res[i] = i + mapper.current_mapping = res + eng = MainEngine(backend=backend, engine_list=[mapper]) qubit = eng.allocate_qubit() - X | qubit + Rx(math.pi) | qubit with pytest.raises(Exception): qubit[0].__del__() eng.flush() @@ -100,26 +92,80 @@ def mock_send(*args, **kwargs): eng.next_engine = dummy +def test_ibm_sent_error_2(monkeypatch): + backend = _ibm.IBMBackend(verbose=True) + mapper = BasicMapperEngine() + res = dict() + for i in range(4): + res[i] = i + mapper.current_mapping = res + eng = MainEngine(backend=backend, engine_list=[mapper]) + qubit = eng.allocate_qubit() + Rx(math.pi) | qubit + + with pytest.raises(Exception): + S | qubit # no setup to decompose S gate, so not accepted by the backend + dummy = DummyEngine() + dummy.is_last_engine = True + eng.next_engine = dummy + + def test_ibm_retrieve(monkeypatch): # patch send def mock_retrieve(*args, **kwargs): - return {'date': '2017-01-19T14:28:47.622Z', - 'data': {'time': 14.429004907608032, 'counts': {'00111': 396, - '00101': 27, - '00000': 601}, - 'qasm': ('...')}} + return { + 'data': { + 'counts': { + '0x0': 504, + '0x2': 8, + '0xc': 6, + '0xe': 482 + } + }, + 'header': { + 'clbit_labels': [['c', 0], ['c', 1], ['c', 2], ['c', 3]], + 'creg_sizes': [['c', 4]], + 'memory_slots': + 4, + 'n_qubits': + 32, + 'name': + 'circuit0', + 'qreg_sizes': [['q', 32]], + 'qubit_labels': [['q', 0], ['q', 1], ['q', 2], ['q', 3], + ['q', 4], ['q', 5], ['q', 6], ['q', 7], + ['q', 8], ['q', 9], ['q', 10], ['q', 11], + ['q', 12], ['q', 13], ['q', 14], ['q', 15], + ['q', 16], ['q', 17], ['q', 18], ['q', 19], + ['q', 20], ['q', 21], ['q', 22], ['q', 23], + ['q', 24], ['q', 25], ['q', 26], ['q', 27], + ['q', 28], ['q', 29], ['q', 30], ['q', 31]] + }, + 'metadata': { + 'measure_sampling': True, + 'method': 'statevector', + 'parallel_shots': 1, + 'parallel_state_update': 16 + }, + 'seed_simulator': 465435780, + 'shots': 1000, + 'status': 'DONE', + 'success': True, + 'time_taken': 0.0045786460000000005 + } + monkeypatch.setattr(_ibm, "retrieve", mock_retrieve) - backend = _ibm.IBMBackend(retrieve_execution="ab1s2") - rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions]) - connectivity = set([(1, 2), (2, 4), (0, 2), (3, 2), (4, 3), (0, 1)]) - engine_list = [TagRemover(), - LocalOptimizer(10), - AutoReplacer(rule_set), - TagRemover(), - IBM5QubitMapper(), - SwapAndCNOTFlipper(connectivity), - LocalOptimizer(10)] - eng = MainEngine(backend=backend, engine_list=engine_list) + backend = _ibm.IBMBackend(retrieve_execution="ab1s2", num_runs=1000) + mapper = BasicMapperEngine() + res = dict() + for i in range(4): + res[i] = i + mapper.current_mapping = res + ibm_setup = [mapper] + setup = restrictedgateset.get_engine_list(one_qubit_gates=(Rx, Ry, Rz, H), + two_qubit_gates=(CNOT, )) + setup.extend(ibm_setup) + eng = MainEngine(backend=backend, engine_list=setup) unused_qubit = eng.allocate_qubit() qureg = eng.allocate_qureg(3) # entangle the qureg @@ -134,43 +180,135 @@ def mock_retrieve(*args, **kwargs): # run the circuit eng.flush() prob_dict = eng.backend.get_probabilities([qureg[0], qureg[2], qureg[1]]) - assert prob_dict['111'] == pytest.approx(0.38671875) - assert prob_dict['101'] == pytest.approx(0.0263671875) + assert prob_dict['000'] == pytest.approx(0.504) + assert prob_dict['111'] == pytest.approx(0.482) + assert prob_dict['011'] == pytest.approx(0.006) def test_ibm_backend_functional_test(monkeypatch): - correct_info = ('{"qasms": [{"qasm": "\\ninclude \\"qelib1.inc\\";' - '\\nqreg q[3];\\ncreg c[3];\\nh q[2];\\ncx q[2], q[0];' - '\\ncx q[2], q[1];\\ntdg q[2];\\nsdg q[2];' - '\\nbarrier q[2], q[0], q[1];' - '\\nu3(0.2, -pi/2, pi/2) q[2];\\nmeasure q[2] -> ' - 'c[2];\\nmeasure q[0] -> c[0];\\nmeasure q[1] -> c[1];"}]' - ', "shots": 1024, "maxCredits": 5, "backend": {"name": ' - '"simulator"}}') + correct_info = { + 'json': [{ + 'qubits': [1], + 'name': 'u2', + 'params': [0, 3.141592653589793] + }, { + 'qubits': [1, 2], + 'name': 'cx' + }, { + 'qubits': [1, 3], + 'name': 'cx' + }, { + 'qubits': [1], + 'name': 'u3', + 'params': [6.28318530718, 0, 0] + }, { + 'qubits': [1], + 'name': 'u1', + 'params': [11.780972450962] + }, { + 'qubits': [1], + 'name': 'u3', + 'params': [6.28318530718, 0, 0] + }, { + 'qubits': [1], + 'name': 'u1', + 'params': [10.995574287564] + }, { + 'qubits': [1, 2, 3], + 'name': 'barrier' + }, { + 'qubits': [1], + 'name': 'u3', + 'params': [0.2, -1.5707963267948966, 1.5707963267948966] + }, { + 'qubits': [1], + 'name': 'measure', + 'memory': [1] + }, { + 'qubits': [2], + 'name': 'measure', + 'memory': [2] + }, { + 'qubits': [3], + 'name': 'measure', + 'memory': [3] + }], + 'nq': + 4, + 'shots': + 1000, + 'maxCredits': + 10, + 'backend': { + 'name': 'ibmq_qasm_simulator' + } + } + # {'qasms': [{'qasm': '\ninclude "qelib1.inc";\nqreg q[4];\ncreg c[4];\nu2(0,pi/2) q[1];\ncx q[1], q[2];\ncx q[1], q[3];\nu3(6.28318530718, 0, 0) q[1];\nu1(11.780972450962) q[1];\nu3(6.28318530718, 0, 0) q[1];\nu1(10.995574287564) q[1];\nu3(0.2, -pi/2, pi/2) q[1];\nmeasure q[1] -> c[1];\nmeasure q[2] -> c[2];\nmeasure q[3] -> c[3];'}], 'json': [{'qubits': [1], 'name': 'u2', 'params': [0, 3.141592653589793]}, {'qubits': [1, 2], 'name': 'cx'}, {'qubits': [1, 3], 'name': 'cx'}, {'qubits': [1], 'name': 'u3', 'params': [6.28318530718, 0, 0]}, {'qubits': [1], 'name': 'u1', 'params': [11.780972450962]}, {'qubits': [1], 'name': 'u3', 'params': [6.28318530718, 0, 0]}, {'qubits': [1], 'name': 'u1', 'params': [10.995574287564]}, {'qubits': [1], 'name': 'u3', 'params': [0.2, -1.5707963267948966, 1.5707963267948966]}, {'qubits': [1], 'name': 'measure', 'memory': [1]}, {'qubits': [2], 'name': 'measure', 'memory': [2]}, {'qubits': [3], 'name': 'measure', 'memory': [3]}], 'nq': 4, 'shots': 1000, 'maxCredits': 10, 'backend': {'name': 'ibmq_qasm_simulator'}} def mock_send(*args, **kwargs): - assert json.loads(args[0]) == json.loads(correct_info) - return {'date': '2017-01-19T14:28:47.622Z', - 'data': {'time': 14.429004907608032, 'counts': {'00111': 396, - '00101': 27, - '00000': 601}, - 'qasm': ('...')}} + assert args[0] == correct_info + return { + 'data': { + 'counts': { + '0x0': 504, + '0x2': 8, + '0xc': 6, + '0xe': 482 + } + }, + 'header': { + 'clbit_labels': [['c', 0], ['c', 1], ['c', 2], ['c', 3]], + 'creg_sizes': [['c', 4]], + 'memory_slots': + 4, + 'n_qubits': + 32, + 'name': + 'circuit0', + 'qreg_sizes': [['q', 32]], + 'qubit_labels': [['q', 0], ['q', 1], ['q', 2], ['q', 3], + ['q', 4], ['q', 5], ['q', 6], ['q', 7], + ['q', 8], ['q', 9], ['q', 10], ['q', 11], + ['q', 12], ['q', 13], ['q', 14], ['q', 15], + ['q', 16], ['q', 17], ['q', 18], ['q', 19], + ['q', 20], ['q', 21], ['q', 22], ['q', 23], + ['q', 24], ['q', 25], ['q', 26], ['q', 27], + ['q', 28], ['q', 29], ['q', 30], ['q', 31]] + }, + 'metadata': { + 'measure_sampling': True, + 'method': 'statevector', + 'parallel_shots': 1, + 'parallel_state_update': 16 + }, + 'seed_simulator': 465435780, + 'shots': 1000, + 'status': 'DONE', + 'success': True, + 'time_taken': 0.0045786460000000005 + } + monkeypatch.setattr(_ibm, "send", mock_send) - backend = _ibm.IBMBackend(verbose=True) + backend = _ibm.IBMBackend(verbose=True, num_runs=1000) + import sys # no circuit has been executed -> raises exception with pytest.raises(RuntimeError): backend.get_probabilities([]) - rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions]) - - engine_list = [TagRemover(), - LocalOptimizer(10), - AutoReplacer(rule_set), - TagRemover(), - IBM5QubitMapper(), - SwapAndCNOTFlipper(ibmqx4_connections), - LocalOptimizer(10)] - eng = MainEngine(backend=backend, engine_list=engine_list) + mapper = BasicMapperEngine() + res = dict() + for i in range(4): + res[i] = i + mapper.current_mapping = res + ibm_setup = [mapper] + setup = restrictedgateset.get_engine_list(one_qubit_gates=(Rx, Ry, Rz, H), + two_qubit_gates=(CNOT, ), + other_gates=(Barrier, )) + setup.extend(ibm_setup) + eng = MainEngine(backend=backend, engine_list=setup) + # 4 qubits circuit is run, but first is unused to test ability for + # get_probability to return the correct values for a subset of the total + # register unused_qubit = eng.allocate_qubit() qureg = eng.allocate_qureg(3) # entangle the qureg @@ -184,9 +322,21 @@ def mock_send(*args, **kwargs): All(Measure) | qureg # run the circuit eng.flush() - prob_dict = eng.backend.get_probabilities([qureg[0], qureg[2], qureg[1]]) - assert prob_dict['111'] == pytest.approx(0.38671875) - assert prob_dict['101'] == pytest.approx(0.0263671875) + prob_dict = eng.backend.get_probabilities([qureg[2], qureg[1]]) + assert prob_dict['00'] == pytest.approx(0.512) + assert prob_dict['11'] == pytest.approx(0.488) + result = "\nu2(0,pi/2) q[1];\ncx q[1], q[2];\ncx q[1], q[3];" + if sys.version_info.major == 3: + result += "\nu3(6.28318530718, 0, 0) q[1];\nu1(11.780972450962) q[1];" + result += "\nu3(6.28318530718, 0, 0) q[1];\nu1(10.995574287564) q[1];" + else: + result += "\nu3(6.28318530718, 0, 0) q[1];\nu1(11.780972451) q[1];" + result += "\nu3(6.28318530718, 0, 0) q[1];\nu1(10.9955742876) q[1];" + result += "\nbarrier q[1], q[2], q[3];" + result += "\nu3(0.2, -pi/2, pi/2) q[1];\nmeasure q[1] -> c[1];" + result += "\nmeasure q[2] -> c[2];\nmeasure q[3] -> c[3];" + + assert eng.backend.get_qasm() == result with pytest.raises(RuntimeError): eng.backend.get_probabilities(eng.allocate_qubit()) diff --git a/projectq/backends/_resource_test.py b/projectq/backends/_resource_test.py index 664b687ad..cf7122c01 100755 --- a/projectq/backends/_resource_test.py +++ b/projectq/backends/_resource_test.py @@ -20,7 +20,7 @@ from projectq.cengines import DummyEngine, MainEngine, NotYetMeasuredError from projectq.meta import LogicalQubitIDTag -from projectq.ops import All, Allocate, CNOT, Command, H, Measure, QFT, Rz, X +from projectq.ops import All, Allocate, CNOT, Command, H, Measure, QFT, Rz, Rzz, X from projectq.types import WeakQubitRef from projectq.backends import ResourceCounter @@ -74,6 +74,7 @@ def test_resource_counter(): CNOT | (qubit1, qubit3) Rz(0.1) | qubit1 Rz(0.3) | qubit1 + Rzz(0.5) | qubit1 All(Measure) | qubit1 + qubit3 @@ -81,7 +82,7 @@ def test_resource_counter(): int(qubit1) assert resource_counter.max_width == 2 - assert resource_counter.depth_of_dag == 5 + assert resource_counter.depth_of_dag == 6 str_repr = str(resource_counter) assert str_repr.count(" HGate : 1") == 1 diff --git a/projectq/backends/_sim/_cppkernels/simulator.hpp b/projectq/backends/_sim/_cppkernels/simulator.hpp index c4e611eba..d248ed038 100755 --- a/projectq/backends/_sim/_cppkernels/simulator.hpp +++ b/projectq/backends/_sim/_cppkernels/simulator.hpp @@ -38,7 +38,7 @@ class Simulator{ public: using calc_type = double; using complex_type = std::complex; - using StateVector = std::vector>; + using StateVector = std::vector>; using Map = std::map; using RndEngine = std::mt19937; using Term = std::vector>; @@ -55,11 +55,18 @@ class Simulator{ void allocate_qubit(unsigned id){ if (map_.count(id) == 0){ map_[id] = N_++; - auto newvec = StateVector(1UL << N_); - #pragma omp parallel for schedule(static) + StateVector newvec; // avoid large memory allocations + if( tmpBuff1_.capacity() >= (1UL << N_) ) + std::swap(newvec, tmpBuff1_); + newvec.resize(1UL << N_); +#pragma omp parallel for schedule(static) for (std::size_t i = 0; i < newvec.size(); ++i) newvec[i] = (i < vec_.size())?vec_[i]:0.; - vec_ = std::move(newvec); + std::swap(vec_, newvec); + // recycle large memory + std::swap(tmpBuff1_, newvec); + if( tmpBuff1_.capacity() < tmpBuff2_.capacity() ) + std::swap(tmpBuff1_, tmpBuff2_); } else throw(std::runtime_error( @@ -113,12 +120,18 @@ class Simulator{ } } else{ - StateVector newvec((1UL << (N_-1))); - #pragma omp parallel for schedule(static) + StateVector newvec; // avoid costly memory reallocations + if( tmpBuff1_.capacity() >= (1UL << (N_-1)) ) + std::swap(tmpBuff1_, newvec); + newvec.resize((1UL << (N_-1))); + #pragma omp parallel for schedule(static) if(0) for (std::size_t i = 0; i < vec_.size(); i += 2*delta) std::copy_n(&vec_[i + static_cast(value)*delta], delta, &newvec[i/2]); - vec_ = std::move(newvec); + std::swap(vec_, newvec); + std::swap(tmpBuff1_, newvec); + if( tmpBuff1_.capacity() < tmpBuff2_.capacity() ) + std::swap(tmpBuff1_, tmpBuff2_); for (auto& p : map_){ if (p.second > pos) @@ -189,8 +202,8 @@ class Simulator{ } template - void apply_controlled_gate(M const& m, std::vector ids, - std::vector ctrl){ + void apply_controlled_gate(M const& m, const std::vector& ids, + const std::vector& ctrl){ auto fused_gates = fused_gates_; fused_gates.insert(m, ids, ctrl); @@ -209,8 +222,8 @@ class Simulator{ } template - void emulate_math(F const& f, QuReg quregs, std::vector ctrl, - unsigned num_threads=1){ + void emulate_math(F const& f, QuReg quregs, const std::vector& ctrl, + bool parallelize = false){ run(); auto ctrlmask = get_control_mask(ctrl); @@ -218,37 +231,76 @@ class Simulator{ for (unsigned j = 0; j < quregs[i].size(); ++j) quregs[i][j] = map_[quregs[i][j]]; - StateVector newvec(vec_.size(), 0.); - std::vector res(quregs.size()); - - #pragma omp parallel for schedule(static) firstprivate(res) num_threads(num_threads) - for (std::size_t i = 0; i < vec_.size(); ++i){ - if ((ctrlmask&i) == ctrlmask){ - for (unsigned qr_i = 0; qr_i < quregs.size(); ++qr_i){ - res[qr_i] = 0; - for (unsigned qb_i = 0; qb_i < quregs[qr_i].size(); ++qb_i) - res[qr_i] |= ((i >> quregs[qr_i][qb_i])&1) << qb_i; - } - f(res); - auto new_i = i; - for (unsigned qr_i = 0; qr_i < quregs.size(); ++qr_i){ - for (unsigned qb_i = 0; qb_i < quregs[qr_i].size(); ++qb_i){ - if (!(((new_i >> quregs[qr_i][qb_i])&1) == ((res[qr_i] >> qb_i)&1))) - new_i ^= (1UL << quregs[qr_i][qb_i]); - } - } - newvec[new_i] += vec_[i]; - } - else - newvec[i] += vec_[i]; + StateVector newvec; // avoid costly memory reallocations + if( tmpBuff1_.capacity() >= vec_.size() ) + std::swap(newvec, tmpBuff1_); + newvec.resize(vec_.size()); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < vec_.size(); i++) + newvec[i] = 0; + +//#pragma omp parallel reduction(+:newvec[:newvec.size()]) if(parallelize) // requires OpenMP 4.5 + { + std::vector res(quregs.size()); + //#pragma omp for schedule(static) + for (std::size_t i = 0; i < vec_.size(); ++i){ + if ((ctrlmask&i) == ctrlmask){ + for (unsigned qr_i = 0; qr_i < quregs.size(); ++qr_i){ + res[qr_i] = 0; + for (unsigned qb_i = 0; qb_i < quregs[qr_i].size(); ++qb_i) + res[qr_i] |= ((i >> quregs[qr_i][qb_i])&1) << qb_i; + } + f(res); + auto new_i = i; + for (unsigned qr_i = 0; qr_i < quregs.size(); ++qr_i){ + for (unsigned qb_i = 0; qb_i < quregs[qr_i].size(); ++qb_i){ + if (!(((new_i >> quregs[qr_i][qb_i])&1) == ((res[qr_i] >> qb_i)&1))) + new_i ^= (1UL << quregs[qr_i][qb_i]); + } + } + newvec[new_i] += vec_[i]; + } + else + newvec[i] += vec_[i]; + } } - vec_ = std::move(newvec); + std::swap(vec_, newvec); + std::swap(tmpBuff1_, newvec); + } + + // faster version without calling python + template + inline void emulate_math_addConstant(int a, const QuReg& quregs, const std::vector& ctrl) + { + emulate_math([a](std::vector &res){for(auto& x: res) x = x + a;}, quregs, ctrl, true); + } + + // faster version without calling python + template + inline void emulate_math_addConstantModN(int a, int N, const QuReg& quregs, const std::vector& ctrl) + { + emulate_math([a,N](std::vector &res){for(auto& x: res) x = (x + a) % N;}, quregs, ctrl, true); + } + + // faster version without calling python + template + inline void emulate_math_multiplyByConstantModN(int a, int N, const QuReg& quregs, const std::vector& ctrl) + { + emulate_math([a,N](std::vector &res){for(auto& x: res) x = (x * a) % N;}, quregs, ctrl, true); } calc_type get_expectation_value(TermsDict const& td, std::vector const& ids){ run(); calc_type expectation = 0.; - auto current_state = vec_; + + StateVector current_state; // avoid costly memory reallocations + if( tmpBuff1_.capacity() >= vec_.size() ) + std::swap(tmpBuff1_, current_state); + current_state.resize(vec_.size()); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < vec_.size(); ++i) + current_state[i] = vec_[i]; + for (auto const& term : td){ auto const& coefficient = term.second; apply_term(term.first, ids, {}); @@ -260,17 +312,29 @@ class Simulator{ auto const a2 = std::real(vec_[i]); auto const b2 = std::imag(vec_[i]); delta += a1 * a2 - b1 * b2; + // reset vec_ + vec_[i] = current_state[i]; } expectation += coefficient * delta; - vec_ = current_state; } + std::swap(current_state, tmpBuff1_); return expectation; } void apply_qubit_operator(ComplexTermsDict const& td, std::vector const& ids){ run(); - auto new_state = StateVector(vec_.size(), 0.); - auto current_state = vec_; + StateVector new_state, current_state; // avoid costly memory reallocations + if( tmpBuff1_.capacity() >= vec_.size() ) + std::swap(tmpBuff1_, new_state); + if( tmpBuff2_.capacity() >= vec_.size() ) + std::swap(tmpBuff2_, current_state); + new_state.resize(vec_.size()); + current_state.resize(vec_.size()); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < vec_.size(); ++i){ + new_state[i] = 0; + current_state[i] = vec_[i]; + } for (auto const& term : td){ auto const& coefficient = term.second; apply_term(term.first, ids, {}); @@ -280,7 +344,9 @@ class Simulator{ vec_[i] = current_state[i]; } } - vec_ = std::move(new_state); + std::swap(vec_, new_state); + std::swap(tmpBuff1_, new_state); + std::swap(tmpBuff2_, current_state); } calc_type get_probability(std::vector const& bit_string, @@ -452,6 +518,8 @@ class Simulator{ #pragma omp parallel kernel(vec_, ids[4], ids[3], ids[2], ids[1], ids[0], m, ctrlmask); break; + default: + throw std::invalid_argument("Gates with more than 5 qubits are not supported!"); } fused_gates_ = Fusion(); @@ -500,6 +568,12 @@ class Simulator{ unsigned fusion_qubits_min_, fusion_qubits_max_; RndEngine rnd_eng_; std::function rng_; + + // large array buffers to avoid costly reallocations + static StateVector tmpBuff1_, tmpBuff2_; }; +Simulator::StateVector Simulator::tmpBuff1_; +Simulator::StateVector Simulator::tmpBuff2_; + #endif diff --git a/projectq/backends/_sim/_cppsim.cpp b/projectq/backends/_sim/_cppsim.cpp index 74498d4e2..cab68d0ee 100755 --- a/projectq/backends/_sim/_cppsim.cpp +++ b/projectq/backends/_sim/_cppsim.cpp @@ -50,6 +50,9 @@ PYBIND11_PLUGIN(_cppsim) { .def("measure_qubits", &Simulator::measure_qubits_return) .def("apply_controlled_gate", &Simulator::apply_controlled_gate) .def("emulate_math", &emulate_math_wrapper) + .def("emulate_math_addConstant", &Simulator::emulate_math_addConstant) + .def("emulate_math_addConstantModN", &Simulator::emulate_math_addConstantModN) + .def("emulate_math_multiplyByConstantModN", &Simulator::emulate_math_multiplyByConstantModN) .def("get_expectation_value", &Simulator::get_expectation_value) .def("apply_qubit_operator", &Simulator::apply_qubit_operator) .def("emulate_time_evolution", &Simulator::emulate_time_evolution) diff --git a/projectq/backends/_sim/_simulator.py b/projectq/backends/_sim/_simulator.py index 2218c3471..19e884d6d 100755 --- a/projectq/backends/_sim/_simulator.py +++ b/projectq/backends/_sim/_simulator.py @@ -33,10 +33,12 @@ TimeEvolution) from projectq.types import WeakQubitRef +FALLBACK_TO_PYSIM = False try: from ._cppsim import Simulator as SimulatorBackend except ImportError: from ._pysim import Simulator as SimulatorBackend + FALLBACK_TO_PYSIM = True class Simulator(BasicEngine): @@ -384,14 +386,34 @@ def _handle(self, cmd): ID = cmd.qubits[0][0].id self._simulator.deallocate_qubit(ID) elif isinstance(cmd.gate, BasicMathGate): + # improve performance by using C++ code for some commomn gates + from projectq.libs.math import (AddConstant, + AddConstantModN, + MultiplyByConstantModN) qubitids = [] for qr in cmd.qubits: qubitids.append([]) for qb in qr: qubitids[-1].append(qb.id) - math_fun = cmd.gate.get_math_function(cmd.qubits) - self._simulator.emulate_math(math_fun, qubitids, - [qb.id for qb in cmd.control_qubits]) + if FALLBACK_TO_PYSIM: + math_fun = cmd.gate.get_math_function(cmd.qubits) + self._simulator.emulate_math(math_fun, qubitids, + [qb.id for qb in cmd.control_qubits]) + else: + # individual code for different standard gates to make it faster! + if isinstance(cmd.gate, AddConstant): + self._simulator.emulate_math_addConstant(cmd.gate.a, qubitids, + [qb.id for qb in cmd.control_qubits]) + elif isinstance(cmd.gate, AddConstantModN): + self._simulator.emulate_math_addConstantModN(cmd.gate.a, cmd.gate.N, qubitids, + [qb.id for qb in cmd.control_qubits]) + elif isinstance(cmd.gate, MultiplyByConstantModN): + self._simulator.emulate_math_multiplyByConstantModN(cmd.gate.a, cmd.gate.N, qubitids, + [qb.id for qb in cmd.control_qubits]) + else: + math_fun = cmd.gate.get_math_function(cmd.qubits) + self._simulator.emulate_math(math_fun, qubitids, + [qb.id for qb in cmd.control_qubits]) elif isinstance(cmd.gate, TimeEvolution): op = [(list(term), coeff) for (term, coeff) in cmd.gate.hamiltonian.terms.items()] diff --git a/projectq/backends/_sim/_simulator_test.py b/projectq/backends/_sim/_simulator_test.py index 546ab3584..9f7d298cb 100755 --- a/projectq/backends/_sim/_simulator_test.py +++ b/projectq/backends/_sim/_simulator_test.py @@ -30,8 +30,8 @@ from projectq.cengines import (BasicEngine, BasicMapperEngine, DummyEngine, LocalOptimizer, NotYetMeasuredError) from projectq.ops import (All, Allocate, BasicGate, BasicMathGate, CNOT, - Command, H, Measure, QubitOperator, Rx, Ry, Rz, S, - TimeEvolution, Toffoli, X, Y, Z) + Command, H, MatrixGate, Measure, QubitOperator, + Rx, Ry, Rz, S, TimeEvolution, Toffoli, X, Y, Z) from projectq.meta import Control, Dagger, LogicalQubitIDTag from projectq.types import WeakQubitRef @@ -97,37 +97,32 @@ def receive(self, command_list): return None -class Mock1QubitGate(BasicGate): - def __init__(self): - BasicGate.__init__(self) - self.cnt = 0 +class Mock1QubitGate(MatrixGate): + def __init__(self): + MatrixGate.__init__(self) + self.cnt = 0 - @property - def matrix(self): - self.cnt += 1 - return [[0, 1], [1, 0]] + @property + def matrix(self): + self.cnt += 1 + return [[0, 1], [1, 0]] -class Mock6QubitGate(BasicGate): - def __init__(self): - BasicGate.__init__(self) - self.cnt = 0 +class Mock6QubitGate(MatrixGate): + def __init__(self): + MatrixGate.__init__(self) + self.cnt = 0 - @property - def matrix(self): - self.cnt += 1 - return numpy.eye(2 ** 6) + @property + def matrix(self): + self.cnt += 1 + return numpy.eye(2 ** 6) class MockNoMatrixGate(BasicGate): - def __init__(self): - BasicGate.__init__(self) - self.cnt = 0 - - @property - def matrix(self): - self.cnt += 1 - raise AttributeError + def __init__(self): + BasicGate.__init__(self) + self.cnt = 0 def test_simulator_is_available(sim): @@ -147,15 +142,15 @@ def test_simulator_is_available(sim): new_cmd.gate = Mock1QubitGate() assert sim.is_available(new_cmd) - assert new_cmd.gate.cnt == 4 + assert new_cmd.gate.cnt == 1 new_cmd.gate = Mock6QubitGate() assert not sim.is_available(new_cmd) - assert new_cmd.gate.cnt == 4 + assert new_cmd.gate.cnt == 1 new_cmd.gate = MockNoMatrixGate() assert not sim.is_available(new_cmd) - assert new_cmd.gate.cnt == 7 + assert new_cmd.gate.cnt == 0 def test_simulator_cheat(sim): @@ -688,3 +683,55 @@ def receive(command_list): qubit1[0].id: qubit0[0].id} assert (sim._convert_logical_to_mapped_qureg(qubit0 + qubit1) == qubit1 + qubit0) + + +def test_simulator_constant_math_emulation(): + if "cpp_simulator" not in get_available_simulators(): + pytest.skip("No C++ simulator") + return + + results = [[[1, 1, 0, 0, 0]], [[0, 1, 0, 0, 0]], [[0, 1, 1, 1, 0]]] + + import projectq.backends._sim._simulator as _sim + from projectq.backends._sim._pysim import Simulator as PySim + from projectq.backends._sim._cppsim import Simulator as CppSim + from projectq.libs.math import (AddConstant, AddConstantModN, + MultiplyByConstantModN) + + def gate_filter(eng, cmd): + g = cmd.gate + if isinstance(g, BasicMathGate): + return False + return eng.next_engine.is_available(cmd) + + def run_simulation(sim): + eng = MainEngine(sim, []) + quint = eng.allocate_qureg(5) + AddConstant(3) | quint + All(Measure) | quint + eng.flush() + results[0].append([int(qb) for qb in quint]) + + AddConstantModN(4, 5) | quint + All(Measure) | quint + eng.flush() + results[1].append([int(qb) for qb in quint]) + + MultiplyByConstantModN(15, 16) | quint + All(Measure) | quint + eng.flush() + results[2].append([int(qb) for qb in quint]) + + cppsim = Simulator(gate_fusion=False) + cppsim._simulator = CppSim(1) + run_simulation(cppsim) + + _sim.FALLBACK_TO_PYSIM = True + pysim = Simulator() + pysim._simulator = PySim(1) + # run_simulation(pysim) + + for result in results: + ref = result[0] + for res in result[1:]: + assert ref == res diff --git a/projectq/cengines/_basicmapper.py b/projectq/cengines/_basicmapper.py index 4d4cef177..5fc0f9a81 100644 --- a/projectq/cengines/_basicmapper.py +++ b/projectq/cengines/_basicmapper.py @@ -81,3 +81,7 @@ def add_logical_id(command, old_tags=deepcopy(cmd.tags)): drop_engine_after(self) else: self.send([new_cmd]) + + def receive(self, command_list): + for cmd in command_list: + self._send_cmd_with_mapped_ids(cmd) diff --git a/projectq/cengines/_ibm5qubitmapper.py b/projectq/cengines/_ibm5qubitmapper.py index 7a2659a30..2c85749d2 100755 --- a/projectq/cengines/_ibm5qubitmapper.py +++ b/projectq/cengines/_ibm5qubitmapper.py @@ -11,12 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Contains a compiler engine to map to the 5-qubit IBM chip """ -from copy import deepcopy - import itertools from projectq.cengines import BasicMapperEngine @@ -39,8 +36,7 @@ class IBM5QubitMapper(BasicMapperEngine): without performing Swaps, the mapping procedure **raises an Exception**. """ - - def __init__(self): + def __init__(self, connections=None): """ Initialize an IBM 5-qubit mapper compiler engine. @@ -49,6 +45,16 @@ def __init__(self): BasicMapperEngine.__init__(self) self.current_mapping = dict() self._reset() + self._cmds = [] + self._interactions = dict() + + if connections is None: + #general connectivity easier for testing functions + self.connections = set([(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), + (2, 1), (2, 3), (2, 4), (3, 1), (3, 4), + (4, 3)]) + else: + self.connections = connections def is_available(self, cmd): """ @@ -67,17 +73,6 @@ def _reset(self): self._cmds = [] self._interactions = dict() - def _is_cnot(self, cmd): - """ - Check if the command corresponds to a CNOT (controlled NOT gate). - - Args: - cmd (Command): Command to check whether it is a controlled NOT - gate. - """ - return (isinstance(cmd.gate, NOT.__class__) and - get_control_count(cmd) == 1) - def _determine_cost(self, mapping): """ Determines the cost of the circuit with the given mapping. @@ -90,15 +85,15 @@ def _determine_cost(self, mapping): Cost measure taking into account CNOT directionality or None if the circuit cannot be executed given the mapping. """ - from projectq.setups.ibm import ibmqx4_connections as connections + cost = 0 for tpl in self._interactions: ctrl_id = tpl[0] target_id = tpl[1] ctrl_pos = mapping[ctrl_id] target_pos = mapping[target_id] - if not (ctrl_pos, target_pos) in connections: - if (target_pos, ctrl_pos) in connections: + if not (ctrl_pos, target_pos) in self.connections: + if (target_pos, ctrl_pos) in self.connections: cost += self._interactions[tpl] else: return None @@ -114,20 +109,22 @@ def _run(self): the mapping was already determined but more CNOTs get sent down the pipeline. """ - if (len(self.current_mapping) > 0 and - max(self.current_mapping.values()) > 4): + if (len(self.current_mapping) > 0 + and max(self.current_mapping.values()) > 4): raise RuntimeError("Too many qubits allocated. The IBM Q " "device supports at most 5 qubits and no " "intermediate measurements / " "reallocations.") if len(self._interactions) > 0: - logical_ids = [qbid for qbid in self.current_mapping] + logical_ids = list(self.current_mapping) best_mapping = self.current_mapping best_cost = None for physical_ids in itertools.permutations(list(range(5)), len(logical_ids)): - mapping = {logical_ids[i]: physical_ids[i] - for i in range(len(logical_ids))} + mapping = { + logical_ids[i]: physical_ids[i] + for i in range(len(logical_ids)) + } new_cost = self._determine_cost(mapping) if new_cost is not None: if best_cost is None or new_cost < best_cost: @@ -153,7 +150,7 @@ def _store(self, cmd): """ if not cmd.gate == FlushGate(): target = cmd.qubits[0][0].id - if self._is_cnot(cmd): + if _is_cnot(cmd): # CNOT encountered ctrl = cmd.control_qubits[0].id if not (ctrl, target) in self._interactions: @@ -187,3 +184,15 @@ def receive(self, command_list): if isinstance(cmd.gate, FlushGate): self._run() self._reset() + + +def _is_cnot(cmd): + """ + Check if the command corresponds to a CNOT (controlled NOT gate). + + Args: + cmd (Command): Command to check whether it is a controlled NOT + gate. + """ + return (isinstance(cmd.gate, NOT.__class__) + and get_control_count(cmd) == 1) diff --git a/projectq/cengines/_ibm5qubitmapper_test.py b/projectq/cengines/_ibm5qubitmapper_test.py index 5c4c4c4da..ea6d383b6 100755 --- a/projectq/cengines/_ibm5qubitmapper_test.py +++ b/projectq/cengines/_ibm5qubitmapper_test.py @@ -11,14 +11,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._ibm5qubitmapper.py.""" import pytest from projectq import MainEngine from projectq.cengines import DummyEngine -from projectq.ops import H, CNOT, X, Measure, All +from projectq.ops import H, CNOT, All from projectq.cengines import _ibm5qubitmapper, SwapAndCNOTFlipper from projectq.backends import IBMBackend @@ -28,15 +27,20 @@ def test_ibm5qubitmapper_is_available(monkeypatch): # Test that IBM5QubitMapper calls IBMBackend if gate is available. def mock_send(*args, **kwargs): return "Yes" + monkeypatch.setattr(_ibm5qubitmapper.IBMBackend, "is_available", mock_send) mapper = _ibm5qubitmapper.IBM5QubitMapper() assert mapper.is_available("TestCommand") == "Yes" def test_ibm5qubitmapper_invalid_circuit(): + connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=backend, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -51,9 +55,13 @@ def test_ibm5qubitmapper_invalid_circuit(): def test_ibm5qubitmapper_valid_circuit1(): + connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=backend, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -70,9 +78,13 @@ def test_ibm5qubitmapper_valid_circuit1(): def test_ibm5qubitmapper_valid_circuit2(): + connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=backend, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -89,6 +101,7 @@ def test_ibm5qubitmapper_valid_circuit2(): def test_ibm5qubitmapper_valid_circuit2_ibmqx4(): + connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) backend = DummyEngine(save_commands=True) class FakeIBMBackend(IBMBackend): @@ -99,8 +112,11 @@ class FakeIBMBackend(IBMBackend): fake.is_available = backend.is_available backend.is_last_engine = True - eng = MainEngine(backend=fake, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=fake, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -119,9 +135,12 @@ class FakeIBMBackend(IBMBackend): def test_ibm5qubitmapper_optimizeifpossible(): backend = DummyEngine(save_commands=True) connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper(), - SwapAndCNOTFlipper(connectivity)]) + eng = MainEngine( + backend=backend, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity), + SwapAndCNOTFlipper(connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -158,8 +177,10 @@ def test_ibm5qubitmapper_toomanyqubits(): backend = DummyEngine(save_commands=True) connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper(), - SwapAndCNOTFlipper(connectivity)]) + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(), + SwapAndCNOTFlipper(connectivity) + ]) qubits = eng.allocate_qureg(6) All(H) | qubits CNOT | (qubits[0], qubits[1]) diff --git a/projectq/cengines/_optimize.py b/projectq/cengines/_optimize.py index 2e72540b9..0c9765288 100755 --- a/projectq/cengines/_optimize.py +++ b/projectq/cengines/_optimize.py @@ -116,7 +116,7 @@ def _get_gate_indices(self, idx, i, IDs): def _optimize(self, idx, lim=None): """ - Try to merge or even cancel successive gates using the get_merged and + Try to remove identity gates using the is_identity function, then merge or even cancel successive gates using the get_merged and get_inverse functions of the gate (see, e.g., BasicRotationGate). It does so for all qubit command lists. @@ -130,6 +130,20 @@ def _optimize(self, idx, lim=None): new_gateloc = limit while i < limit - 1: + # can be dropped if the gate is equivalent to an identity gate + if self._l[idx][i].is_identity(): + # determine index of this gate on all qubits + qubitids = [qb.id for sublist in self._l[idx][i].all_qubits + for qb in sublist] + gid = self._get_gate_indices(idx, i, qubitids) + for j in range(len(qubitids)): + new_list = (self._l[qubitids[j]][0:gid[j]] + + self._l[qubitids[j]][gid[j] +1:]) + self._l[qubitids[j]] = new_list + i = 0 + limit -= 1 + continue + # can be dropped if two in a row are self-inverses inv = self._l[idx][i].get_inverse() diff --git a/projectq/cengines/_optimize_test.py b/projectq/cengines/_optimize_test.py index e0196f83b..121dbb471 100755 --- a/projectq/cengines/_optimize_test.py +++ b/projectq/cengines/_optimize_test.py @@ -16,6 +16,7 @@ import pytest +import math from projectq import MainEngine from projectq.cengines import DummyEngine from projectq.ops import (CNOT, H, Rx, Ry, AllocateQubitGate, X, @@ -127,3 +128,22 @@ def test_local_optimizer_mergeable_gates(): # Expect allocate, one Rx gate, and flush gate assert len(backend.received_commands) == 3 assert backend.received_commands[1].gate == Rx(10 * 0.5) + + +def test_local_optimizer_identity_gates(): + local_optimizer = _optimize.LocalOptimizer(m=4) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + # Test that it merges mergeable gates such as Rx + qb0 = eng.allocate_qubit() + for _ in range(10): + Rx(0.0) | qb0 + Ry(0.0) | qb0 + Rx(4*math.pi) | qb0 + Ry(4*math.pi) | qb0 + Rx(0.5) | qb0 + assert len(backend.received_commands) == 0 + eng.flush() + # Expect allocate, one Rx gate, and flush gate + assert len(backend.received_commands) == 3 + assert backend.received_commands[1].gate == Rx(0.5) diff --git a/projectq/cengines/_replacer/_replacer.py b/projectq/cengines/_replacer/_replacer.py index 29c883123..85fa303dc 100755 --- a/projectq/cengines/_replacer/_replacer.py +++ b/projectq/cengines/_replacer/_replacer.py @@ -175,7 +175,6 @@ def _process_command(self, cmd): # use decomposition chooser to determine the best decomposition chosen_decomp = self._decomp_chooser(cmd, decomp_list) - # the decomposed command must have the same tags # (plus the ones it gets from meta-statements inside the # decomposition rule). diff --git a/projectq/libs/math/_gates.py b/projectq/libs/math/_gates.py index fe1df6784..ef5cade99 100755 --- a/projectq/libs/math/_gates.py +++ b/projectq/libs/math/_gates.py @@ -90,6 +90,14 @@ class AddConstantModN(BasicMathGate): qunum = eng.allocate_qureg(5) # 5-qubit number X | qunum[1] # qunum is now equal to 2 AddConstantModN(3, 4) | qunum # qunum is now equal to 1 + + .. note:: + + Pre-conditions: + + * c < N + * c >= 0 + * The value stored in the quantum register must be lower than N """ def __init__(self, a, N): """ @@ -145,6 +153,14 @@ def SubConstantModN(a, N): qunum = eng.allocate_qureg(3) # 3-qubit number X | qunum[1] # qunum is now equal to 2 SubConstantModN(4,5) | qunum # qunum is now -2 = 6 = 1 (mod 5) + + .. note:: + + Pre-conditions: + + * c < N + * c >= 0 + * The value stored in the quantum register must be lower than N """ return AddConstantModN(N - a, N) @@ -162,6 +178,15 @@ class MultiplyByConstantModN(BasicMathGate): qunum = eng.allocate_qureg(5) # 5-qubit number X | qunum[2] # qunum is now equal to 4 MultiplyByConstantModN(3,5) | qunum # qunum is now 2. + + .. note:: + + Pre-conditions: + + * c < N + * c >= 0 + * gcd(c, N) == 1 + * The value stored in the quantum register must be lower than N """ def __init__(self, a, N): """ diff --git a/projectq/libs/revkit/_control_function.py b/projectq/libs/revkit/_control_function.py index 99f7b880d..87f4e3804 100644 --- a/projectq/libs/revkit/_control_function.py +++ b/projectq/libs/revkit/_control_function.py @@ -109,7 +109,7 @@ def __or__(self, qubits): "provided qubits") # convert reversible circuit to ProjectQ code and execute it - _exec(revkit.write_projectq(log=True)["contents"], qs) + _exec(revkit.to_projectq(mct=True), qs) def _check_function(self): """ diff --git a/projectq/libs/revkit/_permutation.py b/projectq/libs/revkit/_permutation.py index b4967528a..160f027d2 100644 --- a/projectq/libs/revkit/_permutation.py +++ b/projectq/libs/revkit/_permutation.py @@ -81,7 +81,7 @@ def __or__(self, qubits): self.kwargs.get("synth", revkit.tbs)() # convert reversible circuit to ProjectQ code and execute it - _exec(revkit.write_projectq(log=True)["contents"], qs) + _exec(revkit.to_projectq(mct=True), qs) def _check_permutation(self): """ diff --git a/projectq/libs/revkit/_phase.py b/projectq/libs/revkit/_phase.py index 4aadc319e..8a9e2ab24 100644 --- a/projectq/libs/revkit/_phase.py +++ b/projectq/libs/revkit/_phase.py @@ -111,7 +111,7 @@ def __or__(self, qubits): "provided qubits") # convert reversible circuit to ProjectQ code and execute it - _exec(revkit.write_projectq(log=True)["contents"], qs) + _exec(revkit.to_projectq(mct=True), qs) def _check_function(self): """ diff --git a/projectq/meta/_loop.py b/projectq/meta/_loop.py index e28d64ea8..1b7408232 100755 --- a/projectq/meta/_loop.py +++ b/projectq/meta/_loop.py @@ -87,6 +87,7 @@ def run(self): engines, i.e., if .. code-block:: python + is_meta_tag_supported(next_engine, LoopTag) == False """ error_message = ("\n Error. Qubits have been allocated in with " diff --git a/projectq/ops/__init__.py b/projectq/ops/__init__.py index 32ff8ab54..dd73cc2d5 100755 --- a/projectq/ops/__init__.py +++ b/projectq/ops/__init__.py @@ -15,6 +15,7 @@ from ._basics import (NotMergeable, NotInvertible, BasicGate, + MatrixGate, SelfInverseGate, BasicRotationGate, ClassicalInstructionGate, @@ -24,6 +25,7 @@ from ._command import apply_command, Command from ._metagates import (DaggeredGate, get_inverse, + is_identity, ControlledGate, C, Tensor, @@ -36,3 +38,5 @@ from ._uniformly_controlled_rotation import (UniformlyControlledRy, UniformlyControlledRz) from ._state_prep import StatePreparation +from ._qpegate import QPE +from ._qaagate import QAA diff --git a/projectq/ops/_basics.py b/projectq/ops/_basics.py index 7f724c93e..c7bdd31bc 100755 --- a/projectq/ops/_basics.py +++ b/projectq/ops/_basics.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Defines the BasicGate class, the base class of all gates, the BasicRotationGate class, the SelfInverseGate, the FastForwardingGate, the @@ -39,9 +38,10 @@ from projectq.types import BasicQubit from ._command import Command, apply_command +import unicodedata ANGLE_PRECISION = 12 -ANGLE_TOLERANCE = 10 ** -ANGLE_PRECISION +ANGLE_TOLERANCE = 10**-ANGLE_PRECISION RTOL = 1e-10 ATOL = 1e-12 @@ -64,7 +64,7 @@ class NotInvertible(Exception): class BasicGate(object): """ - Base class of all gates. + Base class of all gates. (Don't use it directly but derive from it) """ def __init__(self): """ @@ -157,7 +157,7 @@ def make_tuple_of_qureg(qubits): (or list of Qubits) objects. """ if not isinstance(qubits, tuple): - qubits = (qubits,) + qubits = (qubits, ) qubits = list(qubits) @@ -204,39 +204,20 @@ def __or__(self, qubits): apply_command(cmd) def __eq__(self, other): - """ Return True if equal (i.e., instance of same class). - - Unless both have a matrix attribute in which case we also check - that the matrices are identical as people might want to do the - following: - - Example: - .. code-block:: python - - gate = BasicGate() - gate.matrix = numpy.matrix([[1,0],[0, -1]]) - """ - if hasattr(self, 'matrix'): - if not hasattr(other, 'matrix'): - return False - if hasattr(other, 'matrix'): - if not hasattr(self, 'matrix'): - return False - if hasattr(self, 'matrix') and hasattr(other, 'matrix'): - if (not isinstance(self.matrix, np.matrix) or - not isinstance(other.matrix, np.matrix)): - raise TypeError("One of the gates doesn't have the correct " - "type (numpy.matrix) for the matrix " - "attribute.") - if (self.matrix.shape == other.matrix.shape and - np.allclose(self.matrix, other.matrix, - rtol=RTOL, atol=ATOL, - equal_nan=False)): - return True - else: - return False + """ + Equality comparision + + Return True if instance of the same class, unless other is an instance + of :class:MatrixGate, in which case equality is to be checked by + testing for existence and (approximate) equality of matrix + representations. + """ + if isinstance(other, self.__class__): + return True + elif isinstance(other, MatrixGate): + return NotImplemented else: - return isinstance(other, self.__class__) + return False def __ne__(self, other): return not self.__eq__(other) @@ -244,9 +225,85 @@ def __ne__(self, other): def __str__(self): raise NotImplementedError('This gate does not implement __str__.') + def to_string(self, symbols): + """ + String representation + + Achieve same function as str() but can be extended for configurable + representation + """ + return str(self) + def __hash__(self): return hash(str(self)) + def is_identity(self): + return False + + +class MatrixGate(BasicGate): + """ + Defines a gate class whose instances are defined by a matrix. + + Note: + Use this gate class only for gates acting on a small numbers of qubits. + In general, consider instead using one of the provided ProjectQ gates + or define a new class as this allows the compiler to work symbolically. + + Example: + + .. code-block:: python + + gate = MatrixGate([[0, 1], [1, 0]]) + gate | qubit + """ + def __init__(self, matrix=None): + """ + Initialize MatrixGate + + Args: + matrix(numpy.matrix): matrix which defines the gate. Default: None + """ + BasicGate.__init__(self) + self._matrix = np.matrix(matrix) if matrix is not None else None + + @property + def matrix(self): + return self._matrix + + @matrix.setter + def matrix(self, matrix): + self._matrix = np.matrix(matrix) + + def __eq__(self, other): + """ + Equality comparision + + Return True only if both gates have a matrix respresentation and the + matrices are (approximately) equal. Otherwise return False. + """ + if not hasattr(other, 'matrix'): + return False + if (not isinstance(self.matrix, np.matrix) + or not isinstance(other.matrix, np.matrix)): + raise TypeError("One of the gates doesn't have the correct " + "type (numpy.matrix) for the matrix " + "attribute.") + if (self.matrix.shape == other.matrix.shape and np.allclose( + self.matrix, other.matrix, rtol=RTOL, atol=ATOL, + equal_nan=False)): + return True + return False + + def __str__(self): + return ("MatrixGate(" + str(self.matrix.tolist()) + ")") + + def __hash__(self): + return hash(str(self)) + + def get_inverse(self): + return MatrixGate(np.linalg.inv(self.matrix)) + class SelfInverseGate(BasicGate): """ @@ -298,7 +355,23 @@ def __str__(self): [CLASSNAME]([ANGLE]) """ - return str(self.__class__.__name__) + "(" + str(self.angle) + ")" + return self.to_string() + + def to_string(self, symbols=False): + """ + Return the string representation of a BasicRotationGate. + + Args: + symbols (bool): uses the pi character and round the angle for a + more user friendly display if True, full angle + written in radian otherwise. + """ + if symbols: + angle = ("(" + str(round(self.angle / math.pi, 3)) + + unicodedata.lookup("GREEK SMALL LETTER PI") + ")") + else: + angle = "(" + str(self.angle) + ")" + return str(self.__class__.__name__) + angle def tex_str(self): """ @@ -310,7 +383,8 @@ def tex_str(self): [CLASSNAME]$_[ANGLE]$ """ - return str(self.__class__.__name__) + "$_{" + str(self.angle) + "}$" + return (str(self.__class__.__name__) + "$_{" + + str(round(self.angle / math.pi, 3)) + "\\pi}$") def get_inverse(self): """ @@ -356,6 +430,12 @@ def __ne__(self, other): def __hash__(self): return hash(str(self)) + def is_identity(self): + """ + Return True if the gate is equivalent to an Identity gate + """ + return self.angle == 0. or self.angle == 4 * math.pi + class BasicPhaseGate(BasicGate): """ @@ -552,6 +632,7 @@ def math_fun(a): def math_function(x): return list(math_fun(*x)) + self._math_function = math_function def __str__(self): diff --git a/projectq/ops/_basics_test.py b/projectq/ops/_basics_test.py index 61a1de767..a58a24e4c 100755 --- a/projectq/ops/_basics_test.py +++ b/projectq/ops/_basics_test.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2017 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,17 +12,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.ops._basics.""" -from copy import deepcopy import math import numpy as np import pytest from projectq.types import Qubit, Qureg -from projectq.ops import Command +from projectq.ops import Command, X from projectq import MainEngine from projectq.cengines import DummyEngine @@ -49,13 +48,13 @@ def test_basic_gate_make_tuple_of_qureg(main_engine): qubit3 = Qubit(main_engine, 3) qureg = Qureg([qubit2, qubit3]) case1 = _basics.BasicGate.make_tuple_of_qureg(qubit0) - assert case1 == ([qubit0],) + assert case1 == ([qubit0], ) case2 = _basics.BasicGate.make_tuple_of_qureg([qubit0, qubit1]) - assert case2 == ([qubit0, qubit1],) + assert case2 == ([qubit0, qubit1], ) case3 = _basics.BasicGate.make_tuple_of_qureg(qureg) - assert case3 == (qureg,) - case4 = _basics.BasicGate.make_tuple_of_qureg((qubit0,)) - assert case4 == ([qubit0],) + assert case3 == (qureg, ) + case4 = _basics.BasicGate.make_tuple_of_qureg((qubit0, )) + assert case4 == ([qubit0], ) case5 = _basics.BasicGate.make_tuple_of_qureg((qureg, qubit0)) assert case5 == (qureg, [qubit0]) @@ -68,20 +67,15 @@ def test_basic_gate_generate_command(main_engine): qureg = Qureg([qubit2, qubit3]) basic_gate = _basics.BasicGate() command1 = basic_gate.generate_command(qubit0) - assert command1 == Command(main_engine, basic_gate, - ([qubit0],)) + assert command1 == Command(main_engine, basic_gate, ([qubit0], )) command2 = basic_gate.generate_command([qubit0, qubit1]) - assert command2 == Command(main_engine, basic_gate, - ([qubit0, qubit1],)) + assert command2 == Command(main_engine, basic_gate, ([qubit0, qubit1], )) command3 = basic_gate.generate_command(qureg) - assert command3 == Command(main_engine, basic_gate, - (qureg,)) - command4 = basic_gate.generate_command((qubit0,)) - assert command4 == Command(main_engine, basic_gate, - ([qubit0],)) + assert command3 == Command(main_engine, basic_gate, (qureg, )) + command4 = basic_gate.generate_command((qubit0, )) + assert command4 == Command(main_engine, basic_gate, ([qubit0], )) command5 = basic_gate.generate_command((qureg, qubit0)) - assert command5 == Command(main_engine, basic_gate, - (qureg, [qubit0])) + assert command5 == Command(main_engine, basic_gate, (qureg, [qubit0])) def test_basic_gate_or(): @@ -100,8 +94,8 @@ def test_basic_gate_or(): basic_gate | [qubit0, qubit1] command3 = basic_gate.generate_command(qureg) basic_gate | qureg - command4 = basic_gate.generate_command((qubit0,)) - basic_gate | (qubit0,) + command4 = basic_gate.generate_command((qubit0, )) + basic_gate | (qubit0, ) command5 = basic_gate.generate_command((qureg, qubit0)) basic_gate | (qureg, qubit0) received_commands = [] @@ -109,8 +103,9 @@ def test_basic_gate_or(): for cmd in saving_backend.received_commands: if not isinstance(cmd.gate, _basics.FastForwardingGate): received_commands.append(cmd) - assert received_commands == ([command1, command2, command3, command4, - command5]) + assert received_commands == ([ + command1, command2, command3, command4, command5 + ]) def test_basic_gate_compare(): @@ -118,13 +113,12 @@ def test_basic_gate_compare(): gate2 = _basics.BasicGate() assert gate1 == gate2 assert not (gate1 != gate2) - gate3 = _basics.BasicGate() + gate3 = _basics.MatrixGate() gate3.matrix = np.matrix([[1, 0], [0, -1]]) assert gate1 != gate3 - gate4 = _basics.BasicGate() + gate4 = _basics.MatrixGate() gate4.matrix = [[1, 0], [0, -1]] - with pytest.raises(TypeError): - gate4 == gate3 + assert gate4 == gate3 def test_comparing_different_gates(): @@ -164,15 +158,17 @@ def test_basic_rotation_gate_init(input_angle, modulo_angle): def test_basic_rotation_gate_str(): - basic_rotation_gate = _basics.BasicRotationGate(0.5) - assert str(basic_rotation_gate) == "BasicRotationGate(0.5)" + gate = _basics.BasicRotationGate(math.pi) + assert str(gate) == "BasicRotationGate(3.14159265359)" + assert gate.to_string(symbols=True) == u"BasicRotationGate(1.0Ï€)" + assert gate.to_string(symbols=False) == "BasicRotationGate(3.14159265359)" def test_basic_rotation_tex_str(): - basic_rotation_gate = _basics.BasicRotationGate(0.5) - assert basic_rotation_gate.tex_str() == "BasicRotationGate$_{0.5}$" - basic_rotation_gate = _basics.BasicRotationGate(4 * math.pi - 1e-13) - assert basic_rotation_gate.tex_str() == "BasicRotationGate$_{0.0}$" + gate = _basics.BasicRotationGate(0.5 * math.pi) + assert gate.tex_str() == "BasicRotationGate$_{0.5\\pi}$" + gate = _basics.BasicRotationGate(4 * math.pi - 1e-13) + assert gate.tex_str() == "BasicRotationGate$_{0.0\\pi}$" @pytest.mark.parametrize("input_angle, inverse_angle", @@ -195,6 +191,19 @@ def test_basic_rotation_gate_get_merged(): assert merged_gate == basic_rotation_gate3 +def test_basic_rotation_gate_is_identity(): + basic_rotation_gate1 = _basics.BasicRotationGate(0.) + basic_rotation_gate2 = _basics.BasicRotationGate(1. * math.pi) + basic_rotation_gate3 = _basics.BasicRotationGate(2. * math.pi) + basic_rotation_gate4 = _basics.BasicRotationGate(3. * math.pi) + basic_rotation_gate5 = _basics.BasicRotationGate(4. * math.pi) + assert basic_rotation_gate1.is_identity() + assert not basic_rotation_gate2.is_identity() + assert not basic_rotation_gate3.is_identity() + assert not basic_rotation_gate4.is_identity() + assert basic_rotation_gate5.is_identity() + + def test_basic_rotation_gate_comparison_and_hash(): basic_rotation_gate1 = _basics.BasicRotationGate(0.5) basic_rotation_gate2 = _basics.BasicRotationGate(0.5) @@ -295,3 +304,35 @@ def __init__(self): # Test a=2, b=3, and c=5 should give a=2, b=3, c=11 math_fun = gate.get_math_function(("qreg1", "qreg2", "qreg3")) assert math_fun([2, 3, 5]) == [2, 3, 11] + + +def test_matrix_gate(): + gate1 = _basics.MatrixGate() + gate2 = _basics.MatrixGate() + with pytest.raises(TypeError): + assert gate1 == gate2 + gate3 = _basics.MatrixGate([[0, 1], [1, 0]]) + gate4 = _basics.MatrixGate([[0, 1], [1, 0]]) + gate5 = _basics.MatrixGate([[1, 0], [0, -1]]) + assert gate3 == gate4 + assert gate4 != gate5 + with pytest.raises(TypeError): + assert gate1 != gate3 + with pytest.raises(TypeError): + assert gate3 != gate1 + gate6 = _basics.BasicGate() + assert gate6 != gate1 + assert gate6 != gate3 + assert gate1 != gate6 + assert gate3 != gate6 + gate7 = gate5.get_inverse() + gate8 = _basics.MatrixGate([[1, 0], [0, (1 + 1j) / math.sqrt(2)]]) + assert gate7 == gate5 + assert gate7 != gate8 + gate9 = _basics.MatrixGate([[1, 0], [0, (1 - 1j) / math.sqrt(2)]]) + gate10 = gate9.get_inverse() + assert gate10 == gate8 + assert gate3 == X + assert X == gate3 + assert str(gate3) == "MatrixGate([[0, 1], [1, 0]])" + assert hash(gate3) == hash("MatrixGate([[0, 1], [1, 0]])") diff --git a/projectq/ops/_command.py b/projectq/ops/_command.py index 5186502fa..317356c19 100755 --- a/projectq/ops/_command.py +++ b/projectq/ops/_command.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ This file defines the apply_command function and the Command class. @@ -31,6 +30,7 @@ optimizer to cancel the following two gates .. code-block:: python + Swap | (qubit1, qubit2) Swap | (qubit2, qubit1) @@ -82,7 +82,6 @@ class Command(object): and hence adds its LoopTag to the end. all_qubits: A tuple of control_qubits + qubits """ - def __init__(self, engine, gate, qubits, controls=(), tags=()): """ Initialize a Command object. @@ -106,9 +105,10 @@ def __init__(self, engine, gate, qubits, controls=(), tags=()): tags (list[object]): Tags associated with the command. """ - qubits = tuple([WeakQubitRef(qubit.engine, qubit.id) - for qubit in qreg] - for qreg in qubits) + + qubits = tuple( + [WeakQubitRef(qubit.engine, qubit.id) for qubit in qreg] + for qreg in qubits) self.gate = gate self.tags = list(tags) @@ -126,11 +126,8 @@ def qubits(self, qubits): def __deepcopy__(self, memo): """ Deepcopy implementation. Engine should stay a reference.""" - return Command(self.engine, - deepcopy(self.gate), - self.qubits, - list(self.control_qubits), - deepcopy(self.tags)) + return Command(self.engine, deepcopy(self.gate), self.qubits, + list(self.control_qubits), deepcopy(self.tags)) def get_inverse(self): """ @@ -143,12 +140,19 @@ def get_inverse(self): NotInvertible: If the gate does not provide an inverse (see BasicGate.get_inverse) """ - return Command(self._engine, - projectq.ops.get_inverse(self.gate), - self.qubits, - list(self.control_qubits), + return Command(self._engine, projectq.ops.get_inverse(self.gate), + self.qubits, list(self.control_qubits), deepcopy(self.tags)) + def is_identity(self): + """ + Evaluate if the gate called in the command object is an identity gate. + + Returns: + True if the gate is equivalent to an Identity gate, False otherwise + """ + return projectq.ops.is_identity(self.gate) + def get_merged(self, other): """ Merge this command with another one and return the merged command @@ -161,12 +165,10 @@ def get_merged(self, other): NotMergeable: if the gates don't supply a get_merged()-function or can't be merged for other reasons. """ - if (self.tags == other.tags and self.all_qubits == other.all_qubits and - self.engine == other.engine): - return Command(self.engine, - self.gate.get_merged(other.gate), - self.qubits, - self.control_qubits, + if (self.tags == other.tags and self.all_qubits == other.all_qubits + and self.engine == other.engine): + return Command(self.engine, self.gate.get_merged(other.gate), + self.qubits, self.control_qubits, deepcopy(self.tags)) raise projectq.ops.NotMergeable("Commands not mergeable.") @@ -219,8 +221,9 @@ def control_qubits(self, qubits): Args: control_qubits (Qureg): quantum register """ - self._control_qubits = ([WeakQubitRef(qubit.engine, qubit.id) - for qubit in qubits]) + self._control_qubits = ([ + WeakQubitRef(qubit.engine, qubit.id) for qubit in qubits + ]) self._control_qubits = sorted(self._control_qubits, key=lambda x: x.id) def add_control_qubits(self, qubits): @@ -236,9 +239,9 @@ def add_control_qubits(self, qubits): gate, i.e., the gate is only executed if all qubits are in state 1. """ - assert(isinstance(qubits, list)) - self._control_qubits.extend([WeakQubitRef(qubit.engine, qubit.id) - for qubit in qubits]) + assert (isinstance(qubits, list)) + self._control_qubits.extend( + [WeakQubitRef(qubit.engine, qubit.id) for qubit in qubits]) self._control_qubits = sorted(self._control_qubits, key=lambda x: x.id) @property @@ -250,7 +253,7 @@ def all_qubits(self): WeakQubitRef objects) containing the control qubits and T[1:] contains the quantum registers to which the gate is applied. """ - return (self.control_qubits,) + self.qubits + return (self.control_qubits, ) + self.qubits @property def engine(self): @@ -285,11 +288,9 @@ def __eq__(self, other): Returns: True if Command objects are equal (same gate, applied to same qubits; ordered modulo interchangeability; and same tags) """ - if (isinstance(other, self.__class__) and - self.gate == other.gate and - self.tags == other.tags and - self.engine == other.engine and - self.all_qubits == other.all_qubits): + if (isinstance(other, self.__class__) and self.gate == other.gate + and self.tags == other.tags and self.engine == other.engine + and self.all_qubits == other.all_qubits): return True return False @@ -297,13 +298,16 @@ def __ne__(self, other): return not self.__eq__(other) def __str__(self): + return self.to_string() + + def to_string(self, symbols=False): """ Get string representation of this Command object. """ qubits = self.qubits ctrlqubits = self.control_qubits if len(ctrlqubits) > 0: - qubits = (self.control_qubits,) + qubits + qubits = (self.control_qubits, ) + qubits qstring = "" if len(qubits) == 1: qstring = str(Qureg(qubits[0])) @@ -314,4 +318,4 @@ def __str__(self): qstring += ", " qstring = qstring[:-2] + " )" cstring = "C" * len(ctrlqubits) - return cstring + str(self.gate) + " | " + qstring + return cstring + self.gate.to_string(symbols) + " | " + qstring diff --git a/projectq/ops/_command_test.py b/projectq/ops/_command_test.py index ae1407836..b0b4d54c8 100755 --- a/projectq/ops/_command_test.py +++ b/projectq/ops/_command_test.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2017 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +16,7 @@ """Tests for projectq.ops._command.""" from copy import deepcopy +import sys import math import pytest @@ -36,8 +38,8 @@ def test_command_init(main_engine): qureg0 = Qureg([Qubit(main_engine, 0)]) qureg1 = Qureg([Qubit(main_engine, 1)]) qureg2 = Qureg([Qubit(main_engine, 2)]) - qureg3 = Qureg([Qubit(main_engine, 3)]) - qureg4 = Qureg([Qubit(main_engine, 4)]) + # qureg3 = Qureg([Qubit(main_engine, 3)]) + # qureg4 = Qureg([Qubit(main_engine, 4)]) gate = BasicGate() cmd = _command.Command(main_engine, gate, (qureg0, qureg1, qureg2)) assert cmd.gate == gate @@ -133,6 +135,19 @@ def test_command_get_merged(main_engine): cmd.get_merged(cmd4) +def test_command_is_identity(main_engine): + qubit = main_engine.allocate_qubit() + qubit2 = main_engine.allocate_qubit() + cmd = _command.Command(main_engine, Rx(0.), (qubit,)) + cmd2 = _command.Command(main_engine, Rx(0.5), (qubit2,)) + inverse_cmd = cmd.get_inverse() + inverse_cmd2 = cmd2.get_inverse() + assert inverse_cmd.gate.is_identity() + assert cmd.gate.is_identity() + assert not inverse_cmd2.gate.is_identity() + assert not cmd2.gate.is_identity() + + def test_command_order_qubits(main_engine): qubit0 = Qureg([Qubit(main_engine, 0)]) qubit1 = Qureg([Qubit(main_engine, 1)]) @@ -232,9 +247,32 @@ def test_command_comparison(main_engine): def test_command_str(): qubit = Qureg([Qubit(main_engine, 0)]) ctrl_qubit = Qureg([Qubit(main_engine, 1)]) - cmd = _command.Command(main_engine, Rx(0.5), (qubit,)) + cmd = _command.Command(main_engine, Rx(0.5*math.pi), (qubit,)) cmd.tags = ["TestTag"] cmd.add_control_qubits(ctrl_qubit) - assert str(cmd) == "CRx(0.5) | ( Qureg[1], Qureg[0] )" - cmd2 = _command.Command(main_engine, Rx(0.5), (qubit,)) - assert str(cmd2) == "Rx(0.5) | Qureg[0]" + cmd2 = _command.Command(main_engine, Rx(0.5*math.pi), (qubit,)) + if sys.version_info.major == 3: + assert cmd.to_string(symbols=False) == "CRx(1.570796326795) | ( Qureg[1], Qureg[0] )" + assert str(cmd2) == "Rx(1.570796326795) | Qureg[0]" + else: + assert cmd.to_string(symbols=False) == "CRx(1.5707963268) | ( Qureg[1], Qureg[0] )" + assert str(cmd2) == "Rx(1.5707963268) | Qureg[0]" + + +def test_command_to_string(): + qubit = Qureg([Qubit(main_engine, 0)]) + ctrl_qubit = Qureg([Qubit(main_engine, 1)]) + cmd = _command.Command(main_engine, Rx(0.5*math.pi), (qubit,)) + cmd.tags = ["TestTag"] + cmd.add_control_qubits(ctrl_qubit) + cmd2 = _command.Command(main_engine, Rx(0.5*math.pi), (qubit,)) + + assert cmd.to_string(symbols=True) == u"CRx(0.5Ï€) | ( Qureg[1], Qureg[0] )" + assert cmd2.to_string(symbols=True) == u"Rx(0.5Ï€) | Qureg[0]" + if sys.version_info.major == 3: + assert cmd.to_string(symbols=False) == "CRx(1.570796326795) | ( Qureg[1], Qureg[0] )" + assert cmd2.to_string(symbols=False) == "Rx(1.570796326795) | Qureg[0]" + else: + assert cmd.to_string(symbols=False) == "CRx(1.5707963268) | ( Qureg[1], Qureg[0] )" + assert cmd2.to_string(symbols=False) == "Rx(1.5707963268) | Qureg[0]" + diff --git a/projectq/ops/_gates.py b/projectq/ops/_gates.py index 530fcce76..be2240d00 100755 --- a/projectq/ops/_gates.py +++ b/projectq/ops/_gates.py @@ -16,17 +16,29 @@ Contains definitions of standard gates such as * Hadamard (H) * Pauli-X (X / NOT) +* Pauli-Y (Y) * Pauli-Z (Z) +* S and its inverse (S / Sdagger) * T and its inverse (T / Tdagger) +* SqrtX gate (SqrtX) * Swap gate (Swap) +* SqrtSwap gate (SqrtSwap) +* Entangle (Entangle) * Phase gate (Ph) +* Rotation-X (Rx) +* Rotation-Y (Ry) * Rotation-Z (Rz) +* Rotation-XX on two qubits (Rxx) +* Rotation-YY on two qubits (Ryy) +* Rotation-ZZ on two qubits (Rzz) * Phase-shift (R) * Measurement (Measure) and meta gates, i.e., * Allocate / Deallocate qubits * Flush gate (end of circuit) +* Barrier +* FlipBits """ import math @@ -37,6 +49,7 @@ from projectq.ops import get_inverse from ._basics import (BasicGate, + MatrixGate, SelfInverseGate, BasicRotationGate, BasicPhaseGate, @@ -44,7 +57,6 @@ FastForwardingGate, BasicMathGate) from ._command import apply_command -from projectq.types import BasicQubit class HGate(SelfInverseGate): @@ -110,7 +122,7 @@ def __str__(self): #: Shortcut (instance of) :class:`projectq.ops.SGate` S = SGate() -#: Shortcut (instance of) :class:`projectq.ops.SGate` +#: Inverse (and shortcut) of :class:`projectq.ops.SGate` Sdag = Sdagger = get_inverse(S) @@ -125,7 +137,7 @@ def __str__(self): #: Shortcut (instance of) :class:`projectq.ops.TGate` T = TGate() -#: Shortcut (instance of) :class:`projectq.ops.TGate` +#: Inverse (and shortcut) of :class:`projectq.ops.TGate` Tdag = Tdagger = get_inverse(T) @@ -145,10 +157,9 @@ def __str__(self): SqrtX = SqrtXGate() -class SwapGate(SelfInverseGate, BasicMathGate): +class SwapGate(SelfInverseGate): """ Swap gate class (swaps 2 qubits) """ def __init__(self): - BasicMathGate.__init__(self, lambda x, y: (y, x)) SelfInverseGate.__init__(self) self.interchangeable_qubit_indices = [[0, 1]] @@ -217,7 +228,7 @@ def matrix(self): class Ry(BasicRotationGate): - """ RotationX gate class """ + """ RotationY gate class """ @property def matrix(self): return np.matrix([[math.cos(0.5 * self.angle), @@ -234,6 +245,36 @@ def matrix(self): [0, cmath.exp(.5 * 1j * self.angle)]]) +class Rxx(BasicRotationGate): + """ RotationXX gate class """ + @property + def matrix(self): + return np.matrix([[cmath.cos(.5 * self.angle), 0, 0, -1j*cmath.sin(.5 * self.angle)], + [0, cmath.cos( .5 * self.angle), -1j*cmath.sin(.5 * self.angle), 0], + [0, -1j*cmath.sin(.5 * self.angle), cmath.cos( .5 * self.angle), 0], + [-1j*cmath.sin(.5 * self.angle), 0, 0, cmath.cos( .5 * self.angle)]]) + + +class Ryy(BasicRotationGate): + """ RotationYY gate class """ + @property + def matrix(self): + return np.matrix([[cmath.cos(.5 * self.angle), 0, 0, 1j*cmath.sin(.5 * self.angle)], + [0, cmath.cos( .5 * self.angle), -1j*cmath.sin(.5 * self.angle), 0], + [0, -1j*cmath.sin(.5 * self.angle), cmath.cos( .5 * self.angle), 0], + [1j*cmath.sin(.5 * self.angle), 0, 0, cmath.cos( .5 * self.angle)]]) + + +class Rzz(BasicRotationGate): + """ RotationZZ gate class """ + @property + def matrix(self): + return np.matrix([[cmath.exp(-.5 * 1j * self.angle), 0, 0, 0], + [0, cmath.exp( .5 * 1j * self.angle), 0, 0], + [0, 0, cmath.exp( .5 * 1j * self.angle), 0], + [0, 0, 0, cmath.exp(-.5 * 1j * self.angle)]]) + + class R(BasicPhaseGate): """ Phase-shift gate (equivalent to Rz up to a global phase) """ @property @@ -338,3 +379,56 @@ def get_inverse(self): #: Shortcut (instance of) :class:`projectq.ops.BarrierGate` Barrier = BarrierGate() + + +class FlipBits(SelfInverseGate): + """ Gate for flipping qubits by means of XGates """ + def __init__(self, bits_to_flip): + """ + Initialize FlipBits gate. + + Example: + .. code-block:: python + + qureg = eng.allocate_qureg(2) + FlipBits([0, 1]) | qureg + + Args: + bits_to_flip(list[int]|list[bool]|str|int): int or array of 0/1, + True/False, or string of 0/1 identifying the qubits to flip. + In case of int, the bits to flip are determined from the + binary digits, with the least significant bit corresponding + to qureg[0]. If bits_to_flip is negative, exactly all qubits + which would not be flipped for the input -bits_to_flip-1 are + flipped, i.e., bits_to_flip=-1 flips all qubits. + """ + SelfInverseGate.__init__(self) + if isinstance(bits_to_flip, int): + self.bits_to_flip = bits_to_flip + else: + self.bits_to_flip = 0 + for i in reversed(list(bits_to_flip)): + bit = 0b1 if i == '1' or i == 1 or i is True else 0b0 + self.bits_to_flip = (self.bits_to_flip << 1) | bit + + def __str__(self): + return "FlipBits("+str(self.bits_to_flip)+")" + + def __or__(self, qubits): + quregs_tuple = self.make_tuple_of_qureg(qubits) + if len(quregs_tuple) > 1: + raise ValueError(self.__str__()+' can only be applied to qubits,' + 'quregs, arrays of qubits, and tuples with one' + 'individual qubit') + for qureg in quregs_tuple: + for i, qubit in enumerate(qureg): + if (self.bits_to_flip >> i) & 1: + XGate() | qubit + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.bits_to_flip == other.bits_to_flip + return False + + def __hash__(self): + return hash(self.__str__()) diff --git a/projectq/ops/_gates_test.py b/projectq/ops/_gates_test.py index 002ab8d20..88efa3a19 100755 --- a/projectq/ops/_gates_test.py +++ b/projectq/ops/_gates_test.py @@ -19,9 +19,10 @@ import numpy as np import pytest -from projectq.ops import (get_inverse, SelfInverseGate, BasicRotationGate, - ClassicalInstructionGate, FastForwardingGate, - BasicGate) +from projectq import MainEngine +from projectq.ops import (All, FlipBits, get_inverse, SelfInverseGate, + BasicRotationGate, ClassicalInstructionGate, + FastForwardingGate, BasicGate, Measure) from projectq.ops import _gates @@ -155,6 +156,42 @@ def test_rz(angle): assert np.allclose(gate.matrix, expected_matrix) +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, + 4 * math.pi]) +def test_rxx(angle): + gate = _gates.Rxx(angle) + expected_matrix = np.matrix([[cmath.cos(.5 * angle), 0, 0, -1j * cmath.sin(.5 * angle)], + [0, cmath.cos(.5 * angle), -1j * cmath.sin(.5 * angle), 0], + [0, -1j * cmath.sin(.5 * angle), cmath.cos(.5 * angle), 0], + [-1j * cmath.sin(.5 * angle), 0, 0, cmath.cos(.5 * angle)]]) + assert gate.matrix.shape == expected_matrix.shape + assert np.allclose(gate.matrix, expected_matrix) + + +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, + 4 * math.pi]) +def test_ryy(angle): + gate = _gates.Ryy(angle) + expected_matrix = np.matrix([[cmath.cos(.5 * angle), 0, 0, 1j * cmath.sin(.5 * angle)], + [0, cmath.cos(.5 * angle), -1j * cmath.sin(.5 * angle), 0], + [0, -1j * cmath.sin(.5 * angle), cmath.cos(.5 * angle), 0], + [ 1j * cmath.sin(.5 * angle), 0, 0, cmath.cos(.5 * angle)]]) + assert gate.matrix.shape == expected_matrix.shape + assert np.allclose(gate.matrix, expected_matrix) + + +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, + 4 * math.pi]) +def test_rzz(angle): + gate = _gates.Rzz(angle) + expected_matrix = np.matrix([[cmath.exp(-.5 * 1j * angle), 0, 0, 0], + [0, cmath.exp( .5 * 1j * angle), 0, 0], + [0, 0, cmath.exp( .5 * 1j * angle), 0], + [0, 0, 0, cmath.exp(-.5 * 1j * angle)]]) + assert gate.matrix.shape == expected_matrix.shape + assert np.allclose(gate.matrix, expected_matrix) + + @pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi]) def test_ph(angle): gate = _gates.Ph(angle) @@ -217,3 +254,78 @@ def test_barrier_gate(): assert str(gate) == "Barrier" assert gate.get_inverse() == _gates.BarrierGate() assert isinstance(_gates.Barrier, _gates.BarrierGate) + + +def test_flip_bits_equality_and_hash(): + gate1 = _gates.FlipBits([1, 0, 0, 1]) + gate2 = _gates.FlipBits([1, 0, 0, 1]) + gate3 = _gates.FlipBits([0, 1, 0, 1]) + assert gate1 == gate2 + assert hash(gate1) == hash(gate2) + assert gate1 != gate3 + assert gate1 != _gates.X + + +def test_flip_bits_str(): + gate1 = _gates.FlipBits([0, 0, 1]) + assert str(gate1) == "FlipBits(4)" + + +def test_error_on_tuple_input(): + with pytest.raises(ValueError): + _gates.FlipBits(2) | (None, None) + + +flip_bits_testdata = [ + ([0, 1, 0, 1], '0101'), + ([1, 0, 1, 0], '1010'), + ([False, True, False, True], '0101'), + ('0101', '0101'), + ('1111', '1111'), + ('0000', '0000'), + (8, '0001'), + (11, '1101'), + (1, '1000'), + (-1, '1111'), + (-2, '0111'), + (-3, '1011'), +] + + +@pytest.mark.parametrize("bits_to_flip, result", flip_bits_testdata) +def test_simulator_flip_bits(bits_to_flip, result): + eng = MainEngine() + qubits = eng.allocate_qureg(4) + FlipBits(bits_to_flip) | qubits + eng.flush() + assert pytest.approx(eng.backend.get_probability(result, qubits)) == 1. + All(Measure) | qubits + + +def test_flip_bits_can_be_applied_to_various_qubit_qureg_formats(): + eng = MainEngine() + qubits = eng.allocate_qureg(4) + eng.flush() + assert pytest.approx(eng.backend.get_probability('0000', qubits)) == 1. + FlipBits([0, 1, 1, 0]) | qubits + eng.flush() + assert pytest.approx(eng.backend.get_probability('0110', qubits)) == 1. + FlipBits([1]) | qubits[0] + eng.flush() + assert pytest.approx(eng.backend.get_probability('1110', qubits)) == 1. + FlipBits([1]) | (qubits[0], ) + eng.flush() + assert pytest.approx(eng.backend.get_probability('0110', qubits)) == 1. + FlipBits([1, 1]) | [qubits[0], qubits[1]] + eng.flush() + assert pytest.approx(eng.backend.get_probability('1010', qubits)) == 1. + FlipBits(-1) | qubits + eng.flush() + assert pytest.approx(eng.backend.get_probability('0101', qubits)) == 1. + FlipBits(-4) | [qubits[0], qubits[1], qubits[2], qubits[3]] + eng.flush() + assert pytest.approx(eng.backend.get_probability('0110', qubits)) == 1. + FlipBits(2) | [qubits[0]] + [qubits[1], qubits[2]] + eng.flush() + assert pytest.approx(eng.backend.get_probability('0010', qubits)) == 1. + All(Measure) | qubits diff --git a/projectq/ops/_metagates.py b/projectq/ops/_metagates.py old mode 100755 new mode 100644 index c2c20969b..cca5e7412 --- a/projectq/ops/_metagates.py +++ b/projectq/ops/_metagates.py @@ -80,10 +80,10 @@ def __init__(self, gate): pass def __str__(self): - """ + r""" Return string representation (str(gate) + \"^\dagger\"). """ - return str(self._gate) + "^\dagger" + return str(self._gate) + r"^\dagger" def tex_str(self): """ @@ -132,6 +132,22 @@ def get_inverse(gate): except NotInvertible: return DaggeredGate(gate) +def is_identity(gate): + """ + Return True if the gate is an identity gate. + + Tries to call gate.is_identity and, upon failure, returns False + + Args: + gate: Gate of which to get the inverse + + Example: + .. code-block:: python + + get_inverse(Rx(2*math.pi)) # returns True + get_inverse(Rx(math.pi)) # returns False + """ + return gate.is_identity() class ControlledGate(BasicGate): """ diff --git a/projectq/ops/_metagates_test.py b/projectq/ops/_metagates_test.py index c5a62d239..8632a99e5 100755 --- a/projectq/ops/_metagates_test.py +++ b/projectq/ops/_metagates_test.py @@ -72,7 +72,7 @@ def test_daggered_gate_init(): def test_daggered_gate_str(): daggered_gate = _metagates.DaggeredGate(Y) - assert str(daggered_gate) == str(Y) + "^\dagger" + assert str(daggered_gate) == str(Y) + r"^\dagger" def test_daggered_gate_hashable(): @@ -87,13 +87,13 @@ def test_daggered_gate_hashable(): def test_daggered_gate_tex_str(): daggered_gate = _metagates.DaggeredGate(Y) str_Y = Y.tex_str() if hasattr(Y, 'tex_str') else str(Y) - assert daggered_gate.tex_str() == str_Y + "${}^\dagger$" + assert daggered_gate.tex_str() == str_Y + r"${}^\dagger$" # test for a gate with tex_str method rx = Rx(0.5) daggered_rx = _metagates.DaggeredGate(rx) str_rx = rx.tex_str() if hasattr(rx, 'tex_str') else str(rx) - assert daggered_rx.tex_str() == str_rx + "${}^\dagger$" + assert daggered_rx.tex_str() == str_rx + r"${}^\dagger$" def test_daggered_gate_get_inverse(): @@ -122,6 +122,16 @@ def test_get_inverse(): inv2 = _metagates.get_inverse(invertible_gate) assert inv2 == Y +def test_is_identity(): + # Choose gate which is not an identity gate: + non_identity_gate=Rx(0.5) + assert not non_identity_gate.is_identity() + assert not _metagates.is_identity(non_identity_gate) + # Choose gate which is an identity gate: + identity_gate=Rx(0.) + assert identity_gate.is_identity() + assert _metagates.is_identity(identity_gate) + def test_controlled_gate_init(): one_control = _metagates.ControlledGate(Y, 1) diff --git a/projectq/ops/_qaagate.py b/projectq/ops/_qaagate.py new file mode 100755 index 000000000..751b9dc60 --- /dev/null +++ b/projectq/ops/_qaagate.py @@ -0,0 +1,81 @@ +# Copyright 2019 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ._basics import BasicGate + + +class QAA(BasicGate): + """ + Quantum Aplitude Aplification gate. + + (Quick reference https://en.wikipedia.org/wiki/Amplitude_amplification. + Complete reference G. Brassard, P. Hoyer, M. Mosca, A. Tapp (2000) + Quantum Amplitude Amplification and Estimation + https://arxiv.org/abs/quant-ph/0005055) + + Quantum Amplitude Amplification (QAA) executes the algorithm, but not + the final measurement required to obtain the marked state(s) with high + probability. The starting state on wich the QAA algorithm is executed + is the one resulting of aplying the Algorithm on the |0> state. + + Example: + .. code-block:: python + + def func_algorithm(eng,system_qubits): + All(H) | system_qubits + + def func_oracle(eng,system_qubits,qaa_ancilla): + # This oracle selects the state |010> as the one marked + with Compute(eng): + All(X) | system_qubits[0::2] + with Control(eng, system_qubits): + X | qaa_ancilla + Uncompute(eng) + + system_qubits = eng.allocate_qureg(3) + # Prepare the qaa_ancilla qubit in the |-> state + qaa_ancilla = eng.allocate_qubit() + X | qaa_ancilla + H | qaa_ancilla + + # Creates the initial state form the Algorithm + func_algorithm(eng, system_qubits) + # Apply Quantum Amplitude Amplification the correct number of times + num_it = int(math.pi/4.*math.sqrt(1 << 3)) + with Loop(eng, num_it): + QAA(func_algorithm, func_oracle) | (system_qubits, qaa_ancilla) + + All(Measure) | system_qubits + + Warning: + No qubit allocation/deallocation may take place during the call + to the defined Algorithm :code:`func_algorithm` + + Attributes: + func_algorithm: Algorithm that initialite the state and to be used + in the QAA algorithm + func_oracle: The Oracle that marks the state(s) as "good" + system_qubits: the system we are interested on + qaa_ancilla: auxiliary qubit that helps to invert the amplitude of the + "good" states + + """ + def __init__(self, algorithm, oracle): + BasicGate.__init__(self) + self.algorithm = algorithm + self.oracle = oracle + + def __str__(self): + return 'QAA(Algorithm = {0}, Oracle = {1})'.format( + str(self.algorithm.__name__), str(self.oracle.__name__)) diff --git a/projectq/ops/_qaagate_test.py b/projectq/ops/_qaagate_test.py new file mode 100755 index 000000000..3e15e6801 --- /dev/null +++ b/projectq/ops/_qaagate_test.py @@ -0,0 +1,27 @@ +# Copyright 2019 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for projectq.ops._qaagate.""" + +from projectq.ops import _qaagate, All, H, X + + +def test_qaa_str(): + + def func_algorithm(): All(H) + + def func_oracle(): All(X) + + gate = _qaagate.QAA(func_algorithm, func_oracle) + assert str(gate) == "QAA(Algorithm = func_algorithm, Oracle = func_oracle)" diff --git a/projectq/ops/_qpegate.py b/projectq/ops/_qpegate.py new file mode 100755 index 000000000..08beee743 --- /dev/null +++ b/projectq/ops/_qpegate.py @@ -0,0 +1,29 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ._basics import BasicGate + + +class QPE(BasicGate): + """ + Quantum Phase Estimation gate. + + See setups.decompositions for the complete implementation + """ + def __init__(self, unitary): + BasicGate.__init__(self) + self.unitary = unitary + + def __str__(self): + return 'QPE({})'.format(str(self.unitary)) diff --git a/projectq/ops/_qpegate_test.py b/projectq/ops/_qpegate_test.py new file mode 100755 index 000000000..5ffcbf185 --- /dev/null +++ b/projectq/ops/_qpegate_test.py @@ -0,0 +1,23 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for projectq.ops._qpegate.""" + +from projectq.ops import _qpegate, X + + +def test_qpe_str(): + unitary = X + gate = _qpegate.QPE(unitary) + assert str(gate) == "QPE(X)" diff --git a/projectq/ops/_qubit_operator.py b/projectq/ops/_qubit_operator.py index aa9a29f9a..7fb65d1b2 100644 --- a/projectq/ops/_qubit_operator.py +++ b/projectq/ops/_qubit_operator.py @@ -362,9 +362,9 @@ def get_inverse(self): Raises: NotInvertible: Not implemented for QubitOperators which have multiple terms or a coefficient with absolute value - not equal to 1. + not equal to 1. """ - + if len(self.terms) == 1: (term, coefficient), = self.terms.items() if (not abs(coefficient) < 1 - EQ_TOLERANCE and not diff --git a/projectq/ops/_state_prep.py b/projectq/ops/_state_prep.py index 455a47a52..4ef51879d 100644 --- a/projectq/ops/_state_prep.py +++ b/projectq/ops/_state_prep.py @@ -30,9 +30,9 @@ def __init__(self, final_state): StatePreparation([0.5, -0.5j, -0.5, 0.5]) | qureg Note: - The amplitude of state k is final_state[k]. When the state k is - written in binary notation, then qureg[0] denotes the qubit - whose state corresponds to the least significant bit of k. + final_state[k] is taken to be the amplitude of the computational + basis state whose string is equal to the binary representation + of k. Args: final_state(list[complex]): wavefunction of the desired diff --git a/projectq/setups/decompositions/__init__.py b/projectq/setups/decompositions/__init__.py index aab71b28c..de557a065 100755 --- a/projectq/setups/decompositions/__init__.py +++ b/projectq/setups/decompositions/__init__.py @@ -16,22 +16,27 @@ barrier, carb1qubit2cnotrzandry, crz2cxandrz, + cnot2rxx, cnot2cz, cnu2toffoliandcu, entangle, globalphase, + h2rx, ph2r, qubitop2onequbit, qft2crandhadamard, r2rzandph, rx2rz, ry2rz, + rz2rx, sqrtswap2cnot, stateprep2cnot, swap2cnot, toffoli2cnotandtgate, time_evolution, - uniformlycontrolledr2cnot) + uniformlycontrolledr2cnot, + phaseestimation, + amplitudeamplification) all_defined_decomposition_rules = [ rule @@ -39,21 +44,26 @@ barrier, carb1qubit2cnotrzandry, crz2cxandrz, + cnot2rxx, cnot2cz, cnu2toffoliandcu, entangle, globalphase, + h2rx, ph2r, qubitop2onequbit, qft2crandhadamard, r2rzandph, rx2rz, ry2rz, + rz2rx, sqrtswap2cnot, stateprep2cnot, swap2cnot, toffoli2cnotandtgate, time_evolution, - uniformlycontrolledr2cnot] + uniformlycontrolledr2cnot, + phaseestimation, + amplitudeamplification] for rule in module.all_defined_decomposition_rules ] diff --git a/projectq/setups/decompositions/amplitudeamplification.py b/projectq/setups/decompositions/amplitudeamplification.py new file mode 100644 index 000000000..517aadeed --- /dev/null +++ b/projectq/setups/decompositions/amplitudeamplification.py @@ -0,0 +1,111 @@ +# Copyright 2019 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Registers a decomposition for quantum amplitude amplification. + +(Quick reference https://en.wikipedia.org/wiki/Amplitude_amplification. +Complete reference G. Brassard, P. Hoyer, M. Mosca, A. Tapp (2000) +Quantum Amplitude Amplification and Estimation +https://arxiv.org/abs/quant-ph/0005055) + +Quantum Amplitude Amplification (QAA) executes the algorithm, but not +the final measurement required to obtain the marked state(s) with high +probability. The starting state on wich the QAA algorithm is executed +is the one resulting of aplying the Algorithm on the |0> state. + +Example: + .. code-block:: python + + def func_algorithm(eng,system_qubits): + All(H) | system_qubits + + def func_oracle(eng,system_qubits,qaa_ancilla): + # This oracle selects the state |010> as the one marked + with Compute(eng): + All(X) | system_qubits[0::2] + with Control(eng, system_qubits): + X | qaa_ancilla + Uncompute(eng) + + system_qubits = eng.allocate_qureg(3) + # Prepare the qaa_ancilla qubit in the |-> state + qaa_ancilla = eng.allocate_qubit() + X | qaa_ancilla + H | qaa_ancilla + + # Creates the initial state form the Algorithm + func_algorithm(eng, system_qubits) + # Apply Quantum Amplitude Amplification the correct number of times + num_it = int(math.pi/4.*math.sqrt(1 << 3)) + with Loop(eng, num_it): + QAA(func_algorithm, func_oracle) | (system_qubits, qaa_ancilla) + + All(Measure) | system_qubits + +Warning: + No qubit allocation/deallocation may take place during the call + to the defined Algorithm :code:`func_algorithm` + +Attributes: + func_algorithm: Algorithm that initialite the state and to be used + in the QAA algorithm + func_oracle: The Oracle that marks the state(s) as "good" + system_qubits: the system we are interested on + qaa_ancilla: auxiliary qubit that helps to invert the amplitude of the + "good" states + +""" + +import math +import numpy as np + +from projectq.cengines import DecompositionRule +from projectq.meta import Control, Compute, Uncompute, CustomUncompute, Dagger +from projectq.ops import X, Z, Ph, All + +from projectq.ops import QAA + + +def _decompose_QAA(cmd): + """ Decompose the Quantum Amplitude Apmplification algorithm as a gate. """ + eng = cmd.engine + + # System-qubit is the first qubit/qureg. Ancilla qubit is the second qubit + system_qubits = cmd.qubits[0] + qaa_ancilla = cmd.qubits[1] + + # The Oracle and the Algorithm + Oracle = cmd.gate.oracle + A = cmd.gate.algorithm + + # Apply the oracle to invert the amplitude of the good states, S_Chi + Oracle(eng, system_qubits, qaa_ancilla) + + # Apply the inversion of the Algorithm, + # the inversion of the aplitude of |0> and the Algorithm + + with Compute(eng): + with Dagger(eng): + A(eng, system_qubits) + All(X) | system_qubits + with Control(eng, system_qubits[0:-1]): + Z | system_qubits[-1] + with CustomUncompute(eng): + All(X) | system_qubits + A(eng, system_qubits) + Ph(math.pi) | system_qubits[0] + + +#: Decomposition rules +all_defined_decomposition_rules = [DecompositionRule(QAA, _decompose_QAA)] diff --git a/projectq/setups/decompositions/amplitudeamplification_test.py b/projectq/setups/decompositions/amplitudeamplification_test.py new file mode 100644 index 000000000..f99681713 --- /dev/null +++ b/projectq/setups/decompositions/amplitudeamplification_test.py @@ -0,0 +1,182 @@ +# Copyright 2019 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.amplitudeamplification.py." + +import math +import pytest + +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import (AutoReplacer, DecompositionRuleSet, MainEngine) + +from projectq.ops import (X, H, Ry, All, Measure) +from projectq.meta import Loop, Control, Compute, Uncompute + +from projectq.ops import QAA +from projectq.setups.decompositions import amplitudeamplification as aa + + +def hache_algorithm(eng, qreg): + All(H) | qreg + + +def simple_oracle(eng, system_q, control): + # This oracle selects the state |1010101> as the one marked + with Compute(eng): + All(X) | system_q[1::2] + with Control(eng, system_q): + X | control + Uncompute(eng) + + +def test_simple_grover(): + rule_set = DecompositionRuleSet(modules=[aa]) + + eng = MainEngine(backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + ]) + + system_qubits = eng.allocate_qureg(7) + + # Prepare the control qubit in the |-> state + control = eng.allocate_qubit() + X | control + H | control + + # Creates the initial state form the Algorithm + hache_algorithm(eng, system_qubits) + + # Get the amplitude of the marked state before the AA + # to calculate the number of iterations + eng.flush() + prob1010101 = eng.backend.get_probability('1010101', system_qubits) + + total_amp_before = math.sqrt(prob1010101) + theta_before = math.asin(total_amp_before) + + # Apply Quantum Amplitude Amplification the correct number of times + # Theta is calculated previously using get_probability + # We calculate also the theoretical final probability + # of getting the good state + num_it = int(math.pi / (4. * theta_before) + 1) + theoretical_prob = math.sin((2 * num_it + 1.) * theta_before)**2 + with Loop(eng, num_it): + QAA(hache_algorithm, simple_oracle) | (system_qubits, control) + + # Get the probabilty of getting the marked state after the AA + # to compare with the theoretical probability after teh AA + eng.flush() + prob1010101 = eng.backend.get_probability('1010101', system_qubits) + total_prob_after = prob1010101 + + All(Measure) | system_qubits + H | control + Measure | control + result = [int(q) for q in system_qubits] + control_result = int(control) + + eng.flush() + + assert total_prob_after == pytest.approx(theoretical_prob, abs=1e-6), ( + "The obtained probability is less than expected %f vs. %f" % + (total_prob_after, theoretical_prob)) + + +def complex_algorithm(eng, qreg): + All(H) | qreg + with Control(eng, qreg[0]): + All(X) | qreg[1:] + All(Ry(math.pi / 4)) | qreg[1:] + with Control(eng, qreg[-1]): + All(X) | qreg[1:-1] + + +def complex_oracle(eng, system_q, control): + # This oracle selects the subspace |000000>+|111111> as the good one + with Compute(eng): + with Control(eng, system_q[0]): + All(X) | system_q[1:] + H | system_q[0] + All(X) | system_q + + with Control(eng, system_q): + X | control + + Uncompute(eng) + + +def test_complex_aa(): + rule_set = DecompositionRuleSet(modules=[aa]) + + eng = MainEngine(backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + ]) + + system_qubits = eng.allocate_qureg(6) + + # Prepare the control qubit in the |-> state + control = eng.allocate_qubit() + X | control + H | control + + # Creates the initial state form the Algorithm + complex_algorithm(eng, system_qubits) + + # Get the probabilty of getting the marked state before the AA + # to calculate the number of iterations + eng.flush() + prob000000 = eng.backend.get_probability('000000', system_qubits) + prob111111 = eng.backend.get_probability('111111', system_qubits) + + total_amp_before = math.sqrt(prob000000 + prob111111) + theta_before = math.asin(total_amp_before) + + # Apply Quantum Amplitude Amplification the correct number of times + # Theta is calculated previously using get_probability + # We calculate also the theoretical final probability + # of getting the good state + num_it = int(math.pi / (4. * theta_before) + 1) + theoretical_prob = math.sin((2 * num_it + 1.) * theta_before)**2 + with Loop(eng, num_it): + QAA(complex_algorithm, complex_oracle) | (system_qubits, control) + + # Get the probabilty of getting the marked state after the AA + # to compare with the theoretical probability after the AA + eng.flush() + prob000000 = eng.backend.get_probability('000000', system_qubits) + prob111111 = eng.backend.get_probability('111111', system_qubits) + total_prob_after = prob000000 + prob111111 + + All(Measure) | system_qubits + H | control + Measure | control + result = [int(q) for q in system_qubits] + control_result = int(control) + + eng.flush() + + assert total_prob_after == pytest.approx(theoretical_prob, abs=1e-2), ( + "The obtained probability is less than expected %f vs. %f" % + (total_prob_after, theoretical_prob)) + + +def test_string_functions(): + algorithm = hache_algorithm + oracle = simple_oracle + gate = QAA(algorithm, oracle) + assert (str(gate) == + "QAA(Algorithm = hache_algorithm, Oracle = simple_oracle)") diff --git a/projectq/setups/decompositions/arb1qubit2rzandry_test.py b/projectq/setups/decompositions/arb1qubit2rzandry_test.py index 90eba5edf..02ec907d6 100644 --- a/projectq/setups/decompositions/arb1qubit2rzandry_test.py +++ b/projectq/setups/decompositions/arb1qubit2rzandry_test.py @@ -23,8 +23,8 @@ from projectq.backends import Simulator from projectq.cengines import (AutoReplacer, DecompositionRuleSet, DummyEngine, InstructionFilter, MainEngine) -from projectq.ops import (BasicGate, ClassicalInstructionGate, Measure, Ph, R, - Rx, Ry, Rz, X) +from projectq.ops import (BasicGate, ClassicalInstructionGate, MatrixGate, + Measure, Ph, R, Rx, Ry, Rz, X) from projectq.meta import Control from . import arb1qubit2rzandry as arb1q @@ -51,7 +51,7 @@ def test_recognize_incorrect_gates(): # Does not have matrix attribute: BasicGate() | qubit # Two qubit gate: - two_qubit_gate = BasicGate() + two_qubit_gate = MatrixGate() two_qubit_gate.matrix = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] two_qubit_gate | qubit @@ -121,7 +121,7 @@ def create_test_matrices(): def test_decomposition(gate_matrix): for basis_state in ([1, 0], [0, 1]): # Create single qubit gate with gate_matrix - test_gate = BasicGate() + test_gate = MatrixGate() test_gate.matrix = np.matrix(gate_matrix) correct_dummy_eng = DummyEngine(save_commands=True) @@ -165,7 +165,7 @@ def test_decomposition(gate_matrix): [[0, 2], [4, 0]], [[1, 2], [4, 0]]]) def test_decomposition_errors(gate_matrix): - test_gate = BasicGate() + test_gate = MatrixGate() test_gate.matrix = np.matrix(gate_matrix) rule_set = DecompositionRuleSet(modules=[arb1q]) eng = MainEngine(backend=DummyEngine(), diff --git a/projectq/setups/decompositions/carb1qubit2cnotrzandry_test.py b/projectq/setups/decompositions/carb1qubit2cnotrzandry_test.py index 7d324e81c..44b29f526 100644 --- a/projectq/setups/decompositions/carb1qubit2cnotrzandry_test.py +++ b/projectq/setups/decompositions/carb1qubit2cnotrzandry_test.py @@ -21,8 +21,8 @@ from projectq.cengines import (AutoReplacer, DecompositionRuleSet, DummyEngine, InstructionFilter, MainEngine) from projectq.meta import Control -from projectq.ops import (All, BasicGate, ClassicalInstructionGate, Measure, - Ph, R, Rx, Ry, Rz, X, XGate) +from projectq.ops import (All, BasicGate, ClassicalInstructionGate, + MatrixGate, Measure, Ph, R, Rx, Ry, Rz, X, XGate) from projectq.setups.decompositions import arb1qubit2rzandry_test as arb1q_t from . import carb1qubit2cnotrzandry as carb1q @@ -57,7 +57,7 @@ def test_recognize_incorrect_gates(): # Does not have matrix attribute: BasicGate() | qubit # Two qubit gate: - two_qubit_gate = BasicGate() + two_qubit_gate = MatrixGate() two_qubit_gate.matrix = np.matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) two_qubit_gate | qubit @@ -94,7 +94,7 @@ def test_recognize_v(gate_matrix): @pytest.mark.parametrize("gate_matrix", arb1q_t.create_test_matrices()) def test_decomposition(gate_matrix): # Create single qubit gate with gate_matrix - test_gate = BasicGate() + test_gate = MatrixGate() test_gate.matrix = np.matrix(gate_matrix) for basis_state in ([1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], diff --git a/projectq/setups/decompositions/cnot2rxx.py b/projectq/setups/decompositions/cnot2rxx.py new file mode 100644 index 000000000..a1fa2e6ac --- /dev/null +++ b/projectq/setups/decompositions/cnot2rxx.py @@ -0,0 +1,61 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Module uses ideas from "Basic circuit compilation techniques +# for an ion-trap quantum machine" by Dmitri Maslov (2017) at +# https://iopscience.iop.org/article/10.1088/1367-2630/aa5e47 +""" +Registers a decomposition to for a CNOT gate in terms of Rxx, Rx and Ry gates. +""" + +from projectq.cengines import DecompositionRule +from projectq.meta import get_control_count +from projectq.ops import Ph, Rxx, Ry, Rx, X +import math + + +def _decompose_cnot2rxx_M(cmd): + """ Decompose CNOT gate into Rxx gate. """ + # Labelled 'M' for 'minus' because decomposition ends with a Ry(-pi/2) + ctrl = cmd.control_qubits + Ry(math.pi / 2) | ctrl[0] + Ph(7 * math.pi / 4) | ctrl[0] + Rx(-math.pi / 2) | ctrl[0] + Rx(-math.pi / 2) | cmd.qubits[0][0] + Rxx(math.pi / 2) | (ctrl[0], cmd.qubits[0][0]) + Ry(-1 * math.pi / 2) | ctrl[0] + + +def _decompose_cnot2rxx_P(cmd): + """ Decompose CNOT gate into Rxx gate. """ + # Labelled 'P' for 'plus' because decomposition ends with a Ry(+pi/2) + ctrl = cmd.control_qubits + Ry(-math.pi / 2) | ctrl[0] + Ph(math.pi / 4) | ctrl[0] + Rx(-math.pi / 2) | ctrl[0] + Rx(math.pi / 2) | cmd.qubits[0][0] + Rxx(math.pi / 2) | (ctrl[0], cmd.qubits[0][0]) + Ry(math.pi / 2) | ctrl[0] + + +def _recognize_cnot2(cmd): + """ Identify that the command is a CNOT gate (control - X gate)""" + return get_control_count(cmd) == 1 + + +#: Decomposition rules +all_defined_decomposition_rules = [ + DecompositionRule(X.__class__, _decompose_cnot2rxx_M, _recognize_cnot2), + DecompositionRule(X.__class__, _decompose_cnot2rxx_P, _recognize_cnot2) +] diff --git a/projectq/setups/decompositions/cnot2rxx_test.py b/projectq/setups/decompositions/cnot2rxx_test.py new file mode 100644 index 000000000..bc0d0c077 --- /dev/null +++ b/projectq/setups/decompositions/cnot2rxx_test.py @@ -0,0 +1,124 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.cnot2rxx.py." + +import pytest +import numpy as np + +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import (AutoReplacer, DecompositionRuleSet, DummyEngine, + InstructionFilter) +from projectq.meta import Control +from projectq.ops import All, CNOT, CZ, Measure, X, Z + +from . import cnot2rxx + + +def test_recognize_correct_gates(): + """Test that recognize_cnot recognizes cnot gates. """ + saving_backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=saving_backend) + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qubit3 = eng.allocate_qubit() + eng.flush() + # Create a control function in 3 different ways + CZ | (qubit1, qubit2) + with Control(eng, qubit2): + Z | qubit1 + X | qubit1 + with Control(eng, qubit2 + qubit3): + Z | qubit1 + eng.flush() + eng.flush(deallocate_qubits=True) + for cmd in saving_backend.received_commands[4:7]: + assert cnot2rxx._recognize_cnot2(cmd) + for cmd in saving_backend.received_commands[7:9]: + assert not cnot2rxx._recognize_cnot2(cmd) + + +def _decomp_gates(eng, cmd): + """ Test that the cmd.gate is a gate of class X """ + if len(cmd.control_qubits) == 1 and isinstance(cmd.gate, X.__class__): + return False + return True + + +# ------------test_decomposition function-------------# +# Creates two engines, correct_eng and test_eng. +# correct_eng implements CNOT gate. +# test_eng implements the decomposition of the CNOT gate. +# correct_qb and test_qb represent results of these two engines, respectively. +# +# The decomposition in this case only produces the same state as CNOT up to a +# global phase. +# test_vector and correct_vector represent the final wave states of correct_qb +# and test_qb. +# +# The dot product of correct_vector and test_vector should have absolute value +# 1, if the two vectors are the same up to a global phase. + + +def test_decomposition(): + """ Test that this decomposition of CNOT produces correct amplitudes + + Function tests each DecompositionRule in + cnot2rxx.all_defined_decomposition_rules + """ + decomposition_rule_list = cnot2rxx.all_defined_decomposition_rules + for rule in decomposition_rule_list: + for basis_state_index in range(0, 4): + basis_state = [0] * 4 + basis_state[basis_state_index] = 1. + correct_dummy_eng = DummyEngine(save_commands=True) + correct_eng = MainEngine(backend=Simulator(), + engine_list=[correct_dummy_eng]) + rule_set = DecompositionRuleSet(rules=[rule]) + test_dummy_eng = DummyEngine(save_commands=True) + test_eng = MainEngine(backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(_decomp_gates), + test_dummy_eng + ]) + test_sim = test_eng.backend + correct_sim = correct_eng.backend + correct_qb = correct_eng.allocate_qubit() + correct_ctrl_qb = correct_eng.allocate_qubit() + correct_eng.flush() + test_qb = test_eng.allocate_qubit() + test_ctrl_qb = test_eng.allocate_qubit() + test_eng.flush() + + correct_sim.set_wavefunction(basis_state, + correct_qb + correct_ctrl_qb) + test_sim.set_wavefunction(basis_state, test_qb + test_ctrl_qb) + CNOT | (test_ctrl_qb, test_qb) + CNOT | (correct_ctrl_qb, correct_qb) + + test_eng.flush() + correct_eng.flush() + + assert len(correct_dummy_eng.received_commands) == 5 + assert len(test_dummy_eng.received_commands) == 10 + + assert correct_eng.backend.cheat()[1] == pytest.approx( + test_eng.backend.cheat()[1], rel=1e-12, abs=1e-12) + + All(Measure) | test_qb + test_ctrl_qb + All(Measure) | correct_qb + correct_ctrl_qb + test_eng.flush(deallocate_qubits=True) + correct_eng.flush(deallocate_qubits=True) diff --git a/projectq/setups/decompositions/h2rx.py b/projectq/setups/decompositions/h2rx.py new file mode 100644 index 000000000..b54533bad --- /dev/null +++ b/projectq/setups/decompositions/h2rx.py @@ -0,0 +1,57 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Module uses ideas from "Basic circuit compilation techniques for an +# ion-trap quantum machine" by Dmitri Maslov (2017) at +# https://iopscience.iop.org/article/10.1088/1367-2630/aa5e47 +""" +Registers a decomposition for the H gate into an Ry and Rx gate. +""" + +import math + +from projectq.cengines import DecompositionRule +from projectq.meta import get_control_count +from projectq.ops import Ph, Rx, Ry, H + + +def _decompose_h2rx_M(cmd): + """ Decompose the Ry gate.""" + # Labelled 'M' for 'minus' because decomposition ends with a Ry(-pi/2) + qubit = cmd.qubits[0] + Rx(math.pi) | qubit + Ph(math.pi/2) | qubit + Ry(-1 * math.pi / 2) | qubit + + +def _decompose_h2rx_N(cmd): + """ Decompose the Ry gate.""" + # Labelled 'N' for 'neutral' because decomposition doesn't end with + # Ry(pi/2) or Ry(-pi/2) + qubit = cmd.qubits[0] + Ry(math.pi / 2) | qubit + Ph(3*math.pi/2) | qubit + Rx(-1 * math.pi) | qubit + + +def _recognize_HNoCtrl(cmd): + """ For efficiency reasons only if no control qubits.""" + return get_control_count(cmd) == 0 + + +#: Decomposition rules +all_defined_decomposition_rules = [ + DecompositionRule(H.__class__, _decompose_h2rx_N, _recognize_HNoCtrl), + DecompositionRule(H.__class__, _decompose_h2rx_M, _recognize_HNoCtrl) +] diff --git a/projectq/setups/decompositions/h2rx_test.py b/projectq/setups/decompositions/h2rx_test.py new file mode 100644 index 000000000..2df048801 --- /dev/null +++ b/projectq/setups/decompositions/h2rx_test.py @@ -0,0 +1,117 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.h2rx.py" + +import numpy as np + +import pytest + +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import (AutoReplacer, DecompositionRuleSet, DummyEngine, + InstructionFilter) +from projectq.meta import Control +from projectq.ops import Measure, X, H, HGate + +from . import h2rx + + +def test_recognize_correct_gates(): + """ Test that recognize_HNoCtrl recognizes ctrl qubits """ + saving_backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=saving_backend) + qubit = eng.allocate_qubit() + ctrl_qubit = eng.allocate_qubit() + eng.flush() + H | qubit + with Control(eng, ctrl_qubit): + H | qubit + eng.flush(deallocate_qubits=True) + assert h2rx._recognize_HNoCtrl(saving_backend.received_commands[3]) + assert not h2rx._recognize_HNoCtrl(saving_backend.received_commands[4]) + + +def h_decomp_gates(eng, cmd): + """ Test that cmd.gate is a gate of class HGate """ + g = cmd.gate + if isinstance(g, HGate): # H is just a shortcut to HGate + return False + else: + return True + + +# ------------test_decomposition function-------------# +# Creates two engines, correct_eng and test_eng. +# correct_eng implements H gate. +# test_eng implements the decomposition of the H gate. +# correct_qb and test_qb represent results of these two engines, respectively. +# +# The decomposition in this case only produces the same state as H up to a +# global phase. +# test_vector and correct_vector represent the final wave states of correct_qb +# and test_qb. +# The dot product of correct_vector and test_vector should have absolute value +# 1, if the two vectors are the same up to a global phase. + + +def test_decomposition(): + """ Test that this decomposition of H produces correct amplitudes + + Function tests each DecompositionRule in + h2rx.all_defined_decomposition_rules + """ + decomposition_rule_list = h2rx.all_defined_decomposition_rules + for rule in decomposition_rule_list: + for basis_state_index in range(2): + basis_state = [0] * 2 + basis_state[basis_state_index] = 1. + + correct_dummy_eng = DummyEngine(save_commands=True) + correct_eng = MainEngine(backend=Simulator(), + engine_list=[correct_dummy_eng]) + + rule_set = DecompositionRuleSet(rules=[rule]) + test_dummy_eng = DummyEngine(save_commands=True) + test_eng = MainEngine(backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(h_decomp_gates), + test_dummy_eng + ]) + + correct_qb = correct_eng.allocate_qubit() + correct_eng.flush() + test_qb = test_eng.allocate_qubit() + test_eng.flush() + + correct_eng.backend.set_wavefunction(basis_state, correct_qb) + test_eng.backend.set_wavefunction(basis_state, test_qb) + + H | correct_qb + H | test_qb + + correct_eng.flush() + test_eng.flush() + + assert H in (cmd.gate + for cmd in correct_dummy_eng.received_commands) + assert H not in (cmd.gate + for cmd in test_dummy_eng.received_commands) + + assert correct_eng.backend.cheat()[1] == pytest.approx( + test_eng.backend.cheat()[1], rel=1e-12, abs=1e-12) + + Measure | test_qb + Measure | correct_qb diff --git a/projectq/setups/decompositions/phaseestimation.py b/projectq/setups/decompositions/phaseestimation.py new file mode 100644 index 000000000..faf7523cf --- /dev/null +++ b/projectq/setups/decompositions/phaseestimation.py @@ -0,0 +1,129 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Registers a decomposition for phase estimation. + +(reference https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm) + +The Quantum Phase Estimation (QPE) executes the algorithm up to the inverse +QFT included. The following steps measuring the ancillas and computing the +phase should be executed outside of the QPE. + +The decomposition uses as ancillas (qpe_ancillas) the first qubit/qureg in +the Command and as system qubits teh second qubit/qureg in the Command. + +The unitary operator for which the phase estimation is estimated (unitary) +is the gate in Command + +Example: + .. code-block:: python + + # Example using a ProjectQ gate + + n_qpe_ancillas = 3 + qpe_ancillas = eng.allocate_qureg(n_qpe_ancillas) + system_qubits = eng.allocate_qureg(1) + angle = cmath.pi*2.*0.125 + U = Ph(angle) # unitary_specfic_to_the_problem() + + # Apply Quantum Phase Estimation + QPE(U) | (qpe_ancillas, system_qubits) + + All(Measure) | qpe_ancillas + # Compute the phase from the ancilla measurement + #(https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm) + phasebinlist = [int(q) for q in qpe_ancillas] + phase_in_bin = ''.join(str(j) for j in phasebinlist) + phase_int = int(phase_in_bin,2) + phase = phase_int / (2 ** n_qpe_ancillas) + print (phase) + + # Example using a function (two_qubit_gate). + # Instead of applying QPE on a gate U one could provide a function + + def two_qubit_gate(system_q, time): + CNOT | (system_q[0], system_q[1]) + Ph(2.0*cmath.pi*(time * 0.125)) | system_q[1] + CNOT | (system_q[0], system_q[1]) + + n_qpe_ancillas = 3 + qpe_ancillas = eng.allocate_qureg(n_qpe_ancillas) + system_qubits = eng.allocate_qureg(2) + X | system_qubits[0] + + # Apply Quantum Phase Estimation + QPE(two_qubit_gate) | (qpe_ancillas, system_qubits) + + All(Measure) | qpe_ancillas + # Compute the phase from the ancilla measurement + #(https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm) + phasebinlist = [int(q) for q in qpe_ancillas] + phase_in_bin = ''.join(str(j) for j in phasebinlist) + phase_int = int(phase_in_bin,2) + phase = phase_int / (2 ** n_qpe_ancillas) + print (phase) + +Attributes: + unitary (BasicGate): Unitary Operation either a ProjectQ gate or a function f. + Calling the function with the parameters system_qubits(Qureg) and time (integer), + i.e. f(system_qubits, time), applies to the system qubits a unitary defined in f + with parameter time. + + +""" + +import numpy as np + +from projectq.cengines import DecompositionRule +from projectq.meta import Control, Loop, get_control_count +from projectq.ops import H, Tensor, get_inverse, QFT + +from projectq.ops import QPE + + +def _decompose_QPE(cmd): + """ Decompose the Quantum Phase Estimation gate. """ + eng = cmd.engine + + # Ancillas is the first qubit/qureg. System-qubit is the second qubit/qureg + qpe_ancillas = cmd.qubits[0] + system_qubits = cmd.qubits[1] + + # Hadamard on the ancillas + Tensor(H) | qpe_ancillas + + # The Unitary Operator + U = cmd.gate.unitary + + # Control U on the system_qubits + if (callable(U)): + # If U is a function + for i in range(len(qpe_ancillas)): + with Control(eng, qpe_ancillas[i]): + U(system_qubits, time=2**i) + else: + for i in range(len(qpe_ancillas)): + ipower = int(2**i) + with Loop(eng, ipower): + with Control(eng, qpe_ancillas[i]): + U | system_qubits + + # Inverse QFT on the ancillas + get_inverse(QFT) | qpe_ancillas + +#: Decomposition rules +all_defined_decomposition_rules = [ + DecompositionRule(QPE, _decompose_QPE) +] diff --git a/projectq/setups/decompositions/phaseestimation_test.py b/projectq/setups/decompositions/phaseestimation_test.py new file mode 100644 index 000000000..1b8f8e63c --- /dev/null +++ b/projectq/setups/decompositions/phaseestimation_test.py @@ -0,0 +1,162 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.phaseestimation.py." + +import cmath +import numpy as np +import pytest + +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import (AutoReplacer, DecompositionRuleSet, + DummyEngine, InstructionFilter, MainEngine) + +from projectq.ops import X, H, All, Measure, Tensor, Ph, CNOT, StatePreparation + +from projectq.ops import (BasicGate) + +from projectq.ops import QPE +from projectq.setups.decompositions import phaseestimation as pe +from projectq.setups.decompositions import qft2crandhadamard as dqft +import projectq.setups.decompositions.stateprep2cnot as stateprep2cnot +import projectq.setups.decompositions.uniformlycontrolledr2cnot as ucr2cnot + + +def test_simple_test_X_eigenvectors(): + rule_set = DecompositionRuleSet(modules=[pe, dqft]) + eng = MainEngine(backend=Simulator(), + engine_list=[AutoReplacer(rule_set), + ]) + results = np.array([]) + for i in range(100): + autovector = eng.allocate_qureg(1) + X | autovector + H | autovector + unit = X + ancillas = eng.allocate_qureg(1) + QPE(unit) | (ancillas, autovector) + All(Measure) | ancillas + fasebinlist = [int(q) for q in ancillas] + fasebin = ''.join(str(j) for j in fasebinlist) + faseint = int(fasebin, 2) + phase = faseint / (2. ** (len(ancillas))) + results = np.append(results, phase) + All(Measure) | autovector + eng.flush() + + num_phase = (results == 0.5).sum() + assert num_phase/100. >= 0.35, "Statistics phase calculation are not correct (%f vs. %f)" % (num_phase/100., 0.35) + + +def test_Ph_eigenvectors(): + rule_set = DecompositionRuleSet(modules=[pe, dqft]) + eng = MainEngine(backend=Simulator(), + engine_list=[AutoReplacer(rule_set), + ]) + results = np.array([]) + for i in range(100): + autovector = eng.allocate_qureg(1) + theta = cmath.pi*2.*0.125 + unit = Ph(theta) + ancillas = eng.allocate_qureg(3) + QPE(unit) | (ancillas, autovector) + All(Measure) | ancillas + fasebinlist = [int(q) for q in ancillas] + fasebin = ''.join(str(j) for j in fasebinlist) + faseint = int(fasebin, 2) + phase = faseint / (2. ** (len(ancillas))) + results = np.append(results, phase) + All(Measure) | autovector + eng.flush() + + num_phase = (results == 0.125).sum() + assert num_phase/100. >= 0.35, "Statistics phase calculation are not correct (%f vs. %f)" % (num_phase/100., 0.35) + + +def two_qubit_gate(system_q, time): + CNOT | (system_q[0], system_q[1]) + Ph(2.0*cmath.pi*(time * 0.125)) | system_q[1] + CNOT | (system_q[0], system_q[1]) + + +def test_2qubitsPh_andfunction_eigenvectors(): + rule_set = DecompositionRuleSet(modules=[pe, dqft]) + eng = MainEngine(backend=Simulator(), + engine_list=[AutoReplacer(rule_set), + ]) + results = np.array([]) + for i in range(100): + autovector = eng.allocate_qureg(2) + X | autovector[0] + ancillas = eng.allocate_qureg(3) + QPE(two_qubit_gate) | (ancillas, autovector) + All(Measure) | ancillas + fasebinlist = [int(q) for q in ancillas] + fasebin = ''.join(str(j) for j in fasebinlist) + faseint = int(fasebin, 2) + phase = faseint / (2. ** (len(ancillas))) + results = np.append(results, phase) + All(Measure) | autovector + eng.flush() + + num_phase = (results == 0.125).sum() + assert num_phase/100. >= 0.34, "Statistics phase calculation are not correct (%f vs. %f)" % (num_phase/100., 0.34) + + +def test_X_no_eigenvectors(): + rule_set = DecompositionRuleSet(modules=[pe, dqft, stateprep2cnot, ucr2cnot]) + eng = MainEngine(backend=Simulator(), + engine_list=[AutoReplacer(rule_set), + ]) + results = np.array([]) + results_plus = np.array([]) + results_minus = np.array([]) + for i in range(100): + autovector = eng.allocate_qureg(1) + amplitude0 = (np.sqrt(2) + np.sqrt(6))/4. + amplitude1 = (np.sqrt(2) - np.sqrt(6))/4. + StatePreparation([amplitude0, amplitude1]) | autovector + unit = X + ancillas = eng.allocate_qureg(1) + QPE(unit) | (ancillas, autovector) + All(Measure) | ancillas + fasebinlist = [int(q) for q in ancillas] + fasebin = ''.join(str(j) for j in fasebinlist) + faseint = int(fasebin, 2) + phase = faseint / (2. ** (len(ancillas))) + results = np.append(results, phase) + Tensor(H) | autovector + if np.allclose(phase, .0, rtol=1e-1): + results_plus = np.append(results_plus, phase) + All(Measure) | autovector + autovector_result = int(autovector) + assert autovector_result == 0 + elif np.allclose(phase, .5, rtol=1e-1): + results_minus = np.append(results_minus, phase) + All(Measure) | autovector + autovector_result = int(autovector) + assert autovector_result == 1 + eng.flush() + + total = len(results_plus) + len(results_minus) + plus_probability = len(results_plus)/100. + assert total == pytest.approx(100, abs=5) + assert plus_probability == pytest.approx(1./4., abs = 1e-1), "Statistics on |+> probability are not correct (%f vs. %f)" % (plus_probability, 1./4.) + + +def test_string(): + unit = X + gate = QPE(unit) + assert (str(gate) == "QPE(X)") diff --git a/projectq/setups/decompositions/qubitop2onequbit.py b/projectq/setups/decompositions/qubitop2onequbit.py index b66f64b25..61f7954e7 100644 --- a/projectq/setups/decompositions/qubitop2onequbit.py +++ b/projectq/setups/decompositions/qubitop2onequbit.py @@ -12,6 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +Registers a decomposition rule for a unitary QubitOperator to one qubit gates. +""" + import cmath from projectq.cengines import DecompositionRule diff --git a/projectq/setups/decompositions/rz2rx.py b/projectq/setups/decompositions/rz2rx.py new file mode 100644 index 000000000..f49ba72e1 --- /dev/null +++ b/projectq/setups/decompositions/rz2rx.py @@ -0,0 +1,67 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Module uses ideas from "Basic circuit compilation techniques for an +# ion-trap quantum machine" by Dmitri Maslov (2017) at +# https://iopscience.iop.org/article/10.1088/1367-2630/aa5e47 +""" +Registers a decomposition for the Rz gate into an Rx and Ry(pi/2) or Ry(-pi/2) +gate +""" + +import math + +from projectq.cengines import DecompositionRule +from projectq.meta import Compute, Control, get_control_count, Uncompute +from projectq.ops import Rx, Ry, Rz + + +def _decompose_rz2rx_P(cmd): + """ Decompose the Rz using negative angle. """ + # Labelled 'P' for 'plus' because decomposition ends with a Ry(+pi/2) + qubit = cmd.qubits[0] + eng = cmd.engine + angle = cmd.gate.angle + + with Control(eng, cmd.control_qubits): + with Compute(eng): + Ry(-math.pi / 2.) | qubit + Rx(-angle) | qubit + Uncompute(eng) + + +def _decompose_rz2rx_M(cmd): + """ Decompose the Rz using positive angle. """ + # Labelled 'M' for 'minus' because decomposition ends with a Ry(-pi/2) + qubit = cmd.qubits[0] + eng = cmd.engine + angle = cmd.gate.angle + + with Control(eng, cmd.control_qubits): + with Compute(eng): + Ry(math.pi / 2.) | qubit + Rx(angle) | qubit + Uncompute(eng) + + +def _recognize_RzNoCtrl(cmd): + """ Decompose the gate only if the command represents a single qubit gate (if it is not part of a control gate).""" + return get_control_count(cmd) == 0 + + +#: Decomposition rules +all_defined_decomposition_rules = [ + DecompositionRule(Rz, _decompose_rz2rx_P, _recognize_RzNoCtrl), + DecompositionRule(Rz, _decompose_rz2rx_M, _recognize_RzNoCtrl) +] diff --git a/projectq/setups/decompositions/rz2rx_test.py b/projectq/setups/decompositions/rz2rx_test.py new file mode 100644 index 000000000..7c6c9962f --- /dev/null +++ b/projectq/setups/decompositions/rz2rx_test.py @@ -0,0 +1,125 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.rz2rx.py" + +import math +import numpy as np +import pytest + +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import (AutoReplacer, DecompositionRuleSet, DummyEngine, + InstructionFilter) +from projectq.meta import Control +from projectq.ops import Measure, Rz + +from . import rz2rx + + +def test_recognize_correct_gates(): + """ Test that recognize_RzNoCtrl recognizes ctrl qubits """ + saving_backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=saving_backend) + qubit = eng.allocate_qubit() + ctrl_qubit = eng.allocate_qubit() + eng.flush() + Rz(0.3) | qubit + with Control(eng, ctrl_qubit): + Rz(0.4) | qubit + eng.flush(deallocate_qubits=True) + assert rz2rx._recognize_RzNoCtrl(saving_backend.received_commands[3]) + assert not rz2rx._recognize_RzNoCtrl(saving_backend.received_commands[4]) + + +def rz_decomp_gates(eng, cmd): + """ Test that cmd.gate is the gate Rz """ + g = cmd.gate + if isinstance(g, Rz): + return False + else: + return True + + +# ------------test_decomposition function-------------# +# Creates two engines, correct_eng and test_eng. +# correct_eng implements Rz(angle) gate. +# test_eng implements the decomposition of the Rz(angle) gate. +# correct_qb and test_qb represent results of these two engines, respectively. +# +# The decomposition only needs to produce the same state in a qubit up to a +# global phase. +# test_vector and correct_vector represent the final wave states of correct_qb +# and test_qb. +# +# The dot product of correct_vector and test_vector should have absolute value +# 1, if the two vectors are the same up to a global phase. + + +@pytest.mark.parametrize("angle", [0, math.pi, 2 * math.pi, 4 * math.pi, 0.5]) +def test_decomposition(angle): + """ + Test that this decomposition of Rz produces correct amplitudes + + Note that this function tests each DecompositionRule in + rz2rx.all_defined_decomposition_rules + """ + decomposition_rule_list = rz2rx.all_defined_decomposition_rules + for rule in decomposition_rule_list: + for basis_state in ([1, 0], [0, 1]): + correct_dummy_eng = DummyEngine(save_commands=True) + correct_eng = MainEngine(backend=Simulator(), + engine_list=[correct_dummy_eng]) + + rule_set = DecompositionRuleSet(rules=[rule]) + test_dummy_eng = DummyEngine(save_commands=True) + test_eng = MainEngine(backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(rz_decomp_gates), + test_dummy_eng + ]) + + correct_qb = correct_eng.allocate_qubit() + Rz(angle) | correct_qb + correct_eng.flush() + + test_qb = test_eng.allocate_qubit() + Rz(angle) | test_qb + test_eng.flush() + + # Create empty vectors for the wave vectors for the correct and + # test qubits + correct_vector = np.zeros((2, 1), dtype=np.complex_) + test_vector = np.zeros((2, 1), dtype=np.complex_) + + i = 0 + for fstate in ['0', '1']: + test = test_eng.backend.get_amplitude(fstate, test_qb) + correct = correct_eng.backend.get_amplitude(fstate, correct_qb) + correct_vector[i] = correct + test_vector[i] = test + i += 1 + + # Necessary to transpose vector to use matrix dot product + test_vector = test_vector.transpose() + # Remember that transposed vector should come first in product + vector_dot_product = np.dot(test_vector, correct_vector) + + assert np.absolute(vector_dot_product) == pytest.approx(1, + rel=1e-12, + abs=1e-12) + + Measure | test_qb + Measure | correct_qb diff --git a/projectq/setups/ibm.py b/projectq/setups/ibm.py index a5fb2c802..acedeed00 100755 --- a/projectq/setups/ibm.py +++ b/projectq/setups/ibm.py @@ -11,46 +11,116 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +""" +Defines a setup allowing to compile code for the IBM quantum chips: +->Any 5 qubit devices +->the ibmq online simulator +->the melbourne 15 qubit device +It provides the `engine_list` for the `MainEngine' based on the requested +device. Decompose the circuit into a Rx/Ry/Rz/H/CNOT gate set that will be +translated in the backend in the U1/U2/U3/CX gate set. """ -Defines a setup useful for the IBM QE chip with 5 qubits. -It provides the `engine_list` for the `MainEngine`, and contains an -AutoReplacer with most of the gate decompositions of ProjectQ, among others -it includes: +import projectq +import projectq.setups.decompositions +from projectq.setups import restrictedgateset +from projectq.ops import (Rx, Ry, Rz, H, CNOT, Barrier) +from projectq.cengines import (LocalOptimizer, IBM5QubitMapper, + SwapAndCNOTFlipper, BasicMapperEngine, + GridMapper) +from projectq.backends._ibm._ibm_http_client import show_devices - * Controlled z-rotations --> Controlled NOTs and single-qubit rotations - * Toffoli gate --> CNOT and single-qubit gates - * m-Controlled global phases --> (m-1)-controlled phase-shifts - * Global phases --> ignore - * (controlled) Swap gates --> CNOTs and Toffolis - * Arbitrary single qubit gates --> Rz and Ry - * Controlled arbitrary single qubit gates --> Rz, Ry, and CNOT gates -Moreover, it contains `LocalOptimizers` and a custom mapper for the CNOT -gates. +def get_engine_list(token=None, device=None): + # Access to the hardware properties via show_devices + # Can also be extended to take into account gate fidelities, new available + # gate, etc.. + devices = show_devices(token) + ibm_setup = [] + if device not in devices: + raise DeviceOfflineError('Error when configuring engine list: device ' + 'requested for Backend not connected') + if devices[device]['nq'] == 5: + # The requested device is a 5 qubit processor + # Obtain the coupling map specific to the device + coupling_map = devices[device]['coupling_map'] + coupling_map = list2set(coupling_map) + mapper = IBM5QubitMapper(coupling_map) + ibm_setup = [ + mapper, + SwapAndCNOTFlipper(coupling_map), + LocalOptimizer(10) + ] + elif device == 'ibmq_qasm_simulator': + # The 32 qubit online simulator doesn't need a specific mapping for + # gates. Can also run wider gateset but this setup keep the + # restrictedgateset setup for coherence + mapper = BasicMapperEngine() + # Note: Manual Mapper doesn't work, because its map is updated only if + # gates are applied if gates in the register are not used, then it + # will lead to state errors + res = dict() + for i in range(devices[device]['nq']): + res[i] = i + mapper.current_mapping = res + ibm_setup = [mapper] + elif device == 'ibmq_16_melbourne': + # Only 15 qubits available on this ibmqx2 unit(in particular qubit 7 + # on the grid), therefore need custom grid mapping + grid_to_physical = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 15, + 8: 14, + 9: 13, + 10: 12, + 11: 11, + 12: 10, + 13: 9, + 14: 8, + 15: 7 + } + coupling_map = devices[device]['coupling_map'] + coupling_map = list2set(coupling_map) + ibm_setup = [ + GridMapper(2, 8, grid_to_physical), + LocalOptimizer(5), + SwapAndCNOTFlipper(coupling_map), + LocalOptimizer(5) + ] + else: + # If there is an online device not handled into ProjectQ it's not too + # bad, the engine_list can be constructed manually with the + # appropriate mapper and the 'coupling_map' parameter + raise DeviceNotHandledError('Device not yet fully handled by ProjectQ') -""" + # Most IBM devices accept U1,U2,U3,CX gates. + # Most gates need to be decomposed into a subset that is manually converted + # in the backend (until the implementation of the U1,U2,U3) + # available gates decomposable now for U1,U2,U3: Rx,Ry,Rz and H + setup = restrictedgateset.get_engine_list(one_qubit_gates=(Rx, Ry, Rz, H), + two_qubit_gates=(CNOT, ), + other_gates=(Barrier, )) + setup.extend(ibm_setup) + return setup -import projectq -import projectq.setups.decompositions -from projectq.cengines import (TagRemover, - LocalOptimizer, - AutoReplacer, - IBM5QubitMapper, - SwapAndCNOTFlipper, - DecompositionRuleSet) - - -ibmqx4_connections = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) - - -def get_engine_list(): - rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions]) - return [TagRemover(), - LocalOptimizer(10), - AutoReplacer(rule_set), - TagRemover(), - IBM5QubitMapper(), - SwapAndCNOTFlipper(ibmqx4_connections), - LocalOptimizer(10)] + +class DeviceOfflineError(Exception): + pass + + +class DeviceNotHandledError(Exception): + pass + + +def list2set(coupling_list): + result = [] + for el in coupling_list: + result.append(tuple(el)) + return set(result) diff --git a/projectq/setups/ibm_test.py b/projectq/setups/ibm_test.py index 598b949cb..26b41b24a 100644 --- a/projectq/setups/ibm_test.py +++ b/projectq/setups/ibm_test.py @@ -13,17 +13,60 @@ # limitations under the License. """Tests for projectq.setup.ibm.""" -import projectq -from projectq import MainEngine -from projectq.cengines import IBM5QubitMapper, SwapAndCNOTFlipper +import pytest -def test_ibm_cnot_mapper_in_cengines(): +def test_ibm_cnot_mapper_in_cengines(monkeypatch): import projectq.setups.ibm - found = 0 - for engine in projectq.setups.ibm.get_engine_list(): - if isinstance(engine, IBM5QubitMapper): - found |= 1 - if isinstance(engine, SwapAndCNOTFlipper): - found |= 2 - assert found == 3 + + def mock_show_devices(*args, **kwargs): + connections = set([(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), (2, 1), + (2, 3), (2, 4), (3, 1), (3, 4), (4, 3)]) + return { + 'ibmq_burlington': { + 'coupling_map': connections, + 'version': '0.0.0', + 'nq': 5 + }, + 'ibmq_16_melbourne': { + 'coupling_map': connections, + 'version': '0.0.0', + 'nq': 15 + }, + 'ibmq_qasm_simulator': { + 'coupling_map': connections, + 'version': '0.0.0', + 'nq': 32 + } + } + + monkeypatch.setattr(projectq.setups.ibm, "show_devices", mock_show_devices) + engines_5qb = projectq.setups.ibm.get_engine_list(device='ibmq_burlington') + engines_15qb = projectq.setups.ibm.get_engine_list( + device='ibmq_16_melbourne') + engines_simulator = projectq.setups.ibm.get_engine_list( + device='ibmq_qasm_simulator') + assert len(engines_5qb) == 15 + assert len(engines_15qb) == 16 + assert len(engines_simulator) == 13 + + +def test_ibm_errors(monkeypatch): + import projectq.setups.ibm + + def mock_show_devices(*args, **kwargs): + connections = set([(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), (2, 1), + (2, 3), (2, 4), (3, 1), (3, 4), (4, 3)]) + return { + 'ibmq_imaginary': { + 'coupling_map': connections, + 'version': '0.0.0', + 'nq': 6 + } + } + + monkeypatch.setattr(projectq.setups.ibm, "show_devices", mock_show_devices) + with pytest.raises(projectq.setups.ibm.DeviceOfflineError): + projectq.setups.ibm.get_engine_list(device='ibmq_burlington') + with pytest.raises(projectq.setups.ibm.DeviceNotHandledError): + projectq.setups.ibm.get_engine_list(device='ibmq_imaginary') diff --git a/projectq/setups/restrictedgateset.py b/projectq/setups/restrictedgateset.py index f02268fd7..fe0c00ba2 100644 --- a/projectq/setups/restrictedgateset.py +++ b/projectq/setups/restrictedgateset.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Defines a setup to compile to a restricted gate set. @@ -27,10 +26,9 @@ import projectq.libs.math import projectq.setups.decompositions from projectq.cengines import (AutoReplacer, DecompositionRuleSet, - InstructionFilter, LocalOptimizer, - TagRemover) -from projectq.ops import (BasicMathGate, ClassicalInstructionGate, CNOT, - ControlledGate, get_inverse, QFT, Swap) + InstructionFilter, LocalOptimizer, TagRemover) +from projectq.ops import (BasicGate, BasicMathGate, ClassicalInstructionGate, + CNOT, ControlledGate, get_inverse, QFT, Swap) def high_level_gates(eng, cmd): @@ -60,9 +58,14 @@ def one_and_two_qubit_gates(eng, cmd): return False +def default_chooser(cmd, decomposition_list): + return decomposition_list[0] + + def get_engine_list(one_qubit_gates="any", - two_qubit_gates=(CNOT,), - other_gates=()): + two_qubit_gates=(CNOT, ), + other_gates=(), + compiler_chooser=default_chooser): """ Returns an engine list to compile to a restricted gate set. @@ -73,8 +76,8 @@ def get_engine_list(one_qubit_gates="any", even the gate sets which work might not yet be optimized. So make sure to double check and potentially extend the decomposition rules. This implemention currently requires that the one qubit gates must - contain Rz and at least one of {Ry(best), Rx, H} and the two qubit gate - must contain CNOT (recommended) or CZ. + contain Rz and at least one of {Ry(best), Rx, H} and the two qubit + gate must contain CNOT (recommended) or CZ. Note: Classical instructions gates such as e.g. Flush and Measure are @@ -86,23 +89,28 @@ def get_engine_list(one_qubit_gates="any", other_gates=(TimeEvolution,)) Args: - one_qubit_gates: "any" allows any one qubit gate, otherwise provide - a tuple of the allowed gates. If the gates are + one_qubit_gates: "any" allows any one qubit gate, otherwise provide a + tuple of the allowed gates. If the gates are instances of a class (e.g. X), it allows all gates - which are equal to it. If the gate is a class (Rz), it - allows all instances of this class. Default is "any" - two_qubit_gates: "any" allows any two qubit gate, otherwise provide - a tuple of the allowed gates. If the gates are + which are equal to it. If the gate is a class (Rz), + it allows all instances of this class. Default is + "any" + two_qubit_gates: "any" allows any two qubit gate, otherwise provide a + tuple of the allowed gates. If the gates are instances of a class (e.g. CNOT), it allows all gates which are equal to it. If the gate is a class, it allows all instances of this class. Default is (CNOT,). other_gates: A tuple of the allowed gates. If the gates are - instances of a class (e.g. QFT), it allows - all gates which are equal to it. If the gate is a - class, it allows all instances of this class. + instances of a class (e.g. QFT), it allows all gates + which are equal to it. If the gate is a class, it + allows all instances of this class. + compiler_chooser:function selecting the decomposition to use in the + Autoreplacer engine Raises: - TypeError: If input is for the gates is not "any" or a tuple. + TypeError: If input is for the gates is not "any" or a tuple. Also if + element within tuple is not a class or instance of BasicGate + (e.g. CRz which is a shortcut function) Returns: A list of suitable compiler engines. @@ -117,41 +125,56 @@ def get_engine_list(one_qubit_gates="any", if not isinstance(other_gates, tuple): raise TypeError("other_gates parameter must be a tuple.") - rule_set = DecompositionRuleSet(modules=[projectq.libs.math, - projectq.setups.decompositions]) - allowed_gate_classes = [] + rule_set = DecompositionRuleSet( + modules=[projectq.libs.math, projectq.setups.decompositions]) + allowed_gate_classes = [] # n-qubit gates allowed_gate_instances = [] + allowed_gate_classes1 = [] # 1-qubit gates + allowed_gate_instances1 = [] + allowed_gate_classes2 = [] # 2-qubit gates + allowed_gate_instances2 = [] + if one_qubit_gates != "any": for gate in one_qubit_gates: if inspect.isclass(gate): - allowed_gate_classes.append(gate) + allowed_gate_classes1.append(gate) + elif isinstance(gate, BasicGate): + allowed_gate_instances1.append(gate) else: - allowed_gate_instances.append((gate, 0)) + raise TypeError("unsupported one_qubit_gates argument") if two_qubit_gates != "any": for gate in two_qubit_gates: if inspect.isclass(gate): # Controlled gate classes don't yet exists and would require # separate treatment assert not isinstance(gate, ControlledGate) - allowed_gate_classes.append(gate) - else: + allowed_gate_classes2.append(gate) + elif isinstance(gate, BasicGate): if isinstance(gate, ControlledGate): - allowed_gate_instances.append((gate._gate, gate._n)) + allowed_gate_instances2.append((gate._gate, gate._n)) else: - allowed_gate_instances.append((gate, 0)) + allowed_gate_instances2.append((gate, 0)) + else: + raise TypeError("unsupported two_qubit_gates argument") for gate in other_gates: if inspect.isclass(gate): # Controlled gate classes don't yet exists and would require # separate treatment assert not isinstance(gate, ControlledGate) allowed_gate_classes.append(gate) - else: + elif isinstance(gate, BasicGate): if isinstance(gate, ControlledGate): allowed_gate_instances.append((gate._gate, gate._n)) else: allowed_gate_instances.append((gate, 0)) + else: + raise TypeError("unsupported other_gates argument") allowed_gate_classes = tuple(allowed_gate_classes) allowed_gate_instances = tuple(allowed_gate_instances) + allowed_gate_classes1 = tuple(allowed_gate_classes1) + allowed_gate_instances1 = tuple(allowed_gate_instances1) + allowed_gate_classes2 = tuple(allowed_gate_classes2) + allowed_gate_instances2 = tuple(allowed_gate_instances2) def low_level_gates(eng, cmd): all_qubits = [q for qr in cmd.all_qubits for q in qr] @@ -166,19 +189,30 @@ def low_level_gates(eng, cmd): return True elif (cmd.gate, len(cmd.control_qubits)) in allowed_gate_instances: return True - else: - return False - - return [AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(high_level_gates), - LocalOptimizer(5), - AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(one_and_two_qubit_gates), - LocalOptimizer(5), - AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(low_level_gates), - LocalOptimizer(5), - ] + elif (isinstance(cmd.gate, allowed_gate_classes1) + and len(all_qubits) == 1): + return True + elif (isinstance(cmd.gate, allowed_gate_classes2) + and len(all_qubits) == 2): + return True + elif cmd.gate in allowed_gate_instances1 and len(all_qubits) == 1: + return True + elif ((cmd.gate, len(cmd.control_qubits)) in allowed_gate_instances2 + and len(all_qubits) == 2): + return True + return False + + return [ + AutoReplacer(rule_set, compiler_chooser), + TagRemover(), + InstructionFilter(high_level_gates), + LocalOptimizer(5), + AutoReplacer(rule_set, compiler_chooser), + TagRemover(), + InstructionFilter(one_and_two_qubit_gates), + LocalOptimizer(5), + AutoReplacer(rule_set, compiler_chooser), + TagRemover(), + InstructionFilter(low_level_gates), + LocalOptimizer(5), + ] diff --git a/projectq/setups/restrictedgateset_test.py b/projectq/setups/restrictedgateset_test.py index ebe767c1e..fe9754aa7 100644 --- a/projectq/setups/restrictedgateset_test.py +++ b/projectq/setups/restrictedgateset_test.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.setups.restrictedgateset.""" import pytest @@ -20,8 +19,9 @@ from projectq.cengines import DummyEngine from projectq.libs.math import (AddConstant, AddConstantModN, MultiplyByConstantModN) -from projectq.ops import (BasicGate, CNOT, H, Measure, QFT, QubitOperator, Rx, - Rz, Swap, TimeEvolution, Toffoli, X) +from projectq.ops import (BasicGate, CNOT, CRz, H, Measure, QFT, QubitOperator, + Rx, Rz, Swap, TimeEvolution, Toffoli, X) +from projectq.meta import Control import projectq.setups.restrictedgateset as restrictedgateset @@ -48,17 +48,18 @@ def test_restriction(): two_qubit_gates=(CNOT, AddConstant, Swap), other_gates=(Toffoli, AddConstantModN, MultiplyByConstantModN(2, 8))) backend = DummyEngine(save_commands=True) - eng = projectq.MainEngine(backend, engine_list) + eng = projectq.MainEngine(backend, engine_list, verbose=True) qubit1 = eng.allocate_qubit() qubit2 = eng.allocate_qubit() qubit3 = eng.allocate_qubit() eng.flush() CNOT | (qubit1, qubit2) H | qubit1 - Rz(0.2) | qubit1 + with Control(eng, qubit2): + Rz(0.2) | qubit1 Measure | qubit1 - AddConstant(1) | qubit1 + qubit2 - AddConstantModN(1, 9) | qubit1 + qubit2 + qubit3 + AddConstant(1) | (qubit1 + qubit2) + AddConstantModN(1, 9) | (qubit1 + qubit2 + qubit3) Toffoli | (qubit1 + qubit2, qubit3) Swap | (qubit1, qubit2) MultiplyByConstantModN(2, 8) | qubit1 + qubit2 + qubit3 @@ -70,15 +71,15 @@ def test_restriction(): assert backend.received_commands[4].gate == X assert len(backend.received_commands[4].control_qubits) == 1 assert backend.received_commands[5].gate == H - assert backend.received_commands[6].gate == Rz(0.2) - assert backend.received_commands[7].gate == Measure - assert backend.received_commands[8].gate == AddConstant(1) - assert backend.received_commands[9].gate == AddConstantModN(1, 9) - assert backend.received_commands[10].gate == X - assert len(backend.received_commands[10].control_qubits) == 2 - assert backend.received_commands[11].gate == Swap - assert backend.received_commands[12].gate == MultiplyByConstantModN(2, 8) - for cmd in backend.received_commands[13:]: + assert backend.received_commands[6].gate == Rz(0.1) + assert backend.received_commands[10].gate == Measure + assert backend.received_commands[11].gate == AddConstant(1) + assert backend.received_commands[12].gate == AddConstantModN(1, 9) + assert backend.received_commands[13].gate == X + assert len(backend.received_commands[13].control_qubits) == 2 + assert backend.received_commands[14].gate == Swap + assert backend.received_commands[15].gate == MultiplyByConstantModN(2, 8) + for cmd in backend.received_commands[16:]: assert cmd.gate != QFT assert not isinstance(cmd.gate, Rx) assert not isinstance(cmd.gate, MultiplyByConstantModN) @@ -87,8 +88,14 @@ def test_restriction(): def test_wrong_init(): with pytest.raises(TypeError): - engine_list = restrictedgateset.get_engine_list(two_qubit_gates=(CNOT)) + restrictedgateset.get_engine_list(two_qubit_gates=(CNOT)) + with pytest.raises(TypeError): + restrictedgateset.get_engine_list(one_qubit_gates="Any") + with pytest.raises(TypeError): + restrictedgateset.get_engine_list(other_gates="any") + with pytest.raises(TypeError): + restrictedgateset.get_engine_list(one_qubit_gates=(CRz, )) with pytest.raises(TypeError): - engine_list = restrictedgateset.get_engine_list(one_qubit_gates="Any") + restrictedgateset.get_engine_list(two_qubit_gates=(CRz, )) with pytest.raises(TypeError): - engine_list = restrictedgateset.get_engine_list(other_gates="any") + restrictedgateset.get_engine_list(other_gates=(CRz, )) diff --git a/projectq/setups/trapped_ion_decomposer.py b/projectq/setups/trapped_ion_decomposer.py new file mode 100644 index 000000000..f5d19f1c8 --- /dev/null +++ b/projectq/setups/trapped_ion_decomposer.py @@ -0,0 +1,148 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Module uses ideas from "Basic circuit compilation techniques +# for an ion-trap quantum machine" by Dmitri Maslov (2017) at +# https://iopscience.iop.org/article/10.1088/1367-2630/aa5e47 +""" +Apply the restricted gate set setup for trapped ion based quantum computers. + +It provides the `engine_list` for the `MainEngine`, restricting the gate set to +Rx and Ry single qubit gates and the Rxx two qubit gates. + +A decomposition chooser is implemented following the ideas in QUOTE for +reducing the number of Ry gates in the new circuit. + +NOTE: + +Because the decomposition chooser is only called when a gate has to be +decomposed, this reduction will work better when the entire circuit has to be +decomposed. Otherwise, If the circuit has both superconding gates and native +ion trapped gates the decomposed circuit will not be optimal. +""" + +from projectq.setups import restrictedgateset +from projectq.ops import (Rxx, Rx, Ry) +from projectq.meta import get_control_count + +# ------------------chooser_Ry_reducer-------------------# +# If the qubit is not in the prev_Ry_sign dictionary, then no decomposition +# occured +# If the value is: +# -1 then the last gate applied (during a decomposition!) was Ry(-math.pi/2) +# 1 then the last gate applied (during a decomposition!) was Ry(+math.pi/2) +# 0 then the last gate applied (during a decomposition!) was a Rx + +prev_Ry_sign = dict() # Keeps track of most recent Ry sign, i.e. +# whether we had Ry(-pi/2) or Ry(pi/2) +# prev_Ry_sign[qubit_index] should hold -1 or +# +1 + + +def chooser_Ry_reducer(cmd, decomposition_list): + """ + Choose the decomposition so as to maximise Ry cancellations, based on the + previous decomposition used for the given qubit. + + Note: + Classical instructions gates e.g. Flush and Measure are automatically + allowed. + + Returns: + A decomposition object from the decomposition_list. + """ + decomp_rule = dict() + name = 'default' + + for decomp in decomposition_list: + try: + # NB: need to (possibly) raise an exception before setting the + # name variable below + decomposition = decomp.decompose.__name__.split('_') + decomp_rule[decomposition[3]] = decomp + name = decomposition[2] + # 'M' stands for minus, 'P' stands for plus 'N' stands for neutral + # e.g. decomp_rule['M'] will give you the decomposition_rule that + # ends with a Ry(-pi/2) + except IndexError: + pass + + local_prev_Ry_sign = prev_Ry_sign.setdefault(cmd.engine, dict()) + + if name == 'cnot2rxx': + assert get_control_count(cmd) == 1 + ctrl_id = cmd.control_qubits[0].id + + if local_prev_Ry_sign.get(ctrl_id, -1) <= 0: + # If the previous qubit had Ry(-pi/2) choose the decomposition + # that starts with Ry(pi/2) + local_prev_Ry_sign[ctrl_id] = -1 + # Now the prev_Ry_sign is set to -1 since at the end of the + # decomposition we will have a Ry(-pi/2) + return decomp_rule['M'] + + # Previous qubit had Ry(pi/2) choose decomposition that starts + # with Ry(-pi/2) and ends with R(pi/2) + local_prev_Ry_sign[ctrl_id] = 1 + return decomp_rule['P'] + + if name == 'h2rx': + qubit_id = [qb.id for qureg in cmd.qubits for qb in qureg] + assert len(qubit_id) == 1 # this should be a single qubit gate + qubit_id = qubit_id[0] + + if local_prev_Ry_sign.get(qubit_id, 0) == 0: + local_prev_Ry_sign[qubit_id] = 1 + return decomp_rule['M'] + + local_prev_Ry_sign[qubit_id] = 0 + return decomp_rule['N'] + + if name == 'rz2rx': + qubit_id = [qb.id for qureg in cmd.qubits for qb in qureg] + assert len(qubit_id) == 1 # this should be a single qubit gate + qubit_id = qubit_id[0] + + if local_prev_Ry_sign.get(qubit_id, -1) <= 0: + local_prev_Ry_sign[qubit_id] = -1 + return decomp_rule['M'] + + local_prev_Ry_sign[qubit_id] = 1 + return decomp_rule['P'] + + # No decomposition chosen, so use the first decompostion in the list + # like the default function + return decomposition_list[0] + + +def get_engine_list(): + """ + Returns an engine list compiling code into a trapped ion based compiled + circuit code. + + Note: + + - Classical instructions gates such as e.g. Flush and Measure are + automatically allowed. + - The restricted gate set engine does not work with Rxx gates, as + ProjectQ will by default bounce back and forth between Cz gates and Cx + gates. An appropriate decomposition chooser needs to be used! + + Returns: + A list of suitable compiler engines. + """ + return restrictedgateset.get_engine_list( + one_qubit_gates=(Rx, Ry), + two_qubit_gates=(Rxx, ), + compiler_chooser=chooser_Ry_reducer) diff --git a/projectq/setups/trapped_ion_decomposer_test.py b/projectq/setups/trapped_ion_decomposer_test.py new file mode 100644 index 000000000..23b6485c6 --- /dev/null +++ b/projectq/setups/trapped_ion_decomposer_test.py @@ -0,0 +1,150 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.trapped_ion_decomposer.py." + +import projectq +from projectq.ops import (Rx, Ry, Rz, H, X, CNOT, Measure, Rxx, + ClassicalInstructionGate) +from projectq.cengines import (MainEngine, DummyEngine, AutoReplacer, + TagRemover, InstructionFilter, + DecompositionRuleSet, DecompositionRule) +from projectq.meta import get_control_count + +from . import restrictedgateset +from .trapped_ion_decomposer import chooser_Ry_reducer, get_engine_list + + +def filter_gates(eng, cmd): + if isinstance(cmd.gate, ClassicalInstructionGate): + return True + if ((cmd.gate == X and get_control_count(cmd) == 1) or cmd.gate == H + or isinstance(cmd.gate, Rz)): + return False + return True + + +def test_chooser_Ry_reducer_synthetic(): + backend = DummyEngine(save_commands=True) + rule_set = DecompositionRuleSet( + modules=[projectq.libs.math, projectq.setups.decompositions]) + + engine_list = [ + AutoReplacer(rule_set, chooser_Ry_reducer), + TagRemover(), + InstructionFilter(filter_gates), + ] + + eng = MainEngine(backend=backend, engine_list=engine_list) + control = eng.allocate_qubit() + target = eng.allocate_qubit() + CNOT | (control, target) + CNOT | (control, target) + eng.flush() + idx0 = len(backend.received_commands) - 2 + idx1 = len(backend.received_commands) + CNOT | (control, target) + eng.flush() + + assert isinstance(backend.received_commands[idx0].gate, Ry) + assert isinstance(backend.received_commands[idx1].gate, Ry) + assert (backend.received_commands[idx0].gate.get_inverse() == + backend.received_commands[idx1].gate) + + eng = MainEngine(backend=backend, engine_list=engine_list) + control = eng.allocate_qubit() + target = eng.allocate_qubit() + H | target + eng.flush() + idx0 = len(backend.received_commands) - 2 + idx1 = len(backend.received_commands) + H | target + eng.flush() + + assert isinstance(backend.received_commands[idx0].gate, Ry) + assert isinstance(backend.received_commands[idx1].gate, Ry) + assert (backend.received_commands[idx0].gate.get_inverse() == + backend.received_commands[idx1].gate) + + eng = MainEngine(backend=backend, engine_list=engine_list) + control = eng.allocate_qubit() + target = eng.allocate_qubit() + Rz(1.23456) | target + eng.flush() + idx0 = len(backend.received_commands) - 2 + idx1 = len(backend.received_commands) + Rz(1.23456) | target + eng.flush() + + assert isinstance(backend.received_commands[idx0].gate, Ry) + assert isinstance(backend.received_commands[idx1].gate, Ry) + assert (backend.received_commands[idx0].gate.get_inverse() == + backend.received_commands[idx1].gate) + + +def _dummy_h2nothing_A(cmd): + qubit = cmd.qubits[0] + Ry(1.23456) | qubit + + +def test_chooser_Ry_reducer_unsupported_gate(): + backend = DummyEngine(save_commands=True) + rule_set = DecompositionRuleSet( + rules=[DecompositionRule(H.__class__, _dummy_h2nothing_A)]) + + engine_list = [ + AutoReplacer(rule_set, chooser_Ry_reducer), + TagRemover(), + InstructionFilter(filter_gates), + ] + + eng = MainEngine(backend=backend, engine_list=engine_list) + qubit = eng.allocate_qubit() + H | qubit + eng.flush() + + for cmd in backend.received_commands: + print(cmd) + + assert isinstance(backend.received_commands[1].gate, Ry) + + +def test_chooser_Ry_reducer(): + # Without the chooser_Ry_reducer function, i.e. if the restricted gate set + # just picked the first option in each decomposition list, the circuit + # below would be decomposed into 8 single qubit gates and 1 two qubit + # gate. + # + # Including the Allocate, Measure and Flush commands, this would result in + # 13 commands. + # + # Using the chooser_Rx_reducer you get 10 commands, since you now have 4 + # single qubit gates and 1 two qubit gate. + + for engine_list, count in [(restrictedgateset.get_engine_list( + one_qubit_gates=(Rx, Ry), + two_qubit_gates=(Rxx, )), 13), + (get_engine_list(), 11)]: + + backend = DummyEngine(save_commands=True) + eng = projectq.MainEngine(backend, engine_list, verbose=True) + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + H | qubit1 + CNOT | (qubit1, qubit2) + Rz(0.2) | qubit1 + Measure | qubit1 + eng.flush() + + assert len(backend.received_commands) == count diff --git a/requirements.txt b/requirements.txt index 903d45bdc..60d6b013c 100755 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pybind11>=2.2.3 requests scipy networkx +matplotlib>=2.2.3 diff --git a/setup.py b/setup.py index 604f006af..5049a3a06 100755 --- a/setup.py +++ b/setup.py @@ -1,29 +1,61 @@ -from setuptools import setup, Extension, find_packages, Feature -from setuptools.command.build_ext import build_ext -import sys -import os -import setuptools +# Some of the setup.py code is inspired or copied from SQLAlchemy +# SQLAlchemy was created by Michael Bayer. -# This reads the __version__ variable from projectq/_version.py -exec(open('projectq/_version.py').read()) +# Major contributing authors include: -# Readme file as long_description: -long_description = open('README.rst').read() +# - Michael Bayer +# - Jason Kirtland +# - Gaetan de Menten +# - Diana Clarke +# - Michael Trier +# - Philip Jenvey +# - Ants Aasma +# - Paul Johnston +# - Jonathan Ellis +# - Damien Nguyen (ProjectQ) -# Read in requirements.txt -with open('requirements.txt', 'r') as f_requirements: - requirements = f_requirements.readlines() -requirements = [r.strip() for r in requirements] +# Copyright 2005-2020 SQLAlchemy and ProjectQ authors and contributors (see above) + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from __future__ import print_function +from setuptools import setup, Extension, find_packages +from distutils.errors import (CompileError, LinkError, CCompilerError, + DistutilsExecError, DistutilsPlatformError) +from setuptools import Distribution as _Distribution +from setuptools.command.build_ext import build_ext +import sys +import os +import subprocess +import platform + +# ============================================================================== +# Helper functions and classes class get_pybind_include(object): - """Helper class to determine the pybind11 include path + '''Helper class to determine the pybind11 include path The purpose of this class is to postpone importing pybind11 until it is actually installed, so that the ``get_include()`` - method can be invoked. """ - + method can be invoked. ''' def __init__(self, user=False): self.user = user @@ -32,141 +64,409 @@ def __str__(self): return pybind11.get_include(self.user) -cppsim = Feature( - 'C++ Simulator', - standard=True, - ext_modules=[ - Extension( - 'projectq.backends._sim._cppsim', - ['projectq/backends/_sim/_cppsim.cpp'], - include_dirs=[ - # Path to pybind11 headers - get_pybind_include(), - get_pybind_include(user=True) - ], - language='c++' - ), - ], -) - - -def has_flag(compiler, flagname=None): - """ +def important_msgs(*msgs): + print('*' * 75) + for msg in msgs: + print(msg) + print('*' * 75) + + +def status_msgs(*msgs): + print('-' * 75) + for msg in msgs: + print('# INFO: ', msg) + print('-' * 75) + + +def compiler_test(compiler, + flagname=None, + link=False, + include='', + body='', + postargs=None): + ''' Return a boolean indicating whether a flag name is supported on the specified compiler. - """ + ''' import tempfile f = tempfile.NamedTemporaryFile('w', suffix='.cpp', delete=False) - f.write('int main (int argc, char **argv) { return 0; }') + f.write('{}\nint main (int argc, char **argv) {{ {} return 0; }}'.format( + include, body)) f.close() ret = True - try: - if flagname is None: - compiler.compile([f.name]) - else: - compiler.compile([f.name], extra_postargs=[flagname]) - except: - ret = False - os.unlink(f.name) - return ret + if postargs is None: + postargs = [flagname] if flagname is not None else None + elif flagname is not None: + postargs.append(flagname) -def knows_intrinsics(compiler): - """ - Return a boolean indicating whether the compiler can handle intrinsics. - """ - import tempfile - f = tempfile.NamedTemporaryFile('w', suffix='.cpp', delete=False) - f.write('#include \nint main (int argc, char **argv) ' - '{ __m256d neg = _mm256_set1_pd(1.0); }') - f.close() - ret = True try: - compiler.compile([f.name], extra_postargs=['-march=native']) - except setuptools.distutils.errors.CompileError: + exec_name = os.path.join(tempfile.mkdtemp(), 'test') + + if compiler.compiler_type == 'msvc': + olderr = os.dup(sys.stderr.fileno()) + err = open('err.txt', 'w') + os.dup2(err.fileno(), sys.stderr.fileno()) + + obj_file = compiler.compile([f.name], extra_postargs=postargs) + if not os.path.exists(obj_file[0]): + raise RuntimeError('') + if link: + compiler.link_executable(obj_file, + exec_name, + extra_postargs=postargs) + + if compiler.compiler_type == 'msvc': + err.close() + os.dup2(olderr, sys.stderr.fileno()) + with open('err.txt', 'r') as err_file: + if err_file.readlines(): + raise RuntimeError('') + except (CompileError, LinkError, RuntimeError): ret = False os.unlink(f.name) return ret +def _fix_macosx_header_paths(*args): + # Fix path to SDK headers if necessary + _MACOSX_XCODE_REF_PATH = ('/Applications/Xcode.app/Contents/' + + 'Developer/Platforms/MacOSX.platform/' + + 'Developer') + _MACOSX_DEVTOOLS_REF_PATH = '/Library/Developer/CommandLineTools/' + _has_xcode = os.path.exists(_MACOSX_XCODE_REF_PATH) + _has_devtools = os.path.exists(_MACOSX_DEVTOOLS_REF_PATH) + if not _has_xcode and not _has_devtools: + important_msgs('ERROR: Must install either Xcode or ' + + 'CommandLineTools!') + raise BuildFailed() + + def _do_replace(idx, item): + if not _has_xcode and _MACOSX_XCODE_REF_PATH in item: + compiler_args[idx] = item.replace(_MACOSX_XCODE_REF_PATH, + _MACOSX_DEVTOOLS_REF_PATH) + + if not _has_devtools and _MACOSX_DEVTOOLS_REF_PATH in item: + compiler_args[idx] = item.replace(_MACOSX_DEVTOOLS_REF_PATH, + _MACOSX_XCODE_REF_PATH) + + for compiler_args in args: + for idx, item in enumerate(compiler_args): + _do_replace(idx, item) + + +# ------------------------------------------------------------------------------ + + +class BuildFailed(Exception): + def __init__(self): + self.cause = sys.exc_info()[1] # work around py 2/3 different syntax + + +# ------------------------------------------------------------------------------ +# Python build related variable + +cpython = platform.python_implementation() == 'CPython' +ext_errors = (CCompilerError, DistutilsExecError, DistutilsPlatformError) +if sys.platform == 'win32': + # 2.6's distutils.msvc9compiler can raise an IOError when failing to + # find the compiler + ext_errors += (IOError, ) + +# ============================================================================== + +# This reads the __version__ variable from projectq/_version.py +exec(open('projectq/_version.py').read()) + +# Readme file as long_description: +long_description = open('README.rst').read() + +# Read in requirements.txt +with open('requirements.txt', 'r') as f_requirements: + requirements = f_requirements.readlines() +requirements = [r.strip() for r in requirements] + +# ------------------------------------------------------------------------------ +# ProjectQ C++ extensions + +ext_modules = [ + Extension( + 'projectq.backends._sim._cppsim', + ['projectq/backends/_sim/_cppsim.cpp'], + include_dirs=[ + # Path to pybind11 headers + get_pybind_include(), + get_pybind_include(user=True) + ], + language='c++'), +] + +# ============================================================================== + + class BuildExt(build_ext): - """A custom build extension for adding compiler-specific options.""" + '''A custom build extension for adding compiler-specific options.''' c_opts = { 'msvc': ['/EHsc'], 'unix': [], } + def run(self): + try: + build_ext.run(self) + except DistutilsPlatformError: + raise BuildFailed() + def build_extensions(self): + self._configure_compiler() + for ext in self.extensions: + ext.extra_compile_args = self.opts + ext.extra_link_args = self.link_opts + try: + build_ext.build_extensions(self) + except ext_errors: + raise BuildFailed() + except ValueError: + # this can happen on Windows 64 bit, see Python issue 7511 + if "'path'" in str(sys.exc_info()[1]): # works with both py 2/3 + raise BuildFailed() + raise + + def _configure_compiler(self): if sys.platform == 'darwin': - self.c_opts['unix'] += ['-mmacosx-version-min=10.7'] - if has_flag(self.compiler, '-stdlib=libc++'): + _fix_macosx_header_paths(self.compiler.compiler, + self.compiler.compiler_so) + + if compiler_test(self.compiler, '-stdlib=libc++'): self.c_opts['unix'] += ['-stdlib=libc++'] ct = self.compiler.compiler_type - opts = self.c_opts.get(ct, []) + self.opts = self.c_opts.get(ct, []) + self.link_opts = [] - if not has_flag(self.compiler): - self.warning("Something is wrong with your C++ compiler.\n" - "Failed to compile a simple test program!\n") - return + if not compiler_test(self.compiler): + important_msgs( + 'ERROR: something is wrong with your C++ compiler.\n' + 'Failed to compile a simple test program!') + raise BuildFailed() + + # ------------------------------ + + status_msgs('Configuring OpenMP') + self._configure_openmp() + status_msgs('Configuring compiler intrinsics') + self._configure_intrinsics() + status_msgs('Configuring C++ standard') + self._configure_cxx_standard() - openmp = '' - if has_flag(self.compiler, '-fopenmp'): - openmp = '-fopenmp' - elif has_flag(self.compiler, '-qopenmp'): - openmp = '-qopenmp' - if ct == 'msvc': - openmp = '' # supports only OpenMP 2.0 - - if knows_intrinsics(self.compiler): - opts.append('-DINTRIN') - if ct == 'msvc': - opts.append('/arch:AVX') - else: - opts.append('-march=native') - - opts.append(openmp) + # ------------------------------ + # Other compiler tests + + status_msgs('Other compiler tests') if ct == 'unix': - if not has_flag(self.compiler, '-std=c++11'): - self.warning("Compiler needs to have C++11 support!") + if compiler_test(self.compiler, '-fvisibility=hidden'): + self.opts.append('-fvisibility=hidden') + self.opts.append("-DVERSION_INFO=\"{}\"".format( + self.distribution.get_version())) + elif ct == 'msvc': + self.opts.append("/DVERSION_INFO=\\'{}\\'".format( + self.distribution.get_version())) + + status_msgs('Finished configuring compiler!') + + def _configure_openmp(self): + if self.compiler.compiler_type == 'msvc': + return + + kwargs = { + 'link': True, + 'include': '#include ', + 'body': 'int a = omp_get_num_threads(); ++a;' + } + + for flag in ['-openmp', '-fopenmp', '-qopenmp', '/Qopenmp']: + if compiler_test(self.compiler, flag, **kwargs): + self.opts.append(flag) + self.link_opts.append(flag) return - opts.append('-DVERSION_INFO="%s"' - % self.distribution.get_version()) - opts.append('-std=c++11') - if has_flag(self.compiler, '-fvisibility=hidden'): - opts.append('-fvisibility=hidden') - elif ct == 'msvc': - opts.append('/DVERSION_INFO=\\"%s\\"' - % self.distribution.get_version()) - for ext in self.extensions: - ext.extra_compile_args = opts - ext.extra_link_args = [openmp] - try: - build_ext.build_extensions(self) - except setuptools.distutils.errors.CompileError: - self.warning("") - - def warning(self, warning_text): - raise Exception(warning_text + "\nCould not install the C++-Simulator." - "\nProjectQ will default to the (slow) Python " - "simulator.\nUse --without-cppsimulator to skip " - "building the (faster) C++ version of the simulator.") - - -setup( - name='projectq', - version=__version__, - author='ProjectQ', - author_email='info@projectq.ch', - url='http://www.projectq.ch', - description=('ProjectQ - ' - 'An open source software framework for quantum computing'), - long_description=long_description, - features={'cppsimulator': cppsim}, - install_requires=requirements, - cmdclass={'build_ext': BuildExt}, - zip_safe=False, - license='Apache 2', - packages=find_packages() -) + flag = '-fopenmp' + if (sys.platform == 'darwin' and compiler_test(self.compiler, flag)): + try: + llvm_root = subprocess.check_output( + ['brew', '--prefix', 'llvm']).decode('utf-8')[:-1] + compiler_root = subprocess.check_output( + ['which', self.compiler.compiler[0]]).decode('utf-8')[:-1] + + # Only add the flag if the compiler we are using is the one + # from HomeBrew + if llvm_root in compiler_root: + l_arg = '-L{}/lib'.format(llvm_root) + if compiler_test(self.compiler, + flag, + postargs=[l_arg], + **kwargs): + self.opts.append(flag) + self.link_opts.extend((l_arg, flag)) + return + except subprocess.CalledProcessError: + pass + + try: + # Only relevant for MacPorts users with clang-3.7 + port_path = subprocess.check_output(['which', 'port' + ]).decode('utf-8')[:-1] + macports_root = os.path.dirname(os.path.dirname(port_path)) + compiler_root = subprocess.check_output( + ['which', self.compiler.compiler[0]]).decode('utf-8')[:-1] + + # Only add the flag if the compiler we are using is the one + # from MacPorts + if macports_root in compiler_root: + c_arg = '-I{}/include/libomp'.format(macports_root) + l_arg = '-L{}/lib/libomp'.format(macports_root) + + if compiler_test(self.compiler, + flag, + postargs=[c_arg, l_arg], + **kwargs): + self.opts.extend((c_arg, flag)) + self.link_opts.extend((l_arg, flag)) + return + except subprocess.CalledProcessError: + pass + + important_msgs('WARNING: compiler does not support OpenMP!') + + def _configure_intrinsics(self): + for flag in [ + '-march=native', '-mavx2', '/arch:AVX2', '/arch:CORE-AVX2', + '/arch:AVX' + ]: + if compiler_test( + self.compiler, + flagname=flag, + link=False, + include='#include ', + body='__m256d neg = _mm256_set1_pd(1.0); (void)neg;'): + + if sys.platform == 'win32': + self.opts.extend(('/DINTRIN', flag)) + else: + self.opts.extend(('-DINTRIN', flag)) + break + + for flag in ['-ffast-math', '-fast', '/fast', '/fp:precise']: + if compiler_test(self.compiler, flagname=flag): + self.opts.append(flag) + break + + def _configure_cxx_standard(self): + if self.compiler.compiler_type == 'msvc': + return + + cxx_standards = [17, 14, 11] + if sys.version_info[0] < 3: + cxx_standards = [year for year in cxx_standards if year < 17] + + if sys.platform == 'darwin': + _, minor_version, _ = [ + int(i) for i in platform.mac_ver()[0].split('.') + ] + if minor_version < 14: + cxx_standards = [year for year in cxx_standards if year < 17] + + for year in cxx_standards: + flag = '-std=c++{}'.format(year) + if compiler_test(self.compiler, flag): + self.opts.append(flag) + return + flag = '/Qstd=c++{}'.format(year) + if compiler_test(self.compiler, flag): + self.opts.append(flag) + return + + important_msgs('ERROR: compiler needs to have at least C++11 support!') + raise BuildFailed() + + +class Distribution(_Distribution): + def has_ext_modules(self): + # We want to always claim that we have ext_modules. This will be fine + # if we don't actually have them (such as on PyPy) because nothing + # will get built, however we don't want to provide an overally broad + # Wheel package when building a wheel without C support. This will + # ensure that Wheel knows to treat us as if the build output is + # platform specific. + return True + + +# ============================================================================== + + +def run_setup(with_cext): + kwargs = {} + if with_cext: + kwargs['ext_modules'] = ext_modules + else: + kwargs['ext_modules'] = [] + + setup(name='projectq', + version=__version__, + author='ProjectQ', + author_email='info@projectq.ch', + url='http://www.projectq.ch', + project_urls={ + 'Documentation': 'https://projectq.readthedocs.io/en/latest/', + 'Issue Tracker': + 'https://github.com/ProjectQ-Framework/ProjectQ/', + }, + description=( + 'ProjectQ - ' + 'An open source software framework for quantum computing'), + long_description=long_description, + install_requires=requirements, + cmdclass={'build_ext': BuildExt}, + zip_safe=False, + license='Apache 2', + packages=find_packages(), + distclass=Distribution, + **kwargs) + + +# ============================================================================== + +if not cpython: + run_setup(False) + important_msgs( + 'WARNING: C/C++ extensions are not supported on ' + + 'some features are disabled (e.g. C++ simulator).', + 'Plain-Python build succeeded.', + ) +elif os.environ.get('DISABLE_PROJECTQ_CEXT'): + run_setup(False) + important_msgs( + 'DISABLE_PROJECTQ_CEXT is set; ' + + 'not attempting to build C/C++ extensions.', + 'Plain-Python build succeeded.', + ) + +else: + try: + run_setup(True) + except BuildFailed as exc: + important_msgs( + exc.cause, + 'WARNING: Some C/C++ extensions could not be compiled, ' + + 'some features are disabled (e.g. C++ simulator).', + 'Failure information, if any, is above.', + 'Retrying the build without the C/C++ extensions now.', + ) + + run_setup(False) + + important_msgs( + 'WARNING: Some C/C++ extensions could not be compiled, ' + + 'some features are disabled (e.g. C++ simulator).', + 'Plain-Python build succeeded.', + )