diff --git a/.gitignore b/.gitignore index c3957d2e0..ca0430898 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,179 @@ -# 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/doxygen + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# ============================================================================== +# Prerequisites +*.d + +# C++ +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# ============================================================================== + +# Windows artifacts +thumbs.db + +# Mac OSX artifacts +*.DS_Store \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index fbe009436..47f727e2a 100755 --- a/.travis.yml +++ b/.travis.yml @@ -42,6 +42,9 @@ install: - if [ "${PYTHON:0:1}" = "3" ]; then pip$PY install dormouse; fi - pip$PY install -e . +before_script: + - "echo 'backend: Agg' > matplotlibrc" + # command to run tests script: export OMP_NUM_THREADS=1 && pytest projectq --cov projectq diff --git a/docs/conf.py b/docs/conf.py index 971cefd8d..ef791d9ae 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,124 @@ 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 + +for desc in descriptions: + fname = os.path.join(os.path.dirname(os.path.abspath('__file__')), + '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..afb18ba18 --- /dev/null +++ b/docs/package_description.py @@ -0,0 +1,167 @@ +import inspect +import sys +import os + + +class PackageDescription(object): + package_list = [] + + def __init__(self, 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 = name + self.desc = desc + if name not in PackageDescription.package_list: + PackageDescription.package_list.append(name) + + self.module = sys.modules['projectq.{}'.format(self.name)] + self.module_special_members = module_special_members + + self.submodule_special_members = module_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 module_root in obj.__file__) + if name[0] != '_'] + + self.subpackages = [] + self.submodules = [] + for name, obj in sub: + if '{}.{}'.format(self.name, + name) in PackageDescription.package_list: + self.subpackages.append((name, obj)) + else: + self.submodules.append((name, obj)) + + self.subpackages.sort(key=lambda x: x[0].lower()) + self.submodules.sort(key=lambda x: x[0].lower()) + + self.members = [(name, obj) + for name, obj in inspect.getmembers( + self.module, + lambda obj: (inspect.isclass(obj) + or inspect.isfunction(obj) + or isinstance(obj, (int, + float, + tuple, + list, + dict, + set, + frozenset, + str)))) + if name[0] != '_'] + self.members.sort(key=lambda x: x[0].lower()) + + def get_ReST(self): + new_lines = [] + new_lines.append(self.name) + new_lines.append('=' * len(self.name)) + new_lines.append('') + + if self.desc: + new_lines.append(self.desc.strip()) + new_lines.append('') + + submodule_has_index = False + + if self.subpackages: + new_lines.append('Subpackages') + new_lines.append('-' * len(new_lines[-1])) + new_lines.append('') + new_lines.append('.. toctree::') + new_lines.append(' :maxdepth: 1') + new_lines.append('') + for name, _ in self.subpackages: + new_lines.append(' projectq.{}.{}'.format(self.name, name)) + new_lines.append('') + else: + submodule_has_index = True + new_lines.append('.. autosummary::') + new_lines.append('') + if self.submodules: + for name, _ in self.submodules: + new_lines.append('\tprojectq.{}.{}'.format(self.name, + name)) + new_lines.append('') + if self.members: + for name, _ in self.members: + new_lines.append('\tprojectq.{}.{}'.format(self.name, + name)) + new_lines.append('') + + if self.submodules: + new_lines.append('Submodules') + new_lines.append('-' * len(new_lines[-1])) + new_lines.append('') + if self.submodules_desc: + new_lines.append(self.submodules_desc.strip()) + new_lines.append('') + + if not submodule_has_index: + new_lines.append('.. autosummary::') + new_lines.append('') + for name, _ in self.submodules: + new_lines.append(' projectq.{}.{}'.format(self.name, + name)) + new_lines.append('') + + for name, _ in self.submodules: + new_lines.append(name) + new_lines.append('^' * len(new_lines[-1])) + new_lines.append('') + new_lines.append('.. automodule:: projectq.{}.{}'.format( + self.name, name)) + new_lines.append(' :members:') + if self.submodule_special_members: + new_lines.append(' :special-members: {}'.format( + self.submodule_special_members)) + new_lines.append(' :undoc-members:') + new_lines.append('') + + new_lines.append('Module contents') + new_lines.append('-' * len(new_lines[-1])) + new_lines.append('') + new_lines.append('.. automodule:: projectq.{}'.format(self.name)) + new_lines.append(' :members:') + new_lines.append(' :undoc-members:') + new_lines.append(' :special-members: {}'.format( + self.module_special_members)) + new_lines.append(' :imported-members:') + new_lines.append('') + + if self.helper_submodules: + new_lines.append('Helper sub-modules') + new_lines.append('-' * len(new_lines[-1])) + new_lines.append('') + for title, name, params in self.helper_submodules: + new_lines.append(title) + new_lines.append('^' * len(title)) + new_lines.append('') + new_lines.append('.. automodule:: projectq.{}.{}'.format( + self.name, name)) + for param in params: + new_lines.append(' {}'.format(param)) + new_lines.append('') + + assert not new_lines[-1] + return new_lines[:-1] diff --git a/docs/projectq.backends.rst b/docs/projectq.backends.rst deleted file mode 100755 index 621f7ce86..000000000 --- a/docs/projectq.backends.rst +++ /dev/null @@ -1,20 +0,0 @@ -backends -======== - -.. autosummary:: - - projectq.backends.CommandPrinter - projectq.backends.CircuitDrawer - projectq.backends.Simulator - projectq.backends.ClassicalSimulator - projectq.backends.ResourceCounter - projectq.backends.IBMBackend - - -Module contents ---------------- - -.. automodule:: projectq.backends - :members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.cengines.rst b/docs/projectq.cengines.rst deleted file mode 100755 index 5a3c963a6..000000000 --- a/docs/projectq.cengines.rst +++ /dev/null @@ -1,33 +0,0 @@ -cengines -======== - -The ProjectQ compiler engines package. - -.. autosummary:: - projectq.cengines.AutoReplacer - projectq.cengines.BasicEngine - projectq.cengines.BasicMapper - projectq.cengines.CommandModifier - projectq.cengines.CompareEngine - projectq.cengines.DecompositionRule - projectq.cengines.DecompositionRuleSet - projectq.cengines.DummyEngine - projectq.cengines.ForwarderEngine - projectq.cengines.GridMapper - projectq.cengines.InstructionFilter - projectq.cengines.IBM5QubitMapper - projectq.cengines.LinearMapper - projectq.cengines.LocalOptimizer - projectq.cengines.ManualMapper - projectq.cengines.MainEngine - projectq.cengines.SwapAndCNOTFlipper - projectq.cengines.TagRemover - - -Module contents ---------------- - -.. automodule:: projectq.cengines - :members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.libs.math.rst b/docs/projectq.libs.math.rst deleted file mode 100755 index 1567978b5..000000000 --- a/docs/projectq.libs.math.rst +++ /dev/null @@ -1,21 +0,0 @@ -math -==== - -A tiny math library which will be extended thoughout the next weeks. Right now, it only contains the math functions necessary to run Beauregard's implementation of Shor's algorithm. - -.. autosummary:: - - projectq.libs.math.all_defined_decomposition_rules - projectq.libs.math.AddConstant - projectq.libs.math.SubConstant - projectq.libs.math.AddConstantModN - projectq.libs.math.SubConstantModN - projectq.libs.math.MultiplyByConstantModN - -Module contents ---------------- - -.. automodule:: projectq.libs.math - :members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.libs.revkit.rst b/docs/projectq.libs.revkit.rst deleted file mode 100644 index 90a2dbb18..000000000 --- a/docs/projectq.libs.revkit.rst +++ /dev/null @@ -1,34 +0,0 @@ -revkit -====== - -This library integrates `RevKit `_ into -ProjectQ to allow some automatic synthesis routines for reversible logic. The -library adds the following operations that can be used to construct quantum -circuits: - -- :class:`~projectq.libs.revkit.ControlFunctionOracle`: Synthesizes a reversible circuit from Boolean control function -- :class:`~projectq.libs.revkit.PermutationOracle`: Synthesizes a reversible circuit for a permutation -- :class:`~projectq.libs.revkit.PhaseOracle`: Synthesizes phase circuit from an arbitrary Boolean function - -RevKit can be installed from PyPi with `pip install revkit`. - -.. note:: - - The RevKit Python module must be installed in order to use this ProjectQ library. - - There exist precompiled binaries in PyPi, as well as a source distribution. - Note that a C++ compiler with C++17 support is required to build the RevKit - python module from source. Examples for compatible compilers are Clang - 6.0, GCC 7.3, and GCC 8.1. - -The integration of RevKit into ProjectQ and other quantum programming languages is described in the paper - - * Mathias Soeken, Thomas Haener, and Martin Roetteler "Programming Quantum Computers Using Design Automation," in: Design Automation and Test in Europe (2018) [`arXiv:1803.01022 `_] - -Module contents ---------------- - -.. automodule:: projectq.libs.revkit - :members: - :special-members: __init__,__or__ - :imported-members: diff --git a/docs/projectq.libs.rst b/docs/projectq.libs.rst deleted file mode 100755 index 9f2c8cd4b..000000000 --- a/docs/projectq.libs.rst +++ /dev/null @@ -1,20 +0,0 @@ -libs -==== - -The library collection of ProjectQ which, for now, consists of a tiny math library and an interface library to RevKit. Soon, more libraries will be added. - -Subpackages ------------ - -.. toctree:: - - projectq.libs.math - projectq.libs.revkit - -Module contents ---------------- - -.. automodule:: projectq.libs - :members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.meta.rst b/docs/projectq.meta.rst deleted file mode 100755 index 14c3d9eea..000000000 --- a/docs/projectq.meta.rst +++ /dev/null @@ -1,32 +0,0 @@ -meta -==== - -Contains meta statements which allow more optimal code while making it easier for users to write their code. -Examples are `with Compute`, followed by an automatic uncompute or `with Control`, which allows the user to condition an entire code block upon the state of a qubit. - - -.. autosummary:: - - projectq.meta.DirtyQubitTag - projectq.meta.LogicalQubitIDTag - projectq.meta.LoopTag - projectq.meta.Loop - projectq.meta.Compute - projectq.meta.Uncompute - projectq.meta.CustomUncompute - projectq.meta.ComputeTag - projectq.meta.UncomputeTag - projectq.meta.Control - projectq.meta.get_control_count - projectq.meta.Dagger - projectq.meta.insert_engine - projectq.meta.drop_engine_after - -Module contents ---------------- - -.. automodule:: projectq.meta - :members: - :undoc-members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.ops.rst b/docs/projectq.ops.rst deleted file mode 100755 index 8fbe2e287..000000000 --- a/docs/projectq.ops.rst +++ /dev/null @@ -1,65 +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.FlipBits - - -Module contents ---------------- - -.. automodule:: projectq.ops - :members: - :special-members: __init__,__or__ - :imported-members: diff --git a/docs/projectq.setups.decompositions.rst b/docs/projectq.setups.decompositions.rst deleted file mode 100755 index 883395db6..000000000 --- a/docs/projectq.setups.decompositions.rst +++ /dev/null @@ -1,182 +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.cnu2toffoliandcu - projectq.setups.decompositions.crz2cxandrz - projectq.setups.decompositions.entangle - projectq.setups.decompositions.globalphase - 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.sqrtswap2cnot - projectq.setups.decompositions.stateprep2cnot - projectq.setups.decompositions.swap2cnot - projectq.setups.decompositions.time_evolution - projectq.setups.decompositions.toffoli2cnotandtgate - projectq.setups.decompositions.uniformlycontrolledr2cnot - - -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.cnu2toffoliandcu module ------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.cnu2toffoliandcu - :members: - :undoc-members: - -projectq.setups.decompositions.crz2cxandrz module -------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.crz2cxandrz - :members: - :undoc-members: - -projectq.setups.decompositions.entangle module ----------------------------------------------- - -.. automodule:: projectq.setups.decompositions.entangle - :members: - :undoc-members: - -projectq.setups.decompositions.globalphase module -------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.globalphase - :members: - :undoc-members: - -projectq.setups.decompositions.ph2r module ------------------------------------------- - -.. automodule:: projectq.setups.decompositions.ph2r - :members: - :undoc-members: - -projectq.setups.decompositions.qft2crandhadamard module -------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.qft2crandhadamard - :members: - :undoc-members: - -projectq.setups.decompositions.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.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: - - -Module contents ---------------- - -.. automodule:: projectq.setups.decompositions - :members: - :undoc-members: - :imported-members: diff --git a/docs/projectq.setups.rst b/docs/projectq.setups.rst deleted file mode 100755 index 058469f07..000000000 --- a/docs/projectq.setups.rst +++ /dev/null @@ -1,94 +0,0 @@ -setups -====== - -The setups package contains a collection of setups which can be loaded by the `MainEngine`. Each setup contains a `get_engine_list` function which returns a list of compiler engines: - -Example: - .. code-block:: python - - import projectq.setups.ibm as ibm_setup - from projectq import MainEngine - eng = MainEngine(engine_list=ibm_setup.get_engine_list()) - # eng uses the default Simulator backend - -The subpackage decompositions contains all the individual decomposition rules -which can be given to, e.g., an `AutoReplacer`. - - -Subpackages ------------ - -.. toctree:: - :maxdepth: 1 - - projectq.setups.decompositions - -Submodules ----------- - -Each of the submodules contains a setup which can be used to specify the -`engine_list` used by the `MainEngine` : - -.. autosummary:: - - projectq.setups.default - projectq.setups.grid - projectq.setups.ibm - projectq.setups.ibm16 - projectq.setups.linear - projectq.setups.restrictedgateset - -default -------- - -.. automodule:: projectq.setups.default - :members: - :special-members: __init__ - :undoc-members: - -grid ----- - -.. automodule:: projectq.setups.grid - :members: - :special-members: __init__ - :undoc-members: - -ibm ---- - -.. automodule:: projectq.setups.ibm - :members: - :special-members: __init__ - :undoc-members: - -ibm16 ------ - -.. automodule:: projectq.setups.ibm16 - :members: - :special-members: __init__ - :undoc-members: - -linear ------- - -.. automodule:: projectq.setups.linear - :members: - :special-members: __init__ - :undoc-members: - -restrictedgateset ------------------ - -.. automodule:: projectq.setups.restrictedgateset - :members: - :special-members: __init__ - :undoc-members: - -Module contents ---------------- - -.. automodule:: projectq.setups - :members: - :special-members: __init__ diff --git a/docs/projectq.types.rst b/docs/projectq.types.rst deleted file mode 100755 index 4f26edc9d..000000000 --- a/docs/projectq.types.rst +++ /dev/null @@ -1,18 +0,0 @@ -types -===== - -The types package contains quantum types such as Qubit, Qureg, and WeakQubitRef. With further development of the math library, also quantum integers, quantum fixed point numbers etc. will be added. - -.. autosummary:: - projectq.types.BasicQubit - projectq.types.Qubit - projectq.types.Qureg - projectq.types.WeakQubitRef - -Module contents ---------------- - -.. automodule:: projectq.types - :members: - :special-members: - :imported-members: diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 289af1aef..cec2e75e7 100755 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -25,13 +25,9 @@ or, alternatively, `clone/download `_ thi ProjectQ comes with a high-performance quantum simulator written in C++. Please see the detailed OS specific installation instructions below to make sure that you are installing the fastest version. .. note:: - The setup will try to build a C++-Simulator, which is much faster than the Python implementation. If it fails, you may use the `--without-cppsimulator` parameter, i.e., - - .. code-block:: bash - - python -m pip install --user --global-option=--without-cppsimulator . - - and the framework will use the **slow Python simulator instead**. Note that this only works if the installation has been tried once without the `--without-cppsimulator` parameter and hence all requirements are now installed. See the instructions below if you want to run larger simulations. The Python simulator works perfectly fine for the small examples (e.g., running Shor's algorithm for factoring 15 or 21). + The setup will try to build a C++-Simulator, which is much faster than the Python implementation. If the C++ compilation were to fail, the setup will install a pure Python implementation of the simulator instead. The Python simulator should work fine for small examples (e.g., running Shor's algorithm for factoring 15 or 21). + + If you want to skip the installation of the C++-Simulator altogether, you can define the ``DISABLE_PROJECTQ_CEXT`` environment variable to avoid any compilation steps. .. note:: If building the C++-Simulator does not work out of the box, consider specifying a different compiler. For example: @@ -40,13 +36,13 @@ ProjectQ comes with a high-performance quantum simulator written in C++. Please env CC=g++-5 python -m pip install --user projectq - Please note that the compiler you specify must support **C++11**! + Please note that the compiler you specify must support at leaste **C++11**! .. note:: Please use pip version v6.1.0 or higher as this ensures that dependencies are installed in the `correct order `_. .. note:: - ProjectQ should be installed on each computer individually as the C++ simulator compilation creates binaries which are optimized for the specific hardware on which it is being installed (potentially using our AVX version and `-march=native`). Therefore, sharing the same ProjectQ installation across different hardware can cause problems. + ProjectQ should be installed on each computer individually as the C++ simulator compilation creates binaries which are optimized for the specific hardware on which it is being installed (potentially using our AVX version and `-march=native`). Therefore, sharing the same ProjectQ installation across different hardware may cause some problems. Detailed instructions and OS-specific hints @@ -70,38 +66,75 @@ Detailed instructions and OS-specific hints .. code-block:: bash - sudo pip3 install --user projectq + sudo python3 -m pip install --user projectq + + all dependencies (such as numpy and pybind11) should be installed automatically. + + +**ArchLinux/Manjaro**: + + Make sure that you have a C/C++ compiler installed: + + .. code-block:: bash + + sudo pacman -Syu gcc + + You only need to install Python (and the package manager). For version 3, run + + .. code-block:: bash + + sudo pacman -Syu python python-pip + + When you then run + + .. code-block:: bash + + sudo python3 -m pip install --user projectq all dependencies (such as numpy and pybind11) should be installed automatically. **Windows**: - It is easiest to install a pre-compiled version of Python, including numpy and many more useful packages. One way to do so is using, e.g., the Python3.5 installers from `python.org `_ or `ANACONDA `_. Installing ProjectQ right away will succeed for the (slow) Python simulator (i.e., with the `--without-cppsimulator` flag). For a compiled version of the simulator, install the Visual C++ Build Tools and the Microsoft Windows SDK prior to doing a pip install. The built simulator will not support multi-threading due to the limited OpenMP support of msvc. + It is easiest to install a pre-compiled version of Python, including numpy and many more useful packages. One way to do so is using, e.g., the Python 3.7 installers from `python.org `_ or `ANACONDA `_. Installing ProjectQ right away will succeed for the (slow) Python simulator. For a compiled version of the simulator, install the Visual C++ Build Tools and the Microsoft Windows SDK prior to doing a pip install. The built simulator will not support multi-threading due to the limited OpenMP support of the Visual Studio compiler. + + If the Python executable is added to your PATH (option normally suggested at the end of the Python installation procedure), you can then open a cmdline window (WIN + R, type "cmd" and click *OK*) and enter the following in order to install ProjectQ: + + .. code-block:: batch + + python -m pip install --user projectq + Should you want to run multi-threaded simulations, you can install a compiler which supports newer OpenMP versions, such as MinGW GCC and then manually build the C++ simulator with OpenMP enabled. **macOS**: - These are the steps to install ProjectQ on a new Mac: + Similarly to the other platforms, installing ProjectQ without the C++ simulator is really easy: - In order to install the fast C++ simulator, we require that your system has a C++ compiler (see option 3 below on how to only install the slower Python simulator via the `--without-cppsimulator` parameter) + .. code-block:: bash - Below you will find two options to install the fast C++ simulator. The first one is the easiest and requires only the standard compiler which Apple distributes with XCode. The second option uses macports to install the simulator with additional support for multi-threading by using OpenMP, which makes it slightly faster. We show how to install the required C++ compiler (clang) which supports OpenMP and additionally, we show how to install a newer python version. + python3 -m pip install --user projectq -.. note:: - Depending on your system you might need to use `sudo` for the installation. + + In order to install the fast C++ simulator, we require that a C++ compiler is installed on your system. There are essentially three options you can choose from: + + 1. Using the compiler provided by Apple through the XCode command line tools. + 2. Using Homebrew + 3. Using MacPorts + + For both options 2 and 3, you will be required to first install the XCode command line tools -1. Installation using XCode and the default python: - Install XCode by opening a terminal and running the following command: + **Apple XCode command line tool** + + Install the XCode command line tools by opening a terminal window and running the following command: .. code-block:: bash xcode-select --install - - Next, you will need to install Python and pip. See option 2 for information on how to install a newer python version with macports. Here, we are using the standard python which is preinstalled with macOS. Pip can be installed by: + + Next, you will need to install Python and pip. See options 2 and 3 for information on how to install a newer python version with either Homebrew or MacPorts. Here, we are using the standard python which is preinstalled with macOS. Pip can be installed by: .. code-block:: bash @@ -111,56 +144,64 @@ Detailed instructions and OS-specific hints .. code-block:: bash - python -m pip install --user projectq + python3 -m pip install --user projectq + Note that the compiler provided by Apple is currently not able to compile ProjectQ's multi-threaded code. -2. Installation using macports: + **Homebrew** - Either use the standard python and install pip as shown in option 1 or better use macports to install a newer python version, e.g., Python 3.5 and the corresponding pip. Visit `macports.org `_ and install the latest version (afterwards open a new terminal). Then, use macports to install Python 3.5 by + First install the XCode command line tools. Then install Homebrew with the following command: .. code-block:: bash - sudo port install python35 + /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" - It might show a warning that if you intend to use python from the terminal, you should also install + Then proceed to install Python as well as a C/C++ compiler (note: gcc installed via Homebrew may lead to some issues): .. code-block:: bash - sudo port install py35-readline - - Install pip by + brew install python llvm + + You should now be able to install ProjectQ with the C++ simulator using the following command: .. code-block:: bash - sudo port install py35-pip + env P=/usr/local/opt/llvm/bin CC=$P/clang CXX=$P/clang++ python3 -m pip install --user projectq + + + **MacPorts** + + Visit `macports.org `_ and install the latest version that corresponds to your operating system's version. Afterwards, open a new terminal window. - Next, we can install ProjectQ with the high performance simulator written in C++. First, we will need to install a suitable compiler with support for **C++11**, OpenMP, and instrinsics. The best option is to install clang 3.9 also using macports (note: gcc installed via macports does not work) + Then, use macports to install Python 3.7 by entering the following command .. code-block:: bash - sudo port install clang-3.9 + sudo port install python37 - ProjectQ is now installed by: + It might show a warning that if you intend to use python from the terminal. In this case, you should also install .. code-block:: bash - env CC=clang-mp-3.9 env CXX=clang++-mp-3.9 python3.5 -m pip install --user projectq + sudo port install py37-gnureadline + + Install pip by -3. Installation with only the slow Python simulator: + .. code-block:: bash - While this simulator works fine for small examples, it is suggested to install the high performance simulator written in C++. + sudo port install py37-pip - If you just want to install ProjectQ with the (slow) Python simulator and no compiler, then first try to install ProjectQ with the default compiler + Next, we can install ProjectQ with the high performance simulator written in C++. First, we will need to install a suitable compiler with support for **C++11**, OpenMP, and instrinsics. The best option is to install clang 9.0 also using macports (note: gcc installed via macports does not work). .. code-block:: bash - python -m pip install --user projectq + sudo port install clang-9.0 - which most likely will fail. Then, try again with the flag ``--without-cppsimulator``: + ProjectQ is now installed by: .. code-block:: bash - python -m pip install --user --global-option=--without-cppsimulator projectq + env CC=clang-mp-9.0 env CXX=clang++-mp-9.0 /opt/local/bin/python3.7 -m pip install --user projectq The ProjectQ syntax diff --git a/examples/ibm.py b/examples/ibm.py index 05e042230..11a81a832 100755 --- a/examples/ibm.py +++ b/examples/ibm.py @@ -2,9 +2,10 @@ from projectq.backends import IBMBackend from projectq.ops import Measure, Entangle, All from projectq import MainEngine +import getpass -def run_entangle(eng, num_qubits=5): +def run_entangle(eng, num_qubits=3): """ Runs an entangling operation on the provided compiler engine. @@ -37,9 +38,20 @@ def run_entangle(eng, num_qubits=5): if __name__ == "__main__": + #devices commonly available : + #ibmq_16_melbourne (15 qubit) + #ibmq_essex (5 qubit) + #ibmq_qasm_simulator (32 qubits) + device = None #replace by the IBM device name you want to use + token = None #replace by the token given by IBMQ + if token is None: + token = getpass.getpass(prompt='IBM Q token > ') + if device is None: + token = getpass.getpass(prompt='IBM device > ') # create main compiler engine for the IBM back-end - eng = MainEngine(IBMBackend(use_hardware=True, num_runs=1024, - verbose=False, device='ibmqx4'), - engine_list=projectq.setups.ibm.get_engine_list()) + eng = MainEngine(IBMBackend(use_hardware=True, token=token, num_runs=1024, + verbose=False, device=device), + engine_list=projectq.setups.ibm.get_engine_list( + token=token, device=device)) # run the circuit and print the result print(run_entangle(eng)) diff --git a/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 6a3319779..4813a52b4 100755 --- a/projectq/backends/__init__.py +++ b/projectq/backends/__init__.py @@ -26,7 +26,7 @@ * an interface to the IBM Quantum Experience chip (and simulator). """ from ._printer import CommandPrinter -from ._circuits import CircuitDrawer +from ._circuits import CircuitDrawer, CircuitDrawerMatplotlib from ._sim import Simulator, ClassicalSimulator from ._resource import ResourceCounter from ._ibm import IBMBackend diff --git a/projectq/backends/_circuits/__init__.py b/projectq/backends/_circuits/__init__.py index 1f22faec4..be22d24d2 100755 --- a/projectq/backends/_circuits/__init__.py +++ b/projectq/backends/_circuits/__init__.py @@ -13,4 +13,8 @@ # limitations under the License. from ._to_latex import to_latex +from ._plot import to_draw + from ._drawer import CircuitDrawer +from ._drawer_matplotlib import CircuitDrawerMatplotlib + diff --git a/projectq/backends/_circuits/_drawer.py b/projectq/backends/_circuits/_drawer.py index 269f592a2..2562a07dd 100755 --- a/projectq/backends/_circuits/_drawer.py +++ b/projectq/backends/_circuits/_drawer.py @@ -11,13 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Contains a compiler engine which generates TikZ Latex code describing the circuit. """ -import sys - from builtins import input from projectq.cengines import LastEngineException, BasicEngine @@ -42,9 +39,9 @@ def __init__(self, gate, lines, ctrl_lines): self.id = -1 def __eq__(self, other): - return (self.gate == other.gate and self.lines == other.lines and - self.ctrl_lines == other.ctrl_lines and - self.id == other.id) + return (self.gate == other.gate and self.lines == other.lines + and self.ctrl_lines == other.ctrl_lines + and self.id == other.id) def __ne__(self, other): return not self.__eq__(other) @@ -153,6 +150,9 @@ def __init__(self, accept_input=False, default_measure=0): self._free_lines = [] self._map = dict() + # Order in which qubit lines are drawn + self._drawing_order = [] + def is_available(self, cmd): """ Specialized implementation of is_available: Returns True if the @@ -190,7 +190,7 @@ def set_qubit_locations(self, id_to_loc): raise RuntimeError("set_qubit_locations() has to be called before" " applying gates!") - for k in range(min(id_to_loc), max(id_to_loc)+1): + for k in range(min(id_to_loc), max(id_to_loc) + 1): if k not in id_to_loc: raise RuntimeError("set_qubit_locations(): Invalid id_to_loc " "mapping provided. All ids in the provided" @@ -221,12 +221,13 @@ def _print_cmd(self, cmd): self._free_lines.append(qubit_id) if self.is_last_engine and cmd.gate == Measure: - assert(get_control_count(cmd) == 0) + assert get_control_count(cmd) == 0 + for qureg in cmd.qubits: for qubit in qureg: if self._accept_input: m = None - while m != '0' and m != '1' and m != 1 and m != 0: + while m not in ('0', '1', 1, 0): prompt = ("Input measurement result (0 or 1) for " "qubit " + str(qubit) + ": ") m = input(prompt) @@ -244,7 +245,9 @@ def _print_cmd(self, cmd): for l in all_lines: self._qubit_lines[l].append(item) - def get_latex(self): + self._drawing_order.append(all_lines[0]) + + def get_latex(self, ordered=False, draw_gates_in_parallel=True): """ Return the latex document string representing the circuit. @@ -256,6 +259,12 @@ def get_latex(self): python3 my_circuit.py | pdflatex where my_circuit.py calls this function and prints it to the terminal. + + Args: + ordered(bool): flag if the gates should be drawn in the order they + were added to the circuit + draw_gates_in_parallel(bool): flag if parallel gates should be drawn + parallel (True), or not (False) """ qubit_lines = dict() @@ -271,10 +280,13 @@ def get_latex(self): new_cmd.id = cmd.lines[0] qubit_lines[new_line].append(new_cmd) - circuit = [] - for lines in qubit_lines: - circuit.append(qubit_lines[lines]) - return to_latex(qubit_lines) + drawing_order = None + if ordered: + drawing_order = self._drawing_order + + return to_latex(qubit_lines, + drawing_order=drawing_order, + draw_gates_in_parallel=draw_gates_in_parallel) def receive(self, command_list): """ diff --git a/projectq/backends/_circuits/_drawer_matplotlib.py b/projectq/backends/_circuits/_drawer_matplotlib.py new file mode 100644 index 000000000..3b16d914e --- /dev/null +++ b/projectq/backends/_circuits/_drawer_matplotlib.py @@ -0,0 +1,231 @@ +# 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): + """ + 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) diff --git a/projectq/backends/_circuits/_drawer_matplotlib_test.py b/projectq/backends/_circuits/_drawer_matplotlib_test.py new file mode 100644 index 000000000..a76fbc99b --- /dev/null +++ b/projectq/backends/_circuits/_drawer_matplotlib_test.py @@ -0,0 +1,148 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Tests for projectq.backends.circuits._drawer.py. +""" + +import pytest +from projectq import MainEngine +from projectq.cengines import DummyEngine +from projectq.ops import (H, X, Rx, CNOT, Swap, Measure, Command, BasicGate) +from projectq.types import WeakQubitRef + +from . import _drawer_matplotlib as _drawer +from ._drawer_matplotlib import CircuitDrawerMatplotlib + + +def test_drawer_measurement(): + drawer = CircuitDrawerMatplotlib(default_measure=0) + eng = MainEngine(drawer, []) + qubit = eng.allocate_qubit() + Measure | qubit + assert int(qubit) == 0 + + drawer = CircuitDrawerMatplotlib(default_measure=1) + eng = MainEngine(drawer, []) + qubit = eng.allocate_qubit() + Measure | qubit + assert int(qubit) == 1 + + drawer = CircuitDrawerMatplotlib(accept_input=True) + eng = MainEngine(drawer, []) + qubit = eng.allocate_qubit() + + old_input = _drawer.input + + _drawer.input = lambda x: '1' + Measure | qubit + assert int(qubit) == 1 + _drawer.input = old_input + + +class MockEngine(object): + def is_available(self, cmd): + self.cmd = cmd + self.called = True + return False + + +def test_drawer_isavailable(): + drawer = CircuitDrawerMatplotlib() + drawer.is_last_engine = True + + qb0 = WeakQubitRef(None, 0) + qb1 = WeakQubitRef(None, 1) + qb2 = WeakQubitRef(None, 2) + qb3 = WeakQubitRef(None, 3) + + for gate in (X, Rx(1.0)): + for qubits in (([qb0], ), ([qb0, qb1], ), ([qb0, qb1, qb2], )): + print(qubits) + cmd = Command(None, gate, qubits) + assert drawer.is_available(cmd) + + cmd0 = Command(None, X, ([qb0], )) + cmd1 = Command(None, Swap, ([qb0], [qb1])) + cmd2 = Command(None, Swap, ([qb0], [qb1]), [qb2]) + cmd3 = Command(None, Swap, ([qb0], [qb1]), [qb2, qb3]) + + assert drawer.is_available(cmd1) + assert drawer.is_available(cmd2) + assert drawer.is_available(cmd3) + + mock_engine = MockEngine() + mock_engine.called = False + drawer.is_last_engine = False + drawer.next_engine = mock_engine + + assert not drawer.is_available(cmd0) + assert mock_engine.called + assert mock_engine.cmd is cmd0 + + assert not drawer.is_available(cmd1) + assert mock_engine.called + assert mock_engine.cmd is cmd1 + + +def _draw_subst(qubit_lines, qubit_labels=None, drawing_order=None, **kwargs): + return qubit_lines + + +class MyGate(BasicGate): + def __init__(self, *args): + BasicGate.__init__(self) + self.params = args + + def __str__(self): + param_str = '{}'.format(self.params[0]) + for param in self.params[1:]: + param_str += ',{}'.format(param) + return str(self.__class__.__name__) + "(" + param_str + ")" + + +def test_drawer_draw(): + old_draw = _drawer.to_draw + _drawer.to_draw = _draw_subst + + backend = DummyEngine() + + drawer = CircuitDrawerMatplotlib() + + eng = MainEngine(backend, [drawer]) + qureg = eng.allocate_qureg(3) + H | qureg[1] + H | qureg[0] + X | qureg[0] + Rx(1) | qureg[1] + CNOT | (qureg[0], qureg[1]) + Swap | (qureg[0], qureg[1]) + MyGate(1.2) | qureg[2] + MyGate(1.23456789) | qureg[2] + MyGate(1.23456789, 2.3456789) | qureg[2] + MyGate(1.23456789, 'aaaaaaaa', 'bbb', 2.34) | qureg[2] + X | qureg[0] + + qubit_lines = drawer.draw() + + assert qubit_lines == { + 0: [('H', [0], []), ('X', [0], []), None, ('Swap', [0, 1], []), + ('X', [0], [])], + 1: [('H', [1], []), ('Rx(1.00)', [1], []), ('X', [1], [0]), None, + None], + 2: [('MyGate(1.20)', [2], []), ('MyGate(1.23)', [2], []), + ('MyGate(1.23,2.35)', [2], []), + ('MyGate(1.23,aaaaa...,bbb,2.34)', [2], []), None] + } + + _drawer.to_draw = old_draw diff --git a/projectq/backends/_circuits/_drawer_test.py b/projectq/backends/_circuits/_drawer_test.py index 7df4bd0ee..b73513b64 100755 --- a/projectq/backends/_circuits/_drawer_test.py +++ b/projectq/backends/_circuits/_drawer_test.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Tests for projectq.backends.circuits._drawer.py. """ @@ -20,19 +19,17 @@ from projectq import MainEngine from projectq.cengines import LastEngineException -from projectq.ops import (H, - X, - CNOT, - Measure) +from projectq.ops import (H, X, CNOT, Measure) from projectq.meta import Control import projectq.backends._circuits._drawer as _drawer from projectq.backends._circuits._drawer import CircuitItem, CircuitDrawer -def test_drawer_getlatex(): +@pytest.mark.parametrize("ordered", [False, True]) +def test_drawer_getlatex(ordered): old_latex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x drawer = CircuitDrawer() drawer.set_qubit_locations({0: 1, 1: 0}) @@ -46,13 +43,13 @@ def test_drawer_getlatex(): X | qureg[0] CNOT | (qureg[0], qureg[1]) - lines = drawer2.get_latex() + lines = drawer2.get_latex(ordered=ordered) assert len(lines) == 2 assert len(lines[0]) == 4 assert len(lines[1]) == 3 # check if it was sent on correctly: - lines = drawer.get_latex() + lines = drawer.get_latex(ordered=ordered) assert len(lines) == 2 assert len(lines[0]) == 3 assert len(lines[1]) == 4 diff --git a/projectq/backends/_circuits/_plot.py b/projectq/backends/_circuits/_plot.py new file mode 100644 index 000000000..edc0a1f72 --- /dev/null +++ b/projectq/backends/_circuits/_plot.py @@ -0,0 +1,628 @@ +# 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'] + (width_list[0]) * 0.5 + for idx in range(1, depth): + gate_grid[idx] = gate_grid[idx - 1] + column_spacing + ( + width_list[idx] + width_list[idx - 1]) * 0.5 + gate_grid[-1] = gate_grid[-2] + column_spacing + width_list[-1] * 0.5 + return gate_grid + + +# ============================================================================== +# Basic helper functions + + +def text(axes, gate_pos, wire_pos, textstr, plot_params): + """ + Draws a text box on the figure. + + Args: + axes (matplotlib.axes.Axes): axes object + gate_pos (float): x coordinate of the gate [data units] + wire_pos (float): y coordinate of the qubit wire + textstr (str): text of the gate and box + plot_params (dict): plot parameters + box (bool): draw the rectangle box if box is True + """ + return axes.text(gate_pos, + wire_pos, + textstr, + color='k', + ha='center', + va='center', + clip_on=True, + size=plot_params['fontsize']) + + +# ============================================================================== + + +def create_figure(plot_params): + """ + Create a new figure as well as a new axes instance + + Args: + plot_params (dict): plot parameters + + Returns: + A tuple with (figure, axes) + """ + fig = plt.figure(facecolor='w', edgecolor='w') + axes = plt.axes() + axes.set_axis_off() + axes.set_aspect('equal') + plot_params['units_per_inch'] = fig.dpi / axes.get_window_extent().width + return fig, axes + + +def resize_figure(fig, axes, width, height, plot_params): + """ + Resizes a figure and adjust the limits of the axes instance to make sure + that the distances in data coordinates on the screen stay constant. + + Args: + fig (matplotlib.figure.Figure): figure object + axes (matplotlib.axes.Axes): axes object + width (float): new figure width + height (float): new figure height + plot_params (dict): plot parameters + + Returns: + A tuple with (figure, axes) + """ + fig.set_size_inches(width, height) + + new_limits = plot_params['units_per_inch'] * np.array([width, height]) + axes.set_xlim(0, new_limits[0]) + axes.set_ylim(0, new_limits[1]) + + +def draw_gates(axes, qubit_lines, drawing_order, gate_grid, wire_grid, + plot_params): + """ + Draws the gates. + + Args: + qubit_lines (dict): list of gates for each qubit axis + drawing_order (dict): index of the wire for each qubit ID to be drawn + gate_grid (np.ndarray): x positions of the gates + wire_grid (np.ndarray): y positions of the qubit wires + plot_params (dict): plot parameters + + Returns: + A tuple with (figure, axes) + """ + for qubit_line in qubit_lines.values(): + for idx, data in enumerate(qubit_line): + if data is not None: + (gate_str, targets, controls) = data + targets_order = [drawing_order[tgt] for tgt in targets] + draw_gate( + axes, gate_str, gate_grid[idx], + [wire_grid[tgt] for tgt in targets_order], targets_order, + [wire_grid[drawing_order[ctrl]] + for ctrl in controls], plot_params) + + +def draw_gate(axes, gate_str, gate_pos, target_wires, targets_order, + control_wires, plot_params): + """ + Draws a single gate at a given location. + + Args: + axes (AxesSubplot): axes object + gate_str (str): string representation of a gate + gate_pos (float): x coordinate of the gate [data units] + target_wires (list): y coordinates of the target qubits + targets_order (list): index of the wires corresponding to the target + qubit IDs + control_wires (list): y coordinates of the control qubits + plot_params (dict): plot parameters + + Returns: + A tuple with (figure, axes) + """ + # Special cases + if gate_str == 'Z' and len(control_wires) == 1: + draw_control_z_gate(axes, gate_pos, target_wires[0], control_wires[0], + plot_params) + elif gate_str == 'X': + draw_x_gate(axes, gate_pos, target_wires[0], plot_params) + elif gate_str == 'Swap': + draw_swap_gate(axes, gate_pos, target_wires[0], target_wires[1], + plot_params) + elif gate_str == 'Measure': + draw_measure_gate(axes, gate_pos, target_wires[0], plot_params) + else: + if len(target_wires) == 1: + draw_generic_gate(axes, gate_pos, target_wires[0], gate_str, + plot_params) + else: + if sorted(targets_order) != list( + range(min(targets_order), + max(targets_order) + 1)): + raise RuntimeError( + 'Multi-qubit gate with non-neighbouring qubits!\n' + + 'Gate: {} on wires {}'.format(gate_str, targets_order)) + + multi_qubit_gate(axes, gate_str, gate_pos, min(target_wires), + max(target_wires), plot_params) + + if not control_wires: + return + + for control_wire in control_wires: + axes.add_patch( + Circle((gate_pos, control_wire), + plot_params['control_radius'], + ec='k', + fc='k', + fill=True, + lw=plot_params['linewidth'])) + + all_wires = target_wires + control_wires + axes.add_line( + Line2D((gate_pos, gate_pos), (min(all_wires), max(all_wires)), + color='k', + lw=plot_params['linewidth'])) + + +def draw_generic_gate(axes, gate_pos, wire_pos, gate_str, plot_params): + """ + Draws a measurement gate. + + Args: + axes (AxesSubplot): axes object + gate_pos (float): x coordinate of the gate [data units] + wire_pos (float): y coordinate of the qubit wire + gate_str (str) : string representation of a gate + plot_params (dict): plot parameters + """ + obj = text(axes, gate_pos, wire_pos, gate_str, plot_params) + obj.set_zorder(7) + + factor = plot_params['units_per_inch'] / obj.figure.dpi + gate_offset = plot_params['gate_offset'] + + renderer = obj.figure.canvas.get_renderer() + width = obj.get_window_extent(renderer).width * factor + 2 * gate_offset + height = obj.get_window_extent(renderer).height * factor + 2 * gate_offset + + axes.add_patch( + Rectangle((gate_pos - width / 2, wire_pos - height / 2), + width, + height, + ec='k', + fc='w', + fill=True, + lw=plot_params['linewidth'], + zorder=6)) + + +def draw_measure_gate(axes, gate_pos, wire_pos, plot_params): + """ + Draws a measurement gate. + + Args: + axes (AxesSubplot): axes object + gate_pos (float): x coordinate of the gate [data units] + wire_pos (float): y coordinate of the qubit wire + plot_params (dict): plot parameters + """ + # pylint: disable=invalid-name + + width = plot_params['mgate_width'] + height = 0.9 * width + y_ref = wire_pos - 0.3 * height + + # Cannot use PatchCollection for the arc due to bug in matplotlib code... + arc = Arc((gate_pos, y_ref), + width * 0.7, + height * 0.8, + theta1=0, + theta2=180, + ec='k', + fc='w', + zorder=5) + axes.add_patch(arc) + + patches = [ + Rectangle((gate_pos - width / 2, wire_pos - height / 2), + width, + height, + fill=True), + Line2D((gate_pos, gate_pos + width * 0.35), + (y_ref, wire_pos + height * 0.35), + color='k', + linewidth=1) + ] + + gate = PatchCollection(patches, + edgecolors='k', + facecolors='w', + linewidths=plot_params['linewidth'], + zorder=5) + gate.set_label('Measure') + axes.add_collection(gate) + + +def multi_qubit_gate(axes, gate_str, gate_pos, wire_pos_min, wire_pos_max, + plot_params): + """ + Draws a multi-target qubit gate. + + Args: + axes (matplotlib.axes.Axes): axes object + gate_str (str): string representation of a gate + gate_pos (float): x coordinate of the gate [data units] + wire_pos_min (float): y coordinate of the lowest qubit wire + wire_pos_max (float): y coordinate of the highest qubit wire + plot_params (dict): plot parameters + """ + gate_offset = plot_params['gate_offset'] + y_center = (wire_pos_max - wire_pos_min) / 2 + wire_pos_min + obj = axes.text(gate_pos, + y_center, + gate_str, + color='k', + ha='center', + va='center', + size=plot_params['fontsize'], + zorder=7) + height = wire_pos_max - wire_pos_min + 2 * gate_offset + inv = axes.transData.inverted() + width = inv.transform_bbox( + obj.get_window_extent(obj.figure.canvas.get_renderer())).width + return axes.add_patch( + Rectangle((gate_pos - width / 2, wire_pos_min - gate_offset), + width, + height, + edgecolor='k', + facecolor='w', + fill=True, + lw=plot_params['linewidth'], + zorder=6)) + + +def draw_x_gate(axes, gate_pos, wire_pos, plot_params): + """ + Draws the symbol for a X/NOT gate. + + Args: + axes (matplotlib.axes.Axes): axes object + gate_pos (float): x coordinate of the gate [data units] + wire_pos (float): y coordinate of the qubit wire [data units] + plot_params (dict): plot parameters + """ + not_radius = plot_params['not_radius'] + + gate = PatchCollection([ + Circle((gate_pos, wire_pos), not_radius, fill=False), + Line2D((gate_pos, gate_pos), + (wire_pos - not_radius, wire_pos + not_radius)) + ], + edgecolors='k', + facecolors='w', + linewidths=plot_params['linewidth']) + gate.set_label('NOT') + axes.add_collection(gate) + + +def draw_control_z_gate(axes, gate_pos, wire_pos1, wire_pos2, plot_params): + """ + Draws the symbol for a controlled-Z gate. + + Args: + axes (matplotlib.axes.Axes): axes object + wire_pos (float): x coordinate of the gate [data units] + y1 (float): y coordinate of the 1st qubit wire + y2 (float): y coordinate of the 2nd qubit wire + plot_params (dict): plot parameters + """ + gate = PatchCollection([ + Circle( + (gate_pos, wire_pos1), plot_params['control_radius'], fill=True), + Circle( + (gate_pos, wire_pos2), plot_params['control_radius'], fill=True), + Line2D((gate_pos, gate_pos), (wire_pos1, wire_pos2)) + ], + edgecolors='k', + facecolors='k', + linewidths=plot_params['linewidth']) + gate.set_label('CZ') + axes.add_collection(gate) + + +def draw_swap_gate(axes, gate_pos, wire_pos1, wire_pos2, plot_params): + """ + Draws the symbol for a SWAP gate. + + Args: + axes (matplotlib.axes.Axes): axes object + x (float): x coordinate [data units] + y1 (float): y coordinate of the 1st qubit wire + y2 (float): y coordinate of the 2nd qubit wire + plot_params (dict): plot parameters + """ + delta = plot_params['swap_delta'] + + lines = [] + for wire_pos in (wire_pos1, wire_pos2): + lines.append([(gate_pos - delta, wire_pos - delta), + (gate_pos + delta, wire_pos + delta)]) + lines.append([(gate_pos - delta, wire_pos + delta), + (gate_pos + delta, wire_pos - delta)]) + lines.append([(gate_pos, wire_pos1), (gate_pos, wire_pos2)]) + + gate = LineCollection(lines, + colors='k', + linewidths=plot_params['linewidth']) + gate.set_label('SWAP') + axes.add_collection(gate) + + +def draw_wires(axes, n_labels, gate_grid, wire_grid, plot_params): + """ + Draws all the circuit qubit wires. + + Args: + axes (matplotlib.axes.Axes): axes object + n_labels (int): number of qubit + gate_grid (ndarray): array with the ref. x positions of the gates + wire_grid (ndarray): array with the ref. y positions of the qubit + wires + plot_params (dict): plot parameters + """ + # pylint: disable=invalid-name + + lines = [] + for i in range(n_labels): + lines.append(((gate_grid[0] - plot_params['column_spacing'], + wire_grid[i]), (gate_grid[-1], wire_grid[i]))) + all_lines = LineCollection(lines, + linewidths=plot_params['linewidth'], + edgecolor='k') + all_lines.set_label('qubit_wires') + axes.add_collection(all_lines) + + +def draw_labels(axes, qubit_labels, drawing_order, wire_grid, plot_params): + """ + Draws the labels at the start of each qubit wire + + Args: + axes (matplotlib.axes.Axes): axes object + qubit_labels (list): labels of the qubit to be drawn + drawing_order (dict): Mapping between wire indices and qubit IDs + gate_grid (ndarray): array with the ref. x positions of the gates + wire_grid (ndarray): array with the ref. y positions of the qubit + wires + plot_params (dict): plot parameters + """ + for qubit_id in qubit_labels: + wire_idx = drawing_order[qubit_id] + text(axes, plot_params['x_offset'], wire_grid[wire_idx], + qubit_labels[qubit_id], plot_params) diff --git a/projectq/backends/_circuits/_plot_test.py b/projectq/backends/_circuits/_plot_test.py new file mode 100644 index 000000000..cd5d3ab0f --- /dev/null +++ b/projectq/backends/_circuits/_plot_test.py @@ -0,0 +1,289 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + Tests for projectq.backends._circuits._plot.py. + + To generate the baseline images, + run the tests with '--mpl-generate-path=baseline' + + Then run the tests simply with '--mpl' +""" +import pytest +from copy import deepcopy +import projectq.backends._circuits._plot as _plot + +# ============================================================================== + + +class PseudoCanvas(object): + def __init__(self): + pass + + def draw(self): + pass + + def get_renderer(self): + return + + +class PseudoFigure(object): + def __init__(self): + self.canvas = PseudoCanvas() + self.dpi = 1 + + +class PseudoBBox(object): + def __init__(self, width, height): + self.width = width + self.height = height + + +class PseudoText(object): + def __init__(self, text): + self.text = text + self.figure = PseudoFigure() + + def get_window_extent(self, *args): + return PseudoBBox(len(self.text), 1) + + def remove(self): + pass + + +class PseudoTransform(object): + def __init__(self): + pass + + def inverted(self): + return self + + def transform_bbox(self, bbox): + return bbox + + +class PseudoAxes(object): + def __init__(self): + self.figure = PseudoFigure() + self.transData = PseudoTransform() + + def add_patch(self, x): + return x + + def text(self, x, y, text, *args, **kwargse): + return PseudoText(text) + + +# ============================================================================== + + +@pytest.fixture(scope="module") +def plot_params(): + params = deepcopy(_plot._DEFAULT_PLOT_PARAMS) + params.update([('units_per_inch', 1)]) + return params + + +@pytest.fixture +def axes(): + return PseudoAxes() + + +# ============================================================================== + + +@pytest.mark.parametrize('gate_str', ['X', 'Swap', 'Measure', 'Y', 'Rz(1.00)']) +def test_gate_width(axes, gate_str, plot_params): + width = _plot.gate_width(axes, gate_str, plot_params) + if gate_str == 'X': + assert width == 2 * plot_params['not_radius'] / plot_params[ + 'units_per_inch'] + elif gate_str == 'Swap': + assert width == 2 * plot_params['swap_delta'] / plot_params[ + 'units_per_inch'] + elif gate_str == 'Measure': + assert width == plot_params['mgate_width'] + else: + assert width == len(gate_str) + 2 * plot_params['gate_offset'] + + +def test_calculate_gate_grid(axes, plot_params): + qubit_lines = { + 0: [('X', [0], []), ('X', [0], []), ('X', [0], []), ('X', [0], [])] + } + + gate_grid = _plot.calculate_gate_grid(axes, qubit_lines, plot_params) + assert len(gate_grid) == 5 + assert gate_grid[0] > plot_params['labels_margin'] + width = [gate_grid[i + 1] - gate_grid[i] for i in range(4)] + + # Column grid is given by: + # |---*---|---*---|---*---|---*---| + # |-- w --|-- w --|-- w --|.5w| + + column_spacing = plot_params['column_spacing'] + ref_width = _plot.gate_width(axes, 'X', plot_params) + + for w in width[:-1]: + assert ref_width + column_spacing == pytest.approx(w) + assert 0.5 * ref_width + column_spacing == pytest.approx(width[-1]) + + +def test_create_figure(plot_params): + fig, axes = _plot.create_figure(plot_params) + + +def test_draw_single_gate(axes, plot_params): + with pytest.raises(RuntimeError): + _plot.draw_gate(axes, 'MyGate', 2, [0, 0, 0], [0, 1, 3], [], + plot_params) + _plot.draw_gate(axes, 'MyGate', 2, [0, 0, 0], [0, 1, 2], [], plot_params) + + +def test_draw_simple(plot_params): + qubit_lines = { + 0: [('X', [0], []), ('Z', [0], []), ('Z', [0], [1]), + ('Swap', [0, 1], []), ('Measure', [0], [])], + 1: [None, None, None, None, None] + } + fig, axes = _plot.to_draw(qubit_lines) + + units_per_inch = plot_params['units_per_inch'] + not_radius = plot_params['not_radius'] + control_radius = plot_params['control_radius'] + swap_delta = plot_params['swap_delta'] + wire_height = plot_params['wire_height'] * units_per_inch + mgate_width = plot_params['mgate_width'] + + labels = [] + text_gates = [] + measure_gates = [] + for text in axes.texts: + if text.get_text() == '$|0\\rangle$': + labels.append(text) + elif text.get_text() == ' ': + measure_gates.append(text) + else: + text_gates.append(text) + + assert all( + label.get_position()[0] == pytest.approx(plot_params['x_offset']) + for label in labels) + assert (abs(labels[1].get_position()[1] + - labels[0].get_position()[1]) == pytest.approx(wire_height)) + + # X gate + x_gate = [obj for obj in axes.collections if obj.get_label() == 'NOT'][0] + # find the filled circles + assert (x_gate.get_paths()[0].get_extents().width == pytest.approx( + 2 * not_radius)) + assert (x_gate.get_paths()[0].get_extents().height == pytest.approx( + 2 * not_radius)) + # find the vertical bar + x_vertical = x_gate.get_paths()[1] + assert len(x_vertical) == 2 + assert x_vertical.get_extents().width == 0. + assert (x_vertical.get_extents().height == pytest.approx( + 2 * plot_params['not_radius'])) + + # Z gate + assert len(text_gates) == 1 + assert text_gates[0].get_text() == 'Z' + assert text_gates[0].get_position()[1] == pytest.approx(2 * wire_height) + + # CZ gate + cz_gate = [obj for obj in axes.collections if obj.get_label() == 'CZ'][0] + # find the filled circles + for control in cz_gate.get_paths()[:-1]: + assert control.get_extents().width == pytest.approx(2 * control_radius) + assert control.get_extents().height == pytest.approx(2 + * control_radius) + # find the vertical bar + cz_vertical = cz_gate.get_paths()[-1] + assert len(cz_vertical) == 2 + assert cz_vertical.get_extents().width == 0. + assert (cz_vertical.get_extents().height == pytest.approx(wire_height)) + + # Swap gate + swap_gate = [obj for obj in axes.collections + if obj.get_label() == 'SWAP'][0] + # find the filled circles + for qubit in swap_gate.get_paths()[:-1]: + assert qubit.get_extents().width == pytest.approx(2 * swap_delta) + assert qubit.get_extents().height == pytest.approx(2 * swap_delta) + # find the vertical bar + swap_vertical = swap_gate.get_paths()[-1] + assert len(swap_vertical) == 2 + assert swap_vertical.get_extents().width == 0. + assert (swap_vertical.get_extents().height == pytest.approx(wire_height)) + + # Measure gate + measure_gate = [ + obj for obj in axes.collections if obj.get_label() == 'Measure' + ][0] + + assert (measure_gate.get_paths()[0].get_extents().width == pytest.approx( + mgate_width)) + assert (measure_gate.get_paths()[0].get_extents().height == pytest.approx( + 0.9 * mgate_width)) + + +def test_draw_advanced(plot_params): + qubit_lines = {0: [('X', [0], []), ('Measure', [0], [])], 1: [None, None]} + + with pytest.raises(RuntimeError): + _plot.to_draw(qubit_lines, qubit_labels={1: 'qb1', 2: 'qb2'}) + + with pytest.raises(RuntimeError): + _plot.to_draw(qubit_lines, drawing_order={0: 0, 1: 2}) + + with pytest.raises(RuntimeError): + _plot.to_draw(qubit_lines, drawing_order={1: 1, 2: 0}) + + # -------------------------------------------------------------------------- + + _, axes = _plot.to_draw(qubit_lines) + for text in axes.texts: + assert text.get_text() == r'$|0\rangle$' + + # NB numbering of wire starts from bottom. + _, axes = _plot.to_draw(qubit_lines, + qubit_labels={ + 0: 'qb0', + 1: 'qb1' + }, + drawing_order={ + 0: 0, + 1: 1 + }) + assert ([axes.texts[qubit_id].get_text() + for qubit_id in range(2)] == ['qb0', 'qb1']) + + positions = [axes.texts[qubit_id].get_position() for qubit_id in range(2)] + assert positions[1][1] > positions[0][1] + + _, axes = _plot.to_draw(qubit_lines, + qubit_labels={ + 0: 'qb2', + 1: 'qb3' + }, + drawing_order={ + 0: 1, + 1: 0 + }) + + assert ([axes.texts[qubit_id].get_text() + for qubit_id in range(2)] == ['qb2', 'qb3']) + + positions = [axes.texts[qubit_id].get_position() for qubit_id in range(2)] + assert positions[1][1] < positions[0][1] diff --git a/projectq/backends/_circuits/_to_latex.py b/projectq/backends/_circuits/_to_latex.py index 1c028acfc..385f3d3f3 100755 --- a/projectq/backends/_circuits/_to_latex.py +++ b/projectq/backends/_circuits/_to_latex.py @@ -17,7 +17,7 @@ Measure, SqrtSwap, Swap, X, Z) -def to_latex(circuit): +def to_latex(circuit, drawing_order=None, draw_gates_in_parallel=True): """ Translates a given circuit to a TikZ picture in a Latex document. @@ -38,8 +38,12 @@ class name string as a key. Every gate can have its own width, height, pre function, and written using write_settings(). Args: - circuit (list>): Each qubit line is a list of + circuit (list): Each qubit line is a list of CircuitItem objects, i.e., in circuit[line]. + drawing_order (list): A list of qubit lines from which + the gates to be read from + draw_gates_in_parallel (bool): If gates should (False) + or not (True) be parallel in the circuit Returns: tex_doc_str (string): Latex document string which can be compiled @@ -57,7 +61,10 @@ class name string as a key. Every gate can have its own width, height, pre settings = write_settings(get_default_settings()) text = _header(settings) - text += _body(circuit, settings) + text += _body(circuit, + settings, + drawing_order, + draw_gates_in_parallel=draw_gates_in_parallel) text += _footer(settings) return text @@ -83,39 +90,88 @@ def get_default_settings(): """ settings = dict() settings['gate_shadow'] = True - settings['lines'] = ({'style': 'very thin', 'double_classical': True, - 'init_quantum': True, 'double_lines_sep': .04}) - settings['gates'] = ({'HGate': {'width': .5, 'offset': .3, - 'pre_offset': .1}, - 'XGate': {'width': .35, 'height': .35, - 'offset': .1}, - 'SqrtXGate': {'width': .7, 'offset': .3, - 'pre_offset': .1}, - 'SwapGate': {'width': .35, 'height': .35, - 'offset': .1}, - 'SqrtSwapGate': {'width': .35, 'height': .35, - 'offset': .1}, - 'Rx': {'width': 1., 'height': .8, 'pre_offset': .2, - 'offset': .3}, - 'Ry': {'width': 1., 'height': .8, 'pre_offset': .2, - 'offset': .3}, - 'Rz': {'width': 1., 'height': .8, 'pre_offset': .2, - 'offset': .3}, - 'Ph': {'width': 1., 'height': .8, 'pre_offset': .2, - 'offset': .3}, - 'EntangleGate': {'width': 1.8, 'offset': .2, - 'pre_offset': .2}, - 'DeallocateQubitGate': {'height': .15, 'offset': .2, - 'width': .2, - 'pre_offset': .1}, - 'AllocateQubitGate': {'height': .15, 'width': .2, - 'offset': .1, - 'pre_offset': .1, - 'draw_id': False, - 'allocate_at_zero': False}, - 'MeasureGate': {'width': 0.75, 'offset': .2, - 'height': .5, 'pre_offset': .2} - }) + settings['lines'] = ({ + 'style': 'very thin', + 'double_classical': True, + 'init_quantum': True, + 'double_lines_sep': .04 + }) + settings['gates'] = ({ + 'HGate': { + 'width': .5, + 'offset': .3, + 'pre_offset': .1 + }, + 'XGate': { + 'width': .35, + 'height': .35, + 'offset': .1 + }, + 'SqrtXGate': { + 'width': .7, + 'offset': .3, + 'pre_offset': .1 + }, + 'SwapGate': { + 'width': .35, + 'height': .35, + 'offset': .1 + }, + 'SqrtSwapGate': { + 'width': .35, + 'height': .35, + 'offset': .1 + }, + 'Rx': { + 'width': 1., + 'height': .8, + 'pre_offset': .2, + 'offset': .3 + }, + 'Ry': { + 'width': 1., + 'height': .8, + 'pre_offset': .2, + 'offset': .3 + }, + 'Rz': { + 'width': 1., + 'height': .8, + 'pre_offset': .2, + 'offset': .3 + }, + 'Ph': { + 'width': 1., + 'height': .8, + 'pre_offset': .2, + 'offset': .3 + }, + 'EntangleGate': { + 'width': 1.8, + 'offset': .2, + 'pre_offset': .2 + }, + 'DeallocateQubitGate': { + 'height': .15, + 'offset': .2, + 'width': .2, + 'pre_offset': .1 + }, + 'AllocateQubitGate': { + 'height': .15, + 'width': .2, + 'offset': .1, + 'pre_offset': .1, + 'draw_id': False, + 'allocate_at_zero': False + }, + 'MeasureGate': { + 'width': 0.75, + 'offset': .2, + 'height': .5, + 'pre_offset': .2 + } + }) settings['control'] = {'size': .1, 'shadow': False} return settings @@ -153,8 +209,7 @@ def _header(settings): gate_style += ("\\tikzstyle{operator}=[basic,minimum size=1.5em]\n" "\\tikzstyle{phase}=[fill=black,shape=circle," + "minimum size={}".format(settings['control']['size']) + - "cm,inner sep=0pt,outer sep=0pt,draw=black" - ) + "cm,inner sep=0pt,outer sep=0pt,draw=black") if settings['control']['shadow']: gate_style += ",basicshadow" gate_style += ("]\n\\tikzstyle{none}=[inner sep=0pt,outer sep=-.5pt," @@ -167,9 +222,9 @@ def _header(settings): x_gate_radius = min(settings['gates']['XGate']['height'], settings['gates']['XGate']['width']) gate_style += ("{x_rad}cm,minimum width={x_rad}cm,inner sep=-1pt," - "{linestyle}]\n" - ).format(x_rad=x_gate_radius, - linestyle=settings['lines']['style']) + "{linestyle}]\n").format( + x_rad=x_gate_radius, + linestyle=settings['lines']['style']) if settings['gate_shadow']: gate_style += ("\\tikzset{\nshadowed/.style={preaction={transform " "canvas={shift={(0.5pt,-0.5pt)}}, draw=gray, opacity=" @@ -182,13 +237,19 @@ def _header(settings): return packages + init + gate_style + edge_style -def _body(circuit, settings): +def _body(circuit, settings, drawing_order=None, draw_gates_in_parallel=True): """ Return the body of the Latex document, including the entire circuit in TikZ format. Args: circuit (list>): Circuit to draw. + settings: Dictionary of settings to use for the TikZ image. + drawing_order: A list of circuit wires from where to read + one gate command. + draw_gates_in_parallel: Are the gate/commands occupying a + single time step in the circuit diagram? For example, False means + that gates can be parallel in the circuit. Returns: tex_str (string): Latex string to draw the entire circuit. @@ -196,8 +257,19 @@ def _body(circuit, settings): code = [] conv = _Circ2Tikz(settings, len(circuit)) - for line in range(len(circuit)): - code.append(conv.to_tikz(line, circuit)) + + to_where = None + if drawing_order is None: + drawing_order = list(range(len(circuit))) + else: + to_where = 1 + + for line in drawing_order: + code.append( + conv.to_tikz(line, + circuit, + end=to_where, + draw_gates_in_parallel=draw_gates_in_parallel)) return "".join(code) @@ -219,7 +291,6 @@ class _Circ2Tikz(object): It uses the settings dictionary for gate offsets, sizes, spacing, ... """ - def __init__(self, settings, num_lines): """ Initialize a circuit to latex converter object. @@ -234,7 +305,7 @@ def __init__(self, settings, num_lines): self.op_count = [0] * num_lines self.is_quantum = [settings['lines']['init_quantum']] * num_lines - def to_tikz(self, line, circuit, end=None): + def to_tikz(self, line, circuit, end=None, draw_gates_in_parallel=True): """ Generate the TikZ code for one line of the circuit up to a certain gate. @@ -247,6 +318,7 @@ def to_tikz(self, line, circuit, end=None): line (int): Line to generate the TikZ code for. circuit (list>): The circuit to draw. end (int): Gate index to stop at (for recursion). + draw_gates_in_parallel (bool): True or False for how to place gates Returns: tikz_code (string): TikZ code representing the current qubit line @@ -272,19 +344,23 @@ def to_tikz(self, line, circuit, end=None): gate_idx += 1 tikz_code.append(self.to_tikz(l, circuit, gate_idx)) + # we are taking care of gate 0 (the current one) circuit[l] = circuit[l][1:] all_lines = lines + ctrl_lines - pos = max([self.pos[l] for l in range(min(all_lines), - max(all_lines) + 1)]) + pos = max([ + self.pos[l] for l in range(min(all_lines), + max(all_lines) + 1) + ]) for l in range(min(all_lines), max(all_lines) + 1): self.pos[l] = pos + self._gate_pre_offset(gate) connections = "" for l in all_lines: connections += self._line(self.op_count[l] - 1, - self.op_count[l], line=l) + self.op_count[l], + line=l) add_str = "" if gate == X: # draw NOT-gate with controls @@ -298,7 +374,8 @@ def to_tikz(self, line, circuit, end=None): elif gate == Swap: add_str = self._swap_gate(lines, ctrl_lines) elif gate == SqrtSwap: - add_str = self._sqrtswap_gate(lines, ctrl_lines, + add_str = self._sqrtswap_gate(lines, + ctrl_lines, daggered=False) elif gate == get_inverse(SqrtSwap): add_str = self._sqrtswap_gate(lines, ctrl_lines, daggered=True) @@ -319,10 +396,13 @@ def to_tikz(self, line, circuit, end=None): "cm,xshift=-{shift2}cm]{op}.east);\n" "\\draw[edgestyle] ([yshift=-{shift1}cm]{op}." "center) to ([yshift=-{shift2}cm,xshift=-" - "{shift1}cm]{op}.north east);" - ).format(op=op, pos=self.pos[l], line=l, - shift0=shift0, shift1=shift1, - shift2=shift2) + "{shift1}cm]{op}.north east);").format( + op=op, + pos=self.pos[l], + line=l, + shift0=shift0, + shift1=shift1, + shift2=shift2) self.op_count[l] += 1 self.pos[l] += (self._gate_width(gate) + self._gate_offset(gate)) @@ -336,15 +416,15 @@ def to_tikz(self, line, circuit, end=None): xpos = self.pos[line] try: if (self.settings['gates']['AllocateQubitGate'] - ['allocate_at_zero']): + ['allocate_at_zero']): self.pos[line] -= self._gate_pre_offset(gate) xpos = self._gate_pre_offset(gate) except KeyError: pass - self.pos[line] = max(xpos + self._gate_offset(gate) + - self._gate_width(gate), self.pos[line]) - add_str = add_str.format(self._op(line), xpos, line, - id_str) + self.pos[line] = max( + xpos + self._gate_offset(gate) + self._gate_width(gate), + self.pos[line]) + add_str = add_str.format(self._op(line), xpos, line, id_str) self.op_count[line] += 1 self.is_quantum[line] = self.settings['lines']['init_quantum'] elif gate == Deallocate: @@ -353,9 +433,10 @@ def to_tikz(self, line, circuit, end=None): add_str = "\n\\node[none] ({}) at ({},-{}) {{}};" add_str = add_str.format(op, self.pos[line], line) yshift = str(self._gate_height(gate)) + "cm]" - add_str += ("\n\\draw ([yshift={yshift}{op}.center) edge " - "[edgestyle] ([yshift=-{yshift}{op}.center);" - ).format(op=op, yshift=yshift) + add_str += ( + "\n\\draw ([yshift={yshift}{op}.center) edge " + "[edgestyle] ([yshift=-{yshift}{op}.center);").format( + op=op, yshift=yshift) self.op_count[line] += 1 self.pos[line] += (self._gate_width(gate) + self._gate_offset(gate)) @@ -370,6 +451,11 @@ def to_tikz(self, line, circuit, end=None): if not gate == Allocate: tikz_code.append(connections) + if not draw_gates_in_parallel: + for l in range(len(self.pos)): + if l != line: + self.pos[l] = self.pos[line] + circuit[line] = circuit[line][end:] return "".join(tikz_code) @@ -402,7 +488,7 @@ def _sqrtswap_gate(self, lines, ctrl_lines, daggered): ctrl_lines (list): List of qubit lines which act as controls. daggered (bool): Show the daggered one if True. """ - assert(len(lines) == 2) # sqrt swap gate acts on 2 qubits + assert (len(lines) == 2) # sqrt swap gate acts on 2 qubits delta_pos = self._gate_offset(SqrtSwap) gate_width = self._gate_width(SqrtSwap) lines.sort() @@ -420,20 +506,27 @@ def _sqrtswap_gate(self, lines, ctrl_lines, daggered): swap_style += ",shadowed" gate_str += ("\n\\node[swapstyle] ({op}) at ({pos},-{line}) {{}};" "\n\\draw[{swap_style}] ({s1})--({s2});\n" - "\\draw[{swap_style}] ({s3})--({s4});" - ).format(op=op, s1=s1, s2=s2, s3=s3, s4=s4, - line=line, pos=self.pos[line], - swap_style=swap_style) + "\\draw[{swap_style}] ({s3})--({s4});").format( + op=op, + s1=s1, + s2=s2, + s3=s3, + s4=s4, + line=line, + pos=self.pos[line], + swap_style=swap_style) # add a circled 1/2 midpoint = (lines[0] + lines[1]) / 2. pos = self.pos[lines[0]] - op_mid = "line{}_gate{}".format( - '{}-{}'.format(*lines), self.op_count[lines[0]]) + op_mid = "line{}_gate{}".format('{}-{}'.format(*lines), + self.op_count[lines[0]]) gate_str += ("\n\\node[xstyle] ({op}) at ({pos},-{line})\ - {{\\scriptsize $\\frac{{1}}{{2}}{dagger}$}};" - ).format(op=op_mid, line=midpoint, pos=pos, - dagger='^{{\\dagger}}' if daggered else '') + {{\\scriptsize $\\frac{{1}}{{2}}{dagger}$}};").format( + op=op_mid, + line=midpoint, + pos=pos, + dagger='^{{\\dagger}}' if daggered else '') # add two vertical lines to connect circled 1/2 gate_str += "\n\\draw ({}) edge[edgestyle] ({});".format( @@ -468,7 +561,7 @@ def _swap_gate(self, lines, ctrl_lines): ctrl_lines (list): List of qubit lines which act as controls. """ - assert(len(lines) == 2) # swap gate acts on 2 qubits + assert (len(lines) == 2) # swap gate acts on 2 qubits delta_pos = self._gate_offset(Swap) gate_width = self._gate_width(Swap) lines.sort() @@ -486,10 +579,15 @@ def _swap_gate(self, lines, ctrl_lines): swap_style += ",shadowed" gate_str += ("\n\\node[swapstyle] ({op}) at ({pos},-{line}) {{}};" "\n\\draw[{swap_style}] ({s1})--({s2});\n" - "\\draw[{swap_style}] ({s3})--({s4});" - ).format(op=op, s1=s1, s2=s2, s3=s3, s4=s4, - line=line, pos=self.pos[line], - swap_style=swap_style) + "\\draw[{swap_style}] ({s3})--({s4});").format( + op=op, + s1=s1, + s2=s2, + s3=s3, + s4=s4, + line=line, + pos=self.pos[line], + swap_style=swap_style) gate_str += self._line(lines[0], lines[1]) if len(ctrl_lines) > 0: @@ -519,15 +617,15 @@ def _x_gate(self, lines, ctrl_lines): ctrl_lines (list): List of qubit lines which act as controls. """ - assert(len(lines) == 1) # NOT gate only acts on 1 qubit + assert (len(lines) == 1) # NOT gate only acts on 1 qubit line = lines[0] delta_pos = self._gate_offset(X) gate_width = self._gate_width(X) op = self._op(line) gate_str = ("\n\\node[xstyle] ({op}) at ({pos},-{line}) {{}};\n\\draw" "[edgestyle] ({op}.north)--({op}.south);\n\\draw" - "[edgestyle] ({op}.west)--({op}.east);" - ).format(op=op, line=line, pos=self.pos[line]) + "[edgestyle] ({op}.west)--({op}.east);").format( + op=op, line=line, pos=self.pos[line]) if len(ctrl_lines) > 0: for ctrl in ctrl_lines: @@ -705,10 +803,16 @@ def _line(self, p1, p2, double=False, line=None): line_sep = self.settings['lines']['double_lines_sep'] shift1 = shift.format(line_sep / 2.) shift2 = shift.format(-line_sep / 2.) - edges_str = edge_str.format(shift=shift1, op1=op1, op2=op2, - loc1=loc1, loc2=loc2) - edges_str += edge_str.format(shift=shift2, op1=op1, op2=op2, - loc1=loc1, loc2=loc2) + edges_str = edge_str.format(shift=shift1, + op1=op1, + op2=op2, + loc1=loc1, + loc2=loc2) + edges_str += edge_str.format(shift=shift2, + op1=op1, + op2=op2, + loc1=loc1, + loc2=loc2) return edges_str def _regular_gate(self, gate, lines, ctrl_lines): @@ -744,23 +848,24 @@ def _regular_gate(self, gate, lines, ctrl_lines): for l in lines: node1 = node_str.format(self._op(l), pos, l) node2 = ("\n\\node[none,minimum height={}cm,outer sep=0] ({}) at" - " ({},-{}) {{}};" - ).format(gate_height, self._op(l, offset=1), - pos + gate_width / 2., l) - node3 = node_str.format(self._op(l, offset=2), - pos + gate_width, l) + " ({},-{}) {{}};").format(gate_height, + self._op(l, offset=1), + pos + gate_width / 2., l) + node3 = node_str.format(self._op(l, offset=2), pos + gate_width, l) tex_str += node1 + node2 + node3 if l not in gate_lines: - tex_str += self._line(self.op_count[l] - 1, self.op_count[l], + tex_str += self._line(self.op_count[l] - 1, + self.op_count[l], line=l) tex_str += ("\n\\draw[operator,edgestyle,outer sep={width}cm] ([" "yshift={half_height}cm]{op1}) rectangle ([yshift=-" - "{half_height}cm]{op2}) node[pos=.5] {{{name}}};" - ).format(width=gate_width, op1=self._op(imin), - op2=self._op(imax, offset=2), - half_height=.5 * gate_height, - name=name) + "{half_height}cm]{op2}) node[pos=.5] {{{name}}};").format( + width=gate_width, + op1=self._op(imin), + op2=self._op(imax, offset=2), + half_height=.5 * gate_height, + name=name) for l in lines: self.pos[l] = pos + gate_width / 2. diff --git a/projectq/backends/_circuits/_to_latex_test.py b/projectq/backends/_circuits/_to_latex_test.py index bf26b9923..c993bcf2e 100755 --- a/projectq/backends/_circuits/_to_latex_test.py +++ b/projectq/backends/_circuits/_to_latex_test.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Tests for projectq.backends._circuits._to_latex.py. """ @@ -22,18 +21,19 @@ from projectq import MainEngine from projectq.cengines import LastEngineException -from projectq.ops import (BasicGate, - H, - X, - CNOT, - Measure, - Z, - Swap, - SqrtX, - SqrtSwap, - C, - get_inverse, - ) +from projectq.ops import ( + BasicGate, + H, + X, + CNOT, + Measure, + Z, + Swap, + SqrtX, + SqrtSwap, + C, + get_inverse, +) from projectq.meta import Control from projectq.backends import CircuitDrawer @@ -47,7 +47,7 @@ def test_tolatex(): old_footer = _to_latex._footer _to_latex._header = lambda x: "H" - _to_latex._body = lambda x, y: x + _to_latex._body = lambda x, settings, drawing_order, draw_gates_in_parallel: x _to_latex._footer = lambda x: "F" latex = _to_latex.to_latex("B") @@ -68,11 +68,26 @@ def test_default_settings(): def test_header(): - settings = {'gate_shadow': False, 'control': {'shadow': False, 'size': 0}, - 'gates': {'MeasureGate': {'height': 0, 'width': 0}, - 'XGate': {'height': 1, 'width': .5} - }, - 'lines': {'style': 'my_style'}} + settings = { + 'gate_shadow': False, + 'control': { + 'shadow': False, + 'size': 0 + }, + 'gates': { + 'MeasureGate': { + 'height': 0, + 'width': 0 + }, + 'XGate': { + 'height': 1, + 'width': .5 + } + }, + 'lines': { + 'style': 'my_style' + } + } header = _to_latex._header(settings) assert 'minimum' in header @@ -104,7 +119,7 @@ def test_large_gates(): drawer = _drawer.CircuitDrawer() eng = MainEngine(drawer, []) old_tolatex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x qubit1 = eng.allocate_qubit() qubit2 = eng.allocate_qubit() @@ -136,7 +151,7 @@ def test_body(): drawer = _drawer.CircuitDrawer() eng = MainEngine(drawer, []) old_tolatex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x qubit1 = eng.allocate_qubit() qubit2 = eng.allocate_qubit() @@ -181,12 +196,135 @@ def test_body(): assert code.count("{{{}}}".format(str(Z))) == 1 # 1 Z gate assert code.count("{red}") == 3 +def test_body_with_drawing_order_and_gates_parallel(): + drawer = _drawer.CircuitDrawer() + eng = MainEngine(drawer, []) + old_tolatex = _drawer.to_latex + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x + + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qubit3 = eng.allocate_qubit() + + H | qubit1 + H | qubit2 + H | qubit3 + CNOT | (qubit1, qubit3) + + # replicates the above order + order = [0, 1, 2, # initializations + 0, 1, 2, # H1, H3, H2 + 0 # CNOT + ] + + del qubit1 + eng.flush() + + circuit_lines = drawer.get_latex() + _drawer.to_latex = old_tolatex + + settings = _to_latex.get_default_settings() + settings['gates']['AllocateQubitGate']['draw_id'] = True + code = _to_latex._body(circuit_lines, settings, + drawing_order=order, + draw_gates_in_parallel=True) + + # there are three Hadamards in parallel + assert code.count("node[pos=.5] {H}") == 3 + + # line1_gate0 is initialisation + # line1_gate1 is empty + # line1_gate2 is for Hadamard on line1 + # line1_gate3 is empty + # XOR of CNOT is node[xstyle] (line1_gate4) + assert code.count("node[xstyle] (line2_gate4)") == 1 + + # and the CNOT is at position 1.4, because of the offsets + assert code.count("node[phase] (line0_gate4) at (1.4") == 1 + assert code.count("node[xstyle] (line2_gate4) at (1.4") == 1 + + +def test_body_with_drawing_order_and_gates_not_parallel(): + drawer = _drawer.CircuitDrawer() + eng = MainEngine(drawer, []) + old_tolatex = _drawer.to_latex + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x + + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qubit3 = eng.allocate_qubit() + + H | qubit1 + H | qubit2 + H | qubit3 + CNOT | (qubit1, qubit3) + + # replicates the above order + order = [0, 1, 2, # initializations + 0, 1, 2, # H1, H3, H2 + 0 # CNOT + ] + + del qubit1 + eng.flush() + + circuit_lines = drawer.get_latex() + _drawer.to_latex = old_tolatex + + settings = _to_latex.get_default_settings() + settings['gates']['AllocateQubitGate']['draw_id'] = True + code = _to_latex._body(circuit_lines, settings, + drawing_order=order, + draw_gates_in_parallel=False) + + # and the CNOT is at position 4.0, because of the offsets + # which are 0.5 * 3 * 2 (due to three Hadamards) + the initialisations + assert code.count("node[phase] (line0_gate4) at (4.0,-0)") == 1 + assert code.count("node[xstyle] (line2_gate4) at (4.0,-2)") == 1 + +def test_body_without_drawing_order_and_gates_not_parallel(): + drawer = _drawer.CircuitDrawer() + eng = MainEngine(drawer, []) + old_tolatex = _drawer.to_latex + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x + + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qubit3 = eng.allocate_qubit() + + H | qubit1 + H | qubit2 + H | qubit3 + CNOT | (qubit1, qubit3) + + # replicates the above order + order = [0, 1, 2, # initializations + 0, 1, 2, # H1, H3, H2 + 0 # CNOT + ] + + del qubit1 + eng.flush() + + circuit_lines = drawer.get_latex() + _drawer.to_latex = old_tolatex + + settings = _to_latex.get_default_settings() + settings['gates']['AllocateQubitGate']['draw_id'] = True + code = _to_latex._body(circuit_lines, settings, + draw_gates_in_parallel=False) + + # line1_gate1 is after the cnot line2_gate_4 + idx1 = code.find("node[xstyle] (line2_gate4)") + idx2 = code.find("node[none] (line1_gate1)") + assert idx1 < idx2 + def test_qubit_allocations_at_zero(): drawer = _drawer.CircuitDrawer() eng = MainEngine(drawer, []) old_tolatex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x a = eng.allocate_qureg(4) @@ -219,7 +357,7 @@ def test_qubit_lines_classicalvsquantum1(): drawer = _drawer.CircuitDrawer() eng = MainEngine(drawer, []) old_tolatex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x qubit1 = eng.allocate_qubit() diff --git a/projectq/backends/_ibm/_ibm.py b/projectq/backends/_ibm/_ibm.py index 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/backends/_resource_test.py b/projectq/backends/_resource_test.py index 664b687ad..cf7122c01 100755 --- a/projectq/backends/_resource_test.py +++ b/projectq/backends/_resource_test.py @@ -20,7 +20,7 @@ from projectq.cengines import DummyEngine, MainEngine, NotYetMeasuredError from projectq.meta import LogicalQubitIDTag -from projectq.ops import All, Allocate, CNOT, Command, H, Measure, QFT, Rz, X +from projectq.ops import All, Allocate, CNOT, Command, H, Measure, QFT, Rz, Rzz, X from projectq.types import WeakQubitRef from projectq.backends import ResourceCounter @@ -74,6 +74,7 @@ def test_resource_counter(): CNOT | (qubit1, qubit3) Rz(0.1) | qubit1 Rz(0.3) | qubit1 + Rzz(0.5) | qubit1 All(Measure) | qubit1 + qubit3 @@ -81,7 +82,7 @@ def test_resource_counter(): int(qubit1) assert resource_counter.max_width == 2 - assert resource_counter.depth_of_dag == 5 + assert resource_counter.depth_of_dag == 6 str_repr = str(resource_counter) assert str_repr.count(" HGate : 1") == 1 diff --git a/projectq/backends/_sim/_cppkernels/simulator.hpp b/projectq/backends/_sim/_cppkernels/simulator.hpp index c4e611eba..d248ed038 100755 --- a/projectq/backends/_sim/_cppkernels/simulator.hpp +++ b/projectq/backends/_sim/_cppkernels/simulator.hpp @@ -38,7 +38,7 @@ class Simulator{ public: using calc_type = double; using complex_type = std::complex; - using StateVector = std::vector>; + using StateVector = std::vector>; using Map = std::map; using RndEngine = std::mt19937; using Term = std::vector>; @@ -55,11 +55,18 @@ class Simulator{ void allocate_qubit(unsigned id){ if (map_.count(id) == 0){ map_[id] = N_++; - auto newvec = StateVector(1UL << N_); - #pragma omp parallel for schedule(static) + StateVector newvec; // avoid large memory allocations + if( tmpBuff1_.capacity() >= (1UL << N_) ) + std::swap(newvec, tmpBuff1_); + newvec.resize(1UL << N_); +#pragma omp parallel for schedule(static) for (std::size_t i = 0; i < newvec.size(); ++i) newvec[i] = (i < vec_.size())?vec_[i]:0.; - vec_ = std::move(newvec); + std::swap(vec_, newvec); + // recycle large memory + std::swap(tmpBuff1_, newvec); + if( tmpBuff1_.capacity() < tmpBuff2_.capacity() ) + std::swap(tmpBuff1_, tmpBuff2_); } else throw(std::runtime_error( @@ -113,12 +120,18 @@ class Simulator{ } } else{ - StateVector newvec((1UL << (N_-1))); - #pragma omp parallel for schedule(static) + StateVector newvec; // avoid costly memory reallocations + if( tmpBuff1_.capacity() >= (1UL << (N_-1)) ) + std::swap(tmpBuff1_, newvec); + newvec.resize((1UL << (N_-1))); + #pragma omp parallel for schedule(static) if(0) for (std::size_t i = 0; i < vec_.size(); i += 2*delta) std::copy_n(&vec_[i + static_cast(value)*delta], delta, &newvec[i/2]); - vec_ = std::move(newvec); + std::swap(vec_, newvec); + std::swap(tmpBuff1_, newvec); + if( tmpBuff1_.capacity() < tmpBuff2_.capacity() ) + std::swap(tmpBuff1_, tmpBuff2_); for (auto& p : map_){ if (p.second > pos) @@ -189,8 +202,8 @@ class Simulator{ } template - void apply_controlled_gate(M const& m, std::vector ids, - std::vector ctrl){ + void apply_controlled_gate(M const& m, const std::vector& ids, + const std::vector& ctrl){ auto fused_gates = fused_gates_; fused_gates.insert(m, ids, ctrl); @@ -209,8 +222,8 @@ class Simulator{ } template - void emulate_math(F const& f, QuReg quregs, std::vector ctrl, - unsigned num_threads=1){ + void emulate_math(F const& f, QuReg quregs, const std::vector& ctrl, + bool parallelize = false){ run(); auto ctrlmask = get_control_mask(ctrl); @@ -218,37 +231,76 @@ class Simulator{ for (unsigned j = 0; j < quregs[i].size(); ++j) quregs[i][j] = map_[quregs[i][j]]; - StateVector newvec(vec_.size(), 0.); - std::vector res(quregs.size()); - - #pragma omp parallel for schedule(static) firstprivate(res) num_threads(num_threads) - for (std::size_t i = 0; i < vec_.size(); ++i){ - if ((ctrlmask&i) == ctrlmask){ - for (unsigned qr_i = 0; qr_i < quregs.size(); ++qr_i){ - res[qr_i] = 0; - for (unsigned qb_i = 0; qb_i < quregs[qr_i].size(); ++qb_i) - res[qr_i] |= ((i >> quregs[qr_i][qb_i])&1) << qb_i; - } - f(res); - auto new_i = i; - for (unsigned qr_i = 0; qr_i < quregs.size(); ++qr_i){ - for (unsigned qb_i = 0; qb_i < quregs[qr_i].size(); ++qb_i){ - if (!(((new_i >> quregs[qr_i][qb_i])&1) == ((res[qr_i] >> qb_i)&1))) - new_i ^= (1UL << quregs[qr_i][qb_i]); - } - } - newvec[new_i] += vec_[i]; - } - else - newvec[i] += vec_[i]; + StateVector newvec; // avoid costly memory reallocations + if( tmpBuff1_.capacity() >= vec_.size() ) + std::swap(newvec, tmpBuff1_); + newvec.resize(vec_.size()); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < vec_.size(); i++) + newvec[i] = 0; + +//#pragma omp parallel reduction(+:newvec[:newvec.size()]) if(parallelize) // requires OpenMP 4.5 + { + std::vector res(quregs.size()); + //#pragma omp for schedule(static) + for (std::size_t i = 0; i < vec_.size(); ++i){ + if ((ctrlmask&i) == ctrlmask){ + for (unsigned qr_i = 0; qr_i < quregs.size(); ++qr_i){ + res[qr_i] = 0; + for (unsigned qb_i = 0; qb_i < quregs[qr_i].size(); ++qb_i) + res[qr_i] |= ((i >> quregs[qr_i][qb_i])&1) << qb_i; + } + f(res); + auto new_i = i; + for (unsigned qr_i = 0; qr_i < quregs.size(); ++qr_i){ + for (unsigned qb_i = 0; qb_i < quregs[qr_i].size(); ++qb_i){ + if (!(((new_i >> quregs[qr_i][qb_i])&1) == ((res[qr_i] >> qb_i)&1))) + new_i ^= (1UL << quregs[qr_i][qb_i]); + } + } + newvec[new_i] += vec_[i]; + } + else + newvec[i] += vec_[i]; + } } - vec_ = std::move(newvec); + std::swap(vec_, newvec); + std::swap(tmpBuff1_, newvec); + } + + // faster version without calling python + template + inline void emulate_math_addConstant(int a, const QuReg& quregs, const std::vector& ctrl) + { + emulate_math([a](std::vector &res){for(auto& x: res) x = x + a;}, quregs, ctrl, true); + } + + // faster version without calling python + template + inline void emulate_math_addConstantModN(int a, int N, const QuReg& quregs, const std::vector& ctrl) + { + emulate_math([a,N](std::vector &res){for(auto& x: res) x = (x + a) % N;}, quregs, ctrl, true); + } + + // faster version without calling python + template + inline void emulate_math_multiplyByConstantModN(int a, int N, const QuReg& quregs, const std::vector& ctrl) + { + emulate_math([a,N](std::vector &res){for(auto& x: res) x = (x * a) % N;}, quregs, ctrl, true); } calc_type get_expectation_value(TermsDict const& td, std::vector const& ids){ run(); calc_type expectation = 0.; - auto current_state = vec_; + + StateVector current_state; // avoid costly memory reallocations + if( tmpBuff1_.capacity() >= vec_.size() ) + std::swap(tmpBuff1_, current_state); + current_state.resize(vec_.size()); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < vec_.size(); ++i) + current_state[i] = vec_[i]; + for (auto const& term : td){ auto const& coefficient = term.second; apply_term(term.first, ids, {}); @@ -260,17 +312,29 @@ class Simulator{ auto const a2 = std::real(vec_[i]); auto const b2 = std::imag(vec_[i]); delta += a1 * a2 - b1 * b2; + // reset vec_ + vec_[i] = current_state[i]; } expectation += coefficient * delta; - vec_ = current_state; } + std::swap(current_state, tmpBuff1_); return expectation; } void apply_qubit_operator(ComplexTermsDict const& td, std::vector const& ids){ run(); - auto new_state = StateVector(vec_.size(), 0.); - auto current_state = vec_; + StateVector new_state, current_state; // avoid costly memory reallocations + if( tmpBuff1_.capacity() >= vec_.size() ) + std::swap(tmpBuff1_, new_state); + if( tmpBuff2_.capacity() >= vec_.size() ) + std::swap(tmpBuff2_, current_state); + new_state.resize(vec_.size()); + current_state.resize(vec_.size()); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < vec_.size(); ++i){ + new_state[i] = 0; + current_state[i] = vec_[i]; + } for (auto const& term : td){ auto const& coefficient = term.second; apply_term(term.first, ids, {}); @@ -280,7 +344,9 @@ class Simulator{ vec_[i] = current_state[i]; } } - vec_ = std::move(new_state); + std::swap(vec_, new_state); + std::swap(tmpBuff1_, new_state); + std::swap(tmpBuff2_, current_state); } calc_type get_probability(std::vector const& bit_string, @@ -452,6 +518,8 @@ class Simulator{ #pragma omp parallel kernel(vec_, ids[4], ids[3], ids[2], ids[1], ids[0], m, ctrlmask); break; + default: + throw std::invalid_argument("Gates with more than 5 qubits are not supported!"); } fused_gates_ = Fusion(); @@ -500,6 +568,12 @@ class Simulator{ unsigned fusion_qubits_min_, fusion_qubits_max_; RndEngine rnd_eng_; std::function rng_; + + // large array buffers to avoid costly reallocations + static StateVector tmpBuff1_, tmpBuff2_; }; +Simulator::StateVector Simulator::tmpBuff1_; +Simulator::StateVector Simulator::tmpBuff2_; + #endif diff --git a/projectq/backends/_sim/_cppsim.cpp b/projectq/backends/_sim/_cppsim.cpp index 74498d4e2..cab68d0ee 100755 --- a/projectq/backends/_sim/_cppsim.cpp +++ b/projectq/backends/_sim/_cppsim.cpp @@ -50,6 +50,9 @@ PYBIND11_PLUGIN(_cppsim) { .def("measure_qubits", &Simulator::measure_qubits_return) .def("apply_controlled_gate", &Simulator::apply_controlled_gate) .def("emulate_math", &emulate_math_wrapper) + .def("emulate_math_addConstant", &Simulator::emulate_math_addConstant) + .def("emulate_math_addConstantModN", &Simulator::emulate_math_addConstantModN) + .def("emulate_math_multiplyByConstantModN", &Simulator::emulate_math_multiplyByConstantModN) .def("get_expectation_value", &Simulator::get_expectation_value) .def("apply_qubit_operator", &Simulator::apply_qubit_operator) .def("emulate_time_evolution", &Simulator::emulate_time_evolution) diff --git a/projectq/backends/_sim/_simulator.py b/projectq/backends/_sim/_simulator.py index 2218c3471..19e884d6d 100755 --- a/projectq/backends/_sim/_simulator.py +++ b/projectq/backends/_sim/_simulator.py @@ -33,10 +33,12 @@ TimeEvolution) from projectq.types import WeakQubitRef +FALLBACK_TO_PYSIM = False try: from ._cppsim import Simulator as SimulatorBackend except ImportError: from ._pysim import Simulator as SimulatorBackend + FALLBACK_TO_PYSIM = True class Simulator(BasicEngine): @@ -384,14 +386,34 @@ def _handle(self, cmd): ID = cmd.qubits[0][0].id self._simulator.deallocate_qubit(ID) elif isinstance(cmd.gate, BasicMathGate): + # improve performance by using C++ code for some commomn gates + from projectq.libs.math import (AddConstant, + AddConstantModN, + MultiplyByConstantModN) qubitids = [] for qr in cmd.qubits: qubitids.append([]) for qb in qr: qubitids[-1].append(qb.id) - math_fun = cmd.gate.get_math_function(cmd.qubits) - self._simulator.emulate_math(math_fun, qubitids, - [qb.id for qb in cmd.control_qubits]) + if FALLBACK_TO_PYSIM: + math_fun = cmd.gate.get_math_function(cmd.qubits) + self._simulator.emulate_math(math_fun, qubitids, + [qb.id for qb in cmd.control_qubits]) + else: + # individual code for different standard gates to make it faster! + if isinstance(cmd.gate, AddConstant): + self._simulator.emulate_math_addConstant(cmd.gate.a, qubitids, + [qb.id for qb in cmd.control_qubits]) + elif isinstance(cmd.gate, AddConstantModN): + self._simulator.emulate_math_addConstantModN(cmd.gate.a, cmd.gate.N, qubitids, + [qb.id for qb in cmd.control_qubits]) + elif isinstance(cmd.gate, MultiplyByConstantModN): + self._simulator.emulate_math_multiplyByConstantModN(cmd.gate.a, cmd.gate.N, qubitids, + [qb.id for qb in cmd.control_qubits]) + else: + math_fun = cmd.gate.get_math_function(cmd.qubits) + self._simulator.emulate_math(math_fun, qubitids, + [qb.id for qb in cmd.control_qubits]) elif isinstance(cmd.gate, TimeEvolution): op = [(list(term), coeff) for (term, coeff) in cmd.gate.hamiltonian.terms.items()] diff --git a/projectq/backends/_sim/_simulator_test.py b/projectq/backends/_sim/_simulator_test.py index 9e301be0d..9f7d298cb 100755 --- a/projectq/backends/_sim/_simulator_test.py +++ b/projectq/backends/_sim/_simulator_test.py @@ -683,3 +683,55 @@ def receive(command_list): qubit1[0].id: qubit0[0].id} assert (sim._convert_logical_to_mapped_qureg(qubit0 + qubit1) == qubit1 + qubit0) + + +def test_simulator_constant_math_emulation(): + if "cpp_simulator" not in get_available_simulators(): + pytest.skip("No C++ simulator") + return + + results = [[[1, 1, 0, 0, 0]], [[0, 1, 0, 0, 0]], [[0, 1, 1, 1, 0]]] + + import projectq.backends._sim._simulator as _sim + from projectq.backends._sim._pysim import Simulator as PySim + from projectq.backends._sim._cppsim import Simulator as CppSim + from projectq.libs.math import (AddConstant, AddConstantModN, + MultiplyByConstantModN) + + def gate_filter(eng, cmd): + g = cmd.gate + if isinstance(g, BasicMathGate): + return False + return eng.next_engine.is_available(cmd) + + def run_simulation(sim): + eng = MainEngine(sim, []) + quint = eng.allocate_qureg(5) + AddConstant(3) | quint + All(Measure) | quint + eng.flush() + results[0].append([int(qb) for qb in quint]) + + AddConstantModN(4, 5) | quint + All(Measure) | quint + eng.flush() + results[1].append([int(qb) for qb in quint]) + + MultiplyByConstantModN(15, 16) | quint + All(Measure) | quint + eng.flush() + results[2].append([int(qb) for qb in quint]) + + cppsim = Simulator(gate_fusion=False) + cppsim._simulator = CppSim(1) + run_simulation(cppsim) + + _sim.FALLBACK_TO_PYSIM = True + pysim = Simulator() + pysim._simulator = PySim(1) + # run_simulation(pysim) + + for result in results: + ref = result[0] + for res in result[1:]: + assert ref == res diff --git a/projectq/cengines/_basicmapper.py b/projectq/cengines/_basicmapper.py index 4d4cef177..5fc0f9a81 100644 --- a/projectq/cengines/_basicmapper.py +++ b/projectq/cengines/_basicmapper.py @@ -81,3 +81,7 @@ def add_logical_id(command, old_tags=deepcopy(cmd.tags)): drop_engine_after(self) else: self.send([new_cmd]) + + def receive(self, command_list): + for cmd in command_list: + self._send_cmd_with_mapped_ids(cmd) diff --git a/projectq/cengines/_ibm5qubitmapper.py b/projectq/cengines/_ibm5qubitmapper.py index 7a2659a30..2c85749d2 100755 --- a/projectq/cengines/_ibm5qubitmapper.py +++ b/projectq/cengines/_ibm5qubitmapper.py @@ -11,12 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Contains a compiler engine to map to the 5-qubit IBM chip """ -from copy import deepcopy - import itertools from projectq.cengines import BasicMapperEngine @@ -39,8 +36,7 @@ class IBM5QubitMapper(BasicMapperEngine): without performing Swaps, the mapping procedure **raises an Exception**. """ - - def __init__(self): + def __init__(self, connections=None): """ Initialize an IBM 5-qubit mapper compiler engine. @@ -49,6 +45,16 @@ def __init__(self): BasicMapperEngine.__init__(self) self.current_mapping = dict() self._reset() + self._cmds = [] + self._interactions = dict() + + if connections is None: + #general connectivity easier for testing functions + self.connections = set([(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), + (2, 1), (2, 3), (2, 4), (3, 1), (3, 4), + (4, 3)]) + else: + self.connections = connections def is_available(self, cmd): """ @@ -67,17 +73,6 @@ def _reset(self): self._cmds = [] self._interactions = dict() - def _is_cnot(self, cmd): - """ - Check if the command corresponds to a CNOT (controlled NOT gate). - - Args: - cmd (Command): Command to check whether it is a controlled NOT - gate. - """ - return (isinstance(cmd.gate, NOT.__class__) and - get_control_count(cmd) == 1) - def _determine_cost(self, mapping): """ Determines the cost of the circuit with the given mapping. @@ -90,15 +85,15 @@ def _determine_cost(self, mapping): Cost measure taking into account CNOT directionality or None if the circuit cannot be executed given the mapping. """ - from projectq.setups.ibm import ibmqx4_connections as connections + cost = 0 for tpl in self._interactions: ctrl_id = tpl[0] target_id = tpl[1] ctrl_pos = mapping[ctrl_id] target_pos = mapping[target_id] - if not (ctrl_pos, target_pos) in connections: - if (target_pos, ctrl_pos) in connections: + if not (ctrl_pos, target_pos) in self.connections: + if (target_pos, ctrl_pos) in self.connections: cost += self._interactions[tpl] else: return None @@ -114,20 +109,22 @@ def _run(self): the mapping was already determined but more CNOTs get sent down the pipeline. """ - if (len(self.current_mapping) > 0 and - max(self.current_mapping.values()) > 4): + if (len(self.current_mapping) > 0 + and max(self.current_mapping.values()) > 4): raise RuntimeError("Too many qubits allocated. The IBM Q " "device supports at most 5 qubits and no " "intermediate measurements / " "reallocations.") if len(self._interactions) > 0: - logical_ids = [qbid for qbid in self.current_mapping] + logical_ids = list(self.current_mapping) best_mapping = self.current_mapping best_cost = None for physical_ids in itertools.permutations(list(range(5)), len(logical_ids)): - mapping = {logical_ids[i]: physical_ids[i] - for i in range(len(logical_ids))} + mapping = { + logical_ids[i]: physical_ids[i] + for i in range(len(logical_ids)) + } new_cost = self._determine_cost(mapping) if new_cost is not None: if best_cost is None or new_cost < best_cost: @@ -153,7 +150,7 @@ def _store(self, cmd): """ if not cmd.gate == FlushGate(): target = cmd.qubits[0][0].id - if self._is_cnot(cmd): + if _is_cnot(cmd): # CNOT encountered ctrl = cmd.control_qubits[0].id if not (ctrl, target) in self._interactions: @@ -187,3 +184,15 @@ def receive(self, command_list): if isinstance(cmd.gate, FlushGate): self._run() self._reset() + + +def _is_cnot(cmd): + """ + Check if the command corresponds to a CNOT (controlled NOT gate). + + Args: + cmd (Command): Command to check whether it is a controlled NOT + gate. + """ + return (isinstance(cmd.gate, NOT.__class__) + and get_control_count(cmd) == 1) diff --git a/projectq/cengines/_ibm5qubitmapper_test.py b/projectq/cengines/_ibm5qubitmapper_test.py index 5c4c4c4da..ea6d383b6 100755 --- a/projectq/cengines/_ibm5qubitmapper_test.py +++ b/projectq/cengines/_ibm5qubitmapper_test.py @@ -11,14 +11,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._ibm5qubitmapper.py.""" import pytest from projectq import MainEngine from projectq.cengines import DummyEngine -from projectq.ops import H, CNOT, X, Measure, All +from projectq.ops import H, CNOT, All from projectq.cengines import _ibm5qubitmapper, SwapAndCNOTFlipper from projectq.backends import IBMBackend @@ -28,15 +27,20 @@ def test_ibm5qubitmapper_is_available(monkeypatch): # Test that IBM5QubitMapper calls IBMBackend if gate is available. def mock_send(*args, **kwargs): return "Yes" + monkeypatch.setattr(_ibm5qubitmapper.IBMBackend, "is_available", mock_send) mapper = _ibm5qubitmapper.IBM5QubitMapper() assert mapper.is_available("TestCommand") == "Yes" def test_ibm5qubitmapper_invalid_circuit(): + connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=backend, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -51,9 +55,13 @@ def test_ibm5qubitmapper_invalid_circuit(): def test_ibm5qubitmapper_valid_circuit1(): + connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=backend, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -70,9 +78,13 @@ def test_ibm5qubitmapper_valid_circuit1(): def test_ibm5qubitmapper_valid_circuit2(): + connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=backend, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -89,6 +101,7 @@ def test_ibm5qubitmapper_valid_circuit2(): def test_ibm5qubitmapper_valid_circuit2_ibmqx4(): + connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) backend = DummyEngine(save_commands=True) class FakeIBMBackend(IBMBackend): @@ -99,8 +112,11 @@ class FakeIBMBackend(IBMBackend): fake.is_available = backend.is_available backend.is_last_engine = True - eng = MainEngine(backend=fake, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=fake, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -119,9 +135,12 @@ class FakeIBMBackend(IBMBackend): def test_ibm5qubitmapper_optimizeifpossible(): backend = DummyEngine(save_commands=True) connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper(), - SwapAndCNOTFlipper(connectivity)]) + eng = MainEngine( + backend=backend, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity), + SwapAndCNOTFlipper(connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -158,8 +177,10 @@ def test_ibm5qubitmapper_toomanyqubits(): backend = DummyEngine(save_commands=True) connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper(), - SwapAndCNOTFlipper(connectivity)]) + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(), + SwapAndCNOTFlipper(connectivity) + ]) qubits = eng.allocate_qureg(6) All(H) | qubits CNOT | (qubits[0], qubits[1]) diff --git a/projectq/cengines/_optimize.py b/projectq/cengines/_optimize.py index 2e72540b9..0c9765288 100755 --- a/projectq/cengines/_optimize.py +++ b/projectq/cengines/_optimize.py @@ -116,7 +116,7 @@ def _get_gate_indices(self, idx, i, IDs): def _optimize(self, idx, lim=None): """ - Try to merge or even cancel successive gates using the get_merged and + Try to remove identity gates using the is_identity function, then merge or even cancel successive gates using the get_merged and get_inverse functions of the gate (see, e.g., BasicRotationGate). It does so for all qubit command lists. @@ -130,6 +130,20 @@ def _optimize(self, idx, lim=None): new_gateloc = limit while i < limit - 1: + # can be dropped if the gate is equivalent to an identity gate + if self._l[idx][i].is_identity(): + # determine index of this gate on all qubits + qubitids = [qb.id for sublist in self._l[idx][i].all_qubits + for qb in sublist] + gid = self._get_gate_indices(idx, i, qubitids) + for j in range(len(qubitids)): + new_list = (self._l[qubitids[j]][0:gid[j]] + + self._l[qubitids[j]][gid[j] +1:]) + self._l[qubitids[j]] = new_list + i = 0 + limit -= 1 + continue + # can be dropped if two in a row are self-inverses inv = self._l[idx][i].get_inverse() diff --git a/projectq/cengines/_optimize_test.py b/projectq/cengines/_optimize_test.py index e0196f83b..121dbb471 100755 --- a/projectq/cengines/_optimize_test.py +++ b/projectq/cengines/_optimize_test.py @@ -16,6 +16,7 @@ import pytest +import math from projectq import MainEngine from projectq.cengines import DummyEngine from projectq.ops import (CNOT, H, Rx, Ry, AllocateQubitGate, X, @@ -127,3 +128,22 @@ def test_local_optimizer_mergeable_gates(): # Expect allocate, one Rx gate, and flush gate assert len(backend.received_commands) == 3 assert backend.received_commands[1].gate == Rx(10 * 0.5) + + +def test_local_optimizer_identity_gates(): + local_optimizer = _optimize.LocalOptimizer(m=4) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + # Test that it merges mergeable gates such as Rx + qb0 = eng.allocate_qubit() + for _ in range(10): + Rx(0.0) | qb0 + Ry(0.0) | qb0 + Rx(4*math.pi) | qb0 + Ry(4*math.pi) | qb0 + Rx(0.5) | qb0 + assert len(backend.received_commands) == 0 + eng.flush() + # Expect allocate, one Rx gate, and flush gate + assert len(backend.received_commands) == 3 + assert backend.received_commands[1].gate == Rx(0.5) diff --git a/projectq/cengines/_replacer/_replacer.py b/projectq/cengines/_replacer/_replacer.py index 29c883123..85fa303dc 100755 --- a/projectq/cengines/_replacer/_replacer.py +++ b/projectq/cengines/_replacer/_replacer.py @@ -175,7 +175,6 @@ def _process_command(self, cmd): # use decomposition chooser to determine the best decomposition chosen_decomp = self._decomp_chooser(cmd, decomp_list) - # the decomposed command must have the same tags # (plus the ones it gets from meta-statements inside the # decomposition rule). diff --git a/projectq/libs/math/_gates.py b/projectq/libs/math/_gates.py index fe1df6784..ef5cade99 100755 --- a/projectq/libs/math/_gates.py +++ b/projectq/libs/math/_gates.py @@ -90,6 +90,14 @@ class AddConstantModN(BasicMathGate): qunum = eng.allocate_qureg(5) # 5-qubit number X | qunum[1] # qunum is now equal to 2 AddConstantModN(3, 4) | qunum # qunum is now equal to 1 + + .. note:: + + Pre-conditions: + + * c < N + * c >= 0 + * The value stored in the quantum register must be lower than N """ def __init__(self, a, N): """ @@ -145,6 +153,14 @@ def SubConstantModN(a, N): qunum = eng.allocate_qureg(3) # 3-qubit number X | qunum[1] # qunum is now equal to 2 SubConstantModN(4,5) | qunum # qunum is now -2 = 6 = 1 (mod 5) + + .. note:: + + Pre-conditions: + + * c < N + * c >= 0 + * The value stored in the quantum register must be lower than N """ return AddConstantModN(N - a, N) @@ -162,6 +178,15 @@ class MultiplyByConstantModN(BasicMathGate): qunum = eng.allocate_qureg(5) # 5-qubit number X | qunum[2] # qunum is now equal to 4 MultiplyByConstantModN(3,5) | qunum # qunum is now 2. + + .. note:: + + Pre-conditions: + + * c < N + * c >= 0 + * gcd(c, N) == 1 + * The value stored in the quantum register must be lower than N """ def __init__(self, a, N): """ diff --git a/projectq/ops/__init__.py b/projectq/ops/__init__.py index db4a38b79..dd73cc2d5 100755 --- a/projectq/ops/__init__.py +++ b/projectq/ops/__init__.py @@ -25,6 +25,7 @@ from ._command import apply_command, Command from ._metagates import (DaggeredGate, get_inverse, + is_identity, ControlledGate, C, Tensor, @@ -37,3 +38,5 @@ from ._uniformly_controlled_rotation import (UniformlyControlledRy, UniformlyControlledRz) from ._state_prep import StatePreparation +from ._qpegate import QPE +from ._qaagate import QAA diff --git a/projectq/ops/_basics.py b/projectq/ops/_basics.py index 4bca84429..c7bdd31bc 100755 --- a/projectq/ops/_basics.py +++ b/projectq/ops/_basics.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Defines the BasicGate class, the base class of all gates, the BasicRotationGate class, the SelfInverseGate, the FastForwardingGate, the @@ -39,9 +38,10 @@ from projectq.types import BasicQubit from ._command import Command, apply_command +import unicodedata ANGLE_PRECISION = 12 -ANGLE_TOLERANCE = 10 ** -ANGLE_PRECISION +ANGLE_TOLERANCE = 10**-ANGLE_PRECISION RTOL = 1e-10 ATOL = 1e-12 @@ -157,7 +157,7 @@ def make_tuple_of_qureg(qubits): (or list of Qubits) objects. """ if not isinstance(qubits, tuple): - qubits = (qubits,) + qubits = (qubits, ) qubits = list(qubits) @@ -208,8 +208,9 @@ def __eq__(self, other): Equality comparision Return True if instance of the same class, unless other is an instance - of :class:MatrixGate, in which case equality is to be checked by testing - for existence and (approximate) equality of matrix representations. + of :class:MatrixGate, in which case equality is to be checked by + testing for existence and (approximate) equality of matrix + representations. """ if isinstance(other, self.__class__): return True @@ -224,9 +225,21 @@ def __ne__(self, other): def __str__(self): raise NotImplementedError('This gate does not implement __str__.') + def to_string(self, symbols): + """ + String representation + + Achieve same function as str() but can be extended for configurable + representation + """ + return str(self) + def __hash__(self): return hash(str(self)) + def is_identity(self): + return False + class MatrixGate(BasicGate): """ @@ -271,20 +284,19 @@ def __eq__(self, other): """ if not hasattr(other, 'matrix'): return False - if (not isinstance(self.matrix, np.matrix) or - not isinstance(other.matrix, np.matrix)): + if (not isinstance(self.matrix, np.matrix) + or not isinstance(other.matrix, np.matrix)): raise TypeError("One of the gates doesn't have the correct " "type (numpy.matrix) for the matrix " "attribute.") - if (self.matrix.shape == other.matrix.shape and - np.allclose(self.matrix, other.matrix, - rtol=RTOL, atol=ATOL, - equal_nan=False)): + if (self.matrix.shape == other.matrix.shape and np.allclose( + self.matrix, other.matrix, rtol=RTOL, atol=ATOL, + equal_nan=False)): return True return False def __str__(self): - return("MatrixGate(" + str(self.matrix.tolist()) + ")") + return ("MatrixGate(" + str(self.matrix.tolist()) + ")") def __hash__(self): return hash(str(self)) @@ -343,7 +355,23 @@ def __str__(self): [CLASSNAME]([ANGLE]) """ - return str(self.__class__.__name__) + "(" + str(self.angle) + ")" + return self.to_string() + + def to_string(self, symbols=False): + """ + Return the string representation of a BasicRotationGate. + + Args: + symbols (bool): uses the pi character and round the angle for a + more user friendly display if True, full angle + written in radian otherwise. + """ + if symbols: + angle = ("(" + str(round(self.angle / math.pi, 3)) + + unicodedata.lookup("GREEK SMALL LETTER PI") + ")") + else: + angle = "(" + str(self.angle) + ")" + return str(self.__class__.__name__) + angle def tex_str(self): """ @@ -355,7 +383,8 @@ def tex_str(self): [CLASSNAME]$_[ANGLE]$ """ - return str(self.__class__.__name__) + "$_{" + str(self.angle) + "}$" + return (str(self.__class__.__name__) + "$_{" + + str(round(self.angle / math.pi, 3)) + "\\pi}$") def get_inverse(self): """ @@ -401,6 +430,12 @@ def __ne__(self, other): def __hash__(self): return hash(str(self)) + def is_identity(self): + """ + Return True if the gate is equivalent to an Identity gate + """ + return self.angle == 0. or self.angle == 4 * math.pi + class BasicPhaseGate(BasicGate): """ @@ -597,6 +632,7 @@ def math_fun(a): def math_function(x): return list(math_fun(*x)) + self._math_function = math_function def __str__(self): diff --git a/projectq/ops/_basics_test.py b/projectq/ops/_basics_test.py index 80cc80183..a58a24e4c 100755 --- a/projectq/ops/_basics_test.py +++ b/projectq/ops/_basics_test.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2017 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,10 +12,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.ops._basics.""" -from copy import deepcopy import math import numpy as np @@ -49,13 +48,13 @@ def test_basic_gate_make_tuple_of_qureg(main_engine): qubit3 = Qubit(main_engine, 3) qureg = Qureg([qubit2, qubit3]) case1 = _basics.BasicGate.make_tuple_of_qureg(qubit0) - assert case1 == ([qubit0],) + assert case1 == ([qubit0], ) case2 = _basics.BasicGate.make_tuple_of_qureg([qubit0, qubit1]) - assert case2 == ([qubit0, qubit1],) + assert case2 == ([qubit0, qubit1], ) case3 = _basics.BasicGate.make_tuple_of_qureg(qureg) - assert case3 == (qureg,) - case4 = _basics.BasicGate.make_tuple_of_qureg((qubit0,)) - assert case4 == ([qubit0],) + assert case3 == (qureg, ) + case4 = _basics.BasicGate.make_tuple_of_qureg((qubit0, )) + assert case4 == ([qubit0], ) case5 = _basics.BasicGate.make_tuple_of_qureg((qureg, qubit0)) assert case5 == (qureg, [qubit0]) @@ -68,20 +67,15 @@ def test_basic_gate_generate_command(main_engine): qureg = Qureg([qubit2, qubit3]) basic_gate = _basics.BasicGate() command1 = basic_gate.generate_command(qubit0) - assert command1 == Command(main_engine, basic_gate, - ([qubit0],)) + assert command1 == Command(main_engine, basic_gate, ([qubit0], )) command2 = basic_gate.generate_command([qubit0, qubit1]) - assert command2 == Command(main_engine, basic_gate, - ([qubit0, qubit1],)) + assert command2 == Command(main_engine, basic_gate, ([qubit0, qubit1], )) command3 = basic_gate.generate_command(qureg) - assert command3 == Command(main_engine, basic_gate, - (qureg,)) - command4 = basic_gate.generate_command((qubit0,)) - assert command4 == Command(main_engine, basic_gate, - ([qubit0],)) + assert command3 == Command(main_engine, basic_gate, (qureg, )) + command4 = basic_gate.generate_command((qubit0, )) + assert command4 == Command(main_engine, basic_gate, ([qubit0], )) command5 = basic_gate.generate_command((qureg, qubit0)) - assert command5 == Command(main_engine, basic_gate, - (qureg, [qubit0])) + assert command5 == Command(main_engine, basic_gate, (qureg, [qubit0])) def test_basic_gate_or(): @@ -100,8 +94,8 @@ def test_basic_gate_or(): basic_gate | [qubit0, qubit1] command3 = basic_gate.generate_command(qureg) basic_gate | qureg - command4 = basic_gate.generate_command((qubit0,)) - basic_gate | (qubit0,) + command4 = basic_gate.generate_command((qubit0, )) + basic_gate | (qubit0, ) command5 = basic_gate.generate_command((qureg, qubit0)) basic_gate | (qureg, qubit0) received_commands = [] @@ -109,8 +103,9 @@ def test_basic_gate_or(): for cmd in saving_backend.received_commands: if not isinstance(cmd.gate, _basics.FastForwardingGate): received_commands.append(cmd) - assert received_commands == ([command1, command2, command3, command4, - command5]) + assert received_commands == ([ + command1, command2, command3, command4, command5 + ]) def test_basic_gate_compare(): @@ -163,15 +158,17 @@ def test_basic_rotation_gate_init(input_angle, modulo_angle): def test_basic_rotation_gate_str(): - basic_rotation_gate = _basics.BasicRotationGate(0.5) - assert str(basic_rotation_gate) == "BasicRotationGate(0.5)" + gate = _basics.BasicRotationGate(math.pi) + assert str(gate) == "BasicRotationGate(3.14159265359)" + assert gate.to_string(symbols=True) == u"BasicRotationGate(1.0Ï€)" + assert gate.to_string(symbols=False) == "BasicRotationGate(3.14159265359)" def test_basic_rotation_tex_str(): - basic_rotation_gate = _basics.BasicRotationGate(0.5) - assert basic_rotation_gate.tex_str() == "BasicRotationGate$_{0.5}$" - basic_rotation_gate = _basics.BasicRotationGate(4 * math.pi - 1e-13) - assert basic_rotation_gate.tex_str() == "BasicRotationGate$_{0.0}$" + gate = _basics.BasicRotationGate(0.5 * math.pi) + assert gate.tex_str() == "BasicRotationGate$_{0.5\\pi}$" + gate = _basics.BasicRotationGate(4 * math.pi - 1e-13) + assert gate.tex_str() == "BasicRotationGate$_{0.0\\pi}$" @pytest.mark.parametrize("input_angle, inverse_angle", @@ -194,6 +191,19 @@ def test_basic_rotation_gate_get_merged(): assert merged_gate == basic_rotation_gate3 +def test_basic_rotation_gate_is_identity(): + basic_rotation_gate1 = _basics.BasicRotationGate(0.) + basic_rotation_gate2 = _basics.BasicRotationGate(1. * math.pi) + basic_rotation_gate3 = _basics.BasicRotationGate(2. * math.pi) + basic_rotation_gate4 = _basics.BasicRotationGate(3. * math.pi) + basic_rotation_gate5 = _basics.BasicRotationGate(4. * math.pi) + assert basic_rotation_gate1.is_identity() + assert not basic_rotation_gate2.is_identity() + assert not basic_rotation_gate3.is_identity() + assert not basic_rotation_gate4.is_identity() + assert basic_rotation_gate5.is_identity() + + def test_basic_rotation_gate_comparison_and_hash(): basic_rotation_gate1 = _basics.BasicRotationGate(0.5) basic_rotation_gate2 = _basics.BasicRotationGate(0.5) @@ -316,10 +326,10 @@ def test_matrix_gate(): assert gate1 != gate6 assert gate3 != gate6 gate7 = gate5.get_inverse() - gate8 = _basics.MatrixGate([[1, 0], [0, (1+1j)/math.sqrt(2)]]) + gate8 = _basics.MatrixGate([[1, 0], [0, (1 + 1j) / math.sqrt(2)]]) assert gate7 == gate5 assert gate7 != gate8 - gate9 = _basics.MatrixGate([[1, 0], [0, (1-1j)/math.sqrt(2)]]) + gate9 = _basics.MatrixGate([[1, 0], [0, (1 - 1j) / math.sqrt(2)]]) gate10 = gate9.get_inverse() assert gate10 == gate8 assert gate3 == X diff --git a/projectq/ops/_command.py b/projectq/ops/_command.py index 5186502fa..f9268c420 100755 --- a/projectq/ops/_command.py +++ b/projectq/ops/_command.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ This file defines the apply_command function and the Command class. @@ -82,7 +81,6 @@ class Command(object): and hence adds its LoopTag to the end. all_qubits: A tuple of control_qubits + qubits """ - def __init__(self, engine, gate, qubits, controls=(), tags=()): """ Initialize a Command object. @@ -106,9 +104,10 @@ def __init__(self, engine, gate, qubits, controls=(), tags=()): tags (list[object]): Tags associated with the command. """ - qubits = tuple([WeakQubitRef(qubit.engine, qubit.id) - for qubit in qreg] - for qreg in qubits) + + qubits = tuple( + [WeakQubitRef(qubit.engine, qubit.id) for qubit in qreg] + for qreg in qubits) self.gate = gate self.tags = list(tags) @@ -126,11 +125,8 @@ def qubits(self, qubits): def __deepcopy__(self, memo): """ Deepcopy implementation. Engine should stay a reference.""" - return Command(self.engine, - deepcopy(self.gate), - self.qubits, - list(self.control_qubits), - deepcopy(self.tags)) + return Command(self.engine, deepcopy(self.gate), self.qubits, + list(self.control_qubits), deepcopy(self.tags)) def get_inverse(self): """ @@ -143,12 +139,19 @@ def get_inverse(self): NotInvertible: If the gate does not provide an inverse (see BasicGate.get_inverse) """ - return Command(self._engine, - projectq.ops.get_inverse(self.gate), - self.qubits, - list(self.control_qubits), + return Command(self._engine, projectq.ops.get_inverse(self.gate), + self.qubits, list(self.control_qubits), deepcopy(self.tags)) + def is_identity(self): + """ + Evaluate if the gate called in the command object is an identity gate. + + Returns: + True if the gate is equivalent to an Identity gate, False otherwise + """ + return projectq.ops.is_identity(self.gate) + def get_merged(self, other): """ Merge this command with another one and return the merged command @@ -161,12 +164,10 @@ def get_merged(self, other): NotMergeable: if the gates don't supply a get_merged()-function or can't be merged for other reasons. """ - if (self.tags == other.tags and self.all_qubits == other.all_qubits and - self.engine == other.engine): - return Command(self.engine, - self.gate.get_merged(other.gate), - self.qubits, - self.control_qubits, + if (self.tags == other.tags and self.all_qubits == other.all_qubits + and self.engine == other.engine): + return Command(self.engine, self.gate.get_merged(other.gate), + self.qubits, self.control_qubits, deepcopy(self.tags)) raise projectq.ops.NotMergeable("Commands not mergeable.") @@ -219,8 +220,9 @@ def control_qubits(self, qubits): Args: control_qubits (Qureg): quantum register """ - self._control_qubits = ([WeakQubitRef(qubit.engine, qubit.id) - for qubit in qubits]) + self._control_qubits = ([ + WeakQubitRef(qubit.engine, qubit.id) for qubit in qubits + ]) self._control_qubits = sorted(self._control_qubits, key=lambda x: x.id) def add_control_qubits(self, qubits): @@ -236,9 +238,9 @@ def add_control_qubits(self, qubits): gate, i.e., the gate is only executed if all qubits are in state 1. """ - assert(isinstance(qubits, list)) - self._control_qubits.extend([WeakQubitRef(qubit.engine, qubit.id) - for qubit in qubits]) + assert (isinstance(qubits, list)) + self._control_qubits.extend( + [WeakQubitRef(qubit.engine, qubit.id) for qubit in qubits]) self._control_qubits = sorted(self._control_qubits, key=lambda x: x.id) @property @@ -250,7 +252,7 @@ def all_qubits(self): WeakQubitRef objects) containing the control qubits and T[1:] contains the quantum registers to which the gate is applied. """ - return (self.control_qubits,) + self.qubits + return (self.control_qubits, ) + self.qubits @property def engine(self): @@ -285,11 +287,9 @@ def __eq__(self, other): Returns: True if Command objects are equal (same gate, applied to same qubits; ordered modulo interchangeability; and same tags) """ - if (isinstance(other, self.__class__) and - self.gate == other.gate and - self.tags == other.tags and - self.engine == other.engine and - self.all_qubits == other.all_qubits): + if (isinstance(other, self.__class__) and self.gate == other.gate + and self.tags == other.tags and self.engine == other.engine + and self.all_qubits == other.all_qubits): return True return False @@ -297,13 +297,16 @@ def __ne__(self, other): return not self.__eq__(other) def __str__(self): + return self.to_string() + + def to_string(self, symbols=False): """ Get string representation of this Command object. """ qubits = self.qubits ctrlqubits = self.control_qubits if len(ctrlqubits) > 0: - qubits = (self.control_qubits,) + qubits + qubits = (self.control_qubits, ) + qubits qstring = "" if len(qubits) == 1: qstring = str(Qureg(qubits[0])) @@ -314,4 +317,4 @@ def __str__(self): qstring += ", " qstring = qstring[:-2] + " )" cstring = "C" * len(ctrlqubits) - return cstring + str(self.gate) + " | " + qstring + return cstring + self.gate.to_string(symbols) + " | " + qstring diff --git a/projectq/ops/_command_test.py b/projectq/ops/_command_test.py index ae1407836..b0b4d54c8 100755 --- a/projectq/ops/_command_test.py +++ b/projectq/ops/_command_test.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2017 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +16,7 @@ """Tests for projectq.ops._command.""" from copy import deepcopy +import sys import math import pytest @@ -36,8 +38,8 @@ def test_command_init(main_engine): qureg0 = Qureg([Qubit(main_engine, 0)]) qureg1 = Qureg([Qubit(main_engine, 1)]) qureg2 = Qureg([Qubit(main_engine, 2)]) - qureg3 = Qureg([Qubit(main_engine, 3)]) - qureg4 = Qureg([Qubit(main_engine, 4)]) + # qureg3 = Qureg([Qubit(main_engine, 3)]) + # qureg4 = Qureg([Qubit(main_engine, 4)]) gate = BasicGate() cmd = _command.Command(main_engine, gate, (qureg0, qureg1, qureg2)) assert cmd.gate == gate @@ -133,6 +135,19 @@ def test_command_get_merged(main_engine): cmd.get_merged(cmd4) +def test_command_is_identity(main_engine): + qubit = main_engine.allocate_qubit() + qubit2 = main_engine.allocate_qubit() + cmd = _command.Command(main_engine, Rx(0.), (qubit,)) + cmd2 = _command.Command(main_engine, Rx(0.5), (qubit2,)) + inverse_cmd = cmd.get_inverse() + inverse_cmd2 = cmd2.get_inverse() + assert inverse_cmd.gate.is_identity() + assert cmd.gate.is_identity() + assert not inverse_cmd2.gate.is_identity() + assert not cmd2.gate.is_identity() + + def test_command_order_qubits(main_engine): qubit0 = Qureg([Qubit(main_engine, 0)]) qubit1 = Qureg([Qubit(main_engine, 1)]) @@ -232,9 +247,32 @@ def test_command_comparison(main_engine): def test_command_str(): qubit = Qureg([Qubit(main_engine, 0)]) ctrl_qubit = Qureg([Qubit(main_engine, 1)]) - cmd = _command.Command(main_engine, Rx(0.5), (qubit,)) + cmd = _command.Command(main_engine, Rx(0.5*math.pi), (qubit,)) cmd.tags = ["TestTag"] cmd.add_control_qubits(ctrl_qubit) - assert str(cmd) == "CRx(0.5) | ( Qureg[1], Qureg[0] )" - cmd2 = _command.Command(main_engine, Rx(0.5), (qubit,)) - assert str(cmd2) == "Rx(0.5) | Qureg[0]" + cmd2 = _command.Command(main_engine, Rx(0.5*math.pi), (qubit,)) + if sys.version_info.major == 3: + assert cmd.to_string(symbols=False) == "CRx(1.570796326795) | ( Qureg[1], Qureg[0] )" + assert str(cmd2) == "Rx(1.570796326795) | Qureg[0]" + else: + assert cmd.to_string(symbols=False) == "CRx(1.5707963268) | ( Qureg[1], Qureg[0] )" + assert str(cmd2) == "Rx(1.5707963268) | Qureg[0]" + + +def test_command_to_string(): + qubit = Qureg([Qubit(main_engine, 0)]) + ctrl_qubit = Qureg([Qubit(main_engine, 1)]) + cmd = _command.Command(main_engine, Rx(0.5*math.pi), (qubit,)) + cmd.tags = ["TestTag"] + cmd.add_control_qubits(ctrl_qubit) + cmd2 = _command.Command(main_engine, Rx(0.5*math.pi), (qubit,)) + + assert cmd.to_string(symbols=True) == u"CRx(0.5Ï€) | ( Qureg[1], Qureg[0] )" + assert cmd2.to_string(symbols=True) == u"Rx(0.5Ï€) | Qureg[0]" + if sys.version_info.major == 3: + assert cmd.to_string(symbols=False) == "CRx(1.570796326795) | ( Qureg[1], Qureg[0] )" + assert cmd2.to_string(symbols=False) == "Rx(1.570796326795) | Qureg[0]" + else: + assert cmd.to_string(symbols=False) == "CRx(1.5707963268) | ( Qureg[1], Qureg[0] )" + assert cmd2.to_string(symbols=False) == "Rx(1.5707963268) | Qureg[0]" + diff --git a/projectq/ops/_gates.py b/projectq/ops/_gates.py index d524e4b3b..be2240d00 100755 --- a/projectq/ops/_gates.py +++ b/projectq/ops/_gates.py @@ -16,17 +16,29 @@ Contains definitions of standard gates such as * Hadamard (H) * Pauli-X (X / NOT) +* Pauli-Y (Y) * Pauli-Z (Z) +* S and its inverse (S / Sdagger) * T and its inverse (T / Tdagger) +* SqrtX gate (SqrtX) * Swap gate (Swap) +* SqrtSwap gate (SqrtSwap) +* Entangle (Entangle) * Phase gate (Ph) +* Rotation-X (Rx) +* Rotation-Y (Ry) * Rotation-Z (Rz) +* Rotation-XX on two qubits (Rxx) +* Rotation-YY on two qubits (Ryy) +* Rotation-ZZ on two qubits (Rzz) * Phase-shift (R) * Measurement (Measure) and meta gates, i.e., * Allocate / Deallocate qubits * Flush gate (end of circuit) +* Barrier +* FlipBits """ import math @@ -110,7 +122,7 @@ def __str__(self): #: Shortcut (instance of) :class:`projectq.ops.SGate` S = SGate() -#: Shortcut (instance of) :class:`projectq.ops.SGate` +#: Inverse (and shortcut) of :class:`projectq.ops.SGate` Sdag = Sdagger = get_inverse(S) @@ -125,7 +137,7 @@ def __str__(self): #: Shortcut (instance of) :class:`projectq.ops.TGate` T = TGate() -#: Shortcut (instance of) :class:`projectq.ops.TGate` +#: Inverse (and shortcut) of :class:`projectq.ops.TGate` Tdag = Tdagger = get_inverse(T) @@ -145,10 +157,9 @@ def __str__(self): SqrtX = SqrtXGate() -class SwapGate(SelfInverseGate, BasicMathGate): +class SwapGate(SelfInverseGate): """ Swap gate class (swaps 2 qubits) """ def __init__(self): - BasicMathGate.__init__(self, lambda x, y: (y, x)) SelfInverseGate.__init__(self) self.interchangeable_qubit_indices = [[0, 1]] @@ -217,7 +228,7 @@ def matrix(self): class Ry(BasicRotationGate): - """ RotationX gate class """ + """ RotationY gate class """ @property def matrix(self): return np.matrix([[math.cos(0.5 * self.angle), @@ -234,6 +245,36 @@ def matrix(self): [0, cmath.exp(.5 * 1j * self.angle)]]) +class Rxx(BasicRotationGate): + """ RotationXX gate class """ + @property + def matrix(self): + return np.matrix([[cmath.cos(.5 * self.angle), 0, 0, -1j*cmath.sin(.5 * self.angle)], + [0, cmath.cos( .5 * self.angle), -1j*cmath.sin(.5 * self.angle), 0], + [0, -1j*cmath.sin(.5 * self.angle), cmath.cos( .5 * self.angle), 0], + [-1j*cmath.sin(.5 * self.angle), 0, 0, cmath.cos( .5 * self.angle)]]) + + +class Ryy(BasicRotationGate): + """ RotationYY gate class """ + @property + def matrix(self): + return np.matrix([[cmath.cos(.5 * self.angle), 0, 0, 1j*cmath.sin(.5 * self.angle)], + [0, cmath.cos( .5 * self.angle), -1j*cmath.sin(.5 * self.angle), 0], + [0, -1j*cmath.sin(.5 * self.angle), cmath.cos( .5 * self.angle), 0], + [1j*cmath.sin(.5 * self.angle), 0, 0, cmath.cos( .5 * self.angle)]]) + + +class Rzz(BasicRotationGate): + """ RotationZZ gate class """ + @property + def matrix(self): + return np.matrix([[cmath.exp(-.5 * 1j * self.angle), 0, 0, 0], + [0, cmath.exp( .5 * 1j * self.angle), 0, 0], + [0, 0, cmath.exp( .5 * 1j * self.angle), 0], + [0, 0, 0, cmath.exp(-.5 * 1j * self.angle)]]) + + class R(BasicPhaseGate): """ Phase-shift gate (equivalent to Rz up to a global phase) """ @property diff --git a/projectq/ops/_gates_test.py b/projectq/ops/_gates_test.py index efcd63b0a..88efa3a19 100755 --- a/projectq/ops/_gates_test.py +++ b/projectq/ops/_gates_test.py @@ -156,6 +156,42 @@ def test_rz(angle): assert np.allclose(gate.matrix, expected_matrix) +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, + 4 * math.pi]) +def test_rxx(angle): + gate = _gates.Rxx(angle) + expected_matrix = np.matrix([[cmath.cos(.5 * angle), 0, 0, -1j * cmath.sin(.5 * angle)], + [0, cmath.cos(.5 * angle), -1j * cmath.sin(.5 * angle), 0], + [0, -1j * cmath.sin(.5 * angle), cmath.cos(.5 * angle), 0], + [-1j * cmath.sin(.5 * angle), 0, 0, cmath.cos(.5 * angle)]]) + assert gate.matrix.shape == expected_matrix.shape + assert np.allclose(gate.matrix, expected_matrix) + + +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, + 4 * math.pi]) +def test_ryy(angle): + gate = _gates.Ryy(angle) + expected_matrix = np.matrix([[cmath.cos(.5 * angle), 0, 0, 1j * cmath.sin(.5 * angle)], + [0, cmath.cos(.5 * angle), -1j * cmath.sin(.5 * angle), 0], + [0, -1j * cmath.sin(.5 * angle), cmath.cos(.5 * angle), 0], + [ 1j * cmath.sin(.5 * angle), 0, 0, cmath.cos(.5 * angle)]]) + assert gate.matrix.shape == expected_matrix.shape + assert np.allclose(gate.matrix, expected_matrix) + + +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, + 4 * math.pi]) +def test_rzz(angle): + gate = _gates.Rzz(angle) + expected_matrix = np.matrix([[cmath.exp(-.5 * 1j * angle), 0, 0, 0], + [0, cmath.exp( .5 * 1j * angle), 0, 0], + [0, 0, cmath.exp( .5 * 1j * angle), 0], + [0, 0, 0, cmath.exp(-.5 * 1j * angle)]]) + assert gate.matrix.shape == expected_matrix.shape + assert np.allclose(gate.matrix, expected_matrix) + + @pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi]) def test_ph(angle): gate = _gates.Ph(angle) diff --git a/projectq/ops/_metagates.py b/projectq/ops/_metagates.py old mode 100755 new mode 100644 index b2e7959fe..cca5e7412 --- a/projectq/ops/_metagates.py +++ b/projectq/ops/_metagates.py @@ -132,6 +132,22 @@ def get_inverse(gate): except NotInvertible: return DaggeredGate(gate) +def is_identity(gate): + """ + Return True if the gate is an identity gate. + + Tries to call gate.is_identity and, upon failure, returns False + + Args: + gate: Gate of which to get the inverse + + Example: + .. code-block:: python + + get_inverse(Rx(2*math.pi)) # returns True + get_inverse(Rx(math.pi)) # returns False + """ + return gate.is_identity() class ControlledGate(BasicGate): """ diff --git a/projectq/ops/_metagates_test.py b/projectq/ops/_metagates_test.py index c42393b06..8632a99e5 100755 --- a/projectq/ops/_metagates_test.py +++ b/projectq/ops/_metagates_test.py @@ -122,6 +122,16 @@ def test_get_inverse(): inv2 = _metagates.get_inverse(invertible_gate) assert inv2 == Y +def test_is_identity(): + # Choose gate which is not an identity gate: + non_identity_gate=Rx(0.5) + assert not non_identity_gate.is_identity() + assert not _metagates.is_identity(non_identity_gate) + # Choose gate which is an identity gate: + identity_gate=Rx(0.) + assert identity_gate.is_identity() + assert _metagates.is_identity(identity_gate) + def test_controlled_gate_init(): one_control = _metagates.ControlledGate(Y, 1) diff --git a/projectq/ops/_qaagate.py b/projectq/ops/_qaagate.py new file mode 100755 index 000000000..751b9dc60 --- /dev/null +++ b/projectq/ops/_qaagate.py @@ -0,0 +1,81 @@ +# Copyright 2019 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ._basics import BasicGate + + +class QAA(BasicGate): + """ + Quantum Aplitude Aplification gate. + + (Quick reference https://en.wikipedia.org/wiki/Amplitude_amplification. + Complete reference G. Brassard, P. Hoyer, M. Mosca, A. Tapp (2000) + Quantum Amplitude Amplification and Estimation + https://arxiv.org/abs/quant-ph/0005055) + + Quantum Amplitude Amplification (QAA) executes the algorithm, but not + the final measurement required to obtain the marked state(s) with high + probability. The starting state on wich the QAA algorithm is executed + is the one resulting of aplying the Algorithm on the |0> state. + + Example: + .. code-block:: python + + def func_algorithm(eng,system_qubits): + All(H) | system_qubits + + def func_oracle(eng,system_qubits,qaa_ancilla): + # This oracle selects the state |010> as the one marked + with Compute(eng): + All(X) | system_qubits[0::2] + with Control(eng, system_qubits): + X | qaa_ancilla + Uncompute(eng) + + system_qubits = eng.allocate_qureg(3) + # Prepare the qaa_ancilla qubit in the |-> state + qaa_ancilla = eng.allocate_qubit() + X | qaa_ancilla + H | qaa_ancilla + + # Creates the initial state form the Algorithm + func_algorithm(eng, system_qubits) + # Apply Quantum Amplitude Amplification the correct number of times + num_it = int(math.pi/4.*math.sqrt(1 << 3)) + with Loop(eng, num_it): + QAA(func_algorithm, func_oracle) | (system_qubits, qaa_ancilla) + + All(Measure) | system_qubits + + Warning: + No qubit allocation/deallocation may take place during the call + to the defined Algorithm :code:`func_algorithm` + + Attributes: + func_algorithm: Algorithm that initialite the state and to be used + in the QAA algorithm + func_oracle: The Oracle that marks the state(s) as "good" + system_qubits: the system we are interested on + qaa_ancilla: auxiliary qubit that helps to invert the amplitude of the + "good" states + + """ + def __init__(self, algorithm, oracle): + BasicGate.__init__(self) + self.algorithm = algorithm + self.oracle = oracle + + def __str__(self): + return 'QAA(Algorithm = {0}, Oracle = {1})'.format( + str(self.algorithm.__name__), str(self.oracle.__name__)) diff --git a/projectq/ops/_qaagate_test.py b/projectq/ops/_qaagate_test.py new file mode 100755 index 000000000..3e15e6801 --- /dev/null +++ b/projectq/ops/_qaagate_test.py @@ -0,0 +1,27 @@ +# Copyright 2019 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for projectq.ops._qaagate.""" + +from projectq.ops import _qaagate, All, H, X + + +def test_qaa_str(): + + def func_algorithm(): All(H) + + def func_oracle(): All(X) + + gate = _qaagate.QAA(func_algorithm, func_oracle) + assert str(gate) == "QAA(Algorithm = func_algorithm, Oracle = func_oracle)" diff --git a/projectq/ops/_qpegate.py b/projectq/ops/_qpegate.py new file mode 100755 index 000000000..08beee743 --- /dev/null +++ b/projectq/ops/_qpegate.py @@ -0,0 +1,29 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ._basics import BasicGate + + +class QPE(BasicGate): + """ + Quantum Phase Estimation gate. + + See setups.decompositions for the complete implementation + """ + def __init__(self, unitary): + BasicGate.__init__(self) + self.unitary = unitary + + def __str__(self): + return 'QPE({})'.format(str(self.unitary)) diff --git a/projectq/ops/_qpegate_test.py b/projectq/ops/_qpegate_test.py new file mode 100755 index 000000000..5ffcbf185 --- /dev/null +++ b/projectq/ops/_qpegate_test.py @@ -0,0 +1,23 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for projectq.ops._qpegate.""" + +from projectq.ops import _qpegate, X + + +def test_qpe_str(): + unitary = X + gate = _qpegate.QPE(unitary) + assert str(gate) == "QPE(X)" diff --git a/projectq/setups/decompositions/__init__.py b/projectq/setups/decompositions/__init__.py index aab71b28c..de557a065 100755 --- a/projectq/setups/decompositions/__init__.py +++ b/projectq/setups/decompositions/__init__.py @@ -16,22 +16,27 @@ barrier, carb1qubit2cnotrzandry, crz2cxandrz, + cnot2rxx, cnot2cz, cnu2toffoliandcu, entangle, globalphase, + h2rx, ph2r, qubitop2onequbit, qft2crandhadamard, r2rzandph, rx2rz, ry2rz, + rz2rx, sqrtswap2cnot, stateprep2cnot, swap2cnot, toffoli2cnotandtgate, time_evolution, - uniformlycontrolledr2cnot) + uniformlycontrolledr2cnot, + phaseestimation, + amplitudeamplification) all_defined_decomposition_rules = [ rule @@ -39,21 +44,26 @@ barrier, carb1qubit2cnotrzandry, crz2cxandrz, + cnot2rxx, cnot2cz, cnu2toffoliandcu, entangle, globalphase, + h2rx, ph2r, qubitop2onequbit, qft2crandhadamard, r2rzandph, rx2rz, ry2rz, + rz2rx, sqrtswap2cnot, stateprep2cnot, swap2cnot, toffoli2cnotandtgate, time_evolution, - uniformlycontrolledr2cnot] + uniformlycontrolledr2cnot, + phaseestimation, + amplitudeamplification] for rule in module.all_defined_decomposition_rules ] diff --git a/projectq/setups/decompositions/amplitudeamplification.py b/projectq/setups/decompositions/amplitudeamplification.py new file mode 100644 index 000000000..517aadeed --- /dev/null +++ b/projectq/setups/decompositions/amplitudeamplification.py @@ -0,0 +1,111 @@ +# Copyright 2019 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Registers a decomposition for quantum amplitude amplification. + +(Quick reference https://en.wikipedia.org/wiki/Amplitude_amplification. +Complete reference G. Brassard, P. Hoyer, M. Mosca, A. Tapp (2000) +Quantum Amplitude Amplification and Estimation +https://arxiv.org/abs/quant-ph/0005055) + +Quantum Amplitude Amplification (QAA) executes the algorithm, but not +the final measurement required to obtain the marked state(s) with high +probability. The starting state on wich the QAA algorithm is executed +is the one resulting of aplying the Algorithm on the |0> state. + +Example: + .. code-block:: python + + def func_algorithm(eng,system_qubits): + All(H) | system_qubits + + def func_oracle(eng,system_qubits,qaa_ancilla): + # This oracle selects the state |010> as the one marked + with Compute(eng): + All(X) | system_qubits[0::2] + with Control(eng, system_qubits): + X | qaa_ancilla + Uncompute(eng) + + system_qubits = eng.allocate_qureg(3) + # Prepare the qaa_ancilla qubit in the |-> state + qaa_ancilla = eng.allocate_qubit() + X | qaa_ancilla + H | qaa_ancilla + + # Creates the initial state form the Algorithm + func_algorithm(eng, system_qubits) + # Apply Quantum Amplitude Amplification the correct number of times + num_it = int(math.pi/4.*math.sqrt(1 << 3)) + with Loop(eng, num_it): + QAA(func_algorithm, func_oracle) | (system_qubits, qaa_ancilla) + + All(Measure) | system_qubits + +Warning: + No qubit allocation/deallocation may take place during the call + to the defined Algorithm :code:`func_algorithm` + +Attributes: + func_algorithm: Algorithm that initialite the state and to be used + in the QAA algorithm + func_oracle: The Oracle that marks the state(s) as "good" + system_qubits: the system we are interested on + qaa_ancilla: auxiliary qubit that helps to invert the amplitude of the + "good" states + +""" + +import math +import numpy as np + +from projectq.cengines import DecompositionRule +from projectq.meta import Control, Compute, Uncompute, CustomUncompute, Dagger +from projectq.ops import X, Z, Ph, All + +from projectq.ops import QAA + + +def _decompose_QAA(cmd): + """ Decompose the Quantum Amplitude Apmplification algorithm as a gate. """ + eng = cmd.engine + + # System-qubit is the first qubit/qureg. Ancilla qubit is the second qubit + system_qubits = cmd.qubits[0] + qaa_ancilla = cmd.qubits[1] + + # The Oracle and the Algorithm + Oracle = cmd.gate.oracle + A = cmd.gate.algorithm + + # Apply the oracle to invert the amplitude of the good states, S_Chi + Oracle(eng, system_qubits, qaa_ancilla) + + # Apply the inversion of the Algorithm, + # the inversion of the aplitude of |0> and the Algorithm + + with Compute(eng): + with Dagger(eng): + A(eng, system_qubits) + All(X) | system_qubits + with Control(eng, system_qubits[0:-1]): + Z | system_qubits[-1] + with CustomUncompute(eng): + All(X) | system_qubits + A(eng, system_qubits) + Ph(math.pi) | system_qubits[0] + + +#: Decomposition rules +all_defined_decomposition_rules = [DecompositionRule(QAA, _decompose_QAA)] diff --git a/projectq/setups/decompositions/amplitudeamplification_test.py b/projectq/setups/decompositions/amplitudeamplification_test.py new file mode 100644 index 000000000..f99681713 --- /dev/null +++ b/projectq/setups/decompositions/amplitudeamplification_test.py @@ -0,0 +1,182 @@ +# Copyright 2019 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.amplitudeamplification.py." + +import math +import pytest + +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import (AutoReplacer, DecompositionRuleSet, MainEngine) + +from projectq.ops import (X, H, Ry, All, Measure) +from projectq.meta import Loop, Control, Compute, Uncompute + +from projectq.ops import QAA +from projectq.setups.decompositions import amplitudeamplification as aa + + +def hache_algorithm(eng, qreg): + All(H) | qreg + + +def simple_oracle(eng, system_q, control): + # This oracle selects the state |1010101> as the one marked + with Compute(eng): + All(X) | system_q[1::2] + with Control(eng, system_q): + X | control + Uncompute(eng) + + +def test_simple_grover(): + rule_set = DecompositionRuleSet(modules=[aa]) + + eng = MainEngine(backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + ]) + + system_qubits = eng.allocate_qureg(7) + + # Prepare the control qubit in the |-> state + control = eng.allocate_qubit() + X | control + H | control + + # Creates the initial state form the Algorithm + hache_algorithm(eng, system_qubits) + + # Get the amplitude of the marked state before the AA + # to calculate the number of iterations + eng.flush() + prob1010101 = eng.backend.get_probability('1010101', system_qubits) + + total_amp_before = math.sqrt(prob1010101) + theta_before = math.asin(total_amp_before) + + # Apply Quantum Amplitude Amplification the correct number of times + # Theta is calculated previously using get_probability + # We calculate also the theoretical final probability + # of getting the good state + num_it = int(math.pi / (4. * theta_before) + 1) + theoretical_prob = math.sin((2 * num_it + 1.) * theta_before)**2 + with Loop(eng, num_it): + QAA(hache_algorithm, simple_oracle) | (system_qubits, control) + + # Get the probabilty of getting the marked state after the AA + # to compare with the theoretical probability after teh AA + eng.flush() + prob1010101 = eng.backend.get_probability('1010101', system_qubits) + total_prob_after = prob1010101 + + All(Measure) | system_qubits + H | control + Measure | control + result = [int(q) for q in system_qubits] + control_result = int(control) + + eng.flush() + + assert total_prob_after == pytest.approx(theoretical_prob, abs=1e-6), ( + "The obtained probability is less than expected %f vs. %f" % + (total_prob_after, theoretical_prob)) + + +def complex_algorithm(eng, qreg): + All(H) | qreg + with Control(eng, qreg[0]): + All(X) | qreg[1:] + All(Ry(math.pi / 4)) | qreg[1:] + with Control(eng, qreg[-1]): + All(X) | qreg[1:-1] + + +def complex_oracle(eng, system_q, control): + # This oracle selects the subspace |000000>+|111111> as the good one + with Compute(eng): + with Control(eng, system_q[0]): + All(X) | system_q[1:] + H | system_q[0] + All(X) | system_q + + with Control(eng, system_q): + X | control + + Uncompute(eng) + + +def test_complex_aa(): + rule_set = DecompositionRuleSet(modules=[aa]) + + eng = MainEngine(backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + ]) + + system_qubits = eng.allocate_qureg(6) + + # Prepare the control qubit in the |-> state + control = eng.allocate_qubit() + X | control + H | control + + # Creates the initial state form the Algorithm + complex_algorithm(eng, system_qubits) + + # Get the probabilty of getting the marked state before the AA + # to calculate the number of iterations + eng.flush() + prob000000 = eng.backend.get_probability('000000', system_qubits) + prob111111 = eng.backend.get_probability('111111', system_qubits) + + total_amp_before = math.sqrt(prob000000 + prob111111) + theta_before = math.asin(total_amp_before) + + # Apply Quantum Amplitude Amplification the correct number of times + # Theta is calculated previously using get_probability + # We calculate also the theoretical final probability + # of getting the good state + num_it = int(math.pi / (4. * theta_before) + 1) + theoretical_prob = math.sin((2 * num_it + 1.) * theta_before)**2 + with Loop(eng, num_it): + QAA(complex_algorithm, complex_oracle) | (system_qubits, control) + + # Get the probabilty of getting the marked state after the AA + # to compare with the theoretical probability after the AA + eng.flush() + prob000000 = eng.backend.get_probability('000000', system_qubits) + prob111111 = eng.backend.get_probability('111111', system_qubits) + total_prob_after = prob000000 + prob111111 + + All(Measure) | system_qubits + H | control + Measure | control + result = [int(q) for q in system_qubits] + control_result = int(control) + + eng.flush() + + assert total_prob_after == pytest.approx(theoretical_prob, abs=1e-2), ( + "The obtained probability is less than expected %f vs. %f" % + (total_prob_after, theoretical_prob)) + + +def test_string_functions(): + algorithm = hache_algorithm + oracle = simple_oracle + gate = QAA(algorithm, oracle) + assert (str(gate) == + "QAA(Algorithm = hache_algorithm, Oracle = simple_oracle)") diff --git a/projectq/setups/decompositions/cnot2rxx.py b/projectq/setups/decompositions/cnot2rxx.py new file mode 100644 index 000000000..a1fa2e6ac --- /dev/null +++ b/projectq/setups/decompositions/cnot2rxx.py @@ -0,0 +1,61 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Module uses ideas from "Basic circuit compilation techniques +# for an ion-trap quantum machine" by Dmitri Maslov (2017) at +# https://iopscience.iop.org/article/10.1088/1367-2630/aa5e47 +""" +Registers a decomposition to for a CNOT gate in terms of Rxx, Rx and Ry gates. +""" + +from projectq.cengines import DecompositionRule +from projectq.meta import get_control_count +from projectq.ops import Ph, Rxx, Ry, Rx, X +import math + + +def _decompose_cnot2rxx_M(cmd): + """ Decompose CNOT gate into Rxx gate. """ + # Labelled 'M' for 'minus' because decomposition ends with a Ry(-pi/2) + ctrl = cmd.control_qubits + Ry(math.pi / 2) | ctrl[0] + Ph(7 * math.pi / 4) | ctrl[0] + Rx(-math.pi / 2) | ctrl[0] + Rx(-math.pi / 2) | cmd.qubits[0][0] + Rxx(math.pi / 2) | (ctrl[0], cmd.qubits[0][0]) + Ry(-1 * math.pi / 2) | ctrl[0] + + +def _decompose_cnot2rxx_P(cmd): + """ Decompose CNOT gate into Rxx gate. """ + # Labelled 'P' for 'plus' because decomposition ends with a Ry(+pi/2) + ctrl = cmd.control_qubits + Ry(-math.pi / 2) | ctrl[0] + Ph(math.pi / 4) | ctrl[0] + Rx(-math.pi / 2) | ctrl[0] + Rx(math.pi / 2) | cmd.qubits[0][0] + Rxx(math.pi / 2) | (ctrl[0], cmd.qubits[0][0]) + Ry(math.pi / 2) | ctrl[0] + + +def _recognize_cnot2(cmd): + """ Identify that the command is a CNOT gate (control - X gate)""" + return get_control_count(cmd) == 1 + + +#: Decomposition rules +all_defined_decomposition_rules = [ + DecompositionRule(X.__class__, _decompose_cnot2rxx_M, _recognize_cnot2), + DecompositionRule(X.__class__, _decompose_cnot2rxx_P, _recognize_cnot2) +] diff --git a/projectq/setups/decompositions/cnot2rxx_test.py b/projectq/setups/decompositions/cnot2rxx_test.py new file mode 100644 index 000000000..bc0d0c077 --- /dev/null +++ b/projectq/setups/decompositions/cnot2rxx_test.py @@ -0,0 +1,124 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.cnot2rxx.py." + +import pytest +import numpy as np + +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import (AutoReplacer, DecompositionRuleSet, DummyEngine, + InstructionFilter) +from projectq.meta import Control +from projectq.ops import All, CNOT, CZ, Measure, X, Z + +from . import cnot2rxx + + +def test_recognize_correct_gates(): + """Test that recognize_cnot recognizes cnot gates. """ + saving_backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=saving_backend) + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qubit3 = eng.allocate_qubit() + eng.flush() + # Create a control function in 3 different ways + CZ | (qubit1, qubit2) + with Control(eng, qubit2): + Z | qubit1 + X | qubit1 + with Control(eng, qubit2 + qubit3): + Z | qubit1 + eng.flush() + eng.flush(deallocate_qubits=True) + for cmd in saving_backend.received_commands[4:7]: + assert cnot2rxx._recognize_cnot2(cmd) + for cmd in saving_backend.received_commands[7:9]: + assert not cnot2rxx._recognize_cnot2(cmd) + + +def _decomp_gates(eng, cmd): + """ Test that the cmd.gate is a gate of class X """ + if len(cmd.control_qubits) == 1 and isinstance(cmd.gate, X.__class__): + return False + return True + + +# ------------test_decomposition function-------------# +# Creates two engines, correct_eng and test_eng. +# correct_eng implements CNOT gate. +# test_eng implements the decomposition of the CNOT gate. +# correct_qb and test_qb represent results of these two engines, respectively. +# +# The decomposition in this case only produces the same state as CNOT up to a +# global phase. +# test_vector and correct_vector represent the final wave states of correct_qb +# and test_qb. +# +# The dot product of correct_vector and test_vector should have absolute value +# 1, if the two vectors are the same up to a global phase. + + +def test_decomposition(): + """ Test that this decomposition of CNOT produces correct amplitudes + + Function tests each DecompositionRule in + cnot2rxx.all_defined_decomposition_rules + """ + decomposition_rule_list = cnot2rxx.all_defined_decomposition_rules + for rule in decomposition_rule_list: + for basis_state_index in range(0, 4): + basis_state = [0] * 4 + basis_state[basis_state_index] = 1. + correct_dummy_eng = DummyEngine(save_commands=True) + correct_eng = MainEngine(backend=Simulator(), + engine_list=[correct_dummy_eng]) + rule_set = DecompositionRuleSet(rules=[rule]) + test_dummy_eng = DummyEngine(save_commands=True) + test_eng = MainEngine(backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(_decomp_gates), + test_dummy_eng + ]) + test_sim = test_eng.backend + correct_sim = correct_eng.backend + correct_qb = correct_eng.allocate_qubit() + correct_ctrl_qb = correct_eng.allocate_qubit() + correct_eng.flush() + test_qb = test_eng.allocate_qubit() + test_ctrl_qb = test_eng.allocate_qubit() + test_eng.flush() + + correct_sim.set_wavefunction(basis_state, + correct_qb + correct_ctrl_qb) + test_sim.set_wavefunction(basis_state, test_qb + test_ctrl_qb) + CNOT | (test_ctrl_qb, test_qb) + CNOT | (correct_ctrl_qb, correct_qb) + + test_eng.flush() + correct_eng.flush() + + assert len(correct_dummy_eng.received_commands) == 5 + assert len(test_dummy_eng.received_commands) == 10 + + assert correct_eng.backend.cheat()[1] == pytest.approx( + test_eng.backend.cheat()[1], rel=1e-12, abs=1e-12) + + All(Measure) | test_qb + test_ctrl_qb + All(Measure) | correct_qb + correct_ctrl_qb + test_eng.flush(deallocate_qubits=True) + correct_eng.flush(deallocate_qubits=True) diff --git a/projectq/setups/decompositions/h2rx.py b/projectq/setups/decompositions/h2rx.py new file mode 100644 index 000000000..b54533bad --- /dev/null +++ b/projectq/setups/decompositions/h2rx.py @@ -0,0 +1,57 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Module uses ideas from "Basic circuit compilation techniques for an +# ion-trap quantum machine" by Dmitri Maslov (2017) at +# https://iopscience.iop.org/article/10.1088/1367-2630/aa5e47 +""" +Registers a decomposition for the H gate into an Ry and Rx gate. +""" + +import math + +from projectq.cengines import DecompositionRule +from projectq.meta import get_control_count +from projectq.ops import Ph, Rx, Ry, H + + +def _decompose_h2rx_M(cmd): + """ Decompose the Ry gate.""" + # Labelled 'M' for 'minus' because decomposition ends with a Ry(-pi/2) + qubit = cmd.qubits[0] + Rx(math.pi) | qubit + Ph(math.pi/2) | qubit + Ry(-1 * math.pi / 2) | qubit + + +def _decompose_h2rx_N(cmd): + """ Decompose the Ry gate.""" + # Labelled 'N' for 'neutral' because decomposition doesn't end with + # Ry(pi/2) or Ry(-pi/2) + qubit = cmd.qubits[0] + Ry(math.pi / 2) | qubit + Ph(3*math.pi/2) | qubit + Rx(-1 * math.pi) | qubit + + +def _recognize_HNoCtrl(cmd): + """ For efficiency reasons only if no control qubits.""" + return get_control_count(cmd) == 0 + + +#: Decomposition rules +all_defined_decomposition_rules = [ + DecompositionRule(H.__class__, _decompose_h2rx_N, _recognize_HNoCtrl), + DecompositionRule(H.__class__, _decompose_h2rx_M, _recognize_HNoCtrl) +] diff --git a/projectq/setups/decompositions/h2rx_test.py b/projectq/setups/decompositions/h2rx_test.py new file mode 100644 index 000000000..2df048801 --- /dev/null +++ b/projectq/setups/decompositions/h2rx_test.py @@ -0,0 +1,117 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.h2rx.py" + +import numpy as np + +import pytest + +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import (AutoReplacer, DecompositionRuleSet, DummyEngine, + InstructionFilter) +from projectq.meta import Control +from projectq.ops import Measure, X, H, HGate + +from . import h2rx + + +def test_recognize_correct_gates(): + """ Test that recognize_HNoCtrl recognizes ctrl qubits """ + saving_backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=saving_backend) + qubit = eng.allocate_qubit() + ctrl_qubit = eng.allocate_qubit() + eng.flush() + H | qubit + with Control(eng, ctrl_qubit): + H | qubit + eng.flush(deallocate_qubits=True) + assert h2rx._recognize_HNoCtrl(saving_backend.received_commands[3]) + assert not h2rx._recognize_HNoCtrl(saving_backend.received_commands[4]) + + +def h_decomp_gates(eng, cmd): + """ Test that cmd.gate is a gate of class HGate """ + g = cmd.gate + if isinstance(g, HGate): # H is just a shortcut to HGate + return False + else: + return True + + +# ------------test_decomposition function-------------# +# Creates two engines, correct_eng and test_eng. +# correct_eng implements H gate. +# test_eng implements the decomposition of the H gate. +# correct_qb and test_qb represent results of these two engines, respectively. +# +# The decomposition in this case only produces the same state as H up to a +# global phase. +# test_vector and correct_vector represent the final wave states of correct_qb +# and test_qb. +# The dot product of correct_vector and test_vector should have absolute value +# 1, if the two vectors are the same up to a global phase. + + +def test_decomposition(): + """ Test that this decomposition of H produces correct amplitudes + + Function tests each DecompositionRule in + h2rx.all_defined_decomposition_rules + """ + decomposition_rule_list = h2rx.all_defined_decomposition_rules + for rule in decomposition_rule_list: + for basis_state_index in range(2): + basis_state = [0] * 2 + basis_state[basis_state_index] = 1. + + correct_dummy_eng = DummyEngine(save_commands=True) + correct_eng = MainEngine(backend=Simulator(), + engine_list=[correct_dummy_eng]) + + rule_set = DecompositionRuleSet(rules=[rule]) + test_dummy_eng = DummyEngine(save_commands=True) + test_eng = MainEngine(backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(h_decomp_gates), + test_dummy_eng + ]) + + correct_qb = correct_eng.allocate_qubit() + correct_eng.flush() + test_qb = test_eng.allocate_qubit() + test_eng.flush() + + correct_eng.backend.set_wavefunction(basis_state, correct_qb) + test_eng.backend.set_wavefunction(basis_state, test_qb) + + H | correct_qb + H | test_qb + + correct_eng.flush() + test_eng.flush() + + assert H in (cmd.gate + for cmd in correct_dummy_eng.received_commands) + assert H not in (cmd.gate + for cmd in test_dummy_eng.received_commands) + + assert correct_eng.backend.cheat()[1] == pytest.approx( + test_eng.backend.cheat()[1], rel=1e-12, abs=1e-12) + + Measure | test_qb + Measure | correct_qb diff --git a/projectq/setups/decompositions/phaseestimation.py b/projectq/setups/decompositions/phaseestimation.py new file mode 100644 index 000000000..faf7523cf --- /dev/null +++ b/projectq/setups/decompositions/phaseestimation.py @@ -0,0 +1,129 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Registers a decomposition for phase estimation. + +(reference https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm) + +The Quantum Phase Estimation (QPE) executes the algorithm up to the inverse +QFT included. The following steps measuring the ancillas and computing the +phase should be executed outside of the QPE. + +The decomposition uses as ancillas (qpe_ancillas) the first qubit/qureg in +the Command and as system qubits teh second qubit/qureg in the Command. + +The unitary operator for which the phase estimation is estimated (unitary) +is the gate in Command + +Example: + .. code-block:: python + + # Example using a ProjectQ gate + + n_qpe_ancillas = 3 + qpe_ancillas = eng.allocate_qureg(n_qpe_ancillas) + system_qubits = eng.allocate_qureg(1) + angle = cmath.pi*2.*0.125 + U = Ph(angle) # unitary_specfic_to_the_problem() + + # Apply Quantum Phase Estimation + QPE(U) | (qpe_ancillas, system_qubits) + + All(Measure) | qpe_ancillas + # Compute the phase from the ancilla measurement + #(https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm) + phasebinlist = [int(q) for q in qpe_ancillas] + phase_in_bin = ''.join(str(j) for j in phasebinlist) + phase_int = int(phase_in_bin,2) + phase = phase_int / (2 ** n_qpe_ancillas) + print (phase) + + # Example using a function (two_qubit_gate). + # Instead of applying QPE on a gate U one could provide a function + + def two_qubit_gate(system_q, time): + CNOT | (system_q[0], system_q[1]) + Ph(2.0*cmath.pi*(time * 0.125)) | system_q[1] + CNOT | (system_q[0], system_q[1]) + + n_qpe_ancillas = 3 + qpe_ancillas = eng.allocate_qureg(n_qpe_ancillas) + system_qubits = eng.allocate_qureg(2) + X | system_qubits[0] + + # Apply Quantum Phase Estimation + QPE(two_qubit_gate) | (qpe_ancillas, system_qubits) + + All(Measure) | qpe_ancillas + # Compute the phase from the ancilla measurement + #(https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm) + phasebinlist = [int(q) for q in qpe_ancillas] + phase_in_bin = ''.join(str(j) for j in phasebinlist) + phase_int = int(phase_in_bin,2) + phase = phase_int / (2 ** n_qpe_ancillas) + print (phase) + +Attributes: + unitary (BasicGate): Unitary Operation either a ProjectQ gate or a function f. + Calling the function with the parameters system_qubits(Qureg) and time (integer), + i.e. f(system_qubits, time), applies to the system qubits a unitary defined in f + with parameter time. + + +""" + +import numpy as np + +from projectq.cengines import DecompositionRule +from projectq.meta import Control, Loop, get_control_count +from projectq.ops import H, Tensor, get_inverse, QFT + +from projectq.ops import QPE + + +def _decompose_QPE(cmd): + """ Decompose the Quantum Phase Estimation gate. """ + eng = cmd.engine + + # Ancillas is the first qubit/qureg. System-qubit is the second qubit/qureg + qpe_ancillas = cmd.qubits[0] + system_qubits = cmd.qubits[1] + + # Hadamard on the ancillas + Tensor(H) | qpe_ancillas + + # The Unitary Operator + U = cmd.gate.unitary + + # Control U on the system_qubits + if (callable(U)): + # If U is a function + for i in range(len(qpe_ancillas)): + with Control(eng, qpe_ancillas[i]): + U(system_qubits, time=2**i) + else: + for i in range(len(qpe_ancillas)): + ipower = int(2**i) + with Loop(eng, ipower): + with Control(eng, qpe_ancillas[i]): + U | system_qubits + + # Inverse QFT on the ancillas + get_inverse(QFT) | qpe_ancillas + +#: Decomposition rules +all_defined_decomposition_rules = [ + DecompositionRule(QPE, _decompose_QPE) +] diff --git a/projectq/setups/decompositions/phaseestimation_test.py b/projectq/setups/decompositions/phaseestimation_test.py new file mode 100644 index 000000000..1b8f8e63c --- /dev/null +++ b/projectq/setups/decompositions/phaseestimation_test.py @@ -0,0 +1,162 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.phaseestimation.py." + +import cmath +import numpy as np +import pytest + +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import (AutoReplacer, DecompositionRuleSet, + DummyEngine, InstructionFilter, MainEngine) + +from projectq.ops import X, H, All, Measure, Tensor, Ph, CNOT, StatePreparation + +from projectq.ops import (BasicGate) + +from projectq.ops import QPE +from projectq.setups.decompositions import phaseestimation as pe +from projectq.setups.decompositions import qft2crandhadamard as dqft +import projectq.setups.decompositions.stateprep2cnot as stateprep2cnot +import projectq.setups.decompositions.uniformlycontrolledr2cnot as ucr2cnot + + +def test_simple_test_X_eigenvectors(): + rule_set = DecompositionRuleSet(modules=[pe, dqft]) + eng = MainEngine(backend=Simulator(), + engine_list=[AutoReplacer(rule_set), + ]) + results = np.array([]) + for i in range(100): + autovector = eng.allocate_qureg(1) + X | autovector + H | autovector + unit = X + ancillas = eng.allocate_qureg(1) + QPE(unit) | (ancillas, autovector) + All(Measure) | ancillas + fasebinlist = [int(q) for q in ancillas] + fasebin = ''.join(str(j) for j in fasebinlist) + faseint = int(fasebin, 2) + phase = faseint / (2. ** (len(ancillas))) + results = np.append(results, phase) + All(Measure) | autovector + eng.flush() + + num_phase = (results == 0.5).sum() + assert num_phase/100. >= 0.35, "Statistics phase calculation are not correct (%f vs. %f)" % (num_phase/100., 0.35) + + +def test_Ph_eigenvectors(): + rule_set = DecompositionRuleSet(modules=[pe, dqft]) + eng = MainEngine(backend=Simulator(), + engine_list=[AutoReplacer(rule_set), + ]) + results = np.array([]) + for i in range(100): + autovector = eng.allocate_qureg(1) + theta = cmath.pi*2.*0.125 + unit = Ph(theta) + ancillas = eng.allocate_qureg(3) + QPE(unit) | (ancillas, autovector) + All(Measure) | ancillas + fasebinlist = [int(q) for q in ancillas] + fasebin = ''.join(str(j) for j in fasebinlist) + faseint = int(fasebin, 2) + phase = faseint / (2. ** (len(ancillas))) + results = np.append(results, phase) + All(Measure) | autovector + eng.flush() + + num_phase = (results == 0.125).sum() + assert num_phase/100. >= 0.35, "Statistics phase calculation are not correct (%f vs. %f)" % (num_phase/100., 0.35) + + +def two_qubit_gate(system_q, time): + CNOT | (system_q[0], system_q[1]) + Ph(2.0*cmath.pi*(time * 0.125)) | system_q[1] + CNOT | (system_q[0], system_q[1]) + + +def test_2qubitsPh_andfunction_eigenvectors(): + rule_set = DecompositionRuleSet(modules=[pe, dqft]) + eng = MainEngine(backend=Simulator(), + engine_list=[AutoReplacer(rule_set), + ]) + results = np.array([]) + for i in range(100): + autovector = eng.allocate_qureg(2) + X | autovector[0] + ancillas = eng.allocate_qureg(3) + QPE(two_qubit_gate) | (ancillas, autovector) + All(Measure) | ancillas + fasebinlist = [int(q) for q in ancillas] + fasebin = ''.join(str(j) for j in fasebinlist) + faseint = int(fasebin, 2) + phase = faseint / (2. ** (len(ancillas))) + results = np.append(results, phase) + All(Measure) | autovector + eng.flush() + + num_phase = (results == 0.125).sum() + assert num_phase/100. >= 0.34, "Statistics phase calculation are not correct (%f vs. %f)" % (num_phase/100., 0.34) + + +def test_X_no_eigenvectors(): + rule_set = DecompositionRuleSet(modules=[pe, dqft, stateprep2cnot, ucr2cnot]) + eng = MainEngine(backend=Simulator(), + engine_list=[AutoReplacer(rule_set), + ]) + results = np.array([]) + results_plus = np.array([]) + results_minus = np.array([]) + for i in range(100): + autovector = eng.allocate_qureg(1) + amplitude0 = (np.sqrt(2) + np.sqrt(6))/4. + amplitude1 = (np.sqrt(2) - np.sqrt(6))/4. + StatePreparation([amplitude0, amplitude1]) | autovector + unit = X + ancillas = eng.allocate_qureg(1) + QPE(unit) | (ancillas, autovector) + All(Measure) | ancillas + fasebinlist = [int(q) for q in ancillas] + fasebin = ''.join(str(j) for j in fasebinlist) + faseint = int(fasebin, 2) + phase = faseint / (2. ** (len(ancillas))) + results = np.append(results, phase) + Tensor(H) | autovector + if np.allclose(phase, .0, rtol=1e-1): + results_plus = np.append(results_plus, phase) + All(Measure) | autovector + autovector_result = int(autovector) + assert autovector_result == 0 + elif np.allclose(phase, .5, rtol=1e-1): + results_minus = np.append(results_minus, phase) + All(Measure) | autovector + autovector_result = int(autovector) + assert autovector_result == 1 + eng.flush() + + total = len(results_plus) + len(results_minus) + plus_probability = len(results_plus)/100. + assert total == pytest.approx(100, abs=5) + assert plus_probability == pytest.approx(1./4., abs = 1e-1), "Statistics on |+> probability are not correct (%f vs. %f)" % (plus_probability, 1./4.) + + +def test_string(): + unit = X + gate = QPE(unit) + assert (str(gate) == "QPE(X)") diff --git a/projectq/setups/decompositions/rz2rx.py b/projectq/setups/decompositions/rz2rx.py new file mode 100644 index 000000000..f49ba72e1 --- /dev/null +++ b/projectq/setups/decompositions/rz2rx.py @@ -0,0 +1,67 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Module uses ideas from "Basic circuit compilation techniques for an +# ion-trap quantum machine" by Dmitri Maslov (2017) at +# https://iopscience.iop.org/article/10.1088/1367-2630/aa5e47 +""" +Registers a decomposition for the Rz gate into an Rx and Ry(pi/2) or Ry(-pi/2) +gate +""" + +import math + +from projectq.cengines import DecompositionRule +from projectq.meta import Compute, Control, get_control_count, Uncompute +from projectq.ops import Rx, Ry, Rz + + +def _decompose_rz2rx_P(cmd): + """ Decompose the Rz using negative angle. """ + # Labelled 'P' for 'plus' because decomposition ends with a Ry(+pi/2) + qubit = cmd.qubits[0] + eng = cmd.engine + angle = cmd.gate.angle + + with Control(eng, cmd.control_qubits): + with Compute(eng): + Ry(-math.pi / 2.) | qubit + Rx(-angle) | qubit + Uncompute(eng) + + +def _decompose_rz2rx_M(cmd): + """ Decompose the Rz using positive angle. """ + # Labelled 'M' for 'minus' because decomposition ends with a Ry(-pi/2) + qubit = cmd.qubits[0] + eng = cmd.engine + angle = cmd.gate.angle + + with Control(eng, cmd.control_qubits): + with Compute(eng): + Ry(math.pi / 2.) | qubit + Rx(angle) | qubit + Uncompute(eng) + + +def _recognize_RzNoCtrl(cmd): + """ Decompose the gate only if the command represents a single qubit gate (if it is not part of a control gate).""" + return get_control_count(cmd) == 0 + + +#: Decomposition rules +all_defined_decomposition_rules = [ + DecompositionRule(Rz, _decompose_rz2rx_P, _recognize_RzNoCtrl), + DecompositionRule(Rz, _decompose_rz2rx_M, _recognize_RzNoCtrl) +] diff --git a/projectq/setups/decompositions/rz2rx_test.py b/projectq/setups/decompositions/rz2rx_test.py new file mode 100644 index 000000000..7c6c9962f --- /dev/null +++ b/projectq/setups/decompositions/rz2rx_test.py @@ -0,0 +1,125 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.rz2rx.py" + +import math +import numpy as np +import pytest + +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import (AutoReplacer, DecompositionRuleSet, DummyEngine, + InstructionFilter) +from projectq.meta import Control +from projectq.ops import Measure, Rz + +from . import rz2rx + + +def test_recognize_correct_gates(): + """ Test that recognize_RzNoCtrl recognizes ctrl qubits """ + saving_backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=saving_backend) + qubit = eng.allocate_qubit() + ctrl_qubit = eng.allocate_qubit() + eng.flush() + Rz(0.3) | qubit + with Control(eng, ctrl_qubit): + Rz(0.4) | qubit + eng.flush(deallocate_qubits=True) + assert rz2rx._recognize_RzNoCtrl(saving_backend.received_commands[3]) + assert not rz2rx._recognize_RzNoCtrl(saving_backend.received_commands[4]) + + +def rz_decomp_gates(eng, cmd): + """ Test that cmd.gate is the gate Rz """ + g = cmd.gate + if isinstance(g, Rz): + return False + else: + return True + + +# ------------test_decomposition function-------------# +# Creates two engines, correct_eng and test_eng. +# correct_eng implements Rz(angle) gate. +# test_eng implements the decomposition of the Rz(angle) gate. +# correct_qb and test_qb represent results of these two engines, respectively. +# +# The decomposition only needs to produce the same state in a qubit up to a +# global phase. +# test_vector and correct_vector represent the final wave states of correct_qb +# and test_qb. +# +# The dot product of correct_vector and test_vector should have absolute value +# 1, if the two vectors are the same up to a global phase. + + +@pytest.mark.parametrize("angle", [0, math.pi, 2 * math.pi, 4 * math.pi, 0.5]) +def test_decomposition(angle): + """ + Test that this decomposition of Rz produces correct amplitudes + + Note that this function tests each DecompositionRule in + rz2rx.all_defined_decomposition_rules + """ + decomposition_rule_list = rz2rx.all_defined_decomposition_rules + for rule in decomposition_rule_list: + for basis_state in ([1, 0], [0, 1]): + correct_dummy_eng = DummyEngine(save_commands=True) + correct_eng = MainEngine(backend=Simulator(), + engine_list=[correct_dummy_eng]) + + rule_set = DecompositionRuleSet(rules=[rule]) + test_dummy_eng = DummyEngine(save_commands=True) + test_eng = MainEngine(backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(rz_decomp_gates), + test_dummy_eng + ]) + + correct_qb = correct_eng.allocate_qubit() + Rz(angle) | correct_qb + correct_eng.flush() + + test_qb = test_eng.allocate_qubit() + Rz(angle) | test_qb + test_eng.flush() + + # Create empty vectors for the wave vectors for the correct and + # test qubits + correct_vector = np.zeros((2, 1), dtype=np.complex_) + test_vector = np.zeros((2, 1), dtype=np.complex_) + + i = 0 + for fstate in ['0', '1']: + test = test_eng.backend.get_amplitude(fstate, test_qb) + correct = correct_eng.backend.get_amplitude(fstate, correct_qb) + correct_vector[i] = correct + test_vector[i] = test + i += 1 + + # Necessary to transpose vector to use matrix dot product + test_vector = test_vector.transpose() + # Remember that transposed vector should come first in product + vector_dot_product = np.dot(test_vector, correct_vector) + + assert np.absolute(vector_dot_product) == pytest.approx(1, + rel=1e-12, + abs=1e-12) + + Measure | test_qb + Measure | correct_qb diff --git a/projectq/setups/ibm.py b/projectq/setups/ibm.py index a5fb2c802..acedeed00 100755 --- a/projectq/setups/ibm.py +++ b/projectq/setups/ibm.py @@ -11,46 +11,116 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +""" +Defines a setup allowing to compile code for the IBM quantum chips: +->Any 5 qubit devices +->the ibmq online simulator +->the melbourne 15 qubit device +It provides the `engine_list` for the `MainEngine' based on the requested +device. Decompose the circuit into a Rx/Ry/Rz/H/CNOT gate set that will be +translated in the backend in the U1/U2/U3/CX gate set. """ -Defines a setup useful for the IBM QE chip with 5 qubits. -It provides the `engine_list` for the `MainEngine`, and contains an -AutoReplacer with most of the gate decompositions of ProjectQ, among others -it includes: +import projectq +import projectq.setups.decompositions +from projectq.setups import restrictedgateset +from projectq.ops import (Rx, Ry, Rz, H, CNOT, Barrier) +from projectq.cengines import (LocalOptimizer, IBM5QubitMapper, + SwapAndCNOTFlipper, BasicMapperEngine, + GridMapper) +from projectq.backends._ibm._ibm_http_client import show_devices - * Controlled z-rotations --> Controlled NOTs and single-qubit rotations - * Toffoli gate --> CNOT and single-qubit gates - * m-Controlled global phases --> (m-1)-controlled phase-shifts - * Global phases --> ignore - * (controlled) Swap gates --> CNOTs and Toffolis - * Arbitrary single qubit gates --> Rz and Ry - * Controlled arbitrary single qubit gates --> Rz, Ry, and CNOT gates -Moreover, it contains `LocalOptimizers` and a custom mapper for the CNOT -gates. +def get_engine_list(token=None, device=None): + # Access to the hardware properties via show_devices + # Can also be extended to take into account gate fidelities, new available + # gate, etc.. + devices = show_devices(token) + ibm_setup = [] + if device not in devices: + raise DeviceOfflineError('Error when configuring engine list: device ' + 'requested for Backend not connected') + if devices[device]['nq'] == 5: + # The requested device is a 5 qubit processor + # Obtain the coupling map specific to the device + coupling_map = devices[device]['coupling_map'] + coupling_map = list2set(coupling_map) + mapper = IBM5QubitMapper(coupling_map) + ibm_setup = [ + mapper, + SwapAndCNOTFlipper(coupling_map), + LocalOptimizer(10) + ] + elif device == 'ibmq_qasm_simulator': + # The 32 qubit online simulator doesn't need a specific mapping for + # gates. Can also run wider gateset but this setup keep the + # restrictedgateset setup for coherence + mapper = BasicMapperEngine() + # Note: Manual Mapper doesn't work, because its map is updated only if + # gates are applied if gates in the register are not used, then it + # will lead to state errors + res = dict() + for i in range(devices[device]['nq']): + res[i] = i + mapper.current_mapping = res + ibm_setup = [mapper] + elif device == 'ibmq_16_melbourne': + # Only 15 qubits available on this ibmqx2 unit(in particular qubit 7 + # on the grid), therefore need custom grid mapping + grid_to_physical = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 15, + 8: 14, + 9: 13, + 10: 12, + 11: 11, + 12: 10, + 13: 9, + 14: 8, + 15: 7 + } + coupling_map = devices[device]['coupling_map'] + coupling_map = list2set(coupling_map) + ibm_setup = [ + GridMapper(2, 8, grid_to_physical), + LocalOptimizer(5), + SwapAndCNOTFlipper(coupling_map), + LocalOptimizer(5) + ] + else: + # If there is an online device not handled into ProjectQ it's not too + # bad, the engine_list can be constructed manually with the + # appropriate mapper and the 'coupling_map' parameter + raise DeviceNotHandledError('Device not yet fully handled by ProjectQ') -""" + # Most IBM devices accept U1,U2,U3,CX gates. + # Most gates need to be decomposed into a subset that is manually converted + # in the backend (until the implementation of the U1,U2,U3) + # available gates decomposable now for U1,U2,U3: Rx,Ry,Rz and H + setup = restrictedgateset.get_engine_list(one_qubit_gates=(Rx, Ry, Rz, H), + two_qubit_gates=(CNOT, ), + other_gates=(Barrier, )) + setup.extend(ibm_setup) + return setup -import projectq -import projectq.setups.decompositions -from projectq.cengines import (TagRemover, - LocalOptimizer, - AutoReplacer, - IBM5QubitMapper, - SwapAndCNOTFlipper, - DecompositionRuleSet) - - -ibmqx4_connections = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) - - -def get_engine_list(): - rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions]) - return [TagRemover(), - LocalOptimizer(10), - AutoReplacer(rule_set), - TagRemover(), - IBM5QubitMapper(), - SwapAndCNOTFlipper(ibmqx4_connections), - LocalOptimizer(10)] + +class DeviceOfflineError(Exception): + pass + + +class DeviceNotHandledError(Exception): + pass + + +def list2set(coupling_list): + result = [] + for el in coupling_list: + result.append(tuple(el)) + return set(result) diff --git a/projectq/setups/ibm_test.py b/projectq/setups/ibm_test.py index 598b949cb..26b41b24a 100644 --- a/projectq/setups/ibm_test.py +++ b/projectq/setups/ibm_test.py @@ -13,17 +13,60 @@ # limitations under the License. """Tests for projectq.setup.ibm.""" -import projectq -from projectq import MainEngine -from projectq.cengines import IBM5QubitMapper, SwapAndCNOTFlipper +import pytest -def test_ibm_cnot_mapper_in_cengines(): +def test_ibm_cnot_mapper_in_cengines(monkeypatch): import projectq.setups.ibm - found = 0 - for engine in projectq.setups.ibm.get_engine_list(): - if isinstance(engine, IBM5QubitMapper): - found |= 1 - if isinstance(engine, SwapAndCNOTFlipper): - found |= 2 - assert found == 3 + + def mock_show_devices(*args, **kwargs): + connections = set([(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), (2, 1), + (2, 3), (2, 4), (3, 1), (3, 4), (4, 3)]) + return { + 'ibmq_burlington': { + 'coupling_map': connections, + 'version': '0.0.0', + 'nq': 5 + }, + 'ibmq_16_melbourne': { + 'coupling_map': connections, + 'version': '0.0.0', + 'nq': 15 + }, + 'ibmq_qasm_simulator': { + 'coupling_map': connections, + 'version': '0.0.0', + 'nq': 32 + } + } + + monkeypatch.setattr(projectq.setups.ibm, "show_devices", mock_show_devices) + engines_5qb = projectq.setups.ibm.get_engine_list(device='ibmq_burlington') + engines_15qb = projectq.setups.ibm.get_engine_list( + device='ibmq_16_melbourne') + engines_simulator = projectq.setups.ibm.get_engine_list( + device='ibmq_qasm_simulator') + assert len(engines_5qb) == 15 + assert len(engines_15qb) == 16 + assert len(engines_simulator) == 13 + + +def test_ibm_errors(monkeypatch): + import projectq.setups.ibm + + def mock_show_devices(*args, **kwargs): + connections = set([(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), (2, 1), + (2, 3), (2, 4), (3, 1), (3, 4), (4, 3)]) + return { + 'ibmq_imaginary': { + 'coupling_map': connections, + 'version': '0.0.0', + 'nq': 6 + } + } + + monkeypatch.setattr(projectq.setups.ibm, "show_devices", mock_show_devices) + with pytest.raises(projectq.setups.ibm.DeviceOfflineError): + projectq.setups.ibm.get_engine_list(device='ibmq_burlington') + with pytest.raises(projectq.setups.ibm.DeviceNotHandledError): + projectq.setups.ibm.get_engine_list(device='ibmq_imaginary') diff --git a/projectq/setups/restrictedgateset.py b/projectq/setups/restrictedgateset.py index 7b3540cf0..fe0c00ba2 100644 --- a/projectq/setups/restrictedgateset.py +++ b/projectq/setups/restrictedgateset.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Defines a setup to compile to a restricted gate set. @@ -27,8 +26,7 @@ import projectq.libs.math import projectq.setups.decompositions from projectq.cengines import (AutoReplacer, DecompositionRuleSet, - InstructionFilter, LocalOptimizer, - TagRemover) + InstructionFilter, LocalOptimizer, TagRemover) from projectq.ops import (BasicGate, BasicMathGate, ClassicalInstructionGate, CNOT, ControlledGate, get_inverse, QFT, Swap) @@ -60,9 +58,14 @@ def one_and_two_qubit_gates(eng, cmd): return False +def default_chooser(cmd, decomposition_list): + return decomposition_list[0] + + def get_engine_list(one_qubit_gates="any", - two_qubit_gates=(CNOT,), - other_gates=()): + two_qubit_gates=(CNOT, ), + other_gates=(), + compiler_chooser=default_chooser): """ Returns an engine list to compile to a restricted gate set. @@ -73,8 +76,8 @@ def get_engine_list(one_qubit_gates="any", even the gate sets which work might not yet be optimized. So make sure to double check and potentially extend the decomposition rules. This implemention currently requires that the one qubit gates must - contain Rz and at least one of {Ry(best), Rx, H} and the two qubit gate - must contain CNOT (recommended) or CZ. + contain Rz and at least one of {Ry(best), Rx, H} and the two qubit + gate must contain CNOT (recommended) or CZ. Note: Classical instructions gates such as e.g. Flush and Measure are @@ -86,21 +89,24 @@ def get_engine_list(one_qubit_gates="any", other_gates=(TimeEvolution,)) Args: - one_qubit_gates: "any" allows any one qubit gate, otherwise provide - a tuple of the allowed gates. If the gates are + one_qubit_gates: "any" allows any one qubit gate, otherwise provide a + tuple of the allowed gates. If the gates are instances of a class (e.g. X), it allows all gates - which are equal to it. If the gate is a class (Rz), it - allows all instances of this class. Default is "any" - two_qubit_gates: "any" allows any two qubit gate, otherwise provide - a tuple of the allowed gates. If the gates are + which are equal to it. If the gate is a class (Rz), + it allows all instances of this class. Default is + "any" + two_qubit_gates: "any" allows any two qubit gate, otherwise provide a + tuple of the allowed gates. If the gates are instances of a class (e.g. CNOT), it allows all gates which are equal to it. If the gate is a class, it allows all instances of this class. Default is (CNOT,). other_gates: A tuple of the allowed gates. If the gates are - instances of a class (e.g. QFT), it allows - all gates which are equal to it. If the gate is a - class, it allows all instances of this class. + instances of a class (e.g. QFT), it allows all gates + which are equal to it. If the gate is a class, it + allows all instances of this class. + compiler_chooser:function selecting the decomposition to use in the + Autoreplacer engine Raises: TypeError: If input is for the gates is not "any" or a tuple. Also if element within tuple is not a class or instance of BasicGate @@ -119,8 +125,8 @@ def get_engine_list(one_qubit_gates="any", if not isinstance(other_gates, tuple): raise TypeError("other_gates parameter must be a tuple.") - rule_set = DecompositionRuleSet(modules=[projectq.libs.math, - projectq.setups.decompositions]) + rule_set = DecompositionRuleSet( + modules=[projectq.libs.math, projectq.setups.decompositions]) allowed_gate_classes = [] # n-qubit gates allowed_gate_instances = [] allowed_gate_classes1 = [] # 1-qubit gates @@ -192,20 +198,21 @@ def low_level_gates(eng, cmd): elif cmd.gate in allowed_gate_instances1 and len(all_qubits) == 1: return True elif ((cmd.gate, len(cmd.control_qubits)) in allowed_gate_instances2 - and len(all_qubits) == 2): + and len(all_qubits) == 2): return True return False - return [AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(high_level_gates), - LocalOptimizer(5), - AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(one_and_two_qubit_gates), - LocalOptimizer(5), - AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(low_level_gates), - LocalOptimizer(5), - ] + return [ + AutoReplacer(rule_set, compiler_chooser), + TagRemover(), + InstructionFilter(high_level_gates), + LocalOptimizer(5), + AutoReplacer(rule_set, compiler_chooser), + TagRemover(), + InstructionFilter(one_and_two_qubit_gates), + LocalOptimizer(5), + AutoReplacer(rule_set, compiler_chooser), + TagRemover(), + InstructionFilter(low_level_gates), + LocalOptimizer(5), + ] diff --git a/projectq/setups/restrictedgateset_test.py b/projectq/setups/restrictedgateset_test.py index 544746f85..fe9754aa7 100644 --- a/projectq/setups/restrictedgateset_test.py +++ b/projectq/setups/restrictedgateset_test.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.setups.restrictedgateset.""" import pytest @@ -89,14 +88,14 @@ def test_restriction(): def test_wrong_init(): with pytest.raises(TypeError): - engine_list = restrictedgateset.get_engine_list(two_qubit_gates=(CNOT)) + restrictedgateset.get_engine_list(two_qubit_gates=(CNOT)) with pytest.raises(TypeError): - engine_list = restrictedgateset.get_engine_list(one_qubit_gates="Any") + restrictedgateset.get_engine_list(one_qubit_gates="Any") with pytest.raises(TypeError): - engine_list = restrictedgateset.get_engine_list(other_gates="any") + restrictedgateset.get_engine_list(other_gates="any") with pytest.raises(TypeError): - engine_list = restrictedgateset.get_engine_list(one_qubit_gates=(CRz,)) + restrictedgateset.get_engine_list(one_qubit_gates=(CRz, )) with pytest.raises(TypeError): - engine_list = restrictedgateset.get_engine_list(two_qubit_gates=(CRz,)) + restrictedgateset.get_engine_list(two_qubit_gates=(CRz, )) with pytest.raises(TypeError): - engine_list = restrictedgateset.get_engine_list(other_gates=(CRz,)) + restrictedgateset.get_engine_list(other_gates=(CRz, )) diff --git a/projectq/setups/trapped_ion_decomposer.py b/projectq/setups/trapped_ion_decomposer.py new file mode 100644 index 000000000..f5d19f1c8 --- /dev/null +++ b/projectq/setups/trapped_ion_decomposer.py @@ -0,0 +1,148 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Module uses ideas from "Basic circuit compilation techniques +# for an ion-trap quantum machine" by Dmitri Maslov (2017) at +# https://iopscience.iop.org/article/10.1088/1367-2630/aa5e47 +""" +Apply the restricted gate set setup for trapped ion based quantum computers. + +It provides the `engine_list` for the `MainEngine`, restricting the gate set to +Rx and Ry single qubit gates and the Rxx two qubit gates. + +A decomposition chooser is implemented following the ideas in QUOTE for +reducing the number of Ry gates in the new circuit. + +NOTE: + +Because the decomposition chooser is only called when a gate has to be +decomposed, this reduction will work better when the entire circuit has to be +decomposed. Otherwise, If the circuit has both superconding gates and native +ion trapped gates the decomposed circuit will not be optimal. +""" + +from projectq.setups import restrictedgateset +from projectq.ops import (Rxx, Rx, Ry) +from projectq.meta import get_control_count + +# ------------------chooser_Ry_reducer-------------------# +# If the qubit is not in the prev_Ry_sign dictionary, then no decomposition +# occured +# If the value is: +# -1 then the last gate applied (during a decomposition!) was Ry(-math.pi/2) +# 1 then the last gate applied (during a decomposition!) was Ry(+math.pi/2) +# 0 then the last gate applied (during a decomposition!) was a Rx + +prev_Ry_sign = dict() # Keeps track of most recent Ry sign, i.e. +# whether we had Ry(-pi/2) or Ry(pi/2) +# prev_Ry_sign[qubit_index] should hold -1 or +# +1 + + +def chooser_Ry_reducer(cmd, decomposition_list): + """ + Choose the decomposition so as to maximise Ry cancellations, based on the + previous decomposition used for the given qubit. + + Note: + Classical instructions gates e.g. Flush and Measure are automatically + allowed. + + Returns: + A decomposition object from the decomposition_list. + """ + decomp_rule = dict() + name = 'default' + + for decomp in decomposition_list: + try: + # NB: need to (possibly) raise an exception before setting the + # name variable below + decomposition = decomp.decompose.__name__.split('_') + decomp_rule[decomposition[3]] = decomp + name = decomposition[2] + # 'M' stands for minus, 'P' stands for plus 'N' stands for neutral + # e.g. decomp_rule['M'] will give you the decomposition_rule that + # ends with a Ry(-pi/2) + except IndexError: + pass + + local_prev_Ry_sign = prev_Ry_sign.setdefault(cmd.engine, dict()) + + if name == 'cnot2rxx': + assert get_control_count(cmd) == 1 + ctrl_id = cmd.control_qubits[0].id + + if local_prev_Ry_sign.get(ctrl_id, -1) <= 0: + # If the previous qubit had Ry(-pi/2) choose the decomposition + # that starts with Ry(pi/2) + local_prev_Ry_sign[ctrl_id] = -1 + # Now the prev_Ry_sign is set to -1 since at the end of the + # decomposition we will have a Ry(-pi/2) + return decomp_rule['M'] + + # Previous qubit had Ry(pi/2) choose decomposition that starts + # with Ry(-pi/2) and ends with R(pi/2) + local_prev_Ry_sign[ctrl_id] = 1 + return decomp_rule['P'] + + if name == 'h2rx': + qubit_id = [qb.id for qureg in cmd.qubits for qb in qureg] + assert len(qubit_id) == 1 # this should be a single qubit gate + qubit_id = qubit_id[0] + + if local_prev_Ry_sign.get(qubit_id, 0) == 0: + local_prev_Ry_sign[qubit_id] = 1 + return decomp_rule['M'] + + local_prev_Ry_sign[qubit_id] = 0 + return decomp_rule['N'] + + if name == 'rz2rx': + qubit_id = [qb.id for qureg in cmd.qubits for qb in qureg] + assert len(qubit_id) == 1 # this should be a single qubit gate + qubit_id = qubit_id[0] + + if local_prev_Ry_sign.get(qubit_id, -1) <= 0: + local_prev_Ry_sign[qubit_id] = -1 + return decomp_rule['M'] + + local_prev_Ry_sign[qubit_id] = 1 + return decomp_rule['P'] + + # No decomposition chosen, so use the first decompostion in the list + # like the default function + return decomposition_list[0] + + +def get_engine_list(): + """ + Returns an engine list compiling code into a trapped ion based compiled + circuit code. + + Note: + + - Classical instructions gates such as e.g. Flush and Measure are + automatically allowed. + - The restricted gate set engine does not work with Rxx gates, as + ProjectQ will by default bounce back and forth between Cz gates and Cx + gates. An appropriate decomposition chooser needs to be used! + + Returns: + A list of suitable compiler engines. + """ + return restrictedgateset.get_engine_list( + one_qubit_gates=(Rx, Ry), + two_qubit_gates=(Rxx, ), + compiler_chooser=chooser_Ry_reducer) diff --git a/projectq/setups/trapped_ion_decomposer_test.py b/projectq/setups/trapped_ion_decomposer_test.py new file mode 100644 index 000000000..23b6485c6 --- /dev/null +++ b/projectq/setups/trapped_ion_decomposer_test.py @@ -0,0 +1,150 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.trapped_ion_decomposer.py." + +import projectq +from projectq.ops import (Rx, Ry, Rz, H, X, CNOT, Measure, Rxx, + ClassicalInstructionGate) +from projectq.cengines import (MainEngine, DummyEngine, AutoReplacer, + TagRemover, InstructionFilter, + DecompositionRuleSet, DecompositionRule) +from projectq.meta import get_control_count + +from . import restrictedgateset +from .trapped_ion_decomposer import chooser_Ry_reducer, get_engine_list + + +def filter_gates(eng, cmd): + if isinstance(cmd.gate, ClassicalInstructionGate): + return True + if ((cmd.gate == X and get_control_count(cmd) == 1) or cmd.gate == H + or isinstance(cmd.gate, Rz)): + return False + return True + + +def test_chooser_Ry_reducer_synthetic(): + backend = DummyEngine(save_commands=True) + rule_set = DecompositionRuleSet( + modules=[projectq.libs.math, projectq.setups.decompositions]) + + engine_list = [ + AutoReplacer(rule_set, chooser_Ry_reducer), + TagRemover(), + InstructionFilter(filter_gates), + ] + + eng = MainEngine(backend=backend, engine_list=engine_list) + control = eng.allocate_qubit() + target = eng.allocate_qubit() + CNOT | (control, target) + CNOT | (control, target) + eng.flush() + idx0 = len(backend.received_commands) - 2 + idx1 = len(backend.received_commands) + CNOT | (control, target) + eng.flush() + + assert isinstance(backend.received_commands[idx0].gate, Ry) + assert isinstance(backend.received_commands[idx1].gate, Ry) + assert (backend.received_commands[idx0].gate.get_inverse() == + backend.received_commands[idx1].gate) + + eng = MainEngine(backend=backend, engine_list=engine_list) + control = eng.allocate_qubit() + target = eng.allocate_qubit() + H | target + eng.flush() + idx0 = len(backend.received_commands) - 2 + idx1 = len(backend.received_commands) + H | target + eng.flush() + + assert isinstance(backend.received_commands[idx0].gate, Ry) + assert isinstance(backend.received_commands[idx1].gate, Ry) + assert (backend.received_commands[idx0].gate.get_inverse() == + backend.received_commands[idx1].gate) + + eng = MainEngine(backend=backend, engine_list=engine_list) + control = eng.allocate_qubit() + target = eng.allocate_qubit() + Rz(1.23456) | target + eng.flush() + idx0 = len(backend.received_commands) - 2 + idx1 = len(backend.received_commands) + Rz(1.23456) | target + eng.flush() + + assert isinstance(backend.received_commands[idx0].gate, Ry) + assert isinstance(backend.received_commands[idx1].gate, Ry) + assert (backend.received_commands[idx0].gate.get_inverse() == + backend.received_commands[idx1].gate) + + +def _dummy_h2nothing_A(cmd): + qubit = cmd.qubits[0] + Ry(1.23456) | qubit + + +def test_chooser_Ry_reducer_unsupported_gate(): + backend = DummyEngine(save_commands=True) + rule_set = DecompositionRuleSet( + rules=[DecompositionRule(H.__class__, _dummy_h2nothing_A)]) + + engine_list = [ + AutoReplacer(rule_set, chooser_Ry_reducer), + TagRemover(), + InstructionFilter(filter_gates), + ] + + eng = MainEngine(backend=backend, engine_list=engine_list) + qubit = eng.allocate_qubit() + H | qubit + eng.flush() + + for cmd in backend.received_commands: + print(cmd) + + assert isinstance(backend.received_commands[1].gate, Ry) + + +def test_chooser_Ry_reducer(): + # Without the chooser_Ry_reducer function, i.e. if the restricted gate set + # just picked the first option in each decomposition list, the circuit + # below would be decomposed into 8 single qubit gates and 1 two qubit + # gate. + # + # Including the Allocate, Measure and Flush commands, this would result in + # 13 commands. + # + # Using the chooser_Rx_reducer you get 10 commands, since you now have 4 + # single qubit gates and 1 two qubit gate. + + for engine_list, count in [(restrictedgateset.get_engine_list( + one_qubit_gates=(Rx, Ry), + two_qubit_gates=(Rxx, )), 13), + (get_engine_list(), 11)]: + + backend = DummyEngine(save_commands=True) + eng = projectq.MainEngine(backend, engine_list, verbose=True) + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + H | qubit1 + CNOT | (qubit1, qubit2) + Rz(0.2) | qubit1 + Measure | qubit1 + eng.flush() + + assert len(backend.received_commands) == count diff --git a/requirements.txt b/requirements.txt index 903d45bdc..60d6b013c 100755 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pybind11>=2.2.3 requests scipy networkx +matplotlib>=2.2.3 diff --git a/setup.py b/setup.py index 604f006af..5049a3a06 100755 --- a/setup.py +++ b/setup.py @@ -1,29 +1,61 @@ -from setuptools import setup, Extension, find_packages, Feature -from setuptools.command.build_ext import build_ext -import sys -import os -import setuptools +# Some of the setup.py code is inspired or copied from SQLAlchemy +# SQLAlchemy was created by Michael Bayer. -# This reads the __version__ variable from projectq/_version.py -exec(open('projectq/_version.py').read()) +# Major contributing authors include: -# Readme file as long_description: -long_description = open('README.rst').read() +# - Michael Bayer +# - Jason Kirtland +# - Gaetan de Menten +# - Diana Clarke +# - Michael Trier +# - Philip Jenvey +# - Ants Aasma +# - Paul Johnston +# - Jonathan Ellis +# - Damien Nguyen (ProjectQ) -# Read in requirements.txt -with open('requirements.txt', 'r') as f_requirements: - requirements = f_requirements.readlines() -requirements = [r.strip() for r in requirements] +# Copyright 2005-2020 SQLAlchemy and ProjectQ authors and contributors (see above) + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from __future__ import print_function +from setuptools import setup, Extension, find_packages +from distutils.errors import (CompileError, LinkError, CCompilerError, + DistutilsExecError, DistutilsPlatformError) +from setuptools import Distribution as _Distribution +from setuptools.command.build_ext import build_ext +import sys +import os +import subprocess +import platform + +# ============================================================================== +# Helper functions and classes class get_pybind_include(object): - """Helper class to determine the pybind11 include path + '''Helper class to determine the pybind11 include path The purpose of this class is to postpone importing pybind11 until it is actually installed, so that the ``get_include()`` - method can be invoked. """ - + method can be invoked. ''' def __init__(self, user=False): self.user = user @@ -32,141 +64,409 @@ def __str__(self): return pybind11.get_include(self.user) -cppsim = Feature( - 'C++ Simulator', - standard=True, - ext_modules=[ - Extension( - 'projectq.backends._sim._cppsim', - ['projectq/backends/_sim/_cppsim.cpp'], - include_dirs=[ - # Path to pybind11 headers - get_pybind_include(), - get_pybind_include(user=True) - ], - language='c++' - ), - ], -) - - -def has_flag(compiler, flagname=None): - """ +def important_msgs(*msgs): + print('*' * 75) + for msg in msgs: + print(msg) + print('*' * 75) + + +def status_msgs(*msgs): + print('-' * 75) + for msg in msgs: + print('# INFO: ', msg) + print('-' * 75) + + +def compiler_test(compiler, + flagname=None, + link=False, + include='', + body='', + postargs=None): + ''' Return a boolean indicating whether a flag name is supported on the specified compiler. - """ + ''' import tempfile f = tempfile.NamedTemporaryFile('w', suffix='.cpp', delete=False) - f.write('int main (int argc, char **argv) { return 0; }') + f.write('{}\nint main (int argc, char **argv) {{ {} return 0; }}'.format( + include, body)) f.close() ret = True - try: - if flagname is None: - compiler.compile([f.name]) - else: - compiler.compile([f.name], extra_postargs=[flagname]) - except: - ret = False - os.unlink(f.name) - return ret + if postargs is None: + postargs = [flagname] if flagname is not None else None + elif flagname is not None: + postargs.append(flagname) -def knows_intrinsics(compiler): - """ - Return a boolean indicating whether the compiler can handle intrinsics. - """ - import tempfile - f = tempfile.NamedTemporaryFile('w', suffix='.cpp', delete=False) - f.write('#include \nint main (int argc, char **argv) ' - '{ __m256d neg = _mm256_set1_pd(1.0); }') - f.close() - ret = True try: - compiler.compile([f.name], extra_postargs=['-march=native']) - except setuptools.distutils.errors.CompileError: + exec_name = os.path.join(tempfile.mkdtemp(), 'test') + + if compiler.compiler_type == 'msvc': + olderr = os.dup(sys.stderr.fileno()) + err = open('err.txt', 'w') + os.dup2(err.fileno(), sys.stderr.fileno()) + + obj_file = compiler.compile([f.name], extra_postargs=postargs) + if not os.path.exists(obj_file[0]): + raise RuntimeError('') + if link: + compiler.link_executable(obj_file, + exec_name, + extra_postargs=postargs) + + if compiler.compiler_type == 'msvc': + err.close() + os.dup2(olderr, sys.stderr.fileno()) + with open('err.txt', 'r') as err_file: + if err_file.readlines(): + raise RuntimeError('') + except (CompileError, LinkError, RuntimeError): ret = False os.unlink(f.name) return ret +def _fix_macosx_header_paths(*args): + # Fix path to SDK headers if necessary + _MACOSX_XCODE_REF_PATH = ('/Applications/Xcode.app/Contents/' + + 'Developer/Platforms/MacOSX.platform/' + + 'Developer') + _MACOSX_DEVTOOLS_REF_PATH = '/Library/Developer/CommandLineTools/' + _has_xcode = os.path.exists(_MACOSX_XCODE_REF_PATH) + _has_devtools = os.path.exists(_MACOSX_DEVTOOLS_REF_PATH) + if not _has_xcode and not _has_devtools: + important_msgs('ERROR: Must install either Xcode or ' + + 'CommandLineTools!') + raise BuildFailed() + + def _do_replace(idx, item): + if not _has_xcode and _MACOSX_XCODE_REF_PATH in item: + compiler_args[idx] = item.replace(_MACOSX_XCODE_REF_PATH, + _MACOSX_DEVTOOLS_REF_PATH) + + if not _has_devtools and _MACOSX_DEVTOOLS_REF_PATH in item: + compiler_args[idx] = item.replace(_MACOSX_DEVTOOLS_REF_PATH, + _MACOSX_XCODE_REF_PATH) + + for compiler_args in args: + for idx, item in enumerate(compiler_args): + _do_replace(idx, item) + + +# ------------------------------------------------------------------------------ + + +class BuildFailed(Exception): + def __init__(self): + self.cause = sys.exc_info()[1] # work around py 2/3 different syntax + + +# ------------------------------------------------------------------------------ +# Python build related variable + +cpython = platform.python_implementation() == 'CPython' +ext_errors = (CCompilerError, DistutilsExecError, DistutilsPlatformError) +if sys.platform == 'win32': + # 2.6's distutils.msvc9compiler can raise an IOError when failing to + # find the compiler + ext_errors += (IOError, ) + +# ============================================================================== + +# This reads the __version__ variable from projectq/_version.py +exec(open('projectq/_version.py').read()) + +# Readme file as long_description: +long_description = open('README.rst').read() + +# Read in requirements.txt +with open('requirements.txt', 'r') as f_requirements: + requirements = f_requirements.readlines() +requirements = [r.strip() for r in requirements] + +# ------------------------------------------------------------------------------ +# ProjectQ C++ extensions + +ext_modules = [ + Extension( + 'projectq.backends._sim._cppsim', + ['projectq/backends/_sim/_cppsim.cpp'], + include_dirs=[ + # Path to pybind11 headers + get_pybind_include(), + get_pybind_include(user=True) + ], + language='c++'), +] + +# ============================================================================== + + class BuildExt(build_ext): - """A custom build extension for adding compiler-specific options.""" + '''A custom build extension for adding compiler-specific options.''' c_opts = { 'msvc': ['/EHsc'], 'unix': [], } + def run(self): + try: + build_ext.run(self) + except DistutilsPlatformError: + raise BuildFailed() + def build_extensions(self): + self._configure_compiler() + for ext in self.extensions: + ext.extra_compile_args = self.opts + ext.extra_link_args = self.link_opts + try: + build_ext.build_extensions(self) + except ext_errors: + raise BuildFailed() + except ValueError: + # this can happen on Windows 64 bit, see Python issue 7511 + if "'path'" in str(sys.exc_info()[1]): # works with both py 2/3 + raise BuildFailed() + raise + + def _configure_compiler(self): if sys.platform == 'darwin': - self.c_opts['unix'] += ['-mmacosx-version-min=10.7'] - if has_flag(self.compiler, '-stdlib=libc++'): + _fix_macosx_header_paths(self.compiler.compiler, + self.compiler.compiler_so) + + if compiler_test(self.compiler, '-stdlib=libc++'): self.c_opts['unix'] += ['-stdlib=libc++'] ct = self.compiler.compiler_type - opts = self.c_opts.get(ct, []) + self.opts = self.c_opts.get(ct, []) + self.link_opts = [] - if not has_flag(self.compiler): - self.warning("Something is wrong with your C++ compiler.\n" - "Failed to compile a simple test program!\n") - return + if not compiler_test(self.compiler): + important_msgs( + 'ERROR: something is wrong with your C++ compiler.\n' + 'Failed to compile a simple test program!') + raise BuildFailed() + + # ------------------------------ + + status_msgs('Configuring OpenMP') + self._configure_openmp() + status_msgs('Configuring compiler intrinsics') + self._configure_intrinsics() + status_msgs('Configuring C++ standard') + self._configure_cxx_standard() - openmp = '' - if has_flag(self.compiler, '-fopenmp'): - openmp = '-fopenmp' - elif has_flag(self.compiler, '-qopenmp'): - openmp = '-qopenmp' - if ct == 'msvc': - openmp = '' # supports only OpenMP 2.0 - - if knows_intrinsics(self.compiler): - opts.append('-DINTRIN') - if ct == 'msvc': - opts.append('/arch:AVX') - else: - opts.append('-march=native') - - opts.append(openmp) + # ------------------------------ + # Other compiler tests + + status_msgs('Other compiler tests') if ct == 'unix': - if not has_flag(self.compiler, '-std=c++11'): - self.warning("Compiler needs to have C++11 support!") + if compiler_test(self.compiler, '-fvisibility=hidden'): + self.opts.append('-fvisibility=hidden') + self.opts.append("-DVERSION_INFO=\"{}\"".format( + self.distribution.get_version())) + elif ct == 'msvc': + self.opts.append("/DVERSION_INFO=\\'{}\\'".format( + self.distribution.get_version())) + + status_msgs('Finished configuring compiler!') + + def _configure_openmp(self): + if self.compiler.compiler_type == 'msvc': + return + + kwargs = { + 'link': True, + 'include': '#include ', + 'body': 'int a = omp_get_num_threads(); ++a;' + } + + for flag in ['-openmp', '-fopenmp', '-qopenmp', '/Qopenmp']: + if compiler_test(self.compiler, flag, **kwargs): + self.opts.append(flag) + self.link_opts.append(flag) return - opts.append('-DVERSION_INFO="%s"' - % self.distribution.get_version()) - opts.append('-std=c++11') - if has_flag(self.compiler, '-fvisibility=hidden'): - opts.append('-fvisibility=hidden') - elif ct == 'msvc': - opts.append('/DVERSION_INFO=\\"%s\\"' - % self.distribution.get_version()) - for ext in self.extensions: - ext.extra_compile_args = opts - ext.extra_link_args = [openmp] - try: - build_ext.build_extensions(self) - except setuptools.distutils.errors.CompileError: - self.warning("") - - def warning(self, warning_text): - raise Exception(warning_text + "\nCould not install the C++-Simulator." - "\nProjectQ will default to the (slow) Python " - "simulator.\nUse --without-cppsimulator to skip " - "building the (faster) C++ version of the simulator.") - - -setup( - name='projectq', - version=__version__, - author='ProjectQ', - author_email='info@projectq.ch', - url='http://www.projectq.ch', - description=('ProjectQ - ' - 'An open source software framework for quantum computing'), - long_description=long_description, - features={'cppsimulator': cppsim}, - install_requires=requirements, - cmdclass={'build_ext': BuildExt}, - zip_safe=False, - license='Apache 2', - packages=find_packages() -) + flag = '-fopenmp' + if (sys.platform == 'darwin' and compiler_test(self.compiler, flag)): + try: + llvm_root = subprocess.check_output( + ['brew', '--prefix', 'llvm']).decode('utf-8')[:-1] + compiler_root = subprocess.check_output( + ['which', self.compiler.compiler[0]]).decode('utf-8')[:-1] + + # Only add the flag if the compiler we are using is the one + # from HomeBrew + if llvm_root in compiler_root: + l_arg = '-L{}/lib'.format(llvm_root) + if compiler_test(self.compiler, + flag, + postargs=[l_arg], + **kwargs): + self.opts.append(flag) + self.link_opts.extend((l_arg, flag)) + return + except subprocess.CalledProcessError: + pass + + try: + # Only relevant for MacPorts users with clang-3.7 + port_path = subprocess.check_output(['which', 'port' + ]).decode('utf-8')[:-1] + macports_root = os.path.dirname(os.path.dirname(port_path)) + compiler_root = subprocess.check_output( + ['which', self.compiler.compiler[0]]).decode('utf-8')[:-1] + + # Only add the flag if the compiler we are using is the one + # from MacPorts + if macports_root in compiler_root: + c_arg = '-I{}/include/libomp'.format(macports_root) + l_arg = '-L{}/lib/libomp'.format(macports_root) + + if compiler_test(self.compiler, + flag, + postargs=[c_arg, l_arg], + **kwargs): + self.opts.extend((c_arg, flag)) + self.link_opts.extend((l_arg, flag)) + return + except subprocess.CalledProcessError: + pass + + important_msgs('WARNING: compiler does not support OpenMP!') + + def _configure_intrinsics(self): + for flag in [ + '-march=native', '-mavx2', '/arch:AVX2', '/arch:CORE-AVX2', + '/arch:AVX' + ]: + if compiler_test( + self.compiler, + flagname=flag, + link=False, + include='#include ', + body='__m256d neg = _mm256_set1_pd(1.0); (void)neg;'): + + if sys.platform == 'win32': + self.opts.extend(('/DINTRIN', flag)) + else: + self.opts.extend(('-DINTRIN', flag)) + break + + for flag in ['-ffast-math', '-fast', '/fast', '/fp:precise']: + if compiler_test(self.compiler, flagname=flag): + self.opts.append(flag) + break + + def _configure_cxx_standard(self): + if self.compiler.compiler_type == 'msvc': + return + + cxx_standards = [17, 14, 11] + if sys.version_info[0] < 3: + cxx_standards = [year for year in cxx_standards if year < 17] + + if sys.platform == 'darwin': + _, minor_version, _ = [ + int(i) for i in platform.mac_ver()[0].split('.') + ] + if minor_version < 14: + cxx_standards = [year for year in cxx_standards if year < 17] + + for year in cxx_standards: + flag = '-std=c++{}'.format(year) + if compiler_test(self.compiler, flag): + self.opts.append(flag) + return + flag = '/Qstd=c++{}'.format(year) + if compiler_test(self.compiler, flag): + self.opts.append(flag) + return + + important_msgs('ERROR: compiler needs to have at least C++11 support!') + raise BuildFailed() + + +class Distribution(_Distribution): + def has_ext_modules(self): + # We want to always claim that we have ext_modules. This will be fine + # if we don't actually have them (such as on PyPy) because nothing + # will get built, however we don't want to provide an overally broad + # Wheel package when building a wheel without C support. This will + # ensure that Wheel knows to treat us as if the build output is + # platform specific. + return True + + +# ============================================================================== + + +def run_setup(with_cext): + kwargs = {} + if with_cext: + kwargs['ext_modules'] = ext_modules + else: + kwargs['ext_modules'] = [] + + setup(name='projectq', + version=__version__, + author='ProjectQ', + author_email='info@projectq.ch', + url='http://www.projectq.ch', + project_urls={ + 'Documentation': 'https://projectq.readthedocs.io/en/latest/', + 'Issue Tracker': + 'https://github.com/ProjectQ-Framework/ProjectQ/', + }, + description=( + 'ProjectQ - ' + 'An open source software framework for quantum computing'), + long_description=long_description, + install_requires=requirements, + cmdclass={'build_ext': BuildExt}, + zip_safe=False, + license='Apache 2', + packages=find_packages(), + distclass=Distribution, + **kwargs) + + +# ============================================================================== + +if not cpython: + run_setup(False) + important_msgs( + 'WARNING: C/C++ extensions are not supported on ' + + 'some features are disabled (e.g. C++ simulator).', + 'Plain-Python build succeeded.', + ) +elif os.environ.get('DISABLE_PROJECTQ_CEXT'): + run_setup(False) + important_msgs( + 'DISABLE_PROJECTQ_CEXT is set; ' + + 'not attempting to build C/C++ extensions.', + 'Plain-Python build succeeded.', + ) + +else: + try: + run_setup(True) + except BuildFailed as exc: + important_msgs( + exc.cause, + 'WARNING: Some C/C++ extensions could not be compiled, ' + + 'some features are disabled (e.g. C++ simulator).', + 'Failure information, if any, is above.', + 'Retrying the build without the C/C++ extensions now.', + ) + + run_setup(False) + + important_msgs( + 'WARNING: Some C/C++ extensions could not be compiled, ' + + 'some features are disabled (e.g. C++ simulator).', + 'Plain-Python build succeeded.', + )