Skip to content
18 changes: 16 additions & 2 deletions projectq/backends/_ibm/_ibm.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class IBMBackend(BasicEngine):
"""
def __init__(self, use_hardware=False, num_runs=1024, verbose=False,
user=None, password=None, device='ibmqx4',
num_retries=3000, interval=1,
retrieve_execution=None):
"""
Initialize the Backend object.
Expand All @@ -62,6 +63,11 @@ def __init__(self, use_hardware=False, num_runs=1024, verbose=False,
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
attempts to obtain results from the IBM API.
(default is 1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't make this an int. We might even only expose num_retries and drop interval here; what do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, the (int) is not necessary. Fixed in 53d2483. But why not expose interval? Gives your users more freedom and I don't see any downsides. In fact, it makes it more obvious that the API works by periodically polling the IBM servers for a result, which is a nice side effect.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I don't see why I'd ever want to change the interval; do you?
Regarding your reason for adding it: One can also just add a comment in the docs about the "periodically polling the IBM servers" to make it obvious.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What makes you so sure that, say 1s, is always optimal for any application using ProjectQ? Maybe someone needs a result as soon as it is available and cannot wait for at least 1s, and maybe someone else knows that their computation will take a while and they don't want to spam IBM with requests to their API that they anyway know will yield no result.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't claim it was optimal; I was just asking if you saw a use case.

If I cannot afford to wait for 1s, then I wouldn't use the service in the first place since the queue tends to be quite long. If I want to retrieve the results at a later point (e.g., because I know that the queue is too long), I'd just use retrieve_execution.

Since you find it useful to have access to interval (beyond it having no downsides), we should add it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My perspective is that of someone developing a library on top of ProjectQ (https://pennylane-pq.readthedocs.io/en/latest/installation.html) and and in my code I anyway have to block until I get results from the queue because the following code immediately depends on the outcome. Using retreive_execution is thus not a good option, but having an option to have the user set interval would be nice :-)

retrieve_execution (int): Job ID to retrieve instead of re-
running the circuit (e.g., if previous run timed out).
"""
Expand All @@ -75,6 +81,8 @@ def __init__(self, use_hardware=False, num_runs=1024, verbose=False,
self._verbose = verbose
self._user = user
self._password = password
self._num_retries = num_retries
self._interval = interval
self._probabilities = dict()
self.qasm = ""
self._measured_ids = []
Expand Down Expand Up @@ -256,11 +264,17 @@ def _run(self):
if self._retrieve_execution is None:
res = send(info, device=self.device,
user=self._user, password=self._password,
shots=self._num_runs, verbose=self._verbose)
shots=self._num_runs,
num_retries=self._num_retries,
interval=self._interval,
verbose=self._verbose)
else:
res = retrieve(device=self.device, user=self._user,
password=self._password,
jobid=self._retrieve_execution)
jobid=self._retrieve_execution,
num_retries=self._num_retries,
interval=self._interval,
verbose=self._verbose)

counts = res['data']['counts']
# Determine random outcome
Expand Down
74 changes: 47 additions & 27 deletions projectq/backends/_ibm/_ibm_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import requests
import getpass
import json
import signal
import sys
import time
from requests.compat import urljoin
Expand All @@ -35,7 +36,8 @@ def is_online(device):
return r.json()['state']


def retrieve(device, user, password, jobid):
def retrieve(device, user, password, jobid, num_retries=3000,
interval=1, verbose=False):
"""
Retrieves a previously run job by its ID.

Expand All @@ -46,12 +48,13 @@ def retrieve(device, user, password, jobid):
jobid (str): Id of the job to retrieve
"""
user_id, access_token = _authenticate(user, password)
res = _get_result(device, jobid, access_token)
res = _get_result(device, jobid, access_token, num_retries=num_retries,
interval=interval, verbose=verbose)
return res


def send(info, device='sim_trivial_2', user=None, password=None,
shots=1, verbose=False):
shots=1, num_retries=3000, interval=1, verbose=False):
"""
Sends QASM through the IBM API and runs the quantum circuit.

Expand Down Expand Up @@ -84,7 +87,9 @@ def send(info, device='sim_trivial_2', user=None, password=None,
execution_id = _run(info, device, user_id, access_token, shots)
if verbose:
print("- Waiting for results...")
res = _get_result(device, execution_id, access_token)
res = _get_result(device, execution_id, access_token,
num_retries=num_retries,
interval=interval, verbose=verbose)
if verbose:
print("- Done.")
return res
Expand Down Expand Up @@ -143,32 +148,47 @@ def _run(qasm, device, user_id, access_token, shots):


def _get_result(device, execution_id, access_token, num_retries=3000,
interval=1):
interval=1, verbose=False):
suffix = 'Jobs/{execution_id}'.format(execution_id=execution_id)
status_url = urljoin(_api_url, 'Backends/{}/queue/status'.format(device))

print("Waiting for results. [Job ID: {}]".format(execution_id))

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)
if verbose:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diff is not doing a great job showing the change here, but you will see that all I did was print only if verbose is True, save the signal handler, set a signal handler that prints the job ID and make sure the old signal handler is restored via try/finally.

print("Waiting for results. [Job ID: {}]".format(execution_id))

original_sigint_handler = signal.getsignal(signal.SIGINT)

def _handle_sigint_during_get_result(*_):
raise Exception("Interrupted. The ID of your submitted job is {}."
.format(execution_id))

try:
signal.signal(signal.SIGINT, _handle_sigint_during_get_result)

for retries in range(num_retries):
r = requests.get(urljoin(_api_url, suffix),
params={"access_token": access_token})
r.raise_for_status()
r_json = r.json()
if '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 'lengthQueue' in r_json:
print("Currently there are {} jobs queued for execution on {}."
.format(r_json['lengthQueue'], device))
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))