diff --git a/.travis.yml b/.travis.yml index 47f727e2a..fbe009436 100755 --- a/.travis.yml +++ b/.travis.yml @@ -42,9 +42,6 @@ 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 eafd50b80..5af3fee8f 100755 --- a/examples/ibm.py +++ b/examples/ibm.py @@ -8,7 +8,7 @@ import projectq.setups.ibm -def run_entangle(eng, num_qubits=3): +def run_entangle(eng, num_qubits=5): """ Runs an entangling operation on the provided compiler engine. diff --git a/projectq/backends/__init__.py b/projectq/backends/__init__.py index f35a3acec..52b8d8c69 100755 --- a/projectq/backends/__init__.py +++ b/projectq/backends/__init__.py @@ -27,7 +27,7 @@ * an interface to the AQT trapped ion system (and simulator). """ from ._printer import CommandPrinter -from ._circuits import CircuitDrawer, CircuitDrawerMatplotlib +from ._circuits import CircuitDrawer 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 be22d24d2..1f22faec4 100755 --- a/projectq/backends/_circuits/__init__.py +++ b/projectq/backends/_circuits/__init__.py @@ -13,8 +13,4 @@ # 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 2562a07dd..85aee3dac 100755 --- a/projectq/backends/_circuits/_drawer.py +++ b/projectq/backends/_circuits/_drawer.py @@ -15,6 +15,8 @@ Contains a compiler engine which generates TikZ Latex code describing the circuit. """ +import sys + from builtins import input from projectq.cengines import LastEngineException, BasicEngine @@ -221,13 +223,12 @@ 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 not in ('0', '1', 1, 0): + while m != '0' and m != '1' and m != 1 and m != 0: prompt = ("Input measurement result (0 or 1) for " "qubit " + str(qubit) + ": ") m = input(prompt) diff --git a/projectq/backends/_circuits/_drawer_matplotlib_test.py b/projectq/backends/_circuits/_drawer_matplotlib_test.py deleted file mode 100644 index a76fbc99b..000000000 --- a/projectq/backends/_circuits/_drawer_matplotlib_test.py +++ /dev/null @@ -1,148 +0,0 @@ -# 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_test.py b/projectq/backends/_circuits/_plot_test.py deleted file mode 100644 index cd5d3ab0f..000000000 --- a/projectq/backends/_circuits/_plot_test.py +++ /dev/null @@ -1,289 +0,0 @@ -# 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 6486ab4d0..b1899f043 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, - and sends the circuit through the IBM API. + The IBM Backend class, which stores the circuit, transforms it to JSON + QASM, and sends the circuit through the IBM API. """ def __init__(self, use_hardware=False, num_runs=1024, verbose=False, - token='', device='ibmq_essex', + user=None, password=None, device='ibmqx4', num_retries=3000, interval=1, retrieve_execution=None): """ @@ -59,8 +59,10 @@ 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). - token (str): IBM quantum experience user password. - device (str): name of the IBM device to use. ibmq_essex By default + 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. 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 @@ -74,15 +76,15 @@ def __init__(self, use_hardware=False, num_runs=1024, verbose=False, if use_hardware: self.device = device else: - self.device = 'ibmq_qasm_simulator' + self.device = 'simulator' self._num_runs = num_runs self._verbose = verbose - self._token=token + self._user = user + self._password = password 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 @@ -91,17 +93,17 @@ def is_available(self, cmd): """ Return true if the command can be executed. - The IBM quantum chip can only do U1,U2,U3,barriers, and CX / CNOT. - Conversion implemented for Rotation gates and H gates. + The IBM quantum chip can do X, Y, Z, T, Tdag, S, Sdag, + rotation gates, barriers, and CX / CNOT. 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 == H: + if g in (T, Tdag, S, Sdag, H, Y, Z): return True if isinstance(g, (Rx, Ry, Rz)): return True @@ -109,11 +111,6 @@ 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 @@ -132,10 +129,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 @@ -157,7 +154,6 @@ 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 " @@ -165,28 +161,22 @@ 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({})'} - 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: + gate = u_strs[str(gate)[0:2]].format(gate.angle) + self.qasm += "\n{} q[{}];".format(gate, qb_pos) + else: 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 += "\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.') + self.qasm += "\n{} q[{}];".format(gate_str, qb_pos) def _logical_to_physical(self, qb_id): """ @@ -208,8 +198,6 @@ 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 @@ -224,7 +212,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 @@ -235,70 +223,68 @@ 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] - mapped_state = "".join(mapped_state) - if mapped_state not in probability_dict: - probability_dict[mapped_state] = probability - else: - probability_dict[mapped_state] += probability + probability_dict["".join(mapped_state)] = probability + return probability_dict def _run(self): """ Run the circuit. - Send the circuit via a non documented IBM API (using JSON written - circuits) using the provided user data / ask for the user token. + Send the circuit via the IBM API (JSON QASM) using the provided user + data / ask for username & password. """ # 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) + 1 + + max_qubit_id = max(self._allocated_qubits) qasm = ("\ninclude \"qelib1.inc\";\nqreg q[{nq}];\ncreg c[{nq}];" + - self.qasm).format(nq=max_qubit_id) + self.qasm).format(nq=max_qubit_id + 1) info = {} - info['json']=self._json - info['nq']=max_qubit_id - + info['qasms'] = [{'qasm': qasm}] info['shots'] = self._num_runs - info['maxCredits'] = 10 + info['maxCredits'] = 5 info['backend'] = {'name': self.device} + info = json.dumps(info) + try: if self._retrieve_execution is None: res = send(info, device=self.device, - token=self._token, + user=self._user, password=self._password, + shots=self._num_runs, num_retries=self._num_retries, interval=self._interval, verbose=self._verbose) else: - res = retrieve(device=self.device, - token=self._token, + res = retrieve(device=self.device, user=self._user, + password=self._password, 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="{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] + state = list(reversed(state)) + state = "".join(state) p_sum += probability star = "" if p_sum >= P and measured == "": @@ -336,3 +322,9 @@ 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 a6d2ab54a..e5e5b56d0 100755 --- a/projectq/backends/_ibm/_ibm_http_client.py +++ b/projectq/backends/_ibm/_ibm_http_client.py @@ -17,7 +17,7 @@ # source at: https://github.com/Qiskit/qiskit-ibmq-provider import getpass -import time +import json import signal import uuid @@ -243,8 +243,6 @@ def _get_result(self, job_status_url = ('Network/ibm-q/Groups/open/Projects/main/Jobs/' + execution_id) - if verbose: - print("Waiting for results. [Job ID: {}]".format(execution_id)) original_sigint_handler = signal.getsignal(signal.SIGINT) @@ -337,111 +335,66 @@ class DeviceOfflineError(Exception): pass -def show_devices(token=None, verbose=False): - """ - Access the list of available devices and their properties (ex: for setup - configuration) +def is_online(device): + url = 'Backends/{}/queue/status'.format(device) + r = requests.get(urljoin(_api_url, url)) + return r.json()['state'] - 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, - token, - jobid, - num_retries=3000, - interval=1, - verbose=False): +def retrieve(device, user, password, 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. - token (str): IBM quantum experience user API token. + user (str): IBM quantum experience user (e-mail) + password (str): IBM quantum experience password jobid (str): Id of the job to retrieve - - Returns: - (dict) result form the IBMQ server """ - 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) + user_id, access_token = _authenticate(user, password) + res = _get_result(device, jobid, access_token, num_retries=num_retries, + interval=interval, verbose=verbose) return res -def send(info, - device='ibmq_qasm_simulator', - token=None, - shots=None, - num_retries=3000, - interval=1, - verbose=False): +def send(info, device='sim_trivial_2', user=None, password=None, + shots=1, num_retries=3000, interval=1, verbose=False): """ Sends QASM through the IBM API and runs the quantum circuit. Args: - 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. + 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. 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). - - Returns: - (dict) result form the IBMQ server - """ try: - ibmq_session = IBMQ() - # Shots argument deprecated, as already - if shots is not None: - info['shots'] = shots + # 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.") + if verbose: print("- Authenticating...") - 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.") + user_id, access_token = _authenticate(user, password) if verbose: - print("- Running code: {}".format(info)) - execution_id = ibmq_session._run(info, device) + print("- Running code: {}".format( + json.loads(info)['qasms'][0]['qasm'])) + execution_id = _run(info, device, user_id, access_token, shots) if verbose: print("- Waiting for results...") - res = ibmq_session._get_result(device, - execution_id, - num_retries=num_retries, - interval=interval, - verbose=verbose) + res = _get_result(device, execution_id, access_token, + num_retries=num_retries, + interval=interval, verbose=verbose) if verbose: print("- Done.") return res @@ -454,3 +407,93 @@ def send(info, 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: # pragma: no cover + 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(*_): # pragma: no cover + 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 5f43b5d28..3340c2ac6 100755 --- a/projectq/backends/_ibm/_ibm_http_client_test.py +++ b/projectq/backends/_ibm/_ibm_http_client_test.py @@ -11,6 +11,7 @@ # 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 @@ -27,31 +28,24 @@ def no_requests(monkeypatch): monkeypatch.delattr("requests.sessions.Session.request") -_API_URL = 'https://api.quantum-computing.ibm.com/api/' -_AUTH_API_URL = 'https://auth.quantum-computing.ibm.com/api/users/loginWithToken' +_api_url = 'https://quantumexperience.ng.bluemix.net/api/' +_api_url_status = 'https://quantumexperience.ng.bluemix.net/api/' def test_send_real_device_online_verbose(monkeypatch): - json_qasm = { - 'qasms': [{ - 'qasm': 'my qasm' - }], - 'shots': 1, - 'json': 'instructions', - 'maxCredits': 10, - 'nq': 1 - } + qasms = {'qasms': [{'qasm': 'my qasm'}]} + json_qasm = json.dumps(qasms) 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.dumps(json_qasm)]) + json_body = ''.join([name_item, json_qasm]) json_data = ''.join(['{', json_body, '}']) shots = 1 device = "ibmqx4" - execution_id = '3' + json_data_run = ''.join(['{"qasm":', json_qasm, '}']) + execution_id = 3 result_ready = [False] result = "my_result" request_num = [0] # To assert correct order of calls @@ -143,10 +137,11 @@ def json(self): def raise_for_status(self): pass - jobs_url = 'Network/ibm-q/Groups/open/Projects/main/Jobs' # Authentication - if (args[1] == _AUTH_API_URL and kwargs["json"]["apiToken"] == token - and request_num[0] == 0): + if (args[0] == urljoin(_api_url, "users/login") and + kwargs["data"]["email"] == email and + kwargs["data"]["password"] == password and + request_num[0] == 1): request_num[0] += 1 return MockPostResponse({"userId": user_id, "id": access_token}) # STEP1 @@ -206,8 +201,8 @@ def raise_for_status(self): monkeypatch.setattr("requests.sessions.Session.put", mocked_requests_put) def user_password_input(prompt): - if prompt == "IBM QE token > ": - return token + if prompt == "IBM QE password > ": + return password monkeypatch.setattr("getpass.getpass", user_password_input) @@ -219,39 +214,9 @@ def user_password_input(prompt): 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): @@ -261,63 +226,22 @@ def __init__(self, json_data, status_code): def json(self): return self.json_data - 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) - + # 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) shots = 1 - token = '12345' - json_qasm = { - 'qasms': [{ - 'qasm': 'my qasm' - }], - 'shots': 1, - 'json': 'instructions', - 'maxCredits': 10, - 'nq': 1 - } + json_qasm = "my_json_qasm" name = 'projectq_test' with pytest.raises(_ibm_http_client.DeviceOfflineError): _ibm_http_client.send(json_qasm, device="ibmqx4", - token=token, - shots=shots, - verbose=True) + user=None, password=None, + shots=shots, verbose=True) -def test_show_device(monkeypatch): - access_token = "access" - user_id = 2016 - +def test_send_that_errors_are_caught(monkeypatch): class MockResponse: def __init__(self, json_data, status_code): self.json_data = json_data @@ -326,191 +250,123 @@ 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 = '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 + status_url = 'Backends/ibmqx4/queue/status' + if args[0] == urljoin(_api_url_status, status_url): + return MockResponse({"state": True}, 200) def mocked_requests_post(*args, **kwargs): # Test that this error gets caught raise requests.exceptions.HTTPError - monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + monkeypatch.setattr("requests.get", mocked_requests_get) + monkeypatch.setattr("requests.post", mocked_requests_post) # Patch login data - token = '12345' + password = 12345 + email = "test@projectq.ch" + monkeypatch.setitem(__builtins__, "input", lambda x: email) + monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) def user_password_input(prompt): - if prompt == "IBM QE token > ": - return token + if prompt == "IBM QE password > ": + return password monkeypatch.setattr("getpass.getpass", user_password_input) shots = 1 - json_qasm = { - 'qasms': [{ - 'qasm': 'my qasm' - }], - 'shots': 1, - 'json': 'instructions', - 'maxCredits': 10, - 'nq': 1 - } + json_qasm = "my_json_qasm" name = 'projectq_test' _ibm_http_client.send(json_qasm, device="ibmqx4", - token=None, - shots=shots, - verbose=True) - - token = '' - with pytest.raises(Exception): - _ibm_http_client.send(json_qasm, - device="ibmqx4", - token=None, - shots=shots, - verbose=True) + user=None, password=None, + shots=shots, verbose=True) def test_send_that_errors_are_caught2(monkeypatch): - class MockResponse: - def __init__(self, json_data, status_code): - pass + 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) def mocked_requests_post(*args, **kwargs): # Test that this error gets caught raise requests.exceptions.RequestException - monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + monkeypatch.setattr("requests.get", mocked_requests_get) + monkeypatch.setattr("requests.post", mocked_requests_post) # Patch login data - token = '12345' + password = 12345 + email = "test@projectq.ch" + monkeypatch.setitem(__builtins__, "input", lambda x: email) + monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) def user_password_input(prompt): - if prompt == "IBM QE token > ": - return token + if prompt == "IBM QE password > ": + return password monkeypatch.setattr("getpass.getpass", user_password_input) shots = 1 - json_qasm = { - 'qasms': [{ - 'qasm': 'my qasm' - }], - 'shots': 1, - 'json': 'instructions', - 'maxCredits': 10, - 'nq': 1 - } + json_qasm = "my_json_qasm" name = 'projectq_test' _ibm_http_client.send(json_qasm, device="ibmqx4", - token=None, - shots=shots, - verbose=True) + user=None, password=None, + shots=shots, verbose=True) def test_send_that_errors_are_caught3(monkeypatch): - class MockResponse: - def __init__(self, json_data, status_code): - pass + 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) def mocked_requests_post(*args, **kwargs): # Test that this error gets caught raise KeyError - monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + monkeypatch.setattr("requests.get", mocked_requests_get) + monkeypatch.setattr("requests.post", mocked_requests_post) # Patch login data - token = '12345' + password = 12345 + email = "test@projectq.ch" + monkeypatch.setitem(__builtins__, "input", lambda x: email) + monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) def user_password_input(prompt): - if prompt == "IBM QE token > ": - return token + if prompt == "IBM QE password > ": + return password monkeypatch.setattr("getpass.getpass", user_password_input) shots = 1 - json_qasm = { - 'qasms': [{ - 'qasm': 'my qasm' - }], - 'shots': 1, - 'json': 'instructions', - 'maxCredits': 10, - 'nq': 1 - } + json_qasm = "my_json_qasm" name = 'projectq_test' _ibm_http_client.send(json_qasm, device="ibmqx4", - token=None, - shots=shots, - verbose=True) + user=None, password=None, + shots=shots, verbose=True) def test_timeout_exception(monkeypatch): - qasms = { - 'qasms': [{ - 'qasm': 'my qasm' - }], - 'shots': 1, - 'json': 'instructions', - 'maxCredits': 10, - 'nq': 1 - } - json_qasm = qasms + qasms = {'qasms': [{'qasm': 'my qasm'}]} + json_qasm = json.dumps(qasms) tries = [0] execution_id = '3' @@ -541,7 +397,7 @@ def raise_for_status(self): execution_id) if args[1] == urljoin(_API_URL, job_url): tries[0] += 1 - return MockResponse({"status": "RUNNING"}, 200) + return MockResponse({"noqasms": "not done"}, 200) #STEP2 elif (args[1] == "/"+execution_id+"/jobUploadUrl"): @@ -570,8 +426,8 @@ def json(self): def raise_for_status(self): pass - jobs_url = 'Network/ibm-q/Groups/open/Projects/main/Jobs' - if args[1] == _AUTH_API_URL: + login_url = 'users/login' + if args[0] == urljoin(_api_url, login_url): return MockPostResponse({"userId": "1", "id": "12"}) # STEP1 @@ -626,6 +482,8 @@ def raise_for_status(self): 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): @@ -641,41 +499,15 @@ def raise_for_status(self): pass # Accessing status of device. Return online. - 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): + 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): 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( - { - "status": "TERMINATED", - 'iteration': request_num[0] - }, 400) + return MockResponse({"noqasms": "not done"}, 200) def mocked_requests_post(*args, **kwargs): class MockRequest: @@ -694,26 +526,23 @@ def json(self): def raise_for_status(self): pass - if args[1] == _AUTH_API_URL: + login_url = 'users/login' + if args[0] == urljoin(_api_url, login_url): return MockPostResponse({"userId": "1", "id": "12"}) - monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) - monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) - + monkeypatch.setattr("requests.get", mocked_requests_get) + monkeypatch.setattr("requests.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", - token="test", + user="test", password="test", jobid="123e", - num_retries=200) - with pytest.raises(Exception): - _ibm_http_client.retrieve(device="ibmqx4", - token="test", - jobid="123ee", - num_retries=200) + verbose=True) def test_retrieve(monkeypatch): + qasms = {'qasms': [{'qasm': 'my qasm'}]} + json_qasm = json.dumps(qasms) request_num = [0] execution_id='3' @@ -781,7 +610,8 @@ def json(self): def raise_for_status(self): pass - if args[1] == _AUTH_API_URL: + login_url = 'users/login' + if args[0] == urljoin(_api_url, login_url): return MockPostResponse({"userId": "1", "id": "12"}) #STEP8 diff --git a/projectq/backends/_ibm/_ibm_test.py b/projectq/backends/_ibm/_ibm_test.py index f6890d34c..df1652b7a 100755 --- a/projectq/backends/_ibm/_ibm_test.py +++ b/projectq/backends/_ibm/_ibm_test.py @@ -11,18 +11,27 @@ # 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 math -from projectq.setups import restrictedgateset +import json + +import projectq.setups.decompositions from projectq import MainEngine from projectq.backends._ibm import _ibm -from projectq.cengines import (BasicMapperEngine, DummyEngine) - +from projectq.cengines import (TagRemover, + LocalOptimizer, + AutoReplacer, + IBM5QubitMapper, + SwapAndCNOTFlipper, + DummyEngine, + DecompositionRuleSet) from projectq.ops import (All, Allocate, Barrier, Command, Deallocate, Entangle, Measure, NOT, Rx, Ry, Rz, S, Sdag, T, Tdag, - X, Y, Z, H, CNOT) + X, Y, Z) + +from projectq.setups.ibm import ibmqx4_connections # Insure that no HTTP request can be made in all tests in this module @@ -31,29 +40,31 @@ def no_requests(monkeypatch): monkeypatch.delattr("requests.sessions.Session.request") -@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)]) +_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)]) 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, False), (1, True), (2, False), (3, False)]) +@pytest.mark.parametrize("num_ctrl_qubits, is_available", [ + (0, True), (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 @@ -72,17 +83,14 @@ def test_ibm_sent_error(monkeypatch): # patch send def mock_send(*args, **kwargs): raise TypeError - monkeypatch.setattr(_ibm, "send", mock_send) + 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]) + eng = MainEngine(backend=backend, + engine_list=[IBM5QubitMapper(), + SwapAndCNOTFlipper(set())]) qubit = eng.allocate_qubit() - Rx(math.pi) | qubit + X | qubit with pytest.raises(Exception): qubit[0].__del__() eng.flush() @@ -92,80 +100,26 @@ 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 { - '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 - } - + return {'date': '2017-01-19T14:28:47.622Z', + 'data': {'time': 14.429004907608032, 'counts': {'00111': 396, + '00101': 27, + '00000': 601}, + 'qasm': ('...')}} monkeypatch.setattr(_ibm, "retrieve", mock_retrieve) - 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) + 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) unused_qubit = eng.allocate_qubit() qureg = eng.allocate_qureg(3) # entangle the qureg @@ -180,135 +134,43 @@ 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['000'] == pytest.approx(0.504) - assert prob_dict['111'] == pytest.approx(0.482) - assert prob_dict['011'] == pytest.approx(0.006) + assert prob_dict['111'] == pytest.approx(0.38671875) + assert prob_dict['101'] == pytest.approx(0.0263671875) def test_ibm_backend_functional_test(monkeypatch): - 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' - } - } + 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"}}') - # {'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 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 - } - + 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': ('...')}} monkeypatch.setattr(_ibm, "send", mock_send) - backend = _ibm.IBMBackend(verbose=True, num_runs=1000) - import sys + backend = _ibm.IBMBackend(verbose=True) # no circuit has been executed -> raises exception with pytest.raises(RuntimeError): backend.get_probabilities([]) - 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 + 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) unused_qubit = eng.allocate_qubit() qureg = eng.allocate_qureg(3) # entangle the qureg @@ -322,21 +184,9 @@ def mock_send(*args, **kwargs): All(Measure) | qureg # run the circuit eng.flush() - 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 + 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) with pytest.raises(RuntimeError): eng.backend.get_probabilities(eng.allocate_qubit()) diff --git a/projectq/backends/_sim/_cppkernels/simulator.hpp b/projectq/backends/_sim/_cppkernels/simulator.hpp index d248ed038..35175a818 100755 --- a/projectq/backends/_sim/_cppkernels/simulator.hpp +++ b/projectq/backends/_sim/_cppkernels/simulator.hpp @@ -19,9 +19,9 @@ #include #if defined(NOINTRIN) || !defined(INTRIN) -#include "nointrin/kernels.hpp" +# include "nointrin/kernels.hpp" #else -#include "intrin/kernels.hpp" +# include "intrin/kernels.hpp" #endif #include "intrin/alignedallocator.hpp" @@ -221,6 +221,57 @@ class Simulator{ fused_gates_ = fused_gates; } + template + void apply_uniformly_controlled_gate(std::vector &unitaries, + unsigned target_id, + std::vector choice_ids, + std::vector ctrl_ids){ + run(); + std::size_t n = vec_.size(); + std::size_t dist = 1UL << map_[target_id]; + + auto mask = get_control_mask(ctrl_ids); + + #pragma omp parallel for collapse(2) schedule(static) + for(std::size_t high = 0; high < n; high += 2*dist){ + for(std::size_t low = 0; low < dist; ++low){ + std::size_t entry = high+low; + if((entry&mask) == mask) { + unsigned u = 0; + for(std::size_t i = 0; i < choice_ids.size(); ++i) + u |= ((entry >> map_[choice_ids[i]]) & 1) << i; + + auto &m = unitaries[u]; + std::complex v[2]; + v[0] = vec_[entry]; + v[1] = vec_[entry + dist]; + vec_[entry] = v[0]*m[0][0] + v[1]*m[0][1]; + vec_[entry + dist] = v[0]*m[1][0] + v[1]*m[1][1]; + } + } + } + } + + void apply_diagonal_gate(std::vector angles, + std::vector ids, + std::vector ctrl_ids) + { + run(); + std::size_t n = vec_.size(); + complex_type I(0., 1.); + auto mask = get_control_mask(ctrl_ids); + + #pragma omp parallel for schedule(static) + for(std::size_t entry = 0; entry < n; ++entry) { + if((entry&mask) == mask) { + unsigned a = 0; + for(std::size_t i = 0; i < ids.size(); ++i) + a |= ((entry >> map_[ids[i]]) & 1) << i; + vec_[entry] *= std::exp(I * angles[a]); + } + } + } + template void emulate_math(F const& f, QuReg quregs, const std::vector& ctrl, bool parallelize = false){ diff --git a/projectq/backends/_sim/_cppsim.cpp b/projectq/backends/_sim/_cppsim.cpp index cab68d0ee..7ebdefe20 100755 --- a/projectq/backends/_sim/_cppsim.cpp +++ b/projectq/backends/_sim/_cppsim.cpp @@ -49,6 +49,8 @@ PYBIND11_PLUGIN(_cppsim) { .def("is_classical", &Simulator::is_classical) .def("measure_qubits", &Simulator::measure_qubits_return) .def("apply_controlled_gate", &Simulator::apply_controlled_gate) + .def("apply_uniformly_controlled_gate", &Simulator::apply_uniformly_controlled_gate) + .def("apply_diagonal_gate", &Simulator::apply_diagonal_gate) .def("emulate_math", &emulate_math_wrapper) .def("emulate_math_addConstant", &Simulator::emulate_math_addConstant) .def("emulate_math_addConstantModN", &Simulator::emulate_math_addConstantModN) diff --git a/projectq/backends/_sim/_pysim.py b/projectq/backends/_sim/_pysim.py index 4faf811f6..dbb754617 100755 --- a/projectq/backends/_sim/_pysim.py +++ b/projectq/backends/_sim/_pysim.py @@ -20,6 +20,7 @@ import random import numpy as _np +import cmath class Simulator(object): @@ -397,6 +398,43 @@ def apply_controlled_gate(self, m, ids, ctrlids): pos = [self._map[ID] for ID in ids] self._multi_qubit_gate(m, pos, mask) + def apply_uniformly_controlled_gate(self, unitaries, target_id, + choice_ids, ctrl_ids): + choice_pos = [self._map[ID] for ID in choice_ids] + pos = self._map[target_id] + mask = self._get_control_mask(ctrl_ids) + + def kernel(u, d, m): + return u * m[0][0] + d * m[0][1], u * m[1][0] + d * m[1][1] + + dist = 1 << pos + n = len(self._state) + for high in range(0, n, 2*dist): + for low in range(0, dist): + entry = high+low + if (entry & mask) == mask: + u = 0 + for i in range(len(choice_pos)): + u |= ((entry >> choice_pos[i]) & 1) << i + id1 = entry + id2 = entry + dist + self._state[id1], self._state[id2] = kernel( + self._state[id1], + self._state[id2], + unitaries[u]) + + def apply_diagonal_gate(self, angles, ids, ctrlids): + pos = [self._map[ID] for ID in ids] + mask = self._get_control_mask(ctrlids) + + n = len(self._state) + for entry in range(n): + if (entry & mask) == mask: + a = 0 + for i in range(len(pos)): + a |= ((entry >> pos[i]) & 1) << i + self._state[entry] *= cmath.exp(1j*angles[a]) + def _single_qubit_gate(self, m, pos, mask): """ Applies the single qubit gate matrix m to the qubit at position `pos` diff --git a/projectq/backends/_sim/_simulator.py b/projectq/backends/_sim/_simulator.py index 19e884d6d..1e67dc1cc 100755 --- a/projectq/backends/_sim/_simulator.py +++ b/projectq/backends/_sim/_simulator.py @@ -19,10 +19,13 @@ """ import math +import cmath import random +from projectq.types import WeakQubitRef from projectq.cengines import BasicEngine from projectq.meta import get_control_count, LogicalQubitIDTag -from projectq.ops import (NOT, +from projectq.ops import (All, + NOT, H, R, Measure, @@ -30,8 +33,9 @@ Allocate, Deallocate, BasicMathGate, - TimeEvolution) -from projectq.types import WeakQubitRef + TimeEvolution, + UniformlyControlledGate, + DiagonalGate) FALLBACK_TO_PYSIM = False try: @@ -106,7 +110,9 @@ def is_available(self, cmd): if (cmd.gate == Measure or cmd.gate == Allocate or cmd.gate == Deallocate or isinstance(cmd.gate, BasicMathGate) or - isinstance(cmd.gate, TimeEvolution)): + isinstance(cmd.gate, TimeEvolution) or + isinstance(cmd.gate, UniformlyControlledGate) or + isinstance(cmd.gate, DiagonalGate)): return True try: m = cmd.gate.matrix @@ -421,6 +427,26 @@ def _handle(self, cmd): qubitids = [qb.id for qb in cmd.qubits[0]] ctrlids = [qb.id for qb in cmd.control_qubits] self._simulator.emulate_time_evolution(op, t, qubitids, ctrlids) + elif isinstance(cmd.gate, UniformlyControlledGate): + choice_ids = [qb.id for qb in cmd.qubits[0]] + ctrl_ids = [qb.id for qb in cmd.control_qubits] + target_id = cmd.qubits[1][0].id + unitaries = [gate.matrix.tolist() for gate in cmd.gate.gates] + assert len(unitaries) == 2**len(choice_ids) + self._simulator.apply_uniformly_controlled_gate(unitaries, + target_id, + choice_ids, + ctrl_ids) + if cmd.gate.up_to_diagonal: + angles = [-cmath.phase(p) for p in cmd.gate.decomposition[1]] + ids = [target_id]+choice_ids + self._simulator.apply_diagonal_gate(angles, ids, []) + elif isinstance(cmd.gate, DiagonalGate): + ids = [q.id for qr in cmd.qubits for q in qr] + ctrlids = [qb.id for qb in cmd.control_qubits] + angles = cmd.gate.angles + assert len(angles) == 2**len(ids) + self._simulator.apply_diagonal_gate(angles, ids, ctrlids) elif len(cmd.gate.matrix) <= 2 ** 5: matrix = cmd.gate.matrix ids = [qb.id for qr in cmd.qubits for qb in qr] diff --git a/projectq/backends/_sim/_simulator_test.py b/projectq/backends/_sim/_simulator_test.py index 9f7d298cb..e13efc064 100755 --- a/projectq/backends/_sim/_simulator_test.py +++ b/projectq/backends/_sim/_simulator_test.py @@ -19,6 +19,7 @@ import copy import math +import cmath import numpy import pytest import random @@ -31,7 +32,8 @@ LocalOptimizer, NotYetMeasuredError) from projectq.ops import (All, Allocate, BasicGate, BasicMathGate, CNOT, Command, H, MatrixGate, Measure, QubitOperator, - Rx, Ry, Rz, S, TimeEvolution, Toffoli, X, Y, Z) + Rx, Ry, Rz, S, T, TimeEvolution, Toffoli, X, Y, Z, + DiagonalGate, UniformlyControlledGate) from projectq.meta import Control, Dagger, LogicalQubitIDTag from projectq.types import WeakQubitRef @@ -521,6 +523,92 @@ def build_matrix(list_single_matrices): init_wavefunction) +def test_simulator_apply_diagonal_gate(sim): + eng = MainEngine(sim) + qureg = eng.allocate_qureg(4) + eng.flush() + + target_1 = qureg[0] + control = qureg[1] + target_0 = qureg[2] + empty = qureg[3] + + wf = [1./4.]*(1 << 4) + eng.backend.set_wavefunction(wf, qureg) + + D = DiagonalGate(angles=range(4)) + with Control(eng, control): + D | (target_0, target_1) + + eng.flush() + qbit_to_bit_map, final_wavefunction = copy.deepcopy(eng.backend.cheat()) + All(Measure) | qureg + + desired_state = [1./4.*cmath.exp(1j*i) for i in + [0, 0, 0, 2, 0, 0, 1, 3, 0, 0, 0, 2, 0, 0, 1, 3]] + assert numpy.allclose(final_wavefunction, desired_state) + + +def test_simulator_apply_uniformly_controlled_gate(): + eng = MainEngine() + qureg = eng.allocate_qureg(3) + eng.flush() + wf = [math.sqrt(1./8.)]*(1 << 3) + eng.backend.set_wavefunction(wf, qureg) + + A = Rx(numpy.pi/5) + B = H + C = Rz(numpy.pi/5) + D = Ry(numpy.pi/3) + U = UniformlyControlledGate([A, B, C, D]) + with Dagger(eng): + U | ([qureg[0], qureg[2]], qureg[1]) + eng.flush() + qbit_to_bit_map, final_wavefunction = copy.deepcopy(eng.backend.cheat()) + vec = numpy.array([final_wavefunction]).T + vec[[1, 2]] = vec[[2, 1]] # reorder basis + vec[[5, 6]] = vec[[6, 5]] + reference = numpy.matrix(scipy.linalg.block_diag(A.matrix, B.matrix, + C.matrix, D.matrix)) + assert numpy.allclose(reference*vec, wf) + + +def test_simulator_apply_uniformly_controlled_gate_with_control(sim): + eng = MainEngine(sim) + qureg = eng.allocate_qureg(5) + eng.flush() + + control = qureg[0] + choice_1 = qureg[1] + target = qureg[2] + empty = qureg[3] + choice_0 = qureg[4] + + gates = [X, H, S, T] + U = UniformlyControlledGate(gates) + + id = Rz(0.0) + gates_equiv = [id, X, id, S, id, X, id, S, + id, H, id, T, id, H, id, T] + U_equiv = UniformlyControlledGate(gates_equiv) + + All(H) | qureg + with Control(eng, control): + U | ([choice_0, choice_1], target) + + with Dagger(eng): + All(H) | qureg + U_equiv | (qureg[0:2]+qureg[3:5], target) + + eng.flush() + qbit_to_bit_map, final_wavefunction = copy.deepcopy(eng.backend.cheat()) + All(Measure) | qureg + + print(final_wavefunction) + desired_state = [1.0] + [0.0]*31 + assert numpy.allclose(final_wavefunction, desired_state) + + def test_simulator_set_wavefunction(sim, mapper): engine_list = [LocalOptimizer()] if mapper is not None: diff --git a/projectq/cengines/_basicmapper.py b/projectq/cengines/_basicmapper.py index 5fc0f9a81..4d4cef177 100644 --- a/projectq/cengines/_basicmapper.py +++ b/projectq/cengines/_basicmapper.py @@ -81,7 +81,3 @@ 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 2c85749d2..7a2659a30 100755 --- a/projectq/cengines/_ibm5qubitmapper.py +++ b/projectq/cengines/_ibm5qubitmapper.py @@ -11,9 +11,12 @@ # 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 @@ -36,7 +39,8 @@ class IBM5QubitMapper(BasicMapperEngine): without performing Swaps, the mapping procedure **raises an Exception**. """ - def __init__(self, connections=None): + + def __init__(self): """ Initialize an IBM 5-qubit mapper compiler engine. @@ -45,16 +49,6 @@ def __init__(self, connections=None): 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): """ @@ -73,6 +67,17 @@ 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. @@ -85,15 +90,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 self.connections: - if (target_pos, ctrl_pos) in self.connections: + if not (ctrl_pos, target_pos) in connections: + if (target_pos, ctrl_pos) in connections: cost += self._interactions[tpl] else: return None @@ -109,22 +114,20 @@ 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 = list(self.current_mapping) + logical_ids = [qbid for qbid in 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: @@ -150,7 +153,7 @@ def _store(self, cmd): """ if not cmd.gate == FlushGate(): target = cmd.qubits[0][0].id - if _is_cnot(cmd): + if self._is_cnot(cmd): # CNOT encountered ctrl = cmd.control_qubits[0].id if not (ctrl, target) in self._interactions: @@ -184,15 +187,3 @@ 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 ea6d383b6..5c4c4c4da 100755 --- a/projectq/cengines/_ibm5qubitmapper_test.py +++ b/projectq/cengines/_ibm5qubitmapper_test.py @@ -11,13 +11,14 @@ # 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, All +from projectq.ops import H, CNOT, X, Measure, All from projectq.cengines import _ibm5qubitmapper, SwapAndCNOTFlipper from projectq.backends import IBMBackend @@ -27,20 +28,15 @@ 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(connections=connectivity) - ]) + eng = MainEngine(backend=backend, + engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -55,13 +51,9 @@ 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(connections=connectivity) - ]) + eng = MainEngine(backend=backend, + engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -78,13 +70,9 @@ 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(connections=connectivity) - ]) + eng = MainEngine(backend=backend, + engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -101,7 +89,6 @@ 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): @@ -112,11 +99,8 @@ class FakeIBMBackend(IBMBackend): fake.is_available = backend.is_available backend.is_last_engine = True - eng = MainEngine( - backend=fake, - engine_list=[ - _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity) - ]) + eng = MainEngine(backend=fake, + engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -135,12 +119,9 @@ 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(connections=connectivity), - SwapAndCNOTFlipper(connectivity) - ]) + eng = MainEngine(backend=backend, + engine_list=[_ibm5qubitmapper.IBM5QubitMapper(), + SwapAndCNOTFlipper(connectivity)]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -177,10 +158,8 @@ 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/libs/isometries/__init__.py b/projectq/libs/isometries/__init__.py new file mode 100644 index 000000000..00af96d6a --- /dev/null +++ b/projectq/libs/isometries/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .decompositions import _decompose_diagonal_gate +from .decompositions import _decompose_uniformly_controlled_gate +from .decompositions import _decompose_isometry +from .single_qubit_gate import _SingleQubitGate +from .apply_decompositions import (_apply_isometry, + _apply_diagonal_gate, + _apply_uniformly_controlled_gate) diff --git a/projectq/libs/isometries/apply_decompositions.py b/projectq/libs/isometries/apply_decompositions.py new file mode 100644 index 000000000..1d5ec8950 --- /dev/null +++ b/projectq/libs/isometries/apply_decompositions.py @@ -0,0 +1,133 @@ +# 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. + +import numpy as np +from math import ceil + +from projectq.ops import Rz, X, CNOT, Ph +from projectq.meta import Dagger, Control, Compute, Uncompute +from . import _decompose_diagonal_gate + + +def _count_trailing_zero_bits(v): + assert v > 0 + v = (v ^ (v - 1)) >> 1 + c = 0 + while(v): + v >>= 1 + c += 1 + return c + + +def _apply_diagonal_gate(decomposition, qureg): + n = len(qureg) + assert n == len(decomposition) - 1 + + for i in range(n): + _apply_uniformly_controlled_rotation(decomposition[i], qureg[i:]) + + p = decomposition[-1][0] + Ph(p) | qureg[0] + + +def _apply_uniformly_controlled_rotation(angles, qureg): + N = len(angles) + n = len(qureg) - 1 + assert 1 << n == N + assert N > 0 + + target = qureg[0] + controls = qureg[1:] + + if N == 1: + Rz(angles[0]) | target + return + + for i in range(N-1): + Rz(angles[i]) | target + control = controls[_count_trailing_zero_bits(i+1)] + CNOT | (control, target) + Rz(angles[N-1]) | target + CNOT | (controls[-1], target) + + +def _apply_uniformly_controlled_gate(decomposition, target, choice_reg, + up_to_diagonal): + gates, phases = decomposition + + assert len(gates) == 1 << len(choice_reg) + assert len(phases) == 2 << len(choice_reg) + + for i in range(len(gates) - 1): + gates[i] | target + control_index = _count_trailing_zero_bits(i+1) + choice = choice_reg[control_index] + CNOT | (choice, target) + gates[-1] | target + + if up_to_diagonal: + return + + decomposed_diagonal = _decompose_diagonal_gate(phases) + _apply_diagonal_gate(decomposed_diagonal, [target]+choice_reg) + + +def _get_one_bits(qureg, bks): + res = [] + for i in range(len(qureg)): + if bks & (1 << i): + res.append(qureg[i]) + return res + + +def _apply_multi_controlled_gate(decomposition, k, s, threshold, qureg): + gates, phases = decomposition + mask = k & ~(1 << s) + ctrl = _get_one_bits(qureg, mask) + eng = qureg[0].engine + + if len(gates) == 1: + if np.allclose(gates[0].matrix, Rz(0).matrix): + return + + if len(ctrl) == 0: + gates[0] | qureg[s] + elif len(ctrl) < threshold: + _apply_uniformly_controlled_gate(decomposition, qureg[s], ctrl, True) + else: + with Control(eng, ctrl): + gates[0] | qureg[s] + + +def _apply_isometry(decomposition, threshold, qureg): + reductions, decomposed_diagonal = decomposition + + n = len(qureg) + eng = qureg[0].engine + + with Dagger(eng): + ncols = range(len(reductions)) + for k in ncols: + for s in range(n): + mcg, ucg = reductions[k][s] + _apply_multi_controlled_gate(mcg, k, s, threshold, qureg) + if len(ucg) > 0: + _apply_uniformly_controlled_gate(ucg, qureg[s], + qureg[s+1:], True) + nqubits = int(ceil(np.log2(len(ncols)))) + if nqubits == 0: + p = decomposed_diagonal[-1][0] + Ph(p) | qureg[0] + else: + _apply_diagonal_gate(decomposed_diagonal, qureg[:nqubits]) diff --git a/projectq/libs/isometries/cppdec.cpp b/projectq/libs/isometries/cppdec.cpp new file mode 100644 index 000000000..0b52761af --- /dev/null +++ b/projectq/libs/isometries/cppdec.cpp @@ -0,0 +1,45 @@ +// 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. + +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(_OPENMP) +#include +#endif +#include "decomposition.hpp" + +namespace py = pybind11; + +PYBIND11_MODULE(cppdec, m) { + py::class_(m, "_DecomposeDiagonal") + .def(py::init&>()) + .def("get_decomposition", &Diagonal::get_decomposition) + ; + + py::class_(m, "_BackendDecomposeUCG") + .def(py::init &>()) + .def("get_decomposition", &UCG::get_decomposition) + ; + + py::class_(m, "_BackendDecomposeIsometry") + .def(py::init()) + .def("get_decomposition", &DecomposeIsometry::get_decomposition) + ; +} diff --git a/projectq/libs/isometries/decompose_diagonal.py b/projectq/libs/isometries/decompose_diagonal.py new file mode 100644 index 000000000..d05d0b983 --- /dev/null +++ b/projectq/libs/isometries/decompose_diagonal.py @@ -0,0 +1,79 @@ +# 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. + +import copy +import math +import cmath +import numpy as np + + +def _is_power_of_2(N): + return (N != 0) and ((N & (N - 1)) == 0) + + +class _DecomposeDiagonal(object): + def __init__(self, phases): + self._angles = [cmath.phase(p) for p in phases] + assert _is_power_of_2(len(phases)) + + def get_decomposition(self): + decomposition = [] + + angles = self._angles + N = len(angles) + + while N >= 2: + rotations = [] + for i in range(0, N, 2): + angles[i//2], rot = _basic_decomposition(angles[i], + angles[i+1]) + rotations.append(rot) + _decompose_rotations(rotations, 0, N//2) + decomposition.append(rotations) + N //= 2 + + decomposition.append([angles[0]]) + return decomposition + + +# global and relative phase +def _basic_decomposition(phi1, phi2): + return (phi1+phi2)/2.0, phi2-phi1 + + +# uniformly controlled rotation (one choice qubit) +def _decompose_rotation(phi1, phi2): + return (phi1 + phi2) / 2.0, (phi1 - phi2) / 2.0 + + +def _decompose_rotations(angles, a, b): + N = b-a + if N <= 1: + return + for i in range(a, a+N//2): + angles[i], angles[i+N//2] = _decompose_rotation(angles[i], + angles[i+N//2]) + _decompose_rotations(angles, a, a+N//2) + _decompose_rotations_reversed(angles, a+N//2, b) + + +def _decompose_rotations_reversed(angles, a, b): + N = b-a + if N <= 1: + return + for i in range(a, a+N//2): + angles[i+N//2], angles[i] = _decompose_rotation(angles[i], + angles[i+N//2]) + _decompose_rotations(angles, a, a+N//2) + _decompose_rotations_reversed(angles, a+N//2, b) diff --git a/projectq/libs/isometries/decompose_isometry.py b/projectq/libs/isometries/decompose_isometry.py new file mode 100644 index 000000000..7a9af3fda --- /dev/null +++ b/projectq/libs/isometries/decompose_isometry.py @@ -0,0 +1,215 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from projectq import MainEngine +from projectq.ops import Rz, UniformlyControlledGate, DiagonalGate +from projectq.ops._basics import BasicGate +from projectq.meta import Control + +import numpy as np +import math + + +class _DecomposeIsometry(object): + def __init__(self, cols, threshold): + self._cols = cols + self._threshold = threshold + + def get_decomposition(self): + n = int(round(np.log2(len(self._cols[0])))) + + # store colums in quregs for easy manipulation + local_engines = [] + local_quregs = [] + for col in self._cols: + eng = MainEngine() + qureg = eng.allocate_qureg(n) + eng.flush() + local_engines.append(eng) + local_quregs.append(qureg) + eng.backend.set_wavefunction(col, qureg) + + reductions = [] + for k in range(len(self._cols)): + reductions.append(_reduce_column(k, local_quregs, self._threshold)) + + phases = [1./c(local_quregs[k], k) for k in range(len(self._cols))] + nqubits = int(math.ceil(np.log2(len(self._cols)))) + phases = phases + [1.0]*((1 << nqubits) - len(phases)) + diagonal = DiagonalGate(phases=phases) + + return reductions, diagonal.decomposition + + +def a(k, s): + return k >> s + + +def b(k, s): + return k - (a(k, s) << s) + + +def c(qureg, t, k=0, s=0): + eng = qureg.engine + n = len(qureg) + t = b(k, s) + t * 2**s + assert 0 <= t and t <= 2**n - 1 + bit_string = ("{0:0"+str(n)+"b}").format(t)[::-1] + eng.flush() + return eng.backend.get_amplitude(bit_string, qureg) + + +# maps [c0,c1] to [1,0] +class ToZeroGate(BasicGate): + @property + def matrix(self): + r = math.sqrt(abs(self.c0)**2 + abs(self.c1)**2) + if r < 1e-15: + return np.matrix([[1, 0], [0, 1]]) + m = np.matrix([[np.conj(self.c0), np.conj(self.c1)], + [-self.c1, self.c0]]) / r + assert np.allclose(m.getH()*m, np.eye(2)) + return m + + def __str__(self): # pragma: no cover + return "TZG" + + +class ToOneGate(BasicGate): + @property + def matrix(self): + r = math.sqrt(abs(self.c0)**2 + abs(self.c1)**2) + if r < 1e-15: + return np.matrix([[1, 0], [0, 1]]) + m = np.matrix([[-self.c1, self.c0], + [np.conj(self.c0), np.conj(self.c1)]]) / r + assert np.allclose(m.getH()*m, np.eye(2)) + return m + + def __str__(self): # pragma: no cover + return "TOG" + + +# compute G_k which reduces column k to |k> +# and apply it to following columns and the user_qureg +def _reduce_column(k, local_quregs, threshold): + n = len(local_quregs[0]) + reduction = [] + for s in range(n): + reduction.append(_disentangle(k, s, local_quregs, threshold)) + return reduction + + +tol = 1e-12 + + +def _disentangle(k, s, local_quregs, threshold): + qureg = local_quregs[k] + n = len(qureg) + + assert n >= 1 + assert 0 <= k and k < 2**n + assert 0 <= s and s < n + + mcg_decomposition = _prepare_disentangle(k, s, local_quregs, threshold) + + for l in range(a(k, s)): + assert abs(c(qureg, l, k, s)) < tol + + if b(k, s+1) == 0: + range_l = list(range(a(k, s+1), 2**(n-1-s))) + else: + range_l = list(range(a(k, s+1)+1, 2**(n-1-s))) + + if ((k >> s) & 1) == 0: + gate = ToZeroGate + else: + gate = ToOneGate + + gates = [] + if len(range_l) == 0: + return [mcg_decomposition, gates] + for l in range(range_l[0]): + gates.append(Rz(0)) + for l in range_l: + U = gate() + U.c0 = c(qureg, 2*l, k, s) + U.c1 = c(qureg, 2*l + 1, k, s) + gates.append(U) + UCG = UniformlyControlledGate(gates, up_to_diagonal=True) + for q in local_quregs: + UCG | (q[s+1:], q[s]) + + return mcg_decomposition, UCG.decomposition + + +def _get_one_bits(qureg, mask): + res = [] + for i in range(len(qureg)): + if mask & (1 << i): + res.append(qureg[i]) + return res + + +def _count_one_bits(mask): + cnt = 0 + while mask: + if mask & 1: + cnt += 1 + mask >>= 1 + return cnt + + +def _prepare_disentangle(k, s, local_quregs, threshold): + qureg = local_quregs[k] + n = len(qureg) + + if b(k, s+1) == 0 or ((k >> s) & 1) != 0: + return [Rz(0)], None + if abs(c(qureg, 2*a(k, s+1)+1, k, s)) <= tol: + return [Rz(0)], None + + assert 1 <= k and k <= 2**n-1 + assert 0 <= s and s <= n-1 + assert (k >> s) & 1 == 0 + assert b(k, s+1) != 0 + + for l in range(a(k, s)): + assert abs(c(qureg, l, k, s)) < tol + + U = ToZeroGate() + U.c0 = c(qureg, 2*a(k, s+1), k, s) + U.c1 = c(qureg, 2*a(k, s+1)+1, k, s) + + mask = k + + ctrl = _count_one_bits(mask) + if ctrl > 0 and ctrl < threshold: + gates = [Rz(0)] * ((1 << ctrl)-1) + [U] + UCG = UniformlyControlledGate(gates, up_to_diagonal=True) + for q in local_quregs: + controls = _get_one_bits(q, mask) + UCG | (controls, q[s]) + return UCG.decomposition + + for q in local_quregs: + qubits = _get_one_bits(q, mask) + e = q.engine + if len(qubits) == 0: + U | q[s] + else: + with Control(e, qubits): + U | q[s] + + return [U], None diff --git a/projectq/libs/isometries/decompose_ucg.py b/projectq/libs/isometries/decompose_ucg.py new file mode 100644 index 000000000..8160bd25e --- /dev/null +++ b/projectq/libs/isometries/decompose_ucg.py @@ -0,0 +1,152 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from projectq.ops import H, Rz +from .single_qubit_gate import _SingleQubitGate + +import numpy as np +import copy +import math +import cmath +import scipy.linalg + + +# Decomposition taken from +# http://lib.tkk.fi/Diss/2007/isbn9789512290918/article3.pdf +class _DecomposeUCG(object): + def __init__(self, gates): + assert len(gates) > 0 + + self.gates = _unwrap(gates) + self.k = int(round(np.log2(len(gates)))) + self.n = self.k+1 + self.diagonal = np.ones(1 << self.n, dtype=complex) + + # call only once + def get_decomposition(self): + if self.k == 0: + return _wrap(self.gates), self.diagonal + + for level in range(self.k): + intervals = 1 << level + interval_length = 1 << (self.k-level) + for interval in range(intervals): + for i in range(interval_length//2): + r = self._apply_basic_decomposition(level, interval, i) + self._merge_controlled_rotations(level, interval, i, r) + + self._decompose_diagonals() + return _wrap(self.gates), self.diagonal + + def _apply_basic_decomposition(self, level, interval, i): + intervals = 1 << level + interval_length = 1 << (self.k-level) + offset = interval*interval_length + + a = self.gates[offset + i] + b = self.gates[offset + interval_length//2 + i] + v, u, r = _basic_decomposition(a, b) + + # store in place + self.gates[offset + i] = v + self.gates[offset + interval_length//2 + i] = u + return r + + def _merge_controlled_rotations(self, level, interval, i, r): + intervals = 1 << level + interval_length = 1 << (self.k-level) + offset = interval*interval_length + + if interval < intervals-1: + # merge with following UCG (not yet decomposed) + index = offset + interval_length + i + self.gates[index] = self.gates[index]*r.getH() + index = offset + 3*interval_length//2 + i + self.gates[index] = self.gates[index]*r + else: + # store trailing rotations in diagonal gate + for m in range(intervals): + off = m*interval_length + index = 2*i + 2*off + self.diagonal[index] *= r.getH().item((0, 0)) + self.diagonal[index+1] *= r.getH().item((1, 1)) + index = interval_length + 2*i + 2*off + self.diagonal[index] *= r.item((0, 0)) + self.diagonal[index+1] *= r.item((1, 1)) + + def _decompose_diagonals(self): + # decompose diagonal gates to CNOTs and merge single qubit gates + h = H.matrix + rz = Rz(-np.pi/2).matrix + self.gates[0] = h*self.gates[0] + for i in range(1, (1 << self.k) - 1): + self.gates[i] = h*self.gates[i]*rz*h + self.gates[-1] = self.gates[-1]*rz*h + + self.gates = [_closest_unitary(G) for G in self.gates] + + # merge Rz gates into final diagonal + phi = cmath.exp(1j*np.pi/4) + N = 1 << self.n + if self.k >= 1: + self.diagonal[:N//2] *= phi + self.diagonal[N//2:] *= 1/phi + if self.k >= 2: + self.diagonal[:N//4] *= 1j + self.diagonal[N//4:N//2] *= -1j + self.diagonal[N//2:3*N//4] *= 1j + self.diagonal[3*N//4:] *= -1j + + # global phase shift + phase = cmath.exp(-1j*((1 << self.k)-1)*np.pi/4) + if self.k >= 3: + phase *= -1 + + self.diagonal *= phase + + +def _closest_unitary(A): + V, __, Wh = scipy.linalg.svd(A) + U = np.matrix(V.dot(Wh)) + return U + + +def _wrap(gates): + return [_SingleQubitGate(gate) for gate in gates] + + +def _unwrap(gates): + return [gate.matrix for gate in gates] + + +# a == r.getH()*u*d*v +# b == r*u*d.getH()*v +def _basic_decomposition(a, b): + x = a * b.getH() + det = np.linalg.det(x) + x11 = x.item((0, 0))/cmath.sqrt(det) + delta = np.pi / 2 + phi = cmath.phase(det) + psi = cmath.phase(x11) + r1 = cmath.exp(1j/2 * (delta - phi/2 - psi)) + r2 = cmath.exp(1j/2 * (delta - phi/2 + psi + np.pi)) + r = np.matrix([[r1, 0], [0, r2]], dtype=complex) + d, u = np.linalg.eig(r * x * r) + # d must be diag(i,-i), otherwise reverse + if(abs(d[0] + 1j) < 1e-10): + d = np.flip(d, 0) + u = np.flip(u, 1) + d = np.diag(np.sqrt(d)) + v = d*u.getH()*r.getH()*b + return v, u, r diff --git a/projectq/libs/isometries/decomposition.hpp b/projectq/libs/isometries/decomposition.hpp new file mode 100644 index 000000000..21df99283 --- /dev/null +++ b/projectq/libs/isometries/decomposition.hpp @@ -0,0 +1,718 @@ +// 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. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using calc_type = double; +using complex_type = std::complex; +using gate_type = std::array, 2>; + +using namespace std::complex_literals; + + +#ifndef TOL +# define TOL 1e-12 +#endif // !TOL + + + +double get_time() { + using Clock = std::chrono::high_resolution_clock; + return std::chrono::duration(Clock::now().time_since_epoch()).count(); +} + +gate_type operator*(const gate_type& l, const gate_type& r) { + complex_type a = l[0][0] * r[0][0] + l[0][1] * r[1][0]; + complex_type b = l[0][0] * r[0][1] + l[0][1] * r[1][1]; + complex_type c = l[1][0] * r[0][0] + l[1][1] * r[1][0]; + complex_type d = l[1][0] * r[0][1] + l[1][1] * r[1][1]; + return {{a,b,c,d}}; +} + +gate_type operator*(complex_type c, const gate_type& g) { + return { c*g[0][0], c*g[0][1], + c*g[1][0], c*g[1][1] }; +} + +gate_type operator+(const gate_type& a, const gate_type& b) { + return { a[0][0]+b[0][0], a[0][1]+b[0][1], + a[1][0]+b[1][0], a[1][1]+b[1][1] }; +} + + +gate_type dagger(const gate_type& g) { + return { std::conj(g[0][0]), std::conj(g[1][0]), + std::conj(g[0][1]), std::conj(g[1][1]) }; +} + + + +// matrix containing normalized eigen vectors assuming eigenvalues +// are (i, -i) +gate_type eigen_vectors(const gate_type& gate) { + gate_type u; + if(std::abs(gate[1][0]) > TOL) { + u[0][0] = 1i - gate[1][1]; + u[0][1] = -1i - gate[1][1]; + u[1][0] = gate[1][0]; + u[1][1] = gate[1][0]; + } else if(std::abs(gate[0][1]) > TOL) { + u[0][0] = gate[0][1]; + u[0][1] = gate[0][1]; + u[1][0] = 1i - gate[0][0]; + u[1][1] = -1i - gate[0][0]; + } else { + if(std::abs(gate[0][0] - 1i) < TOL) { + u[0][0] = 1; + u[1][0] = 0; + + u[0][1] = 0; + u[1][1] = 1; + } else if(std::abs(gate[0][0] + 1i) < TOL) { + u[0][0] = 0; + u[1][0] = 1; + + u[0][1] = 1; + u[1][1] = 0; + } else { + assert(false); + } + return u; + } + + calc_type norm = std::sqrt(std::norm(u[0][0]) + std::norm(u[1][0])); + u[0][0] /= norm; + u[1][0] /= norm; + norm = std::sqrt(std::norm(u[0][1]) + std::norm(u[1][1])); + u[0][1] /= norm; + u[1][1] /= norm; + + return u; +} + +class MCG { +public: + gate_type gate; + using PartialDecomposition = std::vector; + using Decomposition = std::tuple>; + + MCG() { gate = {1,0,0,1}; } + MCG(const gate_type& gate) : gate(gate) { } + + MCG& operator=(const MCG& other) { + gate = other.gate; + return *this; + } + + Decomposition get_decomposition() const { + return std::make_tuple(std::vector(1,gate), + std::vector(0)); + } +}; + +class Diagonal { +public: + using Decomposition = std::vector>; + + unsigned n; + + Diagonal(std::vector &phases) : phases(phases) { + unsigned N = phases.size(); + n = static_cast(std::log2(N)); + assert(1<(N); + for(unsigned i = 0; i < N; ++i) + angles[i] = std::arg(phases[i]); + + Decomposition decomposition; + for(unsigned i = 0; i < n; ++i) { + unsigned length = 1<<(n-i); + std::vector rotations(length/2); + for(unsigned j = 0; j < length/2; ++j) + std::tie(angles[j], rotations[j]) + = basic_decomposition(angles[2*j], angles[2*j+1]); + angles.resize(length/2); + decompose_rotations(rotations); + decomposition.push_back(rotations); + } + + // last angle is global phase + std::vector ph(1,angles[0]); + decomposition.push_back(ph); + return decomposition; + } + + complex_type phase(unsigned index) const { return phases[index]; } + +private: + + + // global and relative phase + std::tuple + basic_decomposition(calc_type phi1, calc_type phi2) const { + return std::make_tuple((phi1+phi2)/2, phi2-phi1); + } + + std::tuple + rotation_decomposition(calc_type phi1, calc_type phi2) const { + return std::make_tuple((phi1+phi2)/2, (phi1-phi2)/2); + } + + void decompose_rotations(std::vector &rotations) { + decompose_rotations_rec(rotations.begin(), rotations.end()); + } + + template + void decompose_rotations_rec(Iter a, Iter b, bool reversed = false) { + unsigned N = std::distance(a,b); + if(N <= 1) + return; + if(reversed == false) + for(Iter i = a; i != a+N/2; ++i) + std::tie(*i, *(i+N/2)) = + rotation_decomposition(*i, *(i+N/2)); + else + for(Iter i = a; i != a+N/2; ++i) + std::tie(*(i+N/2), *i) = + rotation_decomposition(*i, *(i+N/2)); + + decompose_rotations_rec(a, a+N/2, false); + decompose_rotations_rec(a+N/2, b, true); + } + + std::vector phases; + std::vector angles; +}; + +class UCG { +public: + unsigned n; + + using PartialDecomposition = std::vector; + using Decomposition = std::tuple>; + + UCG(std::vector &gates) : gates_(gates) { + n = 1 + static_cast(std::log2(gates.size())); + } + + Decomposition get_decomposition() { + if(decomposed_ == false) + decompose(); + return std::make_tuple(gates_, phases_); + } + + void decompose() { + assert(decomposed_ == false); + decomposed_ = true; + ucg_decomposition(); + } + + gate_type operator()(unsigned i) const { + if(i >= gates_.size()) + std::cout << "Illegal UCG Index" << std::endl; + return gates_[i]; + } + + Diagonal get_diagonal() { + assert(decomposed_ == true); + return Diagonal(phases_); + } + +private: + + std::tuple + ucg_basic_decomposition(const gate_type& a, const gate_type& b) { + gate_type x = { + a[0][0]*std::conj(b[0][0]) + a[0][1]*std::conj(b[0][1]), + a[0][0]*std::conj(b[1][0]) + a[0][1]*std::conj(b[1][1]), + a[1][0]*std::conj(b[0][0]) + a[1][1]*std::conj(b[0][1]), + a[1][0]*std::conj(b[1][0]) + a[1][1]*std::conj(b[1][1]) + }; + complex_type det = x[0][0]*x[1][1] - x[1][0]*x[0][1]; + complex_type x11 = x[0][0]/std::sqrt(det); + calc_type delta = M_PI / 2.0; + calc_type phi = std::arg(det); + calc_type psi = std::arg(x11); + complex_type r1 = std::exp(1i * ((delta - phi/2 - psi) / 2)); + complex_type r2 = std::exp(1i * ((delta - phi/2 + psi + M_PI) / 2)); + gate_type r = {r1, 0.0, 0.0, r2}; + gate_type rxr = { + r1*r1*x[0][0], r1*r2*x[0][1], + r1*r2*x[1][0], r2*r2*x[1][1] + }; + gate_type u = eigen_vectors(rxr); + complex_type z = std::exp(1i * calc_type(M_PI/4)); + gate_type v = { + z*std::conj(r1*u[0][0]), z*std::conj(r2*u[1][0]), + std::conj(z*r1*u[0][1]), std::conj(z*r2*u[1][1]) + }; + v = v*b; + + return std::make_tuple(v,u,r); + } + + complex_type dot(const gate_type& a, const gate_type& b) { + return std::conj(a[0][0]) * b[0][0] + + std::conj(a[0][1]) * b[0][1] + + std::conj(a[1][0]) * b[1][0] + + std::conj(a[1][1]) * b[1][1]; + } + + void project_gate(gate_type& gate) { + calc_type norm = std::sqrt(std::norm(gate[0][1]) + std::norm(gate[1][1])); + gate[0][1] /= norm; + gate[1][1] /= norm; + + complex_type inner = std::conj(gate[0][1])*gate[0][0] + std::conj(gate[1][1])*gate[1][0]; + gate[0][0] -= inner*gate[0][1]; + gate[1][0] -= inner*gate[1][1]; + + norm = std::sqrt(std::norm(gate[0][0]) + std::norm(gate[1][0])); + gate[0][0] /= norm; + gate[1][0] /= norm; + } + + void ucg_decomposition() { + phases_ = std::vector(1<= 4) + for(unsigned i = 0; i < interval_length/2; ++i) { + unsigned offset = interval*interval_length; + auto& a = gates_[offset+i]; + auto& b = gates_[offset+interval_length/2+i]; + gate_type v,u,r; + std::tie(v,u,r) = ucg_basic_decomposition(a,b); + + if(interval == intervals-1) { + for(unsigned m = 0; m < intervals; ++m) { + unsigned offset2 = m*interval_length; + unsigned index = 2*i + 2*offset2; + phases_[index] *= std::conj(r[0][0]); + phases_[index+1] *= std::conj(r[1][1]); + index = interval_length + 2*(i+offset2); + phases_[index] *= r[0][0]; + phases_[index+1] *= r[1][1]; + } + } else { + unsigned index = offset + interval_length + i; + gates_[index][0][0] *= std::conj(r[0][0]); + gates_[index][1][0] *= std::conj(r[0][0]); + gates_[index][0][1] *= std::conj(r[1][1]); + gates_[index][1][1] *= std::conj(r[1][1]); + index += interval_length/2; + gates_[index][0][0] *= r[0][0]; + gates_[index][1][0] *= r[0][0]; + gates_[index][0][1] *= r[1][1]; + gates_[index][1][1] *= r[1][1]; + } + + gates_[offset + i] = v; + gates_[offset + i + interval_length/2] = u; + + project_gate(gates_[offset + i]); + project_gate(gates_[offset + i + interval_length/2]); + } + } + } + + complex_type I(0,1); + calc_type x = 1.0/std::sqrt(2); + gate_type H = {x,x,x,-x}; + complex_type z = std::exp(I*calc_type(M_PI/4)); + gate_type RH = {z*x, z*x, std::conj(z)*x, -std::conj(z)*x}; + + gates_[0] = H * gates_[0]; + for(unsigned i = 1; i < (1<= 1) { + std::transform(phases_.begin(), phases_.begin() + N/2, phases_.begin(), [&](complex_type d){ return d * phi; }); + std::transform(phases_.begin() + N/2, phases_.end(), phases_.begin() + N/2, [&](complex_type d){ return d / phi; }); + } if(controls >= 2) { + std::transform(phases_.begin(), phases_.begin() + N/4, phases_.begin(), [&](complex_type d){ return d * I; }); + std::transform(phases_.begin() + N/4, phases_.begin() + N/2, phases_.begin() + N/4, [&](complex_type d){ return d * -I; }); + std::transform(phases_.begin() + N/2, phases_.begin() + 3*N/4, phases_.begin() + N/2, [&](complex_type d){ return d * I; }); + std::transform(phases_.begin() + 3*N/4, phases_.end(), phases_.begin() + 3*N/4, [&](complex_type d){ return d * -I; }); + } + + complex_type phase = std::exp(-I*calc_type(((1<= 3) + phase *= -1; + for(auto& d : phases_) + d *= phase; + } + + std::vector phases_; + std::vector gates_; + bool decomposed_ = false; +}; + +class DecomposeIsometry { + +public: + using StateVector = std::vector; + using Isometry = std::vector; + + using ReductionStep = std::tuple; + using Reduction = std::vector; + + using ReductionStepDecomposition = std::tuple; + using ReductionDecomposition = std::vector; + using CompleteReductionDecomposition = std::vector; + using Decomposition = std::tuple; + + Isometry V; // list of column vectors + unsigned threshold, n; + + DecomposeIsometry(Isometry V, unsigned threshold) : V(V), threshold(threshold) { + n = int(log2(V[0].size())); + } + + Decomposition get_decomposition() { + unsigned nqubits = int(ceil(log2(V.size()))); + + CompleteReductionDecomposition complete_reduction_decomposition; + for(unsigned k = 0; k < V.size(); ++k) { + auto reduction_decomposition = reduce_column(k); + complete_reduction_decomposition.push_back(reduction_decomposition); + } + + std::vector phases(1 << nqubits, 1); + for(int k = 0; k < V.size(); ++k) + phases[k] = 1.0 / V[k][0]; + auto diagonal = Diagonal(phases); + auto diagonal_decomposition = diagonal.get_decomposition(); + + return std::make_tuple(complete_reduction_decomposition, diagonal_decomposition); + } + +private: + + void debug() { + std::cout << "--- Debug: ---" << std::endl; + + for(unsigned k = 0; k < V.size(); ++k) { + for(unsigned i = 0; i < V[k].size(); ++i) + std::cout << V[k][i] << ", "; + std::cout << std::endl; + } + std::cout << std::endl << "--- /Debug: ---" << std::endl; + } + + ReductionDecomposition reduce_column(unsigned k) { + ReductionDecomposition reduction_decomposition; + for(unsigned s = 0; s < n; ++s) + reduction_decomposition.push_back(disentangle(k,s)); + return reduction_decomposition; + } + + ReductionStepDecomposition disentangle(unsigned k, unsigned s) { + auto mcg_decomposition = prepare_disentangle(k, s); + + for(unsigned l = 0; l < a(k,s); ++l) + assert(std::abs(c(k, l)) < TOL); + + unsigned l_max = 1 << (n-1-s); + unsigned l_min = a(k,s+1); + if(b(k,s+1) > 0) + l_min += 1; + + unsigned target_bit = a(k,s) & 1; + std::vector gates; + gates.reserve(l_max); + for(unsigned l = 0; l < l_min; ++l) + gates.push_back(identity_gate()); + if(target_bit == 0) + for(unsigned l = l_min; l < l_max; ++l) + gates.push_back(to_zero_gate(k, l)); + else + for(unsigned l = l_min; l < l_max; ++l) + gates.push_back(to_one_gate(k, l)); + UCG ucg(gates); + apply_UCG_up_to_diagonal_to_all(ucg, k, s); + + return std::make_tuple(mcg_decomposition, + ucg.get_decomposition()); + } + + unsigned _count_one_bits(unsigned mask) { + unsigned cnt = 0; + while(mask) { + if(mask & 1) + ++cnt; + mask >>= 1; + } + return cnt; + } + + MCG::Decomposition prepare_disentangle(unsigned k, unsigned s) { + if(b(k,s+1) == 0 || ((k>>s)&1) != 0) + return MCG(identity_gate()).get_decomposition(); + if(std::abs(c(k, 2*a(k,s+1)+1)) <= TOL) + return MCG(identity_gate()).get_decomposition(); + + for(unsigned l = 0; l < a(k,s); ++l) + assert(std::abs(c(k, l)) < TOL); + + gate_type U(to_zero_gate(k, a(k,s+1))); + + unsigned mask = k; + unsigned ctrl = _count_one_bits(mask); + + if(ctrl > 0 and ctrl < threshold) { + auto gates = std::vector((1 << ctrl) - 1, identity_gate()); + gates.push_back(U); + UCG ucg(gates); + apply_MCG_to_all(U, k, s); + apply_MCG_as_UCG_to_all(ucg.get_decomposition(), k, s); + return ucg.get_decomposition(); + } else { + apply_MCG_to_all(U, k, s); + MCG mcg(U); + return mcg.get_decomposition(); + } + + assert(false); + } + + void apply_MCG_as_UCG_to_all(const MCG::Decomposition& mcg_decomposition, unsigned k, unsigned s) { + for(unsigned col = 0; col < V.size(); ++col) + apply_MCG_as_UCG(mcg_decomposition, k, s, col); + } + + void apply_UCG_up_to_diagonal_to_all(UCG& ucg, unsigned k, unsigned s) { + apply_UCG_to_all(ucg, k, s); + ucg.decompose(); + apply_inv_diagonal_to_all(ucg.get_diagonal(), k, s); + } + + void apply_MCG_to_all(const MCG& mcg, unsigned k, unsigned s) { + for(unsigned col = 0; col < V.size(); ++col) + apply_MCG(mcg, k, s, col); + } + + void apply_UCG_to_all(const UCG& ucg, unsigned k, unsigned s) { + for(unsigned col = 0; col < V.size(); ++col) + apply_UCG(ucg, k, s, col); + } + + void apply_inv_diagonal_to_all(const Diagonal& diagonal, unsigned k, unsigned s) { + for(unsigned col = 0; col < V.size(); ++col) + apply_inv_diagonal(diagonal, k, s, col); + } + + std::vector _get_one_ids(unsigned k) { + std::vector ids; + for(unsigned i = 0; i < n; ++i) + if((k>>i) & 1) + ids.push_back(i); + return ids; + } + + void apply_MCG_as_UCG(const MCG::Decomposition& mcg_decomposition, unsigned k, unsigned s, unsigned col) { + assert(((k>>s)&1) == 0); + auto ids = _get_one_ids(k); + ids.insert(ids.begin(), s); + + // apply inverse diagonal + auto diagonal = std::get<1>(mcg_decomposition); + if(col < k) { + unsigned a = 0; + for(unsigned i = 0; i < ids.size(); ++i) + a |= ((col >> ids[i]) & 1) << i; + c(col, 0) *= std::conj(diagonal[a]); + } else if (col == k) { + #pragma omp parallel for schedule(static) + for(unsigned j = 0; j < (1<<(n-s)); ++j) { + unsigned entry = (j << s) + b(k,s); + unsigned a = 0; + for(std::size_t i = 0; i < ids.size(); ++i) + a |= ((entry >> ids[i]) & 1) << i; + c(col, j) *= std::conj(diagonal[a]); + } + } else { + #pragma omp parallel for schedule(static) + for(std::size_t entry = 0; entry < (1<> ids[i]) & 1) << i; + c(col, entry) *= std::conj(diagonal[a]); + } + } + } + + void apply_MCG(const MCG& mcg, unsigned k, unsigned s, unsigned col) { + if(col < k) + return; + + unsigned hi = 2*a(k,s+1); + unsigned lo = b(k,s); + unsigned diff = 1 << s; + unsigned mask = (hi< TOL) + std::cout << "bad phase: " << diagonal.phase(q) << std::endl; + if(col < k) { + c(col, 0) *= std::conj(diagonal.phase(col>>s)); + } else if(col == k) { + unsigned target_bit = (k >> s) & 1; + #pragma omp parallel for schedule(static) + for(unsigned i = 0; i < 1<<(n-s-1); ++i) + c(col, i) *= std::conj(diagonal.phase(2*i+target_bit)); + } else { + #pragma omp parallel for collapse(2) schedule(static) + for(unsigned hi = 0; hi < 1<<(n-s); ++hi) + for(unsigned lo = 0; lo < 1<> s; + } + + // return s least significant bits + unsigned b(unsigned k, unsigned s) { + return k & ((1< 0 and len(phases) > 0: + raise ValueError("Provide either a list of angles or of phases") + + if len(angles) > 0: + if not _is_power_of_2(len(angles)): + raise ValueError("Number of angles must be 2^k for k=0,1,2...") + self._angles = copy.copy(angles) + self._phases = [] + elif len(phases) > 0: + if not _is_power_of_2(len(phases)): + raise ValueError("Number of angles must be 2^k for k=0,1,2...") + self._phases = copy.copy(phases) + self._angles = [] + else: + raise ValueError("Provide either a list of angles or of phases") + self.interchangeable_qubit_indices = [] + self._decomposition = None + + @property + def angles(self): + if len(self._angles) == 0: + self._angles = [cmath.phase(phase) for phase in self.phases] + return self._angles + + @property + def phases(self): + if len(self._phases) == 0: + self._phases = [cmath.exp(1j*angle) for angle in self.angles] + return self._phases + + @property + def decomposition(self): + if self._decomposition is None: + from projectq.libs.isometries import _decompose_diagonal_gate + self._decomposition = _decompose_diagonal_gate(self.phases) + return self._decomposition + + def get_inverse(self): + if len(self._angles) > 0: + return DiagonalGate(angles=[-a for a in self._angles]) + return DiagonalGate(phases=[p.conjugate() for p in self._phases]) + + # TODO: can also be merged with uniformly controlled gates + def get_merged(self, other): + if isinstance(other, DiagonalGate): + other_phases = other.phases + if len(self.phases) != len(other_phases): + raise NotMergeable("Cannot merge these two gates.") + new_phases = [self.phases[i]*other_phases[i] for i + in range(len(other_phases))] + return DiagonalGate(phases=new_phases) + else: + raise NotMergeable("Cannot merge these two gates.") + + def __eq__(self, other): + """ Not implemented as this object is a floating point type.""" + return NotImplemented + + def __ne__(self, other): + """ Not implemented as this object is a floating point type.""" + return NotImplemented + + def __str__(self): + return "D" diff --git a/projectq/ops/_diagonal_gate_test.py b/projectq/ops/_diagonal_gate_test.py new file mode 100644 index 000000000..e7fb7ce67 --- /dev/null +++ b/projectq/ops/_diagonal_gate_test.py @@ -0,0 +1,59 @@ +# 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. + +import numpy as np +import cmath + +from projectq import MainEngine +from projectq.ops import BasicGate, Ph, X, Y, Z, H, Measure, Rx, Ry, Rz, All +from projectq.meta import Dagger + +from . import _diagonal_gate as diag + + +def test_merge(): + angles1 = list(range(8)) + angles2 = list(range(8)) + D1 = diag.DiagonalGate(angles=angles1) + D2 = diag.DiagonalGate(angles=angles2) + D3 = D1.get_merged(D2) + for i in range(8): + assert np.isclose(D3.phases[i], cmath.exp(1j*2*i)) + + +def test_inverse(): + angles = list(range(8)) + D = diag.DiagonalGate(angles=angles) + D_inv = D.get_inverse() + for i in range(8): + assert np.isclose(D_inv.phases[i], cmath.exp(-1j*i)) + + +def test_dagger(): + eng = MainEngine() + qureg = eng.allocate_qureg(3) + + angles = list(range(8)) + D = diag.DiagonalGate(angles=angles) + All(H) | qureg + D | qureg + with Dagger(eng): + D | qureg + All(H) | qureg + + eng.flush() + qbit_to_bit_map, vec = eng.backend.cheat() + assert np.isclose(vec[0], 1) + + All(Measure) | qureg diff --git a/projectq/ops/_isometry.py b/projectq/ops/_isometry.py new file mode 100644 index 000000000..931c2b145 --- /dev/null +++ b/projectq/ops/_isometry.py @@ -0,0 +1,146 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ._basics import BasicGate, NotInvertible, NotMergeable + +import copy +import numpy as np + + +class Isometry(BasicGate): + """ + A gate that represents an arbitrary isometry. It is constructed from a + matrix of size 2^n by k. The isometry acts on n qubits, the action on + states |i> with i >= k is undefined. The value of k must be in [1,2^n]. + + Example: + .. code-block:: python + matrix([[1./sqrt(2), 0], + [0, 1./sqrt(2)] + [1./sqrt(2), 0] + [0, 1./sqrt(2)]] + V = Isometry([col_0, col_1]) + V | qureg + + Attributes: + matrix: 2^n by k matrix with orthonormal columns + + Note: + If possible use DiagonalGate of UniformlyControlledGate instead. In + general the gate complexity is exponential in the number of qubits, + so it is very inefficient to decompose large isometries. + """ + def __init__(self, matrix): + array = np.asarray(matrix) + cols = [] + for i in range(array.shape[1]): + cols.append(matrix[:, i]) + + k = len(cols) + if k == 0: + raise ValueError("An isometry needs at least one column.") + n = int(np.log2(len(cols[0]))) + if 2**n != len(cols[0]): + raise ValueError("The length of the columns of an isometry must be" + " a power of 2.") + if k > 2**n: + raise ValueError("An isometry can contain at most 2^n columns.") + for i in range(k): + if len(cols[i]) != 2**n: + raise ValueError("All columns of an isometry must have the" + " same length.") + + for i in range(k): + if not np.isclose(np.linalg.norm(cols[i]), 1): + raise ValueError("The columns of an isometry have to be" + " normalized.") + for j in range(k): + if i != j: + if not np.isclose(np.vdot(cols[i], cols[j]), 0): + raise ValueError("The columns of an isometry have to" + " be orthogonal.") + + self.cols = cols + self.interchangeable_qubit_indices = [] + self._decomposition = None + self._threshold = _get_ucg_mcg_threshold(n) + + @property + def decomposition(self): + if self._decomposition is None: + from projectq.libs.isometries import _decompose_isometry + self._decomposition = _decompose_isometry(self.cols, + self._threshold) + return self._decomposition + + def __str__(self): + return "V" + + def __eq__(self, other): + """ Not implemented as this object is a floating point type.""" + return NotImplemented + + def __ne__(self, other): + """ Not implemented as this object is a floating point type.""" + return NotImplemented + + +# When decomposing up to diagonal gate, for a small number of controls +# a UCG is cheaper than a MCG. Here we empirically find that threshold +def _my_is_available(cmd): + from projectq.ops import (Command, X, Y, Z, T, H, Tdag, S, Sdag, Measure, + Allocate, Deallocate, NOT, Rx, Ry, Rz, Barrier, + Entangle) + from projectq.meta import Control, Compute, Uncompute, get_control_count + + g = cmd.gate + 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): + return True + if isinstance(g, (Rx, Ry, Rz)): + return True + if g in (Measure, Allocate, Deallocate, Barrier): + return True + return False + + +def _count_cnot_in_mcg(n): + from projectq import MainEngine + from projectq.ops import C, Z, H + from projectq.backends import ResourceCounter + from projectq.cengines import (AutoReplacer, DecompositionRuleSet, + DummyEngine, BasicEngine) + import projectq.setups.decompositions + + resource_counter = ResourceCounter() + rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions]) + engines = [AutoReplacer(rule_set), resource_counter] + backend = DummyEngine() + backend.is_available = _my_is_available + eng = MainEngine(backend, engines) + qureg = eng.allocate_qureg(n+1) + C(H, n) | (qureg[1:], qureg[0]) + for item in str(resource_counter).split("\n"): + if "CX : " in item: + return int(item.strip()[5:]) + return 0 + + +def _get_ucg_mcg_threshold(n): + for ctrl in range(2, n): + if (1 << ctrl)-1 > _count_cnot_in_mcg(ctrl): + return ctrl + return n diff --git a/projectq/ops/_isometry_test.py b/projectq/ops/_isometry_test.py new file mode 100644 index 000000000..caa92863a --- /dev/null +++ b/projectq/ops/_isometry_test.py @@ -0,0 +1,73 @@ +# 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. + +import numpy as np +import cmath +import math +import pytest +from scipy.linalg import block_diag + +from projectq import MainEngine +from projectq.ops import BasicGate, Ph, X, Y, Z, H, Measure, Rx, Ry, Rz, All +from projectq.meta import Dagger, Control + +from . import _isometry as iso +from . import _uniformly_controlled_gate as ucg +from . import _diagonal_gate as diag + +from ..setups.decompositions._isometries_fixture import iso_decomp_chooser + + +def create_initial_state(mask, qureg): + n = len(qureg) + for pos in range(n): + if ((mask >> pos) & 1) == 1: + X | qureg[pos] + + +# TODO: figure out why the monkeypatching with the iso_decomp_chooser +# fixture leads to some errors +@pytest.mark.parametrize("index", range(8)) +def test_matrix(index): + A = np.asarray(H.matrix) + B = np.asarray(Ry(7).matrix) + A_B = np.array(block_diag(A, B)) + d = [cmath.exp(1j*i) for i in [1, 2, 3, 4]] + D = np.diag(d) + + eng = MainEngine() + qureg = eng.allocate_qureg(4) + eng.flush() + target = qureg[1] + control = qureg[0] + choice = qureg[2] + + create_initial_state(index, qureg) + + print(np.dot(A_B, D)) + V = iso.Isometry(np.dot(A_B, D)) + + with Control(eng, control): + V | (target, choice) + + with Dagger(eng): + U = ucg.UniformlyControlledGate([H, Ry(7)]) + W = diag.DiagonalGate(phases=d) + W | (target, choice) + U | (choice, target) + + eng.flush() + _, vec = eng.backend.cheat() + print(vec) + assert np.isclose(vec[index], 1) diff --git a/projectq/ops/_uniformly_controlled_gate.py b/projectq/ops/_uniformly_controlled_gate.py new file mode 100644 index 000000000..cc82589bf --- /dev/null +++ b/projectq/ops/_uniformly_controlled_gate.py @@ -0,0 +1,81 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from copy import deepcopy +from projectq.ops import get_inverse, BasicGate +from ._basics import NotInvertible, NotMergeable + + +class UniformlyControlledGate(BasicGate): + """ + A set of 2^k single qubit gates controlled on k choice qubits. + For each state of the choice qubits the corresponding + gate is applied to the target qubit. + + .. code-block:: python + gates = [H, Z, T, S] + U = UniformlyControlledGate(gates) + U | (choice_qubits, target_qubit) + + Attributes: + gates: list of 2^k single qubit gates + """ + def __init__(self, gates, up_to_diagonal=False): + self._gates = deepcopy(gates) + self.interchangeable_qubit_indices = [] + self._decomposition = None + self.up_to_diagonal = up_to_diagonal + + def get_inverse(self): + if self.up_to_diagonal: + raise NotInvertible + inverted_gates = [get_inverse(gate) for gate in self._gates] + return UniformlyControlledGate(inverted_gates) + + def get_merged(self, other): + if self.up_to_diagonal: + raise NotMergeable("Cannot merge these two gates.") + if isinstance(other, UniformlyControlledGate): + from projectq.libs.isometries import _SingleQubitGate + if len(self.gates) != len(other.gates): + raise NotMergeable("Cannot merge these two gates.") + new_gates = [_SingleQubitGate(self.gates[i].matrix * + other.gates[i].matrix) + for i in range(len(other.gates))] + return UniformlyControlledGate(new_gates) + raise NotMergeable("Cannot merge these two gates.") + + @property + def decomposition(self): + if self._decomposition is None: + from projectq.libs.isometries import \ + _decompose_uniformly_controlled_gate + self._decomposition = \ + _decompose_uniformly_controlled_gate(self._gates) + return self._decomposition + + @property + def gates(self): + return self._gates + + def __str__(self): + return "UCG" + + def __eq__(self, other): + """ Not implemented as this object is a floating point type.""" + return NotImplemented + + def __ne__(self, other): + """ Not implemented as this object is a floating point type.""" + return NotImplemented diff --git a/projectq/ops/_uniformly_controlled_gate_test.py b/projectq/ops/_uniformly_controlled_gate_test.py new file mode 100644 index 000000000..2d10e9503 --- /dev/null +++ b/projectq/ops/_uniformly_controlled_gate_test.py @@ -0,0 +1,55 @@ +# 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. + +import cmath +import copy +import numpy as np + +from projectq import MainEngine +from projectq.ops import X, Y, Z, H, All, Measure +from projectq.meta import Dagger + +from . import _uniformly_controlled_gate as ucg + + +def test_merge(): + gates1 = [X, Y, Z, H] + gates2 = [H, Z, Y, X] + U1 = ucg.UniformlyControlledGate(gates1) + U2 = ucg.UniformlyControlledGate(gates2) + U = U1.get_merged(U2) + for i in range(len(gates1)): + assert np.allclose(gates1[i].matrix*gates2[i].matrix, + U.gates[i].matrix) + + +def test_dagger(): + gates = [X, Y, Z, H] + eng = MainEngine() + qureg = eng.allocate_qureg(3) + choice = qureg[1:] + target = qureg[0] + + U = ucg.UniformlyControlledGate(gates) + All(H) | qureg + U | (choice, target) + with Dagger(eng): + U | (choice, target) + All(H) | qureg + + eng.flush() + qbit_to_bit_map, vec = eng.backend.cheat() + assert np.isclose(vec[0], 1) + + All(Measure) | qureg diff --git a/projectq/setups/decompositions/__init__.py b/projectq/setups/decompositions/__init__.py index de557a065..35dac405a 100755 --- a/projectq/setups/decompositions/__init__.py +++ b/projectq/setups/decompositions/__init__.py @@ -34,6 +34,9 @@ swap2cnot, toffoli2cnotandtgate, time_evolution, + uniformly_controlled_gate, + diagonal_gate, + isometry, uniformlycontrolledr2cnot, phaseestimation, amplitudeamplification) @@ -62,6 +65,9 @@ swap2cnot, toffoli2cnotandtgate, time_evolution, + uniformly_controlled_gate, + diagonal_gate, + isometry, uniformlycontrolledr2cnot, phaseestimation, amplitudeamplification] diff --git a/projectq/setups/decompositions/_isometries_fixture.py b/projectq/setups/decompositions/_isometries_fixture.py new file mode 100644 index 000000000..bb5c5e52a --- /dev/null +++ b/projectq/setups/decompositions/_isometries_fixture.py @@ -0,0 +1,65 @@ +import pytest +import projectq +import projectq.libs.isometries.decompositions +import projectq.libs.isometries.decompositions as decompositions + + +def get_available_isometries_decompositions(): + result = ["python"] + try: + from projectq.libs.isometries import cppdec + result.append("cpp") + except ImportError: + # The C++ module was either not installed or is misconfigured. Skip. + pass + return result + + +@pytest.fixture(params=get_available_isometries_decompositions(), + scope='function') +def iso_decomp_chooser(request, monkeypatch): + from projectq.libs.isometries.decompose_diagonal import _DecomposeDiagonal + from projectq.libs.isometries.decompose_ucg import _DecomposeUCG + from projectq.libs.isometries.decompose_isometry import _DecomposeIsometry + + def _decompose_dg(phases): + return _DecomposeDiagonal(phases).get_decomposition() + + def _decompose_ucg(gates): + return _DecomposeUCG(gates).get_decomposition() + + def _decompose_ig(columns, threshold): + return _DecomposeIsometry(columns, threshold).get_decomposition() + + if request.param == 'python': + monkeypatch.setattr(projectq.libs.isometries, + "_decompose_diagonal_gate", + _decompose_dg) + monkeypatch.setattr(projectq.libs.isometries.decompositions, + "_decompose_diagonal_gate", + _decompose_dg) + + monkeypatch.setattr(projectq.libs.isometries, + "_decompose_uniformly_controlled_gate", + _decompose_ucg) + monkeypatch.setattr(projectq.libs.isometries.decompositions, + "_decompose_uniformly_controlled_gate", + _decompose_ucg) + + monkeypatch.setattr(projectq.libs.isometries, + "_decompose_isometry", + _decompose_ig) + monkeypatch.setattr(projectq.libs.isometries.decompositions, + "_decompose_isometry", + _decompose_ig) + + assert decompositions._decompose_diagonal_gate is _decompose_dg + assert decompositions._decompose_uniformly_controlled_gate is _decompose_ucg + assert decompositions._decompose_isometry is _decompose_ig + + else: + # Make sure we are not using the Python verison of the important + # classes + assert decompositions._DecomposeDiagonal is not _DecomposeDiagonal + assert decompositions._DecomposeUCG is not _DecomposeUCG + assert decompositions._DecomposeIsometry is not _DecomposeIsometry diff --git a/projectq/setups/decompositions/arb1qubit2rzandry.py b/projectq/setups/decompositions/arb1qubit2rzandry.py index 6d3ea6e43..fc2d552bf 100644 --- a/projectq/setups/decompositions/arb1qubit2rzandry.py +++ b/projectq/setups/decompositions/arb1qubit2rzandry.py @@ -38,7 +38,8 @@ from projectq.ops import BasicGate, Ph, Ry, Rz -TOLERANCE = 1e-12 +#TOLERANCE = 1e-12 +TOLERANCE = 1e-6 def _recognize_arb1qubit(cmd): diff --git a/projectq/setups/decompositions/diagonal_gate.py b/projectq/setups/decompositions/diagonal_gate.py new file mode 100644 index 000000000..ade3e8897 --- /dev/null +++ b/projectq/setups/decompositions/diagonal_gate.py @@ -0,0 +1,36 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from projectq.cengines import DecompositionRule +from projectq.meta import Control +from projectq.ops import DiagonalGate +from projectq.libs.isometries import _apply_diagonal_gate + + +def _decompose_diagonal_gate(cmd): + diag = cmd.gate + decomposition = diag.decomposition + ctrl = cmd.control_qubits + + qureg = [] + for reg in cmd.qubits: + qureg.extend(reg) + + with Control(cmd.engine, ctrl): + _apply_diagonal_gate(decomposition, qureg) + + +all_defined_decomposition_rules = [ + DecompositionRule(DiagonalGate, _decompose_diagonal_gate) +] diff --git a/projectq/setups/decompositions/diagonal_gate_test.py b/projectq/setups/decompositions/diagonal_gate_test.py new file mode 100644 index 000000000..d3fe93358 --- /dev/null +++ b/projectq/setups/decompositions/diagonal_gate_test.py @@ -0,0 +1,50 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from projectq import MainEngine +from projectq.ops import X, DiagonalGate +from . import diagonal_gate as diag +from ._isometries_fixture import iso_decomp_chooser + +import numpy as np +import cmath +import pytest + + +def create_initial_state(mask, qureg): + n = len(qureg) + for pos in range(n): + if ((mask >> pos) & 1) == 1: + X | qureg[pos] + + +@pytest.mark.parametrize("init", range(16)) +def test_decompose_diagonal_gate(init, iso_decomp_chooser): + angles = list(range(1, 9)) + eng = MainEngine(verbose=True) + qureg = eng.allocate_qureg(4) + eng.flush() + create_initial_state(init, qureg) + + D = DiagonalGate(angles=angles) + cmd = D.generate_command(qureg[1:]) + diag._decompose_diagonal_gate(cmd) + + eng.flush() + qbit_to_bit_map, final_wavefunction = eng.backend.cheat() + print(qbit_to_bit_map) + vec = np.array([final_wavefunction]).T + + print(vec.item(init) - cmath.exp(1j*(((init >> 1) & 7)+1))) + assert np.isclose(vec.item(init), cmath.exp(1j*(((init >> 1) & 7)+1))) diff --git a/projectq/setups/decompositions/isometry.py b/projectq/setups/decompositions/isometry.py new file mode 100644 index 000000000..172ad9048 --- /dev/null +++ b/projectq/setups/decompositions/isometry.py @@ -0,0 +1,38 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from projectq.ops import Isometry +from projectq.meta import Control +from projectq.cengines import DecompositionRule +from projectq.libs.isometries import _apply_isometry + + +def _decompose_isometry(cmd): + iso = cmd.gate + decomposition = iso.decomposition + threshold = iso._threshold + ctrl = cmd.control_qubits + + qureg = [] + for reg in cmd.qubits: + qureg.extend(reg) + + with Control(cmd.engine, ctrl): + _apply_isometry(decomposition, threshold, qureg) + + +#: Decomposition rules +all_defined_decomposition_rules = [ + DecompositionRule(Isometry, _decompose_isometry) +] diff --git a/projectq/setups/decompositions/isometry_test.py b/projectq/setups/decompositions/isometry_test.py new file mode 100644 index 000000000..674b1f87a --- /dev/null +++ b/projectq/setups/decompositions/isometry_test.py @@ -0,0 +1,162 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from projectq import MainEngine +from projectq.ops import (All, Measure, X, Isometry) + +import numpy as np +import cmath +import pytest +from ._isometries_fixture import iso_decomp_chooser + +from . import isometry as iso + + +def normalize(v): + return v/np.linalg.norm(v) + + +def test_state_prep(iso_decomp_chooser): + n = 5 + target_state = np.array([i for i in range(1 << n)]) + target_state = normalize(target_state) + V = target_state + V.shape = (1 << n, 1) + + eng = MainEngine() + qureg = eng.allocate_qureg(n) + eng.flush() + + cmd = Isometry(V).generate_command(qureg) + iso._decompose_isometry(cmd) + + eng.flush() + order, result = eng.backend.cheat() + + assert np.allclose(result, V[:, 0]) + All(Measure) | qureg + + +def test_2_columns(iso_decomp_chooser): + col_0 = normalize(np.array([1.j, 2., 3.j, 4., -5.j, 6., 1+7.j, 8.])) + col_1 = normalize(np.array([8.j, 7., 6.j, 5., -4.j, 3., 1+2.j, 1.])) + # must be orthogonal + col_1 = normalize(col_1 - col_0*(np.vdot(col_0, col_1))) + assert abs(np.vdot(col_0, col_1)) < 1e-10 + V = np.array([col_0, col_1]).transpose() + + eng = MainEngine() + qureg = eng.allocate_qureg(3) + eng.flush() + cmd = Isometry(V).generate_command(qureg) + iso._decompose_isometry(cmd) + eng.flush() + order, result = eng.backend.cheat() + assert np.allclose(result, col_0) + eng.flush() + All(Measure) | qureg + + eng = MainEngine() + qureg = eng.allocate_qureg(3) + eng.flush() + X | qureg[0] + cmd = Isometry(V).generate_command(qureg) + iso._decompose_isometry(cmd) + eng.flush() + order, result = eng.backend.cheat() + assert np.allclose(result, col_1) + eng.flush() + All(Measure) | qureg + + +def create_initial_state(mask, qureg): + n = len(qureg) + for pos in range(n): + if ((mask >> pos) & 1) == 1: + X | qureg[pos] + + +# TODO: figure out why the monkeypatching with the iso_decomp_chooser +# fixture leads to some errors in the phase estimation tests +@pytest.mark.parametrize("index", range(8)) +def test_full_unitary_3_qubits(index): + n = 3 + N = 1 << n + np.random.seed(7) + re = np.random.rand(N, N) + im = np.random.rand(N, N) + M = re + 1j*im + V, R = np.linalg.qr(M, mode='reduced') + + # must be orthogonal + for i in range(N): + for j in range(N): + if i != j: + assert abs(np.vdot(V[:, i], V[:, j])) < 1e-14 + + eng = MainEngine() + qureg = eng.allocate_qureg(n) + eng.flush() + create_initial_state(index, qureg) + cmd = Isometry(V).generate_command(qureg) + iso._decompose_isometry(cmd) + eng.flush() + order, result = eng.backend.cheat() + assert np.allclose(result, V[:, index]) + All(Measure) | qureg + eng.flush() + + +# TODO: figure out why the monkeypatching with the iso_decomp_chooser +# fixture leads to some errors in the phase estimation tests +@pytest.mark.parametrize("index", range(8)) +def test_full_permutation_matrix_3_qubits(index): + n = 3 + N = 1 << n + np.random.seed(7) + V = np.zeros((N, N), dtype=complex) + perm = np.random.permutation(N) + for i in range(N): + V[i, perm[i]] = 1.0 + + eng = MainEngine() + qureg = eng.allocate_qureg(n) + eng.flush() + create_initial_state(index, qureg) + cmd = Isometry(V).generate_command(qureg) + iso._decompose_isometry(cmd) + eng.flush() + order, result = eng.backend.cheat() + print(order) + _print_vec(V[:, index]) + _print_qureg(qureg) + assert np.allclose(result, V[:, index]) + All(Measure) | qureg + eng.flush() + + +# useful for debugging +def _print_qureg(qureg): + eng = qureg.engine + eng.flush() + bla, vec = eng.backend.cheat() + for i in range(len(vec)): + print("{}: {:.3f}, {}".format(i, abs(vec[i]), cmath.phase(vec[i]))) + print("-") + + +def _print_vec(vec): + for i in range(len(vec)): + print("{}: {:.3f}, {}".format(i, abs(vec[i]), cmath.phase(vec[i]))) + print("-") diff --git a/projectq/setups/decompositions/uniformly_controlled_gate.py b/projectq/setups/decompositions/uniformly_controlled_gate.py new file mode 100644 index 000000000..f8b3484f8 --- /dev/null +++ b/projectq/setups/decompositions/uniformly_controlled_gate.py @@ -0,0 +1,38 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from projectq.cengines import DecompositionRule +from projectq.ops import UniformlyControlledGate +from projectq.meta import Control +from projectq.libs.isometries import _apply_uniformly_controlled_gate + + +def _decompose_uniformly_controlled_gate(cmd): + ucg = cmd.gate + + decomposition = ucg.decomposition + choice_reg = cmd.qubits[0] + target = cmd.qubits[1] + ctrl = cmd.control_qubits + up_to_diagonal = ucg.up_to_diagonal + + with Control(cmd.engine, ctrl): + _apply_uniformly_controlled_gate(decomposition, target, + choice_reg, up_to_diagonal) + + +all_defined_decomposition_rules = [ + DecompositionRule(UniformlyControlledGate, + _decompose_uniformly_controlled_gate) +] diff --git a/projectq/setups/decompositions/uniformly_controlled_gate_test.py b/projectq/setups/decompositions/uniformly_controlled_gate_test.py new file mode 100644 index 000000000..01d47a03d --- /dev/null +++ b/projectq/setups/decompositions/uniformly_controlled_gate_test.py @@ -0,0 +1,147 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.uniformly_controlled_gate." +import copy + +import numpy as np +import pytest +import random +from scipy.linalg import block_diag + +from projectq import MainEngine +from projectq.meta import Control, Dagger, Compute, Uncompute +from projectq.ops import H, Rx, Ry, Rz, X, UniformlyControlledGate + +from . import uniformly_controlled_gate as ucg + +from projectq.libs.isometries import _SingleQubitGate + +from ._isometries_fixture import iso_decomp_chooser + + +def test_full_decomposition_1_choice(iso_decomp_chooser): + eng = MainEngine() + qureg = eng.allocate_qureg(2) + eng.flush() + A = Rx(np.pi / 5) + B = Ry(np.pi / 3) + UCG = UniformlyControlledGate([A, B]) + cmd = UCG.generate_command(([qureg[1]], qureg[0])) + with Dagger(eng): + ucg._decompose_uniformly_controlled_gate(cmd) + eng.flush() + qbit_to_bit_map, final_wavefunction = copy.deepcopy(eng.backend.cheat()) + vec = np.array([final_wavefunction]).T + reference = np.matrix(block_diag(A.matrix, B.matrix)) + print(reference * vec) + assert np.isclose((reference * vec).item(0), 1) + + +def test_full_decomposition_2_choice(iso_decomp_chooser): + eng = MainEngine() + qureg = eng.allocate_qureg(3) + eng.flush() + A = Rx(np.pi / 5) + B = H + C = Rz(np.pi / 5) + D = Ry(np.pi / 3) + UCG = UniformlyControlledGate([A, B, C, D]) + cmd = UCG.generate_command((qureg[1:], qureg[0])) + with Dagger(eng): + ucg._decompose_uniformly_controlled_gate(cmd) + eng.flush() + qbit_to_bit_map, final_wavefunction = copy.deepcopy(eng.backend.cheat()) + vec = np.array([final_wavefunction]).T + reference = np.matrix(block_diag(A.matrix, B.matrix, C.matrix, D.matrix)) + print(reference * vec) + assert np.isclose((reference * vec).item(0), 1) + + +def test_full_decomposition_2_choice_target_in_middle(iso_decomp_chooser): + eng = MainEngine() + qureg = eng.allocate_qureg(3) + eng.flush() + A = Rx(np.pi / 5) + B = H + C = Rz(np.pi / 5) + D = Ry(np.pi / 3) + UCG = UniformlyControlledGate([A, B, C, D]) + cmd = UCG.generate_command(([qureg[0], qureg[2]], qureg[1])) + with Dagger(eng): + ucg._decompose_uniformly_controlled_gate(cmd) + eng.flush() + qbit_to_bit_map, final_wavefunction = copy.deepcopy(eng.backend.cheat()) + print(qbit_to_bit_map) + vec = np.array([final_wavefunction]).T + vec[[1, 2]] = vec[[2, 1]] # reorder basis + vec[[5, 6]] = vec[[6, 5]] + reference = np.matrix(block_diag(A.matrix, B.matrix, C.matrix, D.matrix)) + print(reference * vec) + assert np.isclose((reference * vec).item(0), 1) + + +def apply_mask(mask, qureg): + n = len(qureg) + for pos in range(n): + if ((mask >> pos) & 1) == 0: + X | qureg[pos] + + +def create_initial_state(mask, qureg): + n = len(qureg) + for pos in range(n): + if ((mask >> pos) & 1) == 1: + X | qureg[pos] + + +@pytest.mark.parametrize("init", range(10)) +def test_full_decomposition_4_choice_target_in_middle(init, iso_decomp_chooser): + n = 4 + eng = MainEngine() + qureg = eng.allocate_qureg(n) + eng.flush() # makes sure the qubits are allocated in order + create_initial_state(init, qureg) + + random.seed(42) + gates = [] + for i in range(1 << (n - 1)): + a = Rx(random.uniform(0, 2 * np.pi)).matrix + b = Ry(random.uniform(0, 2 * np.pi)).matrix + c = Rx(random.uniform(0, 2 * np.pi)).matrix + gates.append(_SingleQubitGate(a * b * c)) + + choice = qureg[1:] + target = qureg[0] + print(len(choice)) + print(len(gates)) + UCG = UniformlyControlledGate(gates) + dec = UCG.decomposition + + cmd = UCG.generate_command((choice, target)) + with Dagger(eng): + ucg._decompose_uniformly_controlled_gate(cmd) + for k in range(1 << (n - 1)): + with Compute(eng): + apply_mask(k, choice) + with Control(eng, choice): + gates[k] | target + Uncompute(eng) + + eng.flush() + qbit_to_bit_map, final_wavefunction = copy.deepcopy(eng.backend.cheat()) + print(qbit_to_bit_map) + vec = np.array([final_wavefunction]).T + print(vec) + assert np.isclose(abs((vec).item(init)), 1) diff --git a/projectq/setups/ibm.py b/projectq/setups/ibm.py index acedeed00..a5fb2c802 100755 --- a/projectq/setups/ibm.py +++ b/projectq/setups/ibm.py @@ -11,116 +11,46 @@ # 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. -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 - - -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 +It provides the `engine_list` for the `MainEngine`, and contains an +AutoReplacer with most of the gate decompositions of ProjectQ, among others +it includes: + * 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 -class DeviceOfflineError(Exception): - pass - - -class DeviceNotHandledError(Exception): - pass +Moreover, it contains `LocalOptimizers` and a custom mapper for the CNOT +gates. +""" -def list2set(coupling_list): - result = [] - for el in coupling_list: - result.append(tuple(el)) - return set(result) +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)] diff --git a/projectq/setups/ibm_test.py b/projectq/setups/ibm_test.py index 26b41b24a..598b949cb 100644 --- a/projectq/setups/ibm_test.py +++ b/projectq/setups/ibm_test.py @@ -13,60 +13,17 @@ # limitations under the License. """Tests for projectq.setup.ibm.""" -import pytest +import projectq +from projectq import MainEngine +from projectq.cengines import IBM5QubitMapper, SwapAndCNOTFlipper -def test_ibm_cnot_mapper_in_cengines(monkeypatch): +def test_ibm_cnot_mapper_in_cengines(): 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_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') + 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 diff --git a/requirements.txt b/requirements.txt index 60d6b013c..903d45bdc 100755 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,3 @@ pybind11>=2.2.3 requests scipy networkx -matplotlib>=2.2.3