Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions cirq-ionq/cirq_ionq/ionq_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ def __init__(
), f'Target can only be one of {self.SUPPORTED_TARGETS} but was {default_target}.'
assert max_retry_seconds >= 0, 'Negative retry not possible without time machine.'

self.url = f'{url.scheme}://{url.netloc}/{api_version}'
self.url_base = f'{url.scheme}://{url.netloc}'
self.url = f'{self.url_base}/{api_version}'
self.headers = self.api_headers(api_key)
self.default_target = default_target
self.max_retry_seconds = max_retry_seconds
Expand Down Expand Up @@ -220,7 +221,7 @@ def get_results(
extra_query_params: Specify any parameters to include in the request.

Returns:
extra_query_paramsresponse as a dict.
response as a dict.

Raises:
IonQNotFoundException: If job or results don't exist.
Expand Down Expand Up @@ -251,6 +252,24 @@ def request():

return self._make_request(request, {}).json()

def get_shots(self, shots_url):
"""Get job shotwise output from IonQ API.

Args:
shots_url: The shots URL as returned by the IonQ API.

Returns:
response as a dict.

Raises:
IonQException: For other API call failures.
"""

def request():
return requests.get(f"{self.url_base}/{shots_url}", headers=self.headers)

return self._make_request(request, {}).json()

def list_jobs(
self, status: str | None = None, limit: int = 100, batch_size: int = 1000
) -> list[dict[str, Any]]:
Expand Down
17 changes: 17 additions & 0 deletions cirq-ionq/cirq_ionq/ionq_client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,23 @@ def test_ionq_client_get_job_retry(mock_get):
assert mock_get.call_count == 2


@mock.patch('requests.get')
def test_ionq_client_get_shots(mock_get):
mock_get.return_value.ok = True
mock_get.return_value.json.return_value = {'foo': 'bar'}
client = ionq.ionq_client._IonQClient(remote_host='http://example.com', api_key='to_my_heart')
client.batch_mode = False
response = client.get_shots(shots_url="v0.4/results/shots/")
assert response == {'foo': 'bar'}

expected_headers = {
'Authorization': 'apiKey to_my_heart',
'Content-Type': 'application/json',
'User-Agent': client._user_agent(),
}
mock_get.assert_called_with('http://example.com/v0.4/results/shots/', headers=expected_headers)


@mock.patch('requests.get')
def test_ionq_client_get_results(mock_get):
mock_get.return_value.ok = True
Expand Down
14 changes: 14 additions & 0 deletions cirq-ionq/cirq_ionq/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,18 @@ def results(
f'Job was not completed successfully. Instead had status: {self.status()}'
)

shotwise_results = None
retrieve_shotwise_result = self.target().startswith('qpu') or (
"noise" in self._job
and "model" in self._job["noise"]
and self._job["noise"]["model"] != "ideal"
)
if retrieve_shotwise_result:
try:
shotwise_results = self._client.get_shots(self._job["results"]["shots"]["url"])
except:
pass

backend_results = self._client.get_results(
job_id=self.job_id(), sharpen=sharpen, extra_query_params=extra_query_params
)
Expand Down Expand Up @@ -267,6 +279,7 @@ def results(
counts=counts,
num_qubits=self.num_qubits(circuit_index),
measurement_dict=self.measurement_dict(circuit_index=circuit_index),
shotwise_results=shotwise_results,
)
)
return big_endian_results_qpu
Expand All @@ -283,6 +296,7 @@ def results(
num_qubits=self.num_qubits(circuit_index),
measurement_dict=self.measurement_dict(circuit_index=circuit_index),
repetitions=self.repetitions(),
shotwise_results=shotwise_results,
)
)
return big_endian_results_sim
Expand Down
66 changes: 66 additions & 0 deletions cirq-ionq/cirq_ionq/job_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import warnings
from unittest import mock

from numpy import array
import pytest

import cirq_ionq as ionq
Expand Down Expand Up @@ -456,3 +457,68 @@ def test_job_fields_update_status():
assert job.name() == 'bacon'
assert job.num_qubits() == 5
assert job.repetitions() == 1000


def test_shotwise_job_results_ideal_simulator():
mock_client = mock.MagicMock()
mock_client.get_shots.return_value = [1, 1, 1, 1, 1]
mock_client.get_results.return_value = {'0': '1'}
job_dict = {
'id': 'my_id',
'status': 'completed',
'stats': {'qubits': '2'},
'backend': 'simulator',
'metadata': {
'shots': '5',
'measurements': json.dumps([{'measurement0': f'results{chr(31)}0,1'}]),
},
'results': {'shots': {'url': 'http://fake.url/shots'}},
"noise": {"model": "ideal"},
}
job = ionq.Job(mock_client, job_dict)
results = job.results()
cirq_result = results[0].to_cirq_result()
assert cirq_result.measurements["results"].tolist() == [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]]


def test_shotwise_job_results_noisy_simulator():
mock_client = mock.MagicMock()
mock_client.get_results.return_value = {'0': '0.6', '1': '0.4'}
mock_client.get_shots.return_value = [2, 1, 3, 1, 0]
job_dict = {
'id': 'my_id',
'status': 'completed',
'stats': {'qubits': '2'},
'backend': 'simulator',
'metadata': {
'shots': '5',
'measurements': json.dumps([{'measurement0': f'results{chr(31)}0,1'}]),
},
'results': {'shots': {'url': 'http://fake.url/shots'}},
"noise": {"model": "aria-1"},
}
job = ionq.Job(mock_client, job_dict)
results = job.results()
cirq_result = results[0].to_cirq_result()
assert cirq_result.measurements["results"].tolist() == [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]]


def test_job_results_qpu():
mock_client = mock.MagicMock()
mock_client.get_results.return_value = {'0': '0.6', '3': '0.4'}
mock_client.get_shots.return_value = [2, 1, 3, 1, 0]
job_dict = {
'id': 'my_id',
'status': 'completed',
'stats': {'qubits': '2'},
'backend': 'qpu',
'metadata': {
'shots': 5,
'measurements': json.dumps([{'measurement0': f'results{chr(31)}0,1'}]),
},
'results': {'shots': {'url': 'http://fake.url/shots'}},
}
job = ionq.Job(mock_client, job_dict)
results = job.results()
cirq_result = results[0].to_cirq_result()
assert cirq_result.measurements["results"].tolist() == [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]]
85 changes: 61 additions & 24 deletions cirq-ionq/cirq_ionq/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,19 @@ class QPUResult:
"""The results of running on an IonQ QPU."""

def __init__(
self, counts: dict[int, int], num_qubits: int, measurement_dict: dict[str, Sequence[int]]
self,
counts: dict[int, int],
num_qubits: int,
measurement_dict: dict[str, Sequence[int]],
shotwise_results: list[int] | None = None,
):
# We require a consistent ordering, and here we use bitvector as such.
# OrderedDict can be removed in python 3.7, where it is part of the contract.
self._counts = collections.OrderedDict(sorted(counts.items()))
self._num_qubits = num_qubits
self._measurement_dict = measurement_dict
self._repetitions = sum(self._counts.values())
self._shotwise_results = shotwise_results

def num_qubits(self) -> int:
"""Returns the number of qubits the circuit was run on."""
Expand All @@ -44,6 +49,10 @@ def repetitions(self) -> int:
"""Returns the number of times the circuit was run."""
return self._repetitions

def shotwise_results(self) -> list[int] | None:
"""Returns the shotwise results if available, otherwise None."""
return self._shotwise_results

def ordered_results(self, key: str | None = None) -> list[int]:
"""Returns a list of arbitrarily but consistently ordered results as big endian ints.

Expand Down Expand Up @@ -134,12 +143,24 @@ def to_cirq_result(self, params: cirq.ParamResolver | None = None) -> cirq.Resul
'Can convert to cirq results only if the circuit had measurement gates '
'with measurement keys.'
)

measurements = {}
for key, targets in self.measurement_dict().items():
qpu_results = self.ordered_results(key)
measurements[key] = np.array(
list(cirq.big_endian_int_to_bits(x, bit_count=len(targets)) for x in qpu_results)
)
if self.shotwise_results() is not None:
for key, targets in self.measurement_dict().items():
bits = [
list(cirq.big_endian_int_to_bits(int(x), bit_count=len(targets)))[::-1]
for x in self.shotwise_results()
]
measurements[key] = np.array(bits)
else:
for key, targets in self.measurement_dict().items():
qpu_results = self.ordered_results(key)
measurements[key] = np.array(
list(
cirq.big_endian_int_to_bits(x, bit_count=len(targets)) for x in qpu_results
)
)

return cirq.ResultDict(params=params or cirq.ParamResolver({}), measurements=measurements)

def __eq__(self, other):
Expand Down Expand Up @@ -169,11 +190,13 @@ def __init__(
num_qubits: int,
measurement_dict: dict[str, Sequence[int]],
repetitions: int,
shotwise_results: list[int] | None = None,
):
self._probabilities = probabilities
self._num_qubits = num_qubits
self._measurement_dict = measurement_dict
self._repetitions = repetitions
self._shotwise_results = shotwise_results

def num_qubits(self) -> int:
"""Returns the number of qubits the circuit was run on."""
Expand All @@ -187,6 +210,10 @@ def repetitions(self) -> int:
"""
return self._repetitions

def shotwise_results(self) -> list[int] | None:
"""Returns the shotwise results if available, otherwise None."""
return self._shotwise_results

def probabilities(self, key: str | None = None) -> dict[int, float]:
"""Returns the probabilities of the measurement results.

Expand Down Expand Up @@ -264,26 +291,36 @@ def to_cirq_result(
'Can convert to cirq results only if the circuit had measurement gates '
'with measurement keys.'
)
rand = cirq.value.parse_random_state(seed)
measurements = {}
values, weights = zip(*list(self.probabilities().items()))

# normalize weights to sum to 1 if within tolerance because
# IonQ's pauliexp gates results are not extremely precise
total = sum(weights)
if np.isclose(total, 1.0, rtol=0, atol=1e-5):
weights = tuple((w / total for w in weights))
measurements = {}

indices = rand.choice(
range(len(values)), p=weights, size=override_repetitions or self.repetitions()
)
rand_values = np.array(values)[indices]
for key, targets in self.measurement_dict().items():
bits = [
[(value >> (self.num_qubits() - target - 1)) & 1 for target in targets]
for value in rand_values
]
measurements[key] = np.array(bits)
if self.shotwise_results() is not None:
for key, targets in self.measurement_dict().items():
# why do we need to reverse here? In QpuResult we don't do that ..
bits = [
list(cirq.big_endian_int_to_bits(int(x), bit_count=len(targets)))[::-1]
for x in self.shotwise_results()
]
measurements[key] = np.array(bits)
else:
rand = cirq.value.parse_random_state(seed)
values, weights = zip(*list(self.probabilities().items()))
# normalize weights to sum to 1 if within tolerance because
# IonQ's pauliexp gates results are not extremely precise
total = sum(weights)
if np.isclose(total, 1.0, rtol=0, atol=1e-5):
weights = tuple((w / total for w in weights))

indices = rand.choice(
range(len(values)), p=weights, size=override_repetitions or self.repetitions()
)
rand_values = np.array(values)[indices]
for key, targets in self.measurement_dict().items():
bits = [
[(value >> (self.num_qubits() - target - 1)) & 1 for target in targets]
for value in rand_values
]
measurements[key] = np.array(bits)
return cirq.ResultDict(params=params or cirq.ParamResolver({}), measurements=measurements)

def __eq__(self, other):
Expand Down
12 changes: 10 additions & 2 deletions cirq-ionq/cirq_ionq/results_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@


def test_qpu_result_fields():
result = ionq.QPUResult({0: 10, 1: 10}, num_qubits=1, measurement_dict={'a': [0]})
result = ionq.QPUResult(
{0: 10, 1: 10}, num_qubits=1, measurement_dict={'a': [0]}, shotwise_results=[1, 2, 3]
)
assert result.counts() == {0: 10, 1: 10}
assert result.repetitions() == 20
assert result.num_qubits() == 1
assert result.measurement_dict() == {'a': [0]}
assert result.shotwise_results() == [1, 2, 3]


def test_qpu_result_str():
Expand Down Expand Up @@ -160,12 +163,17 @@ def test_ordered_results_invalid_key():

def test_simulator_result_fields():
result = ionq.SimulatorResult(
{0: 0.4, 1: 0.6}, num_qubits=1, measurement_dict={'a': [0]}, repetitions=100
{0: 0.4, 1: 0.6},
num_qubits=1,
measurement_dict={'a': [0]},
repetitions=100,
shotwise_results=[1, 2, 3],
)
assert result.probabilities() == {0: 0.4, 1: 0.6}
assert result.num_qubits() == 1
assert result.measurement_dict() == {'a': [0]}
assert result.repetitions() == 100
assert result.shotwise_results() == [1, 2, 3]


def test_simulator_result_str():
Expand Down