Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open v0.3 API + debiasing #465

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Fixed some typos (thanks to @eltociear, @Darkdragon84)

### Changed

- Update ProjectQ to handle IonQ API v0.3

### Repository

- Update GitHub workflows to work with merge queues
Expand Down
8 changes: 8 additions & 0 deletions projectq/backends/_ionq/_ionq.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ def __init__(
verbose=False,
token=None,
device='ionq_simulator',
error_mitigation=None,
sharpen=None,
num_retries=3000,
interval=1,
retrieve_execution=None,
Expand All @@ -102,6 +104,8 @@ def __init__(
"""
super().__init__()
self.device = device if use_hardware else 'ionq_simulator'
self.error_mitigation = error_mitigation
self._sharpen = sharpen
self._num_runs = num_runs
self._verbose = verbose
self._token = token
Expand Down Expand Up @@ -291,6 +295,9 @@ def _run(self): # pylint: disable=too-many-locals
measured_ids = self._measured_ids[:]
info = {
'circuit': self._circuit,
'gateset': 'qis',
'format': 'ionq.circuit.v0',
'error_mitigation': self.error_mitigation,
'nq': len(qubit_mapping.keys()),
'shots': self._num_runs,
'meas_mapped': [qubit_mapping[qubit_id] for qubit_id in measured_ids],
Expand All @@ -311,6 +318,7 @@ def _run(self): # pylint: disable=too-many-locals
device=self.device,
token=self._token,
jobid=self._retrieve_execution,
sharpen=self._sharpen,
num_retries=self._num_retries,
interval=self._interval,
verbose=self._verbose,
Expand Down
60 changes: 40 additions & 20 deletions projectq/backends/_ionq/_ionq_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
RequestTimeoutError,
)

_API_URL = 'https://api.ionq.co/v0.2/'
_API_URL = 'https://api.ionq.co/v0.3/'
_JOB_API_URL = urljoin(_API_URL, 'jobs/')


Expand All @@ -40,6 +40,7 @@ class IonQ(Session):
def __init__(self, verbose=False):
"""Initialize an session with IonQ's APIs."""
super().__init__()
self.user_agent()
self.backends = {}
self.timeout = 5.0
self.token = None
Expand All @@ -59,7 +60,7 @@ def update_devices_list(self):
},
'ionq_qpu': {
'nq': 11,
'target': 'qpu',
'target': 'qpu.harmony',
},
}
for backend in r_json:
Expand Down Expand Up @@ -101,6 +102,10 @@ def can_run_experiment(self, info, device):
nb_qubit_needed = info['nq']
return nb_qubit_needed <= nb_qubit_max, nb_qubit_max, nb_qubit_needed

def user_agent(self):
"""Set a User-Agent header for this session."""
self.headers.update({'User-Agent': 'projectq-ionq/0.8.0'})

def authenticate(self, token=None):
"""Set an Authorization header for this session.

Expand Down Expand Up @@ -132,19 +137,22 @@ def run(self, info, device):
str: The ID of a newly submitted Job.
"""
argument = {
'target': self.backends[device]['target'],
'target': self.backends[device].get('target'),
'metadata': {
'sdk': 'ProjectQ',
'meas_qubit_ids': json.dumps(info['meas_qubit_ids']),
'meas_qubit_ids': json.dumps(info.get('meas_qubit_ids')),
},
'shots': info['shots'],
'registers': {'meas_mapped': info['meas_mapped']},
'lang': 'json',
'body': {
'qubits': info['nq'],
'circuit': info['circuit'],
'shots': info.get('shots'),
'registers': {'meas_mapped': info.get('meas_mapped')},
'input': {
'format': info.get('format'),
'gateset': info.get('gateset'),
'qubits': info.get('nq'),
'circuit': info.get('circuit'),
},
}
if info.get('error_mitigation') is not None:
argument['error_mitigation'] = info['error_mitigation']

# _API_URL[:-1] strips the trailing slash.
# TODO: Add comprehensive error parsing for non-200 responses.
Expand All @@ -153,11 +161,11 @@ def run(self, info, device):

# Process the response.
r_json = req.json()
status = r_json['status']
status = r_json.get('status')

# Return the job id.
if status == 'ready':
return r_json['id']
return r_json.get('id')

# Otherwise, extract any provided failure info and raise an exception.
failure = r_json.get('failure') or {
Expand All @@ -166,7 +174,9 @@ def run(self, info, device):
}
raise JobSubmissionError(f"{failure['code']}: {failure['error']} (status={status})")

def get_result(self, device, execution_id, num_retries=3000, interval=1):
def get_result(
self, device, execution_id, sharpen=None, num_retries=3000, interval=1
): # pylint: disable=too-many-arguments,too-many-locals
"""
Given a backend and ID, fetch the results for this job's execution.

Expand All @@ -178,6 +188,8 @@ def get_result(self, device, execution_id, num_retries=3000, interval=1):
Args:
device (str): The device used to run this job.
execution_id (str): An IonQ Job ID.
sharpen: A boolean that determines how to aggregate error mitigated.
If True, apply majority vote mitigation; if False, apply average mitigation.
num_retries (int, optional): Number of times to retry the fetch
before raising a timeout error. Defaults to 3000.
interval (int, optional): Number of seconds to wait between retries.
Expand All @@ -196,6 +208,10 @@ def get_result(self, device, execution_id, num_retries=3000, interval=1):
if self._verbose: # pragma: no cover
print(f"Waiting for results. [Job ID: {execution_id}]")

params = {}
if sharpen is not None:
params["sharpen"] = sharpen

original_sigint_handler = signal.getsignal(signal.SIGINT)

def _handle_sigint_during_get_result(*_): # pragma: no cover
Expand All @@ -205,18 +221,20 @@ def _handle_sigint_during_get_result(*_): # pragma: no cover

try:
for retries in range(num_retries):
req = super().get(urljoin(_JOB_API_URL, execution_id))
req = super().get(urljoin(_JOB_API_URL, execution_id), params=params)
req.raise_for_status()
r_json = req.json()
status = r_json['status']
req_json = req.json()
status = req_json['status']

# Check if job is completed.
if status == 'completed':
meas_mapped = r_json['registers']['meas_mapped']
meas_qubit_ids = json.loads(r_json['metadata']['meas_qubit_ids'])
output_probs = r_json['data']['registers']['meas_mapped']
r_get = super().get(urljoin(_JOB_API_URL, req_json.get('results_url')), params=params)
r_json = r_get.json()
meas_mapped = req_json['registers']['meas_mapped']
meas_qubit_ids = json.loads(req_json['metadata']['meas_qubit_ids'])
output_probs = r_json
return {
'nq': r_json['qubits'],
'nq': req_json['qubits'],
'output_probs': output_probs,
'meas_mapped': meas_mapped,
'meas_qubit_ids': meas_qubit_ids,
Expand Down Expand Up @@ -255,6 +273,7 @@ def retrieve(
device,
token,
jobid,
sharpen=None,
num_retries=3000,
interval=1,
verbose=False,
Expand All @@ -281,6 +300,7 @@ def retrieve(
res = ionq_session.get_result(
device,
jobid,
sharpen=sharpen,
num_retries=num_retries,
interval=interval,
)
Expand Down
75 changes: 37 additions & 38 deletions projectq/backends/_ionq/_ionq_http_client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def user_password_input(prompt):

def test_is_online(monkeypatch):
def mock_get(_self, path, *args, **kwargs):
assert 'https://api.ionq.co/v0.2/backends' == path
assert 'https://api.ionq.co/v0.3/backends' == path
mock_response = mock.MagicMock()
mock_response.json = mock.MagicMock(
return_value=[
Expand Down Expand Up @@ -86,7 +86,7 @@ def mock_get(_self, path, *args, **kwargs):

def test_show_devices(monkeypatch):
def mock_get(_self, path, *args, **kwargs):
assert 'https://api.ionq.co/v0.2/backends' == path
assert 'https://api.ionq.co/v0.3/backends' == path
mock_response = mock.MagicMock()
mock_response.json = mock.MagicMock(
return_value=[
Expand Down Expand Up @@ -168,8 +168,9 @@ def _dummy_update(_self):
'metadata': {'sdk': 'ProjectQ', 'meas_qubit_ids': '[2, 3]'},
'shots': 1,
'registers': {'meas_mapped': [2, 3]},
'lang': 'json',
'body': {
'input': {
'format': None,
'gateset': None,
'qubits': 4,
'circuit': [
{'gate': 'x', 'targets': [0]},
Expand All @@ -182,7 +183,7 @@ def _dummy_update(_self):
}

def mock_post(_self, path, *args, **kwargs):
assert path == 'https://api.ionq.co/v0.2/jobs'
assert path == 'https://api.ionq.co/v0.3/jobs'
assert 'json' in kwargs
assert expected_request == kwargs['json']
mock_response = mock.MagicMock()
Expand All @@ -196,20 +197,25 @@ def mock_post(_self, path, *args, **kwargs):
return mock_response

def mock_get(_self, path, *args, **kwargs):
assert path == 'https://api.ionq.co/v0.2/jobs/new-job-id'
mock_response = mock.MagicMock()
mock_response.json = mock.MagicMock(
return_value={
'id': 'new-job-id',
'status': 'completed',
'qubits': 4,
'metadata': {'meas_qubit_ids': '[2, 3]'},
'registers': {'meas_mapped': [2, 3]},
'data': {
'registers': {'meas_mapped': {'2': 1}},
},
}
)
if path == 'https://api.ionq.co/v0.3/jobs/new-job-id':
mock_response = mock.MagicMock()
mock_response.json = mock.MagicMock(
return_value={
'id': 'new-job-id',
'status': 'completed',
'qubits': 4,
'metadata': {'meas_qubit_ids': '[2, 3]'},
'registers': {'meas_mapped': [2, 3]},
'results_url': 'new-job-id/results',
}
)
elif path == 'https://api.ionq.co/v0.3/jobs/new-job-id/results':
mock_response = mock.MagicMock()
mock_response.json = mock.MagicMock(
return_value={'2': 1},
)
else:
raise ValueError(f"Unexpected URL: {path}")
return mock_response

monkeypatch.setattr('requests.sessions.Session.post', mock_post)
Expand Down Expand Up @@ -428,7 +434,7 @@ def _dummy_update(_self):
)

def mock_post(_self, path, **kwargs):
assert path == 'https://api.ionq.co/v0.2/jobs'
assert path == 'https://api.ionq.co/v0.3/jobs'
mock_response = mock.MagicMock()
mock_response.json = mock.MagicMock(return_value=err_data)
return mock_response
Expand Down Expand Up @@ -467,7 +473,7 @@ def _dummy_update(_self):
)

def mock_post(_self, path, *args, **kwargs):
assert path == 'https://api.ionq.co/v0.2/jobs'
assert path == 'https://api.ionq.co/v0.3/jobs'
mock_response = mock.MagicMock()
mock_response.json = mock.MagicMock(
return_value={
Expand All @@ -478,7 +484,7 @@ def mock_post(_self, path, *args, **kwargs):
return mock_response

def mock_get(_self, path, *args, **kwargs):
assert path == 'https://api.ionq.co/v0.2/jobs/new-job-id'
assert path == 'https://api.ionq.co/v0.3/jobs/new-job-id'
mock_response = mock.MagicMock()
mock_response.json = mock.MagicMock(
return_value={
Expand Down Expand Up @@ -525,28 +531,25 @@ def _dummy_update(_self):
'update_devices_list',
_dummy_update.__get__(None, _ionq_http_client.IonQ),
)
request_num = [0]
# request_num = [0]

def mock_get(_self, path, *args, **kwargs):
assert path == 'https://api.ionq.co/v0.2/jobs/old-job-id'
json_response = {
'id': 'old-job-id',
'status': 'running',
}
if request_num[0] > 1:
if path == 'https://api.ionq.co/v0.3/jobs/old-job-id':
json_response = {
'id': 'old-job-id',
'status': 'completed',
'qubits': 4,
'registers': {'meas_mapped': [2, 3]},
'metadata': {'meas_qubit_ids': '[2, 3]'},
'data': {
'registers': {'meas_mapped': {'2': 1}},
},
'results_url': 'old-job-id/results',
}
elif path == 'https://api.ionq.co/v0.3/jobs/old-job-id/results':
json_response = {'2': 1}
else:
raise ValueError(f"Unexpected URL: {path}")

mock_response = mock.MagicMock()
mock_response.json = mock.MagicMock(return_value=json_response)
request_num[0] += 1
return mock_response

monkeypatch.setattr('requests.sessions.Session.get', mock_get)
Expand All @@ -566,12 +569,8 @@ def user_password_input(prompt):

# Code to test:
# Called once per loop in _get_result while the job is not ready.
mock_sleep = mock.MagicMock()
monkeypatch.setattr(_ionq_http_client.time, 'sleep', mock_sleep)
result = _ionq_http_client.retrieve('dummy', token, 'old-job-id')
assert expected == result
# We only sleep twice.
assert 2 == mock_sleep.call_count


def test_retrieve_that_errors_are_caught(monkeypatch):
Expand All @@ -586,7 +585,7 @@ def _dummy_update(_self):
request_num = [0]

def mock_get(_self, path, *args, **kwargs):
assert path == 'https://api.ionq.co/v0.2/jobs/old-job-id'
assert path == 'https://api.ionq.co/v0.3/jobs/old-job-id'
json_response = {
'id': 'old-job-id',
'status': 'running',
Expand Down
Loading
Loading