Skip to content

Commit

Permalink
Updated docs. (#117)
Browse files Browse the repository at this point in the history
* Updated docs.

* Fixes. Linting.

* Auth section fixes.

* Client inside the provider cannot be deleted.

* Avoid leaking unnecessary Qiskit parent class docstrings into the API docs.

* Cleanup, docstrings.

---------

Co-authored-by: Ville Bergholm <ville@meetiqm.com>
  • Loading branch information
smite and Ville Bergholm authored Sep 12, 2024
1 parent 48d4b2f commit 9223485
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 189 deletions.
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.14
=============

* User guide and API documentation updated. `#117 <https://github.com/iqm-finland/qiskit-on-iqm/pull/117>`_

Version 13.13
=============

Expand Down
8 changes: 4 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,16 @@
autodoc_member_order = 'bysource'

# where should signature annotations appear in the docs, function signature or parameter description?
autodoc_typehints = 'description'
autodoc_typehints = 'both'
# autodoc_typehints = 'description' puts the __init__ annotations into its docstring,
# which we thus have to include in the class documentation.
autoclass_content = 'both'
autoclass_content = 'class'

# Sphinx 3.3+: manually clean up type alias rendering in the docs
# autodoc_type_aliases = {'TypeAlias': 'exa.experiment.somemodule.TypeAlias'}

# This is required to make docs build work after the client packages were moved to iqm namespace.
autodoc_mock_imports = ["iqm_client"]
autodoc_mock_imports = []

# -- Autosummary ------------------------------------------------------------

Expand Down Expand Up @@ -146,7 +146,7 @@
'matplotlib': ('https://matplotlib.org/stable', None),
'numpy': ('https://numpy.org/doc/stable', None),
'scipy': ('https://docs.scipy.org/doc/scipy', None),
'qiskit': ('https://qiskit.org/documentation', None),
'qiskit': ('https://docs.quantum.ibm.com/api/qiskit', None),
'iqm_client': ('https://iqm-finland.github.io/iqm-client', None),
}

Expand Down
292 changes: 153 additions & 139 deletions docs/user_guide.rst

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/iqm/qiskit_iqm/examples/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Example code."""
22 changes: 18 additions & 4 deletions src/iqm/qiskit_iqm/iqm_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,27 @@ def target(self) -> Target:
return self._target

def qubit_name_to_index(self, name: str) -> Optional[int]:
"""Given an IQM-style qubit name ('QB1', 'QB2', etc.) return the corresponding index in the register. Returns
None is the given name does not belong to the backend."""
"""Given an IQM-style qubit name, return the corresponding index in the register.
Args:
name: IQM-style qubit name ('QB1', 'QB2', etc.)
Returns:
Index of the given qubit in the quantum register,
or ``None`` if the given qubit is not found on the backend.
"""
return self._qb_to_idx.get(name)

def index_to_qubit_name(self, index: int) -> Optional[str]:
"""Given an index in the backend register return the corresponding IQM-style qubit name ('QB1', 'QB2', etc.).
Returns None if the given index does not correspond to any qubit in the backend."""
"""Given a quantum register index, return the corresponding IQM-style qubit name.
Args:
index: Qubit index in the quantum register.
Returns:
Corresponding IQM-style qubit name ('QB1', 'QB2', etc.), or ``None`` if
the given index does not correspond to any qubit on the backend.
"""
return self._idx_to_qb.get(index)

def validate_compatible_architecture(self, architecture: QuantumArchitectureSpecification) -> bool:
Expand Down
48 changes: 36 additions & 12 deletions src/iqm/qiskit_iqm/iqm_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,11 @@ class IQMJob(JobV1):
"""Implementation of Qiskit's job interface to handle circuit execution on an IQM server.
Args:
backend: the backend instance initiating this job
job_id: string representation of the UUID generated by IQM server
timeout_seconds: maximum time to wait for the job to finish. By default, it is using the IQMClient default.
**kwargs: arguments to be passed to the initializer of the parent class
backend: Backend instance initiating this job.
job_id: String representation of the UUID generated by IQM server.
timeout_seconds: Maximum time to wait for the job to finish. By default, we use the
:class:`~iqm.iqm_client.iqm_client.IQMClient` default.
kwargs: Arguments to be passed to the initializer of the parent class.
"""

def __init__(self, backend: IQMBackend, job_id: str, timeout_seconds: Optional[float] = None, **kwargs):
Expand All @@ -61,7 +62,15 @@ def __init__(self, backend: IQMBackend, job_id: str, timeout_seconds: Optional[f
self._timeout_seconds: float = timeout_seconds if timeout_seconds is not None else DEFAULT_TIMEOUT_SECONDS

def _format_iqm_results(self, iqm_result: RunResult) -> list[tuple[str, list[str]]]:
"""Convert the measurement results from a batch of circuits into the Qiskit format."""
"""Convert the measurement results for a batch of circuits into the Qiskit format.
Args:
iqm_result: measurement results for the circuit batch
Returns:
A list of (circuit_name, measurements) tuples, one tuple for each circuit in the batch.
The measurements are a list of bitstrings, one per shot, representing the state of the classical
registers after the shot.
"""
if iqm_result.measurements is None:
raise ValueError(
f'Cannot format IQM result without measurements. Job status is "{iqm_result.status.value.upper()}"'
Expand All @@ -80,12 +89,24 @@ def _format_iqm_results(self, iqm_result: RunResult) -> list[tuple[str, list[str
def _format_measurement_results(
measurement_results: CircuitMeasurementResults, requested_shots: int, expect_exact_shots: bool = True
) -> list[str]:
"""Convert the measurement results from a circuit into the Qiskit format."""
"""Convert the measurement results from a circuit into the Qiskit format.
Args:
measurement_results: measurement results for a single circuit
requested_shots: number of shots requested
expect_exact_shots: iff True, we must get exactly as many shots as requested
Returns:
For each shot, a bitstring representing the state of the classical registers after the
shot, in little-endian order.
"""
# Mapping from creg index (in the circuit) to an array with shape (shots, len(creg)) with the results.
formatted_results: dict[int, np.ndarray] = {}
for k, v in measurement_results.items():
# measurement keys encode data about the classical registers in the original Qiskit circuit
mk = MeasurementKey.from_string(k)
res = np.array(v, dtype=int)
if len(v) == 0 and not expect_exact_shots:
shots = len(res)
if shots == 0 and not expect_exact_shots:
warnings.warn(
'Received measurement results containing zero shots. '
'In case you are using non-default heralding mode, this could be because of bad calibration.'
Expand All @@ -98,19 +119,22 @@ def _format_measurement_results(
raise ValueError(f'Measurement result {mk} has the wrong shape {res.shape}, expected (*, 1)')
res = res[:, 0]

shots = len(res)
if expect_exact_shots and shots != requested_shots:
raise ValueError(f'Expected {requested_shots} shots but got {shots} for measurement result {mk}')

# group the measurements into cregs, fill in zeros for unused bits
creg = formatted_results.setdefault(mk.creg_idx, np.zeros((shots, mk.creg_len), dtype=int))
creg[:, mk.clbit_idx] = res

# 1. Loop over the registers in the reverse order they were added to the circuit.
# 2. Within each register the highest index is the most significant, so it goes to the leftmost position.
# TODO If the original circuit has a creg that is not used at all we won't know about it here,
# and thus cannot include it (containing only zeros) in the result strings.

# Number of shots is the same for all measurement keys.
# Qiskit uses the little-endian convention in presenting the result bitstrings
# (both between and within registers), hence the [::-1]
return [
' '.join(''.join(map(str, res[s, ::-1])) for _, res in sorted(formatted_results.items(), reverse=True))
for s in range(len(res))
' '.join(''.join(map(str, res[s, :])) for _, res in sorted(formatted_results.items()))[::-1]
for s in range(shots)
]

def submit(self):
Expand Down
14 changes: 13 additions & 1 deletion src/iqm/qiskit_iqm/iqm_naive_move_pass.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Copyright 2024 Qiskit on IQM developers
"""Naive transpilation for N-star architecture"""
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Naive transpilation for the IQM Star architecture."""

from datetime import datetime
from typing import Optional, Union
Expand Down
71 changes: 43 additions & 28 deletions src/iqm/qiskit_iqm/iqm_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Qiskit Backend Provider for IQM backends.
"""Qiskit backend provider for IQM backends.
"""
from __future__ import annotations

from copy import copy
from importlib.metadata import PackageNotFoundError, version
from functools import reduce
Expand Down Expand Up @@ -80,23 +82,23 @@ def max_circuits(self) -> Optional[int]:
def max_circuits(self, value: Optional[int]) -> None:
self._max_circuits = value

def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) -> IQMJob:
"""Run a quantum circuit or a list of quantum circuits on the IQM quantum computer represented with this class.
def run(
self,
run_input: Union[QuantumCircuit, list[QuantumCircuit]],
*,
timeout_seconds: Optional[float] = None,
**options,
) -> IQMJob:
"""Run a quantum circuit or a list of quantum circuits on the IQM quantum computer represented by this backend.
Args:
run_input (Union[QuantumCircuit, list[QuantumCircuit]]): The circuits to run.
options: A dictionary of options for the run. The following options are supported:
shots (int): Number of repetitions of each circuit, for sampling. Default is 1024.
calibration_set_id (str or UUID): ID of the calibration set to use for the run. Default is None.
circuit_compilation_options (CircuitCompilationOptions): Compilation options for the circuits as
documented in ``iqm-client``.
circuit_callback (Callable): Any callback function that will be called for each circuit before sending
the circuits to the device.
timeout_seconds Optional(float): Optional timeout passed to the :class:`IQMJob` in seconds.
run_input: The circuits to run.
timeout_seconds: Maximum time to wait for the job to finish, in seconds. If ``None``, use
the :class:`~iqm.iqm_client.iqm_client.IQMClient` default.
options: Keyword arguments passed on to :meth:`create_run_request`, and documented there.
Returns:
IQMJob: The Job from which the results can be obtained once the circuits are executed.
Job object from which the results can be obtained once the execution has finished.
"""

timeout_seconds = options.pop('timeout_seconds', None)
Expand All @@ -112,15 +114,31 @@ def create_run_request(self, run_input: Union[QuantumCircuit, list[QuantumCircui
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` without ``timeout_seconds``
run_input: Same as in :meth:`run`.
Keyword Args:
shots (int): Number of repetitions of each circuit, for sampling. Default is 1024.
calibration_set_id (Union[str, UUID, None]): ID of the calibration set to use for the run.
Default is ``None``, which means the IQM server will use the current default
calibration set.
circuit_compilation_options (iqm.iqm_client.models.CircuitCompilationOptions):
Compilation options for the circuits, passed on to :mod:`iqm-client`.
circuit_callback (collections.abc.Callable[[list[QuantumCircuit]], Any]):
Callback function that, if provided, will be called for the circuits before sending
them to the device. This may be useful in situations when you do not have explicit
control over transpilation, but need some information on how it was done. This can
happen, for example, when you use pre-implemented algorithms and experiments in
Qiskit, where the implementation of the said algorithm or experiment takes care of
delivering correctly transpiled circuits to the backend. This callback method gives
you a chance to look into those transpiled circuits, and extract any info you need.
As a side effect, you can also use this callback to modify the transpiled circuits
in-place, just before execution; however, we do not recommend to use it for this
purpose.
Returns:
the created run request object
"""
if self.client is None:
raise RuntimeError('Session to IQM client has been closed.')
created run request object
"""
circuits = [run_input] if isinstance(run_input, QuantumCircuit) else run_input

if len(circuits) == 0:
Expand Down Expand Up @@ -170,11 +188,9 @@ def retrieve_job(self, job_id: str) -> IQMJob:
"""Create and return an IQMJob instance associated with this backend with given job id."""
return IQMJob(self, job_id)

def close_client(self):
"""Close IQMClient's session with the authentication server. Discard the client."""
if self.client is not None:
self.client.close_auth_session()
self.client = None
def close_client(self) -> None:
"""Close IQMClient's session with the authentication server."""
self.client.close_auth_session()

def serialize_circuit(self, circuit: QuantumCircuit) -> Circuit:
"""Serialize a quantum circuit into the IQM data transfer format.
Expand Down Expand Up @@ -274,10 +290,9 @@ def __init__(self, client: IQMClient, **kwargs):
raise ValueError('Quantum architecture of the remote quantum computer does not match Adonis.')

super().__init__(client, **kwargs)
self.client = client
self.name = f'IQMFacade{target_architecture.name}Backend'

def _validate_no_empty_cregs(self, circuit):
def _validate_no_empty_cregs(self, circuit: QuantumCircuit) -> bool:
"""Returns True if given circuit has no empty (unused) classical registers, False otherwise."""
cregs_utilization = dict.fromkeys(circuit.cregs, 0)
used_cregs = [circuit.find_bit(i.clbits[0]).registers[0][0] for i in circuit.data if len(i.clbits) > 0]
Expand Down Expand Up @@ -323,7 +338,7 @@ def __init__(self, url: str, **user_auth_args): # contains keyword args auth_se
self.url = url
self.user_auth_args = user_auth_args

def get_backend(self, name=None) -> Union[IQMBackend, IQMFacadeBackend]:
def get_backend(self, name: Optional[str] = None) -> Union[IQMBackend, IQMFacadeBackend]:
"""An IQMBackend instance associated with this provider.
Args:
Expand Down
2 changes: 1 addition & 1 deletion src/iqm/qiskit_iqm/move_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Move gate to be used with Qiskit Quantum Circuits."""
"""MOVE gate to be used on the IQM Star architecture."""

from qiskit.circuit import Gate
from qiskit.circuit.quantumcircuit import QuantumCircuit, QuantumRegister
Expand Down

0 comments on commit 9223485

Please sign in to comment.