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/examples/ibm.py b/examples/ibm.py index 05e042230..37ba4e0d7 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,19 @@ 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/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 85aee3dac..2562a07dd 100755 --- a/projectq/backends/_circuits/_drawer.py +++ b/projectq/backends/_circuits/_drawer.py @@ -15,8 +15,6 @@ Contains a compiler engine which generates TikZ Latex code describing the circuit. """ -import sys - from builtins import input from projectq.cengines import LastEngineException, BasicEngine @@ -223,12 +221,13 @@ def _print_cmd(self, cmd): self._free_lines.append(qubit_id) if self.is_last_engine and cmd.gate == Measure: - assert (get_control_count(cmd) == 0) + assert get_control_count(cmd) == 0 + for qureg in cmd.qubits: for qubit in qureg: if self._accept_input: m = None - while m != '0' and m != '1' and m != 1 and m != 0: + while m not in ('0', '1', 1, 0): prompt = ("Input measurement result (0 or 1) for " "qubit " + str(qubit) + ": ") m = input(prompt) diff --git a/projectq/backends/_circuits/_drawer_matplotlib.py b/projectq/backends/_circuits/_drawer_matplotlib.py new file mode 100644 index 000000000..23a07c767 --- /dev/null +++ b/projectq/backends/_circuits/_drawer_matplotlib.py @@ -0,0 +1,208 @@ +# 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. + + Returns: + A tuple containing the matplotlib figure and axes objects + """ + 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/_plot.py b/projectq/backends/_circuits/_plot.py new file mode 100644 index 000000000..009b00ab7 --- /dev/null +++ b/projectq/backends/_circuits/_plot.py @@ -0,0 +1,607 @@ +# 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. + """ + 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/_ibm/_ibm.py b/projectq/backends/_ibm/_ibm.py index b1899f043..6486ab4d0 100755 --- a/projectq/backends/_ibm/_ibm.py +++ b/projectq/backends/_ibm/_ibm.py @@ -13,7 +13,7 @@ # limitations under the License. """ Back-end to run quantum program on IBM's Quantum Experience.""" - +import math import random import json @@ -41,11 +41,11 @@ class IBMBackend(BasicEngine): """ - The IBM Backend class, which stores the circuit, transforms it to JSON - QASM, and sends the circuit through the IBM API. + The IBM Backend class, which stores the circuit, transforms it to JSON, + and sends the circuit through the IBM API. """ def __init__(self, use_hardware=False, num_runs=1024, verbose=False, - user=None, password=None, device='ibmqx4', + token='', device='ibmq_essex', num_retries=3000, interval=1, retrieve_execution=None): """ @@ -59,10 +59,8 @@ def __init__(self, use_hardware=False, num_runs=1024, verbose=False, verbose (bool): If True, statistics are printed, in addition to the measurement result being registered (at the end of the circuit). - user (string): IBM Quantum Experience user name - password (string): IBM Quantum Experience password - device (string): Device to use ('ibmqx4', or 'ibmqx5') - if use_hardware is set to True. Default is ibmqx4. + token (str): IBM quantum experience user password. + device (str): name of the IBM device to use. ibmq_essex By default num_retries (int): Number of times to retry to obtain results from the IBM API. (default is 3000) interval (float, int): Number of seconds between successive @@ -76,15 +74,15 @@ def __init__(self, use_hardware=False, num_runs=1024, verbose=False, if use_hardware: self.device = device else: - self.device = 'simulator' + self.device = 'ibmq_qasm_simulator' self._num_runs = num_runs self._verbose = verbose - self._user = user - self._password = password + self._token=token self._num_retries = num_retries self._interval = interval self._probabilities = dict() self.qasm = "" + self._json=[] self._measured_ids = [] self._allocated_qubits = set() self._retrieve_execution = retrieve_execution @@ -93,17 +91,17 @@ def is_available(self, cmd): """ Return true if the command can be executed. - The IBM quantum chip can do X, Y, Z, T, Tdag, S, Sdag, - rotation gates, barriers, and CX / CNOT. + The IBM quantum chip can only do U1,U2,U3,barriers, and CX / CNOT. + Conversion implemented for Rotation gates and H gates. Args: cmd (Command): Command for which to check availability """ g = cmd.gate - if g == NOT and get_control_count(cmd) <= 1: + if g == NOT and get_control_count(cmd) == 1: return True if get_control_count(cmd) == 0: - if g in (T, Tdag, S, Sdag, H, Y, Z): + if g == H: return True if isinstance(g, (Rx, Ry, Rz)): return True @@ -111,6 +109,11 @@ def is_available(self, cmd): return True return False + def get_qasm(self): + """ Return the QASM representation of the circuit sent to the backend. + Should be called AFTER calling the ibm device """ + return self.qasm + def _reset(self): """ Reset all temporary variables (after flush gate). """ self._clear = True @@ -129,10 +132,10 @@ def _store(self, cmd): self._probabilities = dict() self._clear = False self.qasm = "" + self._json=[] self._allocated_qubits = set() gate = cmd.gate - if gate == Allocate: self._allocated_qubits.add(cmd.qubits[0][0].id) return @@ -154,6 +157,7 @@ def _store(self, cmd): ctrl_pos = cmd.control_qubits[0].id qb_pos = cmd.qubits[0][0].id self.qasm += "\ncx q[{}], q[{}];".format(ctrl_pos, qb_pos) + self._json.append({'qubits': [ctrl_pos, qb_pos], 'name': 'cx'}) elif gate == Barrier: qb_pos = [qb.id for qr in cmd.qubits for qb in qr] self.qasm += "\nbarrier " @@ -161,22 +165,28 @@ def _store(self, cmd): for pos in qb_pos: qb_str += "q[{}], ".format(pos) self.qasm += qb_str[:-2] + ";" + self._json.append({'qubits': qb_pos, 'name': 'barrier'}) elif isinstance(gate, (Rx, Ry, Rz)): assert get_control_count(cmd) == 0 qb_pos = cmd.qubits[0][0].id u_strs = {'Rx': 'u3({}, -pi/2, pi/2)', 'Ry': 'u3({}, 0, 0)', 'Rz': 'u1({})'} - gate = u_strs[str(gate)[0:2]].format(gate.angle) - self.qasm += "\n{} q[{}];".format(gate, qb_pos) - else: + u_name = {'Rx': 'u3', 'Ry': 'u3', + 'Rz': 'u1'} + u_angle = {'Rx': [gate.angle, -math.pi/2, math.pi/2], 'Ry': [gate.angle, 0, 0], + 'Rz': [gate.angle]} + gate_qasm = u_strs[str(gate)[0:2]].format(gate.angle) + gate_name=u_name[str(gate)[0:2]] + params= u_angle[str(gate)[0:2]] + self.qasm += "\n{} q[{}];".format(gate_qasm, qb_pos) + self._json.append({'qubits': [qb_pos], 'name': gate_name,'params': params}) + elif gate == H: assert get_control_count(cmd) == 0 - if str(gate) in self._gate_names: - gate_str = self._gate_names[str(gate)] - else: - gate_str = str(gate).lower() - qb_pos = cmd.qubits[0][0].id - self.qasm += "\n{} q[{}];".format(gate_str, qb_pos) + self.qasm += "\nu2(0,pi/2) q[{}];".format(qb_pos) + self._json.append({'qubits': [qb_pos], 'name': 'u2','params': [0, 3.141592653589793]}) + else: + raise Exception('Command not authorized. You should run the circuit with the appropriate ibm setup.') def _logical_to_physical(self, qb_id): """ @@ -198,6 +208,8 @@ def _logical_to_physical(self, qb_id): def get_probabilities(self, qureg): """ Return the list of basis states with corresponding probabilities. + If input qureg is a subset of the register used for the experiment, + then returns the projected probabilities over the other states. The measured bits are ordered according to the supplied quantum register, i.e., the left-most bit in the state-string corresponds to @@ -212,7 +224,7 @@ def get_probabilities(self, qureg): Returns: probability_dict (dict): Dictionary mapping n-bit strings to - probabilities. + probabilities. Raises: RuntimeError: If no data is available (i.e., if the circuit has @@ -223,68 +235,70 @@ def get_probabilities(self, qureg): raise RuntimeError("Please, run the circuit first!") probability_dict = dict() - for state in self._probabilities: mapped_state = ['0'] * len(qureg) for i in range(len(qureg)): mapped_state[i] = state[self._logical_to_physical(qureg[i].id)] probability = self._probabilities[state] - probability_dict["".join(mapped_state)] = probability - + mapped_state = "".join(mapped_state) + if mapped_state not in probability_dict: + probability_dict[mapped_state] = probability + else: + probability_dict[mapped_state] += probability return probability_dict def _run(self): """ Run the circuit. - Send the circuit via the IBM API (JSON QASM) using the provided user - data / ask for username & password. + Send the circuit via a non documented IBM API (using JSON written + circuits) using the provided user data / ask for the user token. """ # finally: add measurements (no intermediate measurements are allowed) for measured_id in self._measured_ids: qb_loc = self.main_engine.mapper.current_mapping[measured_id] self.qasm += "\nmeasure q[{}] -> c[{}];".format(qb_loc, qb_loc) - + self._json.append({'qubits': [qb_loc], 'name': 'measure','memory':[qb_loc]}) # return if no operations / measurements have been performed. if self.qasm == "": return - - max_qubit_id = max(self._allocated_qubits) + max_qubit_id = max(self._allocated_qubits) + 1 qasm = ("\ninclude \"qelib1.inc\";\nqreg q[{nq}];\ncreg c[{nq}];" + - self.qasm).format(nq=max_qubit_id + 1) + self.qasm).format(nq=max_qubit_id) info = {} - info['qasms'] = [{'qasm': qasm}] + info['json']=self._json + info['nq']=max_qubit_id + info['shots'] = self._num_runs - info['maxCredits'] = 5 + info['maxCredits'] = 10 info['backend'] = {'name': self.device} - info = json.dumps(info) - try: if self._retrieve_execution is None: res = send(info, device=self.device, - user=self._user, password=self._password, - shots=self._num_runs, + token=self._token, num_retries=self._num_retries, interval=self._interval, verbose=self._verbose) else: - res = retrieve(device=self.device, user=self._user, - password=self._password, + res = retrieve(device=self.device, + token=self._token, jobid=self._retrieve_execution, num_retries=self._num_retries, interval=self._interval, verbose=self._verbose) - counts = res['data']['counts'] # Determine random outcome P = random.random() p_sum = 0. measured = "" + length=len(self._measured_ids) for state in counts: probability = counts[state] * 1. / self._num_runs - state = list(reversed(state)) - state = "".join(state) + state="{0:b}".format(int(state,0)) + state=state.zfill(max_qubit_id) + #states in ibmq are right-ordered, so need to reverse state string + state=state[::-1] p_sum += probability star = "" if p_sum >= P and measured == "": @@ -322,9 +336,3 @@ def receive(self, command_list): else: self._run() self._reset() - - """ - Mapping of gate names from our gate objects to the IBM QASM representation. - """ - _gate_names = {str(Tdag): "tdg", - str(Sdag): "sdg"} diff --git a/projectq/backends/_ibm/_ibm_http_client.py b/projectq/backends/_ibm/_ibm_http_client.py index d713b17fa..98751bf90 100755 --- a/projectq/backends/_ibm/_ibm_http_client.py +++ b/projectq/backends/_ibm/_ibm_http_client.py @@ -13,83 +13,350 @@ # limitations under the License. # helpers to run the jsonified gate sequence on ibm quantum experience server -# api documentation is at https://qcwi-staging.mybluemix.net/explorer/ -import requests +# api documentation does not exist and has to be deduced from the qiskit code source +# at: https://github.com/Qiskit/qiskit-ibmq-provider + import getpass -import json -import signal -import sys import time +import signal +import requests from requests.compat import urljoin +from requests import Session + +_AUTH_API_URL = 'https://auth.quantum-computing.ibm.com/api/users/loginWithToken' +_API_URL = 'https://api.quantum-computing.ibm.com/api/' + +# TODO: call to get the API version automatically +CLIENT_APPLICATION = 'ibmqprovider/0.4.4' + + +class IBMQ(Session): + """ + Manage a session between ProjectQ and the IBMQ web API. + """ + + def __init__(self, **kwargs): + super(IBMQ, self).__init__(**kwargs) # Python 2 compatibility + self.backends = dict() + self.timeout = 5.0 + + def get_list_devices(self, verbose=False): + """ + Get the list of available IBM backends with their properties + + Args: + verbose (bool): print the returned dictionnary if True + + Returns: + (dict) backends dictionary by name device, containing the qubit + size 'nq', the coupling map 'coupling_map' as well as the + device version 'version' + """ + list_device_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + argument = {'allow_redirects': True, 'timeout': (self.timeout, None)} + request = super(IBMQ, self).get(urljoin(_API_URL, list_device_url), + **argument) + request.raise_for_status() + r_json = request.json() + self.backends = dict() + for el in r_json: + self.backends[el['backend_name']] = { + 'nq': el['n_qubits'], + 'coupling_map': el['coupling_map'], + 'version': el['backend_version'] + } + + if verbose: + print('- List of IBMQ devices available:') + print(self.backends) + return self.backends + + def is_online(self, device): + """ + Check if the device is in the list of available IBM backends. + + Args: + device (str): name of the device to check + + Returns: + (bool) True if device is available, False otherwise + """ + return device in self.backends + + def can_run_experiment(self, info, device): + """ + Check if the device is big enough to run the code. + + Args: + info (dict): dictionary sent by the backend containing the code to + run + device (str): name of the ibm device to use + + Returns: + (tuple): (bool) True if device is big enough, False otherwise + (int) maximum number of qubit available on the device + (int) number of qubit needed for the circuit + + """ + nb_qubit_max = self.backends[device]['nq'] + nb_qubit_needed = info['nq'] + return nb_qubit_needed <= nb_qubit_max, nb_qubit_max, nb_qubit_needed + + def _authenticate(self, token=None): + """ + Args: + token (str): IBM quantum experience user API token. + """ + if token is None: + token = getpass.getpass(prompt="IBM QE token > ") + if len(token) == 0: + raise Exception('Error with the IBM QE token') + self.headers.update({'X-Qx-Client-Application': CLIENT_APPLICATION}) + args = { + 'data': None, + 'json': { + 'apiToken': token + }, + 'timeout': (self.timeout, None) + } + request = super(IBMQ, self).post(_AUTH_API_URL, **args) + request.raise_for_status() + r_json = request.json() + self.params.update({'access_token': r_json['id']}) + + def _run(self, info, device): + post_job_url = 'Network/ibm-q/Groups/open/Projects/main/Jobs' + shots = info['shots'] + n_classical_reg = info['nq'] + n_qubits = self.backends[device]['nq'] + version = self.backends[device]['version'] + instructions = info['json'] + maxcredit = info['maxCredits'] + c_label = [] + q_label = [] + for i in range(n_classical_reg): + c_label.append(['c', i]) + for i in range(n_qubits): + q_label.append(['q', i]) + experiment = [{ + 'header': { + 'qreg_sizes': [['q', n_qubits]], + 'n_qubits': n_qubits, + 'memory_slots': n_classical_reg, + 'creg_sizes': [['c', n_classical_reg]], + 'clbit_labels': c_label, + 'qubit_labels': q_label, + 'name': 'circuit0' + }, + 'config': { + 'n_qubits': n_qubits, + 'memory_slots': n_classical_reg + }, + 'instructions': instructions + }] + # Note: qobj_id is not necessary in projectQ, so fixed string for now + argument = { + 'data': None, + 'json': { + 'qObject': { + 'type': 'QASM', + 'schema_version': '1.1.0', + 'config': { + 'shots': shots, + 'max_credits': maxcredit, + 'n_qubits': n_qubits, + 'memory_slots': n_classical_reg, + 'memory': False, + 'parameter_binds': [] + }, + 'experiments': experiment, + 'header': { + 'backend_version': version, + 'backend_name': device + }, + 'qobj_id': 'e72443f5-7752-4e32-9ac8-156f1f3fee18' + }, + 'backend': { + 'name': device + }, + 'shots': shots + }, + 'timeout': (self.timeout, None) + } + request = super(IBMQ, self).post(urljoin(_API_URL, post_job_url), + **argument) + request.raise_for_status() + r_json = request.json() + execution_id = r_json["id"] + return execution_id + + def _get_result(self, + device, + execution_id, + num_retries=3000, + interval=1, + verbose=False): + + job_status_url = ('Network/ibm-q/Groups/open/Projects/main/Jobs/' + + execution_id) + if verbose: + print("Waiting for results. [Job ID: {}]".format(execution_id)) -_api_url = 'https://quantumexperience.ng.bluemix.net/api/' + original_sigint_handler = signal.getsignal(signal.SIGINT) + + def _handle_sigint_during_get_result(*_): + raise Exception( + "Interrupted. The ID of your submitted job is {}.".format( + execution_id)) + + try: + signal.signal(signal.SIGINT, _handle_sigint_during_get_result) + for retries in range(num_retries): + + argument = { + 'allow_redirects': True, + 'timeout': (self.timeout, None) + } + request = super(IBMQ, + self).get(urljoin(_API_URL, job_status_url), + **argument) + request.raise_for_status() + r_json = request.json() + if r_json['status'] == 'COMPLETED': + return r_json['qObjectResult']['results'][0] + if r_json['status'] != 'RUNNING': + raise Exception("Error while running the code: {}.".format( + r_json['status'])) + time.sleep(interval) + if self.is_online(device) and retries % 60 == 0: + self.get_list_devices() + if not self.is_online(device): + raise DeviceOfflineError( + "Device went offline. The ID of " + "your submitted job is {}.".format(execution_id)) + + finally: + if original_sigint_handler is not None: + signal.signal(signal.SIGINT, original_sigint_handler) + + raise Exception("Timeout. The ID of your submitted job is {}.".format( + execution_id)) + + +class DeviceTooSmall(Exception): + pass class DeviceOfflineError(Exception): pass -def is_online(device): - url = 'Backends/{}/queue/status'.format(device) - r = requests.get(urljoin(_api_url, url)) - return r.json()['state'] +def show_devices(token=None, verbose=False): + """ + Access the list of available devices and their properties (ex: for setup + configuration) + Args: + token (str): IBM quantum experience user API token. + verbose (bool): If True, additional information is printed + + Returns: + (list) list of available devices and their properties + """ + ibmq_session = IBMQ() + ibmq_session._authenticate(token=token) + return ibmq_session.get_list_devices(verbose=verbose) -def retrieve(device, user, password, jobid, num_retries=3000, - interval=1, verbose=False): + +def retrieve(device, + token, + jobid, + num_retries=3000, + interval=1, + verbose=False): """ Retrieves a previously run job by its ID. Args: device (str): Device on which the code was run / is running. - user (str): IBM quantum experience user (e-mail) - password (str): IBM quantum experience password + token (str): IBM quantum experience user API token. jobid (str): Id of the job to retrieve + + Returns: + (dict) result form the IBMQ server """ - user_id, access_token = _authenticate(user, password) - res = _get_result(device, jobid, access_token, num_retries=num_retries, - interval=interval, verbose=verbose) + ibmq_session = IBMQ() + ibmq_session._authenticate(token) + ibmq_session.get_list_devices(verbose) + res = ibmq_session._get_result(device, + jobid, + num_retries=num_retries, + interval=interval, + verbose=verbose) return res -def send(info, device='sim_trivial_2', user=None, password=None, - shots=1, num_retries=3000, interval=1, verbose=False): +def send(info, + device='ibmq_qasm_simulator', + token=None, + shots=None, + num_retries=3000, + interval=1, + verbose=False): """ Sends QASM through the IBM API and runs the quantum circuit. Args: - info: Contains QASM representation of the circuit to run. - device (str): Either 'simulator', 'ibmqx4', or 'ibmqx5'. - user (str): IBM quantum experience user. - password (str): IBM quantum experience user password. + info(dict): Contains representation of the circuit to run. + device (str): name of the ibm device. Simulator chosen by default + token (str): IBM quantum experience user API token. shots (int): Number of runs of the same circuit to collect statistics. verbose (bool): If True, additional information is printed, such as measurement statistics. Otherwise, the backend simply registers one measurement result (same behavior as the projectq Simulator). - """ - try: - # check if the device is online - if device in ['ibmqx4', 'ibmqx5']: - online = is_online(device) - if not online: - print("The device is offline (for maintenance?). Use the " - "simulator instead or try again later.") - raise DeviceOfflineError("Device is offline.") + Returns: + (dict) result form the IBMQ server + """ + try: + ibmq_session = IBMQ() + # Shots argument deprecated, as already + if shots is not None: + info['shots'] = shots if verbose: print("- Authenticating...") - user_id, access_token = _authenticate(user, password) + if token is not None: + print('user API token: ' + token) + ibmq_session._authenticate(token) + + # check if the device is online + ibmq_session.get_list_devices(verbose) + online = ibmq_session.is_online(device) + if not online: + print("The device is offline (for maintenance?). Use the " + "simulator instead or try again later.") + raise DeviceOfflineError("Device is offline.") + + # check if the device has enough qubit to run the code + runnable, qmax, qneeded = ibmq_session.can_run_experiment(info, device) + if not runnable: + print( + ("The device is too small ({} qubits available) for the code " + + "requested({} qubits needed) Try to look for another " + + "device with more qubits").format(qmax, qneeded)) + raise DeviceTooSmall("Device is too small.") if verbose: - print("- Running code: {}".format( - json.loads(info)['qasms'][0]['qasm'])) - execution_id = _run(info, device, user_id, access_token, shots) + print("- Running code: {}".format(info)) + execution_id = ibmq_session._run(info, device) if verbose: print("- Waiting for results...") - res = _get_result(device, execution_id, access_token, - num_retries=num_retries, - interval=interval, verbose=verbose) + res = ibmq_session._get_result(device, + execution_id, + num_retries=num_retries, + interval=interval, + verbose=verbose) if verbose: print("- Done.") return res @@ -102,93 +369,3 @@ def send(info, device='sim_trivial_2', user=None, password=None, except KeyError as err: print("- Failed to parse response:") print(err) - - -def _authenticate(email=None, password=None): - """ - :param email: - :param password: - :return: - """ - if email is None: - try: - input_fun = raw_input - except NameError: - input_fun = input - email = input_fun('IBM QE user (e-mail) > ') - if password is None: - password = getpass.getpass(prompt='IBM QE password > ') - - r = requests.post(urljoin(_api_url, 'users/login'), - data={"email": email, "password": password}) - r.raise_for_status() - - json_data = r.json() - user_id = json_data['userId'] - access_token = json_data['id'] - - return user_id, access_token - - -def _run(qasm, device, user_id, access_token, shots): - suffix = 'Jobs' - - r = requests.post(urljoin(_api_url, suffix), - data=qasm, - params={"access_token": access_token, - "deviceRunType": device, - "fromCache": "false", - "shots": shots}, - headers={"Content-Type": "application/json"}) - r.raise_for_status() - - r_json = r.json() - execution_id = r_json["id"] - return execution_id - - -def _get_result(device, execution_id, access_token, num_retries=3000, - interval=1, verbose=False): - suffix = 'Jobs/{execution_id}'.format(execution_id=execution_id) - status_url = urljoin(_api_url, 'Backends/{}/queue/status'.format(device)) - - if verbose: - print("Waiting for results. [Job ID: {}]".format(execution_id)) - - original_sigint_handler = signal.getsignal(signal.SIGINT) - - def _handle_sigint_during_get_result(*_): - raise Exception("Interrupted. The ID of your submitted job is {}." - .format(execution_id)) - - try: - signal.signal(signal.SIGINT, _handle_sigint_during_get_result) - - for retries in range(num_retries): - r = requests.get(urljoin(_api_url, suffix), - params={"access_token": access_token}) - r.raise_for_status() - r_json = r.json() - if 'qasms' in r_json: - qasm = r_json['qasms'][0] - if 'result' in qasm and qasm['result'] is not None: - return qasm['result'] - time.sleep(interval) - if device in ['ibmqx4', 'ibmqx5'] and retries % 60 == 0: - r = requests.get(status_url) - r_json = r.json() - if 'state' in r_json and not r_json['state']: - raise DeviceOfflineError("Device went offline. The ID of " - "your submitted job is {}." - .format(execution_id)) - if verbose and 'lengthQueue' in r_json: - print("Currently there are {} jobs queued for execution " - "on {}." - .format(r_json['lengthQueue'], device)) - - finally: - if original_sigint_handler is not None: - signal.signal(signal.SIGINT, original_sigint_handler) - - raise Exception("Timeout. The ID of your submitted job is {}." - .format(execution_id)) diff --git a/projectq/backends/_ibm/_ibm_http_client_test.py b/projectq/backends/_ibm/_ibm_http_client_test.py index 6162fa618..eb56b1ee4 100755 --- a/projectq/backends/_ibm/_ibm_http_client_test.py +++ b/projectq/backends/_ibm/_ibm_http_client_test.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.backends._ibm_http_client._ibm.py.""" import json @@ -28,24 +27,31 @@ def no_requests(monkeypatch): monkeypatch.delattr("requests.sessions.Session.request") -_api_url = 'https://quantumexperience.ng.bluemix.net/api/' -_api_url_status = 'https://quantumexperience.ng.bluemix.net/api/' +_API_URL = 'https://api.quantum-computing.ibm.com/api/' +_AUTH_API_URL = 'https://auth.quantum-computing.ibm.com/api/users/loginWithToken' def test_send_real_device_online_verbose(monkeypatch): - qasms = {'qasms': [{'qasm': 'my qasm'}]} - json_qasm = json.dumps(qasms) + json_qasm = { + 'qasms': [{ + 'qasm': 'my qasm' + }], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1 + } name = 'projectq_test' + token = '12345' access_token = "access" user_id = 2016 code_id = 11 name_item = '"name":"{name}", "jsonQASM":'.format(name=name) - json_body = ''.join([name_item, json_qasm]) + json_body = ''.join([name_item, json.dumps(json_qasm)]) json_data = ''.join(['{', json_body, '}']) shots = 1 device = "ibmqx4" - json_data_run = ''.join(['{"qasm":', json_qasm, '}']) - execution_id = 3 + execution_id = '3' result_ready = [False] result = "my_result" request_num = [0] # To assert correct order of calls @@ -70,24 +76,39 @@ def raise_for_status(self): pass # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if (args[0] == urljoin(_api_url_status, status_url) and - (request_num[0] == 0 or request_num[0] == 3)): + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if (args[1] == urljoin(_API_URL, status_url) + and (request_num[0] == 1 or request_num[0] == 4)): request_num[0] += 1 - return MockResponse({"state": True}, 200) + connections = set([(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), (2, 1), + (2, 3), (2, 4), (3, 1), (3, 4), (4, 3)]) + return MockResponse([{ + 'backend_name': 'ibmqx4', + 'coupling_map': connections, + 'backend_version': '0.1.547', + 'n_qubits': 32 + }], 200) # Getting result - elif (args[0] == urljoin(_api_url, - "Jobs/{execution_id}".format(execution_id=execution_id)) and - kwargs["params"]["access_token"] == access_token and not - result_ready[0] and request_num[0] == 3): + elif (args[1] == urljoin( + _API_URL, + "Network/ibm-q/Groups/open/Projects/main/Jobs/{execution_id}". + format(execution_id=execution_id)) and not result_ready[0] + and request_num[0] == 3): result_ready[0] = True - return MockResponse({"status": {"id": "NotDone"}}, 200) - elif (args[0] == urljoin(_api_url, - "Jobs/{execution_id}".format(execution_id=execution_id)) and - kwargs["params"]["access_token"] == access_token and - result_ready[0] and request_num[0] == 4): - print("state ok") - return MockResponse({"qasms": [{"result": result}]}, 200) + request_num[0] += 1 + return MockResponse({"status": "RUNNING"}, 200) + elif (args[1] == urljoin( + _API_URL, + "Network/ibm-q/Groups/open/Projects/main/Jobs/{execution_id}". + format(execution_id=execution_id)) and result_ready[0] + and request_num[0] == 5): + return MockResponse( + { + 'qObjectResult': { + "results": [result] + }, + "status": "COMPLETED" + }, 200) def mocked_requests_post(*args, **kwargs): class MockRequest: @@ -107,49 +128,69 @@ def json(self): def raise_for_status(self): pass + jobs_url = 'Network/ibm-q/Groups/open/Projects/main/Jobs' # Authentication - if (args[0] == urljoin(_api_url, "users/login") and - kwargs["data"]["email"] == email and - kwargs["data"]["password"] == password and - request_num[0] == 1): + if (args[1] == _AUTH_API_URL and kwargs["json"]["apiToken"] == token + and request_num[0] == 0): request_num[0] += 1 return MockPostResponse({"userId": user_id, "id": access_token}) # Run code - elif (args[0] == urljoin(_api_url, "Jobs") and - kwargs["data"] == json_qasm and - kwargs["params"]["access_token"] == access_token and - kwargs["params"]["deviceRunType"] == device and - kwargs["params"]["fromCache"] == "false" and - kwargs["params"]["shots"] == shots and - kwargs["headers"]["Content-Type"] == "application/json" and - request_num[0] == 2): + elif (args[1] == urljoin(_API_URL, jobs_url) and kwargs["data"] is None + and kwargs["json"]["backend"]["name"] == device + and kwargs["json"]["qObject"]['config']['shots'] == shots + and request_num[0] == 2): request_num[0] += 1 return MockPostResponse({"id": execution_id}) - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) - # Patch login data - password = 12345 - email = "test@projectq.ch" - monkeypatch.setitem(__builtins__, "input", lambda x: email) - monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) def user_password_input(prompt): - if prompt == "IBM QE password > ": - return password + if prompt == "IBM QE token > ": + return token monkeypatch.setattr("getpass.getpass", user_password_input) # Code to test: res = _ibm_http_client.send(json_qasm, device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) - print(res) + token=None, + shots=shots, + verbose=True) assert res == result + json_qasm['nq'] = 40 + request_num[0] = 0 + with pytest.raises(_ibm_http_client.DeviceTooSmall): + res = _ibm_http_client.send(json_qasm, + device="ibmqx4", + token=None, + shots=shots, + verbose=True) + + +def test_no_password_given(monkeypatch): + token = '' + json_qasm = '' + + def user_password_input(prompt): + if prompt == "IBM QE token > ": + return token + + monkeypatch.setattr("getpass.getpass", user_password_input) + + with pytest.raises(Exception): + res = _ibm_http_client.send(json_qasm, + device="ibmqx4", + token=None, + shots=1, + verbose=True) def test_send_real_device_offline(monkeypatch): + token = '12345' + access_token = "access" + user_id = 2016 + def mocked_requests_get(*args, **kwargs): class MockResponse: def __init__(self, json_data, status_code): @@ -159,22 +200,63 @@ def __init__(self, json_data, status_code): def json(self): return self.json_data - # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url_status, status_url): - return MockResponse({"state": False}, 200) - monkeypatch.setattr("requests.get", mocked_requests_get) + def raise_for_status(self): + pass + + # Accessing status of device. Return offline. + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url): + return MockResponse({}, 200) + + def mocked_requests_post(*args, **kwargs): + class MockRequest: + def __init__(self, body="", url=""): + self.body = body + self.url = url + + class MockPostResponse: + def __init__(self, json_data, text=" "): + self.json_data = json_data + self.text = text + self.request = MockRequest() + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + # Authentication + if (args[1] == _AUTH_API_URL and kwargs["json"]["apiToken"] == token): + return MockPostResponse({"userId": user_id, "id": access_token}) + + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + shots = 1 - json_qasm = "my_json_qasm" + token = '12345' + json_qasm = { + 'qasms': [{ + 'qasm': 'my qasm' + }], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1 + } name = 'projectq_test' with pytest.raises(_ibm_http_client.DeviceOfflineError): _ibm_http_client.send(json_qasm, device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) + token=token, + shots=shots, + verbose=True) -def test_send_that_errors_are_caught(monkeypatch): +def test_show_device(monkeypatch): + access_token = "access" + user_id = 2016 + class MockResponse: def __init__(self, json_data, status_code): self.json_data = json_data @@ -183,123 +265,191 @@ def __init__(self, json_data, status_code): def json(self): return self.json_data + def raise_for_status(self): + pass + def mocked_requests_get(*args, **kwargs): # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url_status, status_url): - return MockResponse({"state": True}, 200) + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url): + connections = set([(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), (2, 1), + (2, 3), (2, 4), (3, 1), (3, 4), (4, 3)]) + return MockResponse([{ + 'backend_name': 'ibmqx4', + 'coupling_map': connections, + 'backend_version': '0.1.547', + 'n_qubits': 32 + }], 200) + + def mocked_requests_post(*args, **kwargs): + class MockRequest: + def __init__(self, body="", url=""): + self.body = body + self.url = url + + class MockPostResponse: + def __init__(self, json_data, text=" "): + self.json_data = json_data + self.text = text + self.request = MockRequest() + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + # Authentication + if (args[1] == _AUTH_API_URL and kwargs["json"]["apiToken"] == token): + return MockPostResponse({"userId": user_id, "id": access_token}) + + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + # Patch login data + token = '12345' + + def user_password_input(prompt): + if prompt == "IBM QE token > ": + return token + + monkeypatch.setattr("getpass.getpass", user_password_input) + assert _ibm_http_client.show_devices() == { + 'ibmqx4': { + 'coupling_map': {(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), (2, 1), + (2, 3), (2, 4), (3, 1), (3, 4), (4, 3)}, + 'version': '0.1.547', + 'nq': 32 + } + } + + +def test_send_that_errors_are_caught(monkeypatch): + class MockResponse: + def __init__(self, json_data, status_code): + pass def mocked_requests_post(*args, **kwargs): # Test that this error gets caught raise requests.exceptions.HTTPError - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) # Patch login data - password = 12345 - email = "test@projectq.ch" - monkeypatch.setitem(__builtins__, "input", lambda x: email) - monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) + token = '12345' def user_password_input(prompt): - if prompt == "IBM QE password > ": - return password + if prompt == "IBM QE token > ": + return token monkeypatch.setattr("getpass.getpass", user_password_input) shots = 1 - json_qasm = "my_json_qasm" + json_qasm = { + 'qasms': [{ + 'qasm': 'my qasm' + }], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1 + } name = 'projectq_test' _ibm_http_client.send(json_qasm, device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) - + token=None, + shots=shots, + verbose=True) -def test_send_that_errors_are_caught2(monkeypatch): - def mocked_requests_get(*args, **kwargs): - class MockResponse: - def __init__(self, json_data, status_code): - self.json_data = json_data - self.status_code = status_code + token = '' + with pytest.raises(Exception): + _ibm_http_client.send(json_qasm, + device="ibmqx4", + token=None, + shots=shots, + verbose=True) - def json(self): - return self.json_data - # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url_status, status_url): - return MockResponse({"state": True}, 200) +def test_send_that_errors_are_caught2(monkeypatch): + class MockResponse: + def __init__(self, json_data, status_code): + pass def mocked_requests_post(*args, **kwargs): # Test that this error gets caught raise requests.exceptions.RequestException - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) # Patch login data - password = 12345 - email = "test@projectq.ch" - monkeypatch.setitem(__builtins__, "input", lambda x: email) - monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) + token = '12345' def user_password_input(prompt): - if prompt == "IBM QE password > ": - return password + if prompt == "IBM QE token > ": + return token monkeypatch.setattr("getpass.getpass", user_password_input) shots = 1 - json_qasm = "my_json_qasm" + json_qasm = { + 'qasms': [{ + 'qasm': 'my qasm' + }], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1 + } name = 'projectq_test' _ibm_http_client.send(json_qasm, device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) + token=None, + shots=shots, + verbose=True) def test_send_that_errors_are_caught3(monkeypatch): - def mocked_requests_get(*args, **kwargs): - class MockResponse: - def __init__(self, json_data, status_code): - self.json_data = json_data - self.status_code = status_code - - def json(self): - return self.json_data - - # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url_status, status_url): - return MockResponse({"state": True}, 200) + class MockResponse: + def __init__(self, json_data, status_code): + pass def mocked_requests_post(*args, **kwargs): # Test that this error gets caught raise KeyError - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) # Patch login data - password = 12345 - email = "test@projectq.ch" - monkeypatch.setitem(__builtins__, "input", lambda x: email) - monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) + token = '12345' def user_password_input(prompt): - if prompt == "IBM QE password > ": - return password + if prompt == "IBM QE token > ": + return token monkeypatch.setattr("getpass.getpass", user_password_input) shots = 1 - json_qasm = "my_json_qasm" + json_qasm = { + 'qasms': [{ + 'qasm': 'my qasm' + }], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1 + } name = 'projectq_test' _ibm_http_client.send(json_qasm, device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) + token=None, + shots=shots, + verbose=True) def test_timeout_exception(monkeypatch): - qasms = {'qasms': [{'qasm': 'my qasm'}]} - json_qasm = json.dumps(qasms) + qasms = { + 'qasms': [{ + 'qasm': 'my qasm' + }], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1 + } + json_qasm = qasms tries = [0] def mocked_requests_get(*args, **kwargs): @@ -314,14 +464,22 @@ def json(self): def raise_for_status(self): pass - # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url, status_url): - return MockResponse({"state": True}, 200) - job_url = 'Jobs/{}'.format("123e") - if args[0] == urljoin(_api_url, job_url): + # Accessing status of device. Return device info. + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url): + connections = set([(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), (2, 1), + (2, 3), (2, 4), (3, 1), (3, 4), (4, 3)]) + return MockResponse([{ + 'backend_name': 'ibmqx4', + 'coupling_map': connections, + 'backend_version': '0.1.547', + 'n_qubits': 32 + }], 200) + job_url = "Network/ibm-q/Groups/open/Projects/main/Jobs/{}".format( + "123e") + if args[1] == urljoin(_API_URL, job_url): tries[0] += 1 - return MockResponse({"noqasms": "not done"}, 200) + return MockResponse({"status": "RUNNING"}, 200) def mocked_requests_post(*args, **kwargs): class MockRequest: @@ -340,27 +498,28 @@ def json(self): def raise_for_status(self): pass - login_url = 'users/login' - if args[0] == urljoin(_api_url, login_url): + jobs_url = 'Network/ibm-q/Groups/open/Projects/main/Jobs' + if args[1] == _AUTH_API_URL: return MockPostResponse({"userId": "1", "id": "12"}) - if args[0] == urljoin(_api_url, 'Jobs'): + if args[1] == urljoin(_API_URL, jobs_url): return MockPostResponse({"id": "123e"}) - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + _ibm_http_client.time.sleep = lambda x: x with pytest.raises(Exception) as excinfo: _ibm_http_client.send(json_qasm, device="ibmqx4", - user="test", password="test", - shots=1, verbose=False) + token="test", + shots=1, + num_retries=10, + verbose=False) assert "123e" in str(excinfo.value) # check that job id is in exception assert tries[0] > 0 def test_retrieve_and_device_offline_exception(monkeypatch): - qasms = {'qasms': [{'qasm': 'my qasm'}]} - json_qasm = json.dumps(qasms) request_num = [0] def mocked_requests_get(*args, **kwargs): @@ -376,15 +535,41 @@ def raise_for_status(self): pass # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url, status_url) and request_num[0] < 2: - return MockResponse({"state": True, "lengthQueue": 10}, 200) - elif args[0] == urljoin(_api_url, status_url): - return MockResponse({"state": False}, 200) - job_url = 'Jobs/{}'.format("123e") - if args[0] == urljoin(_api_url, job_url): + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url) and request_num[0] < 2: + return MockResponse([{ + 'backend_name': 'ibmqx4', + 'coupling_map': None, + 'backend_version': '0.1.547', + 'n_qubits': 32 + }], 200) + elif args[1] == urljoin( + _API_URL, + status_url): # ibmqx4 gets disconnected, replaced by ibmqx5 + return MockResponse([{ + 'backend_name': 'ibmqx5', + 'coupling_map': None, + 'backend_version': '0.1.547', + 'n_qubits': 32 + }], 200) + job_url = "Network/ibm-q/Groups/open/Projects/main/Jobs/{}".format( + "123e") + err_url = "Network/ibm-q/Groups/open/Projects/main/Jobs/{}".format( + "123ee") + if args[1] == urljoin(_API_URL, job_url): + request_num[0] += 1 + return MockResponse( + { + "status": "RUNNING", + 'iteration': request_num[0] + }, 200) + if args[1] == urljoin(_API_URL, err_url): request_num[0] += 1 - return MockResponse({"noqasms": "not done"}, 200) + return MockResponse( + { + "status": "TERMINATED", + 'iteration': request_num[0] + }, 400) def mocked_requests_post(*args, **kwargs): class MockRequest: @@ -403,22 +588,26 @@ def json(self): def raise_for_status(self): pass - login_url = 'users/login' - if args[0] == urljoin(_api_url, login_url): + if args[1] == _AUTH_API_URL: return MockPostResponse({"userId": "1", "id": "12"}) - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + _ibm_http_client.time.sleep = lambda x: x with pytest.raises(_ibm_http_client.DeviceOfflineError): _ibm_http_client.retrieve(device="ibmqx4", - user="test", password="test", - jobid="123e") + token="test", + jobid="123e", + num_retries=200) + with pytest.raises(Exception): + _ibm_http_client.retrieve(device="ibmqx4", + token="test", + jobid="123ee", + num_retries=200) def test_retrieve(monkeypatch): - qasms = {'qasms': [{'qasm': 'my qasm'}]} - json_qasm = json.dumps(qasms) request_num = [0] def mocked_requests_get(*args, **kwargs): @@ -434,16 +623,28 @@ def raise_for_status(self): pass # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url, status_url): - return MockResponse({"state": True}, 200) - job_url = 'Jobs/{}'.format("123e") - if args[0] == urljoin(_api_url, job_url) and request_num[0] < 1: + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url): + return MockResponse([{ + 'backend_name': 'ibmqx4', + 'coupling_map': None, + 'backend_version': '0.1.547', + 'n_qubits': 32 + }], 200) + job_url = 'Network/ibm-q/Groups/open/Projects/main/Jobs/{}'.format( + "123e") + if args[1] == urljoin(_API_URL, job_url) and request_num[0] < 1: request_num[0] += 1 - return MockResponse({"noqasms": "not done"}, 200) - elif args[0] == urljoin(_api_url, job_url): - return MockResponse({"qasms": [{'qasm': 'qasm', - 'result': 'correct'}]}, 200) + return MockResponse({"status": "RUNNING"}, 200) + elif args[1] == urljoin(_API_URL, job_url): + return MockResponse( + { + "qObjectResult": { + 'qasm': 'qasm', + 'results': ['correct'] + }, + "status": "COMPLETED" + }, 200) def mocked_requests_post(*args, **kwargs): class MockRequest: @@ -462,14 +663,14 @@ def json(self): def raise_for_status(self): pass - login_url = 'users/login' - if args[0] == urljoin(_api_url, login_url): + if args[1] == _AUTH_API_URL: return MockPostResponse({"userId": "1", "id": "12"}) - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + _ibm_http_client.time.sleep = lambda x: x res = _ibm_http_client.retrieve(device="ibmqx4", - user="test", password="test", + token="test", jobid="123e") assert res == 'correct' diff --git a/projectq/backends/_ibm/_ibm_test.py b/projectq/backends/_ibm/_ibm_test.py index df1652b7a..f6890d34c 100755 --- a/projectq/backends/_ibm/_ibm_test.py +++ b/projectq/backends/_ibm/_ibm_test.py @@ -11,27 +11,18 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.backends._ibm._ibm.py.""" import pytest -import json - -import projectq.setups.decompositions +import math +from projectq.setups import restrictedgateset from projectq import MainEngine from projectq.backends._ibm import _ibm -from projectq.cengines import (TagRemover, - LocalOptimizer, - AutoReplacer, - IBM5QubitMapper, - SwapAndCNOTFlipper, - DummyEngine, - DecompositionRuleSet) +from projectq.cengines import (BasicMapperEngine, DummyEngine) + from projectq.ops import (All, Allocate, Barrier, Command, Deallocate, Entangle, Measure, NOT, Rx, Ry, Rz, S, Sdag, T, Tdag, - X, Y, Z) - -from projectq.setups.ibm import ibmqx4_connections + X, Y, Z, H, CNOT) # Insure that no HTTP request can be made in all tests in this module @@ -40,31 +31,29 @@ def no_requests(monkeypatch): monkeypatch.delattr("requests.sessions.Session.request") -_api_url = 'https://quantumexperience.ng.bluemix.net/api/' -_api_url_status = 'https://quantumexperience.ng.bluemix.net/api/' - - -@pytest.mark.parametrize("single_qubit_gate, is_available", [ - (X, True), (Y, True), (Z, True), (T, True), (Tdag, True), (S, True), - (Sdag, True), (Allocate, True), (Deallocate, True), (Measure, True), - (NOT, True), (Rx(0.5), True), (Ry(0.5), True), (Rz(0.5), True), - (Barrier, True), (Entangle, False)]) +@pytest.mark.parametrize("single_qubit_gate, is_available", + [(X, False), (Y, False), (Z, False), (H, True), + (T, False), (Tdag, False), (S, False), (Sdag, False), + (Allocate, True), (Deallocate, True), + (Measure, True), (NOT, False), (Rx(0.5), True), + (Ry(0.5), True), (Rz(0.5), True), (Barrier, True), + (Entangle, False)]) def test_ibm_backend_is_available(single_qubit_gate, is_available): eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) qubit1 = eng.allocate_qubit() ibm_backend = _ibm.IBMBackend() - cmd = Command(eng, single_qubit_gate, (qubit1,)) + cmd = Command(eng, single_qubit_gate, (qubit1, )) assert ibm_backend.is_available(cmd) == is_available -@pytest.mark.parametrize("num_ctrl_qubits, is_available", [ - (0, True), (1, True), (2, False), (3, False)]) +@pytest.mark.parametrize("num_ctrl_qubits, is_available", + [(0, False), (1, True), (2, False), (3, False)]) def test_ibm_backend_is_available_control_not(num_ctrl_qubits, is_available): eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) qubit1 = eng.allocate_qubit() qureg = eng.allocate_qureg(num_ctrl_qubits) ibm_backend = _ibm.IBMBackend() - cmd = Command(eng, NOT, (qubit1,), controls=qureg) + cmd = Command(eng, NOT, (qubit1, ), controls=qureg) assert ibm_backend.is_available(cmd) == is_available @@ -83,14 +72,17 @@ def test_ibm_sent_error(monkeypatch): # patch send def mock_send(*args, **kwargs): raise TypeError - monkeypatch.setattr(_ibm, "send", mock_send) + monkeypatch.setattr(_ibm, "send", mock_send) backend = _ibm.IBMBackend(verbose=True) - eng = MainEngine(backend=backend, - engine_list=[IBM5QubitMapper(), - SwapAndCNOTFlipper(set())]) + mapper = BasicMapperEngine() + res = dict() + for i in range(4): + res[i] = i + mapper.current_mapping = res + eng = MainEngine(backend=backend, engine_list=[mapper]) qubit = eng.allocate_qubit() - X | qubit + Rx(math.pi) | qubit with pytest.raises(Exception): qubit[0].__del__() eng.flush() @@ -100,26 +92,80 @@ def mock_send(*args, **kwargs): eng.next_engine = dummy +def test_ibm_sent_error_2(monkeypatch): + backend = _ibm.IBMBackend(verbose=True) + mapper = BasicMapperEngine() + res = dict() + for i in range(4): + res[i] = i + mapper.current_mapping = res + eng = MainEngine(backend=backend, engine_list=[mapper]) + qubit = eng.allocate_qubit() + Rx(math.pi) | qubit + + with pytest.raises(Exception): + S | qubit # no setup to decompose S gate, so not accepted by the backend + dummy = DummyEngine() + dummy.is_last_engine = True + eng.next_engine = dummy + + def test_ibm_retrieve(monkeypatch): # patch send def mock_retrieve(*args, **kwargs): - return {'date': '2017-01-19T14:28:47.622Z', - 'data': {'time': 14.429004907608032, 'counts': {'00111': 396, - '00101': 27, - '00000': 601}, - 'qasm': ('...')}} + return { + 'data': { + 'counts': { + '0x0': 504, + '0x2': 8, + '0xc': 6, + '0xe': 482 + } + }, + 'header': { + 'clbit_labels': [['c', 0], ['c', 1], ['c', 2], ['c', 3]], + 'creg_sizes': [['c', 4]], + 'memory_slots': + 4, + 'n_qubits': + 32, + 'name': + 'circuit0', + 'qreg_sizes': [['q', 32]], + 'qubit_labels': [['q', 0], ['q', 1], ['q', 2], ['q', 3], + ['q', 4], ['q', 5], ['q', 6], ['q', 7], + ['q', 8], ['q', 9], ['q', 10], ['q', 11], + ['q', 12], ['q', 13], ['q', 14], ['q', 15], + ['q', 16], ['q', 17], ['q', 18], ['q', 19], + ['q', 20], ['q', 21], ['q', 22], ['q', 23], + ['q', 24], ['q', 25], ['q', 26], ['q', 27], + ['q', 28], ['q', 29], ['q', 30], ['q', 31]] + }, + 'metadata': { + 'measure_sampling': True, + 'method': 'statevector', + 'parallel_shots': 1, + 'parallel_state_update': 16 + }, + 'seed_simulator': 465435780, + 'shots': 1000, + 'status': 'DONE', + 'success': True, + 'time_taken': 0.0045786460000000005 + } + monkeypatch.setattr(_ibm, "retrieve", mock_retrieve) - backend = _ibm.IBMBackend(retrieve_execution="ab1s2") - rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions]) - connectivity = set([(1, 2), (2, 4), (0, 2), (3, 2), (4, 3), (0, 1)]) - engine_list = [TagRemover(), - LocalOptimizer(10), - AutoReplacer(rule_set), - TagRemover(), - IBM5QubitMapper(), - SwapAndCNOTFlipper(connectivity), - LocalOptimizer(10)] - eng = MainEngine(backend=backend, engine_list=engine_list) + backend = _ibm.IBMBackend(retrieve_execution="ab1s2", num_runs=1000) + mapper = BasicMapperEngine() + res = dict() + for i in range(4): + res[i] = i + mapper.current_mapping = res + ibm_setup = [mapper] + setup = restrictedgateset.get_engine_list(one_qubit_gates=(Rx, Ry, Rz, H), + two_qubit_gates=(CNOT, )) + setup.extend(ibm_setup) + eng = MainEngine(backend=backend, engine_list=setup) unused_qubit = eng.allocate_qubit() qureg = eng.allocate_qureg(3) # entangle the qureg @@ -134,43 +180,135 @@ def mock_retrieve(*args, **kwargs): # run the circuit eng.flush() prob_dict = eng.backend.get_probabilities([qureg[0], qureg[2], qureg[1]]) - assert prob_dict['111'] == pytest.approx(0.38671875) - assert prob_dict['101'] == pytest.approx(0.0263671875) + assert prob_dict['000'] == pytest.approx(0.504) + assert prob_dict['111'] == pytest.approx(0.482) + assert prob_dict['011'] == pytest.approx(0.006) def test_ibm_backend_functional_test(monkeypatch): - correct_info = ('{"qasms": [{"qasm": "\\ninclude \\"qelib1.inc\\";' - '\\nqreg q[3];\\ncreg c[3];\\nh q[2];\\ncx q[2], q[0];' - '\\ncx q[2], q[1];\\ntdg q[2];\\nsdg q[2];' - '\\nbarrier q[2], q[0], q[1];' - '\\nu3(0.2, -pi/2, pi/2) q[2];\\nmeasure q[2] -> ' - 'c[2];\\nmeasure q[0] -> c[0];\\nmeasure q[1] -> c[1];"}]' - ', "shots": 1024, "maxCredits": 5, "backend": {"name": ' - '"simulator"}}') + correct_info = { + 'json': [{ + 'qubits': [1], + 'name': 'u2', + 'params': [0, 3.141592653589793] + }, { + 'qubits': [1, 2], + 'name': 'cx' + }, { + 'qubits': [1, 3], + 'name': 'cx' + }, { + 'qubits': [1], + 'name': 'u3', + 'params': [6.28318530718, 0, 0] + }, { + 'qubits': [1], + 'name': 'u1', + 'params': [11.780972450962] + }, { + 'qubits': [1], + 'name': 'u3', + 'params': [6.28318530718, 0, 0] + }, { + 'qubits': [1], + 'name': 'u1', + 'params': [10.995574287564] + }, { + 'qubits': [1, 2, 3], + 'name': 'barrier' + }, { + 'qubits': [1], + 'name': 'u3', + 'params': [0.2, -1.5707963267948966, 1.5707963267948966] + }, { + 'qubits': [1], + 'name': 'measure', + 'memory': [1] + }, { + 'qubits': [2], + 'name': 'measure', + 'memory': [2] + }, { + 'qubits': [3], + 'name': 'measure', + 'memory': [3] + }], + 'nq': + 4, + 'shots': + 1000, + 'maxCredits': + 10, + 'backend': { + 'name': 'ibmq_qasm_simulator' + } + } + # {'qasms': [{'qasm': '\ninclude "qelib1.inc";\nqreg q[4];\ncreg c[4];\nu2(0,pi/2) q[1];\ncx q[1], q[2];\ncx q[1], q[3];\nu3(6.28318530718, 0, 0) q[1];\nu1(11.780972450962) q[1];\nu3(6.28318530718, 0, 0) q[1];\nu1(10.995574287564) q[1];\nu3(0.2, -pi/2, pi/2) q[1];\nmeasure q[1] -> c[1];\nmeasure q[2] -> c[2];\nmeasure q[3] -> c[3];'}], 'json': [{'qubits': [1], 'name': 'u2', 'params': [0, 3.141592653589793]}, {'qubits': [1, 2], 'name': 'cx'}, {'qubits': [1, 3], 'name': 'cx'}, {'qubits': [1], 'name': 'u3', 'params': [6.28318530718, 0, 0]}, {'qubits': [1], 'name': 'u1', 'params': [11.780972450962]}, {'qubits': [1], 'name': 'u3', 'params': [6.28318530718, 0, 0]}, {'qubits': [1], 'name': 'u1', 'params': [10.995574287564]}, {'qubits': [1], 'name': 'u3', 'params': [0.2, -1.5707963267948966, 1.5707963267948966]}, {'qubits': [1], 'name': 'measure', 'memory': [1]}, {'qubits': [2], 'name': 'measure', 'memory': [2]}, {'qubits': [3], 'name': 'measure', 'memory': [3]}], 'nq': 4, 'shots': 1000, 'maxCredits': 10, 'backend': {'name': 'ibmq_qasm_simulator'}} def mock_send(*args, **kwargs): - assert json.loads(args[0]) == json.loads(correct_info) - return {'date': '2017-01-19T14:28:47.622Z', - 'data': {'time': 14.429004907608032, 'counts': {'00111': 396, - '00101': 27, - '00000': 601}, - 'qasm': ('...')}} + assert args[0] == correct_info + return { + 'data': { + 'counts': { + '0x0': 504, + '0x2': 8, + '0xc': 6, + '0xe': 482 + } + }, + 'header': { + 'clbit_labels': [['c', 0], ['c', 1], ['c', 2], ['c', 3]], + 'creg_sizes': [['c', 4]], + 'memory_slots': + 4, + 'n_qubits': + 32, + 'name': + 'circuit0', + 'qreg_sizes': [['q', 32]], + 'qubit_labels': [['q', 0], ['q', 1], ['q', 2], ['q', 3], + ['q', 4], ['q', 5], ['q', 6], ['q', 7], + ['q', 8], ['q', 9], ['q', 10], ['q', 11], + ['q', 12], ['q', 13], ['q', 14], ['q', 15], + ['q', 16], ['q', 17], ['q', 18], ['q', 19], + ['q', 20], ['q', 21], ['q', 22], ['q', 23], + ['q', 24], ['q', 25], ['q', 26], ['q', 27], + ['q', 28], ['q', 29], ['q', 30], ['q', 31]] + }, + 'metadata': { + 'measure_sampling': True, + 'method': 'statevector', + 'parallel_shots': 1, + 'parallel_state_update': 16 + }, + 'seed_simulator': 465435780, + 'shots': 1000, + 'status': 'DONE', + 'success': True, + 'time_taken': 0.0045786460000000005 + } + monkeypatch.setattr(_ibm, "send", mock_send) - backend = _ibm.IBMBackend(verbose=True) + backend = _ibm.IBMBackend(verbose=True, num_runs=1000) + import sys # no circuit has been executed -> raises exception with pytest.raises(RuntimeError): backend.get_probabilities([]) - rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions]) - - engine_list = [TagRemover(), - LocalOptimizer(10), - AutoReplacer(rule_set), - TagRemover(), - IBM5QubitMapper(), - SwapAndCNOTFlipper(ibmqx4_connections), - LocalOptimizer(10)] - eng = MainEngine(backend=backend, engine_list=engine_list) + mapper = BasicMapperEngine() + res = dict() + for i in range(4): + res[i] = i + mapper.current_mapping = res + ibm_setup = [mapper] + setup = restrictedgateset.get_engine_list(one_qubit_gates=(Rx, Ry, Rz, H), + two_qubit_gates=(CNOT, ), + other_gates=(Barrier, )) + setup.extend(ibm_setup) + eng = MainEngine(backend=backend, engine_list=setup) + # 4 qubits circuit is run, but first is unused to test ability for + # get_probability to return the correct values for a subset of the total + # register unused_qubit = eng.allocate_qubit() qureg = eng.allocate_qureg(3) # entangle the qureg @@ -184,9 +322,21 @@ def mock_send(*args, **kwargs): All(Measure) | qureg # run the circuit eng.flush() - prob_dict = eng.backend.get_probabilities([qureg[0], qureg[2], qureg[1]]) - assert prob_dict['111'] == pytest.approx(0.38671875) - assert prob_dict['101'] == pytest.approx(0.0263671875) + prob_dict = eng.backend.get_probabilities([qureg[2], qureg[1]]) + assert prob_dict['00'] == pytest.approx(0.512) + assert prob_dict['11'] == pytest.approx(0.488) + result = "\nu2(0,pi/2) q[1];\ncx q[1], q[2];\ncx q[1], q[3];" + if sys.version_info.major == 3: + result += "\nu3(6.28318530718, 0, 0) q[1];\nu1(11.780972450962) q[1];" + result += "\nu3(6.28318530718, 0, 0) q[1];\nu1(10.995574287564) q[1];" + else: + result += "\nu3(6.28318530718, 0, 0) q[1];\nu1(11.780972451) q[1];" + result += "\nu3(6.28318530718, 0, 0) q[1];\nu1(10.9955742876) q[1];" + result += "\nbarrier q[1], q[2], q[3];" + result += "\nu3(0.2, -pi/2, pi/2) q[1];\nmeasure q[1] -> c[1];" + result += "\nmeasure q[2] -> c[2];\nmeasure q[3] -> c[3];" + + assert eng.backend.get_qasm() == result with pytest.raises(RuntimeError): eng.backend.get_probabilities(eng.allocate_qubit()) diff --git a/projectq/cengines/_basicmapper.py b/projectq/cengines/_basicmapper.py index 4d4cef177..5fc0f9a81 100644 --- a/projectq/cengines/_basicmapper.py +++ b/projectq/cengines/_basicmapper.py @@ -81,3 +81,7 @@ def add_logical_id(command, old_tags=deepcopy(cmd.tags)): drop_engine_after(self) else: self.send([new_cmd]) + + def receive(self, command_list): + for cmd in command_list: + self._send_cmd_with_mapped_ids(cmd) diff --git a/projectq/cengines/_ibm5qubitmapper.py b/projectq/cengines/_ibm5qubitmapper.py index 7a2659a30..2c85749d2 100755 --- a/projectq/cengines/_ibm5qubitmapper.py +++ b/projectq/cengines/_ibm5qubitmapper.py @@ -11,12 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Contains a compiler engine to map to the 5-qubit IBM chip """ -from copy import deepcopy - import itertools from projectq.cengines import BasicMapperEngine @@ -39,8 +36,7 @@ class IBM5QubitMapper(BasicMapperEngine): without performing Swaps, the mapping procedure **raises an Exception**. """ - - def __init__(self): + def __init__(self, connections=None): """ Initialize an IBM 5-qubit mapper compiler engine. @@ -49,6 +45,16 @@ def __init__(self): BasicMapperEngine.__init__(self) self.current_mapping = dict() self._reset() + self._cmds = [] + self._interactions = dict() + + if connections is None: + #general connectivity easier for testing functions + self.connections = set([(0, 1), (1, 0), (1, 2), (1, 3), (1, 4), + (2, 1), (2, 3), (2, 4), (3, 1), (3, 4), + (4, 3)]) + else: + self.connections = connections def is_available(self, cmd): """ @@ -67,17 +73,6 @@ def _reset(self): self._cmds = [] self._interactions = dict() - def _is_cnot(self, cmd): - """ - Check if the command corresponds to a CNOT (controlled NOT gate). - - Args: - cmd (Command): Command to check whether it is a controlled NOT - gate. - """ - return (isinstance(cmd.gate, NOT.__class__) and - get_control_count(cmd) == 1) - def _determine_cost(self, mapping): """ Determines the cost of the circuit with the given mapping. @@ -90,15 +85,15 @@ def _determine_cost(self, mapping): Cost measure taking into account CNOT directionality or None if the circuit cannot be executed given the mapping. """ - from projectq.setups.ibm import ibmqx4_connections as connections + cost = 0 for tpl in self._interactions: ctrl_id = tpl[0] target_id = tpl[1] ctrl_pos = mapping[ctrl_id] target_pos = mapping[target_id] - if not (ctrl_pos, target_pos) in connections: - if (target_pos, ctrl_pos) in connections: + if not (ctrl_pos, target_pos) in self.connections: + if (target_pos, ctrl_pos) in self.connections: cost += self._interactions[tpl] else: return None @@ -114,20 +109,22 @@ def _run(self): the mapping was already determined but more CNOTs get sent down the pipeline. """ - if (len(self.current_mapping) > 0 and - max(self.current_mapping.values()) > 4): + if (len(self.current_mapping) > 0 + and max(self.current_mapping.values()) > 4): raise RuntimeError("Too many qubits allocated. The IBM Q " "device supports at most 5 qubits and no " "intermediate measurements / " "reallocations.") if len(self._interactions) > 0: - logical_ids = [qbid for qbid in self.current_mapping] + logical_ids = list(self.current_mapping) best_mapping = self.current_mapping best_cost = None for physical_ids in itertools.permutations(list(range(5)), len(logical_ids)): - mapping = {logical_ids[i]: physical_ids[i] - for i in range(len(logical_ids))} + mapping = { + logical_ids[i]: physical_ids[i] + for i in range(len(logical_ids)) + } new_cost = self._determine_cost(mapping) if new_cost is not None: if best_cost is None or new_cost < best_cost: @@ -153,7 +150,7 @@ def _store(self, cmd): """ if not cmd.gate == FlushGate(): target = cmd.qubits[0][0].id - if self._is_cnot(cmd): + if _is_cnot(cmd): # CNOT encountered ctrl = cmd.control_qubits[0].id if not (ctrl, target) in self._interactions: @@ -187,3 +184,15 @@ def receive(self, command_list): if isinstance(cmd.gate, FlushGate): self._run() self._reset() + + +def _is_cnot(cmd): + """ + Check if the command corresponds to a CNOT (controlled NOT gate). + + Args: + cmd (Command): Command to check whether it is a controlled NOT + gate. + """ + return (isinstance(cmd.gate, NOT.__class__) + and get_control_count(cmd) == 1) diff --git a/projectq/cengines/_ibm5qubitmapper_test.py b/projectq/cengines/_ibm5qubitmapper_test.py index 5c4c4c4da..ea6d383b6 100755 --- a/projectq/cengines/_ibm5qubitmapper_test.py +++ b/projectq/cengines/_ibm5qubitmapper_test.py @@ -11,14 +11,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._ibm5qubitmapper.py.""" import pytest from projectq import MainEngine from projectq.cengines import DummyEngine -from projectq.ops import H, CNOT, X, Measure, All +from projectq.ops import H, CNOT, All from projectq.cengines import _ibm5qubitmapper, SwapAndCNOTFlipper from projectq.backends import IBMBackend @@ -28,15 +27,20 @@ def test_ibm5qubitmapper_is_available(monkeypatch): # Test that IBM5QubitMapper calls IBMBackend if gate is available. def mock_send(*args, **kwargs): return "Yes" + monkeypatch.setattr(_ibm5qubitmapper.IBMBackend, "is_available", mock_send) mapper = _ibm5qubitmapper.IBM5QubitMapper() assert mapper.is_available("TestCommand") == "Yes" def test_ibm5qubitmapper_invalid_circuit(): + connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=backend, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -51,9 +55,13 @@ def test_ibm5qubitmapper_invalid_circuit(): def test_ibm5qubitmapper_valid_circuit1(): + connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=backend, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -70,9 +78,13 @@ def test_ibm5qubitmapper_valid_circuit1(): def test_ibm5qubitmapper_valid_circuit2(): + connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=backend, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -89,6 +101,7 @@ def test_ibm5qubitmapper_valid_circuit2(): def test_ibm5qubitmapper_valid_circuit2_ibmqx4(): + connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) backend = DummyEngine(save_commands=True) class FakeIBMBackend(IBMBackend): @@ -99,8 +112,11 @@ class FakeIBMBackend(IBMBackend): fake.is_available = backend.is_available backend.is_last_engine = True - eng = MainEngine(backend=fake, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=fake, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -119,9 +135,12 @@ class FakeIBMBackend(IBMBackend): def test_ibm5qubitmapper_optimizeifpossible(): backend = DummyEngine(save_commands=True) connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper(), - SwapAndCNOTFlipper(connectivity)]) + eng = MainEngine( + backend=backend, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity), + SwapAndCNOTFlipper(connectivity) + ]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -158,8 +177,10 @@ def test_ibm5qubitmapper_toomanyqubits(): backend = DummyEngine(save_commands=True) connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper(), - SwapAndCNOTFlipper(connectivity)]) + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(), + SwapAndCNOTFlipper(connectivity) + ]) qubits = eng.allocate_qureg(6) All(H) | qubits CNOT | (qubits[0], qubits[1]) diff --git a/projectq/ops/_command.py b/projectq/ops/_command.py index 6c320f375..f9268c420 100755 --- a/projectq/ops/_command.py +++ b/projectq/ops/_command.py @@ -104,6 +104,7 @@ def __init__(self, engine, gate, qubits, controls=(), tags=()): tags (list[object]): Tags associated with the command. """ + qubits = tuple( [WeakQubitRef(qubit.engine, qubit.id) for qubit in qreg] for qreg in qubits) diff --git a/projectq/setups/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/tests/_drawmpl_test.py b/projectq/tests/_drawmpl_test.py new file mode 100644 index 000000000..3d78befa6 --- /dev/null +++ b/projectq/tests/_drawmpl_test.py @@ -0,0 +1,53 @@ +# 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._drawer.py. + + To generate the baseline images + run the tests with '--mpl-generate-path=baseline' + + Then run the tests simply with '--mpl' +''' + +import pytest +from projectq import MainEngine +from projectq.ops import * +from projectq.backends import Simulator +from projectq.backends import CircuitDrawerMatplotlib +from projectq.cengines import DecompositionRuleSet, AutoReplacer +import projectq.setups.decompositions + +@pytest.mark.mpl_image_compare +def test_drawer_mpl(): + drawer = CircuitDrawerMatplotlib() + rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions]) + eng = MainEngine(backend=Simulator(), engine_list=[AutoReplacer(rule_set), + drawer]) + ctrl = eng.allocate_qureg(2) + qureg = eng.allocate_qureg(3) + + Swap | (qureg[0], qureg[2]) + C(Swap) | (qureg[0], qureg[1], qureg[2]) + + CNOT | (qureg[0], qureg[2]) + Rx(1.0) | qureg[0] + CNOT | (qureg[1], qureg[2]) + C(X, 2) | (ctrl[0], ctrl[1], qureg[2]) + QFT | qureg + All(Measure) | qureg + + eng.flush() + fig, ax = drawer.draw() + return fig \ No newline at end of file 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