Skip to content

Commit

Permalink
Ibm backend v2 (solves #318 and #347) (#349)
Browse files Browse the repository at this point in the history
* reduced Ry decomposition : modification of restrictedgateset setup to include compiler chooser, extension of available decompositions, new setup based on restrictedgateset but adapted for trapped ion configurations

* Addition of test file for the rz2rx decomposition

Addition of test file for the rz2rx decomposition and edit to comments in the rz2rx file

* Revert "Addition of test file for the rz2rx decomposition"

This reverts commit 5aab56b.

* Create rz2rx_test file

Addition of test file for the rz2rx decomposition

* Update rz2rx.py

Update to comments

* Update rz2rx_test.py

Update to comments

* Minor update: remove accidental file

* Minor update rz2rx.py

Corrected an angle.

* Minor update rz2rx_test.py

Edited comments.

* Create h2rx_test.py

Updated method for test_decomposition which tests that decomposition produces identical wave-state up to a global phase.

* Improvement of the decomposition chooser; Note that basic restricted gate
set will fail decomposing into Rxx because of the default chooser

* Updates to h2rx and cnot2rxx

* Create cnot2rxx_test.py

Testing file for cnot2rxx decomposition. Includes updated method for test_decomposition which tests that decomposition produces identical wave-state up to a global phase.

* basic rotation gates at an angle of 0 or 2pi are removed by the optimizer.
basic rotation gates ignore now the global phase and are defined over 0:2pi

* Update and create trapped_ion_decomposer_test.py

* Minor update

Documentation and comments.

* Update on comments regarding Identity gates

* Changes 1/2 : command can be printed with unicode symbols only via new
to_String method; syntax correction;

* Work in progress, is commutable

* Update to decomposition test files

rz2rx_test now tests each decomposition defined in rz2rx.all_defined_decomposition_rules
Similar for cnot2rxx_test and h2rx_test

* Revert "Work in progress, is commutable"

This reverts commit 27f820c.

* ibmq fix: projectq uses new ibmq API (no doc available, just look at qiskit). Connection fixed for 5qb devices, the 15qb melbourne device and the
online simulator
coupling map obtained from ibm server instead of being manually written
one setup can be used for the 3 different backend

* minor fixes

* update on the ibm example

* minor fixes; revert rotation gates to [0;4pi)

* fix comments

* fix mapper choice for simulator. added comments

* minor fixes with results, mapper and testing files. Improvement of
get_probabilities

* Revert "Merge branch 'develop' into ibm_V2"

This reverts commit cd0452a, reversing
changes made to 03daf79.

* minor fixes

* bug fix in client test file

* fixing bug and python2.7 compatibility for testing files

* fix errors

* major fix on testing files

* minor fix on comments and python 2.7 compatibility

* fix 'super()' call for python 2.7

* additional tests

* python2.7 fix

* increase coverage, fix a print statement

* Some minor stylistic adjustments

* Reindent files and fix some linting warnings/errors

* Improve test coverage

* Improve code readability

Co-authored-by: Nguyen Damien <ngn.damien@gmail.com>
  • Loading branch information
dbretaud and Takishima committed Feb 4, 2020
1 parent a80fa84 commit 7da50cf
Show file tree
Hide file tree
Showing 10 changed files with 1,210 additions and 516 deletions.
19 changes: 15 additions & 4 deletions examples/ibm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
from projectq.backends import IBMBackend
from projectq.ops import Measure, Entangle, All
from projectq import MainEngine
import getpass


def run_entangle(eng, num_qubits=5):
def run_entangle(eng, num_qubits=3):
"""
Runs an entangling operation on the provided compiler engine.
Expand Down Expand Up @@ -37,9 +38,19 @@ def run_entangle(eng, num_qubits=5):


if __name__ == "__main__":
#devices commonly available :
#ibmq_16_melbourne (15 qubit)
#ibmq_essex (5 qubit)
#ibmq_qasm_simulator (32 qubits)
device = None #replace by the IBM device name you want to use
token = None #replace by the token given by IBMQ
if token is None:
token = getpass.getpass(prompt='IBM Q token > ')
if device is None:
token = getpass.getpass(prompt='IBM device > ')
# create main compiler engine for the IBM back-end
eng = MainEngine(IBMBackend(use_hardware=True, num_runs=1024,
verbose=False, device='ibmqx4'),
engine_list=projectq.setups.ibm.get_engine_list())
eng = MainEngine(IBMBackend(use_hardware=True, token=token num_runs=1024,
verbose=False, device=device),
engine_list=projectq.setups.ibm.get_engine_list(token=token, device=device))
# run the circuit and print the result
print(run_entangle(eng))
112 changes: 60 additions & 52 deletions projectq/backends/_ibm/_ibm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -41,11 +41,11 @@

class IBMBackend(BasicEngine):
"""
The IBM Backend class, which stores the circuit, transforms it to JSON
QASM, and sends the circuit through the IBM API.
The IBM Backend class, which stores the circuit, transforms it to JSON,
and sends the circuit through the IBM API.
"""
def __init__(self, use_hardware=False, num_runs=1024, verbose=False,
user=None, password=None, device='ibmqx4',
token='', device='ibmq_essex',
num_retries=3000, interval=1,
retrieve_execution=None):
"""
Expand All @@ -59,10 +59,8 @@ def __init__(self, use_hardware=False, num_runs=1024, verbose=False,
verbose (bool): If True, statistics are printed, in addition to
the measurement result being registered (at the end of the
circuit).
user (string): IBM Quantum Experience user name
password (string): IBM Quantum Experience password
device (string): Device to use ('ibmqx4', or 'ibmqx5')
if use_hardware is set to True. Default is ibmqx4.
token (str): IBM quantum experience user password.
device (str): name of the IBM device to use. ibmq_essex By default
num_retries (int): Number of times to retry to obtain
results from the IBM API. (default is 3000)
interval (float, int): Number of seconds between successive
Expand All @@ -76,15 +74,15 @@ def __init__(self, use_hardware=False, num_runs=1024, verbose=False,
if use_hardware:
self.device = device
else:
self.device = 'simulator'
self.device = 'ibmq_qasm_simulator'
self._num_runs = num_runs
self._verbose = verbose
self._user = user
self._password = password
self._token=token
self._num_retries = num_retries
self._interval = interval
self._probabilities = dict()
self.qasm = ""
self._json=[]
self._measured_ids = []
self._allocated_qubits = set()
self._retrieve_execution = retrieve_execution
Expand All @@ -93,24 +91,29 @@ def is_available(self, cmd):
"""
Return true if the command can be executed.
The IBM quantum chip can do X, Y, Z, T, Tdag, S, Sdag,
rotation gates, barriers, and CX / CNOT.
The IBM quantum chip can only do U1,U2,U3,barriers, and CX / CNOT.
Conversion implemented for Rotation gates and H gates.
Args:
cmd (Command): Command for which to check availability
"""
g = cmd.gate
if g == NOT and get_control_count(cmd) <= 1:
if g == NOT and get_control_count(cmd) == 1:
return True
if get_control_count(cmd) == 0:
if g in (T, Tdag, S, Sdag, H, Y, Z):
if g == H:
return True
if isinstance(g, (Rx, Ry, Rz)):
return True
if g in (Measure, Allocate, Deallocate, Barrier):
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
Expand All @@ -129,10 +132,10 @@ def _store(self, cmd):
self._probabilities = dict()
self._clear = False
self.qasm = ""
self._json=[]
self._allocated_qubits = set()

gate = cmd.gate

if gate == Allocate:
self._allocated_qubits.add(cmd.qubits[0][0].id)
return
Expand All @@ -154,29 +157,36 @@ 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 "
qb_str = ""
for pos in qb_pos:
qb_str += "q[{}], ".format(pos)
self.qasm += qb_str[:-2] + ";"
self._json.append({'qubits': qb_pos, 'name': 'barrier'})
elif isinstance(gate, (Rx, Ry, Rz)):
assert get_control_count(cmd) == 0
qb_pos = cmd.qubits[0][0].id
u_strs = {'Rx': 'u3({}, -pi/2, pi/2)', 'Ry': 'u3({}, 0, 0)',
'Rz': 'u1({})'}
gate = u_strs[str(gate)[0:2]].format(gate.angle)
self.qasm += "\n{} q[{}];".format(gate, qb_pos)
else:
u_name = {'Rx': 'u3', 'Ry': 'u3',
'Rz': 'u1'}
u_angle = {'Rx': [gate.angle, -math.pi/2, math.pi/2], 'Ry': [gate.angle, 0, 0],
'Rz': [gate.angle]}
gate_qasm = u_strs[str(gate)[0:2]].format(gate.angle)
gate_name=u_name[str(gate)[0:2]]
params= u_angle[str(gate)[0:2]]
self.qasm += "\n{} q[{}];".format(gate_qasm, qb_pos)
self._json.append({'qubits': [qb_pos], 'name': gate_name,'params': params})
elif gate == H:
assert get_control_count(cmd) == 0
if str(gate) in self._gate_names:
gate_str = self._gate_names[str(gate)]
else:
gate_str = str(gate).lower()

qb_pos = cmd.qubits[0][0].id
self.qasm += "\n{} q[{}];".format(gate_str, qb_pos)
self.qasm += "\nu2(0,pi/2) q[{}];".format(qb_pos)
self._json.append({'qubits': [qb_pos], 'name': 'u2','params': [0, 3.141592653589793]})
else:
raise Exception('Command not authorized. You should run the circuit with the appropriate ibm setup.')

def _logical_to_physical(self, qb_id):
"""
Expand All @@ -198,6 +208,8 @@ def _logical_to_physical(self, qb_id):
def get_probabilities(self, qureg):
"""
Return the list of basis states with corresponding probabilities.
If input qureg is a subset of the register used for the experiment,
then returns the projected probabilities over the other states.
The measured bits are ordered according to the supplied quantum
register, i.e., the left-most bit in the state-string corresponds to
Expand All @@ -212,7 +224,7 @@ def get_probabilities(self, qureg):
Returns:
probability_dict (dict): Dictionary mapping n-bit strings to
probabilities.
probabilities.
Raises:
RuntimeError: If no data is available (i.e., if the circuit has
Expand All @@ -223,68 +235,70 @@ def get_probabilities(self, qureg):
raise RuntimeError("Please, run the circuit first!")

probability_dict = dict()

for state in self._probabilities:
mapped_state = ['0'] * len(qureg)
for i in range(len(qureg)):
mapped_state[i] = state[self._logical_to_physical(qureg[i].id)]
probability = self._probabilities[state]
probability_dict["".join(mapped_state)] = probability

mapped_state = "".join(mapped_state)
if mapped_state not in probability_dict:
probability_dict[mapped_state] = probability
else:
probability_dict[mapped_state] += probability
return probability_dict

def _run(self):
"""
Run the circuit.
Send the circuit via the IBM API (JSON QASM) using the provided user
data / ask for username & password.
Send the circuit via a non documented IBM API (using JSON written
circuits) using the provided user data / ask for the user token.
"""
# finally: add measurements (no intermediate measurements are allowed)
for measured_id in self._measured_ids:
qb_loc = self.main_engine.mapper.current_mapping[measured_id]
self.qasm += "\nmeasure q[{}] -> c[{}];".format(qb_loc,
qb_loc)

self._json.append({'qubits': [qb_loc], 'name': 'measure','memory':[qb_loc]})
# return if no operations / measurements have been performed.
if self.qasm == "":
return

max_qubit_id = max(self._allocated_qubits)
max_qubit_id = max(self._allocated_qubits) + 1
qasm = ("\ninclude \"qelib1.inc\";\nqreg q[{nq}];\ncreg c[{nq}];" +
self.qasm).format(nq=max_qubit_id + 1)
self.qasm).format(nq=max_qubit_id)
info = {}
info['qasms'] = [{'qasm': qasm}]
info['json']=self._json
info['nq']=max_qubit_id

info['shots'] = self._num_runs
info['maxCredits'] = 5
info['maxCredits'] = 10
info['backend'] = {'name': self.device}
info = json.dumps(info)

try:
if self._retrieve_execution is None:
res = send(info, device=self.device,
user=self._user, password=self._password,
shots=self._num_runs,
token=self._token,
num_retries=self._num_retries,
interval=self._interval,
verbose=self._verbose)
else:
res = retrieve(device=self.device, user=self._user,
password=self._password,
res = retrieve(device=self.device,
token=self._token,
jobid=self._retrieve_execution,
num_retries=self._num_retries,
interval=self._interval,
verbose=self._verbose)

counts = res['data']['counts']
# Determine random outcome
P = random.random()
p_sum = 0.
measured = ""
length=len(self._measured_ids)
for state in counts:
probability = counts[state] * 1. / self._num_runs
state = list(reversed(state))
state = "".join(state)
state="{0:b}".format(int(state,0))
state=state.zfill(max_qubit_id)
#states in ibmq are right-ordered, so need to reverse state string
state=state[::-1]
p_sum += probability
star = ""
if p_sum >= P and measured == "":
Expand Down Expand Up @@ -322,9 +336,3 @@ def receive(self, command_list):
else:
self._run()
self._reset()

"""
Mapping of gate names from our gate objects to the IBM QASM representation.
"""
_gate_names = {str(Tdag): "tdg",
str(Sdag): "sdg"}

0 comments on commit 7da50cf

Please sign in to comment.