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

Allow inspecting a RunRequest without submitting it for execution #115

Merged
merged 4 commits into from
Aug 26, 2024
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
Changelog
=========

Version 13.10
=============

* Allow inspecting a run request before submitting it for execution. `#115 <https://github.com/iqm-finland/qiskit-on-iqm/pull/115>`_
* Require ``iqm-client >= 17.8``. `#115 <https://github.com/iqm-finland/qiskit-on-iqm/pull/115>`_

Version 13.9
============
Expand Down
29 changes: 26 additions & 3 deletions docs/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ quantum computer, and use Qiskit's ``execute`` function as usual:
.. note::

Qiskit's :meth:`qiskit.execute` method performs transpilation by default. If you want to inspect the transpiled
circuits, refer to `circuit_callback` option in the execution options table below. If you want to execute circuits
without automatic transpilation, you can use the :meth:`.IQMBackend.run` method directly; in that case you have to
take care of transpilation yourself. Method :func:`transpile_to_IQM` can be used to transpile circuits.
circuits, refer to `circuit_callback` option in the execution options table below. See also
`Inspecting the final circuits before submitting them for execution`_ for inspecting the actual run request sent for
execution. If you want to execute circuits without automatic transpilation, you can use the :meth:`.IQMBackend
.run` method directly; in that case you have to take care of transpilation yourself. Method
:func:`transpile_to_IQM` can be used to transpile circuits.

You can optionally set IQM backend specific options as additional keyword arguments to the ``execute`` method (which
passes the values down to :meth:`.IQMBackend.run`). For example, if you know an ID of a specific calibration set that
Expand Down Expand Up @@ -424,6 +426,27 @@ connectivity, and the native gateset should match the 5-qubit Adonis architectur
:meth:`.IQMFacadeBackend.run` checks for the presence of unused classical registers, and fails with an error if there
are any.

Inspecting the final circuits before submitting them for execution
------------------------------------------------------------------

It is possible to inspect the final circuits that would be submitted for execution before actually submitting them,
which can be useful for debugging purposes. This can be done using :meth:`.IQMBackend.create_run_request`, which returns
a :class:`~iqm.iqm_client.models.RunRequest` containing the circuits and other data. The method accepts the same
parameters as :meth:`.IQMBackend.run`.

.. code-block:: python

# inspect the run_request without submitting it for execution
run_request = backend.create_run_request(circuit, shots=10)
print(run_request)

# the following two calls submit exactly the same run request for execution on the server
backend.run(circuit, shots=10)
backend.client.submit_run_request(run_request)

It is also possible to print a run request when it is actually submitted by setting the environment variable
``IQM_CLIENT_DEBUG=1``.

More advanced examples
----------------------

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies = [
"numpy",
"qiskit >= 0.45.1, < 0.46",
"qiskit-aer >= 0.13.1, < 0.14",
"iqm-client >= 17.6, < 18.0"
"iqm-client >= 17.8, < 18.0"
]

[project.urls]
Expand Down
26 changes: 21 additions & 5 deletions src/iqm/qiskit_iqm/iqm_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from qiskit import QuantumCircuit
from qiskit.providers import JobStatus, JobV1, Options

from iqm.iqm_client import Circuit, HeraldingMode, Instruction, IQMClient
from iqm.iqm_client import Circuit, HeraldingMode, Instruction, IQMClient, RunRequest
from iqm.iqm_client.util import to_json_dict
from iqm.qiskit_iqm.fake_backends import IQMFakeAdonis
from iqm.qiskit_iqm.iqm_backend import IQMBackendBase
Expand Down Expand Up @@ -82,6 +82,24 @@ def max_circuits(self, value: Optional[int]) -> None:
self._max_circuits = value

def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) -> IQMJob:
run_request = self.create_run_request(run_input, **options)
job_id = self.client.submit_run_request(run_request)
job = IQMJob(self, str(job_id), shots=run_request.shots)
job.circuit_metadata = [c.metadata for c in run_request.circuits]
return job

def create_run_request(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) -> RunRequest:
"""Creates a run request without submitting it for execution.

This can be used to check what would be submitted for execution by an equivalent call to :meth:`run`.

Args:
run_input: same as ``run_input`` for :meth:`run`
options: same as ``options`` for :meth:`run`

Returns:
the created run request object
"""
if self.client is None:
raise RuntimeError('Session to IQM client has been closed.')

Expand Down Expand Up @@ -113,17 +131,15 @@ def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options)
lambda qubits, circuit: qubits.union(set(int(q) for q in circuit.all_qubits())), circuits_serialized, set()
)
qubit_mapping = {str(idx): qb for idx, qb in self._idx_to_qb.items() if idx in used_indices}
job_id = self.client.submit_circuits(

return self.client.create_run_request(
circuits_serialized,
qubit_mapping=qubit_mapping,
calibration_set_id=calibration_set_id if calibration_set_id else None,
shots=shots,
max_circuit_duration_over_t2=max_circuit_duration_over_t2,
heralding_mode=heralding_mode,
)
job = IQMJob(self, str(job_id), shots=shots)
job.circuit_metadata = [c.metadata for c in circuits]
return job

def retrieve_job(self, job_id: str) -> IQMJob:
"""Create and return an IQMJob instance associated with this backend with given job id."""
Expand Down
128 changes: 89 additions & 39 deletions tests/test_iqm_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import re
import uuid

from mockito import ANY, matchers, mock, patch, when
from mockito import ANY, expect, matchers, mock, patch, unstub, verifyNoUnwantedInteractions, when
import numpy as np
import pytest
from qiskit import QuantumCircuit, execute
Expand All @@ -28,7 +28,7 @@
from qiskit.compiler import transpile
import requests

from iqm.iqm_client import HeraldingMode, IQMClient, QuantumArchitecture, RunResult, RunStatus
from iqm.iqm_client import HeraldingMode, IQMClient, QuantumArchitecture, RunRequest, RunResult, RunStatus
from iqm.qiskit_iqm.iqm_provider import IQMBackend, IQMFacadeBackend, IQMJob, IQMProvider
from tests.utils import get_mock_ok_response

Expand All @@ -53,7 +53,7 @@ def circuit_2() -> QuantumCircuit:


@pytest.fixture
def submit_circuits_default_kwargs() -> dict:
def create_run_request_default_kwargs() -> dict:
return {
'qubit_mapping': None,
'calibration_set_id': None,
Expand All @@ -68,6 +68,14 @@ def job_id():
return uuid.uuid4()


@pytest.fixture
def run_request():
run_request = mock(RunRequest)
run_request.circuits = []
run_request.shots = 1
return run_request


def test_default_options(backend):
assert backend.options.shots == 1024
assert backend.options.calibration_set_id is None
Expand Down Expand Up @@ -250,16 +258,16 @@ def test_transpile(backend, circuit):
assert ((idx1, idx2) in cmap) or ((idx2, idx1) in cmap)


def test_run_non_native_circuit_with_the_execute_function(backend, circuit):
def test_run_non_native_circuit_with_the_execute_function(backend, circuit, job_id, run_request):
circuit.h(0)
circuit.cx(0, 1)
circuit.cx(0, 2)

some_id = uuid.uuid4()
backend.client.submit_circuits = lambda *args, **kwargs: some_id
when(backend.client).create_run_request(...).thenReturn(run_request)
when(backend.client).submit_run_request(run_request).thenReturn(job_id)
job = execute(circuit, backend=backend, optimization_level=0)
assert isinstance(job, IQMJob)
assert job.job_id() == str(some_id)
assert job.job_id() == str(job_id)


def test_run_gets_options_from_execute_function(backend, circuit):
Expand All @@ -280,11 +288,12 @@ def run_mock(qc, **kwargs):
)


def test_run_single_circuit(backend, circuit, submit_circuits_default_kwargs, job_id):
def test_run_single_circuit(backend, circuit, create_run_request_default_kwargs, job_id, run_request):
circuit.measure(0, 0)
circuit_ser = backend.serialize_circuit(circuit)
kwargs = submit_circuits_default_kwargs | {'qubit_mapping': {'0': 'QB1'}}
when(backend.client).submit_circuits([circuit_ser], **kwargs).thenReturn(job_id)
kwargs = create_run_request_default_kwargs | {'qubit_mapping': {'0': 'QB1'}}
when(backend.client).create_run_request([circuit_ser], **kwargs).thenReturn(run_request)
when(backend.client).submit_run_request(run_request).thenReturn(job_id)
job = backend.run(circuit)
assert isinstance(job, IQMJob)
assert job.job_id() == str(job_id)
Expand All @@ -295,73 +304,85 @@ def test_run_single_circuit(backend, circuit, submit_circuits_default_kwargs, jo
assert job.job_id() == str(job_id)


def test_run_sets_circuit_metadata_to_the_job(backend):
def test_run_sets_circuit_metadata_to_the_job(backend, run_request, job_id):
circuit_1 = QuantumCircuit(3)
circuit_1.cz(0, 1)
circuit_1.metadata = {'key1': 'value1', 'key2': 'value2'}
circuit_2 = QuantumCircuit(3)
circuit_2.cz(0, 1)
circuit_2.metadata = {'key1': 'value2', 'key2': 'value1'}
some_id = uuid.uuid4()
backend.client.submit_circuits = lambda *args, **kwargs: some_id
run_request.circuits = [backend.serialize_circuit(c) for c in [circuit_1, circuit_2]]
when(backend.client).create_run_request(...).thenReturn(run_request)
when(backend.client).submit_run_request(run_request).thenReturn(job_id)
job = backend.run([circuit_1, circuit_2], shots=10)
assert isinstance(job, IQMJob)
assert job.job_id() == str(some_id)
assert job.job_id() == str(job_id)
assert job.circuit_metadata == [circuit_1.metadata, circuit_2.metadata]


@pytest.mark.parametrize('shots', [13, 978, 1137])
def test_run_with_custom_number_of_shots(backend, circuit, submit_circuits_default_kwargs, job_id, shots):
def test_run_with_custom_number_of_shots(
backend, circuit, create_run_request_default_kwargs, job_id, shots, run_request
):
# pylint: disable=too-many-arguments
circuit.measure(0, 0)
kwargs = submit_circuits_default_kwargs | {'shots': shots, 'qubit_mapping': {'0': 'QB1'}}
when(backend.client).submit_circuits(ANY, **kwargs).thenReturn(job_id)
kwargs = create_run_request_default_kwargs | {'shots': shots, 'qubit_mapping': {'0': 'QB1'}}
when(backend.client).create_run_request(ANY, **kwargs).thenReturn(run_request)
when(backend.client).submit_run_request(run_request).thenReturn(job_id)
backend.run(circuit, shots=shots)


@pytest.mark.parametrize(
'calibration_set_id', ['67e77465-d90e-4839-986e-9270f952b743', uuid.UUID('67e77465-d90e-4839-986e-9270f952b743')]
)
def test_run_with_custom_calibration_set_id(
backend, circuit, submit_circuits_default_kwargs, job_id, calibration_set_id
backend, circuit, create_run_request_default_kwargs, job_id, calibration_set_id, run_request
):
# pylint: disable=too-many-arguments
circuit.measure(0, 0)
circuit_ser = backend.serialize_circuit(circuit)
kwargs = submit_circuits_default_kwargs | {
kwargs = create_run_request_default_kwargs | {
'calibration_set_id': uuid.UUID('67e77465-d90e-4839-986e-9270f952b743'),
'qubit_mapping': {'0': 'QB1'},
}
when(backend.client).submit_circuits([circuit_ser], **kwargs).thenReturn(job_id)
when(backend.client).create_run_request([circuit_ser], **kwargs).thenReturn(run_request)
when(backend.client).submit_run_request(run_request).thenReturn(job_id)

backend.run([circuit], calibration_set_id=calibration_set_id)


def test_run_with_duration_check_disabled(backend, circuit, submit_circuits_default_kwargs, job_id):
def test_run_with_duration_check_disabled(backend, circuit, create_run_request_default_kwargs, job_id, run_request):
circuit.measure(0, 0)
circuit_ser = backend.serialize_circuit(circuit)
kwargs = submit_circuits_default_kwargs | {'qubit_mapping': {'0': 'QB1'}, 'max_circuit_duration_over_t2': 0.0}
when(backend.client).submit_circuits([circuit_ser], **kwargs).thenReturn(job_id)
kwargs = create_run_request_default_kwargs | {'qubit_mapping': {'0': 'QB1'}, 'max_circuit_duration_over_t2': 0.0}
when(backend.client).create_run_request([circuit_ser], **kwargs).thenReturn(run_request)
when(backend.client).submit_run_request(run_request).thenReturn(job_id)

backend.run([circuit], max_circuit_duration_over_t2=0.0)


def test_run_uses_heralding_mode_none_by_default(backend, circuit, submit_circuits_default_kwargs, job_id):
def test_run_uses_heralding_mode_none_by_default(
backend, circuit, create_run_request_default_kwargs, job_id, run_request
):
circuit.measure(0, 0)
circuit_ser = backend.serialize_circuit(circuit)
kwargs = submit_circuits_default_kwargs | {'heralding_mode': HeraldingMode.NONE, 'qubit_mapping': {'0': 'QB1'}}
when(backend.client).submit_circuits([circuit_ser], **kwargs).thenReturn(job_id)
kwargs = create_run_request_default_kwargs | {'heralding_mode': HeraldingMode.NONE, 'qubit_mapping': {'0': 'QB1'}}
when(backend.client).create_run_request([circuit_ser], **kwargs).thenReturn(run_request)
when(backend.client).submit_run_request(run_request).thenReturn(job_id)
backend.run([circuit])


def test_run_with_heralding_mode_zeros(backend, circuit, submit_circuits_default_kwargs, job_id):
def test_run_with_heralding_mode_zeros(backend, circuit, create_run_request_default_kwargs, job_id, run_request):
circuit.measure(0, 0)
circuit_ser = backend.serialize_circuit(circuit)
kwargs = submit_circuits_default_kwargs | {'heralding_mode': HeraldingMode.ZEROS, 'qubit_mapping': {'0': 'QB1'}}
when(backend.client).submit_circuits([circuit_ser], **kwargs).thenReturn(job_id)
kwargs = create_run_request_default_kwargs | {'heralding_mode': HeraldingMode.ZEROS, 'qubit_mapping': {'0': 'QB1'}}
when(backend.client).create_run_request([circuit_ser], **kwargs).thenReturn(run_request)
when(backend.client).submit_run_request(run_request).thenReturn(job_id)
backend.run([circuit], heralding_mode='zeros')


# mypy: disable-error-code="attr-defined"
def test_run_with_circuit_callback(backend, job_id, submit_circuits_default_kwargs):
def test_run_with_circuit_callback(backend, job_id, create_run_request_default_kwargs, run_request):
qc1 = QuantumCircuit(3)
qc1.measure_all()
qc2 = QuantumCircuit(3)
Expand All @@ -378,29 +399,32 @@ def sample_callback(circuits) -> None:

sample_callback.called = False

kwargs = submit_circuits_default_kwargs | {'qubit_mapping': {'0': 'QB1', '1': 'QB2', '2': 'QB3'}}
when(backend.client).submit_circuits(ANY, **kwargs).thenReturn(job_id)
kwargs = create_run_request_default_kwargs | {'qubit_mapping': {'0': 'QB1', '1': 'QB2', '2': 'QB3'}}
when(backend.client).create_run_request(ANY, **kwargs).thenReturn(run_request)
when(backend.client).submit_run_request(run_request).thenReturn(job_id)
backend.run([qc1, qc2], circuit_callback=sample_callback)
assert sample_callback.called is True


def test_run_with_unknown_option(backend, circuit, job_id):
def test_run_with_unknown_option(backend, circuit, job_id, run_request):
circuit.measure_all()
when(backend.client).submit_circuits(...).thenReturn(job_id)
when(backend.client).create_run_request(...).thenReturn(run_request)
when(backend.client).submit_run_request(run_request).thenReturn(job_id)
with pytest.warns(Warning, match=r'Unknown backend option\(s\)'):
backend.run(circuit, to_option_or_not_to_option=17)


def test_run_batch_of_circuits(backend, circuit, submit_circuits_default_kwargs, job_id):
def test_run_batch_of_circuits(backend, circuit, create_run_request_default_kwargs, job_id, run_request):
theta = Parameter('theta')
theta_range = np.linspace(0, 2 * np.pi, 3)
circuit.cz(0, 1)
circuit.r(theta, 0, 0)
circuit.cz(0, 1)
circuits = [circuit.assign_parameters({theta: t}) for t in theta_range]
circuits_serialized = [backend.serialize_circuit(circuit) for circuit in circuits]
kwargs = submit_circuits_default_kwargs | {'qubit_mapping': {'0': 'QB1', '1': 'QB2'}}
when(backend.client).submit_circuits(circuits_serialized, **kwargs).thenReturn(job_id)
kwargs = create_run_request_default_kwargs | {'qubit_mapping': {'0': 'QB1', '1': 'QB2'}}
when(backend.client).create_run_request(circuits_serialized, **kwargs).thenReturn(run_request)
when(backend.client).submit_run_request(run_request).thenReturn(job_id)

job = backend.run(circuits)
assert isinstance(job, IQMJob)
Expand Down Expand Up @@ -466,7 +490,7 @@ def test_get_facade_backend_raises_error_non_matching_architecture(linear_archit
provider.get_backend('facade_adonis')


def test_facade_backend_raises_error_on_remote_execution_fail(adonis_architecture, circuit_2):
def test_facade_backend_raises_error_on_remote_execution_fail(adonis_architecture, circuit_2, run_request):
url = 'http://some_url'
result = {
'status': 'failed',
Expand All @@ -486,7 +510,8 @@ def test_facade_backend_raises_error_on_remote_execution_fail(adonis_architectur
result_status = {'status': 'failed'}

when(IQMClient).get_quantum_architecture().thenReturn(adonis_architecture)
when(IQMClient).submit_circuits(...).thenReturn(uuid.uuid4())
when(IQMClient).create_run_request(...).thenReturn(run_request)
when(IQMClient).submit_run_request(...).thenReturn(uuid.uuid4())
when(IQMClient).get_run(ANY(uuid.UUID)).thenReturn(RunResult.from_dict(result))
when(IQMClient).get_run_status(ANY(uuid.UUID)).thenReturn(RunStatus.from_dict(result_status))

Expand All @@ -495,3 +520,28 @@ def test_facade_backend_raises_error_on_remote_execution_fail(adonis_architectur

with pytest.raises(RuntimeError, match='Remote execution did not succeed'):
backend.run(circuit_2)


def test_create_run_request(backend, circuit, create_run_request_default_kwargs, run_request):
options = {'optimization_level': 0}

circuit.h(0)
circuit.cx(0, 1)
circuit.cx(0, 2)

circuit_transpiled = transpile(circuit, backend, **options)
circuit_serialized = backend.serialize_circuit(circuit_transpiled)
kwargs = create_run_request_default_kwargs | {'qubit_mapping': {'0': 'QB1', '1': 'QB2', '2': 'QB3'}}

# verifies that backend.create_run_request() and backend.run() call client.create_run_request() with same arguments
expect(backend.client, times=2).create_run_request(
[circuit_serialized],
**kwargs,
).thenReturn(run_request)
when(backend.client).submit_run_request(run_request).thenReturn(uuid.uuid4())

assert backend.create_run_request(circuit_transpiled, **options) == run_request
backend.run(circuit_transpiled, **options)

verifyNoUnwantedInteractions()
unstub()