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 6ae6ea7d7..904da9b26 100755 --- a/.travis.yml +++ b/.travis.yml @@ -44,6 +44,9 @@ install: - cd qrack && mkdir _build && cd _build && cmake .. && sudo make install && cd ../.. && sudo rm -r qrack && cd vm6502q/ProjectQ - pip$PY install -e . --global-option="--with-qracksimulator" +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 1937f0962..000000000 --- a/docs/projectq.backends.rst +++ /dev/null @@ -1,21 +0,0 @@ -backends -======== - -.. autosummary:: - - projectq.backends.CommandPrinter - projectq.backends.CircuitDrawer - projectq.backends.Simulator - projectq.backends.ClassicalSimulator - projectq.backends.ResourceCounter - projectq.backends.IBMBackend - projectq.backends.QrackSimulator - - -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 6b9ec5936..000000000 --- a/docs/projectq.ops.rst +++ /dev/null @@ -1,67 +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.MatrixGate - 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 - projectq.ops.QPE - projectq.ops.FlipBits - projectq.ops.QAA - - -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 6206c4a95..000000000 --- a/docs/projectq.setups.decompositions.rst +++ /dev/null @@ -1,216 +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.cnot2cz - projectq.setups.decompositions.cnot2rxx - projectq.setups.decompositions.cnu2toffoliandcu - projectq.setups.decompositions.crz2cxandrz - projectq.setups.decompositions.entangle - projectq.setups.decompositions.globalphase - projectq.setups.decompositions.h2rx - projectq.setups.decompositions.ph2r - projectq.setups.decompositions.qft2crandhadamard - projectq.setups.decompositions.qubitop2onequbit - projectq.setups.decompositions.r2rzandph - projectq.setups.decompositions.rx2rz - projectq.setups.decompositions.ry2rz - projectq.setups.decompositions.rz2rx - projectq.setups.decompositions.sqrtswap2cnot - projectq.setups.decompositions.stateprep2cnot - projectq.setups.decompositions.swap2cnot - projectq.setups.decompositions.time_evolution - projectq.setups.decompositions.toffoli2cnotandtgate - projectq.setups.decompositions.uniformlycontrolledr2cnot - projectq.setups.decompositions.phaseestimation - projectq.setups.decompositions.amplitudeamplification - - -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.cnot2cz module ---------------------------------------------- - -.. automodule:: projectq.setups.decompositions.cnot2cz - :members: - :undoc-members: - -projectq.setups.decompositions.cnot2rxx module ---------------------------------------------- - -.. automodule:: projectq.setups.decompositions.cnot2rxx - :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.h2rx 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.qubitop2onequbit module -------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.qubitop2onequbit - :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.rz2rx module -------------------------------------------- - -.. automodule:: projectq.setups.decompositions.rz2rx - :members: - :undoc-members: - -projectq.setups.decompositions.sqrtswap2cnot module ---------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.sqrtswap2cnot - :members: - :undoc-members: - -projectq.setups.decompositions.stateprep2cnot module ----------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.stateprep2cnot - :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: - -projectq.setups.decompositions.uniformlycontrolledr2cnot module ---------------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.uniformlycontrolledr2cnot - :members: - :undoc-members: - -projectq.setups.decompositions.phaseestimation module ---------------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.phaseestimation - :members: - :undoc-members: - -projectq.setups.decompositions.amplitudeamplification module ---------------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.amplitudeamplification - :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 98e7e611a..000000000 --- a/docs/projectq.setups.rst +++ /dev/null @@ -1,101 +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: - -trapped_ion_decomposer ----------------------- -.. automodule:: projectq.setups.trapped_ion_decomposer - :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/projectq/_version.py b/projectq/_version.py index 61a0a8d6a..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.2" +__version__ = "0.5.0" diff --git a/projectq/backends/__init__.py b/projectq/backends/__init__.py index d345de2a5..aa7c3c104 100755 --- a/projectq/backends/__init__.py +++ b/projectq/backends/__init__.py @@ -26,8 +26,8 @@ * an interface to the IBM Quantum Experience chip (and simulator). """ from ._printer import CommandPrinter -from ._circuits import CircuitDrawer -from ._sim import ClassicalSimulator +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 85aee3dac..2562a07dd 100755 --- a/projectq/backends/_circuits/_drawer.py +++ b/projectq/backends/_circuits/_drawer.py @@ -15,8 +15,6 @@ Contains a compiler engine which generates TikZ Latex code describing the circuit. """ -import sys - from builtins import input from projectq.cengines import LastEngineException, BasicEngine @@ -223,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) 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/_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/_ibm/_ibm.py b/projectq/backends/_ibm/_ibm.py index b1899f043..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,11 @@ 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): """ @@ -59,10 +59,8 @@ 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 @@ -76,15 +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 @@ -93,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 @@ -111,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 @@ -129,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 @@ -154,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 " @@ -161,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): """ @@ -198,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 @@ -212,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 @@ -223,68 +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, + 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, + 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 == "": @@ -322,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 d713b17fa..98751bf90 100755 --- a/projectq/backends/_ibm/_ibm_http_client.py +++ b/projectq/backends/_ibm/_ibm_http_client.py @@ -13,83 +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 signal -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 + + 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)) -_api_url = 'https://quantumexperience.ng.bluemix.net/api/' + 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 + + 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, user, password, jobid, num_retries=3000, - interval=1, verbose=False): + +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, num_retries=num_retries, - interval=interval, verbose=verbose) + 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, num_retries=3000, interval=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, - num_retries=num_retries, - interval=interval, verbose=verbose) + res = ibmq_session._get_result(device, + execution_id, + num_retries=num_retries, + interval=interval, + verbose=verbose) if verbose: print("- Done.") return res @@ -102,93 +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, verbose=False): - suffix = 'Jobs/{execution_id}'.format(execution_id=execution_id) - status_url = urljoin(_api_url, 'Backends/{}/queue/status'.format(device)) - - 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): - 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 and qasm['result'] is not None: - 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 verbose and 'lengthQueue' in r_json: - print("Currently there are {} jobs queued for execution " - "on {}." - .format(r_json['lengthQueue'], device)) - - 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)) 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/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/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/_command.py b/projectq/ops/_command.py index 6c320f375..317356c19 100755 --- a/projectq/ops/_command.py +++ b/projectq/ops/_command.py @@ -30,6 +30,7 @@ optimizer to cancel the following two gates .. code-block:: python + Swap | (qubit1, qubit2) Swap | (qubit2, qubit1) @@ -104,6 +105,7 @@ 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) diff --git a/projectq/setups/decompositions/amplitudeamplification_test.py b/projectq/setups/decompositions/amplitudeamplification_test.py index 415dc61cd..6956be44d 100644 --- a/projectq/setups/decompositions/amplitudeamplification_test.py +++ b/projectq/setups/decompositions/amplitudeamplification_test.py @@ -27,6 +27,8 @@ from projectq.ops import QAA from projectq.setups.decompositions import amplitudeamplification as aa +tolerance = 1e-5 + def hache_algorithm(eng, qreg): All(H) | qreg @@ -71,7 +73,7 @@ def test_simple_grover(): # 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)) # (Probability is maximized for floor, not midpoint rounded) + 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) @@ -90,8 +92,7 @@ def test_simple_grover(): eng.flush() - # NOTE: For Qrack, tolerance is much wider, for now - assert total_prob_after == pytest.approx(theoretical_prob, abs=2e-2), ( + assert total_prob_after == pytest.approx(theoretical_prob, abs=tolerance), ( "The obtained probability is less than expected %f vs. %f" % (total_prob_after, theoretical_prob)) @@ -150,7 +151,7 @@ def test_complex_aa(): # 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)) # (Probability is maximized for floor, not midpoint rounded) + 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) @@ -170,7 +171,7 @@ def test_complex_aa(): eng.flush() - assert total_prob_after == pytest.approx(theoretical_prob, abs=2e-2), ( + 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)) diff --git a/projectq/setups/decompositions/phaseestimation_test.py b/projectq/setups/decompositions/phaseestimation_test.py index e1c0857f9..feae36e65 100644 --- a/projectq/setups/decompositions/phaseestimation_test.py +++ b/projectq/setups/decompositions/phaseestimation_test.py @@ -33,17 +33,15 @@ import projectq.setups.decompositions.stateprep2cnot as stateprep2cnot import projectq.setups.decompositions.uniformlycontrolledr2cnot as ucr2cnot - tolerance = 1e-5 -# NOTE FROM QRACK: This set of tests was contributed by a third party, seems suspicious to Dan Strano's hand-trace of the code, and has historically intermittently failed. def test_simple_test_X_eigenvectors(): rule_set = DecompositionRuleSet(modules=[pe, dqft]) eng = MainEngine(backend=Simulator(), engine_list=[AutoReplacer(rule_set), ]) - num_phase = 0 + results = np.array([]) for i in range(100): autovector = eng.allocate_qureg(1) X | autovector @@ -56,15 +54,12 @@ def test_simple_test_X_eigenvectors(): fasebin = ''.join(str(j) for j in fasebinlist) faseint = int(fasebin, 2) phase = faseint / (2. ** (len(ancillas))) - if (phase == pytest.approx(0.5, rel=tolerance, abs=tolerance)): - num_phase = num_phase + 1 + results = np.append(results, phase) All(Measure) | autovector eng.flush() - if num_phase/100. >= 0.35: - assert True - else: - pytest.xfail("Statistics phase calculation are not correct (%f vs. %f)" % (num_phase/100., 0.35)) + 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(): @@ -72,7 +67,7 @@ def test_Ph_eigenvectors(): eng = MainEngine(backend=Simulator(), engine_list=[AutoReplacer(rule_set), ]) - num_phase = 0 + results = np.array([]) for i in range(100): autovector = eng.allocate_qureg(1) theta = cmath.pi*2.*0.125 @@ -84,14 +79,15 @@ def test_Ph_eigenvectors(): fasebin = ''.join(str(j) for j in fasebinlist) faseint = int(fasebin, 2) phase = faseint / (2. ** (len(ancillas))) - if (phase == pytest.approx(0.125, rel=tolerance, abs=tolerance)): - num_phase = num_phase + 1 + results = np.append(results, phase) All(Measure) | autovector eng.flush() + num_phase = (results == 0.125).sum() if num_phase/100. >= 0.35: assert True else: + #Qrack occassionally produces a lower number pytest.xfail("Statistics phase calculation are not correct (%f vs. %f)" % (num_phase/100., 0.35)) @@ -122,10 +118,7 @@ def test_2qubitsPh_andfunction_eigenvectors(): eng.flush() num_phase = (results == 0.125).sum() - if num_phase/100. >= 0.34: - assert True - else: - pytest.xfail("Statistics phase calculation are not correct (%f vs. %f)" % (num_phase/100., 0.34)) + 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(): @@ -166,7 +159,7 @@ def test_X_no_eigenvectors(): 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 = 2e-1, rel = 2e-1), "Statistics on |+> probability are not correct (%f vs. %f)" % (plus_probability, 1./4.) + 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(): diff --git a/projectq/setups/decompositions/rz2rx_test.py b/projectq/setups/decompositions/rz2rx_test.py index bd6ede3dd..f7b5fb4fc 100644 --- a/projectq/setups/decompositions/rz2rx_test.py +++ b/projectq/setups/decompositions/rz2rx_test.py @@ -27,7 +27,6 @@ from . import rz2rx - tolerance = 1e-6 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/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 ab59cc5d9..383d79ac1 100755 --- a/setup.py +++ b/setup.py @@ -1,29 +1,60 @@ -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: -class get_pybind_include(object): - """Helper class to determine the pybind11 include path +# 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 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,8 +63,7 @@ def __str__(self): return pybind11.get_include(self.user) class get_opencl_library(object): - """Helper class to determine if OpenCL is present""" - + '''Helper class to determine if OpenCL is present''' def __init__(self): pass @@ -47,161 +77,421 @@ def __str__(self): return 'm' -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++' - ), - ], -) - -qracksim = Feature( - 'Qrack Simulator', - standard=False, - ext_modules=[ - Extension( - 'projectq.backends._qracksim._qracksim', - ['projectq/backends/_qracksim/_qracksim.cpp'], - include_dirs=[ - # Path to pybind11 headers - get_pybind_include(), - get_pybind_include(user=True), - ], - libraries=['qrack', str(get_opencl_library())], - language='c++' - ), - ], -) +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 has_flag(compiler, flagname=None): - """ + +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++'), + Extension( + 'projectq.backends._qracksim._qracksim', + ['projectq/backends/_qracksim/_qracksim.cpp'], + include_dirs=[ + # Path to pybind11 headers + get_pybind_include(), + get_pybind_include(user=True), + ], + libraries=['qrack', str(get_opencl_library())], + 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() + + # ------------------------------ - 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') - #Not compatible with Qrack at "dirty qubit" tolerances for deallocation: - #opts.append('-ffast-math') - - opts.append(openmp) + status_msgs('Configuring OpenMP') + self._configure_openmp() + status_msgs('Configuring compiler intrinsics') + self._configure_intrinsics() + status_msgs('Configuring C++ standard') + self._configure_cxx_standard() + + # ------------------------------ + # 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++ simulators." - "\nProjectQ will default to the (slow) Python " - "simulator.\nUse --without-cppsimulator to skip " - "building the (faster) C++ simulators.") - - -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, 'qracksimulator': qracksim}, - 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 + + #Not compatible with Qrack at "dirty qubit" tolerances for deallocation: + #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.', + )